diff --git a/.agent/plans/qdmi-integration-redesign.md b/.agent/plans/qdmi-integration-redesign.md new file mode 100644 index 0000000000..075256e6c0 --- /dev/null +++ b/.agent/plans/qdmi-integration-redesign.md @@ -0,0 +1,366 @@ +# Reconcile QDMI device management with current main + +This ExecPlan is a living document. The sections `Progress`, +`Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must +be kept up to date as work proceeds. + +This ExecPlan must be maintained in accordance with `.agent/PLANS.md` from the +repository root. + +## Purpose / Big Picture + +MQT Core users need one QDMI object model that supports both the convenient +process-wide device catalog and isolated registries for applications and tests. +After this change, a caller can continue to register and open a device through +the default `mqt.core.qdmi.driver` functions, or it can construct a +`DeviceRegistry` and immutable `DeviceManager` that do not share registrations +with the rest of the process. Each open creates a fresh device session, and +devices, sites, operations, jobs, and child devices retain the state required by +their native QDMI handles. + +The work reconciles pull request #1901 with the QDMI changes already on `main`. +The implementation must preserve typed device-configuration transport, stable +device IDs, runtime superconducting and decision-diagram devices, Slurm, Qiskit, +PennyLane, MLIR compiler targets, job retrieval, queue properties, and custom +operation lists. It must not restore the removed neutral-atom stack, TOML +configuration, or the old QDMI C client and singleton driver. + +## Progress + +- [x] (2026-08-18 10:40Z) Refreshed the local `main` and PR #1901 references, + reviewed the three unresolved discussions and the old CI failures, and + created a fresh reconciliation branch from `origin/main`. +- [x] (2026-08-18 10:40Z) Replaced the historical design assumptions with the + hybrid default-registry and explicit-manager design in this ExecPlan. +- [x] (2026-08-18 13:25Z) Added the public registry, manager, and default + convenience interfaces on top of the configuration behavior from current + `main`. +- [x] (2026-08-18 13:25Z) Replaced the singleton driver and C client + implementation with direct, lifetime-safe device state while preserving + all current object operations. +- [x] (2026-08-18 13:25Z) Updated Python bindings and all QDMI consumers while + preserving lazy default-registry behavior in Qiskit, PennyLane, Slurm, and + MLIR. +- [x] (2026-08-18 13:25Z) Updated migration, API, and release documentation and + regenerated stubs. +- [x] (2026-08-18 13:25Z) Passed the focused and complete native suites, the + Python 3.10 and 3.14 test and minimum-version sessions, focused + integration tests, device-free configuration, stub generation, repository + lint, and diff checks. +- [x] (2026-08-18 14:10Z) Rebased the implementation onto + `3354fdaab1732254d75fb9a212351d97f182e086`, rebuilt the complete release + and non-unity lint configurations, reran all 4,053 native tests and 430 + focused Python integration tests, regenerated stubs with nanobind 2.15, + and passed Clang-Tidy on all changed C++ translation units. +- [x] (2026-08-18 23:30Z) Rebased the reconciliation onto + `b66b1dc9b63ca9f3f0b5de88f22bd19b55e2479f`. Preserved the program + serializer work from #2114 and the calibration-job API from #2148 in the + direct device model. The rebase produced a signed reconciliation commit, + and `git verify-commit` accepted its EDDSA signature. +- [x] (2026-08-18 23:55Z) Passed the focused QDMI build and 132 device, 14 + manager, and 14 registry tests. Passed 265 focused Python QDMI and Qiskit + serializer tests. Stub generation produced no tracked diff. A clean + documentation build passed with Graphviz 15.1. +- [x] (2026-08-19 00:20Z) Passed `qiskit-3.14` against Qiskit 2.6.0.dev0 with + 667 tests passed and three expected skips. Rust 1.97 built the Qiskit + source dependency successfully. +- [x] (2026-08-19 00:35Z) Passed the 228-step release build and all 4,093 CTest + tests; only `ScQDMIJobSpecificationTest.QueryJobId` was skipped. Passed a + device-free reconfigure and build with both bundled QDMI devices disabled, + including all 14 registry tests. Passed the lint preset configuration and + its full 482-step non-unity build. Passed `run-clang-tidy-22` on all 18 + existing changed translation units with warnings as errors. +- [ ] Complete link validation. The link checker found current-main internal C++ + target mismatches and unrelated external DNS, 403, and 404 failures before + it was stopped. +- [ ] Publish the lease-protected update to PR #1901 and reply to its open + discussions after explicit authorization. + +## Surprises & Discoveries + +- Observation: PR #1901 predates more than one hundred commits on `main` and now + conflicts with it. Its two commits contain removed neutral-atom and TOML + behavior while missing later QDMI features. Evidence: the cached PR head is + `4925b38bf532846bccac0f28319a2cab2f372179`, while cached `origin/main` is + `43047338aab5d85dbbd4c6bf88dcac31bb2eac74`. +- Observation: current `main` still implements stable-ID registration through + the singleton `qdmi::Driver`, but its public `Device`, `Site`, `Operation`, + and `Job` wrappers already contain the newer queue, retrieval, custom-value, + and child-device behavior. The direct implementation must start from these + current wrappers rather than the older PR copies. +- Observation: network access recovered during implementation. Refreshed + `origin/main` is `b66b1dc9b63ca9f3f0b5de88f22bd19b55e2479f`. It adds the + nanobind 2.15 update, Qiskit program serializers, calibration jobs, the + PennyLane SpecAudit, CMake 4.4 support, and later Qiskit and MLIR fixes after + the original local base. PR #1901 still points to + `4925b38bf532846bccac0f28319a2cab2f372179` and remains conflicted. +- Observation: the complete native run passed 4,053 tests. The focused Python + QDMI, Qiskit, PennyLane, and MLIR run passed 430 tests. The supported Nox + sessions passed with 580 tests and seven skips on Python 3.10, 623 tests and + three skips on Python 3.14, 474 tests and 22 skips for Python 3.10 minimums, + and 517 tests and 18 skips for Python 3.14 minimums. +- Observation: a Python 3.14 failure after a docs-only build came from a cached + wheel that disabled the superconducting provider. A forced reinstall with the + normal provider set resolved the failure; the full Python 3.14 session then + passed. +- Observation: Graphviz 15.1 now provides `dot`, and the clean HTML + documentation build passes. Rust and Cargo 1.97 are also available. The + Qiskit-main Python 3.14 session passes against Qiskit 2.6.0.dev0 with 667 + tests passed and three expected skips. Link checking remains inconclusive + because it found current-main internal C++ target mismatches and unrelated + external network failures before the run was stopped. +- Observation: the non-unity lint build exposed one transitive include in the + Slurm test that the unity release build hid. The final lint configuration + builds all 617 targets, and Clang-Tidy passes all 18 changed C++ translation + units with warnings treated as errors. +- Observation: after the final rebase, the release build completed 228 steps and + CTest passed 4,093 of 4,093 tests with only + `ScQDMIJobSpecificationTest.QueryJobId` skipped. The device-free build and 14 + registry tests pass with both bundled QDMI device targets disabled. The + non-unity lint build completes all 482 steps, and `run-clang-tidy-22` passes + on all 18 existing changed translation units with warnings as errors. + +## Decision Log + +- Decision: Keep the public name `DeviceManager`; do not rename it to + `DeviceExplorer`. Rationale: opening fresh sessions and isolating bulk-open + failures is management behavior, and the name already appears in PR #1901. + Date/Author: 2026-08-18, user and Codex. +- Decision: A `DeviceManager` is an immutable registry snapshot and is never a + singleton. Rationale: explicit managers must be isolated and safe to use in + tests. Only the compatibility registry is process-wide. Date/Author: + 2026-08-18, user and Codex. +- Decision: Preserve the current names `DeviceDefinition`, + `DeviceSessionConfig`, and `DeviceConfigurationSource`. Rationale: current + C++, Python, Qiskit, Slurm, and compiler code already uses these types; the + former PR's `SessionParameters` name would create needless migration work. + Date/Author: 2026-08-18, Codex. +- Decision: Each manager open creates a fresh session. Replacing a definition + changes future opens, while live devices retain their prior session and + library state. Rationale: registration and runtime lifetime must be separate. + Date/Author: 2026-08-18, user and Codex. +- Decision: Keep the process default API as free C++ functions and Python module + functions backed by a locked default registry. Rationale: current adapters + depend on registrations made after module import, while explicit managers need + stable snapshots. Date/Author: 2026-08-18, user and Codex. +- Decision: Retain `include/mqt-core/qdmi/Client.hpp` as a forwarding include + for the device object model, but remove the public legacy `Session`, `Driver`, + and `MQT::CoreQDMIDriver` target. Rationale: recent object-model includes can + keep compiling without preserving the conflicting runtime architecture. + Date/Author: 2026-08-18, Codex. +- Decision: Private implementation headers use `.hpp`, and new implementation + functions use conventional return-type syntax. Rationale: these changes + directly address the two current inline review discussions. Date/Author: + 2026-08-18, PR review. +- Decision: Preserve the #2114 program-format payload contract in the direct + object model. `qdmi::isBinaryProgramFormat` is public in `Device.hpp`, Python + exposes `is_binary_program_format`, and the Qiskit serializer registry uses + the same classification. Rationale: one classifier prevents the C++ client and + Python serializer selection from drifting. Date/Author: 2026-08-18, upstream + #2114 and Codex. +- Decision: Preserve the #2148 calibration API as two + `Device::submitCalibrationJob` overloads and Python `submit_calibration_job`. + Route normal and calibration submission through one private helper with + optional program bytes and an optional shot count. Rationale: QDMI permits a + calibration job with no program and no shot count, while a batch job needs a + list of job handles that the generic byte-payload API cannot express. + Date/Author: 2026-08-18, upstream #2148 and Codex. + +## Outcomes & Retrospective + +The local implementation now provides one QDMI object model with explicit +registry snapshots and process-default convenience functions. It removes the +duplicate singleton and C-client layers, preserves the configuration system and +later QDMI behavior from `main`, and directly addresses the three open PR +discussions. The code and generated stubs pass the available native, Python, +device-free, repository-lint, non-unity build, and Clang-Tidy checks from the +first reconciliation. After the rebase onto +`b66b1dc9b63ca9f3f0b5de88f22bd19b55e2479f`, the focused native and Python +contract tests, complete release and device-free builds, all 4,093 CTest tests, +the complete non-unity lint build, Clang-Tidy, Qiskit-main, stub generation, and +the HTML documentation build pass. The reconciliation commit is +cryptographically signed and verified. Link checking remains inconclusive +because of current-main internal target mismatches and external failures. Remote +publication and discussion replies require explicit authorization. + +## Context and Orientation + +`include/mqt-core/qdmi/Device.hpp` provides the C++ object wrappers, while +`include/mqt-core/qdmi/Client.hpp` is a compatibility forwarding header. The +private `src/qdmi/DeviceApi.hpp` and `src/qdmi/DeviceState.hpp` own library and +session lifetime. `src/qdmi/DeviceRegistry.cpp` discovers `qdmi.json` files, +CMake-generated manifests, environment configuration, and runtime fallback +definitions without loading device libraries. `bindings/qdmi/qdmi.cpp` binds the +object wrappers into `mqt.core.qdmi` and the registry interfaces into +`mqt.core.qdmi.driver`. + +A device definition is inert metadata: a stable ID, a native library path, a +symbol prefix, and default session configuration. A registry stores those +definitions. A manager takes a value snapshot of a registry and opens fresh +native device sessions. The process default registry is a compatibility layer +used by module-level registration and opening functions. It is not a manager +singleton. + +Native QDMI libraries expose C functions with a configured symbol prefix. +Private `DeviceApi` loads those functions and calls device-wide initialize and +finalize functions. Private `DeviceState` owns one native session. Public +objects share the state so a job or site remains valid after its original +`Device` wrapper or `DeviceManager` is destroyed. + +## Plan of Work + +First, promote the current registry parser into public `qdmi::DeviceRegistry` +without changing source precedence, disabled-ID masking, manifest behavior, +typed configuration, or validation. Add a locked process-default registry and +free registration, ID-listing, and opening functions. Add `qdmi::DeviceManager` +as a by-value snapshot with `open` and `openAll`. + +Second, add private `DeviceApi.hpp` and `DeviceState.hpp`. `DeviceApi` loads the +exact QDMI device symbols and is cached weakly by canonical library path and +prefix. A per-key generation token prevents a replacement library generation +from initializing until the old generation has finalized and unloaded. Use RAII +and `std::shared_ptr` construction without raw custom deletion. Each +`DeviceState` owns one allocated and initialized device session. Public child +objects share that state. An operation must compare every supplied site's state +identity with its own state and throw before calling native code when they do +not match. + +Third, move the current object wrapper API into `qdmi/Device.hpp` and its direct +implementation. Preserve queue position and length, job retrieval by ID, custom +operation lists and values, byte and text programs, result decoding, child +devices, equality, and Python lifetime behavior. Remove the singleton driver and +C client only after the direct tests pass. Keep `Client.hpp` as a forwarding +include and move session configuration to a driver-independent header. + +Fourth, bind `DeviceRegistry`, `DeviceManager`, and `OpenAllResult` in +`mqt.core.qdmi.driver`. Preserve the current `DeviceDefinition` keyword +constructor and `open_device` keyword overrides. A default manager snapshots the +default registry at construction; an explicit manager copies the supplied +registry. `open_all` returns `devices` and `errors` mappings by stable ID and +does not stop after one device fails. Keep Qiskit discovery lazy and keep Slurm, +PennyLane, and compiler factories on the default module functions. + +Finally, update CMake exports, documentation, `CHANGELOG.md`, and +`UPGRADING.md`. Regenerate stubs through Nox. Do not edit generated files by +hand and do not add workflow warning suppressions. + +## Concrete Steps + +Run commands from the repository root. Build the direct implementation and its +focused tests first: + + cmake --preset release + cmake --build --preset release --target \ + mqt-core-qdmi-test mqt-core-qdmi-manager-test \ + mqt-core-qdmi-registry-test + ./build/release/test/qdmi/mqt-core-qdmi-test + ./build/release/test/qdmi/manager/mqt-core-qdmi-manager-test + ./build/release/test/qdmi/registry/mqt-core-qdmi-registry-test + uv run --no-sync pytest test/python/qdmi test/python/plugins/qiskit \ + test/python/plugins/qdmi_pennylane test/python/test_mlir.py + +Then run the complete supported validation: + + cmake --build --preset release + ctest --preset release + uvx nox -s stubs + uvx nox -s tests + uvx nox -s minimums + uvx nox -s tests-3.14 + uvx nox -s minimums-3.14 + uvx nox -s qiskit + uvx nox --non-interactive -s docs + uvx nox -s docs -- -b linkcheck + uvx nox -s lint + git diff --check + git status --short + +Also configure and build once with the bundled decision-diagram and +superconducting devices disabled. The device-independent registry tests must +still configure, build, and pass. + +## Validation and Acceptance + +Registry construction and ID listing must not load native code. Runtime fallback +registration must not override a disabled ID. Explicit registries and managers +must remain isolated from process-default registrations. A manager snapshot must +not change after later registration or replacement. + +Every open must create a fresh session. Compatible live sessions may share one +device-wide library initialization. A new generation must wait for the prior +finalization. Devices and derived objects must remain valid after manager and +parent-wrapper destruction. A site from another session must fail before the +native operation callback runs. `openAll` must return successes and failures by +ID in the same result. + +Existing Qiskit, PennyLane, Slurm, and MLIR tests must demonstrate that later +default registrations remain visible and that exact-ID opening does not load +unrelated devices. Windows CI must compile test fixtures without relying on +class template argument deduction for `std::array`. C++ lint must pass without +the warning exclusions from the old PR. + +Acceptance requires a clean diff against current `main`, regenerated stubs, +passing focused and full local checks, and passing exact-head GitHub checks. Old +PR checks do not count as validation of the rewritten branch. + +## Idempotence and Recovery + +All build and test commands are repeatable. Build output stays under `build/`. +Registry discovery reads configuration but does not mutate it. If a direct +object-model milestone fails, keep the old driver files until the equivalent +focused tests pass; remove them only in the subsequent subtractive step. + +Before any remote update, fetch `origin/main` and PR #1901 again. If `main` +advanced, rebase the fresh local commit series and rerun affected checks. Push +the verified tip to `agent/qdmi-integration-redesign` only with +`--force-with-lease` against the refreshed old PR head. + +## Artifacts and Notes + +PR #1901 currently has one outdated but unresolved ownership discussion and two +current style discussions. The ownership reply must explain that the manager is +an immutable, non-singleton snapshot and that returned objects own their state. +The style replies must point to `.hpp` private headers and conventional return +types. Every public GitHub body must begin with `🤖 *AI text below* 🤖`. + +The old PR failed Windows because a test used `std::array` class template +argument deduction without a portable declaration. It also failed C++ lint on +raw custom deletion, missing includes, and style warnings. The rewrite must use +explicit fixture types, direct includes, and RAII instead of suppressing those +diagnostics. + +## Interfaces and Dependencies + +The final C++ interface must provide: + + qdmi::DeviceRegistry() + qdmi::DeviceRegistry(std::vector) + qdmi::DeviceRegistry::definitions() const + qdmi::DeviceRegistry::deviceIds() const + qdmi::DeviceRegistry::registerDevice(definition, replace = false) + qdmi::DeviceRegistry::registerDeviceIfAbsent(definition) + qdmi::DeviceManager() + qdmi::DeviceManager(qdmi::DeviceRegistry) + qdmi::DeviceManager::definitions() const + qdmi::DeviceManager::deviceIds() const + qdmi::DeviceManager::open(id, overrides = {}) const + qdmi::DeviceManager::openAll(overrides = {}) const + qdmi::registerDevice(definition, replace = false) + qdmi::registerDeviceIfAbsent(definition) + qdmi::registeredDeviceIds() + qdmi::openDevice(id, overrides = {}) + +`OpenAllResult` contains `std::map devices` and +`std::map errors`. Unknown IDs and load or session +errors preserve the current `open_device` exception behavior. `openAll` catches +each standard exception, records its message under that ID, and continues. + +Python exposes `DeviceDefinition`, `DeviceRegistry`, `DeviceManager`, +`OpenAllResult`, `register_device`, `register_device_if_absent`, +`registered_device_ids`, and `open_device` from `mqt.core.qdmi.driver`. Device, +site, operation, job, and custom-property types remain in `mqt.core.qdmi`. + +Revision note (2026-08-18): This plan replaces the historical PR #1901 plan. It +starts from current `main`, selects the hybrid default and explicit-instance +API, removes obsolete neutral-atom and TOML assumptions, and records the open +review and CI requirements. diff --git a/CHANGELOG.md b/CHANGELOG.md index d235b3b030..794794216a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -137,6 +137,9 @@ releases may include breaking changes. ### Changed +- 💥 Replace the singleton QDMI driver and client layers with public device + registries, immutable managers, process-default convenience functions, and + lifetime-safe direct device objects ([#1901]) ([**@burgholzer**]) - 💥 Prune dead and misleading CoreIR APIs, including renaming the non-garbage logical output count to `getNoutputQubits()` and `num_output_qubits` ([#2112]) ([**@simon1hofmann**]) @@ -910,6 +913,7 @@ for previous changelogs._ [#1911]: https://github.com/munich-quantum-toolkit/core/pull/1911 [#1910]: https://github.com/munich-quantum-toolkit/core/pull/1910 [#1904]: https://github.com/munich-quantum-toolkit/core/pull/1904 +[#1901]: https://github.com/munich-quantum-toolkit/core/pull/1901 [#1897]: https://github.com/munich-quantum-toolkit/core/pull/1897 [#1895]: https://github.com/munich-quantum-toolkit/core/pull/1895 [#1887]: https://github.com/munich-quantum-toolkit/core/pull/1887 diff --git a/CMakeLists.txt b/CMakeLists.txt index 5710b627c4..0231d99225 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -68,10 +68,6 @@ option(BUILD_MQT_CORE_QDMI_SC_DEVICE "Build the MQT Core superconducting QDMI de ${PROJECT_IS_TOP_LEVEL}) option(BUILD_MQT_CORE_DOCUMENTATION "Generate documentation artifacts as part of the CMake build" OFF) -if(BUILD_MQT_CORE_TESTS AND (NOT BUILD_MQT_CORE_QDMI_DDSIM_DEVICE OR NOT - BUILD_MQT_CORE_QDMI_SC_DEVICE)) - message(FATAL_ERROR "BUILD_MQT_CORE_TESTS requires all bundled QDMI devices") -endif() if(APPLE AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU") message( FATAL_ERROR diff --git a/UPGRADING.md b/UPGRADING.md index 637a2eb583..7409bc5d95 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -296,14 +296,22 @@ replacements: - `mlir/Compiler/FoMaCAdapter.h` and `MQTCompilerFoMaCAdapter` become `mlir/Compiler/QDMIAdapter.h` and `MQTCompilerQDMIAdapter`. -The class and function names do not change. For example: +MQT Core 4 removes the singleton `qdmi::Driver`, the `qdmi::Session` factory, +the `qdmi/driver/Driver.hpp` header, and the `MQT::CoreQDMIDriver` CMake target. +Use the free process-default functions for the shortest migration: ```cpp -#include "qdmi/Client.hpp" +#include "qdmi/DeviceManager.hpp" -auto device = qdmi::Session::openDevice("mqt.ddsim.default"); +auto device = qdmi::openDevice("mqt.ddsim.default"); ``` +Use `qdmi::DeviceRegistry` and `qdmi::DeviceManager` when code needs an isolated +registry snapshot. Each open creates a fresh session. Replacing a definition +affects future opens but does not change live devices. The `qdmi/Client.hpp` +header remains as a forwarding include for the device object model; new code +should include the specific QDMI headers that it uses. + ### QDMI Qiskit primitive options `QDMISampler` and `QDMIEstimator` no longer accept the MQT-specific `options` @@ -456,7 +464,7 @@ stable device ID followed by an explicit open. In Python, replace `add_dynamic_device_library(library_path, prefix, ...)` with: ```python -from mqt.core.fomac import DeviceDefinition, open_device, register_device +from mqt.core.qdmi.driver import DeviceDefinition, open_device, register_device definition = DeviceDefinition("my.device", library_path, prefix, base_url="https://device.example") register_device(definition) @@ -476,19 +484,18 @@ The equivalent C++ flow is: qdmi::DeviceDefinition definition{.id = "my.device", .library = libraryPath, .prefix = prefix}; -auto& driver = qdmi::Driver::get(); -driver.registerDevice(definition); -auto device = fomac::Session::openDevice("my.device"); +qdmi::registerDevice(definition); +auto device = qdmi::openDevice("my.device"); ``` Registration validates and stores metadata without loading native code. Opening -an unknown or disabled ID fails. `fomac::Session::openDevice` creates a fresh -owned session on every call. `qdmi::Driver::open(id)` retains its cached-device -behavior for client callers. +an unknown or disabled ID fails. `qdmi::openDevice` creates a fresh owned +session on every call. An explicit `qdmi::DeviceManager` opens from an isolated, +immutable snapshot instead of the process-default registry. See the {doc}`QDMI device configuration guide ` for the -versioned JSON and TOML formats, configuration precedence, and relocatable -device manifests. +versioned JSON format, configuration precedence, and relocatable device +manifests. ### FoMaC program payload handling diff --git a/bindings/mlir/register_mlir.cpp b/bindings/mlir/register_mlir.cpp index 4f69584a1e..9e4359b314 100644 --- a/bindings/mlir/register_mlir.cpp +++ b/bindings/mlir/register_mlir.cpp @@ -11,8 +11,9 @@ #include "mlir/Compiler/Programs.h" #include "mlir/Compiler/QDMIAdapter.h" #include "mlir/Compiler/Target.h" -#include "qdmi/Client.hpp" // NOLINT(misc-include-cleaner) -#include "qdmi/driver/SessionConfig.hpp" +#include "qdmi/Device.hpp" +#include "qdmi/DeviceManager.hpp" +#include "qdmi/SessionConfig.hpp" #include "qiskit/Qiskit.h" #include @@ -552,7 +553,7 @@ means every operation is native.)pb"); std::move(deviceConfig), std::move(deviceConfigFile), std::move(custom1), std::move(custom2), std::move(custom3), std::move(custom4), std::move(custom5)); - auto device = qdmi::Session::openDevice(deviceId, overrides); + auto device = qdmi::openDevice(deviceId, overrides); return takeResult(mlir::compilerTargetFromDevice(device)); }, "device_id"_a, nb::kw_only(), "base_url"_a = std::nullopt, diff --git a/bindings/qdmi/qdmi.cpp b/bindings/qdmi/qdmi.cpp index 3ddb8c1172..fe2fd5721e 100644 --- a/bindings/qdmi/qdmi.cpp +++ b/bindings/qdmi/qdmi.cpp @@ -8,9 +8,10 @@ * Licensed under the MIT License */ -#include "qdmi/Client.hpp" -#include "qdmi/driver/Driver.hpp" -#include "qdmi/driver/SessionConfig.hpp" +#include "qdmi/Device.hpp" +#include "qdmi/DeviceManager.hpp" +#include "qdmi/DeviceRegistry.hpp" +#include "qdmi/SessionConfig.hpp" #include #include @@ -674,27 +675,134 @@ when the custom slot is unsupported.)pb"); .def_ro("prefix", &qdmi::DeviceDefinition::prefix, R"pb(Prefix used for the QDMI device interface functions.)pb"); + nb::class_( + driver, "DeviceRegistry", + R"pb(Discover or explicitly register QDMI device definitions.)pb") + .def( + nb::init<>(), + R"pb(Discover definitions from the standard configuration sources.)pb") + .def(nb::init>(), "definitions"_a, + R"pb(Create an isolated registry from explicit definitions.)pb") + .def_prop_ro( + "definitions", + [](const qdmi::DeviceRegistry& self) { return self.definitions(); }, + R"pb(Enabled definitions in stable registration order.)pb") + .def_prop_ro("device_ids", &qdmi::DeviceRegistry::deviceIds, + R"pb(Enabled stable device IDs.)pb") + .def("register_device", &qdmi::DeviceRegistry::registerDevice, + "definition"_a, nb::kw_only(), "replace"_a = false, + R"pb(Register a definition, optionally replacing the same ID.)pb") + .def("register_device_if_absent", + &qdmi::DeviceRegistry::registerDeviceIfAbsent, "definition"_a, + R"pb(Register a fallback unless its ID exists or is disabled.)pb"); + + nb::class_( + driver, "OpenAllResult", + R"pb(Per-ID successes and failures from opening all registered devices.)pb") + .def_ro("devices", &qdmi::OpenAllResult::devices, + R"pb(Successfully opened devices keyed by stable ID.)pb") + .def_ro("errors", &qdmi::OpenAllResult::errors, + R"pb(Error messages for devices that could not be opened.)pb"); + + auto manager = nb::class_( + driver, "DeviceManager", + R"pb(An immutable registry snapshot that opens fresh device sessions.)pb"); + manager + .def(nb::init<>(), + R"pb(Snapshot the current process default registry.)pb") + .def(nb::init(), "registry"_a, + R"pb(Snapshot an explicit registry.)pb") + .def_prop_ro( + "definitions", + [](const qdmi::DeviceManager& self) { return self.definitions(); }, + R"pb(Definitions in this immutable snapshot.)pb") + .def_prop_ro("device_ids", &qdmi::DeviceManager::deviceIds, + R"pb(Stable IDs in this immutable snapshot.)pb") + .def( + "open", + [](const qdmi::DeviceManager& self, const std::string& deviceId, + std::optional baseUrl, + std::optional token, + std::optional authFile, + std::optional authUrl, + std::optional username, + std::optional password, + std::optional deviceConfig, + std::optional deviceConfigFile, + std::optional custom1, + std::optional custom2, + std::optional custom3, + std::optional custom4, + std::optional custom5) { + return self.open( + deviceId, + qdmi::makeDeviceSessionConfig( + std::move(baseUrl), std::move(token), std::move(authFile), + std::move(authUrl), std::move(username), + std::move(password), std::move(deviceConfig), + std::move(deviceConfigFile), std::move(custom1), + std::move(custom2), std::move(custom3), std::move(custom4), + std::move(custom5))); + }, + "device_id"_a, nb::kw_only(), "base_url"_a = std::nullopt, + "token"_a = std::nullopt, "auth_file"_a = std::nullopt, + "auth_url"_a = std::nullopt, "username"_a = std::nullopt, + "password"_a = std::nullopt, "device_config"_a = std::nullopt, + "device_config_file"_a = std::nullopt, "custom1"_a = std::nullopt, + "custom2"_a = std::nullopt, "custom3"_a = std::nullopt, + "custom4"_a = std::nullopt, "custom5"_a = std::nullopt, + R"pb(Open a fresh session for one stable device ID.)pb") + .def( + "open_all", + [](const qdmi::DeviceManager& self, + std::optional baseUrl, + std::optional token, + std::optional authFile, + std::optional authUrl, + std::optional username, + std::optional password, + std::optional deviceConfig, + std::optional deviceConfigFile, + std::optional custom1, + std::optional custom2, + std::optional custom3, + std::optional custom4, + std::optional custom5) { + return self.openAll(qdmi::makeDeviceSessionConfig( + std::move(baseUrl), std::move(token), std::move(authFile), + std::move(authUrl), std::move(username), std::move(password), + std::move(deviceConfig), std::move(deviceConfigFile), + std::move(custom1), std::move(custom2), std::move(custom3), + std::move(custom4), std::move(custom5))); + }, + nb::kw_only(), "base_url"_a = std::nullopt, "token"_a = std::nullopt, + "auth_file"_a = std::nullopt, "auth_url"_a = std::nullopt, + "username"_a = std::nullopt, "password"_a = std::nullopt, + "device_config"_a = std::nullopt, + "device_config_file"_a = std::nullopt, "custom1"_a = std::nullopt, + "custom2"_a = std::nullopt, "custom3"_a = std::nullopt, + "custom4"_a = std::nullopt, "custom5"_a = std::nullopt, + R"pb(Open all devices and isolate failures by stable ID.)pb"); + driver.def( "register_device", [](qdmi::DeviceDefinition definition, const bool replace) { - qdmi::Driver::get().registerDevice(std::move(definition), replace); + qdmi::registerDevice(std::move(definition), replace); }, "definition"_a, nb::kw_only(), "replace"_a = false, R"pb(Register a QDMI device definition without loading its library. Args: definition: Definition to validate and store. - replace: Replace an existing definition if it has not been opened. + replace: Replace an existing definition for future opens. Raises: - ValueError: If the definition is invalid or its ID is already registered. - RuntimeError: If replacing an already opened ID.)pb"); + ValueError: If the definition is invalid or its ID is already registered.)pb"); driver.def( "register_device_if_absent", [](qdmi::DeviceDefinition definition) { - return qdmi::Driver::get().registerDeviceIfAbsent( - std::move(definition)); + return qdmi::registerDeviceIfAbsent(std::move(definition)); }, "definition"_a, R"pb(Register a valid QDMI device definition if its ID is absent. @@ -712,8 +820,7 @@ still raise. ValueError: If the definition is invalid.)pb"); driver.def( - "registered_device_ids", - [] { return qdmi::Driver::get().registeredDeviceIds(); }, + "registered_device_ids", &qdmi::registeredDeviceIds, R"pb(Return registered, enabled QDMI device IDs in registration order. This includes devices registered at runtime and does not load native device @@ -738,7 +845,7 @@ libraries or expose their definitions.)pb"); std::move(deviceConfig), std::move(deviceConfigFile), std::move(custom1), std::move(custom2), std::move(custom3), std::move(custom4), std::move(custom5)); - return qdmi::Session::openDevice(deviceId, overrides); + return qdmi::openDevice(deviceId, overrides); }, "device_id"_a, nb::kw_only(), "base_url"_a = std::nullopt, "token"_a = std::nullopt, "auth_file"_a = std::nullopt, diff --git a/docs/qdmi/configuration.md b/docs/qdmi/configuration.md index b0715daa35..e82a33b99a 100644 --- a/docs/qdmi/configuration.md +++ b/docs/qdmi/configuration.md @@ -1,9 +1,9 @@ # QDMI device configuration MQT Core discovers QDMI device definitions from versioned JSON configuration. -Discovery only parses definitions. When the QDMI driver initializes a client -session, it opens the configured native libraries. The stable-ID API opens only -the requested device. +Discovery only parses definitions. A manager or process-default open loads the +requested native library and creates a fresh client session. Stable-ID opening +does not load unrelated devices. :::{warning} QDMI configuration is a native-code loading trust boundary. Use configuration @@ -63,10 +63,10 @@ or: The inline value must be a JSON object. A relative file path is resolved against the registry file that declares it. The complete source is one merge field: changing from `inline` to `file` at a higher-precedence layer replaces the -inherited inline JSON. The Driver adapts inline JSON to QDMI v1 CUSTOM1 and a -file path to CUSTOM2 when opening the native session. Consequently, -`device-config` cannot be combined with raw `custom1` or `custom2`; CUSTOM3 -through CUSTOM5 remain available to providers. +inherited inline JSON. MQT Core adapts inline JSON to QDMI v1 CUSTOM1 and a file +path to CUSTOM2 when opening the native session. Consequently, `device-config` +cannot be combined with raw `custom1` or `custom2`; CUSTOM3 through CUSTOM5 +remain available to providers. Relative library and authentication-file paths are resolved against the file that declared them. For `MQT_CORE_QDMI_CONFIG_JSON`, they resolve against the @@ -75,13 +75,13 @@ current working directory. Unknown keys, invalid types, duplicate IDs within one source, unsupported schema versions, and incomplete enabled definitions are hard errors. Diagnostics name the source and configuration path. Credentials and session values are not -included in Driver warnings. +included in diagnostic warnings. ## Discovery and precedence Definitions are merged field by field by ID, from lowest to highest precedence: -1. generated `*.qdmi.json` fragments packaged beside the MQT Core Driver; +1. generated `*.qdmi.json` fragments packaged beside MQT Core's QDMI library; 2. the system `qdmi.json`; 3. the user or XDG `qdmi.json`; 4. the nearest project `qdmi.json`; @@ -103,9 +103,9 @@ retaining packaged built-ins. ## Using configured devices -When the QDMI driver initializes a client session, it opens the configured -definitions. A failure to load one definition does not hide the remaining -devices. Stable-ID registration does not initialize device libraries. +Stable-ID registration does not initialize device libraries. Opening one ID does +not initialize other definitions. Use a manager's `open_all()` operation when an +application wants per-ID successes and failures from the full snapshot. ```python from mqt.core.qdmi.driver import open_device, registered_device_ids @@ -156,14 +156,18 @@ enabled stable IDs in deterministic registration order. This includes runtime registrations without loading native device libraries or exposing their paths, prefixes, or session configuration. -The equivalent C++ registration operation is -{cpp-api:func}`qdmi::Driver::registerDevice`. Duplicate IDs are rejected unless -`replace` is true, and an opened definition cannot be replaced. -{cpp-api:func}`qdmi::Driver::registeredDeviceIds` provides the same load-free -enumeration, and {cpp-api:func}`qdmi::Driver::open` returns the cached device. -{cpp-api:func}`qdmi::Session::openDevice` returns a fresh device session and -does not add it to the QDMI client catalog. Runtime registrations and explicit -opens are not added to that catalog. +The equivalent process-default C++ functions are +{cpp-api:func}`qdmi::registerDevice`, {cpp-api:func}`qdmi::registeredDeviceIds`, +and {cpp-api:func}`qdmi::openDevice`. Duplicate IDs are rejected unless +`replace` is true. Replacement affects future opens. Live devices retain the +session and library state created from the prior definition. + +For isolated state, construct {cpp-api:class}`qdmi::DeviceRegistry`, register +definitions on that object, and pass it to {cpp-api:class}`qdmi::DeviceManager`. +A manager takes an immutable snapshot. Changes to the source registry or the +process-default registry do not change that snapshot. `DeviceManager::openAll` +continues after an individual failure and returns successes and error messages +by stable ID. Multiple definitions may refer to the same library and prefix. MQT Core reuses the initialized library while creating a fresh QDMI device session, with its own @@ -216,15 +220,15 @@ availability check, or a provider queue length. Built-in targets generate manifests beside their runtime libraries in both build and install trees. Library paths in those fragments contain only the target filename, so moving an installed tree or Python wheel preserves discovery. -Automatic discovery searches relative to the MQT Core Driver, not every library -loaded by the process. An application using a separately installed device -implementation therefore copies its manifest beside the Driver or registers its -definition by stable ID. +Automatic discovery searches relative to the MQT Core QDMI library, not every +library loaded by the process. An application using a separately installed +device implementation therefore copies its manifest beside that library or +registers its definition by stable ID. A fully static executable has no portable shared-module location. Place the fragments beside the executable, point `MQT_CORE_QDMI_CONFIG_FILE` at a complete -configuration, or use {cpp-api:func}`qdmi::Driver::registerDevice` and -{cpp-api:func}`qdmi::Driver::open`. No install prefix is compiled into the +configuration, or use {cpp-api:func}`qdmi::registerDevice` and +{cpp-api:func}`qdmi::openDevice`. No install prefix is compiled into the manifests. An installed MQT Core CMake package provides a helper that colocates selected diff --git a/docs/qdmi/driver.md b/docs/qdmi/driver.md index 38ee4b22a5..83fd82fe1d 100644 --- a/docs/qdmi/driver.md +++ b/docs/qdmi/driver.md @@ -6,55 +6,54 @@ mystnb: number_source_lines: true --- -# MQT Core's QDMI Driver Implementation +# MQT Core QDMI device management ## Objective -A QDMI Driver manages the communication between QDMI devices, such as -[MQT Core's SC QDMI Device](sc_device.md) or -[MQT Core's DDSIM QDMI Device](ddsim_device.md), and QDMI clients, see the -[QDMI specification](https://munich-quantum-software-stack.github.io/QDMI/). -It is responsible for loading the device, forwarding requests from the client to -the device, and sending back the results. MQT Core's QDMI Driver, -{cpp-api:class}`qdmi::Driver`, comes with several preloaded devices when the -bundled devices are enabled. Other devices can be loaded dynamically at runtime -via {cpp-api:func}`qdmi::Driver::registerDevice` and -{cpp-api:func}`qdmi::Driver::open`. Built-in and external devices can also be -registered through -[versioned QDMI device configuration](configuration.md). - -## Building the Bundled Devices +MQT Core loads QDMI devices, opens client sessions, and provides owning C++ and +Python wrappers for devices, sites, operations, and jobs. A device definition +contains inert metadata: a stable ID, the native library path, the QDMI symbol +prefix, and default session configuration. Discovering or registering a +definition does not load native code. + +{cpp-api:class}`qdmi::DeviceRegistry` stores definitions. +{cpp-api:class}`qdmi::DeviceManager` takes an immutable registry snapshot and +opens fresh device sessions. A manager is not a singleton. A returned device or +derived object owns the session and native-library state that it needs, so it +can outlive its manager and parent wrappers. + +MQT Core also provides process-default functions for applications that need one +shared catalog: {cpp-api:func}`qdmi::registerDevice`, +{cpp-api:func}`qdmi::registeredDeviceIds`, and {cpp-api:func}`qdmi::openDevice`. +These functions are the compatibility and adapter interface. Explicit registries +and managers remain isolated from this process-default catalog. + +## Building the bundled devices Standalone MQT Core builds include the DDSIM and superconducting QDMI device libraries by default. When MQT Core is embedded in another CMake project using {code}`FetchContent` or {code}`add_subdirectory`, these device libraries are disabled by default so the consumer does not build implementations it may not -use. They can be selected independently before making MQT Core available: +use. Select them independently before making MQT Core available: - {code}`BUILD_MQT_CORE_QDMI_DDSIM_DEVICE` - {code}`BUILD_MQT_CORE_QDMI_SC_DEVICE` -For example, an embedded simulator consumer can enable only the DDSIM device, -while CUDA-Q can enable the DDSIM and superconducting devices used by its -integration tests. +The QDMI object model, registry, manager, and runtime configuration work without +the bundled devices. Device-specific integration tests run only for the +implementations enabled in a build. -The QDMI driver and QDMI libraries are available independently. Device-free -builds can register external device libraries through -[QDMI device configuration](configuration.md). Building MQT Core's C++ tests -requires both bundled devices so that the complete device integration is tested. +## Python bindings -## Python Bindings +The Python module exposes owning QDMI entities through {py:mod}`mqt.core.qdmi`. +Its {py:mod}`mqt.core.qdmi.driver` submodule provides +{py:class}`~mqt.core.qdmi.driver.DeviceRegistry`, +{py:class}`~mqt.core.qdmi.driver.DeviceManager`, registration, discovery, and +opening. -The QDMI interface is the low-level contract implemented by a QDMI device. The -MQT Core QDMI driver loads device libraries and implements the QDMI client -interface. The C++ QDMI library adds owning wrappers for QDMI devices, sites, -operations, and jobs. The Python module exposes these QDMI entities through -{py:mod}`mqt.core.qdmi`. Its {py:mod}`mqt.core.qdmi.driver` submodule provides -device discovery, registration, and opening. +## Process-default catalog -## Usage - -The following example opens each registered device by its stable ID. +The shortest form opens each device registered in the process-default catalog: ```{code-cell} ipython3 from mqt.core.qdmi.driver import open_device, registered_device_ids @@ -63,3 +62,34 @@ for device_id in registered_device_ids(): device = open_device(device_id) print(device.name()) ``` + +Each {py:func}`~mqt.core.qdmi.driver.open_device` call creates a fresh session. +Registration replacement changes later opens. It does not change a live device. + +## Isolated registry and manager + +Use an explicit registry when a component must not depend on process-global +registration: + +```python +from mqt.core.qdmi.driver import DeviceDefinition, DeviceManager, DeviceRegistry + +registry = DeviceRegistry([ + DeviceDefinition( + "example.device", + "/path/to/libexample-device.so", + "EXAMPLE", + ) +]) +manager = DeviceManager(registry) +device = manager.open("example.device") +``` + +A manager copies the supplied registry. Later changes to the registry do not +change the manager snapshot. +{py:meth}`~mqt.core.qdmi.driver.DeviceManager.open_all` continues after an +individual open fails and returns successful devices and error messages by +stable ID. + +See the [QDMI device configuration guide](configuration.md) for discovery, +precedence, typed session configuration, and relocatable manifests. diff --git a/include/mqt-core/qdmi/Client.hpp b/include/mqt-core/qdmi/Client.hpp index 4efc99ee2d..2151410e92 100644 --- a/include/mqt-core/qdmi/Client.hpp +++ b/include/mqt-core/qdmi/Client.hpp @@ -9,1255 +9,12 @@ */ /** @file Client.hpp - * @brief QDMI C++ device-management interface. + * @brief Compatibility include for the QDMI C++ device object model. */ #pragma once -#include "qdmi/common/Common.hpp" -#include "qdmi/driver/Driver.hpp" -#include "qdmi/types.h" - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace qdmi { -using CustomJobParameter = std::variant; - -/** - * @brief Identifies one of QDMI's implementation-defined custom slots. - * @details The same selector is used for custom device, site, operation, and - * job properties as well as custom job results. - */ -enum class CustomProperty : std::uint8_t { - Custom1 = 1, - Custom2 = 2, - Custom3 = 3, - Custom4 = 4, - Custom5 = 5, -}; - -/** - * @brief Concept for supported custom property value types. - * @details Raw bytes provide a lossless fallback for implementation-defined - * types that cannot be represented by one of the scalar alternatives. - */ -template -concept custom_property_value = - std::same_as || std::same_as || - std::same_as || std::same_as || - std::same_as>; - -namespace detail { -[[nodiscard]] inline std::optional -queuePositionFromResult(const int result, const size_t queuePosition) { - if (result == QDMI_ERROR_NOTSUPPORTED || result == QDMI_ERROR_BADSTATE) { - return std::nullopt; - } - qdmi::throwIfError(result, "Querying job queue position"); - return queuePosition; -} - -template -[[nodiscard]] std::optional -queryCustomValue(Query query, const std::string_view description) { - size_t size = 0; - const auto sizeResult = query(0, nullptr, &size); - if (sizeResult == QDMI_ERROR_NOTSUPPORTED) { - return std::nullopt; - } - qdmi::throwIfError(sizeResult, - "Querying " + std::string(description) + " size"); - - std::vector bytes(size); - if (size != 0) { - qdmi::throwIfError(query(size, bytes.data(), nullptr), - "Querying " + std::string(description)); - } - - if constexpr (std::same_as>) { - return bytes; - } else if constexpr (std::same_as) { - if (bytes.empty() || bytes.back() != std::byte{0}) { - throw std::invalid_argument("Cannot decode " + std::string(description) + - " as a null-terminated string"); - } - return std::string(reinterpret_cast(bytes.data()), - bytes.size() - 1); - } else { - if (bytes.size() != sizeof(T)) { - throw std::invalid_argument("Cannot decode " + std::string(description) + - ": expected " + std::to_string(sizeof(T)) + - " bytes, but the device reported " + - std::to_string(bytes.size())); - } - T value{}; - std::memcpy(&value, bytes.data(), sizeof(T)); - return value; - } -} - -template -[[nodiscard]] std::optional> -queryHandleArray(Query query, const std::string_view description) { - size_t size = 0; - const auto sizeResult = query(0, nullptr, &size); - if (sizeResult == QDMI_ERROR_NOTSUPPORTED) { - return std::nullopt; - } - qdmi::throwIfError(sizeResult, - "Querying " + std::string(description) + " size"); - if (size % sizeof(Handle) != 0) { - throw std::invalid_argument( - "Cannot decode " + std::string(description) + ": the device reported " + - std::to_string(size) + " bytes, which is not a multiple of " + - std::to_string(sizeof(Handle))); - } - - std::vector handles(size / sizeof(Handle)); - if (size != 0) { - qdmi::throwIfError(query(size, static_cast(handles.data()), nullptr), - "Querying " + std::string(description)); - } - return handles; -} - -[[nodiscard]] constexpr QDMI_Device_Property -toDeviceProperty(const CustomProperty property) { - switch (property) { - case CustomProperty::Custom1: - return QDMI_DEVICE_PROPERTY_CUSTOM1; - case CustomProperty::Custom2: - return QDMI_DEVICE_PROPERTY_CUSTOM2; - case CustomProperty::Custom3: - return QDMI_DEVICE_PROPERTY_CUSTOM3; - case CustomProperty::Custom4: - return QDMI_DEVICE_PROPERTY_CUSTOM4; - case CustomProperty::Custom5: - return QDMI_DEVICE_PROPERTY_CUSTOM5; - } - throw std::invalid_argument("Invalid custom property selector"); -} - -[[nodiscard]] constexpr QDMI_Site_Property -toSiteProperty(const CustomProperty property) { - switch (property) { - case CustomProperty::Custom1: - return QDMI_SITE_PROPERTY_CUSTOM1; - case CustomProperty::Custom2: - return QDMI_SITE_PROPERTY_CUSTOM2; - case CustomProperty::Custom3: - return QDMI_SITE_PROPERTY_CUSTOM3; - case CustomProperty::Custom4: - return QDMI_SITE_PROPERTY_CUSTOM4; - case CustomProperty::Custom5: - return QDMI_SITE_PROPERTY_CUSTOM5; - } - throw std::invalid_argument("Invalid custom property selector"); -} - -[[nodiscard]] constexpr QDMI_Operation_Property -toOperationProperty(const CustomProperty property) { - switch (property) { - case CustomProperty::Custom1: - return QDMI_OPERATION_PROPERTY_CUSTOM1; - case CustomProperty::Custom2: - return QDMI_OPERATION_PROPERTY_CUSTOM2; - case CustomProperty::Custom3: - return QDMI_OPERATION_PROPERTY_CUSTOM3; - case CustomProperty::Custom4: - return QDMI_OPERATION_PROPERTY_CUSTOM4; - case CustomProperty::Custom5: - return QDMI_OPERATION_PROPERTY_CUSTOM5; - } - throw std::invalid_argument("Invalid custom property selector"); -} - -[[nodiscard]] constexpr QDMI_Job_Property -toJobProperty(const CustomProperty property) { - switch (property) { - case CustomProperty::Custom1: - return QDMI_JOB_PROPERTY_CUSTOM1; - case CustomProperty::Custom2: - return QDMI_JOB_PROPERTY_CUSTOM2; - case CustomProperty::Custom3: - return QDMI_JOB_PROPERTY_CUSTOM3; - case CustomProperty::Custom4: - return QDMI_JOB_PROPERTY_CUSTOM4; - case CustomProperty::Custom5: - return QDMI_JOB_PROPERTY_CUSTOM5; - } - throw std::invalid_argument("Invalid custom property selector"); -} - -[[nodiscard]] constexpr QDMI_Job_Result -toJobResult(const CustomProperty property) { - switch (property) { - case CustomProperty::Custom1: - return QDMI_JOB_RESULT_CUSTOM1; - case CustomProperty::Custom2: - return QDMI_JOB_RESULT_CUSTOM2; - case CustomProperty::Custom3: - return QDMI_JOB_RESULT_CUSTOM3; - case CustomProperty::Custom4: - return QDMI_JOB_RESULT_CUSTOM4; - case CustomProperty::Custom5: - return QDMI_JOB_RESULT_CUSTOM5; - } - throw std::invalid_argument("Invalid custom property selector"); -} -} // namespace detail - -/** - * @brief Returns whether a program format carries a binary payload. - * @details `QDMI_PROGRAM_FORMAT_QIRBASEMODULE`, - * `QDMI_PROGRAM_FORMAT_QIRADAPTIVEMODULE`, and `QDMI_PROGRAM_FORMAT_QPY` hold - * bitcode or another serialized object. Such a payload can contain a null byte - * and is not text, so it must be submitted as exact bytes. The string overload - * of `Device::submitJob` rejects these formats. - * @param format The program format to classify. - * @return True if the format requires exact-byte submission. - */ -[[nodiscard]] constexpr bool -isBinaryProgramFormat(const QDMI_Program_Format format) noexcept { - return format == QDMI_PROGRAM_FORMAT_QIRBASEMODULE || - format == QDMI_PROGRAM_FORMAT_QIRADAPTIVEMODULE || - format == QDMI_PROGRAM_FORMAT_QPY; -} - -/** - * @brief Concept for ranges that are contiguous in memory and can be - * constructed with a size. - * @details This concept is used to constrain the template parameter of the - * `queryProperty` method. - * @tparam T The type to check. - */ -template -concept size_constructible_contiguous_range = - std::ranges::contiguous_range && std::constructible_from && - requires { typename T::value_type; } && requires(T t) { - { t.data() } -> std::same_as; - }; -/** - * @brief Concept for types that are either integral, floating point, bool, - * std::string, or QDMI_Device_Status. - * @details This concept is used to constrain the template parameter of the - * `queryProperty` method. - * @tparam T The type to check. - */ -template -concept value_or_string = - std::integral || std::floating_point || std::same_as || - std::same_as || std::same_as; - -/** - * @brief Concept for types that are either value_or_string or - * size_constructible_contiguous_range. - * @details This concept is used to constrain the template parameter of the - * `queryProperty` method. - * @tparam T The type to check. - */ -template -concept value_or_string_or_vector = - value_or_string || size_constructible_contiguous_range; - -/** - * @brief Concept for types that are std::optional of value_or_string. - * @details This concept is used to constrain the template parameter of the - * `queryProperty` method. - * @tparam T The type to check. - */ -template -concept is_optional = requires { typename T::value_type; } && - std::same_as>; - -/** - * @brief Concept for types that are either std::string or std::optional of - * std::string. - * @details This concept is used to constrain the template parameter of the - * `queryProperty` method. - * @tparam T The type to check. - */ -template -concept string_or_optional_string = - std::same_as || - (is_optional && std::same_as); - -/// @see remove_optional_t -template struct remove_optional { - using type = T; -}; - -/// @see remove_optional_t -template struct remove_optional> { - using type = U; -}; - -/** - * @brief Helper type to strip std::optional from a type if it is present. - * @details This is useful for template metaprogramming when you want to work - * with the underlying type of optional without caring about its optionality. - * @tparam T The type to strip optional from. - */ -template using remove_optional_t = remove_optional::type; - -/** - * @brief Concept for types that are either size_constructible_contiguous_range - * or std::optional of size_constructible_contiguous_range. - * @details This concept is used to constrain the template parameter of the - * `queryProperty` method. - * @tparam T The type to check. - * @see Operation::queryProperty - */ -template -concept maybe_optional_size_constructible_contiguous_range = - size_constructible_contiguous_range>; - -/** - * @brief Concept for types that are either value_or_string or std::optional of - * value_or_string. - * @details This concept is used to constrain the template parameter of the - * `queryProperty` method. - * @tparam T The type to check. - * @see Site::queryProperty - */ -template -concept maybe_optional_value_or_string = value_or_string>; - -/** - * @brief Concept for types that are either value_or_string_or_vector or - * std::optional of value_or_string_or_vector. - * @details This concept is used to constrain the template parameter of the - * `queryProperty` method. - * @tparam T The type to check. - * @see Operation::queryProperty - */ -template -concept maybe_optional_value_or_string_or_vector = - value_or_string_or_vector>; - -/** - * @brief Configuration structure for session authentication parameters. - * @details All parameters are optional. Only set the parameters needed for - * your authentication method. Parameters are validated when the session is - * constructed. - */ -struct SessionConfig { - /// Authentication token - std::optional token; - /// Path to file containing authentication information - std::optional authFile; - /// URL to authentication server - std::optional authUrl; - /// Username for authentication - std::optional username; - /// Password for authentication - std::optional password; - /// Project ID for session - std::optional projectId; - /// Custom configuration parameter 1 - std::optional custom1; - /// Custom configuration parameter 2 - std::optional custom2; - /// Custom configuration parameter 3 - std::optional custom3; - /// Custom configuration parameter 4 - std::optional custom4; - /// Custom configuration parameter 5 - std::optional custom5; -}; - -class Job; -class Site; -class Device; -class Operation; - -/** - * @brief Class representing the Session library. - * @details This class provides methods to query available devices and - * manage the QDMI session. - * @see QDMI_Session - */ -class Session { -public: - /** - * @brief Creates a Device object from a QDMI_Device handle. - * @param device The QDMI_Device handle to wrap. - * @return A Device object wrapping the given handle. - * @note This is a factory method for use in bindings where a - * session is not accessible. - */ - [[nodiscard]] static Device createSessionlessDevice(QDMI_Device device); - - /** - * @brief Opens a registered QDMI device as a fresh device session. - * @param id Stable registered device ID. - * @param overrides Session values that replace registered defaults. - * @return An owning device wrapper for the new session. - */ - [[nodiscard]] static Device - openDevice(std::string_view id, - const qdmi::DeviceSessionConfig& overrides = {}); - - /** - * @brief Constructs a new QDMI Session with optional authentication. - * @param config Optional session configuration containing authentication - * parameters. If not provided, uses default (no authentication). - * @details Creates, allocates, and initializes a new QDMI session. - */ - explicit Session(const SessionConfig& config = {}); - - /// @see QDMI_SESSION_PROPERTY_DEVICES - [[nodiscard]] std::vector getDevices(); - -private: - /// Query a session property. - template - [[nodiscard]] T queryProperty(const QDMI_Session_Property prop) const { - using StrippedValueType = remove_optional_t::value_type; - - size_t size = 0; - qdmi::throwIfError(QDMI_session_query_session_property(session_.get(), prop, - 0, nullptr, &size), - std::string("Querying size ") + qdmi::toString(prop)); - remove_optional_t value(size / sizeof(StrippedValueType)); - qdmi::throwIfError(QDMI_session_query_session_property( - session_.get(), prop, size, value.data(), nullptr), - std::string("Querying ") + qdmi::toString(prop)); - return value; - } - - std::unique_ptr session_{ - nullptr, QDMI_session_free}; -}; - -static_assert(!std::is_copy_constructible()); -static_assert(!std::is_copy_assignable()); -static_assert(std::is_move_constructible()); -static_assert(std::is_move_assignable()); - -/** - * @brief Class representing a quantum device. - * @details - * This class provides methods to query properties of the device, - * its sites, and its operations. - * - * The class can only be constructed by Session instances. - * - * @see QDMI_Device - */ -class Device { -public: - // NOLINTNEXTLINE(google-explicit-constructor, *-explicit-conversions) - operator QDMI_Device() const { return device_.get(); } - - /// @see QDMI_DEVICE_PROPERTY_NAME - [[nodiscard]] std::string getName() const; - - /// @see QDMI_DEVICE_PROPERTY_VERSION - [[nodiscard]] std::string getVersion() const; - - /// @see QDMI_DEVICE_PROPERTY_STATUS - [[nodiscard]] QDMI_Device_Status getStatus() const; - - /// @see QDMI_DEVICE_PROPERTY_LIBRARYVERSION - [[nodiscard]] std::string getLibraryVersion() const; - - /// @see QDMI_DEVICE_PROPERTY_QUBITSNUM - [[nodiscard]] size_t getQubitsNum() const; - - /// @see QDMI_DEVICE_PROPERTY_SITES - [[nodiscard]] std::vector getSites() const; - - /** - * @brief Returns the list of regular sites (without zone sites) available - * on the device. - * @details Filters all sites and only returns regular sites, i.e., where - * `isZone()` yields `false`. These represent actual potential physical - * qubit locations on the device lattice. - * @returns vector of regular sites - * @see QDMI_DEVICE_PROPERTY_SITES - */ - [[nodiscard]] std::vector getRegularSites() const; - - /** - * @brief Returns the list of zone sites (without regular sites) available - * on the device. - * @details Filters all sites and only returns zone sites, i.e., where - * `isZone()` yields `true`. These represent a zone, i.e., an extent where - * zoned operations can be performed, not individual qubit locations. - * @returns a vector of zone sites - * @see QDMI_DEVICE_PROPERTY_SITES - */ - [[nodiscard]] std::vector getZones() const; - - /// @see QDMI_DEVICE_PROPERTY_OPERATIONS - [[nodiscard]] std::vector getOperations() const; - - /// @see QDMI_DEVICE_PROPERTY_COUPLINGMAP - [[nodiscard]] std::optional>> - getCouplingMap() const; - - /// @see QDMI_DEVICE_PROPERTY_NEEDSCALIBRATION - [[nodiscard]] std::optional getNeedsCalibration() const; - - /// @see QDMI_DEVICE_PROPERTY_QUEUELENGTH - [[nodiscard]] std::optional getQueueLength() const; - - /// @see QDMI_DEVICE_PROPERTY_LENGTHUNIT - [[nodiscard]] std::optional getLengthUnit() const; - - /// @see QDMI_DEVICE_PROPERTY_LENGTHSCALEFACTOR - [[nodiscard]] std::optional getLengthScaleFactor() const; - - /// @see QDMI_DEVICE_PROPERTY_DURATIONUNIT - [[nodiscard]] std::optional getDurationUnit() const; - - /// @see QDMI_DEVICE_PROPERTY_DURATIONSCALEFACTOR - [[nodiscard]] std::optional getDurationScaleFactor() const; - - /// @see QDMI_DEVICE_PROPERTY_MINATOMDISTANCE - [[nodiscard]] std::optional getMinAtomDistance() const; - - /// @see QDMI_DEVICE_PROPERTY_SUPPORTEDPROGRAMFORMATS - [[nodiscard]] std::vector - getSupportedProgramFormats() const; - - /** - * @brief Returns the direct child devices managed by this device. - * @return The child devices, or an empty vector if child devices are not - * supported. - * @see QDMI_DEVICE_PROPERTY_CHILDDEVICES - */ - [[nodiscard]] std::vector getChildDevices() const; - - /** - * @brief Queries an implementation-defined custom device property. - * @tparam T Expected value type. Use `std::vector` to retrieve the - * raw value without interpretation. - * @param property Custom property slot to query. - * @return The decoded value, or `std::nullopt` if the slot is unsupported. - * @throws std::invalid_argument If the returned bytes do not match `T`. - */ - template - [[nodiscard]] std::optional - queryCustomProperty(const CustomProperty property) const { - const auto qdmiProperty = detail::toDeviceProperty(property); - return detail::queryCustomValue( - [this, qdmiProperty](const size_t size, void* value, size_t* sizeRet) { - return QDMI_device_query_device_property(device_.get(), qdmiProperty, - size, value, sizeRet); - }, - "custom device property " + - std::to_string(static_cast(property))); - } - - /** - * @brief Queries a custom device property containing operation handles. - * @param property Custom property slot to query. - * @return Normal QDMI operation wrappers, or `std::nullopt` if the slot is - * unsupported. A supported empty list is returned as an engaged optional. - * @throws std::invalid_argument If the returned byte count is not a multiple - * of `sizeof(QDMI_Operation)`. - */ - [[nodiscard]] std::optional> - queryCustomOperations(CustomProperty property) const; - - /** - * @brief Submits a textual program. - * @details The terminating null byte required by QDMI text formats is - * included in the submitted payload. - * @throws std::invalid_argument If the format requires binary submission, - * names a batch job, or names a calibration run. - * @see QDMI_job_submit - */ - [[nodiscard]] Job submitJob( - const std::string& program, QDMI_Program_Format format, size_t numShots, - const std::optional& custom1 = std::nullopt, - const std::optional& custom2 = std::nullopt, - const std::optional& custom3 = std::nullopt, - const std::optional& custom4 = std::nullopt, - const std::optional& custom5 = std::nullopt) const; - - /** - * @brief Submits a binary program. - * @details The bytes are submitted exactly as provided without appending a - * null byte. - * @throws std::invalid_argument If the format names a batch job or a - * calibration run. - * @see QDMI_job_submit - */ - [[nodiscard]] Job submitJob( - std::span program, QDMI_Program_Format format, - size_t numShots, - const std::optional& custom1 = std::nullopt, - const std::optional& custom2 = std::nullopt, - const std::optional& custom3 = std::nullopt, - const std::optional& custom4 = std::nullopt, - const std::optional& custom5 = std::nullopt) const; - - /** - * @brief Triggers a calibration run. - * @details A device that reports a nonzero - * `QDMI_DEVICE_PROPERTY_NEEDSCALIBRATION` is asked to calibrate by submitting - * a job in the `QDMI_PROGRAM_FORMAT_CALIBRATION` format. QDMI does not - * require a program for such a job, so the payload is optional; when it is - * present, the device defines what it means, which is usually a - * configuration for the run. A calibration run executes no circuit, so no - * shot count is set. - * @param program The calibration payload. An empty span or `std::nullopt` - * means that the job has no payload. - * @see QDMI_job_submit - */ - [[nodiscard]] Job submitCalibrationJob( - std::optional> program = std::nullopt, - const std::optional& custom1 = std::nullopt, - const std::optional& custom2 = std::nullopt, - const std::optional& custom3 = std::nullopt, - const std::optional& custom4 = std::nullopt, - const std::optional& custom5 = std::nullopt) const; - - /** - * @brief Triggers a calibration run with a text payload. - * @details The terminating null byte required by QDMI text formats is - * included in the submitted payload. - * @param program The calibration payload. - * @see QDMI_job_submit - */ - [[nodiscard]] Job submitCalibrationJob( - const std::string& program, - const std::optional& custom1 = std::nullopt, - const std::optional& custom2 = std::nullopt, - const std::optional& custom3 = std::nullopt, - const std::optional& custom4 = std::nullopt, - const std::optional& custom5 = std::nullopt) const; - - /** - * @brief Retrieves an existing job by its device-provided ID. - * @details Opening a job does not submit, clone, or modify the remote job. - * The returned handle can be used to query its state and retrieve results. - * @param jobId The nonempty opaque ID returned by @ref Job::getId. - * @throws std::runtime_error If the driver or device cannot retrieve the job. - * @see QDMI_session_retrieve_job_by_id - */ - [[nodiscard]] Job retrieveJobById(std::string_view jobId) const; - - auto operator<=>(const Device&) const noexcept = default; - -private: - /** - * @brief Constructs a Device object from a QDMI_Device handle. - * @param device The QDMI_Device handle to wrap. - */ - explicit Device(QDMI_Device device) - : device_(device, [](QDMI_Device_impl_d*) {}) {} - - /** - * @brief Constructs a wrapper that retains an owning session. - * @param device The QDMI device handle to wrap. - */ - explicit Device(std::shared_ptr device) - : device_(std::move(device)) {} - - /// Wrap operation handles while retaining their owning device session. - [[nodiscard]] std::vector - wrapOperations(std::span operations) const; - - /// Query a device property. - template - [[nodiscard]] T queryProperty(const QDMI_Device_Property prop) const { - std::string msg = "Querying "; - msg += qdmi::toString(prop); - - if constexpr (string_or_optional_string) { - size_t size = 0; - auto result = QDMI_device_query_device_property(device_.get(), prop, 0, - nullptr, &size); - - if constexpr (is_optional) { - if (result == QDMI_ERROR_NOTSUPPORTED) { - return std::nullopt; - } - } - - qdmi::throwIfError(result, msg); - std::string value(size - 1, '\0'); - result = QDMI_device_query_device_property(device_.get(), prop, size, - value.data(), nullptr); - qdmi::throwIfError(result, msg); - return value; - } else if constexpr (maybe_optional_size_constructible_contiguous_range< - T>) { - size_t size = 0; - auto result = QDMI_device_query_device_property(device_.get(), prop, 0, - nullptr, &size); - - if constexpr (is_optional) { - if (result == QDMI_ERROR_NOTSUPPORTED) { - return std::nullopt; - } - } - - qdmi::throwIfError(result, msg); - remove_optional_t value( - size / sizeof(typename remove_optional_t::value_type)); - result = QDMI_device_query_device_property(device_.get(), prop, size, - value.data(), nullptr); - qdmi::throwIfError(result, msg); - return value; - } else { - remove_optional_t value{}; - const auto result = QDMI_device_query_device_property( - device_.get(), prop, sizeof(remove_optional_t), &value, nullptr); - - if constexpr (is_optional) { - if (result == QDMI_ERROR_NOTSUPPORTED) { - return std::nullopt; - } - } - - qdmi::throwIfError(result, msg); - return value; - } - } - - [[nodiscard]] Job - submitJobImpl(QDMI_Program_Format format, - std::optional> program, - std::optional numShots, - const std::optional& custom1, - const std::optional& custom2, - const std::optional& custom3, - const std::optional& custom4, - const std::optional& custom5) const; - - static void setCustomJobParam(QDMI_Job job, QDMI_Job_Parameter param, - const CustomJobParameter& value); - - /// @brief The underlying device pointer. - std::shared_ptr device_; - - friend class Session; -}; - -/** - * @brief Class representing a submitted job. - * @details - * This class provides methods to query job status and retrieve - * results. - * - * The class can only be constructed by Device instances. - * - * @see QDMI_Job - */ -class Job { -public: - Job(Job&&) noexcept = default; - - auto operator=(Job&& other) noexcept -> Job&; - - // NOLINTNEXTLINE(google-explicit-constructor, *-explicit-conversions) - operator QDMI_Job() const { return job_.get(); } - - /// @see QDMI_job_check - [[nodiscard]] QDMI_Job_Status check() const; - - /** - * @brief @see QDMI_job_wait - * @param timeout The maximum time to wait in seconds. 0 (default) means - * wait indefinitely. - * @return true if the job completed successfully, false if it timed out - */ - [[nodiscard]] bool wait(size_t timeout = 0) const; - - /// @see QDMI_job_cancel - void cancel() const; - - /// Get the job ID - [[nodiscard]] std::string getId() const; - - /// Get the program format - [[nodiscard]] QDMI_Program_Format getProgramFormat() const; - - /** - * @brief Gets a textual program without its terminating null byte. - * @throws std::invalid_argument If the format is not textual or the device - * does not return a null-terminated payload. - */ - [[nodiscard]] std::string getProgram() const; - - /** - * @brief Gets the submitted program bytes exactly as returned by the device. - */ - [[nodiscard]] std::vector getProgramBytes() const; - - /// Get the number of shots - [[nodiscard]] size_t getNumShots() const; - - /** - * @brief Gets the current number of jobs ahead of this job in its queue. - * @return The queue position, or `std::nullopt` if it is unavailable or not - * applicable in the job's current state. - * @throws std::runtime_error If the provider status refresh or property query - * fails for another reason. - * @see QDMI_JOB_PROPERTY_QUEUEPOSITION - */ - [[nodiscard]] std::optional getQueuePosition() const; - - /** - * @brief Queries an implementation-defined custom job property. - * @tparam T Expected value type. Use `std::vector` to retrieve the - * raw value without interpretation. - * @param property Custom property slot to query. - * @return The decoded value, or `std::nullopt` if the slot is unsupported. - * @throws std::invalid_argument If the returned bytes do not match `T`. - */ - template - [[nodiscard]] std::optional - queryCustomProperty(const CustomProperty property) const { - const auto qdmiProperty = detail::toJobProperty(property); - return detail::queryCustomValue( - [this, qdmiProperty](const size_t size, void* value, size_t* sizeRet) { - return QDMI_job_query_property(job_.get(), qdmiProperty, size, value, - sizeRet); - }, - "custom job property " + - std::to_string(static_cast(property))); - } - - /** - * @brief Retrieves an implementation-defined custom job result. - * @tparam T Expected value type. Use `std::vector` to retrieve the - * raw value without interpretation. - * @param property Custom result slot to query. - * @return The decoded value, or `std::nullopt` if the slot is unsupported. - * @throws std::invalid_argument If the returned bytes do not match `T`. - */ - template - [[nodiscard]] std::optional - getCustomResult(const CustomProperty property) const { - const auto qdmiResult = detail::toJobResult(property); - return detail::queryCustomValue( - [this, qdmiResult](const size_t size, void* value, size_t* sizeRet) { - return QDMI_job_get_results(job_.get(), qdmiResult, size, value, - sizeRet); - }, - "custom job result " + std::to_string(static_cast(property))); - } - - /** - * @brief Returns the measurement shots as a vector of bitstrings. - * @see QDMI_JOB_RESULT_SHOTS - */ - [[nodiscard]] std::vector getShots() const; - - /** - * @brief Returns a map of measurement outcomes to their respective counts. - * @see QDMI_JOB_RESULT_HIST_KEYS - * @see QDMI_JOB_RESULT_HIST_VALUES - */ - [[nodiscard]] std::map getCounts() const; - - /** - * @brief Returns the dense state vector as a vector of complex numbers. - * @see QDMI_JOB_RESULT_STATEVECTOR_DENSE - */ - [[nodiscard]] std::vector> getDenseStateVector() const; - - /** - * @brief Returns the dense probabilities as a vector of doubles. - * @see QDMI_JOB_RESULT_PROBABILITIES_DENSE - */ - [[nodiscard]] std::vector getDenseProbabilities() const; - - /** - * @brief Returns the sparse state vector as a map of bitstrings to complex - * amplitudes. - * @see QDMI_JOB_RESULT_STATEVECTOR_SPARSE_KEYS - * @see QDMI_JOB_RESULT_STATEVECTOR_SPARSE_VALUES - */ - [[nodiscard]] std::map> - getSparseStateVector() const; - - /** - * @brief Returns the sparse probabilities as a map of bitstrings to - * probabilities. - * @see QDMI_JOB_RESULT_PROBABILITIES_SPARSE_KEYS - * @see QDMI_JOB_RESULT_PROBABILITIES_SPARSE_VALUES - */ - [[nodiscard]] std::map getSparseProbabilities() const; - - auto operator<=>(const Job&) const noexcept = default; - -private: - /** - * @brief Constructs a Job object from a QDMI_Job handle. - * @param job The QDMI_Job handle to wrap. - * @param device The device that owns the job. - */ - explicit Job(QDMI_Job job, std::shared_ptr device) - : device_(std::move(device)), job_(job, QDMI_job_free) {} - - /** - * @brief Ownership of the device session that owns the job. - * @note Declared before `job_` so the job is freed before its device. - */ - std::shared_ptr device_; - - std::unique_ptr job_{ - nullptr, QDMI_job_free}; - - friend class Device; -}; - -static_assert(!std::is_copy_constructible()); -static_assert(!std::is_copy_assignable()); -static_assert(std::is_move_constructible()); -static_assert(std::is_move_assignable()); - -/** - * @brief Class representing a site (qubit) on the device. - * @details - * This class provides methods to query properties of the site. - * - * The class can only be constructed by Device and Operation instances. - * - * @see QDMI_Site - */ -class Site { -public: - // NOLINTNEXTLINE(google-explicit-constructor, *-explicit-conversions) - operator QDMI_Site() const { return site_; } - - /// @see QDMI_SITE_PROPERTY_INDEX - [[nodiscard]] size_t getIndex() const; - - /// @see QDMI_SITE_PROPERTY_T1 - [[nodiscard]] std::optional getT1() const; - - /// @see QDMI_SITE_PROPERTY_T2 - [[nodiscard]] std::optional getT2() const; - - /// @see QDMI_SITE_PROPERTY_NAME - [[nodiscard]] std::optional getName() const; - - /// @see QDMI_SITE_PROPERTY_XCOORDINATE - [[nodiscard]] std::optional getXCoordinate() const; - - /// @see QDMI_SITE_PROPERTY_YCOORDINATE - [[nodiscard]] std::optional getYCoordinate() const; - - /// @see QDMI_SITE_PROPERTY_ZCOORDINATE - [[nodiscard]] std::optional getZCoordinate() const; - - /// @see QDMI_SITE_PROPERTY_ISZONE - [[nodiscard]] bool isZone() const; - - /// @see QDMI_SITE_PROPERTY_XEXTENT - [[nodiscard]] std::optional getXExtent() const; - - /// @see QDMI_SITE_PROPERTY_YEXTENT - [[nodiscard]] std::optional getYExtent() const; - - /// @see QDMI_SITE_PROPERTY_ZEXTENT - [[nodiscard]] std::optional getZExtent() const; - - /// @see QDMI_SITE_PROPERTY_MODULEINDEX - [[nodiscard]] std::optional getModuleIndex() const; - - /// @see QDMI_SITE_PROPERTY_SUBMODULEINDEX - [[nodiscard]] std::optional getSubmoduleIndex() const; - - /** - * @brief Queries an implementation-defined custom site property. - * @tparam T Expected value type. Use `std::vector` to retrieve the - * raw value without interpretation. - * @param property Custom property slot to query. - * @return The decoded value, or `std::nullopt` if the slot is unsupported. - * @throws std::invalid_argument If the returned bytes do not match `T`. - */ - template - [[nodiscard]] std::optional - queryCustomProperty(const CustomProperty property) const { - const auto qdmiProperty = detail::toSiteProperty(property); - return detail::queryCustomValue( - [this, qdmiProperty](const size_t size, void* value, size_t* sizeRet) { - return QDMI_device_query_site_property( - device_.get(), site_, qdmiProperty, size, value, sizeRet); - }, - "custom site property " + - std::to_string(static_cast(property))); - } - - auto operator<=>(const Site&) const noexcept = default; - -private: - /** - * @brief Constructs a Site object from a QDMI_Site handle. - * @param device The QDMI device handle that owns the site. - * @param site The QDMI_Site handle to wrap. - */ - Site(std::shared_ptr device, QDMI_Site site) - : device_(std::move(device)), site_(site) {} - - /// Query a site property. - template - [[nodiscard]] T queryProperty(const QDMI_Site_Property prop) const { - if constexpr (string_or_optional_string) { - size_t size = 0; - const auto result = QDMI_device_query_site_property( - device_.get(), site_, prop, 0, nullptr, &size); - if constexpr (is_optional) { - if (result == QDMI_ERROR_NOTSUPPORTED) { - return std::nullopt; - } - } - qdmi::throwIfError(result, - std::string("Querying size") + qdmi::toString(prop)); - std::string value(size - 1, '\0'); - qdmi::throwIfError(QDMI_device_query_site_property(device_.get(), site_, - prop, size, - value.data(), nullptr), - std::string("Querying ") + qdmi::toString(prop)); - return value; - } else { - remove_optional_t value{}; - const auto result = QDMI_device_query_site_property( - device_.get(), site_, prop, sizeof(remove_optional_t), &value, - nullptr); - if constexpr (is_optional) { - if (result == QDMI_ERROR_NOTSUPPORTED) { - return std::nullopt; - } - } - qdmi::throwIfError(result, - std::string("Querying ") + qdmi::toString(prop)); - return value; - } - } - - /// @brief The QDMI device handle that owns the site. - std::shared_ptr device_; - - /// @brief The underlying QDMI_Site object. - QDMI_Site site_; - - friend class Device; - friend class Operation; -}; - -/** - * @brief Class representing an operation (gate) supported by the device. - * @details - * This class provides methods to query properties of the - * operation. - * - * The class can only be constructed by Device instances. - * - * @see QDMI_Operation - */ -class Operation { -public: - // NOLINTNEXTLINE(google-explicit-constructor, *-explicit-conversions) - operator QDMI_Operation() const { return operation_; } - - /// @see QDMI_OPERATION_PROPERTY_NAME - [[nodiscard]] std::string - getName(const std::vector& sites = {}, - const std::vector& params = {}) const; - - /// @see QDMI_OPERATION_PROPERTY_QUBITSNUM - [[nodiscard]] std::optional - getQubitsNum(const std::vector& sites = {}, - const std::vector& params = {}) const; - - /// @see QDMI_OPERATION_PROPERTY_PARAMETERSNUM - [[nodiscard]] size_t - getParametersNum(const std::vector& sites = {}, - const std::vector& params = {}) const; - - /// @see QDMI_OPERATION_PROPERTY_DURATION - [[nodiscard]] std::optional - getDuration(const std::vector& sites = {}, - const std::vector& params = {}) const; - - /// @see QDMI_OPERATION_PROPERTY_FIDELITY - [[nodiscard]] std::optional - getFidelity(const std::vector& sites = {}, - const std::vector& params = {}) const; - - /// @see QDMI_OPERATION_PROPERTY_INTERACTIONRADIUS - [[nodiscard]] std::optional - getInteractionRadius(const std::vector& sites = {}, - const std::vector& params = {}) const; - - /// @see QDMI_OPERATION_PROPERTY_BLOCKINGRADIUS - [[nodiscard]] std::optional - getBlockingRadius(const std::vector& sites = {}, - const std::vector& params = {}) const; - - /// @see QDMI_OPERATION_PROPERTY_IDLINGFIDELITY - [[nodiscard]] std::optional - getIdlingFidelity(const std::vector& sites = {}, - const std::vector& params = {}) const; - - /// @see QDMI_OPERATION_PROPERTY_ISZONED - [[nodiscard]] bool isZoned() const; - - /// @see QDMI_OPERATION_PROPERTY_SITES - [[nodiscard]] std::optional> getSites() const; - - /** - * @brief Returns the list of site pairs the local 2-qubit operation can - * be performed on. - * @details For local 2-qubit operations, this function interprets the - * returned list of sites by QDMI as site pairs according to the QDMI - * specification. Hence, this function facilitates easier iteration over - * supported site pairs. - * @return Optional vector of site pairs if this is a local 2-qubit - * operation, std::nullopt otherwise. - * @see QDMI_OPERATION_PROPERTY_SITES - */ - [[nodiscard]] std::optional>> - getSitePairs() const; - - /// @see QDMI_OPERATION_PROPERTY_MEANSHUTTLINGSPEED - [[nodiscard]] std::optional - getMeanShuttlingSpeed(const std::vector& sites = {}, - const std::vector& params = {}) const; - - /** - * @brief Queries an implementation-defined custom operation property. - * @tparam T Expected value type. Use `std::vector` to retrieve the - * raw value without interpretation. - * @param property Custom property slot to query. - * @param sites Sites for context-dependent operation properties. - * @param params Parameters for context-dependent operation properties. - * @return The decoded value, or `std::nullopt` if the slot is unsupported. - * @throws std::invalid_argument If the returned bytes do not match `T`. - */ - template - [[nodiscard]] std::optional - queryCustomProperty(const CustomProperty property, - const std::vector& sites = {}, - const std::vector& params = {}) const { - const auto qdmiProperty = detail::toOperationProperty(property); - std::vector qdmiSites; - qdmiSites.reserve(sites.size()); - std::ranges::transform(sites, std::back_inserter(qdmiSites), - [](const Site& site) -> QDMI_Site { return site; }); - return detail::queryCustomValue( - [this, qdmiProperty, &qdmiSites, - ¶ms](const size_t size, void* value, size_t* sizeRet) { - return QDMI_device_query_operation_property( - device_.get(), operation_, qdmiSites.size(), qdmiSites.data(), - params.size(), params.data(), qdmiProperty, size, value, sizeRet); - }, - "custom operation property " + - std::to_string(static_cast(property))); - } - - auto operator<=>(const Operation&) const noexcept = default; - -private: - /** - * @brief Constructs an Operation object from a QDMI_Operation handle. - * @param device The QDMI device handle that owns the operation. - * @param operation The QDMI_Operation handle to wrap. - */ - Operation(std::shared_ptr device, - QDMI_Operation operation) - : device_(std::move(device)), operation_(operation) {} - - /// Query an operation property. - template - [[nodiscard]] T queryProperty(const QDMI_Operation_Property prop, - const std::vector& sites, - const std::vector& params) const { - std::string msg = "Querying "; - msg += qdmi::toString(prop); - std::vector qdmiSites; - qdmiSites.reserve(sites.size()); - std::ranges::transform(sites, std::back_inserter(qdmiSites), - [](const Site& site) -> QDMI_Site { return site; }); - if constexpr (string_or_optional_string) { - size_t size = 0; - auto result = QDMI_device_query_operation_property( - device_.get(), operation_, sites.size(), qdmiSites.data(), - params.size(), params.data(), prop, 0, nullptr, &size); - if constexpr (is_optional) { - if (result == QDMI_ERROR_NOTSUPPORTED) { - return std::nullopt; - } - } - qdmi::throwIfError(result, msg); - std::string value(size - 1, '\0'); - result = QDMI_device_query_operation_property( - device_.get(), operation_, sites.size(), qdmiSites.data(), - params.size(), params.data(), prop, size, value.data(), nullptr); - qdmi::throwIfError(result, msg); - return value; - } else if constexpr (maybe_optional_size_constructible_contiguous_range< - T>) { - size_t size = 0; - auto result = QDMI_device_query_operation_property( - device_.get(), operation_, sites.size(), qdmiSites.data(), - params.size(), params.data(), prop, 0, nullptr, &size); - if constexpr (is_optional) { - if (result == QDMI_ERROR_NOTSUPPORTED) { - return std::nullopt; - } - } - qdmi::throwIfError(result, msg); - remove_optional_t value( - size / sizeof(typename remove_optional_t::value_type)); - result = QDMI_device_query_operation_property( - device_.get(), operation_, sites.size(), qdmiSites.data(), - params.size(), params.data(), prop, size, value.data(), nullptr); - qdmi::throwIfError(result, msg); - return value; - } else { - remove_optional_t value{}; - const auto result = QDMI_device_query_operation_property( - device_.get(), operation_, sites.size(), qdmiSites.data(), - params.size(), params.data(), prop, sizeof(remove_optional_t), - &value, nullptr); - if constexpr (is_optional) { - if (result == QDMI_ERROR_NOTSUPPORTED) { - return std::nullopt; - } - } - qdmi::throwIfError(result, msg); - return value; - } - } - - /// @brief The QDMI device handle that owns the operation. - std::shared_ptr device_; - - /// @brief The underlying QDMI_Operation object. - QDMI_Operation operation_; - - friend class Device; -}; -} // namespace qdmi +#include "qdmi/Device.hpp" +#include "qdmi/DeviceManager.hpp" +#include "qdmi/DeviceRegistry.hpp" +#include "qdmi/SessionConfig.hpp" diff --git a/include/mqt-core/qdmi/Device.hpp b/include/mqt-core/qdmi/Device.hpp new file mode 100644 index 0000000000..47c58968ce --- /dev/null +++ b/include/mqt-core/qdmi/Device.hpp @@ -0,0 +1,375 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +/// @file Device.hpp +/// @brief C++ object model for QDMI devices. + +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace qdmi { +namespace detail { +struct DeviceState; +struct JobState; +struct DeviceFactory; +} // namespace detail + +using CustomJobParameter = std::variant; + +/// Identifies an implementation-defined custom property or result slot. +enum class CustomProperty : std::uint8_t { + Custom1 = 1, + Custom2 = 2, + Custom3 = 3, + Custom4 = 4, + Custom5 = 5, +}; + +/** + * @brief Returns whether a program format carries a binary payload. + * @details `QDMI_PROGRAM_FORMAT_QIRBASEMODULE`, + * `QDMI_PROGRAM_FORMAT_QIRADAPTIVEMODULE`, and `QDMI_PROGRAM_FORMAT_QPY` hold + * bitcode or another serialized object. Such a payload can contain a null byte + * and must be submitted as exact bytes. + * @param format The program format to classify. + * @return True if the format requires exact-byte submission. + */ +[[nodiscard]] constexpr bool +isBinaryProgramFormat(const QDMI_Program_Format format) noexcept { + return format == QDMI_PROGRAM_FORMAT_QIRBASEMODULE || + format == QDMI_PROGRAM_FORMAT_QIRADAPTIVEMODULE || + format == QDMI_PROGRAM_FORMAT_QPY; +} + +template +concept custom_property_value = + std::same_as || std::same_as || + std::same_as || std::same_as || + std::same_as>; + +namespace detail { +template +[[nodiscard]] std::optional +decodeCustomValue(const std::optional>& bytes, + const std::string& description) { + if (!bytes) { + return std::nullopt; + } + if constexpr (std::same_as>) { + return bytes; + } else if constexpr (std::same_as) { + if (bytes->empty() || bytes->back() != std::byte{0}) { + throw std::invalid_argument("Cannot decode " + description + + " as a null-terminated string"); + } + return std::string(reinterpret_cast(bytes->data()), + bytes->size() - 1); + } else { + if (bytes->size() != sizeof(T)) { + throw std::invalid_argument("Cannot decode " + description + + ": unexpected byte size"); + } + T value{}; + std::memcpy(&value, bytes->data(), sizeof(T)); + return value; + } +} +} // namespace detail + +class Job; +class Site; +class Operation; +class DeviceManager; + +/// One initialized quantum-device session. +class Device { +public: + [[nodiscard]] std::string getName() const; + [[nodiscard]] std::string getVersion() const; + [[nodiscard]] QDMI_Device_Status getStatus() const; + [[nodiscard]] std::string getLibraryVersion() const; + [[nodiscard]] size_t getQubitsNum() const; + [[nodiscard]] std::vector getSites() const; + [[nodiscard]] std::vector getRegularSites() const; + [[nodiscard]] std::vector getZones() const; + [[nodiscard]] std::vector getOperations() const; + [[nodiscard]] std::optional>> + getCouplingMap() const; + [[nodiscard]] std::optional getNeedsCalibration() const; + [[nodiscard]] std::optional getQueueLength() const; + [[nodiscard]] std::optional getLengthUnit() const; + [[nodiscard]] std::optional getLengthScaleFactor() const; + [[nodiscard]] std::optional getDurationUnit() const; + [[nodiscard]] std::optional getDurationScaleFactor() const; + [[nodiscard]] std::optional getMinAtomDistance() const; + [[nodiscard]] std::vector + getSupportedProgramFormats() const; + [[nodiscard]] std::vector getChildDevices() const; + + /// Query a custom device property that contains operation handles. + [[nodiscard]] std::optional> + queryCustomOperations(CustomProperty property) const; + + template + [[nodiscard]] std::optional + queryCustomProperty(const CustomProperty property) const { + return detail::decodeCustomValue(queryCustomPropertyBytes(property), + "custom device property"); + } + + /** + * @brief Submits a textual program. + * @details The terminating null byte required by QDMI text formats is + * included in the submitted payload. + * @throws std::invalid_argument If the format requires binary submission, + * names a batch job, or names a calibration run. + */ + [[nodiscard]] Job submitJob( + const std::string& program, QDMI_Program_Format format, size_t numShots, + const std::optional& custom1 = std::nullopt, + const std::optional& custom2 = std::nullopt, + const std::optional& custom3 = std::nullopt, + const std::optional& custom4 = std::nullopt, + const std::optional& custom5 = std::nullopt) const; + + /// Retrieve an existing job by its provider-defined stable ID. + [[nodiscard]] Job retrieveJobById(std::string_view jobId) const; + + /** + * @brief Submits a binary program exactly as provided. + * @throws std::invalid_argument If the format names a batch job or a + * calibration run. + */ + [[nodiscard]] Job submitJob( + std::span program, QDMI_Program_Format format, + size_t numShots, + const std::optional& custom1 = std::nullopt, + const std::optional& custom2 = std::nullopt, + const std::optional& custom3 = std::nullopt, + const std::optional& custom4 = std::nullopt, + const std::optional& custom5 = std::nullopt) const; + + /** + * @brief Triggers a calibration run. + * @details The payload is optional. A calibration run executes no circuit, + * so no shot count is set. + * @param program The calibration payload. An empty span or `std::nullopt` + * means that the job has no payload. + */ + [[nodiscard]] Job submitCalibrationJob( + std::optional> program = std::nullopt, + const std::optional& custom1 = std::nullopt, + const std::optional& custom2 = std::nullopt, + const std::optional& custom3 = std::nullopt, + const std::optional& custom4 = std::nullopt, + const std::optional& custom5 = std::nullopt) const; + + /** + * @brief Triggers a calibration run with a text payload. + * @details The submitted payload includes the terminating null byte. + */ + [[nodiscard]] Job submitCalibrationJob( + const std::string& program, + const std::optional& custom1 = std::nullopt, + const std::optional& custom2 = std::nullopt, + const std::optional& custom3 = std::nullopt, + const std::optional& custom4 = std::nullopt, + const std::optional& custom5 = std::nullopt) const; + + [[nodiscard]] auto operator<=>(const Device&) const noexcept = default; + +private: + explicit Device(std::shared_ptr state) + : state_(std::move(state)) {} + [[nodiscard]] std::optional> + queryCustomPropertyBytes(CustomProperty property) const; + [[nodiscard]] Job + submitJobImpl(QDMI_Program_Format format, + std::optional> program, + std::optional numShots, + const std::optional& custom1, + const std::optional& custom2, + const std::optional& custom3, + const std::optional& custom4, + const std::optional& custom5) const; + std::shared_ptr state_; + friend class DeviceManager; + friend struct detail::DeviceFactory; +}; + +/// A submitted job retaining its device session. +class Job { +public: + [[nodiscard]] QDMI_Job_Status check() const; + [[nodiscard]] bool wait(size_t timeout = 0) const; + void cancel() const; + [[nodiscard]] std::string getId() const; + [[nodiscard]] QDMI_Program_Format getProgramFormat() const; + /** + * @brief Gets a textual program without its terminating null byte. + * @throws std::invalid_argument If the format is binary or the returned + * payload is not null-terminated. + */ + [[nodiscard]] std::string getProgram() const; + /// Gets the submitted program bytes exactly as returned by the device. + [[nodiscard]] std::vector getProgramBytes() const; + [[nodiscard]] size_t getNumShots() const; + [[nodiscard]] std::optional getQueuePosition() const; + + template + [[nodiscard]] std::optional + queryCustomProperty(const CustomProperty property) const { + return detail::decodeCustomValue(queryCustomPropertyBytes(property), + "custom job property"); + } + template + [[nodiscard]] std::optional + getCustomResult(const CustomProperty property) const { + return detail::decodeCustomValue(getCustomResultBytes(property), + "custom job result"); + } + + [[nodiscard]] std::vector getShots() const; + [[nodiscard]] std::map getCounts() const; + [[nodiscard]] std::vector> getDenseStateVector() const; + [[nodiscard]] std::vector getDenseProbabilities() const; + [[nodiscard]] std::map> + getSparseStateVector() const; + [[nodiscard]] std::map getSparseProbabilities() const; + + [[nodiscard]] auto operator<=>(const Job&) const noexcept = default; + +private: + explicit Job(std::shared_ptr state) + : state_(std::move(state)) {} + [[nodiscard]] std::optional> + queryCustomPropertyBytes(CustomProperty property) const; + [[nodiscard]] std::optional> + getCustomResultBytes(CustomProperty property) const; + std::shared_ptr state_; + friend class Device; +}; + +/// A physical site or zone belonging to a device. +class Site { +public: + [[nodiscard]] size_t getIndex() const; + [[nodiscard]] std::optional getT1() const; + [[nodiscard]] std::optional getT2() const; + [[nodiscard]] std::optional getName() const; + [[nodiscard]] std::optional getXCoordinate() const; + [[nodiscard]] std::optional getYCoordinate() const; + [[nodiscard]] std::optional getZCoordinate() const; + [[nodiscard]] bool isZone() const; + [[nodiscard]] std::optional getXExtent() const; + [[nodiscard]] std::optional getYExtent() const; + [[nodiscard]] std::optional getZExtent() const; + [[nodiscard]] std::optional getModuleIndex() const; + [[nodiscard]] std::optional getSubmoduleIndex() const; + + template + [[nodiscard]] std::optional + queryCustomProperty(const CustomProperty property) const { + return detail::decodeCustomValue(queryCustomPropertyBytes(property), + "custom site property"); + } + + [[nodiscard]] auto operator<=>(const Site&) const noexcept = default; + +private: + Site(std::shared_ptr state, void* handle) + : state_(std::move(state)), handle_(handle) {} + [[nodiscard]] std::optional> + queryCustomPropertyBytes(CustomProperty property) const; + std::shared_ptr state_; + void* handle_ = nullptr; + friend class Device; + friend class Operation; +}; + +/// An operation supported by a device. +class Operation { +public: + [[nodiscard]] std::string + getName(const std::vector& sites = {}, + const std::vector& params = {}) const; + [[nodiscard]] std::optional + getQubitsNum(const std::vector& sites = {}, + const std::vector& params = {}) const; + [[nodiscard]] size_t + getParametersNum(const std::vector& sites = {}, + const std::vector& params = {}) const; + [[nodiscard]] std::optional + getDuration(const std::vector& sites = {}, + const std::vector& params = {}) const; + [[nodiscard]] std::optional + getFidelity(const std::vector& sites = {}, + const std::vector& params = {}) const; + [[nodiscard]] std::optional + getInteractionRadius(const std::vector& sites = {}, + const std::vector& params = {}) const; + [[nodiscard]] std::optional + getBlockingRadius(const std::vector& sites = {}, + const std::vector& params = {}) const; + [[nodiscard]] std::optional + getIdlingFidelity(const std::vector& sites = {}, + const std::vector& params = {}) const; + [[nodiscard]] bool isZoned() const; + [[nodiscard]] std::optional> getSites() const; + [[nodiscard]] std::optional>> + getSitePairs() const; + [[nodiscard]] std::optional + getMeanShuttlingSpeed(const std::vector& sites = {}, + const std::vector& params = {}) const; + + template + [[nodiscard]] std::optional + queryCustomProperty(const CustomProperty property, + const std::vector& sites = {}, + const std::vector& params = {}) const { + return detail::decodeCustomValue( + queryCustomPropertyBytes(property, sites, params), + "custom operation property"); + } + + [[nodiscard]] auto operator<=>(const Operation&) const noexcept = default; + +private: + [[nodiscard]] std::vector + siteHandles(const std::vector& sites) const; + Operation(std::shared_ptr state, void* handle) + : state_(std::move(state)), handle_(handle) {} + [[nodiscard]] std::optional> + queryCustomPropertyBytes(CustomProperty property, + const std::vector& sites, + const std::vector& params) const; + std::shared_ptr state_; + void* handle_ = nullptr; + friend class Device; +}; +} // namespace qdmi diff --git a/include/mqt-core/qdmi/DeviceManager.hpp b/include/mqt-core/qdmi/DeviceManager.hpp new file mode 100644 index 0000000000..72cbef3299 --- /dev/null +++ b/include/mqt-core/qdmi/DeviceManager.hpp @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +/** @file DeviceManager.hpp + * @brief Immutable QDMI registry snapshots and fresh device sessions. + */ + +#pragma once + +#include "qdmi/Device.hpp" +#include "qdmi/DeviceRegistry.hpp" + +#include +#include +#include +#include + +namespace qdmi { + +/// Successes and independent failures from opening every registered device. +struct OpenAllResult { + std::map devices; + std::map errors; +}; + +/** + * @brief An immutable snapshot that opens fresh QDMI device sessions. + * + * A manager is not a singleton. Returned devices and their derived objects own + * the session and library state they need and may outlive this manager. + */ +class DeviceManager { +public: + /// Snapshot the process default registry. + DeviceManager(); + + /// Snapshot an explicit registry by value. + explicit DeviceManager(DeviceRegistry registry); + + [[nodiscard]] const std::vector& definitions() const { + return registry_.definitions(); + } + + [[nodiscard]] std::vector deviceIds() const { + return registry_.deviceIds(); + } + + [[nodiscard]] Device + open(std::string_view id, + const DeviceSessionConfig& sessionOverrides = {}) const; + + [[nodiscard]] OpenAllResult + openAll(const DeviceSessionConfig& sessionOverrides = {}) const; + +private: + DeviceRegistry registry_; +}; + +/// Open one fresh session from the current process default registry. +[[nodiscard]] Device +openDevice(std::string_view id, + const DeviceSessionConfig& sessionOverrides = {}); + +} // namespace qdmi diff --git a/include/mqt-core/qdmi/DeviceRegistry.hpp b/include/mqt-core/qdmi/DeviceRegistry.hpp new file mode 100644 index 0000000000..8b75a74748 --- /dev/null +++ b/include/mqt-core/qdmi/DeviceRegistry.hpp @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +/** @file DeviceRegistry.hpp + * @brief Side-effect-free QDMI device discovery and registration. + */ + +#pragma once + +#include "qdmi/SessionConfig.hpp" + +#include +#include +#include +#include + +namespace qdmi { + +/// Stable metadata used to open fresh sessions for one QDMI device. +struct DeviceDefinition { + std::string id; + std::filesystem::path library; + std::string prefix; + DeviceSessionConfig session; +}; + +/// Discovers and combines device definitions without loading native code. +class DeviceRegistry { +public: + /// Discover definitions from the standard configuration sources. + DeviceRegistry(); + + /// Create an isolated registry from explicit definitions. + explicit DeviceRegistry(std::vector definitions); + + /// Return enabled definitions in stable registration order. + [[nodiscard]] const std::vector& definitions() const { + return definitions_; + } + + /// Return enabled stable IDs without loading native code. + [[nodiscard]] std::vector deviceIds() const; + + /// Register or replace a complete definition. + void registerDevice(DeviceDefinition definition, bool replace = false); + + /// Register a fallback unless the ID exists or is explicitly disabled. + [[nodiscard]] bool registerDeviceIfAbsent(DeviceDefinition definition); + +private: + std::vector definitions_; + std::unordered_set disabledIds_; +}; + +/// Register a definition in the process default registry. +void registerDevice(DeviceDefinition definition, bool replace = false); + +/// Register a default fallback unless its ID exists or is disabled. +[[nodiscard]] bool registerDeviceIfAbsent(DeviceDefinition definition); + +/// Return IDs from the process default registry without loading native code. +[[nodiscard]] std::vector registeredDeviceIds(); + +} // namespace qdmi diff --git a/include/mqt-core/qdmi/SessionConfig.hpp b/include/mqt-core/qdmi/SessionConfig.hpp new file mode 100644 index 0000000000..5ed65a3842 --- /dev/null +++ b/include/mqt-core/qdmi/SessionConfig.hpp @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +/** @file SessionConfig.hpp + * @brief QDMI device-session configuration types and helpers. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace qdmi { + +/// Inline JSON used to configure one QDMI device session. +struct InlineDeviceConfiguration { + std::string json; +}; + +/// JSON file used to configure one QDMI device session. +struct FileDeviceConfiguration { + std::filesystem::path path; +}; + +/// One replaceable source for a runtime device description. +using DeviceConfigurationSource = + std::variant; + +/// Optional parameters applied before one QDMI device session is initialized. +struct DeviceSessionConfig { + std::optional baseUrl; + std::optional token; + std::optional authFile; + std::optional authUrl; + std::optional username; + std::optional password; + std::optional deviceConfiguration; + std::optional custom1; + std::optional custom2; + std::optional custom3; + std::optional custom4; + std::optional custom5; +}; + +/** + * @brief Construct session configuration from the public keyword-style fields. + * @throws std::invalid_argument If both configuration sources are set. + */ +[[nodiscard]] inline DeviceSessionConfig makeDeviceSessionConfig( + std::optional baseUrl, std::optional token, + std::optional authFile, + std::optional authUrl, std::optional username, + std::optional password, + std::optional deviceConfig, + std::optional deviceConfigFile, + std::optional custom1, std::optional custom2, + std::optional custom3, std::optional custom4, + std::optional custom5) { + if (deviceConfig && deviceConfigFile) { + throw std::invalid_argument( + "device_config and device_config_file are mutually exclusive"); + } + std::optional configuration; + if (deviceConfig) { + configuration = InlineDeviceConfiguration{.json = std::move(*deviceConfig)}; + } else if (deviceConfigFile) { + configuration = + FileDeviceConfiguration{.path = std::move(*deviceConfigFile)}; + } + return {.baseUrl = std::move(baseUrl), + .token = std::move(token), + .authFile = std::move(authFile), + .authUrl = std::move(authUrl), + .username = std::move(username), + .password = std::move(password), + .deviceConfiguration = std::move(configuration), + .custom1 = std::move(custom1), + .custom2 = std::move(custom2), + .custom3 = std::move(custom3), + .custom4 = std::move(custom4), + .custom5 = std::move(custom5)}; +} + +} // namespace qdmi diff --git a/include/mqt-core/qdmi/Slurm.hpp b/include/mqt-core/qdmi/Slurm.hpp index 0dcafaf86a..2d127997b7 100644 --- a/include/mqt-core/qdmi/Slurm.hpp +++ b/include/mqt-core/qdmi/Slurm.hpp @@ -15,7 +15,7 @@ #pragma once -#include "qdmi/Client.hpp" +#include "qdmi/Device.hpp" namespace qdmi::slurm { diff --git a/include/mqt-core/qdmi/driver/Driver.hpp b/include/mqt-core/qdmi/driver/Driver.hpp deleted file mode 100644 index 4249fb66af..0000000000 --- a/include/mqt-core/qdmi/driver/Driver.hpp +++ /dev/null @@ -1,543 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -/** @file Driver.hpp - * @brief QDMI driver implementation interfaces. - */ - -#pragma once - -#include "qdmi/common/Common.hpp" - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace qdmi { - -class Session; - -/// Inline JSON used to configure one QDMI device session. -struct InlineDeviceConfiguration { - std::string json; -}; - -/// JSON file used to configure one QDMI device session. -struct FileDeviceConfiguration { - std::filesystem::path path; -}; - -/// One replaceable source for a runtime device description. -using DeviceConfigurationSource = - std::variant; - -/** - * @brief Configuration for device session parameters. - * @details This struct holds optional parameters that can be set on a device - * session before initialization. All parameters are optional. - */ -struct DeviceSessionConfig { - /// Base URL for API endpoint - std::optional baseUrl; - /// Authentication token - std::optional token; - /// Path to file containing authentication information - std::optional authFile; - /// URL to authentication server - std::optional authUrl; - /// Username for authentication - std::optional username; - /// Password for authentication - std::optional password; - /// Typed runtime device-description source. - std::optional deviceConfiguration; - /// Custom configuration parameter 1 - std::optional custom1; - /// Custom configuration parameter 2 - std::optional custom2; - /// Custom configuration parameter 3 - std::optional custom3; - /// Custom configuration parameter 4 - std::optional custom4; - /// Custom configuration parameter 5 - std::optional custom5; -}; - -/** - * @brief Stable registration for a QDMI device. - * @details Registration records this metadata without loading the native - * library. Use @ref Driver::open to create the corresponding device session. - */ -struct DeviceDefinition { - /// Stable identifier used to open this device. - std::string id; - /// Path to the native QDMI device library. - std::filesystem::path library; - /// Prefix used for the QDMI device interface functions. - std::string prefix; - /// Parameters applied before the device session is initialized. - DeviceSessionConfig session; -}; - -/** - * @brief Definition of the device library. - * @details The device library contains function pointers to the QDMI - * device interface functions. - */ -struct DeviceLibrary { - // we keep the naming scheme of QDMI, i.e., snail_case for function names, - // here to ease the `LOAD_SYMBOL` macro later on. - // NOLINTBEGIN(readability-identifier-naming) - /// Function pointer to @ref QDMI_device_initialize. - decltype(QDMI_device_initialize)* device_initialize{}; - /// Function pointer to @ref QDMI_device_finalize. - decltype(QDMI_device_finalize)* device_finalize{}; - /// Function pointer to @ref QDMI_device_session_alloc. - decltype(QDMI_device_session_alloc)* device_session_alloc{}; - /// Function pointer to @ref QDMI_device_session_init. - decltype(QDMI_device_session_init)* device_session_init{}; - /// Function pointer to @ref QDMI_device_session_free. - decltype(QDMI_device_session_free)* device_session_free{}; - /// Function pointer to @ref QDMI_device_session_set_parameter. - decltype(QDMI_device_session_set_parameter)* device_session_set_parameter{}; - /// Function pointer to @ref QDMI_device_session_create_device_job. - decltype(QDMI_device_session_create_device_job)* - device_session_create_device_job{}; - /// Function pointer to @ref QDMI_device_session_retrieve_device_job_by_id. - decltype(QDMI_device_session_retrieve_device_job_by_id)* - device_session_retrieve_device_job_by_id{}; - /// Function pointer to @ref QDMI_device_job_free. - decltype(QDMI_device_job_free)* device_job_free{}; - /// Function pointer to @ref QDMI_device_job_set_parameter. - decltype(QDMI_device_job_set_parameter)* device_job_set_parameter{}; - /// Function pointer to @ref QDMI_device_job_query_property. - decltype(QDMI_device_job_query_property)* device_job_query_property{}; - /// Function pointer to @ref QDMI_device_job_submit. - decltype(QDMI_device_job_submit)* device_job_submit{}; - /// Function pointer to @ref QDMI_device_job_cancel. - decltype(QDMI_device_job_cancel)* device_job_cancel{}; - /// Function pointer to @ref QDMI_device_job_check. - decltype(QDMI_device_job_check)* device_job_check{}; - /// Function pointer to @ref QDMI_device_job_wait. - decltype(QDMI_device_job_wait)* device_job_wait{}; - /// Function pointer to @ref QDMI_device_job_get_results. - decltype(QDMI_device_job_get_results)* device_job_get_results{}; - /// Function pointer to @ref QDMI_device_session_query_device_property. - decltype(QDMI_device_session_query_device_property)* - device_session_query_device_property{}; - /// Function pointer to @ref QDMI_device_session_query_site_property. - decltype(QDMI_device_session_query_site_property)* - device_session_query_site_property{}; - /// Function pointer to @ref QDMI_device_session_query_operation_property. - decltype(QDMI_device_session_query_operation_property)* - device_session_query_operation_property{}; - // NOLINTEND(readability-identifier-naming) - - // Default constructor - DeviceLibrary() = default; - // delete copy constructor and copy assignment operator - DeviceLibrary(const DeviceLibrary&) = delete; - DeviceLibrary& operator=(const DeviceLibrary&) = delete; - // define move constructor and move assignment operator - DeviceLibrary(DeviceLibrary&&) = default; - DeviceLibrary& operator=(DeviceLibrary&&) = default; - // destructor should be virtual to allow for polymorphic deletion - virtual ~DeviceLibrary() = default; -}; - -/** - * @brief Definition of the dynamic device library. - * @details This class is used to load the QDMI device interface functions - * from a dynamic library at runtime. It inherits from DeviceLibrary and - * overrides the constructor and destructor to open and close the library. - */ -class DynamicDeviceLibrary final : public DeviceLibrary { - /// @brief Handle to the dynamic library - void* libHandle_; - -public: - /** - * @brief Constructs a DynamicDeviceLibrary object. - * @details This constructor loads the QDMI device interface functions - * from the dynamic library specified by `libName` and `prefix`. - * @param libName is the name of the dynamic library to load. - * @param prefix is the prefix used for the function names in the library. - */ - DynamicDeviceLibrary(const std::string& libName, const std::string& prefix); - - /** - * @brief Destructor for the DynamicDeviceLibrary. - * @details This destructor calls the @ref QDMI_device_finalize function if it - * is not null and closes the dynamic library. - */ - ~DynamicDeviceLibrary() override; -}; - -/** - * @brief The status of a session. - * @details This enum defines the possible states of a session in the QDMI - * library. A session can be either allocated or initialized. - */ -enum class SessionStatus : uint8_t { - ALLOCATED, ///< The session has been allocated but not initialized - INITIALIZED ///< The session has been initialized and is ready for use -}; -} // namespace qdmi - -/** - * @brief Definition of the QDMI Device. - */ -struct QDMI_Device_impl_d { -private: - /** - * @brief The device library that provides the device interface functions. - * @note This must be a pointer type as we need access to dynamic and static - * libraries that are subclasses of qdmi::DeviceLibrary. - */ - std::shared_ptr library_; - /// @brief The device session handle. - QDMI_Device_Session deviceSession_ = nullptr; - /// Client-facing wrappers for direct child devices. - std::vector> childDevices_; - /** - * @brief Map of jobs to their corresponding unique pointers of - * QDMI_Job_impl_d objects. - */ - std::unordered_map> jobs_; - -public: - /** - * @brief Constructs a top-level QDMI device from an exclusively owned - * library. - * @param lib is the device library to take ownership of. - * @param config is the configuration for device session parameters. - */ - explicit QDMI_Device_impl_d(std::unique_ptr&& lib, - const qdmi::DeviceSessionConfig& config = {}) - : QDMI_Device_impl_d(std::shared_ptr(std::move(lib)), config) {} - - /** - * @brief Constructor for the QDMI device. - * @details This constructor initializes the device session and allocates - * the device session handle. - * @param lib is a shared pointer to the device library that provides the - * device interface functions. - * @param config is the configuration for device session parameters. - * @param childDevice optionally selects a child device for this wrapper. - */ - explicit QDMI_Device_impl_d(std::shared_ptr lib, - const qdmi::DeviceSessionConfig& config = {}, - QDMI_Child_Device childDevice = nullptr); - - /** - * @brief Destructor for the QDMI device. - * @details This destructor frees the device session and clears the jobs map. - */ - ~QDMI_Device_impl_d() { - jobs_.clear(); - childDevices_.clear(); - if (library_ && deviceSession_ != nullptr) { - library_->device_session_free(deviceSession_); - } - } - - /// @returns the library with the device interface functions pointers. - [[nodiscard]] auto getLibrary() const -> const qdmi::DeviceLibrary& { - return *library_; - } - - /** - * @brief Creates a job for the device. - * @see QDMI_device_create_job - */ - auto createJob(QDMI_Job* job) -> int; - - /** - * @brief Retrieves an existing job by its ID. - * @see QDMI_session_retrieve_job_by_id - */ - auto retrieveJobById(const char* jobId, QDMI_Job* job) -> int; - - /** - * @brief Frees the job associated with the device. - * @see QDMI_job_free - */ - auto freeJob(QDMI_Job job) -> void; - - /** - * @brief Queries a device property. - * @see QDMI_device_query_device_property - */ - auto queryDeviceProperty(QDMI_Device_Property prop, size_t size, void* value, - size_t* sizeRet) const -> int; - - /** - * @brief Queries a site property. - * @see QDMI_device_query_site_property - */ - auto querySiteProperty(QDMI_Site site, QDMI_Site_Property prop, size_t size, - void* value, size_t* sizeRet) const -> int; - - /** - * @brief Queries an operation property. - * @see QDMI_device_query_operation_property - */ - auto queryOperationProperty(QDMI_Operation operation, size_t numSites, - const QDMI_Site* sites, size_t numParams, - const double* params, - QDMI_Operation_Property prop, size_t size, - void* value, size_t* sizeRet) const -> int; -}; - -/** - * @brief Definition of the QDMI Job. - */ -struct QDMI_Job_impl_d { -private: - /// @brief The device job handle. - QDMI_Device_Job deviceJob_ = nullptr; - /// @brief The device associated with the job. - QDMI_Device device_ = nullptr; - -public: - /** - * @brief Constructor for the QDMI job. - * @details This constructor initializes the job with the device job handle - * and the device library. - * @param deviceJob is the handle to the device job. - * @param device is the device associated with the job. - */ - explicit QDMI_Job_impl_d(QDMI_Device_Job deviceJob, QDMI_Device device) - : deviceJob_(deviceJob), device_(device) {} - - /** - * @brief Destructor for the QDMI job. - * @details This destructor frees the device job handle using the - * @ref QDMI_device_job_free function from the device library. - */ - ~QDMI_Job_impl_d(); - - /** - * @brief Sets a parameter for the job. - * @see QDMI_job_set_parameter - */ - auto setParameter(QDMI_Job_Parameter param, size_t size, - const void* value) const -> int; - - /** - * @brief Queries a property of the job. - * @see QDMI_job_query_property - */ - auto queryProperty(QDMI_Job_Property prop, size_t size, void* value, - size_t* sizeRet) const -> int; - - /** - * @brief Submits the job to the device. - * @see QDMI_job_submit - */ - [[nodiscard]] auto submit() const -> int; - - /** - * @brief Cancels the job. - * @see QDMI_job_cancel - */ - [[nodiscard]] auto cancel() const -> int; - - /** - * @brief Checks the status of the job. - * @see QDMI_job_check - */ - auto check(QDMI_Job_Status* status) const -> int; - - /** - * @brief Waits for the job to complete but at most for the specified - * timeout. - * @see QDMI_job_wait - */ - [[nodiscard]] auto wait(size_t timeout) const -> int; - - /** - * @brief Gets the results of the job. - * @see QDMI_job_get_results - */ - auto getResults(QDMI_Job_Result result, size_t size, void* data, - size_t* sizeRet) const -> int; - - /** - * @brief Frees the job. - * @note This function just forwards to the device's @ref - * QDMI_Device_impl_d::freeJob function. This function is needed because the - * interface only provides the job handle to the @ref QDMI_job_free function - * and the job's device handle is private. - */ - auto free() -> void; -}; - -/** - * @brief Definition of the QDMI Session. - */ -struct QDMI_Session_impl_d { -private: - /// @brief The status of the session. - qdmi::SessionStatus status_ = qdmi::SessionStatus::ALLOCATED; - /// @brief Snapshot of devices visible when this session was allocated. - std::vector devices_; - -public: - /// @brief Constructor for the QDMI session. - explicit QDMI_Session_impl_d( - const std::vector>& devices); - - /// @brief Constructor from an explicit device-handle snapshot. - explicit QDMI_Session_impl_d(const std::vector& devices); - - /** - * @brief Initializes the session. - * @see QDMI_session_init - */ - auto init() -> int; - - /** - * @brief Sets a parameter for the session. - * @see QDMI_session_set_parameter - */ - auto setParameter(QDMI_Session_Parameter param, size_t size, - const void* value) const -> int; - - /** - * @brief Queries a session property. - * @see QDMI_session_query_session_property - */ - auto querySessionProperty(QDMI_Session_Property prop, size_t size, - void* value, size_t* sizeRet) const -> int; -}; - -namespace qdmi { -/** - * @brief The MQT QDMI driver class. - * @details This driver discovers configured QDMI device definitions and opens - * their libraries. Additional definitions can be registered at runtime. - * @note This class is a singleton that manages the QDMI libraries and - * sessions. It is responsible for loading the libraries, allocating sessions, - * and providing access to the devices. - */ -class Driver final : public Singleton { - friend class Singleton; - friend class Session; - - /// @brief Private constructor to enforce the singleton pattern. - Driver(); - - /** - * @brief Vector of unique pointers to QDMI_Device_impl_d objects. - */ - std::vector> devices_; - - /// @brief Registered definitions in stable registration order. - std::vector definitions_; - - /// @brief IDs disabled by the highest-precedence configuration source. - std::unordered_set disabledDeviceIds_; - - /// @brief Initially discovered definitions visible to the client API. - std::vector clientDefinitionIds_; - - /// @brief Materialized devices exposed through the client API. - std::vector clientDevices_; - - /// @brief Whether the configured client device catalog has been opened. - bool clientCatalogMaterialized_ = false; - - /// @brief Opened devices indexed by their stable registration ID. - std::unordered_map openedDevices_; - - /** - * @brief Map of sessions to their corresponding unique pointers to - * QDMI_Session_impl_d objects. - */ - std::unordered_map> - sessions_; - - /// Opens the initially configured definitions for the client API. - void materializeClientCatalog(); - - /// Opens a fresh device session with per-call overrides. - auto openFresh(std::string_view id, const DeviceSessionConfig& overrides) - -> std::shared_ptr; - -public: - /** - * @returns the process-wide Driver instance. - * @details This out-of-line accessor keeps static-library consumers from - * instantiating separate singleton storage in different translation units. - */ - [[nodiscard]] static auto get() -> Driver&; - - /** - * @brief Registers a device definition without loading its library. - * @param definition The definition to validate and store. - * @param replace Whether an existing unopened definition may be replaced. - * @throws std::invalid_argument If the definition is incomplete or its ID is - * already registered. - * @throws std::runtime_error If replacing an already opened definition. - */ - void registerDevice(DeviceDefinition definition, bool replace = false); - - /** - * @brief Registers a device definition unless its ID is already present. - * @param definition The definition to validate and store. - * @returns Whether the definition was inserted. - * @throws std::invalid_argument If the definition is incomplete. - * @details Existing and explicitly disabled IDs are not inserted. The - * complete definition is validated before checking for either condition. - */ - auto registerDeviceIfAbsent(DeviceDefinition definition) -> bool; - - /** - * @brief Lists the stable IDs of all registered devices. - * @returns The enabled device IDs in deterministic registration order. - * @details This query includes runtime registrations and does not load device - * libraries or expose their definitions. - */ - [[nodiscard]] auto registeredDeviceIds() const -> std::vector; - - /** - * @brief Opens the registered device with the given stable ID. - * @returns The existing device handle when the ID is already open. - * @throws std::out_of_range If the ID is unknown. - * @throws std::runtime_error If loading or session initialization fails. - */ - auto open(std::string_view id) -> QDMI_Device; - - /** - * @brief Allocates a new session. - * @see QDMI_session_alloc - */ - auto sessionAlloc(QDMI_Session* session) -> int; - - /** - * @brief Frees a session. - * @see QDMI_session_free - */ - auto sessionFree(QDMI_Session session) -> void; -}; - -} // namespace qdmi diff --git a/include/mqt-core/qdmi/driver/SessionConfig.hpp b/include/mqt-core/qdmi/driver/SessionConfig.hpp index ff39003390..0dcd702bf0 100644 --- a/include/mqt-core/qdmi/driver/SessionConfig.hpp +++ b/include/mqt-core/qdmi/driver/SessionConfig.hpp @@ -9,59 +9,9 @@ */ /** @file SessionConfig.hpp - * @brief Construction helpers for QDMI device session configuration. + * @brief Compatibility include for QDMI session configuration. */ #pragma once -#include "qdmi/driver/Driver.hpp" - -#include -#include -#include -#include -#include - -namespace qdmi { - -/** - * @brief Construct a device session configuration from individual parameters. - * @throws std::invalid_argument If both an inline device configuration and a - * device configuration file are set. - */ -[[nodiscard]] inline auto makeDeviceSessionConfig( - std::optional baseUrl, std::optional token, - std::optional authFile, - std::optional authUrl, std::optional username, - std::optional password, - std::optional deviceConfig, - std::optional deviceConfigFile, - std::optional custom1, std::optional custom2, - std::optional custom3, std::optional custom4, - std::optional custom5) -> DeviceSessionConfig { - if (deviceConfig && deviceConfigFile) { - throw std::invalid_argument( - "device_config and device_config_file are mutually exclusive"); - } - std::optional configuration; - if (deviceConfig) { - configuration = InlineDeviceConfiguration{.json = std::move(*deviceConfig)}; - } else if (deviceConfigFile) { - configuration = - FileDeviceConfiguration{.path = std::move(*deviceConfigFile)}; - } - return {.baseUrl = std::move(baseUrl), - .token = std::move(token), - .authFile = std::move(authFile), - .authUrl = std::move(authUrl), - .username = std::move(username), - .password = std::move(password), - .deviceConfiguration = std::move(configuration), - .custom1 = std::move(custom1), - .custom2 = std::move(custom2), - .custom3 = std::move(custom3), - .custom4 = std::move(custom4), - .custom5 = std::move(custom5)}; -} - -} // namespace qdmi +#include "qdmi/SessionConfig.hpp" diff --git a/mlir/lib/Compiler/QDMIAdapter.cpp b/mlir/lib/Compiler/QDMIAdapter.cpp index d6fa20e2a7..f6faf6d500 100644 --- a/mlir/lib/Compiler/QDMIAdapter.cpp +++ b/mlir/lib/Compiler/QDMIAdapter.cpp @@ -11,8 +11,9 @@ #include "mlir/Compiler/QDMIAdapter.h" #include "mlir/Compiler/Target.h" -#include "qdmi/Client.hpp" -#include "qdmi/driver/Driver.hpp" +#include "qdmi/Device.hpp" +#include "qdmi/DeviceManager.hpp" +#include "qdmi/DeviceRegistry.hpp" #include #include @@ -414,7 +415,7 @@ compilerTargetFromDeviceId(const std::string_view deviceId) { const auto action = std::string("Failed to open or query QDMI device '") + std::string(deviceId) + "'"; try { - return snapshotCompilerTarget(qdmi::Session::openDevice(deviceId)); + return snapshotCompilerTarget(qdmi::openDevice(deviceId)); } catch (...) { return qdmiError(action, std::current_exception()); } @@ -422,7 +423,7 @@ compilerTargetFromDeviceId(const std::string_view deviceId) { llvm::Expected> registeredQDMIDeviceIds() { try { - return qdmi::Driver::get().registeredDeviceIds(); + return qdmi::registeredDeviceIds(); } catch (...) { return qdmiError("Failed to discover registered QDMI devices", std::current_exception()); diff --git a/mlir/unittests/Compiler/CMakeLists.txt b/mlir/unittests/Compiler/CMakeLists.txt index 92748f0137..8a3b13945a 100644 --- a/mlir/unittests/Compiler/CMakeLists.txt +++ b/mlir/unittests/Compiler/CMakeLists.txt @@ -6,8 +6,11 @@ # # Licensed under the MIT License -add_executable(mqt-core-mlir-unittests-compiler - test_compiler_pipeline.cpp test_compiler_qdmi_adapter.cpp test_compiler_target.cpp) +set(compiler_test_sources test_compiler_pipeline.cpp test_compiler_target.cpp) +if(TARGET MQT::CoreQDMIScDevice AND TARGET MQT::CoreQDMI_DDSIM_Device) + list(APPEND compiler_test_sources test_compiler_qdmi_adapter.cpp) +endif() +add_executable(mqt-core-mlir-unittests-compiler ${compiler_test_sources}) target_link_libraries( mqt-core-mlir-unittests-compiler @@ -25,15 +28,17 @@ target_link_libraries( MLIRQCOPrograms MLIRQIRPrograms) -mqt_copy_qdmi_runtime(mqt-core-mlir-unittests-compiler MQT::CoreQDMIScDevice - MQT::CoreQDMI_DDSIM_Device) -target_compile_definitions( - mqt-core-mlir-unittests-compiler - PRIVATE - MQT_CORE_MLIR_HETEROGENEOUS_SC_CONFIG="${CMAKE_CURRENT_SOURCE_DIR}/Inputs/heterogeneous-sc.json" - MQT_CORE_MLIR_DIRECTIONAL_ONE_WAY_SC_CONFIG="${CMAKE_CURRENT_SOURCE_DIR}/Inputs/directional-one-way-sc.json" - MQT_CORE_MLIR_DIRECTIONAL_TWO_WAY_SC_CONFIG="${CMAKE_CURRENT_SOURCE_DIR}/Inputs/directional-two-way-sc.json" -) +if(TARGET MQT::CoreQDMIScDevice AND TARGET MQT::CoreQDMI_DDSIM_Device) + mqt_copy_qdmi_runtime(mqt-core-mlir-unittests-compiler MQT::CoreQDMIScDevice + MQT::CoreQDMI_DDSIM_Device) + target_compile_definitions( + mqt-core-mlir-unittests-compiler + PRIVATE + MQT_CORE_MLIR_HETEROGENEOUS_SC_CONFIG="${CMAKE_CURRENT_SOURCE_DIR}/Inputs/heterogeneous-sc.json" + MQT_CORE_MLIR_DIRECTIONAL_ONE_WAY_SC_CONFIG="${CMAKE_CURRENT_SOURCE_DIR}/Inputs/directional-one-way-sc.json" + MQT_CORE_MLIR_DIRECTIONAL_TWO_WAY_SC_CONFIG="${CMAKE_CURRENT_SOURCE_DIR}/Inputs/directional-two-way-sc.json" + ) +endif() mqt_mlir_configure_unittest_target(mqt-core-mlir-unittests-compiler REQUIRES_EH) diff --git a/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp b/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp index 16cebd2727..2c3d447a48 100644 --- a/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp +++ b/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp @@ -10,8 +10,8 @@ #include "mlir/Compiler/QDMIAdapter.h" #include "mlir/Compiler/Target.h" -#include "qdmi/Client.hpp" -#include "qdmi/driver/Driver.hpp" +#include "qdmi/DeviceManager.hpp" +#include "qdmi/SessionConfig.hpp" #include #include @@ -35,7 +35,7 @@ findOperation(const CompilerTarget& target, const llvm::StringRef name) { TEST(CompilerQDMIAdapterTest, SnapshotsIQMCalibrationAndLifetime) { const auto target = llvm::cantFail([] { - const auto device = qdmi::Session::openDevice("mqt.sc.iqm.garnet"); + const auto device = qdmi::openDevice("mqt.sc.iqm.garnet"); return mlir::compilerTargetFromDevice(device); }()); @@ -81,7 +81,7 @@ TEST(CompilerQDMIAdapterTest, SnapshotsIQMCalibrationAndLifetime) { } TEST(CompilerQDMIAdapterTest, PreservesMissingTopologyAsAllToAll) { - const auto device = qdmi::Session::openDevice("mqt.ddsim.default"); + const auto device = qdmi::openDevice("mqt.ddsim.default"); const auto target = llvm::cantFail(mlir::compilerTargetFromDevice(device)); EXPECT_EQ(target.numQubits(), 65535); @@ -109,7 +109,7 @@ TEST(CompilerQDMIAdapterTest, RejectsNonhomogeneousOperationSupport) { qdmi::DeviceSessionConfig overrides; overrides.deviceConfiguration = qdmi::FileDeviceConfiguration{MQT_CORE_MLIR_HETEROGENEOUS_SC_CONFIG}; - const auto device = qdmi::Session::openDevice("mqt.sc.default", overrides); + const auto device = qdmi::openDevice("mqt.sc.default", overrides); auto target = mlir::compilerTargetFromDevice(device); ASSERT_FALSE(target); const auto message = llvm::toString(target.takeError()); @@ -121,7 +121,7 @@ TEST(CompilerQDMIAdapterTest, RejectsDirectionalOperationWithoutReverseSites) { qdmi::DeviceSessionConfig overrides; overrides.deviceConfiguration = qdmi::FileDeviceConfiguration{ MQT_CORE_MLIR_DIRECTIONAL_ONE_WAY_SC_CONFIG}; - const auto device = qdmi::Session::openDevice("mqt.sc.default", overrides); + const auto device = qdmi::openDevice("mqt.sc.default", overrides); auto target = mlir::compilerTargetFromDevice(device); ASSERT_FALSE(target); const auto message = llvm::toString(target.takeError()); @@ -133,7 +133,7 @@ TEST(CompilerQDMIAdapterTest, qdmi::DeviceSessionConfig overrides; overrides.deviceConfiguration = qdmi::FileDeviceConfiguration{ MQT_CORE_MLIR_DIRECTIONAL_TWO_WAY_SC_CONFIG}; - const auto device = qdmi::Session::openDevice("mqt.sc.default", overrides); + const auto device = qdmi::openDevice("mqt.sc.default", overrides); const auto target = llvm::cantFail(mlir::compilerTargetFromDevice(device)); ASSERT_EQ(target.couplings().size(), 1); diff --git a/python/mqt/core/qdmi/driver.pyi b/python/mqt/core/qdmi/driver.pyi index 8e23d0c900..79a1e74224 100644 --- a/python/mqt/core/qdmi/driver.pyi +++ b/python/mqt/core/qdmi/driver.pyi @@ -10,6 +10,8 @@ import os import pathlib +from collections.abc import Sequence +from typing import overload import mqt.core.qdmi @@ -69,16 +71,109 @@ class DeviceDefinition: def prefix(self) -> str: """Prefix used for the QDMI device interface functions.""" +class DeviceRegistry: + """Discover or explicitly register QDMI device definitions.""" + + @overload + def __init__(self) -> None: + """Discover definitions from the standard configuration sources.""" + + @overload + def __init__(self, definitions: Sequence[DeviceDefinition]) -> None: + """Create an isolated registry from explicit definitions.""" + + @property + def definitions(self) -> list[DeviceDefinition]: + """Enabled definitions in stable registration order.""" + + @property + def device_ids(self) -> list[str]: + """Enabled stable device IDs.""" + + def register_device(self, definition: DeviceDefinition, *, replace: bool = False) -> None: + """Register a definition, optionally replacing the same ID.""" + + def register_device_if_absent(self, definition: DeviceDefinition) -> bool: + """Register a fallback unless its ID exists or is disabled.""" + +class OpenAllResult: + """Per-ID successes and failures from opening all registered devices.""" + + @property + def devices(self) -> dict[str, mqt.core.qdmi.Device]: + """Successfully opened devices keyed by stable ID.""" + + @property + def errors(self) -> dict[str, str]: + """Error messages for devices that could not be opened.""" + +class DeviceManager: + """An immutable registry snapshot that opens fresh device sessions.""" + + @overload + def __init__(self) -> None: + """Snapshot the current process default registry.""" + + @overload + def __init__(self, registry: DeviceRegistry) -> None: + """Snapshot an explicit registry.""" + + @property + def definitions(self) -> list[DeviceDefinition]: + """Definitions in this immutable snapshot.""" + + @property + def device_ids(self) -> list[str]: + """Stable IDs in this immutable snapshot.""" + + def open( + self, + device_id: str, + *, + base_url: str | None = None, + token: str | None = None, + auth_file: str | os.PathLike | None = None, + auth_url: str | None = None, + username: str | None = None, + password: str | None = None, + device_config: str | None = None, + device_config_file: str | os.PathLike | None = None, + custom1: str | None = None, + custom2: str | None = None, + custom3: str | None = None, + custom4: str | None = None, + custom5: str | None = None, + ) -> mqt.core.qdmi.Device: + """Open a fresh session for one stable device ID.""" + + def open_all( + self, + *, + base_url: str | None = None, + token: str | None = None, + auth_file: str | os.PathLike | None = None, + auth_url: str | None = None, + username: str | None = None, + password: str | None = None, + device_config: str | None = None, + device_config_file: str | os.PathLike | None = None, + custom1: str | None = None, + custom2: str | None = None, + custom3: str | None = None, + custom4: str | None = None, + custom5: str | None = None, + ) -> OpenAllResult: + """Open all devices and isolate failures by stable ID.""" + def register_device(definition: DeviceDefinition, *, replace: bool = False) -> None: """Register a QDMI device definition without loading its library. Args: definition: Definition to validate and store. - replace: Replace an existing definition if it has not been opened. + replace: Replace an existing definition for future opens. Raises: ValueError: If the definition is invalid or its ID is already registered. - RuntimeError: If replacing an already opened ID. """ def register_device_if_absent(definition: DeviceDefinition) -> bool: diff --git a/src/qdmi/CMakeLists.txt b/src/qdmi/CMakeLists.txt index b177b90aa5..268734f3e1 100644 --- a/src/qdmi/CMakeLists.txt +++ b/src/qdmi/CMakeLists.txt @@ -8,14 +8,14 @@ add_subdirectory(common) add_subdirectory(devices) -add_subdirectory(driver) set(TARGET_NAME ${MQT_CORE_TARGET_NAME}-qdmi) if(NOT TARGET ${TARGET_NAME}) add_mqt_core_library(${TARGET_NAME} ALIAS_NAME QDMI) - target_sources(${TARGET_NAME} PRIVATE Client.cpp Slurm.cpp) + target_sources(${TARGET_NAME} PRIVATE Device.cpp DeviceApi.cpp DeviceManager.cpp + DeviceRegistry.cpp DeviceState.cpp Slurm.cpp) target_sources( ${TARGET_NAME} @@ -25,12 +25,31 @@ if(NOT TARGET ${TARGET_NAME}) ${MQT_CORE_INCLUDE_BUILD_DIR} FILES ${MQT_CORE_INCLUDE_BUILD_DIR}/qdmi/Client.hpp - ${MQT_CORE_INCLUDE_BUILD_DIR}/qdmi/Slurm.hpp) + ${MQT_CORE_INCLUDE_BUILD_DIR}/qdmi/Device.hpp + ${MQT_CORE_INCLUDE_BUILD_DIR}/qdmi/DeviceManager.hpp + ${MQT_CORE_INCLUDE_BUILD_DIR}/qdmi/DeviceRegistry.hpp + ${MQT_CORE_INCLUDE_BUILD_DIR}/qdmi/SessionConfig.hpp + ${MQT_CORE_INCLUDE_BUILD_DIR}/qdmi/Slurm.hpp + ${MQT_CORE_INCLUDE_BUILD_DIR}/qdmi/driver/SessionConfig.hpp) target_link_libraries( ${TARGET_NAME} - PUBLIC qdmi::qdmi MQT::CoreQDMICommon MQT::CoreQDMIDriver - PRIVATE spdlog::spdlog) + PUBLIC qdmi::qdmi MQT::CoreQDMICommon + PRIVATE qdmi::qdmi_project_warnings $ + spdlog::spdlog ${CMAKE_DL_LIBS}) + + mqt_get_qdmi_device_targets(QDMI_DEVICE_TARGETS) + if(QDMI_DEVICE_TARGETS) + if(WIN32) + mqt_copy_qdmi_runtime(${TARGET_NAME} ${QDMI_DEVICE_TARGETS}) + else() + add_dependencies(${TARGET_NAME} ${QDMI_DEVICE_TARGETS}) + foreach(device IN LISTS QDMI_DEVICE_TARGETS) + target_link_options(${TARGET_NAME} INTERFACE + $>) + endforeach() + endif() + endif() list(APPEND MQT_CORE_TARGETS ${TARGET_NAME}) endif() diff --git a/src/qdmi/Client.cpp b/src/qdmi/Client.cpp deleted file mode 100644 index 0e448be08d..0000000000 --- a/src/qdmi/Client.cpp +++ /dev/null @@ -1,953 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "qdmi/Client.hpp" - -#include "qdmi/common/Common.hpp" -#include "qdmi/driver/Driver.hpp" - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace qdmi { -namespace { -/// Rejects the formats that `submitJob` cannot carry. -/// A batch job's program is a list of job handles rather than a byte blob, so -/// this API cannot express it at all. A calibration run has its own entry -/// point, because its payload is optional and it takes no shot count. -void rejectUnsupportedProgramFormat(const QDMI_Program_Format format) { - if (format == QDMI_PROGRAM_FORMAT_BATCHJOB) { - throw std::invalid_argument( - "MQT Core does not support batch jobs. A batch job's program is a list " - "of job handles, which this API cannot express"); - } - if (format == QDMI_PROGRAM_FORMAT_CALIBRATION) { - throw std::invalid_argument( - "Use submitCalibrationJob (submit_calibration_job in Python) to " - "trigger a calibration run"); - } -} -} // namespace - -size_t Site::getIndex() const { - return queryProperty(QDMI_SITE_PROPERTY_INDEX); -} -std::optional Site::getT1() const { - return queryProperty>(QDMI_SITE_PROPERTY_T1); -} -std::optional Site::getT2() const { - return queryProperty>(QDMI_SITE_PROPERTY_T2); -} -std::optional Site::getName() const { - return queryProperty>(QDMI_SITE_PROPERTY_NAME); -} -std::optional Site::getXCoordinate() const { - return queryProperty>(QDMI_SITE_PROPERTY_XCOORDINATE); -} -std::optional Site::getYCoordinate() const { - return queryProperty>(QDMI_SITE_PROPERTY_YCOORDINATE); -} -std::optional Site::getZCoordinate() const { - return queryProperty>(QDMI_SITE_PROPERTY_ZCOORDINATE); -} -bool Site::isZone() const { - return queryProperty>(QDMI_SITE_PROPERTY_ISZONE) - .value_or(false); -} -std::optional Site::getXExtent() const { - return queryProperty>(QDMI_SITE_PROPERTY_XEXTENT); -} -std::optional Site::getYExtent() const { - return queryProperty>(QDMI_SITE_PROPERTY_YEXTENT); -} -std::optional Site::getZExtent() const { - return queryProperty>(QDMI_SITE_PROPERTY_ZEXTENT); -} -std::optional Site::getModuleIndex() const { - return queryProperty>(QDMI_SITE_PROPERTY_MODULEINDEX); -} -std::optional Site::getSubmoduleIndex() const { - return queryProperty>( - QDMI_SITE_PROPERTY_SUBMODULEINDEX); -} -std::string Operation::getName(const std::vector& sites, - const std::vector& params) const { - return queryProperty(QDMI_OPERATION_PROPERTY_NAME, sites, - params); -} -std::optional -Operation::getQubitsNum(const std::vector& sites, - const std::vector& params) const { - return queryProperty>(QDMI_OPERATION_PROPERTY_QUBITSNUM, - sites, params); -} -size_t Operation::getParametersNum(const std::vector& sites, - const std::vector& params) const { - return queryProperty(QDMI_OPERATION_PROPERTY_PARAMETERSNUM, sites, - params); -} -std::optional -Operation::getDuration(const std::vector& sites, - const std::vector& params) const { - return queryProperty>( - QDMI_OPERATION_PROPERTY_DURATION, sites, params); -} -std::optional -Operation::getFidelity(const std::vector& sites, - const std::vector& params) const { - return queryProperty>(QDMI_OPERATION_PROPERTY_FIDELITY, - sites, params); -} -std::optional -Operation::getInteractionRadius(const std::vector& sites, - const std::vector& params) const { - return queryProperty>( - QDMI_OPERATION_PROPERTY_INTERACTIONRADIUS, sites, params); -} -std::optional -Operation::getBlockingRadius(const std::vector& sites, - const std::vector& params) const { - return queryProperty>( - QDMI_OPERATION_PROPERTY_BLOCKINGRADIUS, sites, params); -} -std::optional -Operation::getIdlingFidelity(const std::vector& sites, - const std::vector& params) const { - return queryProperty>( - QDMI_OPERATION_PROPERTY_IDLINGFIDELITY, sites, params); -} -bool Operation::isZoned() const { - return queryProperty>(QDMI_OPERATION_PROPERTY_ISZONED, {}, - {}) - .value_or(false); -} -std::optional> Operation::getSites() const { - const auto& qdmiSites = queryProperty>>( - QDMI_OPERATION_PROPERTY_SITES, {}, {}); - if (!qdmiSites.has_value()) { - return std::nullopt; - } - std::vector returnedSites; - returnedSites.reserve(qdmiSites->size()); - std::ranges::transform( - *qdmiSites, std::back_inserter(returnedSites), - [this](const QDMI_Site& site) -> Site { return {device_, site}; }); - return returnedSites; -} -std::optional>> -Operation::getSitePairs() const { - if (const auto qubitsNum = getQubitsNum({}, {}); - !qubitsNum.has_value() || *qubitsNum != 2 || isZoned()) { - return std::nullopt; // Not a 2-qubit operation or operation is zoned - } - - const auto sitesOpt = getSites(); - if (!sitesOpt.has_value()) { - return std::nullopt; - } - - const auto& sitesVec = *sitesOpt; - if (sitesVec.empty() || sitesVec.size() % 2 != 0) { - return std::nullopt; // Invalid: no sites or odd number of sites - } - - std::vector> pairs; - pairs.reserve(sitesVec.size() / 2); - - for (size_t i = 0; i < sitesVec.size(); i += 2) { - pairs.emplace_back(sitesVec[i], sitesVec[i + 1]); - } - - return pairs; -} -std::optional -Operation::getMeanShuttlingSpeed(const std::vector& sites, - const std::vector& params) const { - return queryProperty>( - QDMI_OPERATION_PROPERTY_MEANSHUTTLINGSPEED, sites, params); -} -std::string Device::getName() const { - return queryProperty(QDMI_DEVICE_PROPERTY_NAME); -} - -std::string Device::getVersion() const { - return queryProperty(QDMI_DEVICE_PROPERTY_VERSION); -} - -QDMI_Device_Status Device::getStatus() const { - return queryProperty(QDMI_DEVICE_PROPERTY_STATUS); -} - -std::string Device::getLibraryVersion() const { - return queryProperty(QDMI_DEVICE_PROPERTY_LIBRARYVERSION); -} - -size_t Device::getQubitsNum() const { - return queryProperty(QDMI_DEVICE_PROPERTY_QUBITSNUM); -} - -std::vector Device::getSites() const { - const auto& qdmiSites = - queryProperty>(QDMI_DEVICE_PROPERTY_SITES); - std::vector sites; - sites.reserve(qdmiSites.size()); - std::ranges::transform( - qdmiSites, std::back_inserter(sites), - [this](const QDMI_Site& site) -> Site { return {device_, site}; }); - return sites; -} - -std::vector Device::getRegularSites() const { - auto allSites = getSites(); - const auto newEnd = std::ranges::remove_if( - allSites, [](const auto& s) { return s.isZone(); }); - allSites.erase(newEnd.begin(), newEnd.end()); - return allSites; -} - -std::vector Device::getZones() const { - const auto& allSites = getSites(); - std::vector zones; - zones.reserve(3); // Reserve space for a typical max number of zones - std::ranges::copy_if(allSites, std::back_inserter(zones), - [](const auto& s) { return s.isZone(); }); - return zones; -} - -std::vector Device::getOperations() const { - const auto& qdmiOperations = queryProperty>( - QDMI_DEVICE_PROPERTY_OPERATIONS); - return wrapOperations(qdmiOperations); -} - -std::optional> -Device::queryCustomOperations(const CustomProperty property) const { - const auto qdmiProperty = detail::toDeviceProperty(property); - const auto handles = detail::queryHandleArray( - [this, qdmiProperty](const size_t size, void* value, size_t* sizeRet) { - return QDMI_device_query_device_property(device_.get(), qdmiProperty, - size, value, sizeRet); - }, - "custom operation list " + - std::to_string(static_cast(property))); - if (!handles.has_value()) { - return std::nullopt; - } - return wrapOperations(*handles); -} - -std::vector -Device::wrapOperations(const std::span operations) const { - std::vector wrappedOperations; - wrappedOperations.reserve(operations.size()); - std::ranges::transform( - operations, std::back_inserter(wrappedOperations), - [this](const QDMI_Operation& op) -> Operation { return {device_, op}; }); - return wrappedOperations; -} - -std::optional>> -Device::getCouplingMap() const { - const auto& qdmiCouplingMap = queryProperty< - std::optional>>>( - QDMI_DEVICE_PROPERTY_COUPLINGMAP); - if (!qdmiCouplingMap.has_value()) { - return std::nullopt; - } - - std::vector> couplingMap; - couplingMap.reserve(qdmiCouplingMap->size()); - std::ranges::transform(*qdmiCouplingMap, std::back_inserter(couplingMap), - [this](const std::pair& pair) - -> std::pair { - return { - Site{device_, pair.first}, - Site{device_, pair.second}, - }; - }); - return couplingMap; -} - -std::optional Device::getNeedsCalibration() const { - return queryProperty>( - QDMI_DEVICE_PROPERTY_NEEDSCALIBRATION); -} - -std::optional Device::getQueueLength() const { - return queryProperty>(QDMI_DEVICE_PROPERTY_QUEUELENGTH); -} - -std::optional Device::getLengthUnit() const { - return queryProperty>( - QDMI_DEVICE_PROPERTY_LENGTHUNIT); -} - -std::optional Device::getLengthScaleFactor() const { - return queryProperty>( - QDMI_DEVICE_PROPERTY_LENGTHSCALEFACTOR); -} - -std::optional Device::getDurationUnit() const { - return queryProperty>( - QDMI_DEVICE_PROPERTY_DURATIONUNIT); -} - -std::optional Device::getDurationScaleFactor() const { - return queryProperty>( - QDMI_DEVICE_PROPERTY_DURATIONSCALEFACTOR); -} - -std::optional Device::getMinAtomDistance() const { - return queryProperty>( - QDMI_DEVICE_PROPERTY_MINATOMDISTANCE); -} - -std::vector Device::getSupportedProgramFormats() const { - return queryProperty>( - QDMI_DEVICE_PROPERTY_SUPPORTEDPROGRAMFORMATS); -} - -std::vector Device::getChildDevices() const { - size_t size = 0; - auto result = QDMI_device_query_device_property( - device_.get(), QDMI_DEVICE_PROPERTY_CHILDDEVICES, 0, nullptr, &size); - if (result == QDMI_ERROR_NOTSUPPORTED) { - return {}; - } - qdmi::throwIfError(result, "Querying child devices size"); - if (size % sizeof(QDMI_Device) != 0) { - throw std::runtime_error("Invalid child device list size"); - } - - std::vector handles(size / sizeof(QDMI_Device)); - if (size != 0) { - result = QDMI_device_query_device_property( - device_.get(), QDMI_DEVICE_PROPERTY_CHILDDEVICES, size, - static_cast(handles.data()), nullptr); - qdmi::throwIfError(result, "Querying child devices"); - } - - std::vector devices; - devices.reserve(handles.size()); - std::ranges::transform( - handles, std::back_inserter(devices), - [this](QDMI_Device_impl_d* const handle) { - return Device(std::shared_ptr(device_, handle)); - }); - return devices; -} - -Job Device::submitJob(const std::string& program, - const QDMI_Program_Format format, const size_t numShots, - const std::optional& custom1, - const std::optional& custom2, - const std::optional& custom3, - const std::optional& custom4, - const std::optional& custom5) const { - if (isBinaryProgramFormat(format)) { - throw std::invalid_argument( - "Binary program formats require exact-byte submission"); - } - rejectUnsupportedProgramFormat(format); - - const auto bytes = std::as_bytes( - std::span(program.c_str(), static_cast(program.size() + 1))); - return submitJob(bytes, format, numShots, custom1, custom2, custom3, custom4, - custom5); -} - -Job Device::submitJob(const std::span program, - const QDMI_Program_Format format, const size_t numShots, - const std::optional& custom1, - const std::optional& custom2, - const std::optional& custom3, - const std::optional& custom4, - const std::optional& custom5) const { - rejectUnsupportedProgramFormat(format); - - return submitJobImpl(format, program, numShots, custom1, custom2, custom3, - custom4, custom5); -} - -Job Device::submitJobImpl( - const QDMI_Program_Format format, - const std::optional> program, - const std::optional numShots, - const std::optional& custom1, - const std::optional& custom2, - const std::optional& custom3, - const std::optional& custom4, - const std::optional& custom5) const { - QDMI_Job job = nullptr; - qdmi::throwIfError(QDMI_device_create_job(device_.get(), &job), - "Creating job"); - Job jobWrapper{job, device_}; - - qdmi::throwIfError(QDMI_job_set_parameter(jobWrapper, - QDMI_JOB_PARAMETER_PROGRAMFORMAT, - sizeof(format), &format), - "Setting program format"); - if (program.has_value()) { - qdmi::throwIfError(QDMI_job_set_parameter(jobWrapper, - QDMI_JOB_PARAMETER_PROGRAM, - program->size(), program->data()), - "Setting program"); - } - if (numShots.has_value()) { - qdmi::throwIfError(QDMI_job_set_parameter(jobWrapper, - QDMI_JOB_PARAMETER_SHOTSNUM, - sizeof(*numShots), &*numShots), - "Setting number of shots"); - } - - if (custom1.has_value()) { - setCustomJobParam(jobWrapper, QDMI_JOB_PARAMETER_CUSTOM1, *custom1); - } - if (custom2.has_value()) { - setCustomJobParam(jobWrapper, QDMI_JOB_PARAMETER_CUSTOM2, *custom2); - } - if (custom3.has_value()) { - setCustomJobParam(jobWrapper, QDMI_JOB_PARAMETER_CUSTOM3, *custom3); - } - if (custom4.has_value()) { - setCustomJobParam(jobWrapper, QDMI_JOB_PARAMETER_CUSTOM4, *custom4); - } - if (custom5.has_value()) { - setCustomJobParam(jobWrapper, QDMI_JOB_PARAMETER_CUSTOM5, *custom5); - } - - qdmi::throwIfError(QDMI_job_submit(jobWrapper), "Submitting job"); - return jobWrapper; -} - -Job Device::submitCalibrationJob( - const std::optional> program, - const std::optional& custom1, - const std::optional& custom2, - const std::optional& custom3, - const std::optional& custom4, - const std::optional& custom5) const { - const auto payload = - program.has_value() && !program->empty() ? program : std::nullopt; - return submitJobImpl(QDMI_PROGRAM_FORMAT_CALIBRATION, payload, std::nullopt, - custom1, custom2, custom3, custom4, custom5); -} - -Job Device::submitCalibrationJob( - const std::string& program, - const std::optional& custom1, - const std::optional& custom2, - const std::optional& custom3, - const std::optional& custom4, - const std::optional& custom5) const { - const auto bytes = std::as_bytes( - std::span(program.c_str(), static_cast(program.size() + 1))); - return submitCalibrationJob(bytes, custom1, custom2, custom3, custom4, - custom5); -} - -Job Device::retrieveJobById(const std::string_view jobId) const { - const std::string id{jobId}; - QDMI_Job job = nullptr; - qdmi::throwIfError( - QDMI_session_retrieve_job_by_id(device_.get(), id.c_str(), &job), - "Retrieving job"); - return Job{job, device_}; -} - -void Device::setCustomJobParam(QDMI_Job job, const QDMI_Job_Parameter param, - const CustomJobParameter& value) { - std::visit( - [&](const CustomValue& customValue) { - using T = std::decay_t; - if constexpr (std::is_same_v) { - qdmi::throwIfError(QDMI_job_set_parameter(job, param, - customValue.size() + 1, - customValue.c_str()), - "Setting custom parameter"); - } else { - static_assert(std::is_trivially_copyable_v, - "Custom job parameters must be trivially copyable"); - qdmi::throwIfError( - QDMI_job_set_parameter(job, param, sizeof(T), &customValue), - "Setting custom parameter"); - } - }, - value); -} - -QDMI_Job_Status Job::check() const { - QDMI_Job_Status status{}; - qdmi::throwIfError(QDMI_job_check(job_.get(), &status), - "Checking job status"); - return status; -} - -bool Job::wait(const size_t timeout) const { - const auto ret = QDMI_job_wait(job_.get(), timeout); - if (ret == QDMI_SUCCESS) { - return true; - } - if (ret == QDMI_ERROR_TIMEOUT) { - return false; - } - qdmi::throwIfError(ret, "Waiting for job"); - qdmi::unreachable(); -} - -void Job::cancel() const { - qdmi::throwIfError(QDMI_job_cancel(job_.get()), "Cancelling job"); -} - -auto Job::operator=(Job&& other) noexcept -> Job& { - if (this != &other) { - // Release the current job while its owning device session is still alive. - job_.reset(); - device_ = std::move(other.device_); - job_ = std::move(other.job_); - } - return *this; -} - -std::string Job::getId() const { - size_t size = 0; - qdmi::throwIfError(QDMI_job_query_property(job_.get(), QDMI_JOB_PROPERTY_ID, - 0, nullptr, &size), - "Querying job ID size"); - std::string id(size - 1, '\0'); - qdmi::throwIfError(QDMI_job_query_property(job_.get(), QDMI_JOB_PROPERTY_ID, - size, id.data(), nullptr), - "Querying job ID"); - return id; -} - -QDMI_Program_Format Job::getProgramFormat() const { - QDMI_Program_Format format{}; - qdmi::throwIfError(QDMI_job_query_property(job_.get(), - QDMI_JOB_PROPERTY_PROGRAMFORMAT, - sizeof(format), &format, nullptr), - "Querying program format"); - return format; -} - -std::vector Job::getProgramBytes() const { - size_t size = 0; - qdmi::throwIfError(QDMI_job_query_property(job_.get(), - QDMI_JOB_PROPERTY_PROGRAM, 0, - nullptr, &size), - "Querying program size"); - - std::vector program(size); - if (size != 0) { - qdmi::throwIfError(QDMI_job_query_property(job_.get(), - QDMI_JOB_PROPERTY_PROGRAM, size, - program.data(), nullptr), - "Querying program"); - } - return program; -} - -std::string Job::getProgram() const { - const auto format = getProgramFormat(); - if (isBinaryProgramFormat(format)) { - throw std::invalid_argument( - "Cannot decode a binary program as a string; use getProgramBytes()"); - } - - const auto program = getProgramBytes(); - if (program.empty() || program.back() != std::byte{0}) { - throw std::invalid_argument( - "Cannot decode program as a null-terminated string; use " - "getProgramBytes() for binary payloads"); - } - return {reinterpret_cast(program.data()), program.size() - 1}; -} - -size_t Job::getNumShots() const { - size_t numShots = 0; - qdmi::throwIfError( - QDMI_job_query_property(job_.get(), QDMI_JOB_PROPERTY_SHOTSNUM, - sizeof(numShots), &numShots, nullptr), - "Querying number of shots"); - return numShots; -} - -std::optional Job::getQueuePosition() const { - size_t queuePosition = 0; - const auto result = - QDMI_job_query_property(job_.get(), QDMI_JOB_PROPERTY_QUEUEPOSITION, - sizeof(queuePosition), &queuePosition, nullptr); - return detail::queuePositionFromResult(result, queuePosition); -} - -std::vector Job::getShots() const { - size_t shotsSize = 0; - qdmi::throwIfError(QDMI_job_get_results(job_.get(), QDMI_JOB_RESULT_SHOTS, 0, - nullptr, &shotsSize), - "Querying shots size"); - - if (shotsSize == 0) { - return {}; - } - - std::string shots(shotsSize - 1, '\0'); - qdmi::throwIfError(QDMI_job_get_results(job_.get(), QDMI_JOB_RESULT_SHOTS, - shotsSize, shots.data(), nullptr), - "Querying shots"); - - // Parse the shots (comma-separated) - std::vector shotsVec; - const auto numShots = getNumShots(); - shotsVec.reserve(numShots); - std::istringstream shotsStream(shots); - std::string shot; - while (std::getline(shotsStream, shot, ',')) { - shotsVec.emplace_back(shot); - } - if (shotsVec.size() != numShots) { - throw std::runtime_error("Number of shots mismatch"); - } - - return shotsVec; -} - -std::map Job::getCounts() const { - // Get the histogram keys - size_t keysSize = 0; - qdmi::throwIfError(QDMI_job_get_results(job_.get(), QDMI_JOB_RESULT_HIST_KEYS, - 0, nullptr, &keysSize), - "Querying histogram keys size"); - - if (keysSize == 0) { - return {}; // Empty histogram - } - - std::string keys(keysSize - 1, '\0'); - qdmi::throwIfError(QDMI_job_get_results(job_.get(), QDMI_JOB_RESULT_HIST_KEYS, - keysSize, keys.data(), nullptr), - "Querying histogram keys"); - - // Get the histogram values - size_t valuesSize = 0; - qdmi::throwIfError(QDMI_job_get_results(job_.get(), - QDMI_JOB_RESULT_HIST_VALUES, 0, - nullptr, &valuesSize), - "Querying histogram values size"); - - if (valuesSize % sizeof(size_t) != 0) { - throw std::runtime_error( - "Invalid histogram values size: not a multiple of size_t"); - } - - std::vector values(valuesSize / sizeof(size_t)); - qdmi::throwIfError(QDMI_job_get_results(job_.get(), - QDMI_JOB_RESULT_HIST_VALUES, - valuesSize, values.data(), nullptr), - "Querying histogram values"); - - // Parse the keys (comma-separated) - std::map counts; - std::istringstream keysStream(keys); - std::string key; - size_t idx = 0; - while (std::getline(keysStream, key, ',')) { - if (idx < values.size()) { - counts[key] = values[idx]; - ++idx; - } - } - - if (idx != values.size()) { - throw std::runtime_error("Histogram key/value count mismatch"); - } - - return counts; -} - -std::vector> Job::getDenseStateVector() const { - size_t size = 0; - qdmi::throwIfError(QDMI_job_get_results(job_.get(), - QDMI_JOB_RESULT_STATEVECTOR_DENSE, 0, - nullptr, &size), - "Querying dense state vector size"); - - if (size % sizeof(std::complex) != 0) { - throw std::runtime_error( - "Invalid state vector size: not a multiple of complex"); - } - - std::vector> stateVector(size / - sizeof(std::complex)); - qdmi::throwIfError(QDMI_job_get_results(job_.get(), - QDMI_JOB_RESULT_STATEVECTOR_DENSE, - size, stateVector.data(), nullptr), - "Querying dense state vector"); - return stateVector; -} - -std::vector Job::getDenseProbabilities() const { - size_t size = 0; - qdmi::throwIfError(QDMI_job_get_results(job_.get(), - QDMI_JOB_RESULT_PROBABILITIES_DENSE, - 0, nullptr, &size), - "Querying dense probabilities size"); - - if (size % sizeof(double) != 0) { - throw std::runtime_error( - "Invalid probabilities size: not a multiple of double"); - } - - std::vector probabilities(size / sizeof(double)); - qdmi::throwIfError(QDMI_job_get_results(job_.get(), - QDMI_JOB_RESULT_PROBABILITIES_DENSE, - size, probabilities.data(), nullptr), - "Querying dense probabilities"); - return probabilities; -} - -std::map> Job::getSparseStateVector() const { - size_t keysSize = 0; - qdmi::throwIfError( - QDMI_job_get_results(job_.get(), QDMI_JOB_RESULT_STATEVECTOR_SPARSE_KEYS, - 0, nullptr, &keysSize), - "Querying sparse state vector keys size"); - - if (keysSize == 0) { - return {}; // Empty state vector - } - - std::string keys(keysSize - 1, '\0'); - qdmi::throwIfError( - QDMI_job_get_results(job_.get(), QDMI_JOB_RESULT_STATEVECTOR_SPARSE_KEYS, - keysSize, keys.data(), nullptr), - "Querying sparse state vector keys"); - - size_t valuesSize = 0; - qdmi::throwIfError(QDMI_job_get_results( - job_.get(), QDMI_JOB_RESULT_STATEVECTOR_SPARSE_VALUES, - 0, nullptr, &valuesSize), - "Querying sparse state vector values size"); - - if (valuesSize % sizeof(std::complex) != 0) { - throw std::runtime_error( - "Invalid sparse state vector values size: not a multiple of " - "complex"); - } - - std::vector> values(valuesSize / - sizeof(std::complex)); - qdmi::throwIfError(QDMI_job_get_results( - job_.get(), QDMI_JOB_RESULT_STATEVECTOR_SPARSE_VALUES, - valuesSize, values.data(), nullptr), - "Querying sparse state vector values"); - - // Parse the keys (comma-separated) - std::map> stateVector; - std::istringstream keysStream(keys); - std::string key; - size_t idx = 0; - while (std::getline(keysStream, key, ',')) { - if (idx >= values.size()) { - throw std::runtime_error("Sparse state vector key/value count mismatch"); - } - stateVector[key] = values[idx]; - ++idx; - } - - if (idx != values.size()) { - throw std::runtime_error("Sparse state vector key/value count mismatch"); - } - return stateVector; -} - -std::map Job::getSparseProbabilities() const { - size_t keysSize = 0; - qdmi::throwIfError(QDMI_job_get_results( - job_.get(), QDMI_JOB_RESULT_PROBABILITIES_SPARSE_KEYS, - 0, nullptr, &keysSize), - "Querying sparse probabilities keys size"); - - if (keysSize == 0) { - return {}; // Empty probabilities - } - - std::string keys(keysSize - 1, '\0'); - qdmi::throwIfError(QDMI_job_get_results( - job_.get(), QDMI_JOB_RESULT_PROBABILITIES_SPARSE_KEYS, - keysSize, keys.data(), nullptr), - "Querying sparse probabilities keys"); - - size_t valuesSize = 0; - qdmi::throwIfError( - QDMI_job_get_results(job_.get(), - QDMI_JOB_RESULT_PROBABILITIES_SPARSE_VALUES, 0, - nullptr, &valuesSize), - "Querying sparse probabilities values size"); - - if (valuesSize % sizeof(double) != 0) { - throw std::runtime_error( - "Invalid sparse probabilities values size: not a multiple of double"); - } - - std::vector values(valuesSize / sizeof(double)); - qdmi::throwIfError( - QDMI_job_get_results(job_.get(), - QDMI_JOB_RESULT_PROBABILITIES_SPARSE_VALUES, - valuesSize, values.data(), nullptr), - "Querying sparse probabilities values"); - - // Parse the keys (comma-separated) - std::map probabilities; - std::istringstream keysStream(keys); - std::string key; - size_t idx = 0; - while (std::getline(keysStream, key, ',')) { - if (idx >= values.size()) { - throw std::runtime_error("Sparse probabilities key/value count mismatch"); - } - probabilities[key] = values[idx]; - ++idx; - } - if (idx != values.size()) { - throw std::runtime_error("Sparse probabilities key/value count mismatch"); - } - return probabilities; -} - -Device Session::createSessionlessDevice(QDMI_Device device) { - return Device(device); -} - -Device Session::openDevice(const std::string_view id, - const qdmi::DeviceSessionConfig& overrides) { - return Device(qdmi::Driver::get().openFresh(id, overrides)); -} - -Session::Session(const SessionConfig& config) { - session_ = [] { - QDMI_Session session = nullptr; - const auto result = QDMI_session_alloc(&session); - qdmi::throwIfError(result, "Allocating QDMI session"); - return std::unique_ptr( - session, QDMI_session_free); - }(); - - // Helper to set session parameters - const auto setParameter = [this](const std::optional& value, - QDMI_Session_Parameter param) -> void { - if (value) { - const auto status = static_cast(QDMI_session_set_parameter( - session_.get(), param, value->size() + 1, value->c_str())); - if (status == QDMI_ERROR_NOTSUPPORTED) { - // Optional parameter not supported by session - skip it - SPDLOG_INFO("Session parameter {} not supported (skipped)", - qdmi::toString(param)); - return; - } - if (status == QDMI_SUCCESS) { - return; - } - std::ostringstream ss; - ss << "Setting session parameter " << qdmi::toString(param) << ": " - << qdmi::toString(status) << " (status = " << status << ")"; - qdmi::throwIfError(status, ss.str()); - } - }; - - // Validate file existence for authFile - if (config.authFile) { - if (!std::filesystem::exists(*config.authFile)) { - throw std::runtime_error("Authentication file does not exist: " + - config.authFile->string()); - } - } - // Validate URL format for authUrl - if (config.authUrl) { - // Breakdown of the regex pattern: - // 1. ^https?:// -> Start with http:// or https:// - // 2. (?: -> Start Host Group - // \[[a-fA-F0-9:]+\] -> Branch A: IPv6 (Must be in brackets like - // [::1]) - // -> Note: No \b used here because ']' is a - // non-word char - // | -> OR - // (?: -> Branch B: Alphanumeric Hosts (Group for - // \b check) - // (?:\d{1,3}\.){3}\d{1,3} -> IPv4 (e.g., 127.0.0.1) - // | -> OR - // localhost -> Localhost - // | -> OR - // (?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6} -> - // Domain - // )\b -> End Branch B + Word Boundary (Prevents - // "localhostX") - // ) -> End Host Group - // 3. (?::\d+)? -> Optional Port (e.g., :8080) - // 4. (?:...)*$ -> Optional Path/Query params + End of - // string - static const std::regex URL_PATTERN( - R"(^https?://(?:\[[a-fA-F0-9:]+\]|(?:(?:\d{1,3}\.){3}\d{1,3}|localhost|(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6})\b)(?::\d+)?(?:[-a-zA-Z0-9()@:%_\+.~#?&/=]*)$)", - std::regex::optimize); - if (!std::regex_match(*config.authUrl, URL_PATTERN)) { - throw std::runtime_error("Invalid URL format: " + *config.authUrl); - } - } - - // Set session parameters - setParameter(config.token, QDMI_SESSION_PARAMETER_TOKEN); - if (config.authFile) { - const std::optional authFile = config.authFile->string(); - setParameter(authFile, QDMI_SESSION_PARAMETER_AUTHFILE); - } - setParameter(config.authUrl, QDMI_SESSION_PARAMETER_AUTHURL); - setParameter(config.username, QDMI_SESSION_PARAMETER_USERNAME); - setParameter(config.password, QDMI_SESSION_PARAMETER_PASSWORD); - setParameter(config.projectId, QDMI_SESSION_PARAMETER_PROJECTID); - setParameter(config.custom1, QDMI_SESSION_PARAMETER_CUSTOM1); - setParameter(config.custom2, QDMI_SESSION_PARAMETER_CUSTOM2); - setParameter(config.custom3, QDMI_SESSION_PARAMETER_CUSTOM3); - setParameter(config.custom4, QDMI_SESSION_PARAMETER_CUSTOM4); - setParameter(config.custom5, QDMI_SESSION_PARAMETER_CUSTOM5); - - // Initialize the session - qdmi::throwIfError(QDMI_session_init(session_.get()), "Initializing session"); -} - -std::vector Session::getDevices() { - const auto& qdmiDevices = - queryProperty>(QDMI_SESSION_PROPERTY_DEVICES); - std::vector devices; - devices.reserve(qdmiDevices.size()); - std::ranges::transform( - qdmiDevices, std::back_inserter(devices), - [](QDMI_Device_impl_d* const& dev) -> Device { return Device(dev); }); - return devices; -} -} // namespace qdmi diff --git a/src/qdmi/DefaultDeviceRegistry.hpp b/src/qdmi/DefaultDeviceRegistry.hpp new file mode 100644 index 0000000000..2366fa9964 --- /dev/null +++ b/src/qdmi/DefaultDeviceRegistry.hpp @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "qdmi/DeviceRegistry.hpp" + +namespace qdmi::detail { + +[[nodiscard]] DeviceRegistry snapshotDefaultDeviceRegistry(); + +} // namespace qdmi::detail diff --git a/src/qdmi/Device.cpp b/src/qdmi/Device.cpp new file mode 100644 index 0000000000..833eed1cc0 --- /dev/null +++ b/src/qdmi/Device.cpp @@ -0,0 +1,958 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "qdmi/Device.hpp" + +#include "DeviceState.hpp" +#include "qdmi/common/Common.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include // NOLINT(misc-include-cleaner) +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace qdmi { +namespace { +void rejectUnsupportedProgramFormat(const QDMI_Program_Format format) { + if (format == QDMI_PROGRAM_FORMAT_BATCHJOB) { + throw std::invalid_argument( + "MQT Core does not support batch jobs. A batch job's program is a list " + "of job handles, which this API cannot express"); + } + if (format == QDMI_PROGRAM_FORMAT_CALIBRATION) { + throw std::invalid_argument( + "Use submitCalibrationJob (submit_calibration_job in Python) to " + "trigger a calibration run"); + } +} + +template +[[nodiscard]] auto queryBytes(Query query, const std::string& description) + -> std::optional> { + size_t size = 0; + const auto sizeResult = query(0, nullptr, &size); + if (sizeResult == QDMI_ERROR_NOTSUPPORTED) { + return std::nullopt; + } + throwIfError(sizeResult, "Querying " + description + " size"); + std::vector bytes(size); + if (size != 0) { + throwIfError(query(size, bytes.data(), nullptr), "Querying " + description); + } + return bytes; +} + +template +[[nodiscard]] auto queryValue(Query query, const std::string& description) + -> T { + T value{}; + throwIfError(query(sizeof(T), static_cast(&value), nullptr), + "Querying " + description); + return value; +} + +template +[[nodiscard]] auto queryOptionalValue(Query query, + const std::string& description) + -> std::optional { + T value{}; + const auto result = query(sizeof(T), static_cast(&value), nullptr); + if (result == QDMI_ERROR_NOTSUPPORTED) { + return std::nullopt; + } + throwIfError(result, "Querying " + description); + return value; +} + +template +[[nodiscard]] auto queryVector(Query query, const std::string& description) + -> std::vector { + size_t size = 0; + const auto sizeResult = query(0, nullptr, &size); + if (sizeResult == QDMI_ERROR_NOTSUPPORTED) { + throw std::runtime_error("Querying " + description + ": Not supported."); + } + throwIfError(sizeResult, "Querying " + description + " size"); + if (size % sizeof(T) != 0) { + throw std::runtime_error("Invalid byte size while querying " + description); + } + std::vector values(size / sizeof(T)); + if (size != 0) { + throwIfError(query(size, static_cast(values.data()), nullptr), + "Querying " + description); + } + return values; +} + +template +[[nodiscard]] auto queryOptionalVector(Query query, + const std::string& description) + -> std::optional> { + size_t size = 0; + const auto sizeResult = query(0, nullptr, &size); + if (sizeResult == QDMI_ERROR_NOTSUPPORTED) { + return std::nullopt; + } + throwIfError(sizeResult, "Querying " + description + " size"); + if (size % sizeof(T) != 0) { + throw std::runtime_error("Invalid byte size while querying " + description); + } + std::vector values(size / sizeof(T)); + if (size != 0) { + throwIfError(query(size, static_cast(values.data()), nullptr), + "Querying " + description); + } + return values; +} + +template +[[nodiscard]] auto queryString(Query query, const std::string& description) + -> std::string { + const auto bytes = queryBytes(std::move(query), description); + if (!bytes || bytes->empty() || bytes->back() != std::byte{0}) { + throw std::runtime_error("Invalid string while querying " + description); + } + return {reinterpret_cast(bytes->data()), bytes->size() - 1}; +} + +template +[[nodiscard]] auto queryOptionalString(Query query, + const std::string& description) + -> std::optional { + const auto bytes = queryBytes(std::move(query), description); + if (!bytes) { + return std::nullopt; + } + if (bytes->empty() || bytes->back() != std::byte{0}) { + throw std::runtime_error("Invalid string while querying " + description); + } + return std::string(reinterpret_cast(bytes->data()), + bytes->size() - 1); +} + +[[nodiscard]] auto splitCommaSeparated(const std::string& values) + -> std::vector { + if (values.empty()) { + return {}; + } + std::vector result; + std::istringstream stream(values); + for (std::string value; std::getline(stream, value, ',');) { + result.emplace_back(std::move(value)); + } + return result; +} + +[[nodiscard]] constexpr auto customOffset(const CustomProperty property) + -> int { + const auto offset = static_cast(property) - 1; + if (offset < 0 || offset >= 5) { + throw std::invalid_argument("Invalid custom property selector"); + } + return offset; +} + +[[nodiscard]] constexpr auto deviceCustom(const CustomProperty property) + -> QDMI_Device_Property { + return static_cast(QDMI_DEVICE_PROPERTY_CUSTOM1 + + customOffset(property)); +} +[[nodiscard]] constexpr auto siteCustom(const CustomProperty property) + -> QDMI_Site_Property { + return static_cast(QDMI_SITE_PROPERTY_CUSTOM1 + + customOffset(property)); +} +[[nodiscard]] constexpr auto operationCustom(const CustomProperty property) + -> QDMI_Operation_Property { + return static_cast(QDMI_OPERATION_PROPERTY_CUSTOM1 + + customOffset(property)); +} +[[nodiscard]] constexpr auto jobCustom(const CustomProperty property) + -> QDMI_Device_Job_Property { + return static_cast( + QDMI_DEVICE_JOB_PROPERTY_CUSTOM1 + customOffset(property)); +} +[[nodiscard]] constexpr auto resultCustom(const CustomProperty property) + -> QDMI_Job_Result { + return static_cast(QDMI_JOB_RESULT_CUSTOM1 + + customOffset(property)); +} + +void setCustomJobParameter(const detail::JobState& state, + const QDMI_Device_Job_Parameter parameter, + const CustomJobParameter& value) { + const auto result = std::visit( + [&state, parameter](const auto& typed) { + using T = std::decay_t; + if constexpr (std::same_as) { + return state.device->api().device_job_set_parameter( + state.job, parameter, typed.size() + 1, typed.c_str()); + } else { + return state.device->api().device_job_set_parameter( + state.job, parameter, sizeof(T), &typed); + } + }, + value); + throwIfError(result, "Setting custom parameter"); +} +} // namespace + +std::string Device::getName() const { + return queryString( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->api().device_session_query_device_property( + state_->session(), QDMI_DEVICE_PROPERTY_NAME, size, value, sizeRet); + }, + "device name"); +} +std::string Device::getVersion() const { + return queryString( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->api().device_session_query_device_property( + state_->session(), QDMI_DEVICE_PROPERTY_VERSION, size, value, + sizeRet); + }, + "device version"); +} +QDMI_Device_Status Device::getStatus() const { + return queryValue( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->api().device_session_query_device_property( + state_->session(), QDMI_DEVICE_PROPERTY_STATUS, size, value, + sizeRet); + }, + "device status"); +} +std::string Device::getLibraryVersion() const { + return queryString( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->api().device_session_query_device_property( + state_->session(), QDMI_DEVICE_PROPERTY_LIBRARYVERSION, size, value, + sizeRet); + }, + "device library version"); +} +size_t Device::getQubitsNum() const { + return queryValue( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->api().device_session_query_device_property( + state_->session(), QDMI_DEVICE_PROPERTY_QUBITSNUM, size, value, + sizeRet); + }, + "device qubit count"); +} +std::vector Device::getSites() const { + const auto handles = queryVector( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->api().device_session_query_device_property( + state_->session(), QDMI_DEVICE_PROPERTY_SITES, size, value, + sizeRet); + }, + "device sites"); + std::vector sites; + sites.reserve(handles.size()); + std::ranges::transform(handles, std::back_inserter(sites), + [this](auto* handle) { return Site(state_, handle); }); + return sites; +} +std::vector Device::getRegularSites() const { + auto sites = getSites(); + std::erase_if(sites, [](const Site& site) { return site.isZone(); }); + return sites; +} +std::vector Device::getZones() const { + auto sites = getSites(); + std::erase_if(sites, [](const Site& site) { return !site.isZone(); }); + return sites; +} +std::vector Device::getOperations() const { + const auto handles = queryVector( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->api().device_session_query_device_property( + state_->session(), QDMI_DEVICE_PROPERTY_OPERATIONS, size, value, + sizeRet); + }, + "device operations"); + std::vector operations; + operations.reserve(handles.size()); + std::ranges::transform( + handles, std::back_inserter(operations), + [this](auto* handle) { return Operation(state_, handle); }); + return operations; +} +std::optional>> +Device::getCouplingMap() const { + struct Pair { + QDMI_Site first; + QDMI_Site second; + }; + const auto pairs = queryOptionalVector( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->api().device_session_query_device_property( + state_->session(), QDMI_DEVICE_PROPERTY_COUPLINGMAP, size, value, + sizeRet); + }, + "device coupling map"); + if (!pairs) { + return std::nullopt; + } + std::vector> result; + result.reserve(pairs->size()); + std::ranges::transform( + *pairs, std::back_inserter(result), [this](const Pair& pair) { + return std::pair{Site(state_, pair.first), Site(state_, pair.second)}; + }); + return result; +} + +#define DEVICE_OPTIONAL_VALUE(method, type, property, description) \ + auto Device::method() const -> std::optional { \ + return queryOptionalValue( \ + [this](const size_t size, void* value, size_t* sizeRet) { \ + return state_->api().device_session_query_device_property( \ + state_->session(), property, size, value, sizeRet); \ + }, \ + description); \ + } +DEVICE_OPTIONAL_VALUE(getNeedsCalibration, size_t, + QDMI_DEVICE_PROPERTY_NEEDSCALIBRATION, + "device calibration requirement") +DEVICE_OPTIONAL_VALUE(getQueueLength, size_t, QDMI_DEVICE_PROPERTY_QUEUELENGTH, + "device queue length") +DEVICE_OPTIONAL_VALUE(getLengthScaleFactor, double, + QDMI_DEVICE_PROPERTY_LENGTHSCALEFACTOR, + "device length scale") +DEVICE_OPTIONAL_VALUE(getDurationScaleFactor, double, + QDMI_DEVICE_PROPERTY_DURATIONSCALEFACTOR, + "device duration scale") +DEVICE_OPTIONAL_VALUE(getMinAtomDistance, uint64_t, + QDMI_DEVICE_PROPERTY_MINATOMDISTANCE, + "device minimum atom distance") +#undef DEVICE_OPTIONAL_VALUE + +std::optional Device::getLengthUnit() const { + return queryOptionalString( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->api().device_session_query_device_property( + state_->session(), QDMI_DEVICE_PROPERTY_LENGTHUNIT, size, value, + sizeRet); + }, + "device length unit"); +} +std::optional Device::getDurationUnit() const { + return queryOptionalString( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->api().device_session_query_device_property( + state_->session(), QDMI_DEVICE_PROPERTY_DURATIONUNIT, size, value, + sizeRet); + }, + "device duration unit"); +} +std::vector Device::getSupportedProgramFormats() const { + return queryVector( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->api().device_session_query_device_property( + state_->session(), QDMI_DEVICE_PROPERTY_SUPPORTEDPROGRAMFORMATS, + size, value, sizeRet); + }, + "supported program formats"); +} +std::vector Device::getChildDevices() const { + std::vector result; + result.reserve(state_->children.size()); + std::ranges::transform(state_->children, std::back_inserter(result), + [](const auto& child) { return Device(child); }); + return result; +} +std::optional> +Device::queryCustomOperations(const CustomProperty property) const { + const auto handles = queryOptionalVector( + [this, property](const size_t size, void* value, size_t* sizeRet) { + return state_->api().device_session_query_device_property( + state_->session(), deviceCustom(property), size, value, sizeRet); + }, + "custom operation list"); + if (!handles) { + return std::nullopt; + } + std::vector operations; + operations.reserve(handles->size()); + std::ranges::transform( + *handles, std::back_inserter(operations), + [this](auto* handle) { return Operation(state_, handle); }); + return operations; +} +std::optional> +Device::queryCustomPropertyBytes(const CustomProperty property) const { + return queryBytes( + [this, property](const size_t size, void* value, size_t* sizeRet) { + return state_->api().device_session_query_device_property( + state_->session(), deviceCustom(property), size, value, sizeRet); + }, + "custom device property"); +} +Job Device::submitJob(const std::string& program, + const QDMI_Program_Format format, const size_t numShots, + const std::optional& custom1, + const std::optional& custom2, + const std::optional& custom3, + const std::optional& custom4, + const std::optional& custom5) const { + if (isBinaryProgramFormat(format)) { + throw std::invalid_argument( + "Binary program formats require exact-byte submission"); + } + rejectUnsupportedProgramFormat(format); + + const auto bytes = std::as_bytes( + std::span(program.c_str(), static_cast(program.size() + 1))); + return submitJob(bytes, format, numShots, custom1, custom2, custom3, custom4, + custom5); +} + +Job Device::submitJob(const std::span program, + const QDMI_Program_Format format, const size_t numShots, + const std::optional& custom1, + const std::optional& custom2, + const std::optional& custom3, + const std::optional& custom4, + const std::optional& custom5) const { + rejectUnsupportedProgramFormat(format); + + return submitJobImpl(format, program, numShots, custom1, custom2, custom3, + custom4, custom5); +} + +Job Device::submitJobImpl( + const QDMI_Program_Format format, + const std::optional> program, + const std::optional numShots, + const std::optional& custom1, + const std::optional& custom2, + const std::optional& custom3, + const std::optional& custom4, + const std::optional& custom5) const { + auto jobState = std::make_shared(state_); + throwIfError(jobState->device->api().device_job_set_parameter( + jobState->job, QDMI_DEVICE_JOB_PARAMETER_PROGRAMFORMAT, + sizeof(format), &format), + "Setting program format"); + if (program.has_value()) { + throwIfError(jobState->device->api().device_job_set_parameter( + jobState->job, QDMI_DEVICE_JOB_PARAMETER_PROGRAM, + program->size(), program->data()), + "Setting program"); + } + if (numShots.has_value()) { + throwIfError(jobState->device->api().device_job_set_parameter( + jobState->job, QDMI_DEVICE_JOB_PARAMETER_SHOTSNUM, + sizeof(*numShots), &*numShots), + "Setting number of shots"); + } + const std::array customValues{&custom1, &custom2, &custom3, &custom4, + &custom5}; + for (size_t i = 0; i < customValues.size(); ++i) { + if (*customValues[i]) { + setCustomJobParameter( + *jobState, + static_cast( + QDMI_DEVICE_JOB_PARAMETER_CUSTOM1 + static_cast(i)), + **customValues[i]); + } + } + throwIfError(jobState->device->api().device_job_submit(jobState->job), + "Submitting QDMI job"); + return Job(std::move(jobState)); +} + +Job Device::submitCalibrationJob( + const std::optional> program, + const std::optional& custom1, + const std::optional& custom2, + const std::optional& custom3, + const std::optional& custom4, + const std::optional& custom5) const { + const auto payload = + program.has_value() && !program->empty() ? program : std::nullopt; + return submitJobImpl(QDMI_PROGRAM_FORMAT_CALIBRATION, payload, std::nullopt, + custom1, custom2, custom3, custom4, custom5); +} + +Job Device::submitCalibrationJob( + const std::string& program, + const std::optional& custom1, + const std::optional& custom2, + const std::optional& custom3, + const std::optional& custom4, + const std::optional& custom5) const { + const auto bytes = std::as_bytes( + std::span(program.c_str(), static_cast(program.size() + 1))); + return submitCalibrationJob(bytes, custom1, custom2, custom3, custom4, + custom5); +} + +Job Device::retrieveJobById(const std::string_view jobId) const { + const auto retrieve = state_->api().device_session_retrieve_device_job_by_id; + if (retrieve == nullptr) { + throwIfError(QDMI_ERROR_NOTSUPPORTED, "Retrieving job"); + } + const std::string id{jobId}; + QDMI_Device_Job job = nullptr; + throwIfError(retrieve(state_->session(), id.c_str(), &job), "Retrieving job"); + return Job(std::make_shared(state_, job)); +} + +QDMI_Job_Status Job::check() const { + QDMI_Job_Status status{}; + throwIfError(state_->device->api().device_job_check(state_->job, &status), + "Checking QDMI job"); + return status; +} +bool Job::wait(const size_t timeout) const { + const auto result = + state_->device->api().device_job_wait(state_->job, timeout); + if (result == QDMI_ERROR_TIMEOUT) { + return false; + } + throwIfError(result, "Waiting for QDMI job"); + return true; +} +void Job::cancel() const { + throwIfError(state_->device->api().device_job_cancel(state_->job), + "Canceling QDMI job"); +} +std::string Job::getId() const { + return queryString( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->device->api().device_job_query_property( + state_->job, QDMI_DEVICE_JOB_PROPERTY_ID, size, value, sizeRet); + }, + "job ID"); +} +QDMI_Program_Format Job::getProgramFormat() const { + return queryValue( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->device->api().device_job_query_property( + state_->job, QDMI_DEVICE_JOB_PROPERTY_PROGRAMFORMAT, size, value, + sizeRet); + }, + "job program format"); +} +std::string Job::getProgram() const { + const auto format = getProgramFormat(); + if (isBinaryProgramFormat(format)) { + throw std::invalid_argument( + "Cannot decode a binary program as a string; use getProgramBytes()"); + } + + const auto program = getProgramBytes(); + if (program.empty() || program.back() != std::byte{0}) { + throw std::invalid_argument( + "Cannot decode program as a null-terminated string; use " + "getProgramBytes() for binary payloads"); + } + return {reinterpret_cast(program.data()), program.size() - 1}; +} +std::vector Job::getProgramBytes() const { + const auto program = queryBytes( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->device->api().device_job_query_property( + state_->job, QDMI_DEVICE_JOB_PROPERTY_PROGRAM, size, value, + sizeRet); + }, + "job program"); + if (!program) { + throw std::runtime_error("Querying job program: Not supported."); + } + return *program; +} +size_t Job::getNumShots() const { + return queryValue( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->device->api().device_job_query_property( + state_->job, QDMI_DEVICE_JOB_PROPERTY_SHOTSNUM, size, value, + sizeRet); + }, + "job shot count"); +} +std::optional Job::getQueuePosition() const { + return queryOptionalValue( + [this](const size_t size, void* value, size_t* sizeRet) { + const auto result = state_->device->api().device_job_query_property( + state_->job, QDMI_DEVICE_JOB_PROPERTY_QUEUEPOSITION, size, value, + sizeRet); + return result == QDMI_ERROR_BADSTATE ? QDMI_ERROR_NOTSUPPORTED : result; + }, + "job queue position"); +} +std::optional> +Job::queryCustomPropertyBytes(const CustomProperty property) const { + return queryBytes( + [this, property](const size_t size, void* value, size_t* sizeRet) { + return state_->device->api().device_job_query_property( + state_->job, jobCustom(property), size, value, sizeRet); + }, + "custom job property"); +} +std::optional> +Job::getCustomResultBytes(const CustomProperty property) const { + return queryBytes( + [this, property](const size_t size, void* value, size_t* sizeRet) { + return state_->device->api().device_job_get_results( + state_->job, resultCustom(property), size, value, sizeRet); + }, + "custom job result"); +} +std::vector Job::getShots() const { + const auto bytes = queryBytes( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->device->api().device_job_get_results( + state_->job, QDMI_JOB_RESULT_SHOTS, size, value, sizeRet); + }, + "job shots"); + if (!bytes) { + throw std::runtime_error("Querying job shots: Not supported."); + } + if (bytes->empty()) { + return {}; + } + if (bytes->back() != std::byte{0}) { + throw std::runtime_error("Invalid string while querying job shots"); + } + return splitCommaSeparated(std::string( + reinterpret_cast(bytes->data()), bytes->size() - 1)); +} +std::map Job::getCounts() const { + const auto keys = splitCommaSeparated(queryString( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->device->api().device_job_get_results( + state_->job, QDMI_JOB_RESULT_HIST_KEYS, size, value, sizeRet); + }, + "histogram keys")); + const auto values = queryVector( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->device->api().device_job_get_results( + state_->job, QDMI_JOB_RESULT_HIST_VALUES, size, value, sizeRet); + }, + "histogram values"); + if (keys.size() != values.size()) { + throw std::runtime_error("Histogram key/value lengths do not match"); + } + std::map result; + for (size_t i = 0; i < keys.size(); ++i) { + result.emplace(keys[i], values[i]); + } + return result; +} +std::vector> Job::getDenseStateVector() const { + return queryVector>( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->device->api().device_job_get_results( + state_->job, QDMI_JOB_RESULT_STATEVECTOR_DENSE, size, value, + sizeRet); + }, + "dense state vector"); +} +std::vector Job::getDenseProbabilities() const { + return queryVector( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->device->api().device_job_get_results( + state_->job, QDMI_JOB_RESULT_PROBABILITIES_DENSE, size, value, + sizeRet); + }, + "dense probabilities"); +} +std::map> Job::getSparseStateVector() const { + const auto keys = splitCommaSeparated(queryString( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->device->api().device_job_get_results( + state_->job, QDMI_JOB_RESULT_STATEVECTOR_SPARSE_KEYS, size, value, + sizeRet); + }, + "sparse state-vector keys")); + const auto values = queryVector>( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->device->api().device_job_get_results( + state_->job, QDMI_JOB_RESULT_STATEVECTOR_SPARSE_VALUES, size, value, + sizeRet); + }, + "sparse state-vector values"); + if (keys.size() != values.size()) { + throw std::runtime_error("Sparse state-vector lengths do not match"); + } + std::map> result; + for (size_t i = 0; i < keys.size(); ++i) { + result.emplace(keys[i], values[i]); + } + return result; +} +std::map Job::getSparseProbabilities() const { + const auto keys = splitCommaSeparated(queryString( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->device->api().device_job_get_results( + state_->job, QDMI_JOB_RESULT_PROBABILITIES_SPARSE_KEYS, size, value, + sizeRet); + }, + "sparse probability keys")); + const auto values = queryVector( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->device->api().device_job_get_results( + state_->job, QDMI_JOB_RESULT_PROBABILITIES_SPARSE_VALUES, size, + value, sizeRet); + }, + "sparse probability values"); + if (keys.size() != values.size()) { + throw std::runtime_error("Sparse probability lengths do not match"); + } + std::map result; + for (size_t i = 0; i < keys.size(); ++i) { + result.emplace(keys[i], values[i]); + } + return result; +} + +#define SITE_OPTIONAL_VALUE(method, type, property, description) \ + auto Site::method() const -> std::optional { \ + return queryOptionalValue( \ + [this](const size_t size, void* value, size_t* sizeRet) { \ + return state_->api().device_session_query_site_property( \ + state_->session(), static_cast(handle_), property, \ + size, value, sizeRet); \ + }, \ + description); \ + } +size_t Site::getIndex() const { + return queryValue( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->api().device_session_query_site_property( + state_->session(), static_cast(handle_), + QDMI_SITE_PROPERTY_INDEX, size, value, sizeRet); + }, + "site index"); +} +SITE_OPTIONAL_VALUE(getT1, uint64_t, QDMI_SITE_PROPERTY_T1, "site T1") +SITE_OPTIONAL_VALUE(getT2, uint64_t, QDMI_SITE_PROPERTY_T2, "site T2") +SITE_OPTIONAL_VALUE(getXCoordinate, int64_t, QDMI_SITE_PROPERTY_XCOORDINATE, + "site x coordinate") +SITE_OPTIONAL_VALUE(getYCoordinate, int64_t, QDMI_SITE_PROPERTY_YCOORDINATE, + "site y coordinate") +SITE_OPTIONAL_VALUE(getZCoordinate, int64_t, QDMI_SITE_PROPERTY_ZCOORDINATE, + "site z coordinate") +SITE_OPTIONAL_VALUE(getXExtent, uint64_t, QDMI_SITE_PROPERTY_XEXTENT, + "site x extent") +SITE_OPTIONAL_VALUE(getYExtent, uint64_t, QDMI_SITE_PROPERTY_YEXTENT, + "site y extent") +SITE_OPTIONAL_VALUE(getZExtent, uint64_t, QDMI_SITE_PROPERTY_ZEXTENT, + "site z extent") +SITE_OPTIONAL_VALUE(getModuleIndex, uint64_t, QDMI_SITE_PROPERTY_MODULEINDEX, + "site module index") +SITE_OPTIONAL_VALUE(getSubmoduleIndex, uint64_t, + QDMI_SITE_PROPERTY_SUBMODULEINDEX, "site submodule index") +#undef SITE_OPTIONAL_VALUE +std::optional Site::getName() const { + return queryOptionalString( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->api().device_session_query_site_property( + state_->session(), static_cast(handle_), + QDMI_SITE_PROPERTY_NAME, size, value, sizeRet); + }, + "site name"); +} +bool Site::isZone() const { + return queryOptionalValue( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->api().device_session_query_site_property( + state_->session(), static_cast(handle_), + QDMI_SITE_PROPERTY_ISZONE, size, value, sizeRet); + }, + "site zone flag") + .value_or(false); +} +std::optional> +Site::queryCustomPropertyBytes(const CustomProperty property) const { + return queryBytes( + [this, property](const size_t size, void* value, size_t* sizeRet) { + return state_->api().device_session_query_site_property( + state_->session(), static_cast(handle_), + siteCustom(property), size, value, sizeRet); + }, + "custom site property"); +} + +std::vector +Operation::siteHandles(const std::vector& sites) const { + std::vector handles; + handles.reserve(sites.size()); + std::ranges::transform(sites, std::back_inserter(handles), + [this](const Site& site) { + if (site.state_ != state_) { + throw std::invalid_argument( + "Operation sites must belong to its device"); + } + return static_cast(site.handle_); + }); + return handles; +} + +#define OPERATION_OPTIONAL_VALUE(method, type, property, description) \ + auto Operation::method(const std::vector& sites, \ + const std::vector& params) const \ + -> std::optional { \ + const auto opaqueHandles = siteHandles(sites); \ + const auto* handles = \ + reinterpret_cast(opaqueHandles.data()); \ + return queryOptionalValue( \ + [this, &opaqueHandles, handles, \ + ¶ms](const size_t size, void* value, size_t* sizeRet) { \ + return state_->api().device_session_query_operation_property( \ + state_->session(), static_cast(handle_), \ + opaqueHandles.size(), handles, params.size(), params.data(), \ + property, size, value, sizeRet); \ + }, \ + description); \ + } +std::string Operation::getName(const std::vector& sites, + const std::vector& params) const { + const auto opaqueHandles = siteHandles(sites); + const auto* handles = + reinterpret_cast(opaqueHandles.data()); + return queryString( + [this, &opaqueHandles, handles, ¶ms](const size_t size, void* value, + size_t* sizeRet) { + return state_->api().device_session_query_operation_property( + state_->session(), static_cast(handle_), + opaqueHandles.size(), handles, params.size(), params.data(), + QDMI_OPERATION_PROPERTY_NAME, size, value, sizeRet); + }, + "operation name"); +} +OPERATION_OPTIONAL_VALUE(getQubitsNum, size_t, + QDMI_OPERATION_PROPERTY_QUBITSNUM, + "operation qubit count") +OPERATION_OPTIONAL_VALUE(getDuration, uint64_t, + QDMI_OPERATION_PROPERTY_DURATION, "operation duration") +OPERATION_OPTIONAL_VALUE(getFidelity, double, QDMI_OPERATION_PROPERTY_FIDELITY, + "operation fidelity") +OPERATION_OPTIONAL_VALUE(getInteractionRadius, uint64_t, + QDMI_OPERATION_PROPERTY_INTERACTIONRADIUS, + "operation interaction radius") +OPERATION_OPTIONAL_VALUE(getBlockingRadius, uint64_t, + QDMI_OPERATION_PROPERTY_BLOCKINGRADIUS, + "operation blocking radius") +OPERATION_OPTIONAL_VALUE(getIdlingFidelity, double, + QDMI_OPERATION_PROPERTY_IDLINGFIDELITY, + "operation idling fidelity") +OPERATION_OPTIONAL_VALUE(getMeanShuttlingSpeed, uint64_t, + QDMI_OPERATION_PROPERTY_MEANSHUTTLINGSPEED, + "operation mean shuttling speed") +#undef OPERATION_OPTIONAL_VALUE +size_t Operation::getParametersNum(const std::vector& sites, + const std::vector& params) const { + const auto opaqueHandles = siteHandles(sites); + const auto* handles = + reinterpret_cast(opaqueHandles.data()); + return queryValue( + [this, &opaqueHandles, handles, ¶ms](const size_t size, void* value, + size_t* sizeRet) { + return state_->api().device_session_query_operation_property( + state_->session(), static_cast(handle_), + opaqueHandles.size(), handles, params.size(), params.data(), + QDMI_OPERATION_PROPERTY_PARAMETERSNUM, size, value, sizeRet); + }, + "operation parameter count"); +} +bool Operation::isZoned() const { + const std::vector sites; + const std::vector params; + return queryOptionalValue( + [this, &sites, ¶ms](const size_t size, void* value, + size_t* sizeRet) { + return state_->api().device_session_query_operation_property( + state_->session(), static_cast(handle_), 0, + sites.data(), 0, params.data(), + QDMI_OPERATION_PROPERTY_ISZONED, size, value, sizeRet); + }, + "operation zone flag") + .value_or(false); +} +std::optional> Operation::getSites() const { + const std::vector sites; + const std::vector params; + const auto handles = queryOptionalVector( + [this, &sites, ¶ms](const size_t size, void* value, size_t* sizeRet) { + return state_->api().device_session_query_operation_property( + state_->session(), static_cast(handle_), 0, + sites.data(), 0, params.data(), QDMI_OPERATION_PROPERTY_SITES, size, + value, sizeRet); + }, + "operation sites"); + if (!handles) { + return std::nullopt; + } + std::vector result; + result.reserve(handles->size()); + std::ranges::transform(*handles, std::back_inserter(result), + [this](auto* site) { return Site(state_, site); }); + return result; +} +std::optional>> +Operation::getSitePairs() const { + if (isZoned() || getQubitsNum().value_or(0) != 2) { + return std::nullopt; + } + const auto sites = getSites(); + if (!sites || sites->empty() || sites->size() % 2 != 0) { + return std::nullopt; + } + std::vector> pairs; + pairs.reserve(sites->size() / 2); + for (size_t i = 0; i < sites->size(); i += 2) { + pairs.emplace_back((*sites)[i], (*sites)[i + 1]); + } + return pairs; +} +std::optional> +Operation::queryCustomPropertyBytes(const CustomProperty property, + const std::vector& sites, + const std::vector& params) const { + const auto opaqueHandles = siteHandles(sites); + const auto* handles = + reinterpret_cast(opaqueHandles.data()); + return queryBytes( + [this, property, &opaqueHandles, handles, + ¶ms](const size_t size, void* value, size_t* sizeRet) { + return state_->api().device_session_query_operation_property( + state_->session(), static_cast(handle_), + opaqueHandles.size(), handles, params.size(), params.data(), + operationCustom(property), size, value, sizeRet); + }, + "custom operation property"); +} +} // namespace qdmi diff --git a/src/qdmi/DeviceApi.cpp b/src/qdmi/DeviceApi.cpp new file mode 100644 index 0000000000..adebcad14a --- /dev/null +++ b/src/qdmi/DeviceApi.cpp @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "DeviceApi.hpp" + +#include "qdmi/common/Common.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#endif + +namespace qdmi::detail { +namespace { +struct DeviceApiCacheEntry { + std::weak_ptr api; + std::shared_ptr finalized; +}; + +struct FinalizationSignal { + std::shared_ptr finalized; + + ~FinalizationSignal() { finalized->count_down(); } +}; + +struct DeviceApiGeneration { + FinalizationSignal signal; + DeviceApi api; + + DeviceApiGeneration(const std::filesystem::path& library, + const std::string& prefix, + std::shared_ptr finalized) + : signal{std::move(finalized)}, api(library, prefix) {} +}; + +struct DeviceApiCache { + std::mutex mutex; + std::map libraries; +}; + +[[nodiscard]] DeviceApiCache& deviceApiCache() { + static DeviceApiCache cache; + return cache; +} + +#ifdef _WIN32 +[[nodiscard]] void* openLibrary(const std::filesystem::path& path) { + return LoadLibraryExW(path.wstring().c_str(), nullptr, + LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | + LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); +} +[[nodiscard]] void* loadSymbol(void* library, const std::string& symbol) { + return reinterpret_cast( + GetProcAddress(static_cast(library), symbol.c_str())); +} +void closeLibrary(void* library) { + static_cast(FreeLibrary(static_cast(library))); +} +#else +[[nodiscard]] void* openLibrary(const std::filesystem::path& path) { + return dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL); +} +[[nodiscard]] void* loadSymbol(void* library, const std::string& symbol) { + return dlsym(library, symbol.c_str()); +} +void closeLibrary(void* library) { static_cast(dlclose(library)); } +#endif + +template +[[nodiscard]] Function* resolve(void* library, const std::string& prefix, + const std::string& suffix) { + const auto name = prefix + "_QDMI_" + suffix; + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + auto* function = reinterpret_cast(loadSymbol(library, name)); + if (function == nullptr) { + throw std::runtime_error("Failed to load QDMI symbol '" + name + "'"); + } + return function; +} + +template +[[nodiscard]] Function* resolveOptional(void* library, + const std::string& prefix, + const std::string& suffix) { + const auto name = prefix + "_QDMI_" + suffix; + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + return reinterpret_cast(loadSymbol(library, name)); +} +} // namespace + +DeviceApi::DeviceApi(const std::filesystem::path& library, + const std::string& prefix) + : library_(openLibrary(library)) { + if (library_ == nullptr) { + throw std::runtime_error("Could not open QDMI device library: " + + library.string()); + } + try { + const auto initialize = resolve( + library_, prefix, "device_initialize"); + finalize_ = resolve(library_, prefix, + "device_finalize"); +#define LOAD_QDMI_SYMBOL(name) \ + name = resolve(library_, prefix, #name) + LOAD_QDMI_SYMBOL(device_session_alloc); + LOAD_QDMI_SYMBOL(device_session_init); + LOAD_QDMI_SYMBOL(device_session_free); + LOAD_QDMI_SYMBOL(device_session_set_parameter); + LOAD_QDMI_SYMBOL(device_session_create_device_job); + device_session_retrieve_device_job_by_id = resolveOptional< + decltype(QDMI_device_session_retrieve_device_job_by_id)>( + library_, prefix, "device_session_retrieve_device_job_by_id"); + LOAD_QDMI_SYMBOL(device_job_free); + LOAD_QDMI_SYMBOL(device_job_set_parameter); + LOAD_QDMI_SYMBOL(device_job_query_property); + LOAD_QDMI_SYMBOL(device_job_submit); + LOAD_QDMI_SYMBOL(device_job_cancel); + LOAD_QDMI_SYMBOL(device_job_check); + LOAD_QDMI_SYMBOL(device_job_wait); + LOAD_QDMI_SYMBOL(device_job_get_results); + LOAD_QDMI_SYMBOL(device_session_query_device_property); + LOAD_QDMI_SYMBOL(device_session_query_site_property); + LOAD_QDMI_SYMBOL(device_session_query_operation_property); +#undef LOAD_QDMI_SYMBOL + throwIfError(initialize(), "Initializing QDMI device library"); + initialized_ = true; + } catch (...) { + closeLibrary(library_); + library_ = nullptr; + throw; + } +} + +DeviceApi::~DeviceApi() { + if (initialized_) { + static_cast(finalize_()); + } + if (library_ != nullptr) { + closeLibrary(library_); + } +} + +std::shared_ptr +loadDeviceApi(const std::filesystem::path& library, const std::string& prefix) { + const auto canonicalLibrary = std::filesystem::weakly_canonical(library); + const auto key = canonicalLibrary.string() + "\n" + prefix; + auto& cache = deviceApiCache(); + while (true) { + std::unique_lock lock(cache.mutex); + if (const auto entry = cache.libraries.find(key); + entry != cache.libraries.end()) { + if (auto loaded = entry->second.api.lock()) { + return loaded; + } + const auto finalized = entry->second.finalized; + lock.unlock(); + finalized->wait(); + lock.lock(); + if (const auto stale = cache.libraries.find(key); + stale != cache.libraries.end() && + stale->second.finalized == finalized) { + cache.libraries.erase(stale); + } + continue; + } + + auto finalized = std::make_shared(1); + auto generation = std::make_shared(canonicalLibrary, + prefix, finalized); + auto loaded = + std::shared_ptr(generation, &generation->api); + cache.libraries.emplace( + key, + DeviceApiCacheEntry{.api = loaded, .finalized = std::move(finalized)}); + return loaded; + } +} + +} // namespace qdmi::detail diff --git a/src/qdmi/DeviceApi.hpp b/src/qdmi/DeviceApi.hpp new file mode 100644 index 0000000000..ef9235e0fe --- /dev/null +++ b/src/qdmi/DeviceApi.hpp @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include + +#include +#include +#include + +namespace qdmi::detail { + +/// One loaded and initialized QDMI device implementation. +struct DeviceApi { + DeviceApi() = default; + DeviceApi(const std::filesystem::path& library, const std::string& prefix); + ~DeviceApi(); + + DeviceApi(const DeviceApi&) = delete; + DeviceApi& operator=(const DeviceApi&) = delete; + DeviceApi(DeviceApi&&) = delete; + DeviceApi& operator=(DeviceApi&&) = delete; + + // Keep the QDMI names so every stored signature visibly corresponds to its + // QDMI declaration. + // NOLINTBEGIN(readability-identifier-naming) + decltype(QDMI_device_session_alloc)* device_session_alloc = nullptr; + decltype(QDMI_device_session_init)* device_session_init = nullptr; + decltype(QDMI_device_session_free)* device_session_free = nullptr; + decltype(QDMI_device_session_set_parameter)* device_session_set_parameter = + nullptr; + decltype(QDMI_device_session_create_device_job)* + device_session_create_device_job = nullptr; + decltype(QDMI_device_session_retrieve_device_job_by_id)* + device_session_retrieve_device_job_by_id = nullptr; + decltype(QDMI_device_job_free)* device_job_free = nullptr; + decltype(QDMI_device_job_set_parameter)* device_job_set_parameter = nullptr; + decltype(QDMI_device_job_query_property)* device_job_query_property = nullptr; + decltype(QDMI_device_job_submit)* device_job_submit = nullptr; + decltype(QDMI_device_job_cancel)* device_job_cancel = nullptr; + decltype(QDMI_device_job_check)* device_job_check = nullptr; + decltype(QDMI_device_job_wait)* device_job_wait = nullptr; + decltype(QDMI_device_job_get_results)* device_job_get_results = nullptr; + decltype(QDMI_device_session_query_device_property)* + device_session_query_device_property = nullptr; + decltype(QDMI_device_session_query_site_property)* + device_session_query_site_property = nullptr; + decltype(QDMI_device_session_query_operation_property)* + device_session_query_operation_property = nullptr; + // NOLINTEND(readability-identifier-naming) + +private: + void* library_ = nullptr; + decltype(QDMI_device_finalize)* finalize_ = nullptr; + bool initialized_ = false; +}; + +/// Load or reuse one process-wide QDMI implementation instance. +/// +/// The cache is weak: live sessions share one initialized library keyed by its +/// canonical path and prefix, and the library unloads after the last session is +/// destroyed. A replacement generation waits until prior finalization and +/// unloading have completed. +[[nodiscard]] std::shared_ptr +loadDeviceApi(const std::filesystem::path& library, const std::string& prefix); + +} // namespace qdmi::detail diff --git a/src/qdmi/DeviceManager.cpp b/src/qdmi/DeviceManager.cpp new file mode 100644 index 0000000000..5ae568f015 --- /dev/null +++ b/src/qdmi/DeviceManager.cpp @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "qdmi/DeviceManager.hpp" + +#include "DefaultDeviceRegistry.hpp" +#include "DeviceApi.hpp" +#include "DeviceState.hpp" +#include "qdmi/Device.hpp" +#include "qdmi/DeviceRegistry.hpp" +#include "qdmi/SessionConfig.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace qdmi { +namespace { +template +void overlayValue(std::optional& target, const std::optional& source) { + if (source) { + target = source; + } +} + +void overlay(DeviceSessionConfig& target, const DeviceSessionConfig& source) { + overlayValue(target.baseUrl, source.baseUrl); + overlayValue(target.token, source.token); + overlayValue(target.authFile, source.authFile); + overlayValue(target.authUrl, source.authUrl); + overlayValue(target.username, source.username); + overlayValue(target.password, source.password); + overlayValue(target.deviceConfiguration, source.deviceConfiguration); + overlayValue(target.custom1, source.custom1); + overlayValue(target.custom2, source.custom2); + overlayValue(target.custom3, source.custom3); + overlayValue(target.custom4, source.custom4); + overlayValue(target.custom5, source.custom5); +} +} // namespace + +DeviceManager::DeviceManager() + : registry_(detail::snapshotDefaultDeviceRegistry()) {} + +DeviceManager::DeviceManager(DeviceRegistry registry) + : registry_(std::move(registry)) {} + +Device DeviceManager::open(const std::string_view id, + const DeviceSessionConfig& sessionOverrides) const { + const auto& available = registry_.definitions(); + const auto definition = + std::ranges::find(available, id, &DeviceDefinition::id); + if (definition == available.end()) { + throw std::out_of_range("Unknown QDMI device ID '" + std::string(id) + "'"); + } + const auto library = + detail::loadDeviceApi(definition->library, definition->prefix); + auto parameters = definition->session; + overlay(parameters, sessionOverrides); + return Device(std::make_shared(library, parameters)); +} + +OpenAllResult +DeviceManager::openAll(const DeviceSessionConfig& sessionOverrides) const { + OpenAllResult result; + for (const auto& definition : definitions()) { + try { + result.devices.emplace(definition.id, + open(definition.id, sessionOverrides)); + } catch (const std::exception& error) { + result.errors.emplace(definition.id, error.what()); + } + } + return result; +} + +Device openDevice(const std::string_view id, + const DeviceSessionConfig& sessionOverrides) { + return DeviceManager{}.open(id, sessionOverrides); +} +} // namespace qdmi diff --git a/src/qdmi/driver/DeviceRegistry.cpp b/src/qdmi/DeviceRegistry.cpp similarity index 78% rename from src/qdmi/driver/DeviceRegistry.cpp rename to src/qdmi/DeviceRegistry.cpp index 8d23c2e8cf..cdb41a904f 100644 --- a/src/qdmi/driver/DeviceRegistry.cpp +++ b/src/qdmi/DeviceRegistry.cpp @@ -8,9 +8,10 @@ * Licensed under the MIT License */ -#include "DeviceRegistry.hpp" +#include "qdmi/DeviceRegistry.hpp" -#include "qdmi/driver/Driver.hpp" +#include "DefaultDeviceRegistry.hpp" +#include "qdmi/SessionConfig.hpp" #include // NOLINT(misc-include-cleaner) @@ -19,7 +20,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -31,11 +34,13 @@ #ifdef _WIN32 #include + +#include #else #include #endif -namespace qdmi::detail { +namespace qdmi { namespace { using Json = nlohmann::json; // NOLINT(misc-include-cleaner) @@ -63,8 +68,8 @@ struct DefinitionPatch { std::filesystem::path source; }; -[[nodiscard]] auto sourceLabel(const std::filesystem::path& source, - const std::string_view path) -> std::string { +[[nodiscard]] std::string sourceLabel(const std::filesystem::path& source, + const std::string_view path) { return source.string() + ":" + std::string(path); } @@ -90,10 +95,9 @@ void rejectUnknownKeys(const Json& value, } } -[[nodiscard]] auto optionalString(const Json& value, const std::string& key, - const std::filesystem::path& source, - const std::string& path) - -> std::optional { +[[nodiscard]] std::optional +optionalString(const Json& value, const std::string& key, + const std::filesystem::path& source, const std::string& path) { const auto it = value.find(key); if (it == value.end()) { return std::nullopt; @@ -105,27 +109,25 @@ void rejectUnknownKeys(const Json& value, return it->get(); } -[[nodiscard]] auto resolvePath(std::filesystem::path path, - const std::filesystem::path& base) - -> std::filesystem::path { +[[nodiscard]] std::filesystem::path +resolvePath(std::filesystem::path path, const std::filesystem::path& base) { if (path.is_relative()) { path = base / path; } return path.lexically_normal(); } -[[nodiscard]] auto absolutePath(const std::filesystem::path& path) - -> std::filesystem::path { +[[nodiscard]] std::filesystem::path +absolutePath(const std::filesystem::path& path) { if (path.empty()) { return {}; } return std::filesystem::absolute(path).lexically_normal(); } -[[nodiscard]] auto +[[nodiscard]] SessionPatch parseSessionPatch(const Json& value, const std::filesystem::path& source, - const std::string& path, const std::filesystem::path& base) - -> SessionPatch { + const std::string& path, const std::filesystem::path& base) { requireObject(value, source, path); rejectUnknownKeys(value, {"base-url", "token", "auth-file", "auth-url", "username", @@ -177,10 +179,9 @@ parseSessionPatch(const Json& value, const std::filesystem::path& source, return patch; } -[[nodiscard]] auto +[[nodiscard]] DefinitionPatch parseDevicePatch(const Json& value, const std::filesystem::path& source, - const std::string& path, const std::filesystem::path& base) - -> DefinitionPatch { + const std::string& path, const std::filesystem::path& base) { requireObject(value, source, path); rejectUnknownKeys(value, {"id", "library", "prefix", "enabled", "session"}, source, path); @@ -209,10 +210,9 @@ parseDevicePatch(const Json& value, const std::filesystem::path& source, return patch; } -[[nodiscard]] auto parseConfiguration(const Json& root, - const std::filesystem::path& source, - const std::filesystem::path& base) - -> std::vector { +[[nodiscard]] std::vector +parseConfiguration(const Json& root, const std::filesystem::path& source, + const std::filesystem::path& base) { requireObject(root, source, "$"); rejectUnknownKeys(root, {"schema-version", "qdmi"}, source, "$"); const auto version = root.find("schema-version"); @@ -251,7 +251,7 @@ parseDevicePatch(const Json& value, const std::filesystem::path& source, return patches; } -[[nodiscard]] auto readJson(const std::filesystem::path& path) -> Json { +[[nodiscard]] Json readJson(const std::filesystem::path& path) { std::ifstream stream(path); if (!stream) { throw std::runtime_error("Cannot open QDMI configuration file: " + @@ -295,7 +295,7 @@ void mergePatch(DefinitionPatch& target, const DefinitionPatch& source) { target.source = source.source; } -[[nodiscard]] auto moduleDirectory() -> std::filesystem::path { +[[nodiscard]] std::filesystem::path moduleDirectory() { #ifdef _WIN32 HMODULE module = nullptr; if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | @@ -327,7 +327,7 @@ void mergePatch(DefinitionPatch& target, const DefinitionPatch& source) { #endif } -[[nodiscard]] auto environment(const char* name) -> std::optional { +[[nodiscard]] std::optional environment(const char* name) { #ifdef _WIN32 char* raw = nullptr; size_t size = 0; @@ -381,8 +381,8 @@ void appendFragments(std::vector& files, files.insert(files.end(), found.begin(), found.end()); } -[[nodiscard]] auto nearestProjectConfiguration(std::filesystem::path directory) - -> std::optional { +[[nodiscard]] std::optional +nearestProjectConfiguration(std::filesystem::path directory) { while (!directory.empty()) { auto dedicated = directory / "qdmi.json"; if (std::filesystem::is_regular_file(dedicated)) { @@ -397,7 +397,7 @@ void appendFragments(std::vector& files, return std::nullopt; } -[[nodiscard]] auto discoverFiles() -> std::vector { +[[nodiscard]] std::vector discoverFiles() { std::vector files; const auto root = moduleDirectory(); appendFragments(files, root); @@ -447,8 +447,8 @@ void appendFragments(std::vector& files, return files; } -[[nodiscard]] auto materialize(const DefinitionPatch& patch) - -> std::optional { +[[nodiscard]] std::optional +materialize(const DefinitionPatch& patch) { if (!patch.enabled.value_or(true)) { return std::nullopt; } @@ -460,7 +460,7 @@ void appendFragments(std::vector& files, throw std::invalid_argument(patch.source.string() + ": enabled device '" + patch.id + "' is missing prefix"); } - qdmi::DeviceDefinition definition; + DeviceDefinition definition; definition.id = patch.id; definition.library = *patch.library; definition.prefix = *patch.prefix; @@ -512,11 +512,110 @@ DeviceRegistry::DeviceRegistry() { for (auto& [unused, patch] : merged) { static_cast(unused); if (!patch.enabled.value_or(true)) { - disabledIds_.emplace_back(std::move(patch.id)); + disabledIds_.emplace(std::move(patch.id)); } else if (auto definition = materialize(patch)) { definitions_.emplace_back(std::move(*definition)); } } } -} // namespace qdmi::detail +DeviceRegistry::DeviceRegistry(std::vector definitions) { + for (auto& definition : definitions) { + registerDevice(std::move(definition)); + } +} + +std::vector DeviceRegistry::deviceIds() const { + std::vector ids; + ids.reserve(definitions_.size()); + std::ranges::transform(definitions_, std::back_inserter(ids), + &DeviceDefinition::id); + return ids; +} + +namespace { + +void validateDefinition(const DeviceDefinition& definition) { + if (definition.id.empty()) { + throw std::invalid_argument("Device definition ID must not be empty"); + } + if (definition.library.empty()) { + throw std::invalid_argument("Device definition library must not be empty"); + } + if (definition.prefix.empty()) { + throw std::invalid_argument("Device definition prefix must not be empty"); + } +} + +struct DefaultRegistryState { + std::mutex mutex; + DeviceRegistry registry; +}; + +DefaultRegistryState& defaultRegistryState() { + static DefaultRegistryState state; + return state; +} + +} // namespace + +void DeviceRegistry::registerDevice(DeviceDefinition definition, + const bool replace) { + validateDefinition(definition); + if (disabledIds_.contains(definition.id)) { + if (!replace) { + throw std::invalid_argument("QDMI device ID '" + definition.id + + "' is disabled by configuration"); + } + disabledIds_.erase(definition.id); + } + + const auto existing = + std::ranges::find(definitions_, definition.id, &DeviceDefinition::id); + if (existing == definitions_.end()) { + definitions_.emplace_back(std::move(definition)); + return; + } + if (!replace) { + throw std::invalid_argument("QDMI device ID '" + definition.id + + "' is already registered"); + } + *existing = std::move(definition); +} + +bool DeviceRegistry::registerDeviceIfAbsent(DeviceDefinition definition) { + validateDefinition(definition); + if (disabledIds_.contains(definition.id) || + std::ranges::find(definitions_, definition.id, &DeviceDefinition::id) != + definitions_.end()) { + return false; + } + definitions_.emplace_back(std::move(definition)); + return true; +} + +void registerDevice(DeviceDefinition definition, const bool replace) { + auto& state = defaultRegistryState(); + const std::scoped_lock lock(state.mutex); + state.registry.registerDevice(std::move(definition), replace); +} + +bool registerDeviceIfAbsent(DeviceDefinition definition) { + auto& state = defaultRegistryState(); + const std::scoped_lock lock(state.mutex); + return state.registry.registerDeviceIfAbsent(std::move(definition)); +} + +std::vector registeredDeviceIds() { + auto& state = defaultRegistryState(); + const std::scoped_lock lock(state.mutex); + return state.registry.deviceIds(); +} + +DeviceRegistry detail::snapshotDefaultDeviceRegistry() { + auto& state = defaultRegistryState(); + const std::scoped_lock lock(state.mutex); + return state.registry; +} + +} // namespace qdmi diff --git a/src/qdmi/DeviceState.cpp b/src/qdmi/DeviceState.cpp new file mode 100644 index 0000000000..bc1fd86878 --- /dev/null +++ b/src/qdmi/DeviceState.cpp @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "DeviceState.hpp" + +#include "DeviceApi.hpp" +#include "qdmi/SessionConfig.hpp" +#include "qdmi/common/Common.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace qdmi::detail { +namespace { +[[nodiscard]] QDMI_Device_Session +openSession(const DeviceApi& api, const DeviceSessionConfig& parameters, + QDMI_Child_Device child) { + QDMI_Device_Session session = nullptr; + throwIfError(api.device_session_alloc(&session), + "Allocating QDMI device session"); + try { + const auto set = [&api, session](const std::optional& value, + const QDMI_Device_Session_Parameter key) { + if (!value) { + return; + } + const auto result = api.device_session_set_parameter( + session, key, value->size() + 1, value->c_str()); + if (result == QDMI_ERROR_NOTSUPPORTED) { + SPDLOG_INFO("QDMI device session parameter {} is not supported", + qdmi::toString(key)); + return; + } + throwIfError(result, "Setting QDMI device session parameter " + + std::string(qdmi::toString(key))); + }; + set(parameters.baseUrl, QDMI_DEVICE_SESSION_PARAMETER_BASEURL); + set(parameters.token, QDMI_DEVICE_SESSION_PARAMETER_TOKEN); + if (parameters.authFile) { + set(parameters.authFile->string(), + QDMI_DEVICE_SESSION_PARAMETER_AUTHFILE); + } + set(parameters.authUrl, QDMI_DEVICE_SESSION_PARAMETER_AUTHURL); + set(parameters.username, QDMI_DEVICE_SESSION_PARAMETER_USERNAME); + set(parameters.password, QDMI_DEVICE_SESSION_PARAMETER_PASSWORD); + if (parameters.deviceConfiguration && + (parameters.custom1 || parameters.custom2)) { + throw std::invalid_argument( + "Typed device configuration cannot be combined with raw custom1 or " + "custom2 session parameters"); + } + if (parameters.deviceConfiguration) { + std::visit( + [&set](const auto& source) { + using Source = std::decay_t; + if constexpr (std::same_as) { + set(source.json, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM1); + } else { + set(source.path.string(), QDMI_DEVICE_SESSION_PARAMETER_CUSTOM2); + } + }, + *parameters.deviceConfiguration); + } + set(parameters.custom1, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM1); + set(parameters.custom2, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM2); + set(parameters.custom3, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM3); + set(parameters.custom4, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM4); + set(parameters.custom5, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM5); + if (child != nullptr) { + throwIfError(api.device_session_set_parameter( + session, QDMI_DEVICE_SESSION_PARAMETER_CHILDDEVICE, + sizeof(QDMI_Child_Device), + static_cast(&child)), + "Selecting QDMI child device"); + } + throwIfError(api.device_session_init(session), + "Initializing QDMI device session"); + return session; + } catch (...) { + api.device_session_free(session); + throw; + } +} +} // namespace + +SessionState::SessionState(std::shared_ptr deviceApi, + const DeviceSessionConfig& sessionParameters, + QDMI_Child_Device child, + std::shared_ptr parentSession) + : api(std::move(deviceApi)), + session(openSession(*api, sessionParameters, child)), + parent(std::move(parentSession)) {} + +SessionState::~SessionState() { + if (session != nullptr) { + api->device_session_free(session); + } +} + +DeviceState::DeviceState(std::shared_ptr deviceApi, + const DeviceSessionConfig& sessionParameters, + QDMI_Child_Device child, + std::shared_ptr parentSession) + : sessionState(std::make_shared(std::move(deviceApi), + sessionParameters, child, + std::move(parentSession))), + parameters(sessionParameters) { + if (child != nullptr) { + return; + } + size_t size = 0; + const auto result = api().device_session_query_device_property( + session(), QDMI_DEVICE_PROPERTY_CHILDDEVICES, 0, nullptr, &size); + if (result == QDMI_ERROR_NOTSUPPORTED) { + return; + } + throwIfError(result, "Querying QDMI child devices"); + if (size % sizeof(QDMI_Child_Device) != 0) { + throw std::runtime_error("QDMI device returned an invalid child list"); + } + std::vector handles(size / sizeof(QDMI_Child_Device)); + if (size != 0) { + throwIfError(api().device_session_query_device_property( + session(), QDMI_DEVICE_PROPERTY_CHILDDEVICES, size, + static_cast(handles.data()), nullptr), + "Querying QDMI child devices"); + } + children.reserve(handles.size()); + for (auto* handle : handles) { + children.emplace_back(std::make_shared( + sessionState->api, parameters, handle, sessionState)); + } +} + +JobState::JobState(std::shared_ptr deviceState) + : device(std::move(deviceState)) { + throwIfError( + device->api().device_session_create_device_job(device->session(), &job), + "Creating QDMI device job"); +} + +JobState::JobState(std::shared_ptr deviceState, + QDMI_Device_Job existingJob) + : device(std::move(deviceState)), job(existingJob) {} + +JobState::~JobState() { + if (job != nullptr) { + device->api().device_job_free(job); + } +} +} // namespace qdmi::detail diff --git a/src/qdmi/DeviceState.hpp b/src/qdmi/DeviceState.hpp new file mode 100644 index 0000000000..3d51e578ea --- /dev/null +++ b/src/qdmi/DeviceState.hpp @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "DeviceApi.hpp" +#include "qdmi/Device.hpp" +#include "qdmi/SessionConfig.hpp" + +#include + +#include +#include +#include + +namespace qdmi::detail { + +/// Owns one native QDMI session and keeps its implementation loaded. +/// +/// A child session retains its parent because QDMI child handles originate +/// from the parent session. +struct SessionState { + std::shared_ptr api; + QDMI_Device_Session session = nullptr; + std::shared_ptr parent; + + SessionState(std::shared_ptr deviceApi, + const DeviceSessionConfig& sessionParameters, + QDMI_Child_Device child, + std::shared_ptr parentSession); + ~SessionState(); + SessionState(const SessionState&) = delete; + SessionState& operator=(const SessionState&) = delete; + SessionState(SessionState&&) = delete; + SessionState& operator=(SessionState&&) = delete; +}; + +/// Shared state retained by a Device and every Site or Operation derived from +/// it. Jobs retain the complete DeviceState separately. +struct DeviceState { + std::shared_ptr sessionState; + DeviceSessionConfig parameters; + std::vector> children; + + DeviceState(std::shared_ptr deviceApi, + const DeviceSessionConfig& sessionParameters, + QDMI_Child_Device child = nullptr, + std::shared_ptr parentSession = nullptr); + DeviceState(const DeviceState&) = delete; + DeviceState& operator=(const DeviceState&) = delete; + DeviceState(DeviceState&&) = delete; + DeviceState& operator=(DeviceState&&) = delete; + + [[nodiscard]] const DeviceApi& api() const { return *sessionState->api; } + [[nodiscard]] QDMI_Device_Session session() const { + return sessionState->session; + } +}; + +struct JobState { + std::shared_ptr device; + QDMI_Device_Job job = nullptr; + + explicit JobState(std::shared_ptr deviceState); + JobState(std::shared_ptr deviceState, + QDMI_Device_Job existingJob); + ~JobState(); + JobState(const JobState&) = delete; + JobState& operator=(const JobState&) = delete; + JobState(JobState&&) = delete; + JobState& operator=(JobState&&) = delete; +}; + +/// Test-only construction access for scripted QDMI implementations. +struct DeviceFactory { + [[nodiscard]] static Device + create(std::shared_ptr api, + const DeviceSessionConfig& parameters = {}) { + return Device(std::make_shared(std::move(api), parameters)); + } +}; + +} // namespace qdmi::detail diff --git a/src/qdmi/Slurm.cpp b/src/qdmi/Slurm.cpp index da7ed4e87e..7e1d1f53bb 100644 --- a/src/qdmi/Slurm.cpp +++ b/src/qdmi/Slurm.cpp @@ -10,8 +10,9 @@ #include "qdmi/Slurm.hpp" -#include "qdmi/Client.hpp" -#include "qdmi/driver/Driver.hpp" +#include "qdmi/Device.hpp" +#include "qdmi/DeviceManager.hpp" +#include "qdmi/DeviceRegistry.hpp" #include @@ -126,10 +127,9 @@ Device openDeviceFromLicense() { const auto* const environmentValue = std::getenv("SLURM_JOB_LICENSES"); const std::string licenseSpec = environmentValue == nullptr ? std::string{} : environmentValue; - const auto deviceId = - parseLicense(licenseSpec, qdmi::Driver::get().registeredDeviceIds()); + const auto deviceId = parseLicense(licenseSpec, qdmi::registeredDeviceIds()); - auto device = Session::openDevice(deviceId); + auto device = openDevice(deviceId); const auto status = device.getStatus(); if (status != QDMI_DEVICE_STATUS_IDLE && status != QDMI_DEVICE_STATUS_BUSY) { throw std::runtime_error("SLURM_JOB_LICENSES names QDMI device '" + diff --git a/src/qdmi/driver/CMakeLists.txt b/src/qdmi/driver/CMakeLists.txt deleted file mode 100644 index 44cef1c1bb..0000000000 --- a/src/qdmi/driver/CMakeLists.txt +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM -# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH -# All rights reserved. -# -# SPDX-License-Identifier: MIT -# -# Licensed under the MIT License - -set(TARGET_NAME ${MQT_CORE_TARGET_NAME}-qdmi-driver) - -if(NOT TARGET ${TARGET_NAME}) - # Add driver library - add_mqt_core_library(${TARGET_NAME} ALIAS_NAME QDMIDriver) - - # Add sources to target - target_sources(${TARGET_NAME} PRIVATE DeviceRegistry.cpp Driver.cpp) - - # Add headers using file sets - target_sources( - ${TARGET_NAME} - PUBLIC FILE_SET - HEADERS - BASE_DIRS - ${MQT_CORE_INCLUDE_BUILD_DIR} - FILES - ${MQT_CORE_INCLUDE_BUILD_DIR}/qdmi/driver/Driver.hpp - ${MQT_CORE_INCLUDE_BUILD_DIR}/qdmi/driver/SessionConfig.hpp) - - # Add link libraries - target_link_libraries( - ${TARGET_NAME} - PUBLIC qdmi::qdmi MQT::CoreQDMICommon - PRIVATE qdmi::qdmi_project_warnings $ - spdlog::spdlog ${CMAKE_DL_LIBS}) - - mqt_get_qdmi_device_targets(QDMI_DEVICE_TARGETS) - - # Ensure the driver can find the device libraries at runtime - if(QDMI_DEVICE_TARGETS) - if(WIN32) - mqt_copy_qdmi_runtime(${TARGET_NAME} ${QDMI_DEVICE_TARGETS}) - else() - add_dependencies(${TARGET_NAME} ${QDMI_DEVICE_TARGETS}) - foreach(device IN LISTS QDMI_DEVICE_TARGETS) - target_link_options(${TARGET_NAME} INTERFACE - $>) - endforeach() - endif() - endif() - - # add to list of MQT core targets - list(APPEND MQT_CORE_TARGETS ${TARGET_NAME}) -endif() - -set(MQT_CORE_TARGETS - ${MQT_CORE_TARGETS} - PARENT_SCOPE) diff --git a/src/qdmi/driver/DeviceRegistry.hpp b/src/qdmi/driver/DeviceRegistry.hpp deleted file mode 100644 index 83061f2f62..0000000000 --- a/src/qdmi/driver/DeviceRegistry.hpp +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#pragma once - -#include "qdmi/driver/Driver.hpp" - -#include -#include - -namespace qdmi::detail { - -/// Discovers configured QDMI devices without loading their libraries. -class DeviceRegistry { -public: - DeviceRegistry(); - - [[nodiscard]] const std::vector& definitions() const { - return definitions_; - } - - [[nodiscard]] const std::vector& disabledIds() const { - return disabledIds_; - } - -private: - std::vector definitions_; - std::vector disabledIds_; -}; - -} // namespace qdmi::detail diff --git a/src/qdmi/driver/Driver.cpp b/src/qdmi/driver/Driver.cpp deleted file mode 100644 index 85e8977215..0000000000 --- a/src/qdmi/driver/Driver.cpp +++ /dev/null @@ -1,929 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "qdmi/driver/Driver.hpp" - -#include "DeviceRegistry.hpp" -#include "qdmi/common/Common.hpp" - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef _WIN32 -#include -#else -#include -#endif // _WIN32 - -namespace qdmi { -#ifdef _WIN32 -namespace { -/// Returns the directory of the currently loaded driver library. -[[nodiscard]] auto getDriverDirectory() -> std::filesystem::path { - HMODULE module = nullptr; - if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | - GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, - reinterpret_cast(&getDriverDirectory), - &module) == 0) { - return {}; - } - - std::wstring buffer(MAX_PATH, L'\0'); - DWORD size = 0; - while (true) { - size = GetModuleFileNameW(module, buffer.data(), - static_cast(buffer.size())); - if (size == 0) { - return {}; - } - if (size < buffer.size()) { - buffer.resize(size); - break; - } - buffer.resize(buffer.size() * 2); - } - - return std::filesystem::path(buffer).parent_path(); -} - -/// Loads the device library with the given name, searching in the driver -/// directory if no path is specified. -[[nodiscard]] auto loadDeviceLibrary(const std::string& libName) -> HMODULE { - const auto requested = std::filesystem::path(libName); - // Bare filenames are resolved relative to the Driver. Configured paths are - // already absolute or relative to their declaring file. - const auto path = requested.has_parent_path() - ? requested - : getDriverDirectory() / requested; - // Search beside the device DLL for its dependencies. This is required for - // device implementations such as DDSIM in an installed Python wheel. - return LoadLibraryExW(path.wstring().c_str(), nullptr, - LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | - LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); -} -} // namespace - -#define DL_OPEN(lib) loadDeviceLibrary((lib)) -#define DL_SYM(lib, sym) \ - reinterpret_cast(GetProcAddress(static_cast((lib)), (sym))) -#define DL_CLOSE(lib) FreeLibrary(static_cast((lib))) -#else -#define DL_OPEN(lib) dlopen((lib), RTLD_NOW | RTLD_LOCAL) -#define DL_SYM(lib, sym) dlsym((lib), (sym)) -#define DL_CLOSE(lib) dlclose((lib)) -#endif - -DynamicDeviceLibrary::DynamicDeviceLibrary(const std::string& libName, - const std::string& prefix) - : libHandle_(DL_OPEN(libName.c_str())) { - if (libHandle_ == nullptr) { - throw std::runtime_error("Couldn't open the device library: " + libName); - } - -//===----------------------------------------------------------------------===// -// Macro for loading a symbol from the dynamic library. -// @param symbol is the name of the symbol to load. -#define LOAD_DYNAMIC_SYMBOL(symbol) \ - { \ - const std::string symbolName = std::string(prefix) + "_QDMI_" + #symbol; \ - (symbol) = reinterpret_cast( \ - DL_SYM(libHandle_, symbolName.c_str())); \ - if ((symbol) == nullptr) { \ - throw std::runtime_error("Failed to load symbol: " + symbolName); \ - } \ - } - -#define LOAD_OPTIONAL_DYNAMIC_SYMBOL(symbol) \ - { \ - const std::string symbolName = std::string(prefix) + "_QDMI_" + #symbol; \ - (symbol) = reinterpret_cast( \ - DL_SYM(libHandle_, symbolName.c_str())); \ - } - //===----------------------------------------------------------------------===// - - try { - // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast) - // load the function symbols from the dynamic library - LOAD_DYNAMIC_SYMBOL(device_initialize) - LOAD_DYNAMIC_SYMBOL(device_finalize) - // device session interface - LOAD_DYNAMIC_SYMBOL(device_session_alloc) - LOAD_DYNAMIC_SYMBOL(device_session_init) - LOAD_DYNAMIC_SYMBOL(device_session_free) - LOAD_DYNAMIC_SYMBOL(device_session_set_parameter) - // device job interface - LOAD_DYNAMIC_SYMBOL(device_session_create_device_job) - LOAD_OPTIONAL_DYNAMIC_SYMBOL(device_session_retrieve_device_job_by_id) - LOAD_DYNAMIC_SYMBOL(device_job_free) - LOAD_DYNAMIC_SYMBOL(device_job_set_parameter) - LOAD_DYNAMIC_SYMBOL(device_job_query_property) - LOAD_DYNAMIC_SYMBOL(device_job_submit) - LOAD_DYNAMIC_SYMBOL(device_job_cancel) - LOAD_DYNAMIC_SYMBOL(device_job_check) - LOAD_DYNAMIC_SYMBOL(device_job_wait) - LOAD_DYNAMIC_SYMBOL(device_job_get_results) - // device query interface - LOAD_DYNAMIC_SYMBOL(device_session_query_device_property) - LOAD_DYNAMIC_SYMBOL(device_session_query_site_property) - LOAD_DYNAMIC_SYMBOL(device_session_query_operation_property) - // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast) - // Initialize the device library only after every required symbol is - // available. - throwIfError(device_initialize(), "Failed to initialize device library"); - } catch (...) { - DL_CLOSE(libHandle_); - libHandle_ = nullptr; - throw; - } -} - -#undef LOAD_OPTIONAL_DYNAMIC_SYMBOL -#undef LOAD_DYNAMIC_SYMBOL - -DynamicDeviceLibrary::~DynamicDeviceLibrary() { - // Check if QDMI_device_finalize is not NULL before calling it. - if (device_finalize != nullptr) { - device_finalize(); - } - // close the dynamic library - if (libHandle_ != nullptr) { - DL_CLOSE(libHandle_); - } -} - -namespace { -struct DynamicLibraryCache { - std::mutex mutex; - std::map, - std::weak_ptr> - libraries; -}; - -[[nodiscard]] auto dynamicLibraryCache() -> DynamicLibraryCache& { - static DynamicLibraryCache cache; - return cache; -} - -[[nodiscard]] auto getDynamicDeviceLibrary(const std::string& libName, - const std::string& prefix) - -> std::shared_ptr { - auto& cache = dynamicLibraryCache(); - const std::scoped_lock lock(cache.mutex); - std::error_code error; - auto canonicalPath = std::filesystem::weakly_canonical( - std::filesystem::absolute(std::filesystem::path(libName), error), error); - if (error) { - canonicalPath = std::filesystem::path(libName).lexically_normal(); - } - const auto key = std::pair{canonicalPath.string(), prefix}; - if (const auto library = cache.libraries[key].lock()) { - return library; - } - auto library = std::make_shared(libName, prefix); - cache.libraries[key] = library; - return library; -} - -template -void applyOverride(std::optional& value, - const std::optional& overrideValue) { - if (overrideValue) { - value = overrideValue; - } -} - -[[nodiscard]] auto mergeSessionConfig(const DeviceSessionConfig& defaults, - const DeviceSessionConfig& overrides) - -> DeviceSessionConfig { - auto merged = defaults; - applyOverride(merged.baseUrl, overrides.baseUrl); - applyOverride(merged.token, overrides.token); - applyOverride(merged.authFile, overrides.authFile); - applyOverride(merged.authUrl, overrides.authUrl); - applyOverride(merged.username, overrides.username); - applyOverride(merged.password, overrides.password); - applyOverride(merged.deviceConfiguration, overrides.deviceConfiguration); - applyOverride(merged.custom1, overrides.custom1); - applyOverride(merged.custom2, overrides.custom2); - applyOverride(merged.custom3, overrides.custom3); - applyOverride(merged.custom4, overrides.custom4); - applyOverride(merged.custom5, overrides.custom5); - return merged; -} -} // namespace - -#undef DL_OPEN -#undef DL_SYM -#undef DL_CLOSE -} // namespace qdmi - -QDMI_Device_impl_d::QDMI_Device_impl_d( - std::shared_ptr lib, - const qdmi::DeviceSessionConfig& config, - QDMI_Child_Device_impl_d* const childDevice) - : library_(std::move(lib)) { - if (library_->device_session_alloc(&deviceSession_) != QDMI_SUCCESS) { - throw std::runtime_error("Failed to allocate device session"); - } - - // Set device session parameters from config - auto setParameter = [this](const std::optional& value, - QDMI_Device_Session_Parameter param) { - if (value && library_->device_session_set_parameter) { - const auto status = - static_cast(library_->device_session_set_parameter( - deviceSession_, param, value->size() + 1, value->c_str())); - if (status == QDMI_SUCCESS) { - return; - } - - if (status == QDMI_ERROR_NOTSUPPORTED) { - SPDLOG_INFO( - "Device session parameter {} not supported by device (skipped)", - qdmi::toString(param)); - return; - } - library_->device_session_free(deviceSession_); - std::ostringstream ss; - ss << "Failed to set device session parameter " << qdmi::toString(param) - << ": " << qdmi::toString(status); - throw std::runtime_error(ss.str()); - } - }; - - setParameter(config.baseUrl, QDMI_DEVICE_SESSION_PARAMETER_BASEURL); - setParameter(config.token, QDMI_DEVICE_SESSION_PARAMETER_TOKEN); - if (config.authFile) { - const std::optional authFile = config.authFile->string(); - setParameter(authFile, QDMI_DEVICE_SESSION_PARAMETER_AUTHFILE); - } - setParameter(config.authUrl, QDMI_DEVICE_SESSION_PARAMETER_AUTHURL); - setParameter(config.username, QDMI_DEVICE_SESSION_PARAMETER_USERNAME); - setParameter(config.password, QDMI_DEVICE_SESSION_PARAMETER_PASSWORD); - if (config.deviceConfiguration && - (config.custom1.has_value() || config.custom2.has_value())) { - library_->device_session_free(deviceSession_); - deviceSession_ = nullptr; - throw std::invalid_argument( - "Typed device configuration cannot be combined with raw custom1 or " - "custom2 session parameters"); - } - if (config.deviceConfiguration) { - std::visit( - [&](const auto& source) { - using Source = std::decay_t; - if constexpr (std::is_same_v) { - setParameter(std::optional{source.json}, - QDMI_DEVICE_SESSION_PARAMETER_CUSTOM1); - } else { - setParameter(std::optional{source.path.string()}, - QDMI_DEVICE_SESSION_PARAMETER_CUSTOM2); - } - }, - *config.deviceConfiguration); - } - setParameter(config.custom1, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM1); - setParameter(config.custom2, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM2); - setParameter(config.custom3, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM3); - setParameter(config.custom4, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM4); - setParameter(config.custom5, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM5); - - if (childDevice != nullptr) { - const auto status = - static_cast(library_->device_session_set_parameter( - deviceSession_, QDMI_DEVICE_SESSION_PARAMETER_CHILDDEVICE, - sizeof(QDMI_Child_Device), static_cast(&childDevice))); - if (status != QDMI_SUCCESS) { - library_->device_session_free(deviceSession_); - deviceSession_ = nullptr; - std::ostringstream ss; - ss << "Failed to select child device: " << qdmi::toString(status); - throw std::runtime_error(ss.str()); - } - } - - if (library_->device_session_init(deviceSession_) != QDMI_SUCCESS) { - library_->device_session_free(deviceSession_); - deviceSession_ = nullptr; - throw std::runtime_error("Failed to initialize device session"); - } - - // Child sessions represent leaf devices in the QDMI multicore - // workflow. Only top-level sessions discover and wrap child handles. - if (childDevice != nullptr) { - return; - } - - size_t childrenSize = 0; - auto status = - static_cast(library_->device_session_query_device_property( - deviceSession_, QDMI_DEVICE_PROPERTY_CHILDDEVICES, 0, nullptr, - &childrenSize)); - if (status == QDMI_ERROR_NOTSUPPORTED) { - return; - } - if (status != QDMI_SUCCESS || childrenSize % sizeof(QDMI_Child_Device) != 0) { - library_->device_session_free(deviceSession_); - deviceSession_ = nullptr; - if (status != QDMI_SUCCESS) { - throw std::runtime_error("Failed to query child devices: " + - std::string(qdmi::toString(status))); - } - throw std::runtime_error("Device returned an invalid child device list"); - } - - std::vector children(childrenSize / - sizeof(QDMI_Child_Device)); - if (childrenSize != 0) { - status = - static_cast(library_->device_session_query_device_property( - deviceSession_, QDMI_DEVICE_PROPERTY_CHILDDEVICES, childrenSize, - static_cast(children.data()), nullptr)); - if (status != QDMI_SUCCESS) { - library_->device_session_free(deviceSession_); - deviceSession_ = nullptr; - throw std::runtime_error("Failed to query child devices: " + - std::string(qdmi::toString(status))); - } - } - - try { - childDevices_.reserve(children.size()); - for (auto* const child : children) { - childDevices_.emplace_back( - std::make_unique(library_, config, child)); - } - } catch (...) { - childDevices_.clear(); - library_->device_session_free(deviceSession_); - deviceSession_ = nullptr; - throw; - } -} - -auto QDMI_Device_impl_d::createJob(QDMI_Job* job) -> int { - if (job == nullptr) { - return QDMI_ERROR_INVALIDARGUMENT; - } - QDMI_Device_Job deviceJob = nullptr; - auto result = - library_->device_session_create_device_job(deviceSession_, &deviceJob); - if (result != QDMI_SUCCESS) { - return result; - } - auto uniqueJob = std::make_unique(deviceJob, this); - const auto it = jobs_.emplace(uniqueJob.get(), std::move(uniqueJob)).first; - *job = it->first; - return QDMI_SUCCESS; -} - -auto QDMI_Device_impl_d::retrieveJobById(const char* const jobId, - QDMI_Job* const job) -> int { - if (jobId == nullptr || *jobId == '\0' || job == nullptr) { - return QDMI_ERROR_INVALIDARGUMENT; - } - if (library_->device_session_retrieve_device_job_by_id == nullptr) { - return QDMI_ERROR_NOTSUPPORTED; - } - QDMI_Device_Job deviceJob = nullptr; - const auto result = library_->device_session_retrieve_device_job_by_id( - deviceSession_, jobId, &deviceJob); - if (result != QDMI_SUCCESS) { - return result; - } - auto uniqueJob = std::make_unique(deviceJob, this); - const auto it = jobs_.emplace(uniqueJob.get(), std::move(uniqueJob)).first; - *job = it->first; - return QDMI_SUCCESS; -} - -auto QDMI_Device_impl_d::freeJob(QDMI_Job job) -> void { - if (job != nullptr) { - jobs_.erase(job); - } -} - -auto QDMI_Device_impl_d::queryDeviceProperty(QDMI_Device_Property prop, - const size_t size, void* value, - size_t* sizeRet) const -> int { - if (prop == QDMI_DEVICE_PROPERTY_CHILDDEVICES) { - if (childDevices_.empty()) { - return QDMI_ERROR_NOTSUPPORTED; - } - const auto requiredSize = childDevices_.size() * sizeof(QDMI_Device); - if (value != nullptr) { - if (size < requiredSize) { - return QDMI_ERROR_INVALIDARGUMENT; - } - auto* devices = static_cast(value); - std::ranges::transform( - childDevices_, devices, - [](const auto& child) -> QDMI_Device { return child.get(); }); - } - if (sizeRet != nullptr) { - *sizeRet = requiredSize; - } - return QDMI_SUCCESS; - } - return library_->device_session_query_device_property(deviceSession_, prop, - size, value, sizeRet); -} - -auto QDMI_Device_impl_d::querySiteProperty(QDMI_Site site, - QDMI_Site_Property prop, - const size_t size, void* value, - size_t* sizeRet) const -> int { - return library_->device_session_query_site_property( - deviceSession_, site, prop, size, value, sizeRet); -} - -auto QDMI_Device_impl_d::queryOperationProperty( - QDMI_Operation operation, const size_t numSites, const QDMI_Site* sites, - const size_t numParams, const double* params, QDMI_Operation_Property prop, - const size_t size, void* value, size_t* sizeRet) const -> int { - return library_->device_session_query_operation_property( - deviceSession_, operation, numSites, sites, numParams, params, prop, size, - value, sizeRet); -} - -namespace { -[[nodiscard]] auto toDeviceJobParameter(const QDMI_Job_Parameter& param) - -> QDMI_Device_Job_Parameter { - switch (param) { - case QDMI_JOB_PARAMETER_PROGRAM: - return QDMI_DEVICE_JOB_PARAMETER_PROGRAM; - case QDMI_JOB_PARAMETER_PROGRAMFORMAT: - return QDMI_DEVICE_JOB_PARAMETER_PROGRAMFORMAT; - case QDMI_JOB_PARAMETER_SHOTSNUM: - return QDMI_DEVICE_JOB_PARAMETER_SHOTSNUM; - case QDMI_JOB_PARAMETER_CUSTOM1: - return QDMI_DEVICE_JOB_PARAMETER_CUSTOM1; - case QDMI_JOB_PARAMETER_CUSTOM2: - return QDMI_DEVICE_JOB_PARAMETER_CUSTOM2; - case QDMI_JOB_PARAMETER_CUSTOM3: - return QDMI_DEVICE_JOB_PARAMETER_CUSTOM3; - case QDMI_JOB_PARAMETER_CUSTOM4: - return QDMI_DEVICE_JOB_PARAMETER_CUSTOM4; - case QDMI_JOB_PARAMETER_CUSTOM5: - return QDMI_DEVICE_JOB_PARAMETER_CUSTOM5; - default: - return QDMI_DEVICE_JOB_PARAMETER_MAX; - } -} -} // namespace - -QDMI_Session_impl_d::QDMI_Session_impl_d( - const std::vector>& devices) { - devices_.reserve(devices.size()); - std::ranges::transform(devices, std::back_inserter(devices_), - [](const auto& device) { return device.get(); }); -} - -QDMI_Session_impl_d::QDMI_Session_impl_d( - const std::vector& devices) - : devices_(devices) {} - -QDMI_Job_impl_d::~QDMI_Job_impl_d() { - device_->getLibrary().device_job_free(deviceJob_); -} -auto QDMI_Job_impl_d::setParameter(QDMI_Job_Parameter param, const size_t size, - const void* value) const -> int { - if ((value != nullptr && size == 0) || - IS_INVALID_ARGUMENT(param, QDMI_JOB_PARAMETER)) { - return QDMI_ERROR_INVALIDARGUMENT; - } - return device_->getLibrary().device_job_set_parameter( - deviceJob_, toDeviceJobParameter(param), size, value); -} - -namespace { -[[nodiscard]] auto toDeviceJobProperty(const QDMI_Job_Property& prop) - -> QDMI_Device_Job_Property { - switch (prop) { - case QDMI_JOB_PROPERTY_ID: - return QDMI_DEVICE_JOB_PROPERTY_ID; - case QDMI_JOB_PROPERTY_PROGRAM: - return QDMI_DEVICE_JOB_PROPERTY_PROGRAM; - case QDMI_JOB_PROPERTY_PROGRAMFORMAT: - return QDMI_DEVICE_JOB_PROPERTY_PROGRAMFORMAT; - case QDMI_JOB_PROPERTY_SHOTSNUM: - return QDMI_DEVICE_JOB_PROPERTY_SHOTSNUM; - case QDMI_JOB_PROPERTY_QUEUEPOSITION: - return QDMI_DEVICE_JOB_PROPERTY_QUEUEPOSITION; - case QDMI_JOB_PROPERTY_CUSTOM1: - return QDMI_DEVICE_JOB_PROPERTY_CUSTOM1; - case QDMI_JOB_PROPERTY_CUSTOM2: - return QDMI_DEVICE_JOB_PROPERTY_CUSTOM2; - case QDMI_JOB_PROPERTY_CUSTOM3: - return QDMI_DEVICE_JOB_PROPERTY_CUSTOM3; - case QDMI_JOB_PROPERTY_CUSTOM4: - return QDMI_DEVICE_JOB_PROPERTY_CUSTOM4; - case QDMI_JOB_PROPERTY_CUSTOM5: - return QDMI_DEVICE_JOB_PROPERTY_CUSTOM5; - default: - return QDMI_DEVICE_JOB_PROPERTY_MAX; - } -} -} // namespace - -auto QDMI_Job_impl_d::queryProperty(QDMI_Job_Property prop, const size_t size, - void* value, size_t* sizeRet) const -> int { - return device_->getLibrary().device_job_query_property( - deviceJob_, toDeviceJobProperty(prop), size, value, sizeRet); -} - -auto QDMI_Job_impl_d::submit() const -> int { - return device_->getLibrary().device_job_submit(deviceJob_); -} - -auto QDMI_Job_impl_d::cancel() const -> int { - return device_->getLibrary().device_job_cancel(deviceJob_); -} - -auto QDMI_Job_impl_d::check(QDMI_Job_Status* status) const -> int { - return device_->getLibrary().device_job_check(deviceJob_, status); -} - -auto QDMI_Job_impl_d::wait(size_t timeout) const -> int { - return device_->getLibrary().device_job_wait(deviceJob_, timeout); -} - -auto QDMI_Job_impl_d::getResults(QDMI_Job_Result result, const size_t size, - void* data, size_t* sizeRet) const -> int { - return device_->getLibrary().device_job_get_results(deviceJob_, result, size, - data, sizeRet); -} - -auto QDMI_Job_impl_d::free() -> void { device_->freeJob(this); } - -auto QDMI_Session_impl_d::init() -> int { - if (status_ != qdmi::SessionStatus::ALLOCATED) { - return QDMI_ERROR_BADSTATE; - } - status_ = qdmi::SessionStatus::INITIALIZED; - return QDMI_SUCCESS; -} - -auto QDMI_Session_impl_d::setParameter(QDMI_Session_Parameter param, - const size_t size, - const void* value) const -> int { - if ((value != nullptr && size == 0) || param >= QDMI_SESSION_PARAMETER_MAX) { - return QDMI_ERROR_INVALIDARGUMENT; - } - if (status_ != qdmi::SessionStatus::ALLOCATED) { - return QDMI_ERROR_BADSTATE; - } - return QDMI_ERROR_NOTSUPPORTED; -} - -auto QDMI_Session_impl_d::querySessionProperty(QDMI_Session_Property prop, - size_t size, void* value, - size_t* sizeRet) const -> int { - if ((value != nullptr && size == 0) || prop >= QDMI_SESSION_PROPERTY_MAX) { - return QDMI_ERROR_INVALIDARGUMENT; - } - if (status_ != qdmi::SessionStatus::INITIALIZED) { - return QDMI_ERROR_BADSTATE; - } - if (prop == QDMI_SESSION_PROPERTY_DEVICES) { - if (value != nullptr) { - if (size < devices_.size() * sizeof(QDMI_Device)) { - return QDMI_ERROR_INVALIDARGUMENT; - } - memcpy(value, static_cast(devices_.data()), - devices_.size() * sizeof(QDMI_Device)); - } - if (sizeRet != nullptr) { - *sizeRet = devices_.size() * sizeof(QDMI_Device); - } - return QDMI_SUCCESS; - } - return QDMI_ERROR_NOTSUPPORTED; -} - -namespace qdmi { -namespace { -void validateDefinition(const DeviceDefinition& definition) { - if (definition.id.empty()) { - throw std::invalid_argument("Device definition ID must not be empty"); - } - if (definition.library.empty()) { - throw std::invalid_argument("Device definition library must not be empty"); - } - if (definition.prefix.empty()) { - throw std::invalid_argument("Device definition prefix must not be empty"); - } - if (definition.session.deviceConfiguration && - (definition.session.custom1 || definition.session.custom2)) { - throw std::invalid_argument( - "Typed device configuration cannot be combined with raw custom1 or " - "custom2 session parameters"); - } -} -} // namespace - -auto Driver::get() -> Driver& { - // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - static auto* instance = new Driver(); - return *instance; -} - -Driver::Driver() { - const detail::DeviceRegistry registry; - disabledDeviceIds_.insert(registry.disabledIds().begin(), - registry.disabledIds().end()); - for (const auto& definition : registry.definitions()) { - registerDevice(definition); - clientDefinitionIds_.emplace_back(definition.id); - } -} - -void Driver::registerDevice(DeviceDefinition definition, const bool replace) { - validateDefinition(definition); - if (disabledDeviceIds_.contains(definition.id)) { - if (!replace) { - throw std::invalid_argument("QDMI device ID '" + definition.id + - "' is disabled by configuration"); - } - disabledDeviceIds_.erase(definition.id); - } - const auto existing = - std::ranges::find(definitions_, definition.id, &DeviceDefinition::id); - if (existing == definitions_.end()) { - definitions_.emplace_back(std::move(definition)); - return; - } - if (!replace) { - throw std::invalid_argument("QDMI device ID '" + definition.id + - "' is already registered"); - } - if (openedDevices_.contains(definition.id)) { - throw std::runtime_error("Cannot replace opened QDMI device ID '" + - definition.id + "'"); - } - *existing = std::move(definition); -} - -auto Driver::registerDeviceIfAbsent(DeviceDefinition definition) -> bool { - validateDefinition(definition); - if (disabledDeviceIds_.contains(definition.id) || - std::ranges::find(definitions_, definition.id, &DeviceDefinition::id) != - definitions_.end()) { - return false; - } - definitions_.emplace_back(std::move(definition)); - return true; -} - -auto Driver::registeredDeviceIds() const -> std::vector { - std::vector ids; - ids.reserve(definitions_.size()); - std::ranges::transform(definitions_, std::back_inserter(ids), - &DeviceDefinition::id); - return ids; -} - -auto Driver::open(const std::string_view id) -> QDMI_Device { - if (disabledDeviceIds_.contains(std::string(id))) { - throw std::runtime_error("QDMI device ID '" + std::string(id) + - "' is disabled by configuration"); - } - if (const auto opened = openedDevices_.find(std::string(id)); - opened != openedDevices_.end()) { - return opened->second; - } - const auto definition = - std::ranges::find(definitions_, id, &DeviceDefinition::id); - if (definition == definitions_.end()) { - throw std::out_of_range("Unknown QDMI device ID '" + std::string(id) + "'"); - } - devices_.emplace_back(std::make_unique( - getDynamicDeviceLibrary(definition->library.string(), definition->prefix), - definition->session)); - auto* const device = devices_.back().get(); - openedDevices_.emplace(definition->id, device); - return device; -} - -auto Driver::openFresh(const std::string_view id, - const DeviceSessionConfig& overrides) - -> std::shared_ptr { - if (disabledDeviceIds_.contains(std::string(id))) { - throw std::runtime_error("QDMI device ID '" + std::string(id) + - "' is disabled by configuration"); - } - const auto definition = - std::ranges::find(definitions_, id, &DeviceDefinition::id); - if (definition == definitions_.end()) { - throw std::out_of_range("Unknown QDMI device ID '" + std::string(id) + "'"); - } - return std::make_shared( - getDynamicDeviceLibrary(definition->library.string(), definition->prefix), - mergeSessionConfig(definition->session, overrides)); -} - -void Driver::materializeClientCatalog() { - if (clientCatalogMaterialized_) { - return; - } - clientCatalogMaterialized_ = true; - for (const auto& id : clientDefinitionIds_) { - try { - clientDevices_.emplace_back(open(id)); - } catch (const std::exception& ex) { - const auto definition = - std::ranges::find(definitions_, id, &DeviceDefinition::id); - const auto library = definition == definitions_.end() - ? std::string("") - : definition->library.string(); - SPDLOG_WARN("Skipping configured QDMI device '{}' from '{}': {}", id, - library, ex.what()); - } - } -} - -auto Driver::sessionAlloc(QDMI_Session* session) -> int { - if (session == nullptr) { - return QDMI_ERROR_INVALIDARGUMENT; - } - materializeClientCatalog(); - auto uniqueSession = std::make_unique(clientDevices_); - *session = sessions_.emplace(uniqueSession.get(), std::move(uniqueSession)) - .first->first; - return QDMI_SUCCESS; -} - -auto Driver::sessionFree(QDMI_Session session) -> void { - if (session != nullptr) { - sessions_.erase(session); - } -} -} // namespace qdmi - -int QDMI_session_alloc(QDMI_Session* session) { - return qdmi::Driver::get().sessionAlloc(session); -} - -int QDMI_session_init(QDMI_Session session) { - if (session == nullptr) { - return QDMI_ERROR_INVALIDARGUMENT; - } - return session->init(); -} - -void QDMI_session_free(QDMI_Session session) { - qdmi::Driver::get().sessionFree(session); -} - -int QDMI_session_set_parameter(QDMI_Session session, - QDMI_Session_Parameter param, const size_t size, - const void* value) { - if (session == nullptr) { - return QDMI_ERROR_INVALIDARGUMENT; - } - return session->setParameter(param, size, value); -} - -int QDMI_session_query_session_property(QDMI_Session session, - QDMI_Session_Property prop, size_t size, - void* value, size_t* sizeRet) { - if (session == nullptr) { - return QDMI_ERROR_INVALIDARGUMENT; - } - return session->querySessionProperty(prop, size, value, sizeRet); -} - -int QDMI_device_create_job(QDMI_Device dev, QDMI_Job* job) { - if (dev == nullptr) { - return QDMI_ERROR_INVALIDARGUMENT; - } - return dev->createJob(job); -} - -int QDMI_session_retrieve_job_by_id(QDMI_Device device, const char* jobId, - QDMI_Job* job) { - if (device == nullptr) { - return QDMI_ERROR_INVALIDARGUMENT; - } - return device->retrieveJobById(jobId, job); -} - -void QDMI_job_free(QDMI_Job job) { - if (job != nullptr) { - job->free(); - } -} - -int QDMI_job_set_parameter(QDMI_Job job, QDMI_Job_Parameter param, - const size_t size, const void* value) { - if (job == nullptr) { - return QDMI_ERROR_INVALIDARGUMENT; - } - return job->setParameter(param, size, value); -} - -int QDMI_job_query_property(QDMI_Job job, QDMI_Job_Property prop, - const size_t size, void* value, size_t* sizeRet) { - if (job == nullptr) { - return QDMI_ERROR_INVALIDARGUMENT; - } - return job->queryProperty(prop, size, value, sizeRet); -} - -int QDMI_job_submit(QDMI_Job job) { - if (job == nullptr) { - return QDMI_ERROR_INVALIDARGUMENT; - } - return job->submit(); -} - -int QDMI_job_cancel(QDMI_Job job) { - if (job == nullptr) { - return QDMI_ERROR_INVALIDARGUMENT; - } - return job->cancel(); -} - -int QDMI_job_check(QDMI_Job job, QDMI_Job_Status* status) { - if (job == nullptr) { - return QDMI_ERROR_INVALIDARGUMENT; - } - return job->check(status); -} - -int QDMI_job_wait(QDMI_Job job, size_t timeout) { - if (job == nullptr) { - return QDMI_ERROR_INVALIDARGUMENT; - } - return job->wait(timeout); -} - -int QDMI_job_get_results(QDMI_Job job, QDMI_Job_Result result, - const size_t size, void* data, size_t* sizeRet) { - if (job == nullptr) { - return QDMI_ERROR_INVALIDARGUMENT; - } - return job->getResults(result, size, data, sizeRet); -} - -int QDMI_device_query_device_property(QDMI_Device device, - QDMI_Device_Property prop, - const size_t size, void* value, - size_t* sizeRet) { - if (device == nullptr) { - return QDMI_ERROR_INVALIDARGUMENT; - } - return device->queryDeviceProperty(prop, size, value, sizeRet); -} - -int QDMI_device_query_site_property(QDMI_Device device, QDMI_Site site, - QDMI_Site_Property prop, const size_t size, - void* value, size_t* sizeRet) { - if (device == nullptr) { - return QDMI_ERROR_INVALIDARGUMENT; - } - return device->querySiteProperty(site, prop, size, value, sizeRet); -} - -int QDMI_device_query_operation_property( - QDMI_Device device, QDMI_Operation operation, const size_t numSites, - const QDMI_Site* sites, const size_t numParams, const double* params, - QDMI_Operation_Property prop, const size_t size, void* value, - size_t* sizeRet) { - if (device == nullptr) { - return QDMI_ERROR_INVALIDARGUMENT; - } - return device->queryOperationProperty(operation, numSites, sites, numParams, - params, prop, size, value, sizeRet); -} diff --git a/test/python/qdmi/test_qdmi.py b/test/python/qdmi/test_qdmi.py index e2fb966ce1..c65efd00bf 100644 --- a/test/python/qdmi/test_qdmi.py +++ b/test/python/qdmi/test_qdmi.py @@ -26,9 +26,9 @@ ) from mqt.core.qdmi.driver import ( DeviceDefinition, + DeviceManager, + DeviceRegistry, open_device, - register_device, - register_device_if_absent, registered_device_ids, ) @@ -815,28 +815,35 @@ def test_register_device_does_not_load_nonexistent_library() -> None: assert definition.device_id == "python.missing" assert definition.library_path == library_path assert definition.prefix == "PREFIX" - register_device(definition) + registry = DeviceRegistry([definition]) + manager = DeviceManager(registry) + assert registry.device_ids == ["python.missing"] + assert manager.device_ids == ["python.missing"] with pytest.raises(RuntimeError): - open_device("python.missing") + manager.open("python.missing") + + result = manager.open_all() + assert result.devices == {} + assert "Could not open QDMI device library" in result.errors["python.missing"] def test_register_device_if_absent_only_ignores_existing_id() -> None: """Idempotent registration still validates duplicate definitions.""" definition = DeviceDefinition("python.if-absent", "/nonexistent/device.so", "PREFIX") - assert register_device_if_absent(definition) - assert not register_device_if_absent(definition) + registry = DeviceRegistry([]) + assert registry.register_device_if_absent(definition) + assert not registry.register_device_if_absent(definition) with pytest.raises(ValueError, match="library must not be empty"): - register_device_if_absent(DeviceDefinition("python.if-absent", "", "PREFIX")) + registry.register_device_if_absent(DeviceDefinition("python.if-absent", "", "PREFIX")) def test_registered_device_ids_include_runtime_registrations_in_order() -> None: """Stable-ID enumeration is ordered and does not load native libraries.""" - ids_before = registered_device_ids() - register_device(DeviceDefinition("python.enumeration.first", "/nonexistent/first.so", "FIRST")) - register_device(DeviceDefinition("python.enumeration.second", "/nonexistent/second.so", "SECOND")) + registry = DeviceRegistry([]) + registry.register_device(DeviceDefinition("python.enumeration.first", "/nonexistent/first.so", "FIRST")) + registry.register_device(DeviceDefinition("python.enumeration.second", "/nonexistent/second.so", "SECOND")) - assert registered_device_ids() == [ - *ids_before, + assert registry.device_ids == [ "python.enumeration.first", "python.enumeration.second", ] diff --git a/test/qdmi/CMakeLists.txt b/test/qdmi/CMakeLists.txt index fa7e935a8e..3f44adce46 100644 --- a/test/qdmi/CMakeLists.txt +++ b/test/qdmi/CMakeLists.txt @@ -7,16 +7,6 @@ # Licensed under the MIT License add_subdirectory(devices) -add_subdirectory(driver) +add_subdirectory(device) +add_subdirectory(manager) add_subdirectory(registry) - -set(TARGET_NAME mqt-core-qdmi-test) - -if(TARGET MQT::CoreQDMI) - package_add_test(${TARGET_NAME} MQT::CoreQDMI test_client.cpp test_slurm.cpp) - target_compile_definitions( - ${TARGET_NAME} - PRIVATE "MQT_CORE_QDMI_SLURM_TEST_DEVICE=\"$\"") - add_dependencies(${TARGET_NAME} mqt-core-qdmi-session-device) - mqt_copy_qdmi_runtime(${TARGET_NAME}) -endif() diff --git a/test/qdmi/device/CMakeLists.txt b/test/qdmi/device/CMakeLists.txt new file mode 100644 index 0000000000..5a93c9ae12 --- /dev/null +++ b/test/qdmi/device/CMakeLists.txt @@ -0,0 +1,156 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +if(NOT TARGET MQT::CoreQDMI) + return() +endif() + +add_library(mqt-core-qdmi-session-device SHARED session_device.cpp) +target_link_libraries(mqt-core-qdmi-session-device PRIVATE qdmi::qdmi) +set_target_properties( + mqt-core-qdmi-session-device + PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/device" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/device") + +add_library(mqt-core-qdmi-metadata-device SHARED metadata_device.cpp) +set_target_properties( + mqt-core-qdmi-metadata-device + PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/device" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/device" + QDMI_DEVICE_ID "test.metadata-only" + QDMI_DEVICE_PREFIX "TEST_METADATA" + QDMI_RUNTIME_FILES "metadata-runtime.json") +add_custom_command( + TARGET mqt-core-qdmi-metadata-device + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/metadata-runtime.json" + "$/metadata-runtime.json") +set_property( + TARGET mqt-core-qdmi-metadata-device + APPEND + PROPERTY EXPORT_PROPERTIES QDMI_DEVICE_ID QDMI_DEVICE_PREFIX QDMI_RUNTIME_FILES) +set(metadata_device_export + "${CMAKE_CURRENT_BINARY_DIR}/mqt-core-qdmi-metadata-device-targets.cmake") +export(TARGETS mqt-core-qdmi-metadata-device FILE "${metadata_device_export}") + +set(imported_device_build_dir "${CMAKE_CURRENT_BINARY_DIR}/imported-device-consumer") +set(imported_device_configure_command + ${CMAKE_COMMAND} -S "${CMAKE_CURRENT_SOURCE_DIR}/imported_device" -B + "${imported_device_build_dir}" -G "${CMAKE_GENERATOR}" + "-DMQT_CORE_QDMI_DEVICE_TARGETS=${metadata_device_export}" + "-DMQT_CORE_QDMI_HELPER=${PROJECT_SOURCE_DIR}/cmake/AddMQTQDMIDevice.cmake") +if(CMAKE_GENERATOR_PLATFORM) + list(APPEND imported_device_configure_command -A "${CMAKE_GENERATOR_PLATFORM}") +endif() +if(CMAKE_GENERATOR_TOOLSET) + list(APPEND imported_device_configure_command -T "${CMAKE_GENERATOR_TOOLSET}") +endif() +add_test(NAME mqt-core-qdmi-imported-device-configure COMMAND ${imported_device_configure_command}) +add_test(NAME mqt-core-qdmi-imported-device-build + COMMAND ${CMAKE_COMMAND} --build "${imported_device_build_dir}" --config $) +set_tests_properties(mqt-core-qdmi-imported-device-configure + PROPERTIES FIXTURES_SETUP mqt-core-qdmi-imported-device) +set_tests_properties(mqt-core-qdmi-imported-device-build PROPERTIES FIXTURES_REQUIRED + mqt-core-qdmi-imported-device) + +set(runtime_file_device_build_dir "${CMAKE_CURRENT_BINARY_DIR}/runtime-file-device") +set(runtime_file_device_install_dir "${runtime_file_device_build_dir}/install") +if(WIN32) + set(runtime_file_device_artifact_dir "${CMAKE_INSTALL_BINDIR}") +else() + set(runtime_file_device_artifact_dir "${CMAKE_INSTALL_LIBDIR}") +endif() +set(runtime_file_device_configure_command + ${CMAKE_COMMAND} -S "${CMAKE_CURRENT_SOURCE_DIR}/runtime_file_device" -B + "${runtime_file_device_build_dir}" -G "${CMAKE_GENERATOR}" + "-DMQT_CORE_QDMI_HELPER=${PROJECT_SOURCE_DIR}/cmake/AddMQTQDMIDevice.cmake" + "-DMQT_CORE_QDMI_TEST_SOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR}" "-DCMAKE_BUILD_TYPE=$") +if(CMAKE_GENERATOR_PLATFORM) + list(APPEND runtime_file_device_configure_command -A "${CMAKE_GENERATOR_PLATFORM}") +endif() +if(CMAKE_GENERATOR_TOOLSET) + list(APPEND runtime_file_device_configure_command -T "${CMAKE_GENERATOR_TOOLSET}") +endif() +add_test(NAME mqt-core-qdmi-runtime-file-configure COMMAND ${runtime_file_device_configure_command}) +add_test(NAME mqt-core-qdmi-runtime-file-build + COMMAND ${CMAKE_COMMAND} --build "${runtime_file_device_build_dir}" --config $) +add_test(NAME mqt-core-qdmi-runtime-file-install + COMMAND ${CMAKE_COMMAND} --install "${runtime_file_device_build_dir}" --config $ + --prefix "${runtime_file_device_install_dir}") +add_test( + NAME mqt-core-qdmi-runtime-file-install-verify + COMMAND + ${CMAKE_COMMAND} -E compare_files + "${runtime_file_device_install_dir}/${runtime_file_device_artifact_dir}/metadata-runtime.json" + "${CMAKE_CURRENT_SOURCE_DIR}/metadata-runtime.json") +set_tests_properties(mqt-core-qdmi-runtime-file-configure + PROPERTIES FIXTURES_SETUP mqt-core-qdmi-runtime-file-configure) +set_tests_properties( + mqt-core-qdmi-runtime-file-build + PROPERTIES FIXTURES_REQUIRED mqt-core-qdmi-runtime-file-configure FIXTURES_SETUP + mqt-core-qdmi-runtime-file-build) +set_tests_properties( + mqt-core-qdmi-runtime-file-install PROPERTIES FIXTURES_REQUIRED mqt-core-qdmi-runtime-file-build + FIXTURES_SETUP mqt-core-qdmi-runtime-file-install) +set_tests_properties(mqt-core-qdmi-runtime-file-install-verify + PROPERTIES FIXTURES_REQUIRED mqt-core-qdmi-runtime-file-install) + +function(add_qdmi_configuration_rejection_test name invalid_configuration) + set(build_dir "${CMAKE_CURRENT_BINARY_DIR}/runtime-file-device-invalid-${name}") + set(configure_command + ${CMAKE_COMMAND} -S "${CMAKE_CURRENT_SOURCE_DIR}/runtime_file_device" -B "${build_dir}" -G + "${CMAKE_GENERATOR}" + "-DMQT_CORE_QDMI_HELPER=${PROJECT_SOURCE_DIR}/cmake/AddMQTQDMIDevice.cmake" + "-DMQT_CORE_QDMI_TEST_SOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR}" + "-DMQT_CORE_QDMI_INVALID_CONFIGURATION=${invalid_configuration}" + "-DCMAKE_BUILD_TYPE=$") + if(CMAKE_GENERATOR_PLATFORM) + list(APPEND configure_command -A "${CMAKE_GENERATOR_PLATFORM}") + endif() + if(CMAKE_GENERATOR_TOOLSET) + list(APPEND configure_command -T "${CMAKE_GENERATOR_TOOLSET}") + endif() + add_test(NAME "mqt-core-qdmi-configuration-rejects-${name}" COMMAND ${configure_command}) + set_tests_properties("mqt-core-qdmi-configuration-rejects-${name}" PROPERTIES WILL_FAIL TRUE) +endfunction() + +add_qdmi_configuration_rejection_test(empty-id empty-id) +add_qdmi_configuration_rejection_test(empty-file empty-file) +add_qdmi_configuration_rejection_test(duplicate-base-id duplicate-base-id) +add_qdmi_configuration_rejection_test(duplicate-configured-id duplicate-configured-id) + +if(NOT BUILD_MQT_CORE_QDMI_DDSIM_DEVICE OR NOT BUILD_MQT_CORE_QDMI_SC_DEVICE) + return() +endif() + +generate_prefixed_qdmi_headers(TEST_LIFECYCLE) +add_library(mqt-core-qdmi-lifecycle-device SHARED lifecycle_device.cpp) +target_compile_definitions(mqt-core-qdmi-lifecycle-device + PRIVATE TEST_LIFECYCLE_QDMI_device_EXPORTS) +target_compile_features(mqt-core-qdmi-lifecycle-device PRIVATE cxx_std_20) +target_include_directories(mqt-core-qdmi-lifecycle-device + PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/include") +target_link_libraries(mqt-core-qdmi-lifecycle-device PRIVATE MQT::ProjectOptions + MQT::ProjectWarnings) + +set(TARGET_NAME mqt-core-qdmi-test) +package_add_test(${TARGET_NAME} MQT::CoreQDMI test_device.cpp test_device_api.cpp ../test_slurm.cpp) +target_include_directories(${TARGET_NAME} PRIVATE ${PROJECT_SOURCE_DIR}/src/qdmi + "${CMAKE_CURRENT_BINARY_DIR}/include") +target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQDMICommon qdmi::qdmi + mqt-core-qdmi-lifecycle-device) +target_compile_definitions( + ${TARGET_NAME} + PRIVATE DDSIM_DEVICE_LIBRARY="$" + LIFECYCLE_DEVICE_LIBRARY="$" + MQT_CORE_QDMI_SLURM_TEST_DEVICE="$" + SC_DEVICE_LIBRARY="$") +add_dependencies(${TARGET_NAME} MQT::CoreQDMI_DDSIM_Device MQT::CoreQDMIScDevice + mqt-core-qdmi-session-device) +mqt_copy_qdmi_runtime(${TARGET_NAME}) +mqt_copy_qdmi_runtime(${TARGET_NAME} mqt-core-qdmi-metadata-device) diff --git a/test/qdmi/driver/imported_device/CMakeLists.txt b/test/qdmi/device/imported_device/CMakeLists.txt similarity index 100% rename from test/qdmi/driver/imported_device/CMakeLists.txt rename to test/qdmi/device/imported_device/CMakeLists.txt diff --git a/test/qdmi/device/lifecycle_device.cpp b/test/qdmi/device/lifecycle_device.cpp new file mode 100644 index 0000000000..7790191ee2 --- /dev/null +++ b/test/qdmi/device/lifecycle_device.cpp @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "test_lifecycle_qdmi/device.h" + +#include +#include +#include +#include + +namespace { +struct LifecycleState { + std::mutex mutex; + std::condition_variable changed; + bool blockFinalize = false; + bool finalizeStarted = false; + bool releaseFinalize = false; + size_t initializeCount = 0; + size_t finalizeCount = 0; + size_t overlappingInitializeCount = 0; +}; + +[[nodiscard]] LifecycleState& lifecycleState() { + static LifecycleState state; + return state; +} +} // namespace + +extern "C" { + +TEST_LIFECYCLE_QDMI_EXPORT void testLifecyclePrepareBlockingFinalize() { + auto& state = lifecycleState(); + const std::scoped_lock lock(state.mutex); + state.blockFinalize = true; + state.finalizeStarted = false; + state.releaseFinalize = false; + state.initializeCount = 0; + state.finalizeCount = 0; + state.overlappingInitializeCount = 0; +} + +TEST_LIFECYCLE_QDMI_EXPORT bool +testLifecycleWaitForFinalize(const size_t timeoutMs) { + auto& state = lifecycleState(); + std::unique_lock lock(state.mutex); + return state.changed.wait_for(lock, std::chrono::milliseconds(timeoutMs), + [&state] { return state.finalizeStarted; }); +} + +TEST_LIFECYCLE_QDMI_EXPORT bool +testLifecycleWaitForInitializations(const size_t expected, + const size_t timeoutMs) { + auto& state = lifecycleState(); + std::unique_lock lock(state.mutex); + return state.changed.wait_for( + lock, std::chrono::milliseconds(timeoutMs), + [&state, expected] { return state.initializeCount >= expected; }); +} + +TEST_LIFECYCLE_QDMI_EXPORT void testLifecycleReleaseFinalize() { + auto& state = lifecycleState(); + { + const std::scoped_lock lock(state.mutex); + state.releaseFinalize = true; + } + state.changed.notify_all(); +} + +TEST_LIFECYCLE_QDMI_EXPORT size_t testLifecycleInitializeCount() { + auto& state = lifecycleState(); + const std::scoped_lock lock(state.mutex); + return state.initializeCount; +} + +TEST_LIFECYCLE_QDMI_EXPORT size_t testLifecycleFinalizeCount() { + auto& state = lifecycleState(); + const std::scoped_lock lock(state.mutex); + return state.finalizeCount; +} + +TEST_LIFECYCLE_QDMI_EXPORT size_t testLifecycleOverlappingInitializeCount() { + auto& state = lifecycleState(); + const std::scoped_lock lock(state.mutex); + return state.overlappingInitializeCount; +} + +TEST_LIFECYCLE_QDMI_EXPORT int TEST_LIFECYCLE_QDMI_device_initialize() { + auto& state = lifecycleState(); + { + const std::scoped_lock lock(state.mutex); + ++state.initializeCount; + if (state.finalizeStarted) { + ++state.overlappingInitializeCount; + } + } + state.changed.notify_all(); + return QDMI_SUCCESS; +} + +TEST_LIFECYCLE_QDMI_EXPORT int TEST_LIFECYCLE_QDMI_device_finalize() { + auto& state = lifecycleState(); + std::unique_lock lock(state.mutex); + state.finalizeStarted = true; + state.changed.notify_all(); + if (state.blockFinalize) { + state.changed.wait(lock, [&state] { return state.releaseFinalize; }); + } + state.finalizeStarted = false; + ++state.finalizeCount; + state.changed.notify_all(); + return QDMI_SUCCESS; +} + +// The remaining functions only need to be resolvable for this lifecycle test. +// NOLINTBEGIN(readability-named-parameter) +TEST_LIFECYCLE_QDMI_EXPORT int +TEST_LIFECYCLE_QDMI_device_session_alloc(TEST_LIFECYCLE_QDMI_Device_Session*) { + return QDMI_ERROR_NOTSUPPORTED; +} +TEST_LIFECYCLE_QDMI_EXPORT int +TEST_LIFECYCLE_QDMI_device_session_init(TEST_LIFECYCLE_QDMI_Device_Session) { + return QDMI_ERROR_NOTSUPPORTED; +} +TEST_LIFECYCLE_QDMI_EXPORT void +TEST_LIFECYCLE_QDMI_device_session_free(TEST_LIFECYCLE_QDMI_Device_Session) {} +TEST_LIFECYCLE_QDMI_EXPORT int TEST_LIFECYCLE_QDMI_device_session_set_parameter( + TEST_LIFECYCLE_QDMI_Device_Session, QDMI_Device_Session_Parameter, size_t, + const void*) { + return QDMI_ERROR_NOTSUPPORTED; +} +TEST_LIFECYCLE_QDMI_EXPORT int +TEST_LIFECYCLE_QDMI_device_session_create_device_job( + TEST_LIFECYCLE_QDMI_Device_Session, TEST_LIFECYCLE_QDMI_Device_Job*) { + return QDMI_ERROR_NOTSUPPORTED; +} +TEST_LIFECYCLE_QDMI_EXPORT void +TEST_LIFECYCLE_QDMI_device_job_free(TEST_LIFECYCLE_QDMI_Device_Job) {} +TEST_LIFECYCLE_QDMI_EXPORT int +TEST_LIFECYCLE_QDMI_device_job_set_parameter(TEST_LIFECYCLE_QDMI_Device_Job, + QDMI_Device_Job_Parameter, size_t, + const void*) { + return QDMI_ERROR_NOTSUPPORTED; +} +TEST_LIFECYCLE_QDMI_EXPORT int +TEST_LIFECYCLE_QDMI_device_job_query_property(TEST_LIFECYCLE_QDMI_Device_Job, + QDMI_Device_Job_Property, size_t, + void*, size_t*) { + return QDMI_ERROR_NOTSUPPORTED; +} +TEST_LIFECYCLE_QDMI_EXPORT int +TEST_LIFECYCLE_QDMI_device_job_submit(TEST_LIFECYCLE_QDMI_Device_Job) { + return QDMI_ERROR_NOTSUPPORTED; +} +TEST_LIFECYCLE_QDMI_EXPORT int +TEST_LIFECYCLE_QDMI_device_job_cancel(TEST_LIFECYCLE_QDMI_Device_Job) { + return QDMI_ERROR_NOTSUPPORTED; +} +TEST_LIFECYCLE_QDMI_EXPORT int +TEST_LIFECYCLE_QDMI_device_job_check(TEST_LIFECYCLE_QDMI_Device_Job, + QDMI_Job_Status*) { + return QDMI_ERROR_NOTSUPPORTED; +} +TEST_LIFECYCLE_QDMI_EXPORT int +TEST_LIFECYCLE_QDMI_device_job_wait(TEST_LIFECYCLE_QDMI_Device_Job, size_t) { + return QDMI_ERROR_NOTSUPPORTED; +} +TEST_LIFECYCLE_QDMI_EXPORT int TEST_LIFECYCLE_QDMI_device_job_get_results( + TEST_LIFECYCLE_QDMI_Device_Job, QDMI_Job_Result, size_t, void*, size_t*) { + return QDMI_ERROR_NOTSUPPORTED; +} +TEST_LIFECYCLE_QDMI_EXPORT int +TEST_LIFECYCLE_QDMI_device_session_query_device_property( + TEST_LIFECYCLE_QDMI_Device_Session, QDMI_Device_Property, size_t, void*, + size_t*) { + return QDMI_ERROR_NOTSUPPORTED; +} +TEST_LIFECYCLE_QDMI_EXPORT int +TEST_LIFECYCLE_QDMI_device_session_query_site_property( + TEST_LIFECYCLE_QDMI_Device_Session, TEST_LIFECYCLE_QDMI_Site, + QDMI_Site_Property, size_t, void*, size_t*) { + return QDMI_ERROR_NOTSUPPORTED; +} +TEST_LIFECYCLE_QDMI_EXPORT int +TEST_LIFECYCLE_QDMI_device_session_query_operation_property( + TEST_LIFECYCLE_QDMI_Device_Session, TEST_LIFECYCLE_QDMI_Operation, size_t, + const TEST_LIFECYCLE_QDMI_Site*, size_t, const double*, + QDMI_Operation_Property, size_t, void*, size_t*) { + return QDMI_ERROR_NOTSUPPORTED; +} +// NOLINTEND(readability-named-parameter) + +} // extern "C" diff --git a/test/qdmi/driver/metadata-runtime.json b/test/qdmi/device/metadata-runtime.json similarity index 100% rename from test/qdmi/driver/metadata-runtime.json rename to test/qdmi/device/metadata-runtime.json diff --git a/test/qdmi/driver/metadata_device.cpp b/test/qdmi/device/metadata_device.cpp similarity index 100% rename from test/qdmi/driver/metadata_device.cpp rename to test/qdmi/device/metadata_device.cpp diff --git a/test/qdmi/driver/runtime_file_device/CMakeLists.txt b/test/qdmi/device/runtime_file_device/CMakeLists.txt similarity index 100% rename from test/qdmi/driver/runtime_file_device/CMakeLists.txt rename to test/qdmi/device/runtime_file_device/CMakeLists.txt diff --git a/test/qdmi/driver/session_device.cpp b/test/qdmi/device/session_device.cpp similarity index 100% rename from test/qdmi/driver/session_device.cpp rename to test/qdmi/device/session_device.cpp diff --git a/test/qdmi/test_client.cpp b/test/qdmi/device/test_device.cpp similarity index 68% rename from test/qdmi/test_client.cpp rename to test/qdmi/device/test_device.cpp index bd64000ab8..3870cb97e5 100644 --- a/test/qdmi/test_client.cpp +++ b/test/qdmi/device/test_device.cpp @@ -8,27 +8,26 @@ * Licensed under the MIT License */ -#include "qdmi/Client.hpp" +#include "qdmi/Device.hpp" +#include "qdmi/DeviceManager.hpp" +#include "qdmi/DeviceRegistry.hpp" #include "qdmi/common/Common.hpp" #include #include -#include +#include #include #include #include #include #include -#include -#include #include #include #include #include #include #include -#include #include #include @@ -36,27 +35,33 @@ namespace qdmi { namespace { -auto queryBytes(const std::vector& bytes) { - return [&bytes](const size_t size, void* value, size_t* sizeRet) { - if (sizeRet != nullptr) { - *sizeRet = bytes.size(); - } - if (value != nullptr) { - if (size < bytes.size()) { - return QDMI_ERROR_INVALIDARGUMENT; - } - std::memcpy(value, bytes.data(), bytes.size()); - } - return QDMI_SUCCESS; - }; -} - template auto bytesOf(const T& value) { std::vector bytes(sizeof(T)); std::memcpy(bytes.data(), &value, sizeof(T)); return bytes; } +DeviceManager configuredManager() { + return DeviceManager(DeviceRegistry({ + {.id = "mqt.ddsim.default", + .library = DDSIM_DEVICE_LIBRARY, + .prefix = "MQT_DDSIM"}, + {.id = "mqt.sc.default", + .library = SC_DEVICE_LIBRARY, + .prefix = "MQT_SC"}, + })); +} + +auto getDevices() -> std::vector { + auto manager = configuredManager(); + std::vector devices; + devices.reserve(manager.definitions().size()); + for (const auto& definition : manager.definitions()) { + devices.emplace_back(manager.open(definition.id)); + } + return devices; +} + class DeviceTest : public testing::TestWithParam { protected: Device device; @@ -86,13 +91,7 @@ class DDSimulatorDeviceTest : public testing::Test { private: static auto getDDSimulatorDevice() -> Device { - Session session; - for (const auto& dev : session.getDevices()) { - if (dev.getName() == "MQT Core DDSIM QDMI Device") { - return dev; - } - } - throw std::runtime_error("DD simulator device not found"); + return configuredManager().open("mqt.ddsim.default"); } }; @@ -133,145 +132,53 @@ cx q[0], q[1]; } // namespace -TEST(CustomPropertyTest, SelectorsMapToEveryQDMIPropertyFamily) { - constexpr std::array properties{ - CustomProperty::Custom1, CustomProperty::Custom2, CustomProperty::Custom3, - CustomProperty::Custom4, CustomProperty::Custom5}; - constexpr std::array deviceProperties{ - QDMI_DEVICE_PROPERTY_CUSTOM1, QDMI_DEVICE_PROPERTY_CUSTOM2, - QDMI_DEVICE_PROPERTY_CUSTOM3, QDMI_DEVICE_PROPERTY_CUSTOM4, - QDMI_DEVICE_PROPERTY_CUSTOM5}; - constexpr std::array siteProperties{ - QDMI_SITE_PROPERTY_CUSTOM1, QDMI_SITE_PROPERTY_CUSTOM2, - QDMI_SITE_PROPERTY_CUSTOM3, QDMI_SITE_PROPERTY_CUSTOM4, - QDMI_SITE_PROPERTY_CUSTOM5}; - constexpr std::array operationProperties{ - QDMI_OPERATION_PROPERTY_CUSTOM1, QDMI_OPERATION_PROPERTY_CUSTOM2, - QDMI_OPERATION_PROPERTY_CUSTOM3, QDMI_OPERATION_PROPERTY_CUSTOM4, - QDMI_OPERATION_PROPERTY_CUSTOM5}; - constexpr std::array jobProperties{ - QDMI_JOB_PROPERTY_CUSTOM1, QDMI_JOB_PROPERTY_CUSTOM2, - QDMI_JOB_PROPERTY_CUSTOM3, QDMI_JOB_PROPERTY_CUSTOM4, - QDMI_JOB_PROPERTY_CUSTOM5}; - constexpr std::array jobResults{ - QDMI_JOB_RESULT_CUSTOM1, QDMI_JOB_RESULT_CUSTOM2, QDMI_JOB_RESULT_CUSTOM3, - QDMI_JOB_RESULT_CUSTOM4, QDMI_JOB_RESULT_CUSTOM5}; - - for (size_t i = 0; i < properties.size(); ++i) { - EXPECT_EQ(detail::toDeviceProperty(properties[i]), deviceProperties[i]); - EXPECT_EQ(detail::toSiteProperty(properties[i]), siteProperties[i]); - EXPECT_EQ(detail::toOperationProperty(properties[i]), - operationProperties[i]); - EXPECT_EQ(detail::toJobProperty(properties[i]), jobProperties[i]); - EXPECT_EQ(detail::toJobResult(properties[i]), jobResults[i]); - } -} - -TEST(CustomPropertyTest, RejectsInvalidSelector) { - // NOLINTNEXTLINE(clang-analyzer-optin.core.EnumCastOutOfRange) - constexpr auto invalid = static_cast(0); - EXPECT_THROW(std::ignore = detail::toDeviceProperty(invalid), - std::invalid_argument); - EXPECT_THROW(std::ignore = detail::toSiteProperty(invalid), - std::invalid_argument); - EXPECT_THROW(std::ignore = detail::toOperationProperty(invalid), - std::invalid_argument); - EXPECT_THROW(std::ignore = detail::toJobProperty(invalid), - std::invalid_argument); - EXPECT_THROW(std::ignore = detail::toJobResult(invalid), - std::invalid_argument); -} - TEST(CustomPropertyTest, DecodesSupportedTypes) { const std::vector stringBytes{std::byte{'v'}, std::byte{'a'}, std::byte{'l'}, std::byte{'u'}, std::byte{'e'}, std::byte{0}}; - EXPECT_EQ(detail::queryCustomValue(queryBytes(stringBytes), - "test property"), - "value"); + EXPECT_EQ( + detail::decodeCustomValue(stringBytes, "test property"), + "value"); constexpr bool boolValue = true; - EXPECT_EQ(detail::queryCustomValue(queryBytes(bytesOf(boolValue)), - "test property"), - boolValue); + EXPECT_EQ( + detail::decodeCustomValue(bytesOf(boolValue), "test property"), + boolValue); constexpr int intValue = 42; - EXPECT_EQ(detail::queryCustomValue(queryBytes(bytesOf(intValue)), - "test property"), + EXPECT_EQ(detail::decodeCustomValue(bytesOf(intValue), "test property"), intValue); constexpr double doubleValue = 1.25; - EXPECT_EQ(detail::queryCustomValue(queryBytes(bytesOf(doubleValue)), - "test property"), - doubleValue); - EXPECT_EQ(detail::queryCustomValue>( - queryBytes(stringBytes), "test property"), + EXPECT_EQ( + detail::decodeCustomValue(bytesOf(doubleValue), "test property"), + doubleValue); + EXPECT_EQ(detail::decodeCustomValue>(stringBytes, + "test property"), stringBytes); } TEST(CustomPropertyTest, ReturnsNulloptWhenUnsupported) { - const auto query = [](size_t, void*, size_t*) { - return QDMI_ERROR_NOTSUPPORTED; - }; - EXPECT_EQ(detail::queryCustomValue(query, "test property"), + EXPECT_EQ(detail::decodeCustomValue(std::nullopt, "test property"), std::nullopt); } -TEST(CustomPropertyTest, PropagatesQueryErrors) { - const auto failingSizeQuery = [](size_t, void*, size_t*) { - return QDMI_ERROR_INVALIDARGUMENT; - }; - EXPECT_THROW(std::ignore = detail::queryCustomValue(failingSizeQuery, - "test property"), - std::invalid_argument); - - const auto failingValueQuery = [](const size_t, void* value, - size_t* sizeRet) { - if (sizeRet != nullptr) { - *sizeRet = sizeof(int); - return QDMI_SUCCESS; - } - EXPECT_NE(value, nullptr); - return QDMI_ERROR_INVALIDARGUMENT; - }; - EXPECT_THROW(std::ignore = detail::queryCustomValue(failingValueQuery, - "test property"), - std::invalid_argument); -} - TEST(CustomPropertyTest, SupportsEmptyRawValues) { const std::vector empty; - EXPECT_EQ(detail::queryCustomValue>(queryBytes(empty), - "test property"), - empty); + EXPECT_EQ( + detail::decodeCustomValue>(empty, "test property"), + empty); } TEST(CustomPropertyTest, RejectsIncompatibleRepresentations) { const std::vector empty; - EXPECT_THROW(std::ignore = detail::queryCustomValue( - queryBytes(empty), "test property"), + EXPECT_THROW(std::ignore = detail::decodeCustomValue( + empty, "test property"), std::invalid_argument); const std::vector malformedString{std::byte{'n'}, std::byte{'o'}}; - EXPECT_THROW(std::ignore = detail::queryCustomValue( - queryBytes(malformedString), "test property"), + EXPECT_THROW(std::ignore = detail::decodeCustomValue( + malformedString, "test property"), std::invalid_argument); - EXPECT_THROW(std::ignore = detail::queryCustomValue( - queryBytes(bytesOf(true)), "test property"), - std::invalid_argument); -} - -TEST(QueuePositionTest, ReturnsPositionOnSuccess) { - EXPECT_EQ(detail::queuePositionFromResult(QDMI_SUCCESS, 3), 3); -} - -TEST(QueuePositionTest, ReturnsNulloptWhenUnavailable) { - EXPECT_EQ(detail::queuePositionFromResult(QDMI_ERROR_NOTSUPPORTED, 0), - std::nullopt); - EXPECT_EQ(detail::queuePositionFromResult(QDMI_ERROR_BADSTATE, 0), - std::nullopt); -} - -TEST(QueuePositionTest, PropagatesOtherQueryErrors) { - EXPECT_THROW(std::ignore = detail::queuePositionFromResult( - QDMI_ERROR_INVALIDARGUMENT, 0), + EXPECT_THROW(std::ignore = detail::decodeCustomValue(bytesOf(true), + "test property"), std::invalid_argument); } @@ -363,10 +270,6 @@ TEST(QDMITest, DevicePropertyToString) { "MIN ATOM DISTANCE"); EXPECT_STREQ(qdmi::toString(QDMI_DEVICE_PROPERTY_SUPPORTEDPROGRAMFORMATS), "SUPPORTED PROGRAM FORMATS"); - EXPECT_STREQ(qdmi::toString(QDMI_DEVICE_PROPERTY_CHILDDEVICES), - "CHILD DEVICES"); - EXPECT_STREQ(qdmi::toString(QDMI_DEVICE_PROPERTY_QUEUELENGTH), - "QUEUE LENGTH"); EXPECT_STREQ(qdmi::toString(QDMI_DEVICE_PROPERTY_MAX), "MAX"); EXPECT_STREQ(qdmi::toString(QDMI_DEVICE_PROPERTY_CUSTOM1), "CUSTOM1"); EXPECT_STREQ(qdmi::toString(QDMI_DEVICE_PROPERTY_CUSTOM2), "CUSTOM2"); @@ -375,15 +278,6 @@ TEST(QDMITest, DevicePropertyToString) { EXPECT_STREQ(qdmi::toString(QDMI_DEVICE_PROPERTY_CUSTOM5), "CUSTOM5"); } -TEST(QDMITest, SessionPropertyToString) { - EXPECT_STREQ(qdmi::toString(QDMI_SESSION_PROPERTY_DEVICES), "DEVICES"); -} - -TEST(QDMITest, DeviceSessionParameterToString) { - EXPECT_STREQ(qdmi::toString(QDMI_DEVICE_SESSION_PARAMETER_CHILDDEVICE), - "CHILD DEVICE"); -} - TEST(QDMITest, ThrowIfError) { EXPECT_NO_THROW(qdmi::throwIfError(QDMI_SUCCESS, "Test")); EXPECT_NO_THROW(qdmi::throwIfError(QDMI_WARN_GENERAL, "Test")); @@ -493,10 +387,6 @@ TEST_P(DeviceTest, NeedsCalibration) { EXPECT_NO_THROW(std::ignore = device.getNeedsCalibration()); } -TEST_F(DDSimulatorDeviceTest, QueueLengthIsUnavailable) { - EXPECT_EQ(device.getQueueLength(), std::nullopt); -} - TEST_P(DeviceTest, LengthUnit) { EXPECT_NO_THROW(std::ignore = device.getLengthUnit()); } @@ -948,10 +838,6 @@ c[0] = measure q[0]; EXPECT_NE(job.getId(), job2.getId()); } -TEST_F(JobTest, QueuePositionIsUnavailable) { - EXPECT_EQ(job.getQueuePosition(), std::nullopt); -} - TEST_F(JobTest, UnsupportedCustomPropertyAndResultReturnNullopt) { EXPECT_EQ( job.queryCustomProperty>(CustomProperty::Custom1), @@ -1019,7 +905,8 @@ TEST_F(JobTest, GetShotsReturnsValidShots) { // If the device doesn't support shots, the error message should indicate so const std::string errorMsg(e.what()); EXPECT_TRUE(errorMsg.find("Not supported") != std::string::npos || - errorMsg.find("not supported") != std::string::npos); + errorMsg.find("not supported") != std::string::npos) + << errorMsg; } } @@ -1120,224 +1007,8 @@ TEST_F(SimulatorJobTest, getSparseProbabilitiesReturnsValidProbabilities) { EXPECT_NEAR(it11->second, 0.5, 1e-10); } -TEST(AuthenticationTest, SessionParameterToString) { - EXPECT_STREQ(qdmi::toString(QDMI_SESSION_PARAMETER_TOKEN), "TOKEN"); - EXPECT_STREQ(qdmi::toString(QDMI_SESSION_PARAMETER_AUTHFILE), "AUTH FILE"); - EXPECT_STREQ(qdmi::toString(QDMI_SESSION_PARAMETER_AUTHURL), "AUTH URL"); - EXPECT_STREQ(qdmi::toString(QDMI_SESSION_PARAMETER_USERNAME), "USERNAME"); - EXPECT_STREQ(qdmi::toString(QDMI_SESSION_PARAMETER_PASSWORD), "PASSWORD"); - EXPECT_STREQ(qdmi::toString(QDMI_SESSION_PARAMETER_PROJECTID), "PROJECT ID"); - EXPECT_STREQ(qdmi::toString(QDMI_SESSION_PARAMETER_MAX), "MAX"); - EXPECT_STREQ(qdmi::toString(QDMI_SESSION_PARAMETER_CUSTOM1), "CUSTOM1"); - EXPECT_STREQ(qdmi::toString(QDMI_SESSION_PARAMETER_CUSTOM2), "CUSTOM2"); - EXPECT_STREQ(qdmi::toString(QDMI_SESSION_PARAMETER_CUSTOM3), "CUSTOM3"); - EXPECT_STREQ(qdmi::toString(QDMI_SESSION_PARAMETER_CUSTOM4), "CUSTOM4"); - EXPECT_STREQ(qdmi::toString(QDMI_SESSION_PARAMETER_CUSTOM5), "CUSTOM5"); -} - -TEST(AuthenticationTest, SessionConstructionWithToken) { - // Empty token should be accepted - SessionConfig config1; - config1.token = ""; - EXPECT_NO_THROW({ const Session session(config1); }); - - // Non-empty token should be accepted - SessionConfig config2; - config2.token = "test_token_123"; - EXPECT_NO_THROW({ const Session session(config2); }); - - // Token with special characters should be accepted - SessionConfig config3; - config3.token = "very_long_token_with_special_characters_!@#$%^&*()"; - EXPECT_NO_THROW({ const Session session(config3); }); -} - -TEST(AuthenticationTest, SessionConstructionWithAuthUrl) { - // Valid HTTPS URL - SessionConfig config1; - config1.authUrl = "https://example.com"; - EXPECT_NO_THROW({ const Session session(config1); }); - - // Valid HTTP URL with port and path - SessionConfig config2; - config2.authUrl = "http://auth.server.com:8080/api"; - EXPECT_NO_THROW({ const Session session(config2); }); - - // Valid HTTPS URL with query parameters - SessionConfig config3; - config3.authUrl = "https://auth.example.com/token?param=value"; - EXPECT_NO_THROW({ const Session session(config3); }); - - // Valid localhost URL - SessionConfig configLocalhost; - configLocalhost.authUrl = "http://localhost"; - EXPECT_NO_THROW({ const Session session(configLocalhost); }); - - // Valid localhost URL with port - SessionConfig configLocalhostPort; - configLocalhostPort.authUrl = "http://localhost:8080"; - EXPECT_NO_THROW({ const Session session(configLocalhostPort); }); - - // Valid localhost URL with port and path - SessionConfig configLocalhostPath; - configLocalhostPath.authUrl = "https://localhost:3000/auth/api"; - EXPECT_NO_THROW({ const Session session(configLocalhostPath); }); - - // Valid IPv4 address URL - SessionConfig configIPv4; - configIPv4.authUrl = "http://127.0.0.1:5000/auth"; - EXPECT_NO_THROW({ const Session session(configIPv4); }); - - // Valid IPv6 address URL - SessionConfig configIPv6; - configIPv6.authUrl = "https://[::1]:8080/auth"; - EXPECT_NO_THROW({ const Session session(configIPv6); }); - - // Invalid URL - not a URL at all (validation fails before setting parameter) - SessionConfig config4; - config4.authUrl = "not-a-url"; - EXPECT_THROW({ const Session session(config4); }, std::runtime_error); - - // Invalid URL - unsupported protocol - SessionConfig config5; - config5.authUrl = "ftp://invalid.com"; - EXPECT_THROW({ const Session session(config5); }, std::runtime_error); - - // Invalid URL - missing protocol - SessionConfig config6; - config6.authUrl = "example.com"; - EXPECT_THROW({ const Session session(config6); }, std::runtime_error); - - // Invalid URL - empty - SessionConfig config7; - config7.authUrl = ""; - EXPECT_THROW({ const Session session(config7); }, std::runtime_error); -} - -TEST(AuthenticationTest, SessionConstructionWithAuthFile) { - // Non-existent file (validation fails before setting parameter) - SessionConfig config1; - config1.authFile = "/nonexistent/path/to/file.txt"; - EXPECT_THROW({ const Session session(config1); }, std::runtime_error); - - // Existing file (should succeed even if parameter is unsupported) - const auto tempDir = std::filesystem::temp_directory_path(); - auto tmpPath = tempDir / ("qdmi_test_auth_" + - std::to_string(std::hash{}( - std::this_thread::get_id())) + - ".txt"); - { - std::ofstream tmpFile(tmpPath); - ASSERT_TRUE(tmpFile.is_open()) << "Failed to create temporary file"; - tmpFile << "test_token_content"; - } - - SessionConfig config2; - config2.authFile = tmpPath; - EXPECT_NO_THROW({ const Session session(config2); }); - - // Clean up - std::filesystem::remove(tmpPath); -} - -TEST(AuthenticationTest, SessionConstructionWithUsernamePassword) { - // Username only - SessionConfig config1; - config1.username = "user123"; - EXPECT_NO_THROW({ const Session session(config1); }); - - // Password only - SessionConfig config2; - config2.password = "secure_password"; - EXPECT_NO_THROW({ const Session session(config2); }); - - // Both username and password - SessionConfig config3; - config3.username = "user123"; - config3.password = "secure_password"; - EXPECT_NO_THROW({ const Session session(config3); }); -} - -TEST(AuthenticationTest, SessionConstructionWithProjectId) { - SessionConfig config; - config.projectId = "project-123-abc"; - EXPECT_NO_THROW({ const Session session(config); }); -} - -TEST(AuthenticationTest, SessionConstructionWithMultipleParameters) { - SessionConfig config; - config.token = "test_token"; - config.username = "test_user"; - config.password = "test_pass"; - config.projectId = "test_project"; - EXPECT_NO_THROW({ const Session session(config); }); -} - -TEST(AuthenticationTest, SessionConstructionWithCustomParameters) { - // Custom parameters may not be supported by all devices, or may have specific - // validation requirements. This test verifies they can be passed to the - // Session constructor. Currently a smoke test. - - // Test custom1 - may succeed or fail with validation/unsupported errors - SessionConfig config1; - config1.custom1 = "custom_value_1"; - try { - Session session(config1); - EXPECT_NO_THROW(std::ignore = session.getDevices()); - } catch (const std::invalid_argument&) { - // Validation error - parameter recognized but value invalid - SUCCEED(); - } catch (const std::runtime_error&) { - // Not supported or other error - GTEST_SKIP() << "Custom parameter not supported by backend"; - } - - // Test custom2 - SessionConfig config2; - config2.custom2 = "custom_value_2"; - try { - Session session(config2); - EXPECT_NO_THROW(std::ignore = session.getDevices()); - } catch (const std::invalid_argument&) { - SUCCEED(); - } catch (const std::runtime_error&) { - GTEST_SKIP() << "Custom parameter not supported by backend"; - } - - // Test all custom parameters together - SessionConfig config3; - config3.custom1 = "value1"; - config3.custom2 = "value2"; - config3.custom3 = "value3"; - config3.custom4 = "value4"; - config3.custom5 = "value5"; - try { - Session session(config3); - EXPECT_NO_THROW(std::ignore = session.getDevices()); - } catch (const std::invalid_argument&) { - SUCCEED(); - } catch (const std::runtime_error&) { - GTEST_SKIP() << "Custom parameter not supported by backend"; - } - - // Test mixing custom parameters with standard authentication - SessionConfig config4; - config4.token = "test_token"; - config4.custom1 = "custom_value"; - config4.projectId = "project_id"; - try { - Session session(config4); - EXPECT_NO_THROW(std::ignore = session.getDevices()); - } catch (const std::invalid_argument&) { - SUCCEED(); - } catch (const std::runtime_error&) { - GTEST_SKIP() << "Custom parameter not supported by backend"; - } -} - -TEST(AuthenticationTest, SessionGetDevicesReturnsList) { - Session session; - auto devices = session.getDevices(); +TEST(DeviceManagerTest, DiscoversAndOpensDevices) { + auto devices = getDevices(); EXPECT_FALSE(devices.empty()); @@ -1348,12 +1019,9 @@ TEST(AuthenticationTest, SessionGetDevicesReturnsList) { } } -TEST(AuthenticationTest, SessionMultipleInstances) { - Session session1; - Session session2; - - auto devices1 = session1.getDevices(); - auto devices2 = session2.getDevices(); +TEST(DeviceManagerTest, MultipleInstancesOpenIndependently) { + auto devices1 = getDevices(); + auto devices2 = getDevices(); // Both should return devices EXPECT_FALSE(devices1.empty()); @@ -1363,41 +1031,17 @@ TEST(AuthenticationTest, SessionMultipleInstances) { EXPECT_EQ(devices1.size(), devices2.size()); } -TEST(DeviceOwnershipTest, SiteKeepsFreshSessionAlive) { - const auto site = [] { - auto device = Session::openDevice("mqt.sc.default"); - return device.getSites().front(); - }(); - - EXPECT_EQ(site.getIndex(), 0); -} - -TEST(DeviceOwnershipTest, OperationKeepsFreshSessionAlive) { - const auto operation = [] { - auto device = Session::openDevice("mqt.sc.default"); - return device.getOperations().front(); - }(); - - EXPECT_FALSE(operation.getName().empty()); -} - -TEST(DeviceOwnershipTest, SiteFromOperationKeepsFreshSessionAlive) { - const auto site = [] { - auto device = Session::openDevice("mqt.sc.default"); - const auto operation = device.getOperations().front(); - return operation.getSites().value().front(); - }(); - - EXPECT_FALSE(site.isZone()); -} - -namespace { -// Helper function to get all devices for parameterized tests -auto getDevices() -> std::vector { - Session session; - return session.getDevices(); +TEST(DeviceManagerTest, RejectsSitesFromAnotherDevice) { + auto manager = configuredManager(); + const auto scDevice = manager.open("mqt.sc.default"); + const auto ddDevice = manager.open("mqt.ddsim.default"); + const auto operations = scDevice.getOperations(); + const auto sites = ddDevice.getSites(); + ASSERT_FALSE(operations.empty()); + ASSERT_FALSE(sites.empty()); + EXPECT_THROW(static_cast(operations.front().getName({sites.front()})), + std::invalid_argument); } -} // namespace INSTANTIATE_TEST_SUITE_P( // Custom instantiation name diff --git a/test/qdmi/device/test_device_api.cpp b/test/qdmi/device/test_device_api.cpp new file mode 100644 index 0000000000..067ee2e8ec --- /dev/null +++ b/test/qdmi/device/test_device_api.cpp @@ -0,0 +1,455 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "DeviceApi.hpp" +#include "DeviceState.hpp" +#include "qdmi/Device.hpp" +#include "test_lifecycle_qdmi/device.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +TEST_LIFECYCLE_QDMI_EXPORT void testLifecyclePrepareBlockingFinalize(); +TEST_LIFECYCLE_QDMI_EXPORT bool testLifecycleWaitForFinalize(size_t timeoutMs); +TEST_LIFECYCLE_QDMI_EXPORT bool +testLifecycleWaitForInitializations(size_t expected, size_t timeoutMs); +TEST_LIFECYCLE_QDMI_EXPORT void testLifecycleReleaseFinalize(); +TEST_LIFECYCLE_QDMI_EXPORT size_t testLifecycleInitializeCount(); +TEST_LIFECYCLE_QDMI_EXPORT size_t testLifecycleFinalizeCount(); +TEST_LIFECYCLE_QDMI_EXPORT size_t testLifecycleOverlappingInitializeCount(); +} + +namespace qdmi::detail { +namespace { +// The fake implements a C ABI whose opaque handles intentionally require +// ownership and pointer casts at this isolated test boundary. +// NOLINTBEGIN(readability-named-parameter,cppcoreguidelines-owning-memory) +class ScriptedDeviceApi final { + struct Child { + size_t id; + }; + struct Session { + const ScriptedDeviceApi* owner = nullptr; + QDMI_Child_Device child = nullptr; + }; + struct ScriptedJob { + const ScriptedDeviceApi* owner = nullptr; + }; + + mutable std::array children_{{{0}, {1}}}; + mutable ScriptedJob job_; + static thread_local const ScriptedDeviceApi* activeApi; + + [[nodiscard]] static auto asSession(QDMI_Device_Session session) -> Session* { + return reinterpret_cast(session); + } + [[nodiscard]] static auto asJob(QDMI_Device_Job job) -> ScriptedJob* { + return reinterpret_cast(job); + } + +public: + enum class ChildBehavior : std::uint8_t { + Supported, + Unsupported, + Malformed, + QueryFailure, + SelectionFailure, + }; + + mutable ChildBehavior behavior = ChildBehavior::Supported; + mutable size_t opened = 0; + mutable size_t closed = 0; + mutable std::vector closeOrder; + mutable QDMI_Device_Status deviceStatus = QDMI_DEVICE_STATUS_IDLE; + mutable QDMI_Job_Status jobStatus = QDMI_JOB_STATUS_CREATED; + mutable QDMI_Program_Format programFormat = QDMI_PROGRAM_FORMAT_QASM3; + + [[nodiscard]] auto deviceApi() const -> std::shared_ptr { + activeApi = this; + auto api = std::make_shared(); + api->device_session_alloc = [](QDMI_Device_Session* session) -> int { + ++activeApi->opened; + *session = reinterpret_cast( + new Session{.owner = activeApi}); + return QDMI_SUCCESS; + }; + api->device_session_init = [](QDMI_Device_Session) -> int { + return QDMI_SUCCESS; + }; + api->device_session_free = [](QDMI_Device_Session session) { + asSession(session)->owner->closeSession(session); + }; + api->device_session_set_parameter = + [](QDMI_Device_Session session, + const QDMI_Device_Session_Parameter parameter, const size_t size, + const void* value) -> int { + if (parameter != QDMI_DEVICE_SESSION_PARAMETER_CHILDDEVICE) { + return QDMI_SUCCESS; + } + if (value == nullptr || size != sizeof(QDMI_Child_Device)) { + return QDMI_ERROR_INVALIDARGUMENT; + } + auto* typed = asSession(session); + if (typed->owner->behavior == ChildBehavior::SelectionFailure) { + return QDMI_ERROR_BADSTATE; + } + std::memcpy(static_cast(&typed->child), value, + sizeof(QDMI_Child_Device)); + return QDMI_SUCCESS; + }; + api->device_session_create_device_job = [](QDMI_Device_Session session, + QDMI_Device_Job* job) -> int { + const auto* owner = asSession(session)->owner; + owner->job_.owner = owner; + *job = reinterpret_cast(&owner->job_); + return QDMI_SUCCESS; + }; + api->device_job_free = [](QDMI_Device_Job) {}; + api->device_job_set_parameter = [](QDMI_Device_Job job, + QDMI_Device_Job_Parameter parameter, + size_t size, const void* value) { + return ScriptedDeviceApi::setJobParameter(job, parameter, size, value); + }; + api->device_job_query_property = + [](QDMI_Device_Job job, QDMI_Device_Job_Property property, size_t size, + void* value, size_t* sizeRet) { + return asJob(job)->owner->queryJobProperty(job, property, size, value, + sizeRet); + }; + api->device_job_submit = [](QDMI_Device_Job job) -> int { + asJob(job)->owner->submitJob(job); + return QDMI_SUCCESS; + }; + api->device_job_cancel = [](QDMI_Device_Job job) -> int { + asJob(job)->owner->cancelJob(job); + return QDMI_SUCCESS; + }; + api->device_job_check = [](QDMI_Device_Job job, + QDMI_Job_Status* status) -> int { + *status = asJob(job)->owner->checkJob(job); + return QDMI_SUCCESS; + }; + api->device_job_wait = [](QDMI_Device_Job job, size_t timeout) -> int { + return ScriptedDeviceApi::waitJob(job, timeout) ? QDMI_SUCCESS + : QDMI_ERROR_TIMEOUT; + }; + api->device_job_get_results = [](QDMI_Device_Job job, + QDMI_Job_Result result, size_t size, + void* data, size_t* sizeRet) { + return ScriptedDeviceApi::getJobResult(job, result, size, data, sizeRet); + }; + api->device_session_query_device_property = + [](QDMI_Device_Session session, QDMI_Device_Property property, + size_t size, void* value, size_t* sizeRet) { + return asSession(session)->owner->queryDevice(session, property, size, + value, sizeRet); + }; + api->device_session_query_site_property = + [](QDMI_Device_Session session, QDMI_Site site, + QDMI_Site_Property property, size_t size, void* value, + size_t* sizeRet) { + return ScriptedDeviceApi::querySite(session, site, property, size, + value, sizeRet); + }; + api->device_session_query_operation_property = + [](QDMI_Device_Session session, QDMI_Operation operation, + size_t numSites, const QDMI_Site* sites, size_t numParams, + const double* params, QDMI_Operation_Property property, size_t size, + void* value, size_t* sizeRet) { + return ScriptedDeviceApi::queryOperation( + session, operation, numSites, sites, numParams, params, property, + size, value, sizeRet); + }; + return api; + } + + void closeSession(QDMI_Device_Session session) const noexcept { + if (session == nullptr) { + return; + } + auto* typed = asSession(session); + if (typed->child == nullptr) { + closeOrder.emplace_back("parent"); + } else { + const auto* child = reinterpret_cast(typed->child); + closeOrder.emplace_back("child-" + std::to_string(child->id)); + } + ++closed; + delete typed; + } + + [[nodiscard]] static auto setJobParameter(QDMI_Device_Job, + QDMI_Device_Job_Parameter, size_t, + const void*) -> int { + return QDMI_SUCCESS; + } + [[nodiscard]] auto queryJobProperty(QDMI_Device_Job, + const QDMI_Device_Job_Property property, + const size_t size, void* value, + size_t* sizeRet) const -> int { + if (property != QDMI_DEVICE_JOB_PROPERTY_PROGRAMFORMAT) { + return QDMI_ERROR_NOTSUPPORTED; + } + if (sizeRet != nullptr) { + *sizeRet = sizeof(programFormat); + } + if (value != nullptr) { + if (size < sizeof(programFormat)) { + return QDMI_ERROR_INVALIDARGUMENT; + } + std::memcpy(value, &programFormat, sizeof(programFormat)); + } + return QDMI_SUCCESS; + } + void submitJob(QDMI_Device_Job) const {} + void cancelJob(QDMI_Device_Job) const {} + [[nodiscard]] auto checkJob(QDMI_Device_Job) const -> QDMI_Job_Status { + return jobStatus; + } + [[nodiscard]] static auto waitJob(QDMI_Device_Job, size_t) -> bool { + return false; + } + [[nodiscard]] static auto getJobResult(QDMI_Device_Job, QDMI_Job_Result, + size_t, void*, size_t*) -> int { + return QDMI_ERROR_NOTSUPPORTED; + } + + [[nodiscard]] auto queryDevice(QDMI_Device_Session session, + QDMI_Device_Property property, + const size_t size, void* value, + size_t* sizeRet) const -> int { + const auto* typed = asSession(session); + if (property == QDMI_DEVICE_PROPERTY_CHILDDEVICES) { + if (typed->child != nullptr || behavior == ChildBehavior::Unsupported) { + return QDMI_ERROR_NOTSUPPORTED; + } + if (behavior == ChildBehavior::QueryFailure) { + return QDMI_ERROR_BADSTATE; + } + const auto required = behavior == ChildBehavior::Malformed + ? sizeof(QDMI_Child_Device) + 1 + : children_.size() * sizeof(QDMI_Child_Device); + if (sizeRet != nullptr) { + *sizeRet = required; + } + if (value != nullptr) { + if (size < required || behavior == ChildBehavior::Malformed) { + return QDMI_ERROR_INVALIDARGUMENT; + } + auto* firstChild = children_.data(); + // The fixed-size fake owns two contiguous child records. + // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) + auto* secondChild = children_.data() + 1; + std::array handles{ + reinterpret_cast(firstChild), + reinterpret_cast(secondChild)}; + std::memcpy(value, static_cast(handles.data()), required); + } + return QDMI_SUCCESS; + } + if (property == QDMI_DEVICE_PROPERTY_STATUS) { + if (sizeRet != nullptr) { + *sizeRet = sizeof(deviceStatus); + } + if (value != nullptr) { + if (size < sizeof(deviceStatus)) { + return QDMI_ERROR_INVALIDARGUMENT; + } + std::memcpy(value, &deviceStatus, sizeof(deviceStatus)); + } + return QDMI_SUCCESS; + } + if (property == QDMI_DEVICE_PROPERTY_SUPPORTEDPROGRAMFORMATS) { + if (sizeRet != nullptr) { + *sizeRet = sizeof(programFormat); + } + if (value != nullptr) { + if (size < sizeof(programFormat)) { + return QDMI_ERROR_INVALIDARGUMENT; + } + std::memcpy(value, &programFormat, sizeof(programFormat)); + } + return QDMI_SUCCESS; + } + if (property != QDMI_DEVICE_PROPERTY_NAME) { + return QDMI_ERROR_NOTSUPPORTED; + } + const auto name = + typed->child == nullptr + ? std::string("parent") + : "child-" + std::to_string( + reinterpret_cast(typed->child)->id); + if (sizeRet != nullptr) { + *sizeRet = name.size() + 1; + } + if (value != nullptr) { + if (size < name.size() + 1) { + return QDMI_ERROR_INVALIDARGUMENT; + } + std::memcpy(value, name.c_str(), name.size() + 1); + } + return QDMI_SUCCESS; + } + + [[nodiscard]] static auto querySite(QDMI_Device_Session, QDMI_Site, + QDMI_Site_Property, size_t, void*, + size_t*) -> int { + return QDMI_ERROR_NOTSUPPORTED; + } + [[nodiscard]] static auto queryOperation(QDMI_Device_Session, QDMI_Operation, + size_t, const QDMI_Site*, size_t, + const double*, + QDMI_Operation_Property, size_t, + void*, size_t*) -> int { + return QDMI_ERROR_NOTSUPPORTED; + } +}; +thread_local const ScriptedDeviceApi* ScriptedDeviceApi::activeApi = nullptr; +// NOLINTEND(readability-named-parameter,cppcoreguidelines-owning-memory) + +TEST(DeviceApiTest, ChildRetainsParentSessionAndLibrary) { + const auto api = std::make_shared(); + std::optional retainedChild; + { + const auto parent = DeviceFactory::create(api->deviceApi()); + EXPECT_EQ(parent.getName(), "parent"); + const auto children = parent.getChildDevices(); + ASSERT_EQ(children.size(), 2); + EXPECT_EQ(children[0].getName(), "child-0"); + EXPECT_EQ(children[1].getName(), "child-1"); + retainedChild = children[0]; + } + ASSERT_EQ(api->closeOrder.size(), 1); + EXPECT_EQ(api->closeOrder.front(), "child-1"); + retainedChild.reset(); + ASSERT_EQ(api->closeOrder.size(), 3); + EXPECT_EQ(api->closeOrder.back(), "parent"); + EXPECT_EQ(api->opened, api->closed); +} + +TEST(DeviceApiTest, ReusesCanonicalLibrariesProcessWide) { + const std::filesystem::path library = SC_DEVICE_LIBRARY; + const auto first = loadDeviceApi(library, "MQT_SC"); + const auto alias = library.parent_path() / "." / library.filename(); + const auto second = loadDeviceApi(alias, "MQT_SC"); + EXPECT_EQ(first, second); +} + +TEST(DeviceApiTest, ReopenWaitsForPriorFinalization) { + testLifecyclePrepareBlockingFinalize(); + auto first = loadDeviceApi(LIFECYCLE_DEVICE_LIBRARY, "TEST_LIFECYCLE"); + ASSERT_EQ(testLifecycleInitializeCount(), 1); + + std::thread finalize([api = std::move(first)]() mutable { api.reset(); }); + if (!testLifecycleWaitForFinalize(1000)) { + testLifecycleReleaseFinalize(); + finalize.join(); + FAIL() << "device finalization did not start"; + } + + std::barrier reopenReady(2); + std::shared_ptr reopened; + std::thread reopen([&] { + reopenReady.arrive_and_wait(); + reopened = loadDeviceApi(LIFECYCLE_DEVICE_LIBRARY, "TEST_LIFECYCLE"); + }); + reopenReady.arrive_and_wait(); + + const auto initializedBeforeFinalize = + testLifecycleWaitForInitializations(2, 250); + testLifecycleReleaseFinalize(); + finalize.join(); + reopen.join(); + + EXPECT_FALSE(initializedBeforeFinalize); + EXPECT_EQ(testLifecycleOverlappingInitializeCount(), 0); + EXPECT_EQ(testLifecycleInitializeCount(), 2); + EXPECT_EQ(testLifecycleFinalizeCount(), 1); + reopened.reset(); + EXPECT_EQ(testLifecycleFinalizeCount(), 2); +} + +TEST(DeviceApiTest, HandlesUnsupportedAndInvalidChildLists) { + const auto unsupported = std::make_shared(); + unsupported->behavior = ScriptedDeviceApi::ChildBehavior::Unsupported; + EXPECT_TRUE(DeviceFactory::create(unsupported->deviceApi()) + .getChildDevices() + .empty()); + EXPECT_EQ(unsupported->opened, unsupported->closed); + + const auto malformed = std::make_shared(); + malformed->behavior = ScriptedDeviceApi::ChildBehavior::Malformed; + EXPECT_THROW(static_cast(DeviceFactory::create(malformed->deviceApi())), + std::runtime_error); + EXPECT_EQ(malformed->opened, malformed->closed); + + const auto failed = std::make_shared(); + failed->behavior = ScriptedDeviceApi::ChildBehavior::QueryFailure; + EXPECT_THROW(static_cast(DeviceFactory::create(failed->deviceApi())), + std::runtime_error); + EXPECT_EQ(failed->opened, failed->closed); +} + +TEST(DeviceApiTest, CleansUpWhenChildSelectionFails) { + const auto api = std::make_shared(); + api->behavior = ScriptedDeviceApi::ChildBehavior::SelectionFailure; + EXPECT_THROW(static_cast(DeviceFactory::create(api->deviceApi())), + std::runtime_error); + EXPECT_EQ(api->opened, api->closed); +} + +TEST(DeviceApiTest, RejectsInvalidCustomPropertySelector) { + const auto api = std::make_shared(); + api->behavior = ScriptedDeviceApi::ChildBehavior::Unsupported; + const auto device = DeviceFactory::create(api->deviceApi()); + // NOLINTNEXTLINE(clang-analyzer-optin.core.EnumCastOutOfRange) + constexpr auto invalid = static_cast(0); + EXPECT_THROW(static_cast(device.queryCustomProperty(invalid)), + std::invalid_argument); +} + +TEST(DeviceApiTest, ReturnsQdmiEnumValuesWithoutRedefiningThem) { + const auto api = std::make_shared(); + api->behavior = ScriptedDeviceApi::ChildBehavior::Unsupported; + const auto device = DeviceFactory::create(api->deviceApi()); + // QDMI owns these enum types and values; MQT forwards them unchanged. + // NOLINTBEGIN(clang-analyzer-optin.core.EnumCastOutOfRange) + api->deviceStatus = static_cast(QDMI_DEVICE_STATUS_MAX); + EXPECT_EQ(device.getStatus(), api->deviceStatus); + api->programFormat = + static_cast(QDMI_PROGRAM_FORMAT_MAX); + EXPECT_EQ(device.getSupportedProgramFormats(), + std::vector{api->programFormat}); + auto job = device.submitJob("", QDMI_PROGRAM_FORMAT_QASM3, 1); + api->jobStatus = static_cast(1234); + EXPECT_EQ(job.check(), api->jobStatus); + EXPECT_EQ(job.getProgramFormat(), api->programFormat); + constexpr auto providerDefinedFormat = static_cast(1234); + EXPECT_NO_THROW( + static_cast(device.submitJob("", providerDefinedFormat, 1))); + // NOLINTEND(clang-analyzer-optin.core.EnumCastOutOfRange) +} +} // namespace +} // namespace qdmi::detail diff --git a/test/qdmi/driver/CMakeLists.txt b/test/qdmi/driver/CMakeLists.txt deleted file mode 100644 index 5626e931c1..0000000000 --- a/test/qdmi/driver/CMakeLists.txt +++ /dev/null @@ -1,163 +0,0 @@ -# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM -# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH -# All rights reserved. -# -# SPDX-License-Identifier: MIT -# -# Licensed under the MIT License - -set(TARGET_NAME mqt-core-qdmi-driver-test) - -if(TARGET MQT::CoreQDMIDriver) - add_library(mqt-core-qdmi-metadata-device SHARED metadata_device.cpp) - set_target_properties( - mqt-core-qdmi-metadata-device - PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/device" - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/device" - QDMI_DEVICE_ID "test.metadata-only" - QDMI_DEVICE_PREFIX "TEST_METADATA" - QDMI_RUNTIME_FILES "metadata-runtime.json") - add_custom_command( - TARGET mqt-core-qdmi-metadata-device - POST_BUILD - COMMAND - ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_CURRENT_SOURCE_DIR}/metadata-runtime.json" - "$/metadata-runtime.json") - set_property( - TARGET mqt-core-qdmi-metadata-device - APPEND - PROPERTY EXPORT_PROPERTIES QDMI_DEVICE_ID QDMI_DEVICE_PREFIX QDMI_RUNTIME_FILES) - set(metadata_device_export - "${CMAKE_CURRENT_BINARY_DIR}/mqt-core-qdmi-metadata-device-targets.cmake") - export(TARGETS mqt-core-qdmi-metadata-device FILE "${metadata_device_export}") - - add_library(mqt-core-qdmi-session-device SHARED session_device.cpp) - target_link_libraries(mqt-core-qdmi-session-device PRIVATE qdmi::qdmi) - set_target_properties( - mqt-core-qdmi-session-device - PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/device" - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/device") - - package_add_test(${TARGET_NAME} MQT::CoreQDMIDriver test_driver.cpp) - target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQDMI) - set(config_file "${CMAKE_CURRENT_BINARY_DIR}/$/configured-devices.json") - file( - GENERATE - OUTPUT "${config_file}" - CONTENT - "{\n \"schema-version\": 1,\n \"qdmi\": {\n \"devices\": [\n {\"id\": \"mqt.sc.default\", \"library\": \"$\", \"prefix\": \"MQT_SC\"},\n {\"id\": \"mqt.ddsim.default\", \"library\": \"$\", \"prefix\": \"MQT_DDSIM\"},\n {\"id\": \"test.disabled\", \"enabled\": false},\n {\"id\": \"broken.example\", \"library\": \"missing-device-library\", \"prefix\": \"BROKEN\"}\n ]\n }\n}\n" - ) - file(READ "${PROJECT_SOURCE_DIR}/json/sc/mqt-core-qdmi-sc-device.json" custom_sc_json) - string(REPLACE "MQT SC Default QDMI Device" "Custom SC Driver Device" custom_sc_json - "${custom_sc_json}") - string(REPLACE "\"duration\": 20" "\"duration\": 77" custom_sc_json "${custom_sc_json}") - set(custom_sc_file "${CMAKE_CURRENT_BINARY_DIR}/custom-sc-device.json") - file(WRITE "${custom_sc_file}" "${custom_sc_json}") - string(MAKE_C_IDENTIFIER "${TARGET_NAME}-mqt-core-qdmi-metadata-device" metadata_manifest_stem) - set(metadata_manifest_file - "$/${metadata_manifest_stem}.qdmi.json") - target_compile_definitions( - ${TARGET_NAME} - PRIVATE - "MQT_CORE_QDMI_TEST_CONFIG_FILE=\"${config_file}\"" - "MQT_CORE_QDMI_METADATA_MANIFEST=\"${metadata_manifest_file}\"" - "MQT_CORE_QDMI_SESSION_DEVICE=\"$\"" - "MQT_CORE_QDMI_CUSTOM_SC_FILE=\"${custom_sc_file}\"" - "MQT_CORE_QDMI_DEFAULT_SC_FILE=\"${PROJECT_SOURCE_DIR}/json/sc/mqt-core-qdmi-sc-device.json\"" - "MQT_CORE_QDMI_SC_LIBRARY=\"$\"" - "TEST_DEVICE_LIBRARIES=std::array{ std::pair{\"$\", \"MQT_SC\"}, std::pair{\"$\", \"MQT_DDSIM\"} }" - ) - - mqt_copy_qdmi_runtime(${TARGET_NAME}) - mqt_copy_qdmi_runtime(${TARGET_NAME} mqt-core-qdmi-metadata-device) - add_dependencies(${TARGET_NAME} mqt-core-qdmi-session-device) - - set(imported_device_build_dir "${CMAKE_CURRENT_BINARY_DIR}/imported-device-consumer") - set(imported_device_configure_command - ${CMAKE_COMMAND} -S "${CMAKE_CURRENT_SOURCE_DIR}/imported_device" -B - "${imported_device_build_dir}" -G "${CMAKE_GENERATOR}" - "-DMQT_CORE_QDMI_DEVICE_TARGETS=${metadata_device_export}" - "-DMQT_CORE_QDMI_HELPER=${PROJECT_SOURCE_DIR}/cmake/AddMQTQDMIDevice.cmake") - if(CMAKE_GENERATOR_PLATFORM) - list(APPEND imported_device_configure_command -A "${CMAKE_GENERATOR_PLATFORM}") - endif() - if(CMAKE_GENERATOR_TOOLSET) - list(APPEND imported_device_configure_command -T "${CMAKE_GENERATOR_TOOLSET}") - endif() - add_test(NAME mqt-core-qdmi-imported-device-configure - COMMAND ${imported_device_configure_command}) - add_test(NAME mqt-core-qdmi-imported-device-build - COMMAND ${CMAKE_COMMAND} --build "${imported_device_build_dir}" --config $) - set_tests_properties(mqt-core-qdmi-imported-device-configure - PROPERTIES FIXTURES_SETUP mqt-core-qdmi-imported-device) - set_tests_properties(mqt-core-qdmi-imported-device-build PROPERTIES FIXTURES_REQUIRED - mqt-core-qdmi-imported-device) - - set(runtime_file_device_build_dir "${CMAKE_CURRENT_BINARY_DIR}/runtime-file-device") - set(runtime_file_device_install_dir "${runtime_file_device_build_dir}/install") - if(WIN32) - set(runtime_file_device_artifact_dir "${CMAKE_INSTALL_BINDIR}") - else() - set(runtime_file_device_artifact_dir "${CMAKE_INSTALL_LIBDIR}") - endif() - set(runtime_file_device_configure_command - ${CMAKE_COMMAND} -S "${CMAKE_CURRENT_SOURCE_DIR}/runtime_file_device" -B - "${runtime_file_device_build_dir}" -G "${CMAKE_GENERATOR}" - "-DMQT_CORE_QDMI_HELPER=${PROJECT_SOURCE_DIR}/cmake/AddMQTQDMIDevice.cmake" - "-DMQT_CORE_QDMI_TEST_SOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR}" "-DCMAKE_BUILD_TYPE=$") - if(CMAKE_GENERATOR_PLATFORM) - list(APPEND runtime_file_device_configure_command -A "${CMAKE_GENERATOR_PLATFORM}") - endif() - if(CMAKE_GENERATOR_TOOLSET) - list(APPEND runtime_file_device_configure_command -T "${CMAKE_GENERATOR_TOOLSET}") - endif() - add_test(NAME mqt-core-qdmi-runtime-file-configure - COMMAND ${runtime_file_device_configure_command}) - add_test(NAME mqt-core-qdmi-runtime-file-build - COMMAND ${CMAKE_COMMAND} --build "${runtime_file_device_build_dir}" --config $) - add_test(NAME mqt-core-qdmi-runtime-file-install - COMMAND ${CMAKE_COMMAND} --install "${runtime_file_device_build_dir}" --config $ - --prefix "${runtime_file_device_install_dir}") - add_test( - NAME mqt-core-qdmi-runtime-file-install-verify - COMMAND - ${CMAKE_COMMAND} -E compare_files - "${runtime_file_device_install_dir}/${runtime_file_device_artifact_dir}/metadata-runtime.json" - "${CMAKE_CURRENT_SOURCE_DIR}/metadata-runtime.json") - set_tests_properties(mqt-core-qdmi-runtime-file-configure - PROPERTIES FIXTURES_SETUP mqt-core-qdmi-runtime-file-configure) - set_tests_properties( - mqt-core-qdmi-runtime-file-build - PROPERTIES FIXTURES_REQUIRED mqt-core-qdmi-runtime-file-configure FIXTURES_SETUP - mqt-core-qdmi-runtime-file-build) - set_tests_properties( - mqt-core-qdmi-runtime-file-install - PROPERTIES FIXTURES_REQUIRED mqt-core-qdmi-runtime-file-build FIXTURES_SETUP - mqt-core-qdmi-runtime-file-install) - set_tests_properties(mqt-core-qdmi-runtime-file-install-verify - PROPERTIES FIXTURES_REQUIRED mqt-core-qdmi-runtime-file-install) - - function(add_qdmi_configuration_rejection_test name invalid_configuration) - set(build_dir "${CMAKE_CURRENT_BINARY_DIR}/runtime-file-device-invalid-${name}") - set(configure_command - ${CMAKE_COMMAND} -S "${CMAKE_CURRENT_SOURCE_DIR}/runtime_file_device" -B "${build_dir}" -G - "${CMAKE_GENERATOR}" - "-DMQT_CORE_QDMI_HELPER=${PROJECT_SOURCE_DIR}/cmake/AddMQTQDMIDevice.cmake" - "-DMQT_CORE_QDMI_TEST_SOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR}" - "-DMQT_CORE_QDMI_INVALID_CONFIGURATION=${invalid_configuration}" - "-DCMAKE_BUILD_TYPE=$") - if(CMAKE_GENERATOR_PLATFORM) - list(APPEND configure_command -A "${CMAKE_GENERATOR_PLATFORM}") - endif() - if(CMAKE_GENERATOR_TOOLSET) - list(APPEND configure_command -T "${CMAKE_GENERATOR_TOOLSET}") - endif() - add_test(NAME "mqt-core-qdmi-configuration-rejects-${name}" COMMAND ${configure_command}) - set_tests_properties("mqt-core-qdmi-configuration-rejects-${name}" PROPERTIES WILL_FAIL TRUE) - endfunction() - - add_qdmi_configuration_rejection_test(empty-id empty-id) - add_qdmi_configuration_rejection_test(empty-file empty-file) - add_qdmi_configuration_rejection_test(duplicate-base-id duplicate-base-id) - add_qdmi_configuration_rejection_test(duplicate-configured-id duplicate-configured-id) -endif() diff --git a/test/qdmi/driver/test_driver.cpp b/test/qdmi/driver/test_driver.cpp deleted file mode 100644 index e85197a850..0000000000 --- a/test/qdmi/driver/test_driver.cpp +++ /dev/null @@ -1,1551 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "qdmi/Client.hpp" -#include "qdmi/driver/Driver.hpp" - -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace testing { -namespace { -auto stringConcat5(const std::string& a, const std::string& b, - const std::string& c, const std::string& d, - const std::string& e) -> std::string { - std::stringstream ss; - ss << a << b << c << d << e; - return ss.str(); -} -// NOLINTBEGIN(readability-identifier-naming,cppcoreguidelines-avoid-const-or-ref-data-members) -MATCHER_P2(IsBetween, a, b, - stringConcat5(negation ? "isn't" : "is", " between ", - PrintToString(a), " and ", PrintToString(b))) { - return a <= arg && arg <= b; -} -// NOLINTEND(readability-identifier-naming,cppcoreguidelines-avoid-const-or-ref-data-members) -} // namespace -} // namespace testing - -namespace qc { - -namespace { - -struct ConfiguredDriverEnvironment { - ConfiguredDriverEnvironment() noexcept { -#ifdef _WIN32 - if (_putenv_s("MQT_CORE_QDMI_CONFIG_FILE", - MQT_CORE_QDMI_TEST_CONFIG_FILE) != 0) { -#else - // POSIX exposes setenv through , but include-cleaner does not - // associate the global declaration with that C++ header. - // NOLINTNEXTLINE(misc-include-cleaner) - if (setenv("MQT_CORE_QDMI_CONFIG_FILE", MQT_CORE_QDMI_TEST_CONFIG_FILE, - 1) != 0) { -#endif - std::abort(); - } - } -}; - -const ConfiguredDriverEnvironment CONFIGURED_DRIVER_ENVIRONMENT; - -class ChildDeviceLibrary final : public qdmi::DeviceLibrary { - struct Child { - size_t id; - }; - - struct Session { - ChildDeviceLibrary* library = nullptr; - QDMI_Child_Device child = nullptr; - bool initialized = false; - }; - - static inline ChildDeviceLibrary* activeLibrary = nullptr; - std::array children_{{{0}, {1}}}; - std::unordered_map> sessions_; - - [[nodiscard]] static auto asSession(QDMI_Device_Session_impl_d* const session) - -> Session* { - return reinterpret_cast(session); - } - - static auto alloc(QDMI_Device_Session* session) -> int { - if (session == nullptr || activeLibrary == nullptr) { - return QDMI_ERROR_INVALIDARGUMENT; - } - auto fakeSession = - std::make_unique(Session{.library = activeLibrary}); - auto* const sessionPtr = fakeSession.get(); - auto* const sessionHandle = - reinterpret_cast(sessionPtr); - activeLibrary->sessions_.emplace(sessionHandle, std::move(fakeSession)); - ++activeLibrary->allocatedSessions; - *session = sessionHandle; - return QDMI_SUCCESS; - } - - static void free(QDMI_Device_Session session) { - if (session == nullptr) { - return; - } - auto* const fakeSession = asSession(session); - ++fakeSession->library->freedSessions; - fakeSession->library->sessions_.erase(session); - } - - static auto setParameter(QDMI_Device_Session session, - const QDMI_Device_Session_Parameter parameter, - const size_t size, const void* value) -> int { - if (session == nullptr || value == nullptr || size == 0) { - return QDMI_ERROR_INVALIDARGUMENT; - } - if (parameter != QDMI_DEVICE_SESSION_PARAMETER_CHILDDEVICE) { - return QDMI_ERROR_NOTSUPPORTED; - } - auto* const fakeSession = asSession(session); - if (fakeSession->library->rejectChildSelection || - size != sizeof(QDMI_Child_Device)) { - return QDMI_ERROR_NOTSUPPORTED; - } - std::memcpy(static_cast(&fakeSession->child), value, - sizeof(QDMI_Child_Device)); - fakeSession->library->selectedChildren.emplace_back(fakeSession->child); - return QDMI_SUCCESS; - } - - static auto init(QDMI_Device_Session session) -> int { - if (session == nullptr) { - return QDMI_ERROR_INVALIDARGUMENT; - } - asSession(session)->initialized = true; - return QDMI_SUCCESS; - } - - static auto queryDeviceProperty(QDMI_Device_Session session, - const QDMI_Device_Property property, - const size_t size, void* value, - size_t* sizeRet) -> int { - if (session == nullptr || (value != nullptr && size == 0)) { - return QDMI_ERROR_INVALIDARGUMENT; - } - auto* const fakeSession = asSession(session); - if (!fakeSession->initialized) { - return QDMI_ERROR_BADSTATE; - } - - if (property == QDMI_DEVICE_PROPERTY_CHILDDEVICES) { - if (fakeSession->child != nullptr) { - return QDMI_ERROR_NOTSUPPORTED; - } - auto* const library = fakeSession->library; - if (library->childDevicesNotSupported) { - return QDMI_ERROR_NOTSUPPORTED; - } - if (library->childDeviceQueryFails) { - return QDMI_ERROR_BADSTATE; - } - const size_t requiredSize = - library->malformedChildList - ? sizeof(QDMI_Child_Device) + 1 - : library->children_.size() * sizeof(QDMI_Child_Device); - if (sizeRet != nullptr) { - *sizeRet = requiredSize; - } - if (value != nullptr) { - if (size < requiredSize || library->malformedChildList) { - return QDMI_ERROR_INVALIDARGUMENT; - } - std::array handles{}; - std::ranges::transform( - library->children_, handles.begin(), [](Child& child) { - return reinterpret_cast(&child); - }); - std::memcpy(value, static_cast(handles.data()), - requiredSize); - } - return QDMI_SUCCESS; - } - - if (property == QDMI_DEVICE_PROPERTY_NAME) { - std::string name = "parent"; - if (fakeSession->child != nullptr) { - const auto* child = reinterpret_cast(fakeSession->child); - name = "child-" + std::to_string(child->id); - } - const auto requiredSize = name.size() + 1; - if (sizeRet != nullptr) { - *sizeRet = requiredSize; - } - if (value != nullptr) { - if (size < requiredSize) { - return QDMI_ERROR_INVALIDARGUMENT; - } - std::memcpy(value, name.c_str(), requiredSize); - } - return QDMI_SUCCESS; - } - return QDMI_ERROR_NOTSUPPORTED; - } - -public: - size_t allocatedSessions = 0; - size_t freedSessions = 0; - bool rejectChildSelection = false; - bool malformedChildList = false; - bool childDevicesNotSupported = false; - bool childDeviceQueryFails = false; - std::vector selectedChildren; - - ChildDeviceLibrary() { - activeLibrary = this; - device_session_alloc = alloc; - device_session_free = free; - device_session_set_parameter = setParameter; - device_session_init = init; - device_session_query_device_property = queryDeviceProperty; - } - - ~ChildDeviceLibrary() override { activeLibrary = nullptr; } - - [[nodiscard]] auto childHandle(const size_t index) -> QDMI_Child_Device { - return reinterpret_cast(&children_.at(index)); - } -}; - -[[nodiscard]] auto queryName(QDMI_Device_impl_d* const device) -> std::string { - size_t size = 0; - EXPECT_EQ(QDMI_device_query_device_property(device, QDMI_DEVICE_PROPERTY_NAME, - 0, nullptr, &size), - QDMI_SUCCESS); - std::string name(size - 1, '\0'); - EXPECT_EQ(QDMI_device_query_device_property(device, QDMI_DEVICE_PROPERTY_NAME, - size, name.data(), nullptr), - QDMI_SUCCESS); - return name; -} - -[[nodiscard]] auto openTestDevice(const std::string& library, - const std::string& prefix, - const qdmi::DeviceSessionConfig& session = {}) - -> QDMI_Device { - static size_t nextId = 0; - auto& driver = qdmi::Driver::get(); - const auto id = "test.runtime." + std::to_string(nextId++); - driver.registerDevice( - {.id = id, .library = library, .prefix = prefix, .session = session}); - return driver.open(id); -} - -class DriverTest : public testing::TestWithParam { -protected: - QDMI_Session session = nullptr; - QDMI_Device device = nullptr; - - void SetUp() override { - const auto& deviceName = GetParam(); - - ASSERT_EQ(QDMI_session_alloc(&session), QDMI_SUCCESS) - << "Failed to allocate session."; - - ASSERT_EQ(QDMI_session_init(session), QDMI_SUCCESS) - << "Failed to initialize session."; - - size_t devicesSize = 0; - ASSERT_EQ(QDMI_session_query_session_property(session, - QDMI_SESSION_PROPERTY_DEVICES, - 0, nullptr, &devicesSize), - QDMI_SUCCESS) - << "Failed to retrieve number of devices."; - std::vector devices(devicesSize / sizeof(QDMI_Device)); - ASSERT_EQ(QDMI_session_query_session_property( - session, QDMI_SESSION_PROPERTY_DEVICES, devicesSize, - static_cast(devices.data()), nullptr), - QDMI_SUCCESS) - << "Failed to retrieve devices."; - - for (auto* const dev : devices) { - size_t namesSize = 0; - ASSERT_EQ(QDMI_device_query_device_property( - dev, QDMI_DEVICE_PROPERTY_NAME, 0, nullptr, &namesSize), - QDMI_SUCCESS) - << "Failed to retrieve the length of the device's name."; - std::string name(namesSize - 1, '\0'); - ASSERT_EQ( - QDMI_device_query_device_property(dev, QDMI_DEVICE_PROPERTY_NAME, - namesSize, name.data(), nullptr), - QDMI_SUCCESS) - << "Failed to retrieve the device's name."; - - ASSERT_FALSE(name.empty()) << "Device must provide a non-empty name."; - - if (name == deviceName) { - device = dev; - return; - } - } - FAIL() << "Device with name '" << deviceName - << "' not found in the session."; - } - - void TearDown() override { QDMI_session_free(session); } -}; - -class DriverJobTest : public DriverTest { -protected: - QDMI_Job job = nullptr; - - void SetUp() override { - DriverTest::SetUp(); - ASSERT_EQ(QDMI_device_create_job(device, &job), QDMI_SUCCESS) - << "Failed to create a device job."; - } - - void TearDown() override { - if (job != nullptr) { - QDMI_job_free(job); - job = nullptr; - } - DriverTest::TearDown(); - } -}; - -} // namespace - -TEST(ChildDeviceTest, WrapsOpaqueHandlesInStableClientDevices) { - const auto library = std::make_shared(); - { - QDMI_Device_impl_d parent(library); - size_t size = 0; - ASSERT_EQ( - QDMI_device_query_device_property( - &parent, QDMI_DEVICE_PROPERTY_CHILDDEVICES, 0, nullptr, &size), - QDMI_SUCCESS); - ASSERT_EQ(size, 2 * sizeof(QDMI_Device)); - - std::array children{}; - ASSERT_EQ(QDMI_device_query_device_property( - &parent, QDMI_DEVICE_PROPERTY_CHILDDEVICES, size, - static_cast(children.data()), nullptr), - QDMI_SUCCESS); - EXPECT_EQ(queryName(children[0]), "child-0"); - EXPECT_EQ(queryName(children[1]), "child-1"); - - const auto qdmiChildren = - qdmi::Session::createSessionlessDevice(&parent).getChildDevices(); - ASSERT_EQ(qdmiChildren.size(), 2); - EXPECT_EQ(qdmiChildren[0].getName(), "child-0"); - EXPECT_EQ(qdmiChildren[1].getName(), "child-1"); - - std::array repeatedQuery{}; - ASSERT_EQ(QDMI_device_query_device_property( - &parent, QDMI_DEVICE_PROPERTY_CHILDDEVICES, size, - static_cast(repeatedQuery.data()), nullptr), - QDMI_SUCCESS); - EXPECT_EQ(repeatedQuery, children); - EXPECT_EQ(library->selectedChildren, - (std::vector{library->childHandle(0), library->childHandle(1)})); - EXPECT_EQ(library->allocatedSessions, 3); - EXPECT_EQ(library->freedSessions, 0); - - EXPECT_EQ(QDMI_device_query_device_property( - &parent, QDMI_DEVICE_PROPERTY_CHILDDEVICES, - sizeof(QDMI_Device), static_cast(children.data()), - nullptr), - QDMI_ERROR_INVALIDARGUMENT); - EXPECT_EQ(QDMI_device_query_device_property( - children[0], QDMI_DEVICE_PROPERTY_CHILDDEVICES, 0, nullptr, - nullptr), - QDMI_ERROR_NOTSUPPORTED); - } - EXPECT_EQ(library->freedSessions, 3); -} - -TEST(ChildDeviceTest, CleansUpWhenSelectingAChildFails) { - const auto library = std::make_shared(); - library->rejectChildSelection = true; - EXPECT_THROW(QDMI_Device_impl_d{library}, std::runtime_error); - EXPECT_EQ(library->allocatedSessions, 2); - EXPECT_EQ(library->freedSessions, 2); -} - -TEST(ChildDeviceTest, RejectsMalformedChildLists) { - const auto library = std::make_shared(); - library->malformedChildList = true; - EXPECT_THROW(QDMI_Device_impl_d{library}, std::runtime_error); - EXPECT_EQ(library->allocatedSessions, 1); - EXPECT_EQ(library->freedSessions, 1); -} - -TEST(ChildDeviceTest, SupportsDevicesWithoutChildDevices) { - const auto library = std::make_shared(); - library->childDevicesNotSupported = true; - { - QDMI_Device_impl_d parent(library); - size_t size = 0; - EXPECT_EQ( - QDMI_device_query_device_property( - &parent, QDMI_DEVICE_PROPERTY_CHILDDEVICES, 0, nullptr, &size), - QDMI_ERROR_NOTSUPPORTED); - EXPECT_EQ(library->allocatedSessions, 1); - } - EXPECT_EQ(library->freedSessions, 1); -} - -TEST(ChildDeviceTest, CleansUpWhenQueryingChildDevicesFails) { - const auto library = std::make_shared(); - library->childDeviceQueryFails = true; - EXPECT_THROW(QDMI_Device_impl_d{library}, std::runtime_error); - EXPECT_EQ(library->allocatedSessions, 1); - EXPECT_EQ(library->freedSessions, 1); -} - -TEST_P(DriverTest, SessionSetParameter) { - const std::string authFile = "authfile.txt"; - QDMI_Session uninitializedSession = nullptr; - ASSERT_EQ(QDMI_session_alloc(&uninitializedSession), QDMI_SUCCESS); - EXPECT_EQ(QDMI_session_set_parameter(uninitializedSession, - QDMI_SESSION_PARAMETER_AUTHFILE, 13, - authFile.c_str()), - QDMI_ERROR_NOTSUPPORTED); - EXPECT_EQ(QDMI_session_set_parameter(uninitializedSession, - QDMI_SESSION_PARAMETER_MAX, 0, nullptr), - QDMI_ERROR_INVALIDARGUMENT); - EXPECT_EQ(QDMI_session_set_parameter(session, QDMI_SESSION_PARAMETER_AUTHFILE, - 13, authFile.c_str()), - QDMI_ERROR_BADSTATE); - EXPECT_EQ(QDMI_session_set_parameter(nullptr, QDMI_SESSION_PARAMETER_AUTHFILE, - 13, authFile.c_str()), - QDMI_ERROR_INVALIDARGUMENT); -} - -TEST_P(DriverTest, JobCreate) { - QDMI_Job job = nullptr; - EXPECT_EQ(QDMI_device_create_job(device, &job), QDMI_SUCCESS); - QDMI_job_free(job); - EXPECT_EQ(QDMI_device_create_job(device, nullptr), - QDMI_ERROR_INVALIDARGUMENT); -} - -TEST_P(DriverTest, JobSetParameter) { - EXPECT_EQ(QDMI_job_set_parameter(nullptr, QDMI_JOB_PARAMETER_MAX, 0, nullptr), - QDMI_ERROR_INVALIDARGUMENT); -} - -TEST_P(DriverJobTest, JobSetParameter) { - EXPECT_THAT(QDMI_job_set_parameter(job, QDMI_JOB_PARAMETER_PROGRAM, - sizeof(QDMI_Program_Format), nullptr), - testing::AnyOf(QDMI_SUCCESS, QDMI_ERROR_NOTSUPPORTED)); - const QDMI_Program_Format value = QDMI_PROGRAM_FORMAT_QASM2; - EXPECT_THAT(QDMI_job_set_parameter(job, QDMI_JOB_PARAMETER_PROGRAMFORMAT, - sizeof(QDMI_Program_Format), &value), - testing::AnyOf(QDMI_SUCCESS, QDMI_ERROR_NOTSUPPORTED)); - const size_t numShots = 1; - EXPECT_THAT(QDMI_job_set_parameter(job, QDMI_JOB_PARAMETER_SHOTSNUM, - sizeof(size_t), &numShots), - testing::AnyOf(QDMI_SUCCESS, QDMI_ERROR_NOTSUPPORTED)); - constexpr std::array customParams{ - QDMI_JOB_PARAMETER_CUSTOM1, QDMI_JOB_PARAMETER_CUSTOM2, - QDMI_JOB_PARAMETER_CUSTOM3, QDMI_JOB_PARAMETER_CUSTOM4, - QDMI_JOB_PARAMETER_CUSTOM5}; - for (const auto param : customParams) { - EXPECT_THAT(QDMI_job_set_parameter(job, param, 0, nullptr), - testing::AnyOf(QDMI_SUCCESS, QDMI_ERROR_NOTSUPPORTED)); - } - EXPECT_EQ(QDMI_job_set_parameter(job, QDMI_JOB_PARAMETER_MAX, 0, nullptr), - QDMI_ERROR_INVALIDARGUMENT); -} - -TEST_P(DriverTest, JobQueryProperty) { - EXPECT_EQ(QDMI_job_query_property(nullptr, QDMI_JOB_PROPERTY_MAX, 0, nullptr, - nullptr), - QDMI_ERROR_INVALIDARGUMENT); -} - -TEST_P(DriverJobTest, JobQueryProperty) { - EXPECT_THAT( - QDMI_job_query_property(job, QDMI_JOB_PROPERTY_ID, 0, nullptr, nullptr), - testing::AnyOf(QDMI_SUCCESS, QDMI_ERROR_NOTSUPPORTED)); - - EXPECT_THAT(QDMI_job_query_property(job, QDMI_JOB_PROPERTY_PROGRAM, 0, - nullptr, nullptr), - testing::AnyOf(QDMI_SUCCESS, QDMI_ERROR_NOTSUPPORTED)); - - QDMI_Program_Format value = QDMI_PROGRAM_FORMAT_QASM2; - auto result = QDMI_job_set_parameter(job, QDMI_JOB_PARAMETER_PROGRAMFORMAT, - sizeof(QDMI_Program_Format), &value); - EXPECT_THAT(result, testing::AnyOf(QDMI_SUCCESS, QDMI_ERROR_NOTSUPPORTED)); - if (result == QDMI_SUCCESS) { - value = QDMI_PROGRAM_FORMAT_MAX; - EXPECT_EQ(QDMI_job_query_property(job, QDMI_JOB_PROPERTY_PROGRAMFORMAT, - sizeof(QDMI_Program_Format), &value, - nullptr), - QDMI_SUCCESS); - EXPECT_EQ(value, QDMI_PROGRAM_FORMAT_QASM2); - } - size_t numShots = 1; - result = QDMI_job_set_parameter(job, QDMI_JOB_PARAMETER_SHOTSNUM, - sizeof(QDMI_Program_Format), &numShots); - EXPECT_THAT(result, testing::AnyOf(QDMI_SUCCESS, QDMI_ERROR_NOTSUPPORTED)); - if (result == QDMI_SUCCESS) { - numShots = 0; - EXPECT_EQ(QDMI_job_query_property(job, QDMI_JOB_PROPERTY_SHOTSNUM, - sizeof(size_t), &numShots, nullptr), - QDMI_SUCCESS); - EXPECT_EQ(numShots, 1); - } - - EXPECT_EQ(QDMI_job_query_property(job, QDMI_JOB_PROPERTY_QUEUEPOSITION, 0, - nullptr, nullptr), - QDMI_ERROR_NOTSUPPORTED); - - constexpr std::array customProperties{ - QDMI_JOB_PROPERTY_CUSTOM1, QDMI_JOB_PROPERTY_CUSTOM2, - QDMI_JOB_PROPERTY_CUSTOM3, QDMI_JOB_PROPERTY_CUSTOM4, - QDMI_JOB_PROPERTY_CUSTOM5}; - for (const auto property : customProperties) { - EXPECT_EQ(QDMI_job_query_property(job, property, 0, nullptr, nullptr), - QDMI_ERROR_NOTSUPPORTED); - } -} - -TEST_P(DriverTest, JobSubmit) { - EXPECT_EQ(QDMI_job_submit(nullptr), QDMI_ERROR_INVALIDARGUMENT); -} - -TEST_P(DriverJobTest, JobSubmit) { - EXPECT_THAT(QDMI_job_submit(job), - testing::AnyOf(QDMI_SUCCESS, QDMI_ERROR_NOTSUPPORTED)); -} - -TEST_P(DriverTest, JobCancel) { - EXPECT_EQ(QDMI_job_cancel(nullptr), QDMI_ERROR_INVALIDARGUMENT); -} - -TEST_P(DriverJobTest, JobCancel) { - EXPECT_THAT(QDMI_job_cancel(job), - testing::AnyOf(QDMI_SUCCESS, QDMI_ERROR_INVALIDARGUMENT, - QDMI_ERROR_NOTSUPPORTED)); -} - -TEST_P(DriverTest, JobCheck) { - EXPECT_EQ(QDMI_job_check(nullptr, nullptr), QDMI_ERROR_INVALIDARGUMENT); -} - -TEST_P(DriverJobTest, JobCheck) { - QDMI_Job_Status status = QDMI_JOB_STATUS_RUNNING; - EXPECT_THAT(QDMI_job_check(job, &status), - testing::AnyOf(QDMI_SUCCESS, QDMI_ERROR_NOTSUPPORTED)); -} - -TEST_P(DriverTest, JobWait) { - EXPECT_EQ(QDMI_job_wait(nullptr, 0), QDMI_ERROR_INVALIDARGUMENT); -} - -TEST_P(DriverJobTest, JobWait) { - EXPECT_THAT(QDMI_job_wait(job, 1), - testing::AnyOf(QDMI_SUCCESS, QDMI_ERROR_NOTSUPPORTED, - QDMI_ERROR_TIMEOUT, QDMI_ERROR_BADSTATE)); -} - -TEST_P(DriverTest, JobGetResults) { - EXPECT_EQ( - QDMI_job_get_results(nullptr, QDMI_JOB_RESULT_MAX, 0, nullptr, nullptr), - QDMI_ERROR_INVALIDARGUMENT); -} - -TEST_P(DriverJobTest, JobGetResults) { - EXPECT_THAT( - QDMI_job_get_results(job, QDMI_JOB_RESULT_SHOTS, 0, nullptr, nullptr), - testing::AnyOf(QDMI_SUCCESS, QDMI_ERROR_NOTSUPPORTED, - QDMI_ERROR_BADSTATE)); -} - -TEST_P(DriverTest, QueryDeviceProperty) { - EXPECT_EQ(QDMI_device_query_device_property(device, QDMI_DEVICE_PROPERTY_MAX, - 0, nullptr, nullptr), - QDMI_ERROR_INVALIDARGUMENT); - EXPECT_EQ(QDMI_device_query_device_property(nullptr, QDMI_DEVICE_PROPERTY_MAX, - 0, nullptr, nullptr), - QDMI_ERROR_INVALIDARGUMENT); -} - -TEST_P(DriverTest, QuerySiteProperty) { - EXPECT_EQ(QDMI_device_query_site_property( - device, nullptr, QDMI_SITE_PROPERTY_MAX, 0, nullptr, nullptr), - QDMI_ERROR_INVALIDARGUMENT); - EXPECT_EQ(QDMI_device_query_site_property( - nullptr, nullptr, QDMI_SITE_PROPERTY_MAX, 0, nullptr, nullptr), - QDMI_ERROR_INVALIDARGUMENT); -} - -TEST_P(DriverTest, QueryOperationProperty) { - EXPECT_EQ(QDMI_device_query_operation_property( - device, nullptr, 0, nullptr, 0, nullptr, - QDMI_OPERATION_PROPERTY_MAX, 0, nullptr, nullptr), - QDMI_ERROR_INVALIDARGUMENT); - EXPECT_EQ(QDMI_device_query_operation_property( - nullptr, nullptr, 0, nullptr, 0, nullptr, - QDMI_OPERATION_PROPERTY_MAX, 0, nullptr, nullptr), - QDMI_ERROR_INVALIDARGUMENT); -} - -TEST_P(DriverTest, QueryDeviceVersion) { - size_t size = 0; - ASSERT_EQ(QDMI_device_query_device_property( - device, QDMI_DEVICE_PROPERTY_VERSION, 0, nullptr, &size), - QDMI_SUCCESS) - << "Devices must provide a version."; - std::string value(size - 1, '\0'); - ASSERT_EQ(QDMI_device_query_device_property(device, - QDMI_DEVICE_PROPERTY_VERSION, - size, value.data(), nullptr), - QDMI_SUCCESS) - << "Devices must provide a version."; - EXPECT_FALSE(value.empty()) << "Devices must provide a version."; -} - -TEST_P(DriverTest, QueryDeviceLibraryVersion) { - size_t size = 0; - ASSERT_EQ(QDMI_device_query_device_property( - device, QDMI_DEVICE_PROPERTY_LIBRARYVERSION, 0, nullptr, &size), - QDMI_SUCCESS) - << "Devices must provide a library version."; - std::string value(size - 1, '\0'); - ASSERT_EQ(QDMI_device_query_device_property( - device, QDMI_DEVICE_PROPERTY_LIBRARYVERSION, size, value.data(), - nullptr), - QDMI_SUCCESS) - << "Devices must provide a library version."; - ASSERT_FALSE(value.empty()) << "Devices must provide a library version."; -} - -TEST_P(DriverTest, QueryNumQubits) { - size_t numQubits = 0; - ASSERT_EQ( - QDMI_device_query_device_property(device, QDMI_DEVICE_PROPERTY_QUBITSNUM, - sizeof(size_t), &numQubits, nullptr), - QDMI_SUCCESS) - << "Devices must provide the number of qubits."; - EXPECT_GT(numQubits, 0) << "Number of qubits must be greater than 0."; -} - -TEST_P(DriverTest, QuerySites) { - size_t size = 0; - ASSERT_EQ(QDMI_device_query_device_property( - device, QDMI_DEVICE_PROPERTY_SITES, 0, nullptr, &size), - QDMI_SUCCESS) - << "Devices must provide a list of sites."; - std::vector sites(size / sizeof(QDMI_Site)); - ASSERT_EQ(QDMI_device_query_device_property( - device, QDMI_DEVICE_PROPERTY_SITES, size, - static_cast(sites.data()), nullptr), - QDMI_SUCCESS) - << "Failed to get sites."; - std::unordered_set ids; - for (auto* site : sites) { - uint64_t index = 0; - EXPECT_EQ( - QDMI_device_query_site_property(device, site, QDMI_SITE_PROPERTY_INDEX, - sizeof(uint64_t), &index, nullptr), - QDMI_SUCCESS) - << "Devices must provide a site id"; - EXPECT_TRUE(ids.emplace(index).second) - << "Device must provide unique site ids. Found duplicate id: " << index - << "."; - double t1 = 0; - double t2 = 0; - auto result = QDMI_device_query_site_property( - device, site, QDMI_SITE_PROPERTY_T1, sizeof(double), &t1, nullptr); - ASSERT_THAT(result, testing::AnyOf(QDMI_SUCCESS, QDMI_ERROR_NOTSUPPORTED)); - if (result == QDMI_SUCCESS) { - EXPECT_GT(t1, 0) << "Devices must provide a site T1 time larger than 0."; - } - result = QDMI_device_query_site_property( - device, site, QDMI_SITE_PROPERTY_T2, sizeof(double), &t2, nullptr); - ASSERT_THAT(result, testing::AnyOf(QDMI_SUCCESS, QDMI_ERROR_NOTSUPPORTED)); - if (result == QDMI_SUCCESS) { - EXPECT_GT(t2, 0) << "Devices must provide a site T2 time larger than 0."; - } - EXPECT_EQ(QDMI_device_query_site_property( - device, site, QDMI_SITE_PROPERTY_MAX, 0, nullptr, nullptr), - QDMI_ERROR_INVALIDARGUMENT) - << "The MAX property is not a valid value for any device."; - } -} - -TEST_P(DriverTest, QueryOperations) { - size_t operationsSize = 0; - ASSERT_EQ(QDMI_device_query_device_property(device, - QDMI_DEVICE_PROPERTY_OPERATIONS, - 0, nullptr, &operationsSize), - QDMI_SUCCESS) - << "Failed to get the size to retrieve the operations."; - std::vector operations(operationsSize / - sizeof(QDMI_Operation)); - ASSERT_EQ(QDMI_device_query_device_property( - device, QDMI_DEVICE_PROPERTY_OPERATIONS, operationsSize, - static_cast(operations.data()), nullptr), - QDMI_SUCCESS) - << "Failed to retrieve the operations."; - for (auto* const op : operations) { - size_t namesSize = 0; - ASSERT_EQ(QDMI_device_query_operation_property( - device, op, 0, nullptr, 0, nullptr, - QDMI_OPERATION_PROPERTY_NAME, 0, nullptr, &namesSize), - QDMI_SUCCESS) - << "Failed to get the length of the operation's name."; - std::string name(namesSize - 1, '\0'); - ASSERT_EQ( - QDMI_device_query_operation_property(device, op, 0, nullptr, 0, nullptr, - QDMI_OPERATION_PROPERTY_NAME, - namesSize, name.data(), nullptr), - QDMI_SUCCESS) - << "Failed to retrieve the operation's name."; - EXPECT_FALSE(name.empty()) - << "Device must provide a non-empty name for every operation."; - - size_t numParams = 0; - ASSERT_EQ(QDMI_device_query_operation_property( - device, op, 0, nullptr, 0, nullptr, - QDMI_OPERATION_PROPERTY_PARAMETERSNUM, sizeof(size_t), - &numParams, nullptr), - QDMI_SUCCESS) - << "Failed to query number of parameters for operation."; - - double duration = 0; - double fidelity = 0; - std::random_device rd; - std::mt19937 gen(rd()); - std::uniform_real_distribution dis(0.0, 1.0); - std::vector params(numParams); - for (auto& param : params) { - param = dis(gen); - } - auto result = QDMI_device_query_operation_property( - device, op, 0, nullptr, numParams, params.data(), - QDMI_OPERATION_PROPERTY_DURATION, sizeof(double), &duration, nullptr); - ASSERT_THAT(result, testing::AnyOf(QDMI_SUCCESS, QDMI_ERROR_NOTSUPPORTED)) - << "Failed to query duration for operation " << name << "."; - if (result == QDMI_SUCCESS) { - EXPECT_GT(duration, 0) - << "Duration must be larger than 0 for operation " << name << "."; - } - result = QDMI_device_query_operation_property( - device, op, 0, nullptr, numParams, params.data(), - QDMI_OPERATION_PROPERTY_FIDELITY, sizeof(double), &fidelity, nullptr); - ASSERT_THAT(result, testing::AnyOf(QDMI_SUCCESS, QDMI_ERROR_NOTSUPPORTED)) - << "Failed to query fidelity for operation " << name << "."; - if (result == QDMI_SUCCESS) { - EXPECT_THAT(fidelity, testing::IsBetween(0, 1)) - << "Fidelity must be between 0 and 1 for operation " << name << "."; - } - - EXPECT_EQ(QDMI_device_query_operation_property( - device, op, 0, nullptr, 0, nullptr, - QDMI_OPERATION_PROPERTY_MAX, 0, nullptr, nullptr), - QDMI_ERROR_INVALIDARGUMENT) - << "The MAX property is not a valid value for any device."; - } -} - -TEST_P(DriverTest, SessionAlloc) { - EXPECT_EQ(QDMI_session_alloc(nullptr), QDMI_ERROR_INVALIDARGUMENT); -} - -TEST_P(DriverTest, RetrieveJobById) { - QDMI_Job job = nullptr; - EXPECT_EQ(QDMI_session_retrieve_job_by_id(nullptr, "job-id", &job), - QDMI_ERROR_INVALIDARGUMENT); - EXPECT_EQ(QDMI_session_retrieve_job_by_id(device, nullptr, &job), - QDMI_ERROR_INVALIDARGUMENT); - EXPECT_EQ(QDMI_session_retrieve_job_by_id(device, "", &job), - QDMI_ERROR_INVALIDARGUMENT); - EXPECT_EQ(QDMI_session_retrieve_job_by_id(device, "job-id", nullptr), - QDMI_ERROR_INVALIDARGUMENT); - EXPECT_EQ(QDMI_session_retrieve_job_by_id(device, "job-id", &job), - QDMI_ERROR_NOTSUPPORTED); - EXPECT_EQ(job, nullptr); -} - -TEST_P(DriverTest, SessionInit) { - EXPECT_EQ(QDMI_session_init(nullptr), QDMI_ERROR_INVALIDARGUMENT) - << "`session == nullptr` is not a valid argument."; - EXPECT_EQ(QDMI_session_init(session), QDMI_ERROR_BADSTATE) - << "Session must return `BADSTATE` if it is initialized again."; -} - -TEST_P(DriverTest, QuerySessionProperty) { - EXPECT_EQ(QDMI_session_query_session_property( - nullptr, QDMI_SESSION_PROPERTY_DEVICES, 0, nullptr, nullptr), - QDMI_ERROR_INVALIDARGUMENT) - << "`session == nullptr` is not a valid argument."; - EXPECT_EQ(QDMI_session_query_session_property( - session, QDMI_SESSION_PROPERTY_MAX, 0, nullptr, nullptr), - QDMI_ERROR_INVALIDARGUMENT) - << "`prop >= QDMI_SESSION_PROPERTY_MAX` is not a valid argument."; - - // Must not query on an uninitialized session - QDMI_Session uninitializedSession = nullptr; - ASSERT_EQ(QDMI_session_alloc(&uninitializedSession), QDMI_SUCCESS); - EXPECT_EQ(QDMI_session_query_session_property(uninitializedSession, - QDMI_SESSION_PROPERTY_DEVICES, - 0, nullptr, nullptr), - QDMI_ERROR_BADSTATE); - - constexpr size_t size = sizeof(QDMI_Device) - 1; - std::array devices{}; - EXPECT_EQ(QDMI_session_query_session_property( - session, QDMI_SESSION_PROPERTY_DEVICES, size, - static_cast(devices.data()), nullptr), - QDMI_ERROR_INVALIDARGUMENT) - << "Device must return `INVALIDARGUMENT` if the buffer is too small."; -} - -TEST_P(DriverTest, QueryNeedsCalibration) { - size_t needsCalibration = 0; - const auto ret = QDMI_device_query_device_property( - device, QDMI_DEVICE_PROPERTY_NEEDSCALIBRATION, sizeof(size_t), - &needsCalibration, nullptr); - EXPECT_EQ(ret, QDMI_SUCCESS); - EXPECT_THAT(needsCalibration, testing::AnyOf(0, 1)); -} -constexpr std::array DEVICES{"MQT SC Default QDMI Device", - "MQT Core DDSIM QDMI Device"}; - -namespace { -void registerSessionTestDevice() { - static_cast(qdmi::Driver::get().registerDeviceIfAbsent( - {.id = "test.session-overrides", - .library = MQT_CORE_QDMI_SESSION_DEVICE, - .prefix = "TEST_SESSION", - .session = {.baseUrl = "registered-base", - .token = "registered-token", - .custom1 = "registered-custom"}})); -} -} // namespace - -// Instantiate the test suite with different parameters -INSTANTIATE_TEST_SUITE_P( - // Custom instantiation name - DefaultDevices, - // Test suite name - DriverTest, - // Parameters to test with - testing::ValuesIn(DEVICES), - [](const testing::TestParamInfo& paramInfo) { - std::string name = paramInfo.param; - // Replace spaces with underscores for valid test names - std::ranges::replace(name, ' ', '_'); - // Remove parentheses for valid test names - std::erase(name, '('); - std::erase(name, ')'); - return name; - }); - -TEST(ConfiguredDriverTest, ConstructionRegistersWithoutOpeningDevices) { - const auto [library, prefix] = TEST_DEVICE_LIBRARIES.front(); - EXPECT_NO_THROW(qdmi::Driver::get().registerDevice( - {.id = "mqt.sc.default", .library = library, .prefix = prefix}, true)); -} - -TEST(ConfiguredDriverTest, ExposesWorkingDefinitionsAndIsolatesFailures) { - QDMI_Session session = nullptr; - ASSERT_EQ(QDMI_session_alloc(&session), QDMI_SUCCESS); - ASSERT_EQ(QDMI_session_init(session), QDMI_SUCCESS); - - size_t size = 0; - ASSERT_EQ(QDMI_session_query_session_property( - session, QDMI_SESSION_PROPERTY_DEVICES, 0, nullptr, &size), - QDMI_SUCCESS); - ASSERT_EQ(size % sizeof(QDMI_Device), 0); - std::vector devices(size / sizeof(QDMI_Device)); - ASSERT_EQ(QDMI_session_query_session_property( - session, QDMI_SESSION_PROPERTY_DEVICES, size, - static_cast(devices.data()), nullptr), - QDMI_SUCCESS); - - std::vector names; - std::ranges::transform(devices, std::back_inserter(names), queryName); - EXPECT_THAT(names, - testing::UnorderedElementsAre("IQM Emerald", "IQM Garnet", - "MQT SC Default QDMI Device", - "MQT Core DDSIM QDMI Device")); - QDMI_session_free(session); -} - -TEST(DeviceRegistrationTest, ValidatesDuplicatesAndReplacement) { - auto& driver = qdmi::Driver::get(); - EXPECT_THROW(driver.registerDevice({}), std::invalid_argument); - EXPECT_THROW(driver.open("test.unknown"), std::out_of_range); - - const auto [library, prefix] = TEST_DEVICE_LIBRARIES.front(); - const qdmi::DeviceDefinition original{ - .id = "test.replaceable", .library = library, .prefix = prefix}; - driver.registerDevice(original); - EXPECT_THROW(driver.registerDevice(original), std::invalid_argument); - - auto replacement = original; - replacement.session.custom3 = "replacement"; - EXPECT_NO_THROW(driver.registerDevice(replacement, true)); - auto* const opened = driver.open(original.id); - ASSERT_NE(opened, nullptr); - EXPECT_EQ(driver.open(original.id), opened); - EXPECT_THROW(driver.registerDevice(original, true), std::runtime_error); - EXPECT_NO_THROW(driver.registerDevice( - {.id = "test.upserted", .library = library, .prefix = prefix}, true)); - EXPECT_NE(driver.open("test.upserted"), nullptr); -} - -TEST(DeviceRegistrationTest, RegistersOnlyWhenIdIsAbsent) { - auto& driver = qdmi::Driver::get(); - const auto [library, prefix] = TEST_DEVICE_LIBRARIES.front(); - const qdmi::DeviceDefinition definition{ - .id = "test.insert-if-absent", .library = library, .prefix = prefix}; - EXPECT_TRUE(driver.registerDeviceIfAbsent(definition)); - EXPECT_FALSE(driver.registerDeviceIfAbsent(definition)); - - auto invalidDuplicate = definition; - invalidDuplicate.library.clear(); - EXPECT_THROW(static_cast( - driver.registerDeviceIfAbsent(std::move(invalidDuplicate))), - std::invalid_argument); - - const qdmi::DeviceDefinition disabled{ - .id = "test.disabled", .library = library, .prefix = prefix}; - EXPECT_FALSE(driver.registerDeviceIfAbsent(disabled)); - EXPECT_THROW(static_cast(driver.open(disabled.id)), std::runtime_error); - EXPECT_THROW(driver.registerDevice(disabled), std::invalid_argument); -} - -TEST(DeviceRegistrationTest, RegistrationDoesNotLoadLibraries) { - auto& driver = qdmi::Driver::get(); - driver.registerDevice({.id = "test.missing-library", - .library = "/nonexistent/device-library", - .prefix = "MISSING"}); - EXPECT_THROW(static_cast(driver.open("test.missing-library")), - std::runtime_error); -} - -TEST(DeviceRegistrationTest, - EnumeratesEnabledIdsInOrderWithoutLoadingLibraries) { - auto& driver = qdmi::Driver::get(); - const auto idsBefore = driver.registeredDeviceIds(); - driver.registerDevice({.id = "test.enumeration.first", - .library = "/nonexistent/first-device-library", - .prefix = "MISSING_FIRST"}); - driver.registerDevice({.id = "test.enumeration.second", - .library = "/nonexistent/second-device-library", - .prefix = "MISSING_SECOND"}); - - const auto idsAfter = driver.registeredDeviceIds(); - ASSERT_EQ(idsAfter.size(), idsBefore.size() + 2); - EXPECT_TRUE(std::equal(idsBefore.begin(), idsBefore.end(), idsAfter.begin())); - EXPECT_EQ(idsAfter[idsBefore.size()], "test.enumeration.first"); - EXPECT_EQ(idsAfter[idsBefore.size() + 1], "test.enumeration.second"); - EXPECT_THAT(idsAfter, testing::Not(testing::Contains("test.disabled"))); - - EXPECT_THROW(static_cast(driver.open("test.enumeration.first")), - std::runtime_error); -} - -TEST(DeviceRegistrationTest, SynthesizesManifestForMetadataOnlyTarget) { - std::ifstream manifest(MQT_CORE_QDMI_METADATA_MANIFEST); - ASSERT_TRUE(manifest); - const std::string contents{std::istreambuf_iterator(manifest), - std::istreambuf_iterator()}; - EXPECT_THAT(contents, testing::HasSubstr("\"id\": \"test.metadata-only\"")); - EXPECT_THAT(contents, testing::HasSubstr("\"prefix\": \"TEST_METADATA\"")); - EXPECT_THAT(contents, testing::HasSubstr("mqt-core-qdmi-metadata-device")); -} - -TEST(DeviceRegistrationTest, - FreshOverridesMergeValuesOwnTheirSessionAndStayOutOfCatalog) { - registerSessionTestDevice(); - - const auto clientCatalogSize = [] { - QDMI_Session session = nullptr; - if (QDMI_session_alloc(&session) != QDMI_SUCCESS || - QDMI_session_init(session) != QDMI_SUCCESS) { - throw std::runtime_error("Failed to create QDMI test session"); - } - size_t size = 0; - const auto status = QDMI_session_query_session_property( - session, QDMI_SESSION_PROPERTY_DEVICES, 0, nullptr, &size); - QDMI_session_free(session); - if (status != QDMI_SUCCESS) { - throw std::runtime_error("Failed to query QDMI device catalog"); - } - return size; - }; - - const auto catalogSizeBefore = clientCatalogSize(); - { - qdmi::DeviceSessionConfig overrides; - overrides.token = "override-token"; - overrides.custom2 = "override-custom"; - auto device = - qdmi::Session::openDevice("test.session-overrides", overrides); - EXPECT_EQ(device.getName(), - "base=registered-base;token=override-token;custom1=" - "registered-custom;custom2=override-custom;active=1"); - EXPECT_EQ(clientCatalogSize(), catalogSizeBefore); - } - - qdmi::DeviceSessionConfig probeOverrides; - probeOverrides.token = "probe-token"; - const auto probe = - qdmi::Session::openDevice("test.session-overrides", probeOverrides); - EXPECT_THAT(queryName(probe), testing::HasSubstr("active=1")); - EXPECT_EQ(clientCatalogSize(), catalogSizeBefore); -} - -TEST(DeviceRegistrationTest, TypedConfigurationUsesExactlyOneAdapterSlot) { - static_cast(qdmi::Driver::get().registerDeviceIfAbsent( - {.id = "test.typed-configuration", - .library = MQT_CORE_QDMI_SESSION_DEVICE, - .prefix = "TEST_SESSION", - .session = {.deviceConfiguration = qdmi::InlineDeviceConfiguration{ - .json = R"({"name":"inline"})"}}})); - - const auto inlineDevice = - qdmi::Session::openDevice("test.typed-configuration"); - EXPECT_THAT( - inlineDevice.getName(), - testing::HasSubstr(R"(custom1={"name":"inline"};custom2=)")); - - qdmi::DeviceSessionConfig fileOverrides; - fileOverrides.deviceConfiguration = - qdmi::FileDeviceConfiguration{.path = "device.json"}; - const auto fileDevice = - qdmi::Session::openDevice("test.typed-configuration", fileOverrides); - EXPECT_THAT(fileDevice.getName(), - testing::HasSubstr("custom1=;custom2=device.json")); -} - -TEST(DeviceRegistrationTest, TypedConfigurationRejectsRawAdapterSlotConflict) { - auto& driver = qdmi::Driver::get(); - const qdmi::DeviceDefinition definition{ - .id = "test.typed-conflict", - .library = MQT_CORE_QDMI_SESSION_DEVICE, - .prefix = "TEST_SESSION", - .session = {.deviceConfiguration = - qdmi::InlineDeviceConfiguration{.json = "{}"}, - .custom1 = "raw"}}; - EXPECT_THROW(driver.registerDevice(definition), std::invalid_argument); - - registerSessionTestDevice(); - qdmi::DeviceSessionConfig overrides; - overrides.deviceConfiguration = - qdmi::FileDeviceConfiguration{.path = "device.json"}; - overrides.custom2 = "raw"; - EXPECT_THROW(static_cast(qdmi::Session::openDevice( - "test.session-overrides", overrides)), - std::invalid_argument); -} - -TEST(DeviceRegistrationTest, - RuntimeConfigurationSeparatesModelsUsingOneScProviderLibrary) { - auto& driver = qdmi::Driver::get(); - static_cast( - driver.registerDeviceIfAbsent({.id = "test.sc.runtime-default", - .library = MQT_CORE_QDMI_SC_LIBRARY, - .prefix = "MQT_SC"})); - static_cast(driver.registerDeviceIfAbsent( - {.id = "test.sc.runtime-custom", - .library = MQT_CORE_QDMI_SC_LIBRARY, - .prefix = "MQT_SC", - .session = {.deviceConfiguration = qdmi::FileDeviceConfiguration{ - .path = MQT_CORE_QDMI_CUSTOM_SC_FILE}}})); - - const auto defaultDevice = - qdmi::Session::openDevice("test.sc.runtime-default"); - const auto customDevice = qdmi::Session::openDevice("test.sc.runtime-custom"); - EXPECT_EQ(defaultDevice.getName(), "MQT SC Default QDMI Device"); - EXPECT_EQ(customDevice.getName(), "Custom SC Driver Device"); - ASSERT_FALSE(defaultDevice.getOperations().empty()); - ASSERT_FALSE(customDevice.getOperations().empty()); - EXPECT_EQ(defaultDevice.getOperations().front().getDuration(), 20); - EXPECT_EQ(customDevice.getOperations().front().getDuration(), 77); - - qdmi::DeviceSessionConfig overrides; - overrides.deviceConfiguration = - qdmi::FileDeviceConfiguration{.path = MQT_CORE_QDMI_DEFAULT_SC_FILE}; - const auto overridden = - qdmi::Session::openDevice("test.sc.runtime-custom", overrides); - EXPECT_EQ(overridden.getName(), "MQT SC Default QDMI Device"); - EXPECT_EQ(overridden.getOperations().front().getDuration(), 20); - - static_cast(driver.registerDeviceIfAbsent( - {.id = "test.sc.runtime-invalid", - .library = MQT_CORE_QDMI_SC_LIBRARY, - .prefix = "MQT_SC", - .session = {.deviceConfiguration = - qdmi::InlineDeviceConfiguration{.json = "{}"}}})); - EXPECT_THROW( - static_cast(qdmi::Session::openDevice("test.sc.runtime-invalid")), - std::runtime_error); - EXPECT_EQ(qdmi::Session::openDevice("test.sc.runtime-default").getName(), - "MQT SC Default QDMI Device"); -} - -TEST(DeviceRegistrationTest, FreshOpenCreatesDistinctSessions) { - registerSessionTestDevice(); - const auto first = qdmi::Session::openDevice("test.session-overrides"); - const auto second = qdmi::Session::openDevice("test.session-overrides"); - EXPECT_NE(first, second); -} - -TEST(DeviceRegistrationTest, CustomOperationListSupportsRawAndQDMIQueries) { - registerSessionTestDevice(); - const auto device = qdmi::Session::openDevice("test.session-overrides"); - - size_t size = 0; - ASSERT_EQ(QDMI_device_query_device_property( - device, QDMI_DEVICE_PROPERTY_CUSTOM1, 0, nullptr, &size), - QDMI_SUCCESS); - ASSERT_EQ(size, 2 * sizeof(QDMI_Operation)); - std::vector rawOperations(size / sizeof(QDMI_Operation)); - EXPECT_EQ(QDMI_device_query_device_property( - device, QDMI_DEVICE_PROPERTY_CUSTOM1, - size - sizeof(QDMI_Operation), - static_cast(rawOperations.data()), nullptr), - QDMI_ERROR_INVALIDARGUMENT); - ASSERT_EQ(QDMI_device_query_device_property( - device, QDMI_DEVICE_PROPERTY_CUSTOM1, size, - static_cast(rawOperations.data()), nullptr), - QDMI_SUCCESS); - - const auto operations = - device.queryCustomOperations(qdmi::CustomProperty::Custom1); - ASSERT_TRUE(operations.has_value()); - ASSERT_EQ(operations->size(), rawOperations.size()); - EXPECT_EQ(static_cast(operations->at(0)), - rawOperations.at(0)); - EXPECT_EQ(static_cast(operations->at(1)), - rawOperations.at(1)); - EXPECT_EQ(operations->at(0).getName(), "custom-rx"); - EXPECT_EQ(operations->at(0).getQubitsNum(), 1); - EXPECT_EQ(operations->at(0).getParametersNum(), 1); - EXPECT_EQ(operations->at(1).getName(), "custom-cx"); - EXPECT_EQ(operations->at(1).getQubitsNum(), 2); - EXPECT_EQ(operations->at(1).getParametersNum(), 0); -} - -TEST(DeviceRegistrationTest, - CustomOperationListDistinguishesUnsupportedEmptyAndMalformed) { - registerSessionTestDevice(); - const auto device = qdmi::Session::openDevice("test.session-overrides"); - - EXPECT_EQ(device.queryCustomOperations(qdmi::CustomProperty::Custom4), - std::nullopt); - const auto empty = - device.queryCustomOperations(qdmi::CustomProperty::Custom2); - ASSERT_TRUE(empty.has_value()); - EXPECT_TRUE(empty->empty()); - EXPECT_THROW(static_cast( - device.queryCustomOperations(qdmi::CustomProperty::Custom3)), - std::invalid_argument); -} - -TEST(DeviceRegistrationTest, CustomOperationRetainsOwningDeviceSession) { - registerSessionTestDevice(); - const auto operation = [] { - const auto operations = - qdmi::Session::openDevice("test.session-overrides") - .queryCustomOperations(qdmi::CustomProperty::Custom1); - if (!operations.has_value() || operations->empty()) { - throw std::runtime_error("Test provider returned no custom operations"); - } - return operations->front(); - }(); - - EXPECT_EQ(operation.getName(), "custom-rx"); - EXPECT_EQ(operation.getQubitsNum(), 1); -} - -TEST(DeviceRegistrationTest, - ScRuntimeConfigurationSeparatesModelsUsingOneProviderLibrary) { - auto& driver = qdmi::Driver::get(); - static_cast(driver.registerDeviceIfAbsent( - {.id = "test.sc.runtime-one", - .library = MQT_CORE_QDMI_SC_LIBRARY, - .prefix = "MQT_SC", - .session = {.deviceConfiguration = - qdmi::InlineDeviceConfiguration{.json = R"({ - "schema-version":1, - "name":"SC runtime one", - "numQubits":1, - "durationUnit":{"unit":"ns","scaleFactor":1}, - "qubitProperties":{"defaults":{"t1":10,"t2":20},"overrides":[]}, - "couplings":[], - "operations":[{ - "name":"r", - "numParameters":2, - "numQubits":1, - "duration":7, - "fidelity":0.8 - }] - })"}}})); - static_cast(driver.registerDeviceIfAbsent( - {.id = "test.sc.runtime-two", - .library = MQT_CORE_QDMI_SC_LIBRARY, - .prefix = "MQT_SC", - .session = {.deviceConfiguration = - qdmi::InlineDeviceConfiguration{.json = R"({ - "schema-version":1, - "name":"SC runtime two", - "numQubits":2, - "durationUnit":{"unit":"us","scaleFactor":0.5}, - "qubitProperties":{"defaults":{},"overrides":[]}, - "couplings":[[1,0]], - "operations":[] - })"}}})); - - const auto first = qdmi::Session::openDevice("test.sc.runtime-one"); - const auto second = qdmi::Session::openDevice("test.sc.runtime-two"); - EXPECT_EQ(first.getName(), "SC runtime one"); - EXPECT_EQ(first.getQubitsNum(), 1); - const auto firstSites = first.getSites(); - ASSERT_EQ(firstSites.size(), 1); - EXPECT_EQ(firstSites[0].getT1(), 10); - EXPECT_EQ(firstSites[0].getT2(), 20); - const auto firstOperations = first.getOperations(); - ASSERT_EQ(firstOperations.size(), 1); - EXPECT_EQ(firstOperations[0].getName(), "r"); - EXPECT_EQ(firstOperations[0].getDuration({firstSites[0]}), 7); - EXPECT_EQ(firstOperations[0].getFidelity({firstSites[0]}), 0.8); - EXPECT_EQ(second.getName(), "SC runtime two"); - EXPECT_EQ(second.getQubitsNum(), 2); - const auto secondCouplingMap = second.getCouplingMap(); - ASSERT_TRUE(secondCouplingMap.has_value()); - ASSERT_EQ(secondCouplingMap->size(), 1); - - qdmi::DeviceSessionConfig configurationOverride; - configurationOverride.deviceConfiguration = - qdmi::InlineDeviceConfiguration{.json = R"({ - "schema-version":1, - "name":"SC per-open override", - "numQubits":3, - "durationUnit":{"unit":"ms","scaleFactor":2}, - "qubitProperties":{"defaults":{},"overrides":[]}, - "couplings":[[0,2]], - "operations":[] - })"}; - const auto overridden = - qdmi::Session::openDevice("test.sc.runtime-one", configurationOverride); - EXPECT_EQ(overridden.getName(), "SC per-open override"); - EXPECT_EQ(overridden.getQubitsNum(), 3); -} - -TEST(DeviceRegistrationTest, FreshJobRetainsItsDeviceSession) { - registerSessionTestDevice(); - std::optional job; - { - auto device = qdmi::Session::openDevice("test.session-overrides"); - job.emplace( - device.submitJob("OPENQASM 2.0;", QDMI_PROGRAM_FORMAT_QASM2, 1)); - } - - ASSERT_TRUE(job.has_value()); - EXPECT_EQ(job->getId(), "session-job"); - job.reset(); - - const auto probe = qdmi::Session::openDevice("test.session-overrides"); - EXPECT_THAT(queryName(probe), testing::HasSubstr("active=1")); -} - -TEST(DeviceRegistrationTest, RetrievesExistingJobs) { - registerSessionTestDevice(); - const auto device = qdmi::Session::openDevice("test.session-overrides"); - - QDMI_Job job = nullptr; - ASSERT_EQ(QDMI_session_retrieve_job_by_id(device, "session-job", &job), - QDMI_SUCCESS); - ASSERT_NE(job, nullptr); - - std::array id{}; - EXPECT_EQ(QDMI_job_query_property(job, QDMI_JOB_PROPERTY_ID, id.size(), - id.data(), nullptr), - QDMI_SUCCESS); - EXPECT_STREQ(id.data(), "session-job"); - EXPECT_EQ(QDMI_job_set_parameter(job, QDMI_JOB_PARAMETER_SHOTSNUM, - sizeof(size_t), nullptr), - QDMI_ERROR_BADSTATE); - EXPECT_EQ(QDMI_job_submit(job), QDMI_ERROR_BADSTATE); - QDMI_job_free(job); - - job = nullptr; - EXPECT_EQ(QDMI_session_retrieve_job_by_id(device, "missing", &job), - QDMI_ERROR_NOTFOUND); - EXPECT_EQ(job, nullptr); - - const auto retrievedJob = device.retrieveJobById("session-job"); - EXPECT_EQ(retrievedJob.getId(), "session-job"); -} - -TEST(DeviceRegistrationTest, FreshChildDeviceRetainsItsRootSession) { - registerSessionTestDevice(); - std::optional child; - { - qdmi::DeviceSessionConfig overrides; - overrides.custom5 = "with-child"; - auto root = qdmi::Session::openDevice("test.session-overrides", overrides); - auto children = root.getChildDevices(); - ASSERT_EQ(children.size(), 1); - child.emplace(std::move(children.front())); - } - - ASSERT_TRUE(child.has_value()); - EXPECT_EQ(child->getName(), "child;active=2"); - child.reset(); - - const auto probe = qdmi::Session::openDevice("test.session-overrides"); - EXPECT_THAT(queryName(probe), testing::HasSubstr("active=1")); -} - -TEST(DeviceRegistrationTest, RuntimeRegistrationsStayOutOfClientCatalog) { - auto& driver = qdmi::Driver::get(); - QDMI_Session existingSession = nullptr; - ASSERT_EQ(QDMI_session_alloc(&existingSession), QDMI_SUCCESS); - ASSERT_EQ(QDMI_session_init(existingSession), QDMI_SUCCESS); - size_t originalSize = 0; - ASSERT_EQ(QDMI_session_query_session_property(existingSession, - QDMI_SESSION_PROPERTY_DEVICES, - 0, nullptr, &originalSize), - QDMI_SUCCESS); - - const auto [library, prefix] = TEST_DEVICE_LIBRARIES.front(); - driver.registerDevice( - {.id = "test.snapshot", .library = library, .prefix = prefix}); - ASSERT_NE(driver.open("test.snapshot"), nullptr); - - size_t existingSize = 0; - EXPECT_EQ(QDMI_session_query_session_property(existingSession, - QDMI_SESSION_PROPERTY_DEVICES, - 0, nullptr, &existingSize), - QDMI_SUCCESS); - EXPECT_EQ(existingSize, originalSize); - - QDMI_Session newSession = nullptr; - ASSERT_EQ(QDMI_session_alloc(&newSession), QDMI_SUCCESS); - ASSERT_EQ(QDMI_session_init(newSession), QDMI_SUCCESS); - size_t newSize = 0; - EXPECT_EQ(QDMI_session_query_session_property(newSession, - QDMI_SESSION_PROPERTY_DEVICES, - 0, nullptr, &newSize), - QDMI_SUCCESS); - EXPECT_EQ(newSize, originalSize); - QDMI_session_free(newSession); - QDMI_session_free(existingSession); -} - -TEST(DeviceSessionConfigTest, OpenWithBaseUrl) { - qdmi::DeviceSessionConfig config; - config.baseUrl = "http://localhost:8080"; - - for (const auto& [lib, prefix] : TEST_DEVICE_LIBRARIES) { - EXPECT_NO_THROW( - { static_cast(openTestDevice(lib, prefix, config)); }); - } -} - -TEST(DeviceSessionConfigTest, OpenWithCustomParameters) { - qdmi::DeviceSessionConfig config; - config.custom3 = "RESONANCE_COCOS_V1"; - config.custom4 = "test_value"; - config.baseUrl = "http://localhost:9090"; - - for (const auto& [lib, prefix] : TEST_DEVICE_LIBRARIES) { - // Custom parameters may fail with validation errors or succeed/return false - try { - static_cast(openTestDevice(lib, prefix, config)); - SUCCEED() << "Library loaded or already loaded"; - } catch (const std::runtime_error& e) { - // Custom parameters may be rejected with INVALIDARGUMENT - const std::string msg = e.what(); - if (msg.find("CUSTOM") != std::string::npos && - msg.find("Invalid argument") != std::string::npos) { - SUCCEED() << "Custom parameter validation error (expected): " << msg; - } else { - throw; // Re-throw unexpected errors - } - } - } -} - -TEST(DeviceSessionConfigTest, OpenWithAuthToken) { - qdmi::DeviceSessionConfig config; - config.token = "test_token_123"; - config.baseUrl = "https://api.example.com"; - - for (const auto& [lib, prefix] : TEST_DEVICE_LIBRARIES) { - EXPECT_NO_THROW( - { static_cast(openTestDevice(lib, prefix, config)); }); - } -} - -TEST(DeviceSessionConfigTest, OpenWithAuthFile) { - qdmi::DeviceSessionConfig config; - config.authFile = "/nonexistent/auth.json"; - - for (const auto& [lib, prefix] : TEST_DEVICE_LIBRARIES) { - // This should not throw even with non-existent file because - // if the auth file parameter is not supported, it's skipped - EXPECT_NO_THROW( - { static_cast(openTestDevice(lib, prefix, config)); }); - } -} - -TEST(DeviceSessionConfigTest, OpenWithUsernamePassword) { - qdmi::DeviceSessionConfig config; - config.authUrl = "https://auth.example.com"; - config.username = "quantum_user"; - config.password = "secret_password"; - - for (const auto& [lib, prefix] : TEST_DEVICE_LIBRARIES) { - EXPECT_NO_THROW( - { static_cast(openTestDevice(lib, prefix, config)); }); - } -} - -TEST(DeviceSessionConfigTest, - OpenWithAuthenticationAndRemainingCustomParameters) { - qdmi::DeviceSessionConfig config; - config.baseUrl = "http://localhost:8080"; - config.token = "test_token"; - config.authUrl = "https://auth.example.com"; - config.username = "user"; - config.password = "pass"; - config.custom3 = "value3"; - config.custom4 = "value4"; - config.custom5 = "value5"; - - for (const auto& [lib, prefix] : TEST_DEVICE_LIBRARIES) { - try { - static_cast(openTestDevice(lib, prefix, config)); - SUCCEED() << "Library loaded or already loaded"; - } catch (const std::runtime_error& e) { - // Custom parameters may be rejected with INVALIDARGUMENT - const std::string msg = e.what(); - if (msg.find("CUSTOM") != std::string::npos && - msg.find("Invalid argument") != std::string::npos) { - SUCCEED() << "Custom parameter validation error (expected): " << msg; - } else { - throw; // Re-throw unexpected errors - } - } - } -} - -TEST(DeviceSessionConfigTest, IdempotentLoadingWithDifferentConfigs) { - // This test is explicitly not part of the fixture because this would - // automatically load the default config and the respective libraries. - if constexpr (TEST_DEVICE_LIBRARIES.empty()) { - GTEST_SKIP() << "No dynamic device libraries to test"; - } - for (const auto& [lib, prefix] : TEST_DEVICE_LIBRARIES) { - // Config 1: baseUrl - { - qdmi::DeviceSessionConfig config; - config.baseUrl = "http://localhost:8080"; - EXPECT_NO_THROW(static_cast(openTestDevice(lib, prefix, config));); - } - - // Config 2: different baseUrl and custom parameters - { - qdmi::DeviceSessionConfig config; - config.baseUrl = "http://localhost:9090"; - config.custom3 = "API_V2"; - EXPECT_NO_THROW(static_cast(openTestDevice(lib, prefix, config));); - } - - // Config 3: authentication parameters - { - qdmi::DeviceSessionConfig config; - config.token = "new_token"; - config.authUrl = "https://auth.example.com"; - EXPECT_NO_THROW(static_cast(openTestDevice(lib, prefix, config));); - } - } -} - -TEST(DynamicDeviceLibraryTest, ReusesLibraryWithFreshDeviceSessions) { - const auto [library, prefix] = TEST_DEVICE_LIBRARIES.front(); - auto* const first = - openTestDevice(library, prefix, {.custom3 = "first-session"}); - const auto equivalentLibrary = std::filesystem::path(library).parent_path() / - "." / - std::filesystem::path(library).filename(); - auto* const second = openTestDevice(equivalentLibrary.string(), prefix, - {.custom3 = "second-session"}); - - ASSERT_NE(first, second); - EXPECT_EQ(&first->getLibrary(), &second->getLibrary()); -} - -TEST(DynamicDeviceLibraryTest, OpenReturnsDevice) { - if constexpr (TEST_DEVICE_LIBRARIES.empty()) { - GTEST_SKIP() << "No dynamic device libraries configured for testing."; - } - for (const auto& [lib, prefix] : TEST_DEVICE_LIBRARIES) { - const qdmi::DeviceSessionConfig config; - QDMI_Device device = nullptr; - ASSERT_NO_THROW({ device = openTestDevice(lib, prefix, config); }); - ASSERT_NE(device, nullptr) - << "open should return a non-null device pointer"; - - // Verify the device is valid by querying its name - size_t size = 0; - EXPECT_EQ(QDMI_device_query_device_property( - device, QDMI_DEVICE_PROPERTY_NAME, 0, nullptr, &size), - QDMI_SUCCESS); - EXPECT_GT(size, 0) << "Device should have a non-empty name"; - } -} - -INSTANTIATE_TEST_SUITE_P( - // Custom instantiation name - DefaultDevices, - // Test suite name - DriverJobTest, - // Parameters to test with - testing::ValuesIn(DEVICES), - [](const testing::TestParamInfo& paramInfo) { - std::string name = paramInfo.param; - // Replace spaces with underscores for valid test names - std::ranges::replace(name, ' ', '_'); - // Remove parentheses for valid test names - std::erase(name, '('); - std::erase(name, ')'); - return name; - }); -} // namespace qc diff --git a/test/qdmi/manager/CMakeLists.txt b/test/qdmi/manager/CMakeLists.txt new file mode 100644 index 0000000000..959cb11d6b --- /dev/null +++ b/test/qdmi/manager/CMakeLists.txt @@ -0,0 +1,17 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +set(TARGET_NAME mqt-core-qdmi-manager-test) + +if(TARGET MQT::CoreQDMI AND BUILD_MQT_CORE_QDMI_SC_DEVICE) + package_add_test(${TARGET_NAME} MQT::CoreQDMI test_device_manager.cpp) + target_compile_definitions( + ${TARGET_NAME} PRIVATE SC_DEVICE_LIBRARY="$" + SESSION_DEVICE_LIBRARY="$") + add_dependencies(${TARGET_NAME} mqt-core-qdmi-session-device) +endif() diff --git a/test/qdmi/manager/test_device_manager.cpp b/test/qdmi/manager/test_device_manager.cpp new file mode 100644 index 0000000000..46378b1dd8 --- /dev/null +++ b/test/qdmi/manager/test_device_manager.cpp @@ -0,0 +1,242 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "qdmi/Device.hpp" +#include "qdmi/DeviceManager.hpp" +#include "qdmi/DeviceRegistry.hpp" +#include "qdmi/SessionConfig.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +namespace { + +qdmi::DeviceDefinition scDefinition(std::string id = "mqt.sc.test") { + return { + .id = std::move(id), + .library = SC_DEVICE_LIBRARY, + .prefix = "MQT_SC", + }; +} + +qdmi::DeviceManager scManager(std::string id = "mqt.sc.test") { + return qdmi::DeviceManager( + qdmi::DeviceRegistry({scDefinition(std::move(id))})); +} + +qdmi::DeviceDefinition sessionDefinition(std::string id, + std::string custom1 = {}) { + qdmi::DeviceDefinition definition{ + .id = std::move(id), + .library = SESSION_DEVICE_LIBRARY, + .prefix = "TEST_SESSION", + }; + if (!custom1.empty()) { + definition.session.custom1 = std::move(custom1); + } + return definition; +} + +TEST(DeviceRegistry, RegistersReplacesAndProtectsExistingDefinitions) { + qdmi::DeviceRegistry registry({scDefinition("example")}); + EXPECT_THROW(registry.registerDevice(scDefinition("example")), + std::invalid_argument); + EXPECT_FALSE(registry.registerDeviceIfAbsent(scDefinition("example"))); + + auto replacement = scDefinition("example"); + replacement.prefix = "REPLACEMENT"; + registry.registerDevice(std::move(replacement), true); + ASSERT_EQ(registry.definitions().size(), 1); + EXPECT_EQ(registry.definitions().front().prefix, "REPLACEMENT"); + + EXPECT_TRUE(registry.registerDeviceIfAbsent(scDefinition("fallback"))); + ASSERT_EQ(registry.definitions().size(), 2); + EXPECT_EQ(registry.definitions().back().id, "fallback"); +} + +TEST(DeviceRegistry, RejectsIncompleteDefinitions) { + qdmi::DeviceRegistry registry(std::vector{}); + EXPECT_THROW(registry.registerDevice({}), std::invalid_argument); + EXPECT_THROW( + registry.registerDevice({.id = "missing-library", .prefix = "MQT_SC"}), + std::invalid_argument); + EXPECT_THROW(registry.registerDevice( + {.id = "missing-prefix", .library = SC_DEVICE_LIBRARY}), + std::invalid_argument); +} + +TEST(DeviceManager, LazilyOpensAndKeepsDeviceAlive) { + const auto device = scManager().open("mqt.sc.test"); + EXPECT_EQ(device.getName(), "MQT SC Default QDMI Device"); + EXPECT_FALSE(device.getSites().empty()); +} + +TEST(DeviceManager, OpensDefinitionsIndividually) { + qdmi::DeviceRegistry registry({ + scDefinition("good"), + {.id = "bad", .library = "does-not-exist", .prefix = "MISSING"}, + }); + const qdmi::DeviceManager manager(std::move(registry)); + + EXPECT_EQ(manager.open("good").getName(), "MQT SC Default QDMI Device"); + EXPECT_THROW(static_cast(manager.open("bad")), std::runtime_error); + EXPECT_THROW(static_cast(manager.open("missing")), std::out_of_range); +} + +TEST(DeviceManager, OpensAllDefinitionsAndIsolatesFailures) { + qdmi::DeviceRegistry registry({ + scDefinition("good"), + {.id = "bad", .library = "does-not-exist", .prefix = "MISSING"}, + }); + const qdmi::DeviceManager manager(std::move(registry)); + const auto result = manager.openAll(); + + ASSERT_EQ(result.devices.size(), 1); + EXPECT_EQ(result.devices.at("good").getName(), "MQT SC Default QDMI Device"); + ASSERT_EQ(result.errors.size(), 1); + EXPECT_FALSE(result.errors.at("bad").empty()); +} + +TEST(DeviceManager, ConcurrentOpenCallsShareTheLibrarySafely) { + const auto manager = scManager("concurrent"); + std::vector> names; + names.reserve(4); + for (size_t i = 0; i < 4; ++i) { + names.emplace_back(std::async(std::launch::async, [&manager] { + return manager.open("concurrent").getName(); + })); + } + for (auto& name : names) { + EXPECT_EQ(name.get(), "MQT SC Default QDMI Device"); + } +} + +TEST(DeviceManager, SharesLibrariesButCreatesFreshSessions) { + qdmi::DeviceRegistry registry( + {scDefinition("first"), scDefinition("second")}); + const qdmi::DeviceManager manager(std::move(registry)); + const auto first = manager.open("first"); + const auto second = manager.open("second"); + + EXPECT_NE(first, second); + EXPECT_EQ(first.getName(), second.getName()); + EXPECT_EQ(first.getSites().size(), second.getSites().size()); +} + +TEST(DeviceManager, OpenedObjectsOutliveManager) { + const qdmi::Device device = [] { + const auto manager = scManager("persistent"); + return manager.open("persistent"); + }(); + const qdmi::Site site = [&device] { return device.getSites().front(); }(); + + EXPECT_EQ(device.getName(), "MQT SC Default QDMI Device"); + EXPECT_EQ(site.getIndex(), 0); +} + +TEST(DeviceManager, RejectsIncompleteSymbolSet) { + auto definition = scDefinition("wrong-prefix"); + definition.prefix = "MISSING"; + const qdmi::DeviceManager manager( + qdmi::DeviceRegistry({std::move(definition)})); + EXPECT_THROW(static_cast(manager.open("wrong-prefix")), + std::runtime_error); +} + +TEST(DeviceManager, KeepsAnImmutableRegistrySnapshot) { + qdmi::DeviceRegistry registry({sessionDefinition("snapshot.first")}); + const qdmi::DeviceManager manager(registry); + registry.registerDevice(sessionDefinition("snapshot.second")); + + EXPECT_EQ(manager.deviceIds(), std::vector{"snapshot.first"}); + EXPECT_THROW(static_cast(manager.open("snapshot.second")), + std::out_of_range); +} + +TEST(DeviceManager, DefaultManagersSnapshotRuntimeRegistrations) { + const qdmi::DeviceManager before; + ASSERT_TRUE(qdmi::registerDeviceIfAbsent( + sessionDefinition("test.default.snapshot", "registered"))); + const qdmi::DeviceManager after; + + EXPECT_THROW(static_cast(before.open("test.default.snapshot")), + std::out_of_range); + EXPECT_NE( + after.open("test.default.snapshot").getName().find("custom1=registered"), + std::string::npos); +} + +TEST(DeviceManager, ReplacementOnlyChangesFutureDefaultOpens) { + qdmi::registerDevice(sessionDefinition("test.default.replace", "old"), true); + const auto oldDevice = qdmi::openDevice("test.default.replace"); + qdmi::registerDevice(sessionDefinition("test.default.replace", "new"), true); + const auto newDevice = qdmi::openDevice("test.default.replace"); + + EXPECT_NE(oldDevice.getName().find("custom1=old"), std::string::npos); + EXPECT_NE(newDevice.getName().find("custom1=new"), std::string::npos); +} + +TEST(DeviceManager, PreservesTypedConfigurationAndPerOpenOverrides) { + auto definition = sessionDefinition("typed.configuration"); + definition.session.deviceConfiguration = + qdmi::InlineDeviceConfiguration{.json = R"({"model":"default"})"}; + const qdmi::DeviceManager manager( + qdmi::DeviceRegistry({std::move(definition)})); + + const auto configured = manager.open("typed.configuration"); + EXPECT_NE(configured.getName().find(R"(custom1={"model":"default"})"), + std::string::npos); + + qdmi::DeviceSessionConfig override; + override.deviceConfiguration = + qdmi::FileDeviceConfiguration{.path = "override.json"}; + const auto overridden = manager.open("typed.configuration", override); + EXPECT_NE(overridden.getName().find("custom2=override.json"), + std::string::npos); + + override.custom1 = "raw"; + EXPECT_THROW(static_cast(manager.open("typed.configuration", override)), + std::invalid_argument); +} + +TEST(DeviceManager, PreservesCurrentJobAndCustomOperationFeatures) { + const qdmi::DeviceManager manager( + qdmi::DeviceRegistry({sessionDefinition("current.features")})); + const auto device = manager.open("current.features"); + + const auto operations = + device.queryCustomOperations(qdmi::CustomProperty::Custom1); + ASSERT_TRUE(operations.has_value()); + ASSERT_EQ(operations->size(), 2); + EXPECT_EQ(operations->front().getName(), "custom-rx"); + EXPECT_EQ(operations->back().getQubitsNum(), 2); + const auto emptyOperations = + device.queryCustomOperations(qdmi::CustomProperty::Custom2); + ASSERT_TRUE(emptyOperations.has_value()); + EXPECT_TRUE(emptyOperations->empty()); + EXPECT_THROW(static_cast( + device.queryCustomOperations(qdmi::CustomProperty::Custom3)), + std::runtime_error); + EXPECT_FALSE(device.getQueueLength().has_value()); + + const auto job = device.retrieveJobById("session-job"); + EXPECT_EQ(job.getId(), "session-job"); + EXPECT_FALSE(job.getQueuePosition().has_value()); + EXPECT_THROW(static_cast(device.retrieveJobById("missing")), + std::runtime_error); +} + +} // namespace diff --git a/test/qdmi/registry/CMakeLists.txt b/test/qdmi/registry/CMakeLists.txt index 0860295347..8e2c9205cc 100644 --- a/test/qdmi/registry/CMakeLists.txt +++ b/test/qdmi/registry/CMakeLists.txt @@ -8,9 +8,17 @@ set(TARGET_NAME mqt-core-qdmi-registry-test) -if(TARGET MQT::CoreQDMIDriver) - package_add_test(${TARGET_NAME} MQT::CoreQDMIDriver test_device_registry.cpp) - target_include_directories(${TARGET_NAME} PRIVATE ${PROJECT_SOURCE_DIR}/src/qdmi/driver - ${PROJECT_SOURCE_DIR}/test) - mqt_copy_qdmi_runtime(${TARGET_NAME}) +if(TARGET MQT::CoreQDMI) + package_add_test(${TARGET_NAME} MQT::CoreQDMI test_device_registry.cpp) + target_include_directories(${TARGET_NAME} PRIVATE ${PROJECT_SOURCE_DIR}/test) + if(TARGET MQT::CoreQDMI_DDSIM_Device) + target_compile_definitions(${TARGET_NAME} PRIVATE MQT_CORE_QDMI_TEST_HAS_DDSIM) + endif() + if(TARGET MQT::CoreQDMIScDevice) + target_compile_definitions(${TARGET_NAME} PRIVATE MQT_CORE_QDMI_TEST_HAS_SC) + endif() + mqt_get_qdmi_device_targets(QDMI_DEVICE_TARGETS) + if(QDMI_DEVICE_TARGETS) + mqt_copy_qdmi_runtime(${TARGET_NAME} ${QDMI_DEVICE_TARGETS}) + endif() endif() diff --git a/test/qdmi/registry/test_device_registry.cpp b/test/qdmi/registry/test_device_registry.cpp index 81fcdee107..12b81a07d6 100644 --- a/test/qdmi/registry/test_device_registry.cpp +++ b/test/qdmi/registry/test_device_registry.cpp @@ -8,9 +8,9 @@ * Licensed under the MIT License */ -#include "DeviceRegistry.hpp" +#include "qdmi/DeviceRegistry.hpp" +#include "qdmi/SessionConfig.hpp" #include "qdmi/TestUtils.hpp" -#include "qdmi/driver/Driver.hpp" #include @@ -73,7 +73,7 @@ class ScopedCurrentPath { std::filesystem::path previous_; }; -[[nodiscard]] auto findDefinition(const qdmi::detail::DeviceRegistry& registry, +[[nodiscard]] auto findDefinition(const qdmi::DeviceRegistry& registry, const std::string_view id) -> const qdmi::DeviceDefinition* { const auto& definitions = registry.definitions(); @@ -101,7 +101,7 @@ TEST(DeviceRegistry, ParsesEnvironmentConfigurationWithoutLoadingLibraries) { }]} })"); - const qdmi::detail::DeviceRegistry registry; + const qdmi::DeviceRegistry registry; const auto* definition = findDefinition(registry, "example.device"); ASSERT_NE(definition, nullptr); EXPECT_EQ(std::filesystem::weakly_canonical(definition->library), @@ -125,7 +125,7 @@ TEST(DeviceRegistry, RejectsDuplicateIdsAndUnsupportedKeys) { {"id": "duplicate", "library": "two", "prefix": "TWO"} ]} })"); - EXPECT_THROW(static_cast(qdmi::detail::DeviceRegistry()), + EXPECT_THROW(static_cast(qdmi::DeviceRegistry()), std::invalid_argument); } { @@ -133,7 +133,7 @@ TEST(DeviceRegistry, RejectsDuplicateIdsAndUnsupportedKeys) { "schema-version": 1, "qdmi": {"device-config": {"model": "unused"}} })"); - EXPECT_THROW(static_cast(qdmi::detail::DeviceRegistry()), + EXPECT_THROW(static_cast(qdmi::DeviceRegistry()), std::invalid_argument); } } @@ -156,7 +156,7 @@ TEST(DeviceRegistry, MergesEnvironmentJsonOverExplicitFile) { }]} })"); - const qdmi::detail::DeviceRegistry registry; + const qdmi::DeviceRegistry registry; const auto* definition = findDefinition(registry, "environment"); ASSERT_NE(definition, nullptr); EXPECT_EQ(definition->library, directory.path() / "file.so"); @@ -187,7 +187,7 @@ TEST(DeviceRegistry, DeviceConfigurationSourceReplacesAtomically) { }]} })"); - const qdmi::detail::DeviceRegistry registry; + const qdmi::DeviceRegistry registry; const auto* definition = findDefinition(registry, "environment"); ASSERT_NE(definition, nullptr); ASSERT_TRUE(definition->session.deviceConfiguration); @@ -212,7 +212,7 @@ TEST(DeviceRegistry, SerializesInlineDeviceConfigurationCompactly) { }]} })"); - const qdmi::detail::DeviceRegistry registry; + const qdmi::DeviceRegistry registry; const auto* definition = findDefinition(registry, "inline"); ASSERT_NE(definition, nullptr); ASSERT_TRUE(definition->session.deviceConfiguration); @@ -241,7 +241,7 @@ TEST(DeviceRegistry, RejectsInvalidDeviceConfigurationSources) { std::string(source) + "}}]}}"; const ScopedEnvironmentVariable configJson("MQT_CORE_QDMI_CONFIG_JSON", json); - EXPECT_THROW(static_cast(qdmi::detail::DeviceRegistry()), + EXPECT_THROW(static_cast(qdmi::DeviceRegistry()), std::invalid_argument); } } @@ -261,10 +261,10 @@ TEST(DeviceRegistry, DisabledEnvironmentEntryMasksExplicitDefinition) { "qdmi": {"devices": [{"id": "masked", "enabled": false}]} })"); - const qdmi::detail::DeviceRegistry registry; + qdmi::DeviceRegistry registry; EXPECT_EQ(findDefinition(registry, "masked"), nullptr); - ASSERT_EQ(registry.disabledIds().size(), 1); - EXPECT_EQ(registry.disabledIds().front(), "masked"); + EXPECT_FALSE(registry.registerDeviceIfAbsent( + {.id = "masked", .library = "fallback.so", .prefix = "FALLBACK"})); } TEST(DeviceRegistry, HigherPrecedenceDefinitionMustExplicitlyReenableDevice) { @@ -283,10 +283,10 @@ TEST(DeviceRegistry, HigherPrecedenceDefinitionMustExplicitlyReenableDevice) { "id": "masked", "library": "device.so", "prefix": "DEVICE" }]} })"); - const qdmi::detail::DeviceRegistry registry; + qdmi::DeviceRegistry registry; EXPECT_EQ(findDefinition(registry, "masked"), nullptr); - ASSERT_EQ(registry.disabledIds().size(), 1); - EXPECT_EQ(registry.disabledIds().front(), "masked"); + EXPECT_FALSE(registry.registerDeviceIfAbsent( + {.id = "masked", .library = "fallback.so", .prefix = "FALLBACK"})); } { @@ -297,13 +297,12 @@ TEST(DeviceRegistry, HigherPrecedenceDefinitionMustExplicitlyReenableDevice) { "enabled": true }]} })"); - const qdmi::detail::DeviceRegistry registry; + const qdmi::DeviceRegistry registry; const auto* definition = findDefinition(registry, "masked"); ASSERT_NE(definition, nullptr); EXPECT_EQ(definition->library, std::filesystem::current_path() / "device.so"); EXPECT_EQ(definition->prefix, "DEVICE"); - EXPECT_TRUE(registry.disabledIds().empty()); } } @@ -324,7 +323,7 @@ TEST(DeviceRegistry, ResolvesRelativeConfigurationPathsBeforeCwdChanges) { const ScopedEnvironmentVariable configFile("MQT_CORE_QDMI_CONFIG_FILE", "config/device.json"); const ScopedEnvironmentVariable configJson("MQT_CORE_QDMI_CONFIG_JSON", ""); - const qdmi::detail::DeviceRegistry registry; + const qdmi::DeviceRegistry registry; const auto* definition = findDefinition(registry, "relative"); ASSERT_NE(definition, nullptr); library = definition->library; @@ -347,16 +346,22 @@ TEST(DeviceRegistry, DiscoversGeneratedBuildTreeManifests) { const auto configFile = emptyConfig(directory); const ScopedEnvironmentVariable configJson("MQT_CORE_QDMI_CONFIG_JSON", ""); - const qdmi::detail::DeviceRegistry registry; - ASSERT_EQ(registry.definitions().size(), 4); - EXPECT_EQ(registry.definitions().at(0).id, "mqt.ddsim.default"); - EXPECT_EQ(registry.definitions().at(1).id, "mqt.sc.default"); - EXPECT_EQ(registry.definitions().at(2).id, "mqt.sc.iqm.emerald"); - EXPECT_EQ(registry.definitions().at(3).id, "mqt.sc.iqm.garnet"); + const qdmi::DeviceRegistry registry; + std::vector expectedIds; +#ifdef MQT_CORE_QDMI_TEST_HAS_DDSIM + expectedIds.emplace_back("mqt.ddsim.default"); +#endif +#ifdef MQT_CORE_QDMI_TEST_HAS_SC + expectedIds.emplace_back("mqt.sc.default"); + expectedIds.emplace_back("mqt.sc.iqm.emerald"); + expectedIds.emplace_back("mqt.sc.iqm.garnet"); +#endif + EXPECT_EQ(registry.deviceIds(), expectedIds); for (const auto& definition : registry.definitions()) { EXPECT_TRUE(std::filesystem::is_regular_file(definition.library)); } +#ifdef MQT_CORE_QDMI_TEST_HAS_SC const auto assertPackagedModel = [&](const std::string_view id, const std::string_view filename) { const auto* definition = findDefinition(registry, id); @@ -371,6 +376,7 @@ TEST(DeviceRegistry, DiscoversGeneratedBuildTreeManifests) { }; assertPackagedModel("mqt.sc.iqm.garnet", "iqm-garnet.json"); assertPackagedModel("mqt.sc.iqm.emerald", "iqm-emerald.json"); +#endif } TEST(DeviceRegistry, ReadsProjectConfigurationFromNearestQdmiJson) { @@ -385,7 +391,7 @@ TEST(DeviceRegistry, ReadsProjectConfigurationFromNearestQdmiJson) { const ScopedEnvironmentVariable configFile("MQT_CORE_QDMI_CONFIG_FILE", ""); const ScopedEnvironmentVariable configJson("MQT_CORE_QDMI_CONFIG_JSON", ""); - const qdmi::detail::DeviceRegistry registry; + const qdmi::DeviceRegistry registry; const auto* definition = findDefinition(registry, "json"); ASSERT_NE(definition, nullptr); EXPECT_EQ(std::filesystem::weakly_canonical(definition->library), @@ -418,7 +424,7 @@ TEST(DeviceRegistry, MergesProjectConfigurationOverUserConfiguration) { "XDG_CONFIG_HOME", (directory.path() / "user").string()); #endif - const qdmi::detail::DeviceRegistry registry; + const qdmi::DeviceRegistry registry; const auto* definition = findDefinition(registry, "layered"); ASSERT_NE(definition, nullptr); EXPECT_EQ(definition->library, @@ -443,7 +449,7 @@ TEST(DeviceRegistry, ReportsInvalidDocumentsAndDefinitionTypes) { }) { const ScopedEnvironmentVariable configJson("MQT_CORE_QDMI_CONFIG_JSON", document); - EXPECT_THROW(static_cast(qdmi::detail::DeviceRegistry()), + EXPECT_THROW(static_cast(qdmi::DeviceRegistry()), std::invalid_argument); } } @@ -454,14 +460,13 @@ TEST(DeviceRegistry, ReportsInvalidExplicitJson) { const ScopedEnvironmentVariable configFile( "MQT_CORE_QDMI_CONFIG_FILE", (directory.path() / "missing.json").string()); - EXPECT_THROW(static_cast(qdmi::detail::DeviceRegistry()), - std::runtime_error); + EXPECT_THROW(static_cast(qdmi::DeviceRegistry()), std::runtime_error); } { const auto invalid = directory.write("invalid.json", "{"); const ScopedEnvironmentVariable configFile("MQT_CORE_QDMI_CONFIG_FILE", invalid.string()); - EXPECT_THROW(static_cast(qdmi::detail::DeviceRegistry()), + EXPECT_THROW(static_cast(qdmi::DeviceRegistry()), std::invalid_argument); } } diff --git a/test/qdmi/test_slurm.cpp b/test/qdmi/test_slurm.cpp index f4753ddc62..a543a0d26b 100644 --- a/test/qdmi/test_slurm.cpp +++ b/test/qdmi/test_slurm.cpp @@ -8,8 +8,8 @@ * Licensed under the MIT License */ +#include "qdmi/DeviceRegistry.hpp" #include "qdmi/Slurm.hpp" -#include "qdmi/driver/Driver.hpp" #include #include @@ -72,11 +72,11 @@ class ScopedSlurmLicenses { void registerStatusDevice(const std::string& id, const std::string& configuredStatus) { - static_cast(qdmi::Driver::get().registerDeviceIfAbsent( - {.id = id, - .library = MQT_CORE_QDMI_SLURM_TEST_DEVICE, - .prefix = "TEST_SESSION", - .session = {.custom4 = configuredStatus}})); + static_cast( + qdmi::registerDeviceIfAbsent({.id = id, + .library = MQT_CORE_QDMI_SLURM_TEST_DEVICE, + .prefix = "TEST_SESSION", + .session = {.custom4 = configuredStatus}})); } } // namespace