diff --git a/.agent/plans/qdmi-integration-redesign.md b/.agent/plans/qdmi-integration-redesign.md new file mode 100644 index 0000000000..10377322e6 --- /dev/null +++ b/.agent/plans/qdmi-integration-redesign.md @@ -0,0 +1,197 @@ +# Unify QDMI device management + +This ExecPlan is a living document. Keep `Progress`, `Surprises & Discoveries`, +`Decision Log`, and `Outcomes & Retrospective` current as the work changes. + +This plan follows `.agent/PLANS.md`. The configurable QDMI device foundation is +already part of `main`; this change replaces its overlapping runtime layers with +one public object model. + +## Purpose / Big Picture + +Applications should manage QDMI devices through one sequence: + + configuration -> DeviceRegistry -> DeviceManager -> Device + |-> Site / Operation + `-> Job / child Device + +`DeviceRegistry` discovers and combines device definitions without executing +device code. `DeviceManager` owns a registry snapshot and opens a fresh session +for a selected stable ID. Every returned object retains the library and session +state required by its QDMI handle. + +This makes device discovery inspectable, isolates failures between devices, and +gives C++, Python, Qiskit, and the neutral-atom adapter the same lifecycle +model. The prior FoMaC and client-driver public APIs are removed as part of the +MQT Core v4 transition. + +## Progress + +- [x] (2026-07-15) Added the public `Device`, `Site`, `Operation`, and `Job` + objects using QDMI's enum types and typed custom-property queries. +- [x] (2026-07-15) Added lazy opening, per-ID bulk-open results, child devices, + and lifetime-safe derived objects. +- [x] (2026-07-15) Migrated the Python bindings, Qiskit integration, and + neutral-atom adapter to `mqt.core.qdmi`. +- [x] (2026-07-27) Consolidated registration in `DeviceRegistry` and made + `DeviceManager` an immutable snapshot. +- [x] (2026-07-27) Preserved `qdmi.json`, `[tool.qdmi]`, manifest discovery, + disabled-ID masking, target metadata, and path-like Python arguments. +- [x] (2026-07-27) Serialized replacement library generations and covered + initialization, finalization, cross-session handles, and object lifetimes. +- [x] (2026-07-27) Passed the focused native, Python, documentation, stub, and + lint checks on the configuration branch. +- [x] (2026-07-30) Rebased only the redesign and documentation commits onto + current `main`, which already contains the configurable-device and + mandatory LLVM/MLIR changes. +- [x] (2026-07-30) Preserved the binary-safe submission and retrieval contract + added to `main` after the original branch, including C++, Python, and + stubs. +- [x] (2026-07-30) Passed the complete native build and 3,882 CTest cases, the + Python 3.14 suite, stub generation, documentation, and full lint. +- [x] (2026-07-30) Integrated the optional bundled-device controls from #1965; a + clean build with all bundled devices disabled retains and passes the 12 + device-independent registry tests. + +## Surprises & Discoveries + +- Replaying the original work beside the configurable-device implementation + produced two parsers, two registries, and two stable-ID opening paths. The + useful boundary is one mutable `DeviceRegistry` followed by an immutable + `DeviceManager`. +- A QDMI library may permit only one live initialization while callers still + need independent device sessions. A process-wide weak `DeviceApi` cache shares + compatible live libraries without keeping them loaded indefinitely. +- Child devices, jobs, sites, and operations can outlive their manager or parent + wrapper. Keeping the internal session state in the object graph makes those + handles safe without a separate public session object. +- Current `main` builds LLVM/MLIR and QIR support unconditionally. The redesign + must preserve the MLIR binding and validate with LLVM/MLIR 22 available. +- The post-branch binary-program work initially disappeared with FoMaC. A + focused compile against the tests from `main` exposed the missing byte + overload, which now belongs directly to the unified QDMI object model. +- Optional bundled devices require test dependencies to follow capabilities: + registry tests run without devices, manager tests require only the + superconducting device, and object-model tests require all three built-ins. + +## Decision Log + +- Decision: `DeviceRegistry` is the only mutable discovery and fallback + registration boundary. Rationale: packages can supply a device definition + without mixing configuration mutation into runtime management. Date/Author: + 2026-07-27, implementation review. +- Decision: `DeviceManager` owns an immutable registry snapshot. Rationale: + opening sessions does not require singleton state or a second registration + API. Date/Author: 2026-07-27, implementation review. +- Decision: disabled IDs remain reserved. Rationale: fallback registration must + not undo an explicit higher-precedence disable. Date/Author: 2026-07-27, + configuration integration review. +- Decision: cache `DeviceApi` by canonical library path and symbol prefix using + weak ownership. Rationale: compatible sessions share one live initialization, + and the library unloads after its last object is gone. Replacement waits for + the prior generation to finish finalization. Date/Author: 2026-07-27, + lifecycle review. +- Decision: retain runtime state directly in the device object graph. Rationale: + public session ownership adds another layer but does not improve handle + safety. Date/Author: 2026-07-27, API review. + +## Context and Orientation + +The public C++ interfaces are: + +- `include/mqt-core/qdmi/DeviceRegistry.hpp` +- `include/mqt-core/qdmi/DeviceManager.hpp` +- `include/mqt-core/qdmi/Device.hpp` + +The implementation is in `src/qdmi/`. Private `DeviceApi` owns the dynamic +library and exact QDMI function pointers; private `DeviceState` owns one device +session. Python bindings and stubs are in `bindings/qdmi/qdmi.cpp` and +`python/mqt/core/qdmi.pyi`. + +Configuration remains in `DeviceRegistry.cpp` and `docs/qdmi/configuration.md`. +Qiskit integration is under `python/mqt/core/plugins/qiskit/`; the neutral-atom +adapter is under `src/na/qdmi/`. + +## Plan of Work + +1. Move configuration definitions and registration into the public QDMI object + model while retaining all discovery and precedence behavior from `main`. +2. Open each stable ID through `DeviceManager`, overlaying per-open session + parameters and isolating bulk-open failures by ID. +3. Keep the loaded library and session alive through the returned object graph; + reject cross-session handles before invoking device code. +4. Bind the model directly in Python, migrate Qiskit and neutral-atom callers, + and remove the superseded FoMaC and client-driver layers. +5. Update migration and API documentation, regenerate stubs, and validate the + complete branch. + +## Concrete Steps + +From the repository root, with `MLIR_DIR` pointing to LLVM/MLIR 22: + + ./.agent/run.sh cmake --preset release + ./.agent/run.sh cmake --build --preset release --target \ + mqt-core-qdmi-object-model-test \ + mqt-core-qdmi-manager-test \ + mqt-core-qdmi-registry-test \ + mqt-core-na-qdmi-test + ./.agent/run.sh ctest --test-dir build/release --output-on-failure + +Then validate generated and user-facing surfaces: + + ./.agent/run.sh uvx nox -s stubs + ./.agent/run.sh uvx nox -s tests-3.14 + ./.agent/run.sh uvx nox -s docs + ./.agent/run.sh uvx nox -s lint + git diff --check + +## Validation and Acceptance + +Acceptance requires: + +- registry construction does not initialize device code; +- configuration discovery, explicit definitions, fallback registration, and + disabled-ID masking behave as documented; +- every open creates a fresh session while compatible live sessions share one + library initialization; +- bulk opening isolates failures by stable ID; +- devices and derived objects remain valid after their manager is destroyed; +- Qiskit and neutral-atom integrations use `mqt.core.qdmi`; +- generated stubs match the bindings; and +- native and Python tests, documentation, lint, and `git diff --check` pass, or + any environmental limitation is recorded. + +## Idempotence and Recovery + +Configuration and build commands are repeatable. Build outputs remain under +`build/` and agent caches under `.cache/`; neither is committed. Re-run CMake +after build-system changes. Regenerate stubs from the bindings rather than +editing generated signatures by hand. + +## Outcomes & Retrospective + +The reconstructed branch contains only the v4 device-management redesign on top +of current `main`; the already-merged configuration foundation is no longer +duplicated in its history or diff. The complete release build with LLVM/MLIR 22 +passes all 3,882 CTest cases; two device job-ID cases are intentionally skipped +by their test fixtures. The Python 3.14 suite passes 397 tests with three +upstream-Qiskit skips. Stub generation, warning-as-error documentation, full +lint, and `git diff --check` also pass. A separate configuration with all three +bundled QDMI devices disabled builds and passes the 12 remaining registry tests. + +## Artifacts and Interfaces + +The principal interfaces are: + + qdmi::DeviceRegistry() + qdmi::DeviceRegistry(std::vector) + qdmi::DeviceRegistry::registerDevice(definition, replace) + qdmi::DeviceRegistry::registerDeviceIfAbsent(definition) + qdmi::DeviceManager() + qdmi::DeviceManager(qdmi::DeviceRegistry) + qdmi::DeviceManager::open(id, sessionOverrides) + qdmi::DeviceManager::openAll(sessionOverrides) + +Python exposes the corresponding `DeviceDefinition`, `DeviceRegistry`, +`DeviceManager`, `OpenAllResult`, `SessionParameters`, `Device`, and `Job` +classes from `mqt.core.qdmi`. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc30d211f2..a7ffcb6369 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -167,9 +167,9 @@ jobs: setup-python: true install-pkgs: "nanobind==2.13.0" cpp-linter-extra-args: "-std=c++20" - # The vendored toml++ header is checked upstream and is not maintained - # according to MQT Core's clang-tidy configuration. - cpp-linter-ignore-extra: "vendor/**" + # Private headers have no standalone compile command; their including + # translation units are still checked by clang-tidy. + cpp-linter-ignore-extra: "vendor/**|src/qdmi/DeviceApi.h|src/qdmi/DeviceState.h" setup-mlir: true llvm-version: 22.1.7 diff --git a/CHANGELOG.md b/CHANGELOG.md index b3dc1c4e08..49ca18bd05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,14 @@ releases may include breaking changes. [#1950]) ([**@simon1hofmann**]) - ✨ Add Python bindings for the MQT Compiler Collection ([#1815]) ([**@burgholzer**], [**@denialhaag**]) +- ✨ Add a public `qdmi::DeviceRegistry` and lazy + `qdmi::DeviceManager`/`mqt.core.qdmi` object model with per-device sessions, + failure isolation, and lifetime-safe device objects ([#1901]) + ([**@burgholzer**]) +- ✨ Add ID-keyed `DeviceManager::openAll`/`DeviceManager.open_all` results for + independently opening every configured device ([#1901]) ([**@burgholzer**]) +- ✨ Support keyword-only construction of Python `SessionParameters`, including + path-like authentication files ([#1901]) ([**@burgholzer**]) - ✨ Add support for QDMI child devices to the driver and FoMaC libraries ([#1897], [#1952]) ([**@burgholzer**]) - ✨ Add typed custom property and result queries to the C++ and Python FoMaC @@ -93,6 +101,24 @@ releases may include breaking changes. - 💥 Require LLVM/MLIR and QIR support in every MQT Core build and remove the corresponding build options ([#1953]) ([**@burgholzer**]) +- ♻️ Replace the existing device-management layering with the + `qdmi::DeviceRegistry` → `qdmi::DeviceManager` → `qdmi::Device` object model. + Device libraries now load lazily, sessions are configured per device, child + objects retain their required runtime state, and definitions can be inspected + without executing device code ([#1901]) ([**@burgholzer**]) +- ♻️ Load device libraries through one private `DeviceApi` that owns the library + and stores the exact QDMI function pointer types. The public C++ API uses + QDMI's existing device-status, job-status, and program-format enums directly + instead of redefining them, while client handles remain private ([#1901]) + ([**@burgholzer**]) +- 📝 Add binding-local docstrings for the complete public Python QDMI API + ([#1901]) ([**@burgholzer**]) +- ♻️ Use `device_id` for the Python `DeviceDefinition` property and constructor + argument, avoiding collisions with Python's built-in `id` while retaining `id` + in configuration and C++ ([#1901]) ([**@burgholzer**]) +- ♻️ Migrate the Qiskit provider and neutral-atom adapter to lazily opened + configured QDMI devices and the unified `mqt.core.qdmi` API ([#1901]) + ([**@burgholzer**]) - ⬆️ Raise the minimum supported QDMI version to 1.3.2 ([#1897]) ([**@burgholzer**]) - ⬆️ Require LLVM 22.1 for C++ library builds ([#1549]) ([**@burgholzer**], @@ -102,6 +128,11 @@ releases may include breaking changes. ### Removed +- 🔥 Remove the former device-management namespace, Python module, global + session API, source/include/test trees, and compatibility CMake targets + ([#1901]) ([**@burgholzer**]) +- 🔥 Remove the QDMI client-interface implementation and the `Driver` singleton + ([#1901]) ([**@burgholzer**]) - 🔥 Replace the unstable C++ `Driver::addDynamicDeviceLibrary` and Python `add_dynamic_device_library` APIs with definition registration and stable-ID opening ([#1912]) ([**@burgholzer**]) @@ -115,6 +146,8 @@ releases may include breaking changes. - 🐛 Allow MQT Core to be embedded as a CMake subproject without target collisions and make its bundled QDMI devices individually configurable ([#1965]) ([**@burgholzer**]) +- 🐛 Reuse live QDMI device libraries across device managers and reject + operation sites from another device session ([#1901]) ([**@burgholzer**]) - 🐛 Fix QIR function names for adjoint gates ([#1830]) ([**@denialhaag**]) ## [3.7.0] - 2026-07-09 @@ -690,6 +723,7 @@ changelogs._ [#1912]: https://github.com/munich-quantum-toolkit/core/pull/1912 [#1911]: https://github.com/munich-quantum-toolkit/core/pull/1911 [#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 597afd33a7..1b2e087399 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -166,7 +166,7 @@ if(BUILD_MQT_CORE_DOCUMENTATION) COMMENT "Copying and cleaning up generated MLIR documentation" VERBATIM) endif() - foreach(binding ir dd fomac na) + foreach(binding ir dd qdmi na) add_dependencies(${MQT_CORE_TARGET_NAME}-${binding}-bindings mqt-core-docs) endforeach() endif() @@ -181,8 +181,8 @@ if(BUILD_MQT_CORE_BINDINGS) mqt-core-na mqt-core-ir-bindings mqt-core-dd-bindings - mqt-core-fomac-bindings mqt-core-mlir-bindings + mqt-core-qdmi-bindings mqt-core-na-bindings) if(BUILD_MQT_CORE_QDMI_DDSIM_DEVICE) list(APPEND MQT_CORE_WHEEL_TARGETS mqt-core-qdmi-ddsim-device) diff --git a/UPGRADING.md b/UPGRADING.md index 312f306e7d..51fb0eddf4 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -14,7 +14,247 @@ The bundled QDMI devices now have individual CMake options: standalone MQT Core build. They default to disabled when MQT Core is consumed through CMake's `FetchContent` or `add_subdirectory`; embedded consumers can enable only the devices they need before making MQT Core available. The QDMI -driver and FoMaC libraries remain available independently. +object-model library remains available independently. + +### QDMI device management has been redesigned + +MQT Core v4 replaces the former driver, QDMI client-interface, and compatibility +layers with one object model: + +```text +configuration -> DeviceRegistry -> DeviceManager -> Device + |-> Site / Operation + `-> Job / child Device +``` + +Discovery is side-effect free. A device library is loaded and a session is +initialized only when its stable device ID is passed to `open`. The QDMI +implementation is held by one private library/symbol object; public MQT objects +do not expose client handles. The supported QDMI version is selected centrally +in `cmake/ExternalDependencies.cmake`; incomplete device implementations fail +when the selected device is opened. + +| Concern | C++ | Python | +| --------------------------- | -------------------------------- | ---------------------------------------- | +| Discover registrations | `qdmi::DeviceRegistry` | `qdmi.DeviceManager.definitions` | +| Open one device | `qdmi::DeviceManager::open` | `DeviceManager.open` | +| Open every definition | `qdmi::DeviceManager::openAll` | `DeviceManager.open_all` | +| Device capabilities | `qdmi::Device` | `qdmi.Device` | +| Sites, operations, and jobs | `qdmi::Site`, `Operation`, `Job` | `Device.Site`, `Device.Operation`, `Job` | +| Neutral-atom view | `na::qdmi::Device` | `mqt.core.na.qdmi.Device` | +| CMake target | `MQT::CoreQDMI` | not applicable | + +There is intentionally no compatibility module in v4. The following names and +entry points have been removed: + +- `mqt.core.fomac`, `fomac::*`, their compatibility CMake target, and the + neutral-atom wrappers; +- the `Driver` singleton and `addDynamicDeviceLibrary`; +- MQT Core's QDMI client C functions and public client handles; +- global authentication/session configuration. + +Replace old includes with `qdmi/Device.hpp` or `qdmi/DeviceManager.hpp`, and +link `MQT::CoreQDMI`. + +The C++ object model uses QDMI's own enum types rather than defining parallel +MQT enums. Pass and compare the QDMI constants directly: + +```cpp +device.submitJob(program, QDMI_PROGRAM_FORMAT_QASM3, shots); +if (job.check() == QDMI_JOB_STATUS_DONE) { + // ... +} +``` + +`Device::getStatus()`, `Job::check()`, and the program-format methods therefore +return `QDMI_Device_Status`, `QDMI_Job_Status`, and `QDMI_Program_Format` +respectively. Python retains the convenient `Device.Status`, `Job.Status`, and +`ProgramFormat` names, but those bindings represent the QDMI enums themselves. + +Text programs are submitted as `std::string`/`str` and include QDMI's required +terminating null byte. Binary formats use `std::span`/`bytes` +and preserve the payload exactly. Use `Job::getProgramBytes()` or +`Job.program_bytes` when retrieving binary data. + +#### Before and after + +Python code that used the removed compatibility module: + +```python +# MQT Core 3.x +from mqt.core import fomac + +session = fomac.Session() +device = session.get_devices()[0] +``` + +should select a configured stable ID: + +```python +# MQT Core 4.x +from mqt.core import qdmi + +manager = qdmi.DeviceManager() +device = manager.open("mqt.ddsim.default") +``` + +The equivalent C++ migration is: + +```cpp +// MQT Core 3.x +#include "fomac/FoMaC.hpp" +fomac::Session session; +const auto device = session.getDevices().front(); +``` + +```cpp +// MQT Core 4.x +#include "qdmi/DeviceManager.hpp" +qdmi::DeviceManager manager; +auto device = manager.open("mqt.ddsim.default"); +``` + +#### Opening devices + +Code that needs every configured device can use the bulk-open result. Each +definition is opened independently, and errors remain associated with stable +device IDs. + +Python: + +```python +from mqt.core import qdmi + +result = qdmi.DeviceManager().open_all() +for device_id, device in result.devices.items(): + print(device.name()) +for device_id, error in result.errors.items(): + print(f"{device_id} could not be opened: {error}") +``` + +C++: + +```cpp +#include "qdmi/DeviceManager.hpp" + +const auto result = qdmi::DeviceManager().openAll(); +for (const auto& [id, device] : result.devices) { + // Use the opened device. +} +for (const auto& [id, error] : result.errors) { + // Report or otherwise handle this device failure. +} +``` + +Use `definitions` and `open` instead when selection must happen before any +device library is loaded. In Python the stable identifier property is named +`device_id`; configuration files and C++ continue to use `id`. + +#### Per-device session parameters + +Authentication and device settings no longer belong to a process-wide session. +Put defaults on each device definition and override them for one `open` call: + +```python +from mqt.core import qdmi + +parameters = qdmi.SessionParameters( + token=obtain_token(), + custom1="device-specific-value", +) + +device = qdmi.DeviceManager().open( + "vendor.qpu.production", + session_overrides=parameters, +) +``` + +```cpp +qdmi::SessionParameters parameters; +parameters.token = obtainToken(); +parameters.custom1 = "device-specific-value"; + +auto device = manager.open("vendor.qpu.production", parameters); +``` + +QDMI has no standard project-ID session parameter. Device-specific project or +organization identifiers must use the custom slot documented by that device. MQT +Core no longer accepts a `project_id` value that it cannot forward. + +Multiple definitions may refer to the same shared library while using +independent session parameters. Open devices, child devices, sites, operations, +and jobs share the required internal state, so these objects remain valid after +the manager that opened them is destroyed. + +#### Registering devices with configuration + +Device registration is versioned and keyed by a stable, unique `id`: + +```json +{ + "schema-version": 1, + "qdmi": { + "devices": [ + { + "id": "vendor.qpu.production", + "library": "./libvendor-qdmi-device.so", + "prefix": "VENDOR", + "enabled": true, + "session": { + "base-url": "https://qpu.example", + "auth-file": "./credentials.json" + } + } + ] + } +} +``` + +Relative paths are resolved against the file containing them. Configuration +layers are merged field by field by `id`, and `enabled = false` masks a +lower-precedence definition. Duplicate IDs within one source, unknown keys, +invalid types, and incomplete enabled definitions are errors with source and +configuration-path diagnostics. + +The precedence order, from lowest to highest, is: + +1. packaged manifest fragments; +2. system `qdmi.json`; +3. user or XDG `qdmi.json`; +4. the nearest project `qdmi.json` or `[tool.qdmi]` table in `pyproject.toml`; +5. `MQT_CORE_QDMI_CONFIG_JSON`; + +A dedicated `qdmi.json` wins over `pyproject.toml` in the same directory. +`MQT_CORE_QDMI_CONFIG_FILE` replaces the system, user, and project layers while +retaining packaged devices. See the +{doc}`configuration reference ` for complete schemas and +administrator, project, environment, and runtime examples. + +Static C++ executables have no portable module location from which built-in +manifests can be discovered. Place manifest fragments beside the executable, set +`MQT_CORE_QDMI_CONFIG_FILE`, or construct a `DeviceRegistry` from explicit +definitions. + +#### QDMI child devices + +`qdmi::Device::getChildDevices()` and `Device.child_devices()` return direct +child devices as ordinary device objects. Each child owns the device session and +library state required by its QDMI handle; retaining a child is therefore safe +even after discarding its parent or manager. Devices without child-device +support return an empty list. + +#### Qiskit integration + +`QDMIProvider` now uses `DeviceManager` internally and creates a backend for +every successfully opened, convertible device. Existing code that only creates +`QDMIProvider()` does not need to manage device objects itself. Authentication +keyword arguments are converted to per-open `SessionParameters`. + +Code that directly constructs a `QDMIBackend` should pass a +`mqt.core.qdmi.Device` returned by `DeviceManager.open`. Tests and downstream +integrations that mocked global device enumeration should instead construct an +explicit `DeviceRegistry` or mock `DeviceManager.definitions` and +`DeviceManager.open`. ### LLVM/MLIR required for all source builds @@ -46,43 +286,6 @@ Known limitations: - AppleClang 17+ is required to build MQT Core due to some C++20 features that are not yet properly supported by older versions. -### QDMI runtime device registration - -The unstable runtime-loading helpers have been replaced with registration by a -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 - -definition = DeviceDefinition("my.device", library_path, prefix, base_url="https://device.example") -register_device(definition) -device = open_device("my.device") -``` - -Per-backend session values can be passed directly to -`open_device("my.device", base_url=..., token=...)`. Every call creates a fresh -device session without registering another device ID. Repeated integration setup -can use `register_device_if_absent(definition)` instead of suppressing -duplicate-ID errors; invalid definitions are still rejected, and a device -disabled by higher-precedence configuration remains reserved. - -The equivalent C++ flow is: - -```cpp -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"); -``` - -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. - ### Removal of the density matrix support from the DD package The density matrix support within the DD package has been removed. This change @@ -108,14 +311,6 @@ and [VS Code](https://code.visualstudio.com/docs/devcontainers/containers) can open the repository directly inside the container. If you are on Windows, we recommend using Docker Desktop with the WSL 2 backend. -### QDMI child devices - -The QDMI driver now translates device-library-specific `QDMI_Child_Device` -handles into client-facing `QDMI_Device` handles backed by dedicated child -sessions. Direct child devices can be queried through -`fomac::Device::getChildDevices()` in C++ and `Device.child_devices()` in -Python. Devices without child-device support continue to behave unchanged. - ## [3.7.0] The shared library ABI version (`SOVERSION`) is increased from `3.6` to `3.7`. diff --git a/bindings/CMakeLists.txt b/bindings/CMakeLists.txt index d9678f5ca4..715e995d25 100644 --- a/bindings/CMakeLists.txt +++ b/bindings/CMakeLists.txt @@ -8,6 +8,6 @@ add_subdirectory(ir) add_subdirectory(dd) -add_subdirectory(fomac) +add_subdirectory(qdmi) add_subdirectory(na) add_subdirectory(mlir) diff --git a/bindings/fomac/fomac.cpp b/bindings/fomac/fomac.cpp deleted file mode 100644 index 3606920292..0000000000 --- a/bindings/fomac/fomac.cpp +++ /dev/null @@ -1,784 +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 "fomac/FoMaC.hpp" - -#include "qdmi/driver/Driver.hpp" - -#include -#include -#include // NOLINT(misc-include-cleaner) -#include // NOLINT(misc-include-cleaner) -#include // NOLINT(misc-include-cleaner) -#include // NOLINT(misc-include-cleaner) -#include // NOLINT(misc-include-cleaner) -#include // NOLINT(misc-include-cleaner) -#include // NOLINT(misc-include-cleaner) -#include // NOLINT(misc-include-cleaner) -#include - -#include -#include -#include -#include -#include -#include -#include - -namespace mqt { - -namespace nb = nanobind; -using namespace nb::literals; - -namespace { -template -[[nodiscard]] nb::object queryCustomValue(Query query, - const nb::handle valueType) { - const auto returnValue = - [](std::optional value) -> nb::object { - if (!value.has_value()) { - return nb::none(); - } - return nb::cast(std::move(*value)); - }; - - const auto builtins = nb::builtins(); - if (valueType.is(builtins["str"])) { - return returnValue(query.template operator()()); - } - if (valueType.is(builtins["bool"])) { - return returnValue(query.template operator()()); - } - if (valueType.is(builtins["int"])) { - return returnValue(query.template operator()()); - } - if (valueType.is(builtins["float"])) { - return returnValue(query.template operator()()); - } - if (valueType.is(builtins["bytes"])) { - const auto value = query.template operator()>(); - if (!value.has_value()) { - return nb::none(); - } - return nb::bytes(reinterpret_cast(value->data()), - value->size()); - } - throw nb::type_error( - "value_type must be exactly str, bool, int, float, or bytes"); -} - -[[nodiscard]] auto makeDeviceSessionConfig( - std::optional baseUrl, std::optional token, - std::optional authFile, - std::optional authUrl, std::optional username, - std::optional password, std::optional custom1, - std::optional custom2, std::optional custom3, - std::optional custom4, std::optional custom5) - -> qdmi::DeviceSessionConfig { - 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), - .custom1 = std::move(custom1), - .custom2 = std::move(custom2), - .custom3 = std::move(custom3), - .custom4 = std::move(custom4), - .custom5 = std::move(custom5)}; -} - -} // namespace - -NB_MODULE(MQT_CORE_MODULE_NAME, m) { - // Session class - auto session = nb::class_( - m, "Session", R"pb(A FoMaC session for managing QDMI devices. - -Allows creating isolated sessions with separate authentication settings. -All authentication parameters are optional and can be provided as keyword arguments to the constructor.)pb"); - - session.def( - "__init__", - [](fomac::Session* self, std::optional token, - std::optional authFile, - std::optional authUrl, - std::optional username, - std::optional password, - std::optional projectId, - std::optional custom1, std::optional custom2, - std::optional custom3, std::optional custom4, - std::optional custom5) { - const fomac::SessionConfig config{.token = std::move(token), - .authFile = std::move(authFile), - .authUrl = std::move(authUrl), - .username = std::move(username), - .password = std::move(password), - .projectId = std::move(projectId), - .custom1 = std::move(custom1), - .custom2 = std::move(custom2), - .custom3 = std::move(custom3), - .custom4 = std::move(custom4), - .custom5 = std::move(custom5)}; - new (self) fomac::Session(config); - }, - nb::kw_only(), "token"_a = std::nullopt, "auth_file"_a = std::nullopt, - "auth_url"_a = std::nullopt, "username"_a = std::nullopt, - "password"_a = std::nullopt, "project_id"_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(Create a new FoMaC session with optional authentication. - -Args: - token: Authentication token - auth_file: Path to file containing authentication information - auth_url: URL to authentication server - username: Username for authentication - password: Password for authentication - project_id: Project ID for session - custom1: Custom configuration parameter 1 - custom2: Custom configuration parameter 2 - custom3: Custom configuration parameter 3 - custom4: Custom configuration parameter 4 - custom5: Custom configuration parameter 5 - -Raises: - RuntimeError: If auth_file does not exist - RuntimeError: If auth_url has invalid format - -Example: - >>> from mqt.core.fomac import Session - >>> # Session without authentication - >>> session = Session() - >>> devices = session.get_devices() - >>> - >>> # Session with token authentication - >>> session = Session(token="my_secret_token") - >>> devices = session.get_devices() - >>> - >>> # Session with file-based authentication - >>> session = Session(auth_file="/path/to/auth.json") - >>> devices = session.get_devices() - >>> - >>> # Session with multiple parameters - >>> session = Session( - ... auth_url="https://auth.example.com", username="user", password="pass", project_id="project-123" - ... ) - >>> devices = session.get_devices())pb"); - - session.def("get_devices", &fomac::Session::getDevices, - nb::rv_policy::reference_internal, - R"pb(Get available devices from this session. - -Returns: - List of available devices.)pb"); - - // Job class - auto job = nb::class_( - m, "Job", "A job represents a submitted quantum program execution."); - - job.def("check", &fomac::Job::check, - "Returns the current status of the job."); - - job.def("wait", &fomac::Job::wait, "timeout"_a = 0, - R"pb(Waits for the job to complete. - -Args: - timeout: The maximum time to wait in seconds. If 0, waits indefinitely. - -Returns: - True if the job completed within the timeout, False otherwise.)pb"); - - job.def("cancel", &fomac::Job::cancel, "Cancels the job."); - - job.def("get_shots", &fomac::Job::getShots, - "Returns the raw shot results from the job."); - - job.def("get_counts", &fomac::Job::getCounts, - "Returns the measurement counts from the job."); - - job.def("get_dense_statevector", &fomac::Job::getDenseStateVector, - "Returns the dense statevector from the job (typically only " - "available from simulator devices)."); - - job.def("get_dense_probabilities", &fomac::Job::getDenseProbabilities, - "Returns the dense probabilities from the job (typically only " - "available from simulator devices)."); - - job.def("get_sparse_statevector", &fomac::Job::getSparseStateVector, - "Returns the sparse statevector from the job (typically only " - "available from simulator devices)."); - - job.def("get_sparse_probabilities", &fomac::Job::getSparseProbabilities, - "Returns the sparse probabilities from the job (typically only " - "available from simulator devices)."); - - job.def( - "query_custom_property", - [](const fomac::Job& self, const fomac::CustomProperty customProperty, - const nb::handle valueType) { - return queryCustomValue( - [&self, customProperty]() { - return self.queryCustomProperty(customProperty); - }, - valueType); - }, - "custom_property"_a, "value_type"_a, - nb::sig("def query_custom_property(self, custom_property: " - "CustomProperty, " - "value_type: type[str] | type[bool] | type[int] | type[float] | " - "type[bytes]) -> str | bool | int | float | bytes | None"), - R"pb(Query an implementation-defined custom job property. - -The caller must provide the type documented by the device implementation. -Use ``bytes`` to retrieve the value without interpretation. Returns ``None`` -when the custom slot is unsupported.)pb"); - - job.def( - "get_custom_result", - [](const fomac::Job& self, const fomac::CustomProperty customProperty, - const nb::handle valueType) { - return queryCustomValue( - [&self, customProperty]() { - return self.getCustomResult(customProperty); - }, - valueType); - }, - "custom_property"_a, "value_type"_a, - nb::sig("def get_custom_result(self, custom_property: CustomProperty, " - "value_type: type[str] | type[bool] | type[int] | type[float] | " - "type[bytes]) -> str | bool | int | float | bytes | None"), - R"pb(Return an implementation-defined custom job result. - -The caller must provide the type documented by the device implementation. -Use ``bytes`` to retrieve the value without interpretation. Returns ``None`` -when the custom slot is unsupported.)pb"); - - job.def_prop_ro("id", &fomac::Job::getId, "The job ID."); - - job.def_prop_ro("program_format", &fomac::Job::getProgramFormat, - "The format of the submitted program."); - - job.def_prop_ro("program", &fomac::Job::getProgram, "The submitted program."); - - job.def_prop_ro( - "program_bytes", - [](const fomac::Job& self) { - const auto program = self.getProgramBytes(); - return nb::bytes(program.data(), program.size()); - }, - "The exact bytes of the submitted program."); - - job.def_prop_ro("num_shots", &fomac::Job::getNumShots, - "The number of shots."); - - job.def(nb::self == nb::self, - nb::sig("def __eq__(self, arg: object, /) -> bool")); - job.def(nb::self != nb::self, - nb::sig("def __ne__(self, arg: object, /) -> bool")); - - // JobStatus enum - nb::enum_(job, "Status", "Enumeration of job status.") - .value("CREATED", QDMI_JOB_STATUS_CREATED) - .value("SUBMITTED", QDMI_JOB_STATUS_SUBMITTED) - .value("QUEUED", QDMI_JOB_STATUS_QUEUED) - .value("RUNNING", QDMI_JOB_STATUS_RUNNING) - .value("DONE", QDMI_JOB_STATUS_DONE) - .value("CANCELED", QDMI_JOB_STATUS_CANCELED) - .value("FAILED", QDMI_JOB_STATUS_FAILED); - - // ProgramFormat enum - nb::enum_(m, "ProgramFormat", - "Enumeration of program formats.") - .value("QASM2", QDMI_PROGRAM_FORMAT_QASM2) - .value("QASM3", QDMI_PROGRAM_FORMAT_QASM3) - .value("QIR_BASE_STRING", QDMI_PROGRAM_FORMAT_QIRBASESTRING) - .value("QIR_BASE_MODULE", QDMI_PROGRAM_FORMAT_QIRBASEMODULE) - .value("QIR_ADAPTIVE_STRING", QDMI_PROGRAM_FORMAT_QIRADAPTIVESTRING) - .value("QIR_ADAPTIVE_MODULE", QDMI_PROGRAM_FORMAT_QIRADAPTIVEMODULE) - .value("CALIBRATION", QDMI_PROGRAM_FORMAT_CALIBRATION) - .value("QPY", QDMI_PROGRAM_FORMAT_QPY) - .value("IQM_JSON", QDMI_PROGRAM_FORMAT_IQMJSON) - .value("BATCH_JOB", QDMI_PROGRAM_FORMAT_BATCHJOB) - .value("CUSTOM1", QDMI_PROGRAM_FORMAT_CUSTOM1) - .value("CUSTOM2", QDMI_PROGRAM_FORMAT_CUSTOM2) - .value("CUSTOM3", QDMI_PROGRAM_FORMAT_CUSTOM3) - .value("CUSTOM4", QDMI_PROGRAM_FORMAT_CUSTOM4) - .value("CUSTOM5", QDMI_PROGRAM_FORMAT_CUSTOM5); - - nb::enum_( - m, "CustomProperty", - "An implementation-defined custom property or result slot.") - .value("CUSTOM1", fomac::CustomProperty::Custom1) - .value("CUSTOM2", fomac::CustomProperty::Custom2) - .value("CUSTOM3", fomac::CustomProperty::Custom3) - .value("CUSTOM4", fomac::CustomProperty::Custom4) - .value("CUSTOM5", fomac::CustomProperty::Custom5); - - // Device class - auto device = nb::class_( - m, "Device", - "A device represents a quantum device with its properties and " - "capabilities."); - - nb::enum_(device, "Status", - "Enumeration of device status.") - .value("OFFLINE", QDMI_DEVICE_STATUS_OFFLINE) - .value("IDLE", QDMI_DEVICE_STATUS_IDLE) - .value("BUSY", QDMI_DEVICE_STATUS_BUSY) - .value("ERROR", QDMI_DEVICE_STATUS_ERROR) - .value("MAINTENANCE", QDMI_DEVICE_STATUS_MAINTENANCE) - .value("CALIBRATION", QDMI_DEVICE_STATUS_CALIBRATION); - - device.def("name", &fomac::Device::getName, - "Returns the name of the device."); - - device.def("version", &fomac::Device::getVersion, - "Returns the version of the device."); - - device.def("status", &fomac::Device::getStatus, - "Returns the current status of the device."); - - device.def("library_version", &fomac::Device::getLibraryVersion, - "Returns the version of the library used to define the device."); - - device.def("qubits_num", &fomac::Device::getQubitsNum, - "Returns the number of qubits available on the device."); - - device.def("sites", &fomac::Device::getSites, - "Returns the list of all sites (zone and regular sites) available " - "on the device."); - - device.def("regular_sites", &fomac::Device::getRegularSites, - "Returns the list of regular sites (without zone sites) available " - "on the device."); - - device.def("zones", &fomac::Device::getZones, - "Returns the list of zone sites (without regular sites) available " - "on the device."); - - device.def("operations", &fomac::Device::getOperations, - "Returns the list of operations supported by the device."); - - device.def("coupling_map", &fomac::Device::getCouplingMap, - "Returns the coupling map of the device as a list of site pairs."); - - device.def("needs_calibration", &fomac::Device::getNeedsCalibration, - "Returns whether the device needs calibration."); - - device.def("length_unit", &fomac::Device::getLengthUnit, - "Returns the unit of length used by the device."); - - device.def("length_scale_factor", &fomac::Device::getLengthScaleFactor, - "Returns the scale factor for length used by the device."); - - device.def("duration_unit", &fomac::Device::getDurationUnit, - "Returns the unit of duration used by the device."); - - device.def("duration_scale_factor", &fomac::Device::getDurationScaleFactor, - "Returns the scale factor for duration used by the device."); - - device.def("min_atom_distance", &fomac::Device::getMinAtomDistance, - "Returns the minimum atom distance on the device."); - - device.def("supported_program_formats", - &fomac::Device::getSupportedProgramFormats, - "Returns the list of program formats supported by the device."); - - device.def("child_devices", &fomac::Device::getChildDevices, - "Returns the direct child devices managed by this device."); - - device.def( - "query_custom_property", - [](const fomac::Device& self, const fomac::CustomProperty customProperty, - const nb::handle valueType) { - return queryCustomValue( - [&self, customProperty]() { - return self.queryCustomProperty(customProperty); - }, - valueType); - }, - "custom_property"_a, "value_type"_a, - nb::sig("def query_custom_property(self, custom_property: " - "CustomProperty, " - "value_type: type[str] | type[bool] | type[int] | type[float] | " - "type[bytes]) -> str | bool | int | float | bytes | None"), - R"pb(Query an implementation-defined custom device property. - -The caller must provide the type documented by the device implementation. -Use ``bytes`` to retrieve the value without interpretation. Returns ``None`` -when the custom slot is unsupported.)pb"); - - device.def( - "submit_job", - [](const fomac::Device& self, 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) { - return self.submitJob(program, format, numShots, custom1, custom2, - custom3, custom4, custom5); - }, - "program"_a, "program_format"_a, "num_shots"_a, nb::kw_only(), - "custom1"_a = nb::none(), "custom2"_a = nb::none(), - "custom3"_a = nb::none(), "custom4"_a = nb::none(), - "custom5"_a = nb::none(), nb::rv_policy::reference_internal, - "Submits a text job to the device."); - - device.def( - "submit_job", - [](const fomac::Device& self, const nb::bytes& 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 auto bytes = std::span{ - static_cast(program.data()), program.size()}; - return self.submitJob(bytes, format, numShots, custom1, custom2, - custom3, custom4, custom5); - }, - "program"_a, "program_format"_a, "num_shots"_a, nb::kw_only(), - "custom1"_a = nb::none(), "custom2"_a = nb::none(), - "custom3"_a = nb::none(), "custom4"_a = nb::none(), - "custom5"_a = nb::none(), nb::rv_policy::reference_internal, - "Submits an exact byte payload to the device."); - - device.def("__repr__", [](const fomac::Device& dev) { - return ""; - }); - - device.def(nb::self == nb::self, - nb::sig("def __eq__(self, arg: object, /) -> bool")); - device.def(nb::self != nb::self, - nb::sig("def __ne__(self, arg: object, /) -> bool")); - - // Site class - auto site = nb::class_( - device, "Site", - "A site represents a potential qubit location on a quantum device."); - - site.def("index", &fomac::Site::getIndex, "Returns the index of the site."); - - site.def("t1", &fomac::Site::getT1, - "Returns the T1 coherence time of the site."); - - site.def("t2", &fomac::Site::getT2, - "Returns the T2 coherence time of the site."); - - site.def("name", &fomac::Site::getName, "Returns the name of the site."); - - site.def("x_coordinate", &fomac::Site::getXCoordinate, - "Returns the x coordinate of the site."); - - site.def("y_coordinate", &fomac::Site::getYCoordinate, - "Returns the y coordinate of the site."); - - site.def("z_coordinate", &fomac::Site::getZCoordinate, - "Returns the z coordinate of the site."); - - site.def("is_zone", &fomac::Site::isZone, - "Returns whether the site is a zone."); - - site.def("x_extent", &fomac::Site::getXExtent, - "Returns the x extent of the site."); - - site.def("y_extent", &fomac::Site::getYExtent, - "Returns the y extent of the site."); - - site.def("z_extent", &fomac::Site::getZExtent, - "Returns the z extent of the site."); - - site.def("module_index", &fomac::Site::getModuleIndex, - "Returns the index of the module the site belongs to."); - - site.def("submodule_index", &fomac::Site::getSubmoduleIndex, - "Returns the index of the submodule the site belongs to."); - - site.def( - "query_custom_property", - [](const fomac::Site& self, const fomac::CustomProperty customProperty, - const nb::handle valueType) { - return queryCustomValue( - [&self, customProperty]() { - return self.queryCustomProperty(customProperty); - }, - valueType); - }, - "custom_property"_a, "value_type"_a, - nb::sig("def query_custom_property(self, custom_property: " - "CustomProperty, " - "value_type: type[str] | type[bool] | type[int] | type[float] | " - "type[bytes]) -> str | bool | int | float | bytes | None"), - R"pb(Query an implementation-defined custom site property. - -The caller must provide the type documented by the device implementation. -Use ``bytes`` to retrieve the value without interpretation. Returns ``None`` -when the custom slot is unsupported.)pb"); - - site.def("__repr__", [](const fomac::Site& s) { - return ""; - }); - - site.def(nb::self == nb::self, - nb::sig("def __eq__(self, arg: object, /) -> bool")); - site.def(nb::self != nb::self, - nb::sig("def __ne__(self, arg: object, /) -> bool")); - - // Operation class - auto operation = nb::class_( - device, "Operation", - "An operation represents a quantum operation that can be performed on a " - "quantum device."); - - operation.def("name", &fomac::Operation::getName, - "sites"_a.sig("...") = std::vector{}, - "params"_a.sig("...") = std::vector{}, - "Returns the name of the operation."); - - operation.def("qubits_num", &fomac::Operation::getQubitsNum, - "sites"_a.sig("...") = std::vector{}, - "params"_a.sig("...") = std::vector{}, - "Returns the number of qubits the operation acts on."); - - operation.def("parameters_num", &fomac::Operation::getParametersNum, - "sites"_a.sig("...") = std::vector{}, - "params"_a.sig("...") = std::vector{}, - "Returns the number of parameters the operation has."); - - operation.def("duration", &fomac::Operation::getDuration, - "sites"_a.sig("...") = std::vector{}, - "params"_a.sig("...") = std::vector{}, - "Returns the duration of the operation."); - - operation.def("fidelity", &fomac::Operation::getFidelity, - "sites"_a.sig("...") = std::vector{}, - "params"_a.sig("...") = std::vector{}, - "Returns the fidelity of the operation."); - - operation.def("interaction_radius", &fomac::Operation::getInteractionRadius, - "sites"_a.sig("...") = std::vector{}, - "params"_a.sig("...") = std::vector{}, - "Returns the interaction radius of the operation."); - - operation.def("blocking_radius", &fomac::Operation::getBlockingRadius, - "sites"_a.sig("...") = std::vector{}, - "params"_a.sig("...") = std::vector{}, - "Returns the blocking radius of the operation."); - - operation.def("idling_fidelity", &fomac::Operation::getIdlingFidelity, - "sites"_a.sig("...") = std::vector{}, - "params"_a.sig("...") = std::vector{}, - "Returns the idling fidelity of the operation."); - - operation.def("is_zoned", &fomac::Operation::isZoned, - "Returns whether the operation is zoned."); - - operation.def("sites", &fomac::Operation::getSites, - "Returns the list of sites the operation can be performed on."); - - operation.def("site_pairs", &fomac::Operation::getSitePairs, - "Returns the list of site pairs the local 2-qubit operation " - "can be performed on."); - - operation.def("mean_shuttling_speed", - &fomac::Operation::getMeanShuttlingSpeed, - "sites"_a.sig("...") = std::vector{}, - "params"_a.sig("...") = std::vector{}, - "Returns the mean shuttling speed of the operation."); - - operation.def( - "query_custom_property", - [](const fomac::Operation& self, - const fomac::CustomProperty customProperty, const nb::handle valueType, - const std::vector& sites, - const std::vector& params) { - return queryCustomValue( - [&self, customProperty, &sites, - ¶ms]() { - return self.queryCustomProperty(customProperty, sites, params); - }, - valueType); - }, - "custom_property"_a, "value_type"_a, - "sites"_a.sig("...") = std::vector{}, - "params"_a.sig("...") = std::vector{}, - nb::sig("def query_custom_property(self, custom_property: " - "CustomProperty, " - "value_type: type[str] | type[bool] | type[int] | type[float] | " - "type[bytes], sites: Sequence[mqt.core.fomac.Device.Site] = " - "..., params: Sequence[float] = ...) -> str | bool | int | " - "float | bytes | None"), - R"pb(Query an implementation-defined custom operation property. - -The caller must provide the type documented by the device implementation. -Use ``bytes`` to retrieve the value without interpretation. Returns ``None`` -when the custom slot is unsupported.)pb"); - - operation.def("__repr__", [](const fomac::Operation& op) { - return ""; - }); - - operation.def(nb::self == nb::self, - nb::sig("def __eq__(self, arg: object, /) -> bool")); - operation.def(nb::self != nb::self, - nb::sig("def __ne__(self, arg: object, /) -> bool")); - - nb::class_( - m, "DeviceDefinition", - R"pb(A stable QDMI device registration that can be stored before loading.)pb") - .def( - "__init__", - [](qdmi::DeviceDefinition* self, std::string deviceId, - std::filesystem::path libraryPath, std::string prefix, - const std::optional& baseUrl = std::nullopt, - const std::optional& token = std::nullopt, - const std::optional& authFile = - std::nullopt, - const std::optional& authUrl = std::nullopt, - const std::optional& username = std::nullopt, - const std::optional& password = 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) { - new (self) qdmi::DeviceDefinition{ - .id = std::move(deviceId), - .library = std::move(libraryPath), - .prefix = std::move(prefix), - .session = makeDeviceSessionConfig( - baseUrl, token, authFile, authUrl, username, password, - custom1, custom2, custom3, custom4, custom5)}; - }, - "device_id"_a, "library_path"_a, "prefix"_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, - "custom1"_a = std::nullopt, "custom2"_a = std::nullopt, - "custom3"_a = std::nullopt, "custom4"_a = std::nullopt, - "custom5"_a = std::nullopt, - R"pb(Create a device definition without loading its native library. - -Args: - device_id: Stable identifier used by :func:`open_device`. - library_path: Path to the shared QDMI device library. - prefix: Function prefix used by the library (for example, ``MY_DEVICE``). - base_url: Optional base URL for the device API endpoint. - token: Optional authentication token. - auth_file: Optional path to an authentication file. - auth_url: Optional authentication server URL. - username: Optional authentication username. - password: Optional authentication password. - custom1: Optional custom configuration parameter 1. - custom2: Optional custom configuration parameter 2. - custom3: Optional custom configuration parameter 3. - custom4: Optional custom configuration parameter 4. - custom5: Optional custom configuration parameter 5.)pb") - .def_ro("device_id", &qdmi::DeviceDefinition::id, - R"pb(Stable identifier used to open the device.)pb") - .def_ro("library_path", &qdmi::DeviceDefinition::library, - R"pb(Path to the native QDMI device library.)pb") - .def_ro("prefix", &qdmi::DeviceDefinition::prefix, - R"pb(Prefix used for the QDMI device interface functions.)pb"); - - m.def( - "register_device", - [](qdmi::DeviceDefinition definition, const bool replace) { - qdmi::Driver::get().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. - -Raises: - ValueError: If the definition is invalid or its ID is already registered. - RuntimeError: If replacing an already opened ID.)pb"); - - m.def( - "register_device_if_absent", - [](qdmi::DeviceDefinition definition) { - return qdmi::Driver::get().registerDeviceIfAbsent( - std::move(definition)); - }, - "definition"_a, - R"pb(Register a valid QDMI device definition if its ID is absent. - -Existing and explicitly disabled IDs are not inserted. Invalid definitions -still raise. - -Args: - definition: Definition to validate and store. - -Returns: - bool: Whether the definition was inserted. - -Raises: - ValueError: If the definition is invalid.)pb"); - - m.def( - "open_device", - [](const std::string& deviceId, std::optional baseUrl, - std::optional token, - std::optional authFile, - std::optional authUrl, - std::optional username, - std::optional password, - std::optional custom1, std::optional custom2, - std::optional custom3, std::optional custom4, - std::optional custom5) { - const auto overrides = makeDeviceSessionConfig( - std::move(baseUrl), std::move(token), std::move(authFile), - std::move(authUrl), std::move(username), std::move(password), - std::move(custom1), std::move(custom2), std::move(custom3), - std::move(custom4), std::move(custom5)); - return fomac::Session::openDevice(deviceId, overrides); - }, - "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, "custom1"_a = std::nullopt, - "custom2"_a = std::nullopt, "custom3"_a = std::nullopt, - "custom4"_a = std::nullopt, "custom5"_a = std::nullopt, - R"pb(Open a registered QDMI device by stable ID. - -Every call creates a fresh device session while keeping the stable registration -unchanged. Opening the device loads trusted native device code. - -Args: - device_id: Stable ID of a registered device. - base_url: Optional base URL override for the device API endpoint. - token: Optional authentication token override. - auth_file: Optional authentication-file override. - auth_url: Optional authentication server URL override. - username: Optional authentication username override. - password: Optional authentication password override. - custom1: Optional custom configuration parameter 1 override. - custom2: Optional custom configuration parameter 2 override. - custom3: Optional custom configuration parameter 3 override. - custom4: Optional custom configuration parameter 4 override. - custom5: Optional custom configuration parameter 5 override. - -Returns: - Device: The opened device, ready for direct backend construction. - -Raises: - IndexError: If the ID is not registered. - RuntimeError: If the device library cannot be loaded or initialized.)pb"); -} - -} // namespace mqt diff --git a/bindings/na/CMakeLists.txt b/bindings/na/CMakeLists.txt index 688e7a7ab9..37a57c0685 100644 --- a/bindings/na/CMakeLists.txt +++ b/bindings/na/CMakeLists.txt @@ -22,7 +22,7 @@ if(NOT TARGET ${TARGET_NAME}) INSTALL_DIR . LINK_LIBS - MQT::CoreNAFoMaC) + MQT::CoreNAQDMI) # install the Python stub file in editable mode for better IDE support if(SKBUILD_STATE STREQUAL "editable") diff --git a/bindings/na/register_na.cpp b/bindings/na/register_na.cpp index 3124fe929e..162a96854c 100644 --- a/bindings/na/register_na.cpp +++ b/bindings/na/register_na.cpp @@ -15,15 +15,15 @@ namespace mqt { namespace nb = nanobind; // forward declarations -void registerFomac(nb::module_& m); +void registerQDMI(nb::module_& m); NB_MODULE(MQT_CORE_MODULE_NAME, m) { m.doc() = R"pb(MQT Core NA - The MQT Core Neutral Atom module. This module contains all neutral atom related functionality of MQT Core.)pb"; - nb::module_ fomac = m.def_submodule("fomac"); - registerFomac(fomac); + nb::module_ qdmi = m.def_submodule("qdmi"); + registerQDMI(qdmi); } } // namespace mqt diff --git a/bindings/na/register_fomac.cpp b/bindings/na/register_qdmi.cpp similarity index 83% rename from bindings/na/register_fomac.cpp rename to bindings/na/register_qdmi.cpp index a35059e772..97d664289b 100644 --- a/bindings/na/register_fomac.cpp +++ b/bindings/na/register_qdmi.cpp @@ -8,8 +8,8 @@ * Licensed under the MIT License */ -#include "fomac/FoMaC.hpp" -#include "na/fomac/Device.hpp" +#include "na/qdmi/Device.hpp" +#include "qdmi/Device.hpp" #include "qdmi/devices/na/Generator.hpp" #include @@ -36,12 +36,12 @@ template [[nodiscard]] auto repr(T c) -> std::string { } // namespace // NOLINTNEXTLINE(misc-use-internal-linkage) -void registerFomac(nb::module_& m) { +void registerQDMI(nb::module_& m) { m.doc() = R"pb(Reconstruction of NADevice from QDMI's Device class.)pb"; - nb::module_::import_("mqt.core.fomac"); + nb::module_::import_("mqt.core.qdmi"); - auto device = nb::class_( + auto device = nb::class_( m, "Device", "Represents a device with a lattice of traps."); auto lattice = nb::class_( @@ -107,40 +107,33 @@ void registerFomac(nb::module_& m) { lattice.def(nb::self != nb::self, nb::sig("def __ne__(self, arg: object, /) -> bool")); - device.def_prop_ro("traps", &na::Session::Device::getTraps, + device.def_prop_ro("traps", &na::qdmi::Device::getTraps, nb::rv_policy::reference_internal, "The list of trap positions in the device."); device.def_prop_ro( "t1", - [](const na::Session::Device& dev) { - return dev.getDecoherenceTimes().t1; - }, + [](const na::qdmi::Device& dev) { return dev.getDecoherenceTimes().t1; }, "The T1 time of the device."); device.def_prop_ro( "t2", - [](const na::Session::Device& dev) { - return dev.getDecoherenceTimes().t2; - }, + [](const na::qdmi::Device& dev) { return dev.getDecoherenceTimes().t2; }, "The T2 time of the device."); - device.def("__repr__", [](const fomac::Device& dev) { + device.def("__repr__", [](const qdmi::Device& dev) { return ""; }); device.def_static("try_create_from_device", - &na::Session::Device::tryCreateFromDevice, "device"_a, - R"pb(Create NA FoMaC Device from generic FoMaC Device. + &na::qdmi::Device::tryCreateFromDevice, "device"_a, + R"pb(Create NA QDMI Device from generic QDMI Device. Args: - device: The generic FoMaC Device to convert. + device: The generic QDMI Device to convert. Returns: - The converted NA FoMaC Device or None if the conversion is not possible.)pb"); + The converted NA QDMI Device or None if the conversion is not possible.)pb"); device.def(nb::self == nb::self, nb::sig("def __eq__(self, arg: object, /) -> bool")); device.def(nb::self != nb::self, nb::sig("def __ne__(self, arg: object, /) -> bool")); - - m.def("devices", &na::Session::getDevices, nb::rv_policy::reference_internal, - "Returns a list of available devices."); } } // namespace mqt diff --git a/bindings/patterns.txt b/bindings/patterns.txt index 5ee2e8895e..01ddf6521b 100644 --- a/bindings/patterns.txt +++ b/bindings/patterns.txt @@ -1,4 +1,4 @@ -mqt\.core\.fomac\.(?:Job|Device|Device\.Site)\.query_custom_property$: +(?:mqt\.core\.)?qdmi\.(?:Job|Device|Device\.Site)\.query_custom_property$: \from typing import overload @overload def query_custom_property(self, custom_property: CustomProperty, value_type: type[str]) -> str | None: ... @@ -16,7 +16,7 @@ mqt\.core\.fomac\.(?:Job|Device|Device\.Site)\.query_custom_property$: ) -> str | bool | int | float | bytes | None: \doc -mqt\.core\.fomac\.Job\.get_custom_result$: +(?:mqt\.core\.)?qdmi\.Job\.get_custom_result$: \from typing import overload @overload def get_custom_result(self, custom_property: CustomProperty, value_type: type[str]) -> str | None: ... @@ -34,7 +34,7 @@ mqt\.core\.fomac\.Job\.get_custom_result$: ) -> str | bool | int | float | bytes | None: \doc -mqt\.core\.fomac\.Device\.Operation\.query_custom_property$: +(?:mqt\.core\.)?qdmi\.Device\.Operation\.query_custom_property$: \from typing import overload @overload def query_custom_property( @@ -86,6 +86,21 @@ mqt\.core\.fomac\.Device\.Operation\.query_custom_property$: ) -> str | bool | int | float | bytes | None: \doc +(?:mqt\.core\.)?qdmi\.Device\.submit_job$: + def submit_job( + self, + program: str | bytes, + program_format: ProgramFormat, + num_shots: int, + *, + custom1: str | bool | int | float | None = None, # noqa: PYI041 + custom2: str | bool | int | float | None = None, # noqa: PYI041 + custom3: str | bool | int | float | None = None, # noqa: PYI041 + custom4: str | bool | int | float | None = None, # noqa: PYI041 + custom5: str | bool | int | float | None = None, # noqa: PYI041 + ) -> Job: + \doc + mqt.core.mlir.compile_program: \from typing import overload, Literal @overload diff --git a/bindings/fomac/CMakeLists.txt b/bindings/qdmi/CMakeLists.txt similarity index 82% rename from bindings/fomac/CMakeLists.txt rename to bindings/qdmi/CMakeLists.txt index f95c2f44a7..03ab6f9e8f 100644 --- a/bindings/fomac/CMakeLists.txt +++ b/bindings/qdmi/CMakeLists.txt @@ -6,7 +6,7 @@ # # Licensed under the MIT License -set(TARGET_NAME "${MQT_CORE_TARGET_NAME}-fomac-bindings") +set(TARGET_NAME "${MQT_CORE_TARGET_NAME}-qdmi-bindings") if(NOT TARGET ${TARGET_NAME}) # collect source files @@ -18,16 +18,16 @@ if(NOT TARGET ${TARGET_NAME}) ${TARGET_NAME} ${SOURCES} MODULE_NAME - fomac + qdmi INSTALL_DIR . LINK_LIBS - MQT::CoreFoMaC) + MQT::CoreQDMI) # install the Python stub file in editable mode for better IDE support if(SKBUILD_STATE STREQUAL "editable") install( - FILES ${PROJECT_SOURCE_DIR}/python/mqt/core/fomac.pyi + FILES ${PROJECT_SOURCE_DIR}/python/mqt/core/qdmi.pyi DESTINATION . COMPONENT ${MQT_CORE_TARGET_NAME}_Python) endif() diff --git a/bindings/qdmi/qdmi.cpp b/bindings/qdmi/qdmi.cpp new file mode 100644 index 0000000000..6d5140cb04 --- /dev/null +++ b/bindings/qdmi/qdmi.cpp @@ -0,0 +1,788 @@ +/* + * 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 +#include +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace mqt { + +namespace nb = nanobind; +using namespace nb::literals; +namespace { +template +[[nodiscard]] nb::object queryCustomValue(Query query, + const nb::handle valueType) { + const auto returnValue = + [](std::optional value) -> nb::object { + if (!value.has_value()) { + return nb::none(); + } + return nb::cast(std::move(*value)); + }; + + const auto builtins = nb::builtins(); + if (valueType.is(builtins["str"])) { + return returnValue(query.template operator()()); + } + if (valueType.is(builtins["bool"])) { + return returnValue(query.template operator()()); + } + if (valueType.is(builtins["int"])) { + return returnValue(query.template operator()()); + } + if (valueType.is(builtins["float"])) { + return returnValue(query.template operator()()); + } + if (valueType.is(builtins["bytes"])) { + const auto value = query.template operator()>(); + if (!value.has_value()) { + return nb::none(); + } + return nb::bytes(reinterpret_cast(value->data()), + value->size()); + } + throw nb::type_error( + "value_type must be exactly str, bool, int, float, or bytes"); +} +} // namespace + +NB_MODULE(MQT_CORE_MODULE_NAME, + m) { // NOLINT(performance-unnecessary-value-param) + auto sessionParameters = nb::class_( + m, "SessionParameters", R"pb(Parameters for one QDMI device session.)pb"); + sessionParameters + .def( + "__init__", + [](qdmi::SessionParameters* self, std::optional baseUrl, + std::optional token, + std::optional authFile, + std::optional authUrl, + std::optional username, + std::optional password, + std::optional custom1, + std::optional custom2, + std::optional custom3, + std::optional custom4, + std::optional custom5) { + new (self) qdmi::SessionParameters{ + .baseUrl = std::move(baseUrl), + .token = std::move(token), + .authFile = std::move(authFile), + .authUrl = std::move(authUrl), + .username = std::move(username), + .password = std::move(password), + .custom1 = std::move(custom1), + .custom2 = std::move(custom2), + .custom3 = std::move(custom3), + .custom4 = std::move(custom4), + .custom5 = std::move(custom5), + }; + }, + nb::kw_only(), "base_url"_a = nb::none(), "token"_a = nb::none(), + "auth_file"_a = nb::none(), "auth_url"_a = nb::none(), + "username"_a = nb::none(), "password"_a = nb::none(), + "custom1"_a = nb::none(), "custom2"_a = nb::none(), + "custom3"_a = nb::none(), "custom4"_a = nb::none(), + "custom5"_a = nb::none(), + R"pb(Create session parameters from optional keyword arguments. + +Args: + base_url: Base URL of the device service. + token: Authentication token. + auth_file: Path to an authentication file. + auth_url: URL of the authentication service. + username: Authentication username. + password: Authentication password. + custom1: First implementation-defined session parameter. + custom2: Second implementation-defined session parameter. + custom3: Third implementation-defined session parameter. + custom4: Fourth implementation-defined session parameter. + custom5: Fifth implementation-defined session parameter.)pb") + .def_rw("base_url", &qdmi::SessionParameters::baseUrl, + R"pb(Base URL of the device service.)pb") + .def_rw("token", &qdmi::SessionParameters::token, + R"pb(Authentication token.)pb") + .def_rw("auth_file", &qdmi::SessionParameters::authFile, + R"pb(Path to an authentication file.)pb") + .def_rw("auth_url", &qdmi::SessionParameters::authUrl, + R"pb(URL of the authentication service.)pb") + .def_rw("username", &qdmi::SessionParameters::username, + R"pb(Authentication username.)pb") + .def_rw("password", &qdmi::SessionParameters::password, + R"pb(Authentication password.)pb") + .def_rw("custom1", &qdmi::SessionParameters::custom1, + R"pb(First implementation-defined session parameter.)pb") + .def_rw("custom2", &qdmi::SessionParameters::custom2, + R"pb(Second implementation-defined session parameter.)pb") + .def_rw("custom3", &qdmi::SessionParameters::custom3, + R"pb(Third implementation-defined session parameter.)pb") + .def_rw("custom4", &qdmi::SessionParameters::custom4, + R"pb(Fourth implementation-defined session parameter.)pb") + .def_rw("custom5", &qdmi::SessionParameters::custom5, + R"pb(Fifth implementation-defined session parameter.)pb"); + + auto definition = nb::class_( + m, "DeviceDefinition", + R"pb(A side-effect-free QDMI device registration.)pb"); + definition + .def( + "__init__", + [](qdmi::DeviceDefinition* self, std::string deviceId, + std::filesystem::path library, std::string prefix, + qdmi::SessionParameters session) { + new (self) qdmi::DeviceDefinition{.id = std::move(deviceId), + .library = std::move(library), + .prefix = std::move(prefix), + .session = std::move(session)}; + }, + "device_id"_a, "library"_a, "prefix"_a, nb::kw_only(), + "session"_a = qdmi::SessionParameters{}, + R"pb(Create a device definition without loading its library. + +Args: + device_id: Stable identifier used for discovery and opening. + library: Path to the native QDMI device library. + prefix: Symbol prefix exported by the QDMI implementation. + session: Default parameters for sessions opened from this definition.)pb") + .def_rw("device_id", &qdmi::DeviceDefinition::id, + R"pb(Stable device identifier.)pb") + .def_rw("library", &qdmi::DeviceDefinition::library, + R"pb(Path to the native QDMI device library.)pb") + .def_rw("prefix", &qdmi::DeviceDefinition::prefix, + R"pb(Symbol prefix exported by the device library.)pb") + .def_rw("session", &qdmi::DeviceDefinition::session, + R"pb(Default parameters for newly opened sessions.)pb"); + + auto registry = nb::class_( + m, "DeviceRegistry", + R"pb(Discover or explicitly register QDMI device definitions.)pb"); + registry + .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("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"); + + // Job class + auto job = nb::class_( + m, "Job", + R"pb(A submitted quantum program execution retaining its device session.)pb"); + + job.def("check", &qdmi::Job::check, + R"pb(Return the current QDMI job status.)pb"); + + job.def("wait", &qdmi::Job::wait, "timeout"_a = 0, + R"pb(Waits for the job to complete. + +Args: + timeout: The maximum time to wait in seconds. If 0, waits indefinitely. + +Returns: + True if the job completed within the timeout, False otherwise.)pb"); + + job.def("cancel", &qdmi::Job::cancel, + R"pb(Request cancellation of the job.)pb"); + + job.def("get_shots", &qdmi::Job::getShots, + R"pb(Return the raw shot results.)pb"); + + job.def("get_counts", &qdmi::Job::getCounts, + R"pb(Return measurement counts keyed by bit string.)pb"); + + job.def("get_dense_statevector", &qdmi::Job::getDenseStateVector, + R"pb(Return the dense state vector. + +This result is typically available only from simulator devices.)pb"); + + job.def("get_dense_probabilities", &qdmi::Job::getDenseProbabilities, + R"pb(Return the dense probability vector. + +This result is typically available only from simulator devices.)pb"); + + job.def("get_sparse_statevector", &qdmi::Job::getSparseStateVector, + R"pb(Return the sparse state vector keyed by basis state. + +This result is typically available only from simulator devices.)pb"); + + job.def("get_sparse_probabilities", &qdmi::Job::getSparseProbabilities, + R"pb(Return sparse probabilities keyed by basis state. + +This result is typically available only from simulator devices.)pb"); + + job.def( + "query_custom_property", + [](const qdmi::Job& self, const qdmi::CustomProperty customProperty, + const nb::handle valueType) { + return queryCustomValue( + [&self, customProperty]() { + return self.queryCustomProperty(customProperty); + }, + valueType); + }, + "custom_property"_a, "value_type"_a, + nb::sig("def query_custom_property(self, custom_property: " + "CustomProperty, " + "value_type: type[str] | type[bool] | type[int] | type[float] | " + "type[bytes]) -> str | bool | int | float | bytes | None"), + R"pb(Query an implementation-defined custom job property. + +The caller must provide the type documented by the device implementation. +Use ``bytes`` to retrieve the value without interpretation. + +Args: + custom_property: Custom property slot to query. + value_type: Expected Python type of the property value. + +Returns: + The typed property value, or ``None`` when the slot is unsupported.)pb"); + + job.def( + "get_custom_result", + [](const qdmi::Job& self, const qdmi::CustomProperty customProperty, + const nb::handle valueType) { + return queryCustomValue( + [&self, customProperty]() { + return self.getCustomResult(customProperty); + }, + valueType); + }, + "custom_property"_a, "value_type"_a, + nb::sig("def get_custom_result(self, custom_property: CustomProperty, " + "value_type: type[str] | type[bool] | type[int] | type[float] | " + "type[bytes]) -> str | bool | int | float | bytes | None"), + R"pb(Return an implementation-defined custom job result. + +The caller must provide the type documented by the device implementation. +Use ``bytes`` to retrieve the value without interpretation. + +Args: + custom_property: Custom result slot to retrieve. + value_type: Expected Python type of the result value. + +Returns: + The typed result value, or ``None`` when the slot is unsupported.)pb"); + + job.def_prop_ro("id", &qdmi::Job::getId, + R"pb(The device-assigned job identifier.)pb"); + + job.def_prop_ro("program_format", &qdmi::Job::getProgramFormat, + R"pb(The QDMI format of the submitted program.)pb"); + + job.def_prop_ro("program", &qdmi::Job::getProgram, + R"pb(The submitted program.)pb"); + + job.def_prop_ro( + "program_bytes", + [](const qdmi::Job& self) { + const auto program = self.getProgramBytes(); + return nb::bytes(program.data(), program.size()); + }, + R"pb(The exact bytes of the submitted program.)pb"); + + job.def_prop_ro("num_shots", &qdmi::Job::getNumShots, + R"pb(The requested number of shots.)pb"); + + job.def(nb::self == nb::self, + nb::sig("def __eq__(self, arg: object, /) -> bool"), + R"pb(Return whether two objects refer to the same job.)pb"); + job.def(nb::self != nb::self, + nb::sig("def __ne__(self, arg: object, /) -> bool"), + R"pb(Return whether two objects refer to different jobs.)pb"); + + // JobStatus enum + nb::enum_(job, "Status", + R"pb(Status values defined by QDMI.)pb") + .value("CREATED", QDMI_JOB_STATUS_CREATED) + .value("SUBMITTED", QDMI_JOB_STATUS_SUBMITTED) + .value("QUEUED", QDMI_JOB_STATUS_QUEUED) + .value("RUNNING", QDMI_JOB_STATUS_RUNNING) + .value("DONE", QDMI_JOB_STATUS_DONE) + .value("CANCELED", QDMI_JOB_STATUS_CANCELED) + .value("FAILED", QDMI_JOB_STATUS_FAILED); + + // ProgramFormat enum + nb::enum_(m, "ProgramFormat", + R"pb(Program formats defined by QDMI.)pb") + .value("QASM2", QDMI_PROGRAM_FORMAT_QASM2) + .value("QASM3", QDMI_PROGRAM_FORMAT_QASM3) + .value("QIR_BASE_STRING", QDMI_PROGRAM_FORMAT_QIRBASESTRING) + .value("QIR_BASE_MODULE", QDMI_PROGRAM_FORMAT_QIRBASEMODULE) + .value("QIR_ADAPTIVE_STRING", QDMI_PROGRAM_FORMAT_QIRADAPTIVESTRING) + .value("QIR_ADAPTIVE_MODULE", QDMI_PROGRAM_FORMAT_QIRADAPTIVEMODULE) + .value("CALIBRATION", QDMI_PROGRAM_FORMAT_CALIBRATION) + .value("QPY", QDMI_PROGRAM_FORMAT_QPY) + .value("IQM_JSON", QDMI_PROGRAM_FORMAT_IQMJSON) + .value("BATCH_JOB", QDMI_PROGRAM_FORMAT_BATCHJOB) + .value("CUSTOM1", QDMI_PROGRAM_FORMAT_CUSTOM1) + .value("CUSTOM2", QDMI_PROGRAM_FORMAT_CUSTOM2) + .value("CUSTOM3", QDMI_PROGRAM_FORMAT_CUSTOM3) + .value("CUSTOM4", QDMI_PROGRAM_FORMAT_CUSTOM4) + .value("CUSTOM5", QDMI_PROGRAM_FORMAT_CUSTOM5); + + nb::enum_( + m, "CustomProperty", + R"pb(An implementation-defined custom property or result slot.)pb") + .value("CUSTOM1", qdmi::CustomProperty::Custom1) + .value("CUSTOM2", qdmi::CustomProperty::Custom2) + .value("CUSTOM3", qdmi::CustomProperty::Custom3) + .value("CUSTOM4", qdmi::CustomProperty::Custom4) + .value("CUSTOM5", qdmi::CustomProperty::Custom5); + + // Device class + auto device = nb::class_( + m, "Device", R"pb(One initialized QDMI device session. + +The object owns the native library and session state required by its sites, +operations, child devices, and jobs.)pb"); + + nb::enum_(device, "Status", + R"pb(Status values defined by QDMI.)pb") + .value("OFFLINE", QDMI_DEVICE_STATUS_OFFLINE) + .value("IDLE", QDMI_DEVICE_STATUS_IDLE) + .value("BUSY", QDMI_DEVICE_STATUS_BUSY) + .value("ERROR", QDMI_DEVICE_STATUS_ERROR) + .value("MAINTENANCE", QDMI_DEVICE_STATUS_MAINTENANCE) + .value("CALIBRATION", QDMI_DEVICE_STATUS_CALIBRATION); + + device.def("name", &qdmi::Device::getName, + R"pb(Return the device name reported by its implementation.)pb"); + + device.def( + "version", &qdmi::Device::getVersion, + R"pb(Return the device version reported by its implementation.)pb"); + + device.def("status", &qdmi::Device::getStatus, + R"pb(Return the current QDMI device status.)pb"); + + device.def("library_version", &qdmi::Device::getLibraryVersion, + R"pb(Return the device library version.)pb"); + + device.def("qubits_num", &qdmi::Device::getQubitsNum, + R"pb(Return the number of qubits available on the device.)pb"); + + device.def("sites", &qdmi::Device::getSites, + R"pb(Return all regular sites and zones.)pb"); + + device.def("regular_sites", &qdmi::Device::getRegularSites, + R"pb(Return sites that are not zones.)pb"); + + device.def("zones", &qdmi::Device::getZones, + R"pb(Return sites that represent zones.)pb"); + + device.def("operations", &qdmi::Device::getOperations, + R"pb(Return operations supported by the device.)pb"); + + device.def("coupling_map", &qdmi::Device::getCouplingMap, + R"pb(Return the optional coupling map as site pairs.)pb"); + + device.def("needs_calibration", &qdmi::Device::getNeedsCalibration, + R"pb(Return the optional calibration requirement.)pb"); + + device.def("length_unit", &qdmi::Device::getLengthUnit, + R"pb(Return the optional device length unit.)pb"); + + device.def("length_scale_factor", &qdmi::Device::getLengthScaleFactor, + R"pb(Return the optional length scale factor.)pb"); + + device.def("duration_unit", &qdmi::Device::getDurationUnit, + R"pb(Return the optional device duration unit.)pb"); + + device.def("duration_scale_factor", &qdmi::Device::getDurationScaleFactor, + R"pb(Return the optional duration scale factor.)pb"); + + device.def("min_atom_distance", &qdmi::Device::getMinAtomDistance, + R"pb(Return the optional minimum atom distance.)pb"); + + device.def("supported_program_formats", + &qdmi::Device::getSupportedProgramFormats, + R"pb(Return the QDMI program formats accepted by the device.)pb"); + + device.def("child_devices", &qdmi::Device::getChildDevices, + R"pb(Return directly managed child devices.)pb"); + + device.def( + "query_custom_property", + [](const qdmi::Device& self, const qdmi::CustomProperty customProperty, + const nb::handle valueType) { + return queryCustomValue( + [&self, customProperty]() { + return self.queryCustomProperty(customProperty); + }, + valueType); + }, + "custom_property"_a, "value_type"_a, + nb::sig("def query_custom_property(self, custom_property: " + "CustomProperty, " + "value_type: type[str] | type[bool] | type[int] | type[float] | " + "type[bytes]) -> str | bool | int | float | bytes | None"), + R"pb(Query an implementation-defined custom device property. + +The caller must provide the type documented by the device implementation. +Use ``bytes`` to retrieve the value without interpretation. + +Args: + custom_property: Custom property slot to query. + value_type: Expected Python type of the property value. + +Returns: + The typed property value, or ``None`` when the slot is unsupported.)pb"); + + device.def( + "submit_job", + [](const qdmi::Device& self, 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) { + return self.submitJob(program, format, numShots, custom1, custom2, + custom3, custom4, custom5); + }, + "program"_a, "program_format"_a, "num_shots"_a, nb::kw_only(), + "custom1"_a = nb::none(), "custom2"_a = nb::none(), + "custom3"_a = nb::none(), "custom4"_a = nb::none(), + "custom5"_a = nb::none(), + nb::sig("def submit_job(self, program: str, program_format: " + "ProgramFormat, num_shots: int, *, custom1: str | bool | " + "int | float | None = None, custom2: str | bool | int | " + "float | None = None, custom3: str | bool | int | float | " + "None = None, custom4: str | bool | int | float | None = " + "None, custom5: str | bool | int | float | None = None) " + "-> Job"), + nb::rv_policy::reference_internal, + R"pb(Submit a quantum program to the device. + +Args: + program: Text submitted with a terminating null byte, or exact bytes. + program_format: QDMI format of ``program``. + num_shots: Number of requested executions. + custom1: First implementation-defined job parameter. + custom2: Second implementation-defined job parameter. + custom3: Third implementation-defined job parameter. + custom4: Fourth implementation-defined job parameter. + custom5: Fifth implementation-defined job parameter. + +Returns: + A job retaining the device session.)pb"); + + device.def( + "submit_job", + [](const qdmi::Device& self, const nb::bytes& 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 auto bytes = std::span{ + static_cast(program.data()), program.size()}; + return self.submitJob(bytes, format, numShots, custom1, custom2, + custom3, custom4, custom5); + }, + "program"_a, "program_format"_a, "num_shots"_a, nb::kw_only(), + "custom1"_a = nb::none(), "custom2"_a = nb::none(), + "custom3"_a = nb::none(), "custom4"_a = nb::none(), + "custom5"_a = nb::none(), + nb::sig("def submit_job(self, program: bytes, program_format: " + "ProgramFormat, num_shots: int, *, custom1: str | bool | int | " + "float | None = None, custom2: str | bool | int | float | None " + "= None, custom3: str | bool | int | float | None = None, " + "custom4: str | bool | int | float | None = None, custom5: str " + "| bool | int | float | None = None) -> Job"), + nb::rv_policy::reference_internal, + R"pb(Submit an exact byte payload to the device.)pb"); + + device.def( + "__repr__", + [](const qdmi::Device& dev) { + return ""; + }, + R"pb(Return a concise device representation.)pb"); + + device.def(nb::self == nb::self, + nb::sig("def __eq__(self, arg: object, /) -> bool"), + R"pb(Return whether two objects refer to the same device.)pb"); + device.def(nb::self != nb::self, + nb::sig("def __ne__(self, arg: object, /) -> bool"), + R"pb(Return whether two objects refer to different devices.)pb"); + + // Site class + auto site = nb::class_( + device, "Site", R"pb(A physical site or zone belonging to a device.)pb"); + + site.def("index", &qdmi::Site::getIndex, + R"pb(Return the device-assigned site index.)pb"); + + site.def("t1", &qdmi::Site::getT1, + R"pb(Return the optional T1 coherence time.)pb"); + + site.def("t2", &qdmi::Site::getT2, + R"pb(Return the optional T2 coherence time.)pb"); + + site.def("name", &qdmi::Site::getName, + R"pb(Return the optional site name.)pb"); + + site.def("x_coordinate", &qdmi::Site::getXCoordinate, + R"pb(Return the optional x coordinate.)pb"); + + site.def("y_coordinate", &qdmi::Site::getYCoordinate, + R"pb(Return the optional y coordinate.)pb"); + + site.def("z_coordinate", &qdmi::Site::getZCoordinate, + R"pb(Return the optional z coordinate.)pb"); + + site.def("is_zone", &qdmi::Site::isZone, + R"pb(Return whether this site represents a zone.)pb"); + + site.def("x_extent", &qdmi::Site::getXExtent, + R"pb(Return the optional x extent of the zone.)pb"); + + site.def("y_extent", &qdmi::Site::getYExtent, + R"pb(Return the optional y extent of the zone.)pb"); + + site.def("z_extent", &qdmi::Site::getZExtent, + R"pb(Return the optional z extent of the zone.)pb"); + + site.def("module_index", &qdmi::Site::getModuleIndex, + R"pb(Return the optional module index.)pb"); + + site.def("submodule_index", &qdmi::Site::getSubmoduleIndex, + R"pb(Return the optional submodule index.)pb"); + + site.def( + "query_custom_property", + [](const qdmi::Site& self, const qdmi::CustomProperty customProperty, + const nb::handle valueType) { + return queryCustomValue( + [&self, customProperty]() { + return self.queryCustomProperty(customProperty); + }, + valueType); + }, + "custom_property"_a, "value_type"_a, + nb::sig("def query_custom_property(self, custom_property: " + "CustomProperty, " + "value_type: type[str] | type[bool] | type[int] | type[float] | " + "type[bytes]) -> str | bool | int | float | bytes | None"), + R"pb(Query an implementation-defined custom site property. + +The caller must provide the type documented by the device implementation. +Use ``bytes`` to retrieve the value without interpretation. + +Args: + custom_property: Custom property slot to query. + value_type: Expected Python type of the property value. + +Returns: + The typed property value, or ``None`` when the slot is unsupported.)pb"); + + site.def( + "__repr__", + [](const qdmi::Site& s) { + return ""; + }, + R"pb(Return a concise site representation.)pb"); + + site.def(nb::self == nb::self, + nb::sig("def __eq__(self, arg: object, /) -> bool"), + R"pb(Return whether two objects refer to the same site.)pb"); + site.def(nb::self != nb::self, + nb::sig("def __ne__(self, arg: object, /) -> bool"), + R"pb(Return whether two objects refer to different sites.)pb"); + + // Operation class + auto operation = nb::class_( + device, "Operation", R"pb(A quantum operation supported by a device.)pb"); + + operation.def( + "name", &qdmi::Operation::getName, + "sites"_a.sig("...") = std::vector{}, + "params"_a.sig("...") = std::vector{}, + R"pb(Return the operation name for the given sites and parameters.)pb"); + + operation.def("qubits_num", &qdmi::Operation::getQubitsNum, + "sites"_a.sig("...") = std::vector{}, + "params"_a.sig("...") = std::vector{}, + R"pb(Return the optional operation arity.)pb"); + + operation.def("parameters_num", &qdmi::Operation::getParametersNum, + "sites"_a.sig("...") = std::vector{}, + "params"_a.sig("...") = std::vector{}, + R"pb(Return the number of operation parameters.)pb"); + + operation.def( + "duration", &qdmi::Operation::getDuration, + "sites"_a.sig("...") = std::vector{}, + "params"_a.sig("...") = std::vector{}, + R"pb(Return the optional duration for this operation instance.)pb"); + + operation.def( + "fidelity", &qdmi::Operation::getFidelity, + "sites"_a.sig("...") = std::vector{}, + "params"_a.sig("...") = std::vector{}, + R"pb(Return the optional fidelity for this operation instance.)pb"); + + operation.def("interaction_radius", &qdmi::Operation::getInteractionRadius, + "sites"_a.sig("...") = std::vector{}, + "params"_a.sig("...") = std::vector{}, + R"pb(Return the optional interaction radius.)pb"); + + operation.def("blocking_radius", &qdmi::Operation::getBlockingRadius, + "sites"_a.sig("...") = std::vector{}, + "params"_a.sig("...") = std::vector{}, + R"pb(Return the optional blocking radius.)pb"); + + operation.def("idling_fidelity", &qdmi::Operation::getIdlingFidelity, + "sites"_a.sig("...") = std::vector{}, + "params"_a.sig("...") = std::vector{}, + R"pb(Return the optional idling fidelity.)pb"); + + operation.def("is_zoned", &qdmi::Operation::isZoned, + R"pb(Return whether the operation is restricted to zones.)pb"); + + operation.def("sites", &qdmi::Operation::getSites, + R"pb(Return sites on which the operation is available.)pb"); + + operation.def( + "site_pairs", &qdmi::Operation::getSitePairs, + R"pb(Return supported site pairs for a local two-site operation.)pb"); + + operation.def("mean_shuttling_speed", &qdmi::Operation::getMeanShuttlingSpeed, + "sites"_a.sig("...") = std::vector{}, + "params"_a.sig("...") = std::vector{}, + R"pb(Return the optional mean shuttling speed.)pb"); + + operation.def( + "query_custom_property", + [](const qdmi::Operation& self, const qdmi::CustomProperty customProperty, + const nb::handle valueType, const std::vector& sites, + const std::vector& params) { + return queryCustomValue( + [&self, customProperty, &sites, + ¶ms]() { + return self.queryCustomProperty(customProperty, sites, params); + }, + valueType); + }, + "custom_property"_a, "value_type"_a, + "sites"_a.sig("...") = std::vector{}, + "params"_a.sig("...") = std::vector{}, + nb::sig("def query_custom_property(self, custom_property: " + "CustomProperty, " + "value_type: type[str] | type[bool] | type[int] | type[float] | " + "type[bytes], sites: Sequence[mqt.core.qdmi.Device.Site] = " + "..., params: Sequence[float] = ...) -> str | bool | int | " + "float | bytes | None"), + R"pb(Query an implementation-defined custom operation property. + +The caller must provide the type documented by the device implementation. +Use ``bytes`` to retrieve the value without interpretation. + +Args: + custom_property: Custom property slot to query. + value_type: Expected Python type of the property value. + sites: Sites for the operation instance. + params: Parameters for the operation instance. + +Returns: + The typed property value, or ``None`` when the slot is unsupported.)pb"); + + operation.def( + "__repr__", + [](const qdmi::Operation& op) { + return ""; + }, + R"pb(Return a concise operation representation.)pb"); + + operation.def( + nb::self == nb::self, nb::sig("def __eq__(self, arg: object, /) -> bool"), + R"pb(Return whether two objects refer to the same operation.)pb"); + operation.def( + nb::self != nb::self, nb::sig("def __ne__(self, arg: object, /) -> bool"), + R"pb(Return whether two objects refer to different operations.)pb"); + + auto openAllResult = nb::class_( + m, "OpenAllResult", + R"pb(Devices and per-ID errors produced by bulk opening.)pb"); + openAllResult + .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 failed definitions keyed by stable ID.)pb"); + + auto manager = nb::class_( + m, "DeviceManager", R"pb(Discover and lazily open QDMI devices. + +Definitions are discovered without loading native libraries. Opening a device +creates an independent session while compatible devices may share a loaded +library.)pb"); + manager + .def(nb::init<>(), + R"pb(Create a manager from the standard configuration sources.)pb") + .def(nb::init(), "registry"_a, + R"pb(Create a manager from an immutable registry snapshot.)pb") + .def_prop_ro( + "definitions", + [](const qdmi::DeviceManager& self) { return self.definitions(); }, + R"pb(The manager's immutable device definitions.)pb") + .def( + "open", + [](const qdmi::DeviceManager& self, const std::string& deviceId, + const qdmi::SessionParameters& sessionOverrides) { + return self.open(deviceId, sessionOverrides); + }, + "device_id"_a, nb::kw_only(), + "session_overrides"_a = qdmi::SessionParameters{}, + R"pb(Open one device by stable ID. + +The supplied session values override the definition defaults field by field. +The native library is loaded only when this method is called.)pb") + .def("open_all", &qdmi::DeviceManager::openAll, nb::kw_only(), + "session_overrides"_a = qdmi::SessionParameters{}, + R"pb(Open a snapshot of all definitions independently. + +Failures are retained by device ID and do not prevent other definitions from +opening.)pb"); +} + +} // namespace mqt diff --git a/docs/Doxyfile b/docs/Doxyfile index e3ec51ebf9..b184ae127b 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -2101,7 +2101,7 @@ SKIP_FUNCTION_MACROS = YES # the path). If a tag file is not located in the directory in which Doxygen is # run, you must also specify the path to the tagfile here. -TAGFILES = _build/qdmi.tag=https://munich-quantum-software-stack.github.io/QDMI/v1.3.2/ +TAGFILES = _build/qdmi.tag=$(QDMI_API_URL) # When a file name is specified after GENERATE_TAGFILE, Doxygen will create a # tag file that is based on the input files it reads. See section "Linking to diff --git a/docs/_ext/cpp_api.py b/docs/_ext/cpp_api.py index 6cd0247f76..9a0a1c1151 100644 --- a/docs/_ext/cpp_api.py +++ b/docs/_ext/cpp_api.py @@ -10,6 +10,7 @@ from __future__ import annotations +import os import shutil import subprocess from dataclasses import dataclass @@ -296,7 +297,11 @@ def build_doxygen(app: Sphinx) -> None: try: with urlopen(app.config.qdmi_api_tagfile_url, timeout=30) as response: # ruff:ignore[suspicious-url-open-usage] tagfile.write_bytes(response.read()) - subprocess.run([doxygen, "Doxyfile"], cwd=app.srcdir, check=True) # ruff:ignore[subprocess-without-shell-equals-true] + environment = os.environ.copy() + environment["QDMI_API_URL"] = app.config.qdmi_api_tagfile[1] + subprocess.run( # ruff:ignore[subprocess-without-shell-equals-true] + [doxygen, "Doxyfile"], cwd=app.srcdir, check=True, env=environment + ) except (OSError, subprocess.CalledProcessError) as error: msg = "Unable to generate the native C++ API documentation" raise ExtensionError(msg) from error @@ -317,10 +322,10 @@ def setup(app: Sphinx) -> ExtensionMetadata: Metadata declaring that the extension supports parallel builds. """ app.add_config_value("cpp_api_tagfile", ("_build/doxygen/mqt-core.tag", "cpp/", "_build/doxygen/xml"), "env") - app.add_config_value("qdmi_api_tagfile", ("_tagfiles/qdmi-1.3.2.tag", ""), "env") + app.add_config_value("qdmi_api_tagfile", ("_tagfiles/qdmi.tag", ""), "env") app.add_config_value( "qdmi_api_tagfile_url", - "https://munich-quantum-software-stack.github.io/QDMI/v1.3.2/qdmi.tag", + "https://munich-quantum-software-stack.github.io/QDMI/latest/qdmi.tag", "env", ) app.add_domain(CppApiDomain) diff --git a/docs/conf.py b/docs/conf.py index 5f0be80416..61384a45e3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -14,6 +14,7 @@ import warnings from importlib import metadata from pathlib import Path +from re import search from typing import TYPE_CHECKING import pybtex.plugin @@ -28,6 +29,26 @@ sys.path.insert(0, str(Path(__file__).parent / "_ext")) +def dependency_version(name: str) -> str: + """Read a dependency version from the central CMake configuration. + + Args: + name: Name of the dependency whose version should be read. + + Returns: + The configured dependency version. + + Raises: + RuntimeError: If the dependency version is not configured. + """ + dependencies = (ROOT / "cmake" / "ExternalDependencies.cmake").read_text(encoding="utf-8") + match = search(rf"set\({name}_VERSION\s+([^\s)]+)", dependencies) + if match is None: + msg = f"Unable to determine the configured {name} version" + raise RuntimeError(msg) + return match.group(1) + + try: from mqt.core import __version__ as version except ModuleNotFoundError: @@ -177,8 +198,9 @@ def format_url(self, _e: Entry) -> HRef: # ruff:ignore[no-self-use] cpp_api_tagfile = ("_build/doxygen/mqt-core.tag", "cpp/", "_build/doxygen/xml") qdmi_api_tagfile = ( "_build/qdmi.tag", - "https://munich-quantum-software-stack.github.io/QDMI/v1.3.2/", + f"https://munich-quantum-software-stack.github.io/QDMI/v{dependency_version('QDMI')}/", ) +qdmi_api_tagfile_url = f"{qdmi_api_tagfile[1]}qdmi.tag" # -- Options for HTML output ------------------------------------------------- diff --git a/docs/qdmi/configuration.md b/docs/qdmi/configuration.md index a8190e8111..1979f394d6 100644 --- a/docs/qdmi/configuration.md +++ b/docs/qdmi/configuration.md @@ -1,9 +1,8 @@ # QDMI device configuration MQT Core discovers QDMI device definitions from versioned JSON or TOML -configuration. Discovery only parses definitions. The QDMI client opens the -configured native libraries when its first session is allocated, while stable-ID -APIs open only the requested device. Configuration is therefore a trusted input. +configuration. Discovery only parses definitions; a native device library is +loaded when its stable ID is opened. Configuration is therefore trusted input. ## Device definitions @@ -31,23 +30,21 @@ The following `qdmi.json` registers one device: ``` Every enabled definition requires a stable, unique `id`, a `library`, and a QDMI -symbol `prefix`. The `session` object supports `base-url`, `token`, `auth-file`, -`auth-url`, `username`, `password`, and `custom1` through `custom5`. +symbol `prefix`. The optional `session` object supports `base-url`, `token`, +`auth-file`, `auth-url`, `username`, `password`, and `custom1` through +`custom5`. Relative library and authentication-file paths are resolved against the file -that declared them. For `MQT_CORE_QDMI_CONFIG_JSON`, they resolve against the -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. +that declared them. Paths in `MQT_CORE_QDMI_CONFIG_JSON` resolve against the +current working directory. Unknown keys, invalid types, duplicate IDs within one +source, unsupported schema versions, and incomplete enabled definitions are +errors whose diagnostics name the source and configuration path. ## 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; 2. the system `qdmi.json`; 3. the user or XDG `qdmi.json`; 4. the nearest project `qdmi.json`, or `[tool.qdmi]` in `pyproject.toml` when no @@ -60,83 +57,81 @@ On Unix, file configuration uses `/etc/mqt-core/qdmi.json` and then `mqt-core/qdmi.json` files below `PROGRAMDATA` and `APPDATA`. An entry containing only its ID and `"enabled": false` masks an inherited -definition. Since definitions are merged field by field, a later definition with -the same ID must explicitly set `"enabled": true` to enable it again. Within one -directory, `qdmi.json` takes precedence over `pyproject.toml`. The final -disabled ID remains reserved, so fallback registration cannot silently re-enable -a device that an administrator disabled. +definition. A later configuration layer must explicitly set `"enabled": true` to +enable the ID again. Within one directory, `qdmi.json` takes precedence over +`pyproject.toml`. A finally disabled ID remains reserved, so fallback +registration cannot silently override an administrator's choice. `MQT_CORE_QDMI_CONFIG_FILE` replaces the system, user, and project levels while -retaining packaged built-ins. +retaining packaged definitions. -## Using configured devices +## Registering and opening devices -The Driver opens the discovered definitions when the first client session is -allocated, and a failure to load one definition does not hide the remaining -devices. Registration alone does not initialize device libraries. +Constructing a {cpp-api:class}`qdmi::DeviceRegistry` or the Python +{py:class}`mqt.core.qdmi.DeviceRegistry` without arguments performs standard +discovery. A registry can be extended before it is moved into a device manager: -```python -from mqt.core.fomac import Session +```cpp +#include "qdmi/DeviceManager.hpp" +#include "qdmi/DeviceRegistry.hpp" -for device in Session().get_devices(): - print(device.name()) +qdmi::DeviceRegistry registry; +registry.registerDeviceIfAbsent({ + .id = "example.device", + .library = "/path/to/libexample-device.so", + .prefix = "EXAMPLE", +}); + +qdmi::DeviceManager manager(std::move(registry)); +auto device = manager.open("example.device"); ``` -Set `MQT_CORE_QDMI_CONFIG_FILE` or `MQT_CORE_QDMI_CONFIG_JSON` before creating -the first session. Applications can also register a definition without loading -its library and open it later by stable ID: +`registerDeviceIfAbsent` is intended for device packages that provide a +programmatic fallback. It returns `false` when the stable ID already exists or +is disabled. `registerDevice` rejects duplicates unless `replace` is `true`; +explicit replacement can also re-enable a disabled ID. + +The equivalent Python API is: ```python -from mqt.core.fomac import DeviceDefinition, open_device, register_device +from mqt.core.qdmi import DeviceDefinition, DeviceManager, DeviceRegistry -register_device( +registry = DeviceRegistry() +registry.register_device_if_absent( DeviceDefinition( "example.device", "/path/to/libexample-device.so", "EXAMPLE", - base_url="https://device.example", ) ) -device = open_device("example.device") +device = DeviceManager(registry).open("example.device") +``` + +Construct a registry from a list of definitions when configuration discovery is +not wanted: + +```python +registry = DeviceRegistry([DeviceDefinition("example.device", "/path/to/device", "EXAMPLE")]) +manager = DeviceManager(registry) ``` -Every `open_device` call creates a fresh device session while preserving the -registered defaults and stable ID. This lets separate backend instances use -different credentials without registering process-lifetime UUIDs. The returned -`Device` and any child `Device`, `Site`, `Operation`, or `Job` wrappers derived -from it keep that fresh device session alive. The session is released after the -last such wrapper is destroyed. - -Code paths that may be imported more than once can use -`register_device_if_absent(definition)`. It returns whether the definition was -inserted and ignores an existing or explicitly disabled stable ID; malformed -definitions still raise an error. - -The equivalent C++ registration operation is `qdmi::Driver::registerDevice`. -Duplicate IDs are rejected unless `replace` is true, and an opened definition -cannot be replaced. `qdmi::Driver::open(id)` returns the cached device. -`fomac::Session::openDevice(id, overrides)` 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. - -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 -session parameters, for every definition. +A manager owns an immutable snapshot of its registry. Each `open` call creates a +fresh QDMI device session and applies the supplied `SessionParameters` over the +definition defaults. Compatible sessions share the initialized native library. +Returned devices, child devices, sites, operations, and jobs retain the state +they need and may outlive the manager. ## Relocatable packages and static consumers -Built-in targets generate manifests beside their runtime libraries in both build -and install trees. Library paths in those fragments contain only the target +Built-in targets generate manifests beside their runtime libraries in 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 MQT Core, not every library loaded by +the process. 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 `qdmi::Driver::registerDevice` and `qdmi::Driver::open`. -No install prefix is compiled into the manifests. +configuration, or construct an explicit registry. An installed MQT Core CMake package provides a helper that colocates selected device libraries and manifests with an executable: @@ -144,7 +139,7 @@ device libraries and manifests with an executable: ```cmake find_package(mqt-core CONFIG REQUIRED) add_executable(my-application main.cpp) -target_link_libraries(my-application PRIVATE MQT::CoreFoMaC) +target_link_libraries(my-application PRIVATE MQT::CoreQDMI) mqt_copy_qdmi_runtime( my-application MQT::CoreQDMINaDevice @@ -154,7 +149,7 @@ mqt_copy_qdmi_runtime( Inside an MQT Core build, omitting the device list copies every device registered through `mqt_configure_qdmi_device`. Installed consumers select the -exported device targets they need, as shown above. +exported device targets they need. An external device implementation does not need MQT Core as a build dependency. It can export its stable ID and prefix as target metadata: diff --git a/docs/qdmi/device_management.md b/docs/qdmi/device_management.md new file mode 100644 index 0000000000..411879ad1c --- /dev/null +++ b/docs/qdmi/device_management.md @@ -0,0 +1,47 @@ +# QDMI device management + +MQT Core discovers QDMI device definitions without loading their native +libraries. {cpp-api:class}`qdmi::DeviceManager` opens a selected device lazily, +applies its independent session parameters, and returns +{cpp-api:class}`qdmi::Device`. Jobs, sites, operations, and child devices share +the underlying library/session lifetime. + +```cpp +#include "qdmi/DeviceManager.hpp" + +qdmi::DeviceManager manager; +for (const auto& definition : manager.definitions()) { + try { + auto device = manager.open(definition.id); + // Use this device independently of the manager. + } catch (const std::exception& error) { + // An unavailable device implementation does not affect other definitions. + } +} +``` + +The equivalent Python API lives in {py:mod}`mqt.core.qdmi`: + +```python +from mqt.core.qdmi import DeviceManager + +manager = DeviceManager() +for definition in manager.definitions: + try: + device = manager.open(definition.device_id) + except RuntimeError as error: + print(f"{definition.device_id}: {error}") + continue + print(device.name()) +``` + +Discovery is side-effect free. Each `open` call loads and initializes only the +selected definition, and separate definitions can share one loaded device +library while retaining independent sessions. + +When every configured device is useful, `openAll()`/`open_all()` returns +successfully opened devices and per-ID errors without allowing one unavailable +device implementation to abort the remaining opens. + +See [QDMI configuration](configuration.md) for discovery, precedence, and +registration examples. diff --git a/docs/qdmi/driver.md b/docs/qdmi/driver.md deleted file mode 100644 index 8c6129c9a8..0000000000 --- a/docs/qdmi/driver.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -file_format: mystnb -kernelspec: - name: python3 -mystnb: - number_source_lines: true ---- - -# MQT Core's QDMI Driver Implementation - -## Objective - -A QDMI Driver manages the communication between QDMI devices, such as -[MQT Core's NA QDMI Device](na_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 - -Standalone MQT Core builds include the DDSIM, superconducting, and neutral-atom -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: - -- {code}`BUILD_MQT_CORE_QDMI_DDSIM_DEVICE` -- {code}`BUILD_MQT_CORE_QDMI_NA_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 driver and FoMaC libraries are available independently. Device-free -builds can register external device libraries through -[QDMI device configuration](configuration.md). When MQT Core's C++ tests are -enabled, device-specific tests are omitted with their corresponding device. - -## Python Bindings - -The QDMI Driver is implemented in C++ and exposed to Python via -[{code}`nanobind`](https://nanobind.readthedocs.io/). Direct binding of the QDMI -Client interface functions is not feasible due to technical limitations. -Instead, a FoMaC (Figure of Merits and Constraints) library defines wrapper -classes ({cpp-api:class}`~fomac::Session`, {cpp-api:class}`~fomac::Device`, -{cpp-api:class}`~fomac::Site`, {cpp-api:class}`~fomac::Operation`, -{cpp-api:class}`~fomac::Job`) for the QDMI entities. These classes together with -their methods are then exposed to Python, see -{py:class}`~mqt.core.fomac.Session`, {py:class}`~mqt.core.fomac.Device`, -{py:class}`~mqt.core.fomac.Device.Site`, -{py:class}`~mqt.core.fomac.Device.Operation`, {py:class}`~mqt.core.fomac.Job`. - -## Usage - -The following example shows how to create a session and get devices from the -QDMI driver. - -```{code-cell} ipython3 -from mqt.core.fomac import Session - -# Create a session to interact with QDMI devices -session = Session() - -# Get a list of all available devices -available_devices = session.get_devices() - -# Print the name of every device -for device in available_devices: - print(device.name()) - -``` diff --git a/docs/qdmi/index.md b/docs/qdmi/index.md index 039bffdd50..c83587dd2f 100644 --- a/docs/qdmi/index.md +++ b/docs/qdmi/index.md @@ -4,7 +4,7 @@ The [Quantum Device Management Interface (QDMI)](https://munich-quantum-software-stack.github.io/QDMI/) provides a standardized interface for describing and interacting with quantum devices. This part of MQT Core contains the implementation of QDMI's different -components, such as a [QDMI driver](driver.md), a +components, such as [QDMI device management](device_management.md), a [QDMI device for Neutral Atom Systems](na_device.md), and a [QDMI device for a Classical Quantum Circuit Simulator](ddsim_device). @@ -14,7 +14,7 @@ components, such as a [QDMI driver](driver.md), a NA QDMI Device DDSIM QDMI Device -QDMI Driver -QDMI device configuration +QDMI Device Management +QDMI Configuration QDMI-Qiskit Backend ``` diff --git a/docs/qdmi/na_device.md b/docs/qdmi/na_device.md index 18b5f4b30d..91c75f7aff 100644 --- a/docs/qdmi/na_device.md +++ b/docs/qdmi/na_device.md @@ -24,6 +24,6 @@ file. The structure of this JSON file is defined by the and deserialize the data using the [nlohmann/json](https://json.nlohmann.me) library. During compilation, this JSON file is parsed and the corresponding C++ code is produced by an application (see `src/na/device/App.cpp`) for the actual -QDMI device implementation. The C++ code is then compiled to a library that can -be used by the QDMI driver. An example instance of a device JSON file can be -found in `json/na/device.json`. +QDMI device implementation. The C++ code is then compiled to a device library +that can be opened by the QDMI device manager. An example device JSON file can +be found in `json/na/device.json`. diff --git a/docs/qdmi/qdmi_backend.md b/docs/qdmi/qdmi_backend.md index 5daa1401da..dd1d461725 100644 --- a/docs/qdmi/qdmi_backend.md +++ b/docs/qdmi/qdmi_backend.md @@ -10,7 +10,7 @@ mystnb: The {py:mod}`mqt.core.plugins.qiskit` module provides a Qiskit {py:class}`~qiskit.providers.BackendV2`-compatible interface to QDMI devices via -FoMaC. This integration allows you to execute Qiskit circuits on QDMI-compliant +QDMI. This integration allows you to execute Qiskit circuits on QDMI-compliant quantum devices using a familiar Qiskit workflow. ## Installation @@ -69,7 +69,7 @@ print(f"Results: {counts}") ### Using the Provider The {py:class}`~mqt.core.plugins.qiskit.provider.QDMIProvider` discovers QDMI -devices available through the FoMaC layer. Backends should always be obtained +devices available through the QDMI layer. Backends should always be obtained through the provider rather than instantiated directly. ```{code-cell} ipython3 @@ -126,8 +126,6 @@ The provider supports multiple authentication methods: authentication - **File-based authentication**: Reading credentials from a file - **URL-based authentication**: Connecting to an authentication server -- **Project-based authentication**: Associating sessions with specific projects, - e.g., for accounting or quota management ### Using Authentication Tokens @@ -162,9 +160,11 @@ backend = provider.get_backend("RemoteQuantumDevice") Store credentials in a secure file for better security: ```python +from pathlib import Path + # Authenticate using a credentials file # The file should contain authentication information in the format expected by the service -provider = QDMIProvider(auth_file="/path/to/credentials.txt") +provider = QDMIProvider(auth_file=Path("/path/to/credentials.txt")) ``` ### Authentication Server URL @@ -176,13 +176,13 @@ Connect to a custom authentication server: provider = QDMIProvider(auth_url="https://auth.quantum-service.com/api/v1/auth") ``` -### Project-Based Authentication +### Device-Specific Session Parameters -Associate your session with a specific project or organization: +QDMI reserves five custom session slots for device-specific settings. Consult +the device documentation to determine their meaning: ```python -# Specify a project ID -provider = QDMIProvider(token="your_api_token", project_id="quantum-research-project-2024") +provider = QDMIProvider(token="your_api_token", custom1="device-project") ``` ### Combining Authentication Parameters @@ -196,29 +196,33 @@ provider = QDMIProvider( token="your_api_token", username="your_username", password="your_password", - project_id="your_project_id", + custom1="device-specific-value", auth_url="https://custom-auth.example.com", ) ``` -### Authentication Error Handling +### Authentication and Device-Opening Errors + +The provider tries to open every configured device independently. Devices that +cannot be opened, for example because their credentials are invalid or missing, +are omitted from `provider.backends()`. Other available devices remain usable. -When authentication fails, the provider raises a `RuntimeError`: +For diagnostics, use the lower-level QDMI manager. Its bulk-open result keeps +the successfully opened devices and records each failure by device ID: ```python -try: - provider = QDMIProvider(token="invalid_token") - backends = provider.backends() -except RuntimeError as e: - print(f"Authentication failed: {e}") - # Handle authentication error (e.g., prompt for valid credentials) +from mqt.core import qdmi + +parameters = qdmi.SessionParameters(token="your_api_token") +result = qdmi.DeviceManager().open_all(session_overrides=parameters) +for device_id, error in result.errors.items(): + print(f"{device_id}: {error}") ``` ## Device Capabilities and Target -The backend automatically introspects the FoMaC (QDMI) device and constructs a -Qiskit {py:class}`~qiskit.transpiler.Target` object describing device -capabilities. +The backend automatically introspects the QDMI device and constructs a Qiskit +{py:class}`~qiskit.transpiler.Target` object describing device capabilities. ```{code-cell} ipython3 # Access device properties via the Target @@ -306,8 +310,8 @@ job = backend.run(circuits, parameter_values=param_values, shots=100) ### Job Status -The {py:class}`~mqt.core.plugins.qiskit.job.QDMIJob` wraps a FoMaC (QDMI) job -and provides status tracking: +The {py:class}`~mqt.core.plugins.qiskit.job.QDMIJob` wraps a QDMI job and +provides status tracking: ```python from qiskit.providers import JobStatus @@ -535,7 +539,7 @@ When you run a circuit, the backend: The backend builds its {py:class}`~qiskit.transpiler.Target` by: -1. Querying the FoMaC (QDMI) device for available operations +1. Querying the QDMI device for available operations 2. Mapping each operation to the corresponding Qiskit gate 3. Determining qubit connectivity from the device's coupling map 4. Including operation properties (duration, fidelity) if available diff --git a/doxygen/namespaces.md b/doxygen/namespaces.md index de41a9065f..def23ac1ed 100644 --- a/doxygen/namespaces.md +++ b/doxygen/namespaces.md @@ -8,9 +8,9 @@ Quantum-circuit representation and algorithms. Decision-diagram data structures and simulation algorithms. -@namespace fomac +@namespace qdmi -C++ interface for FoMaC quantum-device management. +C++ interface for QDMI quantum-device management. @namespace na diff --git a/include/mqt-core/fomac/FoMaC.hpp b/include/mqt-core/fomac/FoMaC.hpp deleted file mode 100644 index 652fffca3e..0000000000 --- a/include/mqt-core/fomac/FoMaC.hpp +++ /dev/null @@ -1,1128 +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 FoMaC.hpp - * @brief FoMaC C++ device-management interface. - */ - -#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 fomac { -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 { -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; - } -} - -[[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 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_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 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 or - * does not carry a generic program payload. - * @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 does not carry a generic - * program payload. - * @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; - - 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)) {} - - /// 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; - } - } - - 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 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 fomac diff --git a/include/mqt-core/na/fomac/Device.hpp b/include/mqt-core/na/fomac/Device.hpp deleted file mode 100644 index 9673012277..0000000000 --- a/include/mqt-core/na/fomac/Device.hpp +++ /dev/null @@ -1,175 +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 Device.hpp - * @brief Neutral-atom FoMaC device session implementation. - */ - -#pragma once - -#include "fomac/FoMaC.hpp" -#include "qdmi/devices/na/Generator.hpp" - -// NOLINTNEXTLINE(misc-include-cleaner) -#include - -#include -#include - -namespace na { -/** - * @brief Class representing the Session library with neutral atom extensions. - * @see fomac::Session - */ -class Session { -public: - /** - * @brief Class representing a quantum device with neutral atom extensions. - * @see fomac::Session::Device - * @note Since it inherits from @ref na::Device, Device objects can be - * converted to `nlohmann::json` objects. - */ - class Device : public fomac::Device, na::Device { - - /** - * @brief Initializes the name from the underlying QDMI device. - */ - auto initNameFromDevice() -> void; - - /** - * @brief Initializes the minimum atom distance from the underlying QDMI - * device. - */ - auto initMinAtomDistanceFromDevice() -> bool; - - /** - * @brief Initializes the number of qubits from the underlying QDMI device. - */ - auto initQubitsNumFromDevice() -> void; - - /** - * @brief Initializes the length unit from the underlying QDMI device. - */ - auto initLengthUnitFromDevice() -> bool; - - /** - * @brief Initializes the duration unit from the underlying QDMI device. - */ - auto initDurationUnitFromDevice() -> bool; - - /** - * @brief Initializes the decoherence times from the underlying QDMI device. - */ - auto initDecoherenceTimesFromDevice() -> bool; - - /** - * @brief Initializes the trap lattices from the underlying QDMI device. - * @details It reconstructs the entire lattice structure from the - * information retrieved from the QDMI device, including lattice vectors, - * sublattice offsets, and extent. - * @see na::Device::Lattice - */ - auto initTrapsfromDevice() -> bool; - - /** - * @brief Initializes the all operations from the underlying QDMI device. - */ - auto initOperationsFromDevice() -> bool; - - /** - * @brief Constructs a Device object from a fomac::Session::Device object. - * @param device The fomac::Session::Device object to wrap. - * @note The constructor does not initialize the additional fields of this - * class. For their initialization, the corresponding `init*FromDevice` - * methods must be called, see @ref tryCreateFromDevice. - */ - explicit Device(const fomac::Device& device) - : fomac::Device(device), na::Device() {}; - - public: - /// @returns the length unit of the device. - [[nodiscard]] auto getLengthUnit() const -> const Unit& { - return lengthUnit; - } - - /// @returns the duration unit of the device. - [[nodiscard]] auto getDurationUnit() const -> const Unit& { - return durationUnit; - } - - /// @returns the decoherence times of the device. - [[nodiscard]] auto getDecoherenceTimes() const -> const DecoherenceTimes& { - return decoherenceTimes; - } - - /// @returns the list of trap lattices of the device. - [[nodiscard]] auto getTraps() const -> const std::vector& { - return traps; - } - - /** - * @brief Try to create a Device object from a fomac::Session::Device - * object. - * @details This method attempts to create a Device object by initializing - * all necessary fields from the provided fomac::Session::Device object. If - * any required information is missing or invalid, the method returns - * `std::nullopt`. - * @param device is the fomac::Session::Device object to wrap. - * @return An optional containing the instantiated device if compatible, - * std::nullopt otherwise. - */ - [[nodiscard]] static auto tryCreateFromDevice(const fomac::Device& device) - -> std::optional { - Device d(device); - // The sequence of the following method calls does not matter. - // They are independent of each other. - if (!d.initMinAtomDistanceFromDevice()) { - return std::nullopt; - } - if (!d.initLengthUnitFromDevice()) { - return std::nullopt; - } - if (!d.initDurationUnitFromDevice()) { - return std::nullopt; - } - if (!d.initDecoherenceTimesFromDevice()) { - return std::nullopt; - } - if (!d.initTrapsfromDevice()) { - return std::nullopt; - } - if (!d.initOperationsFromDevice()) { - return std::nullopt; - } - d.initNameFromDevice(); - d.initQubitsNumFromDevice(); - return d; - } - - // The following is the result of - // NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE_ONLY_SERIALIZE(Device, na::Device) - // without any new attributes, which is the reason the macro cannot be used. - template - friend void to_json(BasicJsonType& nlohmannJsonJ, - const Device& nlohmannJsonT) { - // NOLINTNEXTLINE(misc-include-cleaner) - nlohmann::to_json(nlohmannJsonJ, - static_cast(nlohmannJsonT)); - } - }; - - /// @brief Deleted default constructor to prevent instantiation. - Session() = delete; - - /// @see QDMI_SESSION_PROPERTY_DEVICES - [[nodiscard]] static auto getDevices() -> std::vector; -}; - -} // namespace na diff --git a/include/mqt-core/na/qdmi/Device.hpp b/include/mqt-core/na/qdmi/Device.hpp new file mode 100644 index 0000000000..7f819e6084 --- /dev/null +++ b/include/mqt-core/na/qdmi/Device.hpp @@ -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 + */ + +/** @file Device.hpp + * @brief Neutral-atom QDMI device session implementation. + */ + +#pragma once + +#include "qdmi/Device.hpp" +#include "qdmi/devices/na/Generator.hpp" + +// NOLINTNEXTLINE(misc-include-cleaner) +#include + +#include +#include + +namespace na::qdmi { +/** + * @brief A QDMI device enriched with neutral-atom topology information. + */ +class Device : public ::qdmi::Device, public na::Device { + + /** + * @brief Initializes the name from the underlying QDMI device. + */ + auto initNameFromDevice() -> void; + + /** + * @brief Initializes the minimum atom distance from the underlying QDMI + * device. + */ + auto initMinAtomDistanceFromDevice() -> bool; + + /** + * @brief Initializes the number of qubits from the underlying QDMI device. + */ + auto initQubitsNumFromDevice() -> void; + + /** + * @brief Initializes the length unit from the underlying QDMI device. + */ + auto initLengthUnitFromDevice() -> bool; + + /** + * @brief Initializes the duration unit from the underlying QDMI device. + */ + auto initDurationUnitFromDevice() -> bool; + + /** + * @brief Initializes the decoherence times from the underlying QDMI device. + */ + auto initDecoherenceTimesFromDevice() -> bool; + + /** + * @brief Initializes the trap lattices from the underlying QDMI device. + * @details It reconstructs the entire lattice structure from the + * information retrieved from the QDMI device, including lattice vectors, + * sublattice offsets, and extent. + * @see na::Device::Lattice + */ + auto initTrapsfromDevice() -> bool; + + /** + * @brief Initializes the all operations from the underlying QDMI device. + */ + auto initOperationsFromDevice() -> bool; + + /** + * @brief Constructs a neutral-atom device from a generic QDMI device. + * @param device The generic QDMI device to wrap. + * @note The constructor does not initialize the additional fields of this + * class. For their initialization, the corresponding `init*FromDevice` + * methods must be called, see @ref tryCreateFromDevice. + */ + explicit Device(const ::qdmi::Device& device) + : ::qdmi::Device(device), na::Device() {}; + +public: + /// @returns the length unit of the device. + [[nodiscard]] auto getLengthUnit() const -> const Unit& { return lengthUnit; } + + /// @returns the duration unit of the device. + [[nodiscard]] auto getDurationUnit() const -> const Unit& { + return durationUnit; + } + + /// @returns the decoherence times of the device. + [[nodiscard]] auto getDecoherenceTimes() const -> const DecoherenceTimes& { + return decoherenceTimes; + } + + /// @returns the list of trap lattices of the device. + [[nodiscard]] auto getTraps() const -> const std::vector& { + return traps; + } + + /** + * @brief Try to create a neutral-atom device from a generic QDMI device. + * @details This method attempts to create a Device object by initializing + * all necessary fields from the provided QDMI device. If + * any required information is missing or invalid, the method returns + * `std::nullopt`. + * @param device The generic QDMI device to wrap. + * @return An optional containing the instantiated device if compatible, + * std::nullopt otherwise. + */ + [[nodiscard]] static auto tryCreateFromDevice(const ::qdmi::Device& device) + -> std::optional { + Device d(device); + // The sequence of the following method calls does not matter. + // They are independent of each other. + if (!d.initMinAtomDistanceFromDevice()) { + return std::nullopt; + } + if (!d.initLengthUnitFromDevice()) { + return std::nullopt; + } + if (!d.initDurationUnitFromDevice()) { + return std::nullopt; + } + if (!d.initDecoherenceTimesFromDevice()) { + return std::nullopt; + } + if (!d.initTrapsfromDevice()) { + return std::nullopt; + } + if (!d.initOperationsFromDevice()) { + return std::nullopt; + } + d.initNameFromDevice(); + d.initQubitsNumFromDevice(); + return d; + } + + // The following is the result of + // NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE_ONLY_SERIALIZE(Device, na::Device) + // without any new attributes, which is the reason the macro cannot be used. + template + friend void to_json(BasicJsonType& nlohmannJsonJ, + const Device& nlohmannJsonT) { + // NOLINTNEXTLINE(misc-include-cleaner) + nlohmann::to_json(nlohmannJsonJ, + static_cast(nlohmannJsonT)); + } +}; + +} // namespace na::qdmi diff --git a/include/mqt-core/qdmi/Device.hpp b/include/mqt-core/qdmi/Device.hpp new file mode 100644 index 0000000000..646579fe87 --- /dev/null +++ b/include/mqt-core/qdmi/Device.hpp @@ -0,0 +1,313 @@ +/* + * 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 + +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, +}; + +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 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; + + 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 or + * does not carry a generic program payload. + */ + [[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 exactly as provided. + * @throws std::invalid_argument If the format does not carry a generic + * program payload. + */ + [[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; + + [[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; + 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; + + 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..24a80e4708 --- /dev/null +++ b/include/mqt-core/qdmi/DeviceManager.hpp @@ -0,0 +1,62 @@ +/* + * 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 Lazy opening and lifetime management for configured QDMI devices. + +#pragma once + +#include "qdmi/Device.hpp" +#include "qdmi/DeviceRegistry.hpp" + +#include +#include +#include +#include + +namespace qdmi { + +/// Result of independently opening every enabled definition. +struct OpenAllResult { + /// Successfully opened devices keyed by stable device ID. + std::map devices; + /// Error messages for definitions that could not be opened, keyed by ID. + std::map errors; +}; + +/// Lazily opens configured QDMI devices. +/// +/// Each call to @ref open creates an independent device session. Returned +/// devices and objects derived from them own the library and session state they +/// require and may outlive the manager. +class DeviceManager { +public: + DeviceManager(); + explicit DeviceManager(DeviceRegistry registry); + + /// Returns the definitions owned by this manager. + [[nodiscard]] const std::vector& definitions() const { + return registry_.definitions(); + } + + /// Opens one configured device by stable ID. + [[nodiscard]] Device + open(std::string_view id, + const SessionParameters& sessionOverrides = SessionParameters{}) const; + + /// Opens a snapshot of all definitions and isolates failures by ID. + [[nodiscard]] OpenAllResult openAll( + const SessionParameters& sessionOverrides = SessionParameters{}) const; + +private: + DeviceRegistry registry_; +}; + +} // namespace qdmi diff --git a/include/mqt-core/qdmi/DeviceRegistry.hpp b/include/mqt-core/qdmi/DeviceRegistry.hpp new file mode 100644 index 0000000000..3b2ecb2051 --- /dev/null +++ b/include/mqt-core/qdmi/DeviceRegistry.hpp @@ -0,0 +1,89 @@ +/* + * 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 +#include +#include +#include +#include + +namespace qdmi { + +/// Parameters applied to one device session before it is initialized. +struct SessionParameters { + /// Base URL of the device service. + std::optional baseUrl; + /// Authentication token. + std::optional token; + /// Authentication file path. + std::optional authFile; + /// Authentication service URL. + std::optional authUrl; + /// Authentication username. + std::optional username; + /// Authentication password. + std::optional password; + /// First implementation-defined parameter. + std::optional custom1; + /// Second implementation-defined parameter. + std::optional custom2; + /// Third implementation-defined parameter. + std::optional custom3; + /// Fourth implementation-defined parameter. + std::optional custom4; + /// Fifth implementation-defined parameter. + std::optional custom5; +}; + +/// A side-effect-free registration for one QDMI device. +struct DeviceDefinition { + /// Stable device identifier. + std::string id; + /// Path to the QDMI device library. + std::filesystem::path library; + /// Symbol prefix used by the QDMI device library. + std::string prefix; + /// Default session parameters. + SessionParameters session; +}; + +/// Discovers and merges QDMI device definitions without loading libraries. +class DeviceRegistry { +public: + /// Discovers definitions from the standard configuration sources. + DeviceRegistry(); + + /// Creates a registry from explicit definitions without configuration + /// discovery. + explicit DeviceRegistry(std::vector definitions); + + /// Returns enabled definitions in stable registration order. + [[nodiscard]] const std::vector& definitions() const { + return definitions_; + } + + /// Registers a complete definition, optionally replacing an existing or + /// explicitly disabled ID. + void registerDevice(DeviceDefinition definition, bool replace = false); + + /// Registers a definition unless its ID already exists or is disabled. + [[nodiscard]] bool registerDeviceIfAbsent(DeviceDefinition definition); + +private: + std::vector definitions_; + std::unordered_set disabledIds_; +}; + +} // namespace qdmi diff --git a/include/mqt-core/qdmi/common/Common.hpp b/include/mqt-core/qdmi/common/Common.hpp index 0e21332eea..5127fb4fec 100644 --- a/include/mqt-core/qdmi/common/Common.hpp +++ b/include/mqt-core/qdmi/common/Common.hpp @@ -14,7 +14,7 @@ #pragma once -#include +#include #include @@ -180,58 +180,6 @@ constexpr auto toString(const QDMI_STATUS result) -> const char* { */ auto throwIfError(int result, const std::string& msg) -> void; -/// Returns the string representation of the given session parameter @p param. -constexpr auto toString(const QDMI_Session_Parameter param) -> const char* { - switch (param) { - case QDMI_SESSION_PARAMETER_TOKEN: - return "TOKEN"; - case QDMI_SESSION_PARAMETER_AUTHFILE: - return "AUTH FILE"; - case QDMI_SESSION_PARAMETER_AUTHURL: - return "AUTH URL"; - case QDMI_SESSION_PARAMETER_USERNAME: - return "USERNAME"; - case QDMI_SESSION_PARAMETER_PASSWORD: - return "PASSWORD"; - case QDMI_SESSION_PARAMETER_PROJECTID: - return "PROJECT ID"; - case QDMI_SESSION_PARAMETER_MAX: - return "MAX"; - case QDMI_SESSION_PARAMETER_CUSTOM1: - return "CUSTOM1"; - case QDMI_SESSION_PARAMETER_CUSTOM2: - return "CUSTOM2"; - case QDMI_SESSION_PARAMETER_CUSTOM3: - return "CUSTOM3"; - case QDMI_SESSION_PARAMETER_CUSTOM4: - return "CUSTOM4"; - case QDMI_SESSION_PARAMETER_CUSTOM5: - return "CUSTOM5"; - } - unreachable(); -} - -/// Returns the string representation of the given session property @p prop. -constexpr auto toString(const QDMI_Session_Property prop) -> const char* { - switch (prop) { - case QDMI_SESSION_PROPERTY_DEVICES: - return "DEVICES"; - case QDMI_SESSION_PROPERTY_MAX: - return "MAX"; - case QDMI_SESSION_PROPERTY_CUSTOM1: - return "CUSTOM1"; - case QDMI_SESSION_PROPERTY_CUSTOM2: - return "CUSTOM2"; - case QDMI_SESSION_PROPERTY_CUSTOM3: - return "CUSTOM3"; - case QDMI_SESSION_PROPERTY_CUSTOM4: - return "CUSTOM4"; - case QDMI_SESSION_PROPERTY_CUSTOM5: - return "CUSTOM5"; - } - unreachable(); -} - /// Returns the string representation of the given device session parameter /// @p param. constexpr auto toString(const QDMI_Device_Session_Parameter param) -> const diff --git a/include/mqt-core/qdmi/driver/Driver.hpp b/include/mqt-core/qdmi/driver/Driver.hpp deleted file mode 100644 index 1ac0b9b02a..0000000000 --- a/include/mqt-core/qdmi/driver/Driver.hpp +++ /dev/null @@ -1,511 +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 - -namespace fomac { -class Session; -} - -namespace qdmi { - -/** - * @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; - /// 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_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 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 fomac::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 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/noxfile.py b/noxfile.py index c851959af8..01cede51ad 100755 --- a/noxfile.py +++ b/noxfile.py @@ -207,6 +207,9 @@ def stubs(session: nox.Session) -> None: package_root = Path(__file__).parent / "python" / "mqt" / "core" + modules = ["mqt.core.ir", "mqt.core.dd", "mqt.core.qdmi", "mqt.core.mlir", "mqt.core.na"] + module_args = [arg for module in modules for arg in ("--module", module)] + session.run( "python", "-m", @@ -215,16 +218,7 @@ def stubs(session: nox.Session) -> None: "--include-private", "--output-dir", str(package_root), - "--module", - "mqt.core.ir", - "--module", - "mqt.core.dd", - "--module", - "mqt.core.fomac", - "--module", - "mqt.core.mlir", - "--module", - "mqt.core.na", + *module_args, "--pattern-file", "bindings/patterns.txt", ) diff --git a/pyproject.toml b/pyproject.toml index 243f0ce39f..a949177eaa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -237,7 +237,6 @@ convention = "google" default.extend-ignore-re = [ "(?Rm)^.*(#|//)\\s*spellchecker:disable-line$", # ignore line "(?s)(#|//)\\s*spellchecker:off.*?\\n\\s*(#|//)\\s*spellchecker:on", # ignore block - "FoMaC" ] [tool.typos.default.extend-words] @@ -248,6 +247,9 @@ ket = "ket" optin = "optin" nd = "nd" +[tool.typos.default.extend-identifiers] +FoMaC = "FoMaC" + [tool.repo-review.ignore] GH200 = "We use Renovate instead of Dependabot" diff --git a/python/mqt/core/fomac.pyi b/python/mqt/core/fomac.pyi deleted file mode 100644 index 0c3e77df02..0000000000 --- a/python/mqt/core/fomac.pyi +++ /dev/null @@ -1,658 +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 - -import enum -import os -import pathlib -from collections.abc import Sequence -from typing import overload - -class Session: - """A FoMaC session for managing QDMI devices. - - Allows creating isolated sessions with separate authentication settings. - All authentication parameters are optional and can be provided as keyword arguments to the constructor. - """ - - def __init__( - self, - *, - token: str | None = None, - auth_file: str | os.PathLike | None = None, - auth_url: str | None = None, - username: str | None = None, - password: str | None = None, - project_id: str | None = None, - custom1: str | None = None, - custom2: str | None = None, - custom3: str | None = None, - custom4: str | None = None, - custom5: str | None = None, - ) -> None: - """Create a new FoMaC session with optional authentication. - - Args: - token: Authentication token - auth_file: Path to file containing authentication information - auth_url: URL to authentication server - username: Username for authentication - password: Password for authentication - project_id: Project ID for session - custom1: Custom configuration parameter 1 - custom2: Custom configuration parameter 2 - custom3: Custom configuration parameter 3 - custom4: Custom configuration parameter 4 - custom5: Custom configuration parameter 5 - - Raises: - RuntimeError: If auth_file does not exist - RuntimeError: If auth_url has invalid format - - Example: - >>> from mqt.core.fomac import Session - >>> # Session without authentication - >>> session = Session() - >>> devices = session.get_devices() - >>> - >>> # Session with token authentication - >>> session = Session(token="my_secret_token") - >>> devices = session.get_devices() - >>> - >>> # Session with file-based authentication - >>> session = Session(auth_file="/path/to/auth.json") - >>> devices = session.get_devices() - >>> - >>> # Session with multiple parameters - >>> session = Session( - ... auth_url="https://auth.example.com", username="user", password="pass", project_id="project-123" - ... ) - >>> devices = session.get_devices() - """ - - def get_devices(self) -> list[Device]: - """Get available devices from this session. - - Returns: - List of available devices. - """ - -class Job: - """A job represents a submitted quantum program execution.""" - - def check(self) -> Status: - """Returns the current status of the job.""" - - def wait(self, timeout: int = 0) -> bool: - """Waits for the job to complete. - - Args: - timeout: The maximum time to wait in seconds. If 0, waits indefinitely. - - Returns: - True if the job completed within the timeout, False otherwise. - """ - - def cancel(self) -> None: - """Cancels the job.""" - - def get_shots(self) -> list[str]: - """Returns the raw shot results from the job.""" - - def get_counts(self) -> dict[str, int]: - """Returns the measurement counts from the job.""" - - def get_dense_statevector(self) -> list[complex]: - """Returns the dense statevector from the job (typically only available from simulator devices).""" - - def get_dense_probabilities(self) -> list[float]: - """Returns the dense probabilities from the job (typically only available from simulator devices).""" - - def get_sparse_statevector(self) -> dict[str, complex]: - """Returns the sparse statevector from the job (typically only available from simulator devices).""" - - def get_sparse_probabilities(self) -> dict[str, float]: - """Returns the sparse probabilities from the job (typically only available from simulator devices).""" - - @overload - def query_custom_property(self, custom_property: CustomProperty, value_type: type[str]) -> str | None: ... - @overload - def query_custom_property(self, custom_property: CustomProperty, value_type: type[bool]) -> bool | None: ... - @overload - def query_custom_property(self, custom_property: CustomProperty, value_type: type[int]) -> int | None: ... - @overload - def query_custom_property(self, custom_property: CustomProperty, value_type: type[float]) -> float | None: ... - @overload - def query_custom_property(self, custom_property: CustomProperty, value_type: type[bytes]) -> bytes | None: ... - @overload - def query_custom_property( - self, custom_property: CustomProperty, value_type: type[str | bool | int | float | bytes] - ) -> str | bool | int | float | bytes | None: - """Query an implementation-defined custom job property. - - The caller must provide the type documented by the device implementation. - Use ``bytes`` to retrieve the value without interpretation. Returns ``None`` - when the custom slot is unsupported. - """ - - @overload - def get_custom_result(self, custom_property: CustomProperty, value_type: type[str]) -> str | None: ... - @overload - def get_custom_result(self, custom_property: CustomProperty, value_type: type[bool]) -> bool | None: ... - @overload - def get_custom_result(self, custom_property: CustomProperty, value_type: type[int]) -> int | None: ... - @overload - def get_custom_result(self, custom_property: CustomProperty, value_type: type[float]) -> float | None: ... - @overload - def get_custom_result(self, custom_property: CustomProperty, value_type: type[bytes]) -> bytes | None: ... - @overload - def get_custom_result( - self, custom_property: CustomProperty, value_type: type[str | bool | int | float | bytes] - ) -> str | bool | int | float | bytes | None: - """Return an implementation-defined custom job result. - - The caller must provide the type documented by the device implementation. - Use ``bytes`` to retrieve the value without interpretation. Returns ``None`` - when the custom slot is unsupported. - """ - - @property - def id(self) -> str: - """The job ID.""" - - @property - def program_format(self) -> ProgramFormat: - """The format of the submitted program.""" - - @property - def program(self) -> str: - """The submitted program.""" - - @property - def program_bytes(self) -> bytes: - """The exact bytes of the submitted program.""" - - @property - def num_shots(self) -> int: - """The number of shots.""" - - def __eq__(self, arg: object, /) -> bool: ... - def __ne__(self, arg: object, /) -> bool: ... - - class Status(enum.Enum): - """Enumeration of job status.""" - - CREATED = 0 - - SUBMITTED = 1 - - QUEUED = 2 - - RUNNING = 3 - - DONE = 4 - - CANCELED = 5 - - FAILED = 6 - -class ProgramFormat(enum.Enum): - """Enumeration of program formats.""" - - QASM2 = 0 - - QASM3 = 1 - - QIR_BASE_STRING = 2 - - QIR_BASE_MODULE = 3 - - QIR_ADAPTIVE_STRING = 4 - - QIR_ADAPTIVE_MODULE = 5 - - CALIBRATION = 6 - - QPY = 7 - - IQM_JSON = 8 - - BATCH_JOB = 9 - - CUSTOM1 = 999999995 - - CUSTOM2 = 999999996 - - CUSTOM3 = 999999997 - - CUSTOM4 = 999999998 - - CUSTOM5 = 999999999 - -class CustomProperty(enum.Enum): - """An implementation-defined custom property or result slot.""" - - CUSTOM1 = 1 - - CUSTOM2 = 2 - - CUSTOM3 = 3 - - CUSTOM4 = 4 - - CUSTOM5 = 5 - -class Device: - """A device represents a quantum device with its properties and capabilities.""" - - class Status(enum.Enum): - """Enumeration of device status.""" - - OFFLINE = 0 - - IDLE = 1 - - BUSY = 2 - - ERROR = 3 - - MAINTENANCE = 4 - - CALIBRATION = 5 - - def name(self) -> str: - """Returns the name of the device.""" - - def version(self) -> str: - """Returns the version of the device.""" - - def status(self) -> Status: - """Returns the current status of the device.""" - - def library_version(self) -> str: - """Returns the version of the library used to define the device.""" - - def qubits_num(self) -> int: - """Returns the number of qubits available on the device.""" - - def sites(self) -> list[Site]: - """Returns the list of all sites (zone and regular sites) available on the device.""" - - def regular_sites(self) -> list[Site]: - """Returns the list of regular sites (without zone sites) available on the device.""" - - def zones(self) -> list[Site]: - """Returns the list of zone sites (without regular sites) available on the device.""" - - def operations(self) -> list[Operation]: - """Returns the list of operations supported by the device.""" - - def coupling_map(self) -> list[tuple[Site, Site]] | None: - """Returns the coupling map of the device as a list of site pairs.""" - - def needs_calibration(self) -> int | None: - """Returns whether the device needs calibration.""" - - def length_unit(self) -> str | None: - """Returns the unit of length used by the device.""" - - def length_scale_factor(self) -> float | None: - """Returns the scale factor for length used by the device.""" - - def duration_unit(self) -> str | None: - """Returns the unit of duration used by the device.""" - - def duration_scale_factor(self) -> float | None: - """Returns the scale factor for duration used by the device.""" - - def min_atom_distance(self) -> int | None: - """Returns the minimum atom distance on the device.""" - - def supported_program_formats(self) -> list[ProgramFormat]: - """Returns the list of program formats supported by the device.""" - - def child_devices(self) -> list[Device]: - """Returns the direct child devices managed by this device.""" - - @overload - def query_custom_property(self, custom_property: CustomProperty, value_type: type[str]) -> str | None: ... - @overload - def query_custom_property(self, custom_property: CustomProperty, value_type: type[bool]) -> bool | None: ... - @overload - def query_custom_property(self, custom_property: CustomProperty, value_type: type[int]) -> int | None: ... - @overload - def query_custom_property(self, custom_property: CustomProperty, value_type: type[float]) -> float | None: ... - @overload - def query_custom_property(self, custom_property: CustomProperty, value_type: type[bytes]) -> bytes | None: ... - @overload - def query_custom_property( - self, custom_property: CustomProperty, value_type: type[str | bool | int | float | bytes] - ) -> str | bool | int | float | bytes | None: - """Query an implementation-defined custom device property. - - The caller must provide the type documented by the device implementation. - Use ``bytes`` to retrieve the value without interpretation. Returns ``None`` - when the custom slot is unsupported. - """ - - @overload - def submit_job( - self, - program: str, - program_format: ProgramFormat, - num_shots: int, - *, - custom1: str | bool | float | None = None, - custom2: str | bool | float | None = None, - custom3: str | bool | float | None = None, - custom4: str | bool | float | None = None, - custom5: str | bool | float | None = None, - ) -> Job: - """Submits a text job to the device.""" - - @overload - def submit_job( - self, - program: bytes, - program_format: ProgramFormat, - num_shots: int, - *, - custom1: str | bool | float | None = None, - custom2: str | bool | float | None = None, - custom3: str | bool | float | None = None, - custom4: str | bool | float | None = None, - custom5: str | bool | float | None = None, - ) -> Job: - """Submits an exact byte payload to the device.""" - - def __eq__(self, arg: object, /) -> bool: ... - def __ne__(self, arg: object, /) -> bool: ... - - class Site: - """A site represents a potential qubit location on a quantum device.""" - - def index(self) -> int: - """Returns the index of the site.""" - - def t1(self) -> int | None: - """Returns the T1 coherence time of the site.""" - - def t2(self) -> int | None: - """Returns the T2 coherence time of the site.""" - - def name(self) -> str | None: - """Returns the name of the site.""" - - def x_coordinate(self) -> int | None: - """Returns the x coordinate of the site.""" - - def y_coordinate(self) -> int | None: - """Returns the y coordinate of the site.""" - - def z_coordinate(self) -> int | None: - """Returns the z coordinate of the site.""" - - def is_zone(self) -> bool: - """Returns whether the site is a zone.""" - - def x_extent(self) -> int | None: - """Returns the x extent of the site.""" - - def y_extent(self) -> int | None: - """Returns the y extent of the site.""" - - def z_extent(self) -> int | None: - """Returns the z extent of the site.""" - - def module_index(self) -> int | None: - """Returns the index of the module the site belongs to.""" - - def submodule_index(self) -> int | None: - """Returns the index of the submodule the site belongs to.""" - - @overload - def query_custom_property(self, custom_property: CustomProperty, value_type: type[str]) -> str | None: ... - @overload - def query_custom_property(self, custom_property: CustomProperty, value_type: type[bool]) -> bool | None: ... - @overload - def query_custom_property(self, custom_property: CustomProperty, value_type: type[int]) -> int | None: ... - @overload - def query_custom_property(self, custom_property: CustomProperty, value_type: type[float]) -> float | None: ... - @overload - def query_custom_property(self, custom_property: CustomProperty, value_type: type[bytes]) -> bytes | None: ... - @overload - def query_custom_property( - self, custom_property: CustomProperty, value_type: type[str | bool | int | float | bytes] - ) -> str | bool | int | float | bytes | None: - """Query an implementation-defined custom site property. - - The caller must provide the type documented by the device implementation. - Use ``bytes`` to retrieve the value without interpretation. Returns ``None`` - when the custom slot is unsupported. - """ - - def __eq__(self, arg: object, /) -> bool: ... - def __ne__(self, arg: object, /) -> bool: ... - - class Operation: - """An operation represents a quantum operation that can be performed on a quantum device.""" - - def name(self, sites: Sequence[Device.Site] = ..., params: Sequence[float] = ...) -> str: - """Returns the name of the operation.""" - - def qubits_num(self, sites: Sequence[Device.Site] = ..., params: Sequence[float] = ...) -> int | None: - """Returns the number of qubits the operation acts on.""" - - def parameters_num(self, sites: Sequence[Device.Site] = ..., params: Sequence[float] = ...) -> int: - """Returns the number of parameters the operation has.""" - - def duration(self, sites: Sequence[Device.Site] = ..., params: Sequence[float] = ...) -> int | None: - """Returns the duration of the operation.""" - - def fidelity(self, sites: Sequence[Device.Site] = ..., params: Sequence[float] = ...) -> float | None: - """Returns the fidelity of the operation.""" - - def interaction_radius(self, sites: Sequence[Device.Site] = ..., params: Sequence[float] = ...) -> int | None: - """Returns the interaction radius of the operation.""" - - def blocking_radius(self, sites: Sequence[Device.Site] = ..., params: Sequence[float] = ...) -> int | None: - """Returns the blocking radius of the operation.""" - - def idling_fidelity(self, sites: Sequence[Device.Site] = ..., params: Sequence[float] = ...) -> float | None: - """Returns the idling fidelity of the operation.""" - - def is_zoned(self) -> bool: - """Returns whether the operation is zoned.""" - - def sites(self) -> list[Device.Site] | None: - """Returns the list of sites the operation can be performed on.""" - - def site_pairs(self) -> list[tuple[Device.Site, Device.Site]] | None: - """Returns the list of site pairs the local 2-qubit operation can be performed on.""" - - def mean_shuttling_speed(self, sites: Sequence[Device.Site] = ..., params: Sequence[float] = ...) -> int | None: - """Returns the mean shuttling speed of the operation.""" - - @overload - def query_custom_property( - self, - custom_property: CustomProperty, - value_type: type[str], - sites: Sequence[Device.Site] = ..., - params: Sequence[float] = ..., - ) -> str | None: ... - @overload - def query_custom_property( - self, - custom_property: CustomProperty, - value_type: type[bool], - sites: Sequence[Device.Site] = ..., - params: Sequence[float] = ..., - ) -> bool | None: ... - @overload - def query_custom_property( - self, - custom_property: CustomProperty, - value_type: type[int], - sites: Sequence[Device.Site] = ..., - params: Sequence[float] = ..., - ) -> int | None: ... - @overload - def query_custom_property( - self, - custom_property: CustomProperty, - value_type: type[float], - sites: Sequence[Device.Site] = ..., - params: Sequence[float] = ..., - ) -> float | None: ... - @overload - def query_custom_property( - self, - custom_property: CustomProperty, - value_type: type[bytes], - sites: Sequence[Device.Site] = ..., - params: Sequence[float] = ..., - ) -> bytes | None: ... - @overload - def query_custom_property( - self, - custom_property: CustomProperty, - value_type: type[str | bool | int | float | bytes], - sites: Sequence[Device.Site] = ..., - params: Sequence[float] = ..., - ) -> str | bool | int | float | bytes | None: - """Query an implementation-defined custom operation property. - - The caller must provide the type documented by the device implementation. - Use ``bytes`` to retrieve the value without interpretation. Returns ``None`` - when the custom slot is unsupported. - """ - - def __eq__(self, arg: object, /) -> bool: ... - def __ne__(self, arg: object, /) -> bool: ... - -class DeviceDefinition: - """A stable QDMI device registration that can be stored before loading.""" - - def __init__( - self, - device_id: str, - library_path: str | os.PathLike, - prefix: 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, - custom1: str | None = None, - custom2: str | None = None, - custom3: str | None = None, - custom4: str | None = None, - custom5: str | None = None, - ) -> None: - """Create a device definition without loading its native library. - - Args: - device_id: Stable identifier used by :func:`open_device`. - library_path: Path to the shared QDMI device library. - prefix: Function prefix used by the library (for example, ``MY_DEVICE``). - base_url: Optional base URL for the device API endpoint. - token: Optional authentication token. - auth_file: Optional path to an authentication file. - auth_url: Optional authentication server URL. - username: Optional authentication username. - password: Optional authentication password. - custom1: Optional custom configuration parameter 1. - custom2: Optional custom configuration parameter 2. - custom3: Optional custom configuration parameter 3. - custom4: Optional custom configuration parameter 4. - custom5: Optional custom configuration parameter 5. - """ - - @property - def device_id(self) -> str: - """Stable identifier used to open the device.""" - - @property - def library_path(self) -> pathlib.Path: - """Path to the native QDMI device library.""" - - @property - def prefix(self) -> str: - """Prefix used for the QDMI device interface functions.""" - -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. - - 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: - """Register a valid QDMI device definition if its ID is absent. - - Existing and explicitly disabled IDs are not inserted. Invalid definitions - still raise. - - Args: - definition: Definition to validate and store. - - Returns: - bool: Whether the definition was inserted. - - Raises: - ValueError: If the definition is invalid. - """ - -def open_device( - 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, - custom1: str | None = None, - custom2: str | None = None, - custom3: str | None = None, - custom4: str | None = None, - custom5: str | None = None, -) -> Device: - """Open a registered QDMI device by stable ID. - - Every call creates a fresh device session while keeping the stable registration - unchanged. Opening the device loads trusted native device code. - - Args: - device_id: Stable ID of a registered device. - base_url: Optional base URL override for the device API endpoint. - token: Optional authentication token override. - auth_file: Optional authentication-file override. - auth_url: Optional authentication server URL override. - username: Optional authentication username override. - password: Optional authentication password override. - custom1: Optional custom configuration parameter 1 override. - custom2: Optional custom configuration parameter 2 override. - custom3: Optional custom configuration parameter 3 override. - custom4: Optional custom configuration parameter 4 override. - custom5: Optional custom configuration parameter 5 override. - - Returns: - Device: The opened device, ready for direct backend construction. - - Raises: - IndexError: If the ID is not registered. - RuntimeError: If the device library cannot be loaded or initialized. - """ diff --git a/python/mqt/core/na/__init__.pyi b/python/mqt/core/na/__init__.pyi index 8318d27c44..13efca6f0e 100644 --- a/python/mqt/core/na/__init__.pyi +++ b/python/mqt/core/na/__init__.pyi @@ -11,4 +11,4 @@ This module contains all neutral atom related functionality of MQT Core. """ -from . import fomac as fomac +from . import qdmi as qdmi diff --git a/python/mqt/core/na/fomac.pyi b/python/mqt/core/na/qdmi.pyi similarity index 87% rename from python/mqt/core/na/fomac.pyi rename to python/mqt/core/na/qdmi.pyi index 897467bfc1..13b08dab9d 100644 --- a/python/mqt/core/na/fomac.pyi +++ b/python/mqt/core/na/qdmi.pyi @@ -8,9 +8,9 @@ """Reconstruction of NADevice from QDMI's Device class.""" -import mqt.core.fomac +import mqt.core.qdmi -class Device(mqt.core.fomac.Device): +class Device(mqt.core.qdmi.Device): """Represents a device with a lattice of traps.""" class Lattice: @@ -94,18 +94,15 @@ class Device(mqt.core.fomac.Device): """The T2 time of the device.""" @staticmethod - def try_create_from_device(device: mqt.core.fomac.Device) -> Device | None: - """Create NA FoMaC Device from generic FoMaC Device. + def try_create_from_device(device: mqt.core.qdmi.Device) -> Device | None: + """Create NA QDMI Device from generic QDMI Device. Args: - device: The generic FoMaC Device to convert. + device: The generic QDMI Device to convert. Returns: - The converted NA FoMaC Device or None if the conversion is not possible. + The converted NA QDMI Device or None if the conversion is not possible. """ def __eq__(self, arg: object, /) -> bool: ... def __ne__(self, arg: object, /) -> bool: ... - -def devices() -> list[Device]: - """Returns a list of available devices.""" diff --git a/python/mqt/core/plugins/qiskit/backend.py b/python/mqt/core/plugins/qiskit/backend.py index df131e7911..03a560ff0b 100644 --- a/python/mqt/core/plugins/qiskit/backend.py +++ b/python/mqt/core/plugins/qiskit/backend.py @@ -8,7 +8,7 @@ """QDMI Qiskit Backend. -Provides a Qiskit BackendV2-compatible interface to QDMI devices via FoMaC. +Provides a Qiskit BackendV2-compatible interface to QDMI devices. """ from __future__ import annotations @@ -28,7 +28,7 @@ from qiskit.providers import BackendV2, Options from qiskit.transpiler import InstructionProperties, Target -from ... import fomac +from ... import qdmi from .converters import qiskit_to_iqm_json from .exceptions import ( CircuitValidationError, @@ -104,7 +104,7 @@ def _build_gate_mappings_for_backend( class QDMIBackend(BackendV2): - """A Qiskit BackendV2 adapter for QDMI devices via FoMaC. + """A Qiskit BackendV2 adapter for QDMI devices. This backend provides program submission to QDMI devices. It automatically introspects device capabilities and constructs a @@ -115,7 +115,7 @@ class QDMIBackend(BackendV2): rather than instantiated directly. Args: - device: FoMaC device to wrap. + device: QDMI device to wrap. provider: The provider instance that created this backend. Examples: @@ -127,7 +127,7 @@ class QDMIBackend(BackendV2): """ @staticmethod - def is_convertible(device: fomac.Device) -> bool: + def is_convertible(device: qdmi.Device) -> bool: """Returns whether a device can be represented in Qiskit's Target model.""" # Zoned operations cannot easily be represented in Qiskit's Target model return not any(op.is_zoned() for op in device.operations()) @@ -167,11 +167,11 @@ def is_convertible(device: fomac.Device) -> bool: # Initialize derived mappings at class definition time _QISKIT_TO_QDMI_GATE_MAP, _OPERATION_TO_GATE_MAP = _build_gate_mappings_for_backend(_GATE_ALIASES) - def __init__(self, device: fomac.Device, provider: QDMIProvider | None = None) -> None: - """Initialize the backend with a FoMaC device. + def __init__(self, device: qdmi.Device, provider: QDMIProvider | None = None) -> None: + """Initialize the backend with a QDMI device. Args: - device: FoMaC device instance. + device: QDMI device instance. provider: Provider instance that created this backend. Raises: @@ -258,7 +258,7 @@ def _build_target(self) -> Target: return target def _add_operation_to_target( - self, target: Target, op: fomac.Device.Operation, seen_gate_names: MutableSet[str] + self, target: Target, op: qdmi.Device.Operation, seen_gate_names: MutableSet[str] ) -> None: """Add a single device operation to the Target, if it maps to a Qiskit gate. @@ -384,7 +384,7 @@ def _map_qiskit_gate_to_operation_names(qiskit_gate_name: str) -> set[str]: """ return QDMIBackend._QISKIT_TO_QDMI_GATE_MAP.get(qiskit_gate_name.lower(), {qiskit_gate_name.lower()}) - def _get_operation_qargs(self, op: fomac.Device.Operation) -> list[tuple[int]] | list[tuple[int, int]] | list[None]: + def _get_operation_qargs(self, op: qdmi.Device.Operation) -> list[tuple[int]] | list[tuple[int, int]] | list[None]: """Get the qubit argument tuples for an operation. This method determines which qubit indices an operation can act on by: @@ -396,7 +396,7 @@ def _get_operation_qargs(self, op: fomac.Device.Operation) -> list[tuple[int]] | - Multi-qubit (3+): Assumed to be globally available Args: - op: Device operation from FoMaC. + op: Device operation from QDMI. Returns: Sequence of qubit index tuples this operation can act on. @@ -461,8 +461,8 @@ def _preprocess_circuit(self, circuit: QuantumCircuit) -> QuantumCircuit: # ruf return circuit def _convert_circuit( - self, circuit: QuantumCircuit, supported_program_formats: Iterable[fomac.ProgramFormat] - ) -> tuple[str, fomac.ProgramFormat]: + self, circuit: QuantumCircuit, supported_program_formats: Iterable[qdmi.ProgramFormat] + ) -> tuple[str, qdmi.ProgramFormat]: """Convert a :class:`~qiskit.circuit.QuantumCircuit` to one of the supported program formats. The conversion priority order is: @@ -487,9 +487,9 @@ def _convert_circuit( raise UnsupportedFormatError(msg) # Try IQM JSON format first (device-specific) - if fomac.ProgramFormat.IQM_JSON in supported_program_formats: + if qdmi.ProgramFormat.IQM_JSON in supported_program_formats: try: - return qiskit_to_iqm_json(circuit, self._device), fomac.ProgramFormat.IQM_JSON + return qiskit_to_iqm_json(circuit, self._device), qdmi.ProgramFormat.IQM_JSON except UnsupportedOperationError: # Let this propagate so caller can handle fallback raise @@ -498,7 +498,7 @@ def _convert_circuit( raise TranslationError(msg) from exc # Try OpenQASM3 - if fomac.ProgramFormat.QASM3 in supported_program_formats: + if qdmi.ProgramFormat.QASM3 in supported_program_formats: # Qiskit's OpenQASM3 exporter is fairly limited in terms of which gates it supports natively. # So it needs some help from us. exclusion_list = set() @@ -549,15 +549,15 @@ def _convert_circuit( basis_gates = [gate for gate in self.target.operation_names if gate not in exclusion_list] + ["U"] try: - return qasm3.dumps(circuit, basis_gates=basis_gates), fomac.ProgramFormat.QASM3 + return qasm3.dumps(circuit, basis_gates=basis_gates), qdmi.ProgramFormat.QASM3 except Exception as exc: msg = f"Failed to convert circuit to QASM3: {exc}" raise TranslationError(msg) from exc # Try OpenQASM2 (legacy) - if fomac.ProgramFormat.QASM2 in supported_program_formats: + if qdmi.ProgramFormat.QASM2 in supported_program_formats: try: - return qasm2.dumps(circuit), fomac.ProgramFormat.QASM2 + return qasm2.dumps(circuit), qdmi.ProgramFormat.QASM2 except Exception as exc: msg = f"Failed to convert circuit to QASM2: {exc}" raise TranslationError(msg) from exc @@ -640,10 +640,10 @@ def run( device_ops = {op.name().lower() for op in self._device.operations()} # Process each circuit - qdmi_jobs: list[fomac.Job] = [] + qdmi_jobs: list[qdmi.Job] = [] circuit_names: list[str] = [] # First pass: validate and convert all circuits - converted_circuits: list[tuple[str, fomac.ProgramFormat, str]] = [] + converted_circuits: list[tuple[str, qdmi.ProgramFormat, str]] = [] for idx, circuit in enumerate(circuits): # Bind parameters if provided diff --git a/python/mqt/core/plugins/qiskit/converters.py b/python/mqt/core/plugins/qiskit/converters.py index 0768952e79..3d2ed5af55 100644 --- a/python/mqt/core/plugins/qiskit/converters.py +++ b/python/mqt/core/plugins/qiskit/converters.py @@ -26,7 +26,7 @@ if TYPE_CHECKING: from qiskit.circuit import QuantumCircuit - from ... import fomac + from ... import qdmi __all__ = ["qiskit_to_iqm_json"] @@ -35,7 +35,7 @@ def __dir__() -> list[str]: return __all__ -def qiskit_to_iqm_json(circuit: QuantumCircuit, device: fomac.Device) -> str: +def qiskit_to_iqm_json(circuit: QuantumCircuit, device: qdmi.Device) -> str: """Convert a Qiskit :class:`~qiskit.circuit.QuantumCircuit` to IQM JSON format. The IQM JSON format is a device-specific format that encodes quantum operations @@ -48,7 +48,7 @@ def qiskit_to_iqm_json(circuit: QuantumCircuit, device: fomac.Device) -> str: Args: circuit: The Qiskit quantum circuit to convert. - device: The FoMaC device providing site mapping and metadata. + device: The QDMI device providing site mapping and metadata. Returns: JSON string representation of the circuit in IQM format. diff --git a/python/mqt/core/plugins/qiskit/job.py b/python/mqt/core/plugins/qiskit/job.py index 2a957331ee..d4ef2aee1f 100644 --- a/python/mqt/core/plugins/qiskit/job.py +++ b/python/mqt/core/plugins/qiskit/job.py @@ -20,7 +20,7 @@ from qiskit.result import Result from qiskit.result.models import ExperimentResult -from mqt.core import fomac +from mqt.core import qdmi if TYPE_CHECKING: from collections.abc import Sequence @@ -35,35 +35,35 @@ def __dir__() -> list[str]: class QDMIJob(JobV1): - """Qiskit job wrapping one or more QDMI/FoMaC jobs. + """Qiskit job wrapping one or more QDMI jobs. This class handles both single-circuit and multi-circuit execution, aggregating results from multiple QDMI jobs when needed. Args: backend: The backend this job runs on. - jobs: The FoMaC Job object(s). Can be a single job or a list of jobs. + jobs: The QDMI Job object(s). Can be a single job or a list of jobs. circuit_names: The name(s) of the circuit(s) being executed. Can be a single name or a list of names. """ def __init__( self, backend: QDMIBackend, - jobs: fomac.Job | Sequence[fomac.Job], + jobs: qdmi.Job | Sequence[qdmi.Job], circuit_names: str | Sequence[str], ) -> None: """Initialize the job. Args: backend: The backend to use for the job. - jobs: The FoMaC Job object(s). + jobs: The QDMI Job object(s). circuit_names: The name(s) of the circuit(s) the job is associated with. Raises: ValueError: If jobs list is empty or if jobs and circuit_names have mismatched lengths. """ # Normalize to lists - self._jobs = [jobs] if isinstance(jobs, fomac.Job) else jobs + self._jobs = [jobs] if isinstance(jobs, qdmi.Job) else jobs self._circuit_names = [circuit_names] if isinstance(circuit_names, str) else circuit_names # Validate non-empty jobs list @@ -99,11 +99,11 @@ def result(self) -> Result: for idx, (job, circuit_name) in enumerate(zip(self._jobs, self._circuit_names, strict=True)): # Wait for job completion if needed status = job.check() - if status not in {fomac.Job.Status.DONE, fomac.Job.Status.FAILED, fomac.Job.Status.CANCELED}: + if status not in {qdmi.Job.Status.DONE, qdmi.Job.Status.FAILED, qdmi.Job.Status.CANCELED}: job.wait() status = job.check() - success = status == fomac.Job.Status.DONE + success = status == qdmi.Job.Status.DONE overall_success = overall_success and success # Get counts if successful and not cached @@ -146,13 +146,13 @@ def status(self) -> JobStatus: """ # Map QDMI status to Qiskit JobStatus status_map = { - fomac.Job.Status.DONE: JobStatus.DONE, - fomac.Job.Status.RUNNING: JobStatus.RUNNING, - fomac.Job.Status.CANCELED: JobStatus.CANCELLED, - fomac.Job.Status.SUBMITTED: JobStatus.QUEUED, - fomac.Job.Status.QUEUED: JobStatus.QUEUED, - fomac.Job.Status.CREATED: JobStatus.INITIALIZING, - fomac.Job.Status.FAILED: JobStatus.ERROR, + qdmi.Job.Status.DONE: JobStatus.DONE, + qdmi.Job.Status.RUNNING: JobStatus.RUNNING, + qdmi.Job.Status.CANCELED: JobStatus.CANCELLED, + qdmi.Job.Status.SUBMITTED: JobStatus.QUEUED, + qdmi.Job.Status.QUEUED: JobStatus.QUEUED, + qdmi.Job.Status.CREATED: JobStatus.INITIALIZING, + qdmi.Job.Status.FAILED: JobStatus.ERROR, } # Collect all statuses (self._jobs is guaranteed non-empty by __init__) diff --git a/python/mqt/core/plugins/qiskit/provider.py b/python/mqt/core/plugins/qiskit/provider.py index 5d44c55ae9..4a25e9e284 100644 --- a/python/mqt/core/plugins/qiskit/provider.py +++ b/python/mqt/core/plugins/qiskit/provider.py @@ -14,9 +14,14 @@ from __future__ import annotations -from ... import fomac +from typing import TYPE_CHECKING + +from ... import qdmi from .backend import QDMIBackend +if TYPE_CHECKING: + import os + __all__ = ["QDMIProvider"] @@ -25,10 +30,10 @@ def __dir__() -> list[str]: class QDMIProvider: - """Provider for QDMI devices accessed via FoMaC. + """Provider for devices discovered by the QDMI device manager. This provider discovers and manages QDMI devices that are available through - the FoMaC layer. It provides a Qiskit-idiomatic interface for device + the QDMI layer. It provides a Qiskit-idiomatic interface for device discovery and backend instantiation. Examples: @@ -54,11 +59,10 @@ def __init__( self, *, token: str | None = None, - auth_file: str | None = None, + auth_file: str | os.PathLike[str] | None = None, auth_url: str | None = None, username: str | None = None, password: str | None = None, - project_id: str | None = None, **session_kwargs: str, ) -> None: """Initialize the QDMI provider. @@ -69,24 +73,26 @@ def __init__( auth_url: URL to authentication server. username: Username for authentication. password: Password for authentication. - project_id: Project ID for the session. - session_kwargs: Optional additional keyword arguments for Session initialization. + session_kwargs: Optional provider-specific session parameters. + + Raises: + TypeError: If ``session_kwargs`` contains an unknown session parameter. """ - kwargs = { - "token": token, - "auth_file": auth_file, - "auth_url": auth_url, - "username": username, - "password": password, - "project_id": project_id, - } - if session_kwargs: - kwargs.update(session_kwargs) - - self._session = fomac.Session(**kwargs) - self._backends = [ - QDMIBackend(device=d, provider=self) for d in self._session.get_devices() if QDMIBackend.is_convertible(d) - ] + parameters = qdmi.SessionParameters() + parameters.token = token + parameters.auth_file = auth_file + parameters.auth_url = auth_url + parameters.username = username + parameters.password = password + for key, value in session_kwargs.items(): + if not hasattr(parameters, key): + msg = f"Unknown QDMI session parameter: {key}" + raise TypeError(msg) + setattr(parameters, key, value) + + self._manager = qdmi.DeviceManager() + devices = self._manager.open_all(session_overrides=parameters).devices.values() + self._backends = [QDMIBackend(device=d, provider=self) for d in devices if QDMIBackend.is_convertible(d)] def backends(self, name: str | None = None) -> list[QDMIBackend]: """Return all available backends, optionally filtered by name substring. diff --git a/python/mqt/core/qdmi.pyi b/python/mqt/core/qdmi.pyi new file mode 100644 index 0000000000..a439f882ab --- /dev/null +++ b/python/mqt/core/qdmi.pyi @@ -0,0 +1,732 @@ +# 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 + +import enum +import os +import pathlib +from collections.abc import Sequence +from typing import overload + +class SessionParameters: + """Parameters for one QDMI device session.""" + + def __init__( + 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, + custom1: str | None = None, + custom2: str | None = None, + custom3: str | None = None, + custom4: str | None = None, + custom5: str | None = None, + ) -> None: + """Create session parameters from optional keyword arguments. + + Args: + base_url: Base URL of the device service. + token: Authentication token. + auth_file: Path to an authentication file. + auth_url: URL of the authentication service. + username: Authentication username. + password: Authentication password. + custom1: First implementation-defined session parameter. + custom2: Second implementation-defined session parameter. + custom3: Third implementation-defined session parameter. + custom4: Fourth implementation-defined session parameter. + custom5: Fifth implementation-defined session parameter. + """ + + @property + def base_url(self) -> str | None: + """Base URL of the device service.""" + + @base_url.setter + def base_url(self, arg: str | None) -> None: ... + @property + def token(self) -> str | None: + """Authentication token.""" + + @token.setter + def token(self, arg: str | None) -> None: ... + @property + def auth_file(self) -> pathlib.Path | None: + """Path to an authentication file.""" + + @auth_file.setter + def auth_file(self, arg: str | os.PathLike | None) -> None: ... + @property + def auth_url(self) -> str | None: + """URL of the authentication service.""" + + @auth_url.setter + def auth_url(self, arg: str | None) -> None: ... + @property + def username(self) -> str | None: + """Authentication username.""" + + @username.setter + def username(self, arg: str | None) -> None: ... + @property + def password(self) -> str | None: + """Authentication password.""" + + @password.setter + def password(self, arg: str | None) -> None: ... + @property + def custom1(self) -> str | None: + """First implementation-defined session parameter.""" + + @custom1.setter + def custom1(self, arg: str | None) -> None: ... + @property + def custom2(self) -> str | None: + """Second implementation-defined session parameter.""" + + @custom2.setter + def custom2(self, arg: str | None) -> None: ... + @property + def custom3(self) -> str | None: + """Third implementation-defined session parameter.""" + + @custom3.setter + def custom3(self, arg: str | None) -> None: ... + @property + def custom4(self) -> str | None: + """Fourth implementation-defined session parameter.""" + + @custom4.setter + def custom4(self, arg: str | None) -> None: ... + @property + def custom5(self) -> str | None: + """Fifth implementation-defined session parameter.""" + + @custom5.setter + def custom5(self, arg: str | None) -> None: ... + +class DeviceDefinition: + """A side-effect-free QDMI device registration.""" + + def __init__( + self, device_id: str, library: str | os.PathLike, prefix: str, *, session: SessionParameters = ... + ) -> None: + """Create a device definition without loading its library. + + Args: + device_id: Stable identifier used for discovery and opening. + library: Path to the native QDMI device library. + prefix: Symbol prefix exported by the QDMI implementation. + session: Default parameters for sessions opened from this definition. + """ + + @property + def device_id(self) -> str: + """Stable device identifier.""" + + @device_id.setter + def device_id(self, arg: str, /) -> None: ... + @property + def library(self) -> pathlib.Path: + """Path to the native QDMI device library.""" + + @library.setter + def library(self, arg: str | os.PathLike, /) -> None: ... + @property + def prefix(self) -> str: + """Symbol prefix exported by the device library.""" + + @prefix.setter + def prefix(self, arg: str, /) -> None: ... + @property + def session(self) -> SessionParameters: + """Default parameters for newly opened sessions.""" + + @session.setter + def session(self, arg: SessionParameters, /) -> None: ... + +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.""" + + 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 Job: + """A submitted quantum program execution retaining its device session.""" + + def check(self) -> Status: + """Return the current QDMI job status.""" + + def wait(self, timeout: int = 0) -> bool: + """Waits for the job to complete. + + Args: + timeout: The maximum time to wait in seconds. If 0, waits indefinitely. + + Returns: + True if the job completed within the timeout, False otherwise. + """ + + def cancel(self) -> None: + """Request cancellation of the job.""" + + def get_shots(self) -> list[str]: + """Return the raw shot results.""" + + def get_counts(self) -> dict[str, int]: + """Return measurement counts keyed by bit string.""" + + def get_dense_statevector(self) -> list[complex]: + """Return the dense state vector. + + This result is typically available only from simulator devices. + """ + + def get_dense_probabilities(self) -> list[float]: + """Return the dense probability vector. + + This result is typically available only from simulator devices. + """ + + def get_sparse_statevector(self) -> dict[str, complex]: + """Return the sparse state vector keyed by basis state. + + This result is typically available only from simulator devices. + """ + + def get_sparse_probabilities(self) -> dict[str, float]: + """Return sparse probabilities keyed by basis state. + + This result is typically available only from simulator devices. + """ + + @overload + def query_custom_property(self, custom_property: CustomProperty, value_type: type[str]) -> str | None: ... + @overload + def query_custom_property(self, custom_property: CustomProperty, value_type: type[bool]) -> bool | None: ... + @overload + def query_custom_property(self, custom_property: CustomProperty, value_type: type[int]) -> int | None: ... + @overload + def query_custom_property(self, custom_property: CustomProperty, value_type: type[float]) -> float | None: ... + @overload + def query_custom_property(self, custom_property: CustomProperty, value_type: type[bytes]) -> bytes | None: ... + @overload + def query_custom_property( + self, custom_property: CustomProperty, value_type: type[str | bool | int | float | bytes] + ) -> str | bool | int | float | bytes | None: + """Query an implementation-defined custom job property. + + The caller must provide the type documented by the device implementation. + Use ``bytes`` to retrieve the value without interpretation. + + Args: + custom_property: Custom property slot to query. + value_type: Expected Python type of the property value. + + Returns: + The typed property value, or ``None`` when the slot is unsupported. + """ + + @overload + def get_custom_result(self, custom_property: CustomProperty, value_type: type[str]) -> str | None: ... + @overload + def get_custom_result(self, custom_property: CustomProperty, value_type: type[bool]) -> bool | None: ... + @overload + def get_custom_result(self, custom_property: CustomProperty, value_type: type[int]) -> int | None: ... + @overload + def get_custom_result(self, custom_property: CustomProperty, value_type: type[float]) -> float | None: ... + @overload + def get_custom_result(self, custom_property: CustomProperty, value_type: type[bytes]) -> bytes | None: ... + @overload + def get_custom_result( + self, custom_property: CustomProperty, value_type: type[str | bool | int | float | bytes] + ) -> str | bool | int | float | bytes | None: + """Return an implementation-defined custom job result. + + The caller must provide the type documented by the device implementation. + Use ``bytes`` to retrieve the value without interpretation. + + Args: + custom_property: Custom result slot to retrieve. + value_type: Expected Python type of the result value. + + Returns: + The typed result value, or ``None`` when the slot is unsupported. + """ + + @property + def id(self) -> str: + """The device-assigned job identifier.""" + + @property + def program_format(self) -> ProgramFormat: + """The QDMI format of the submitted program.""" + + @property + def program(self) -> str: + """The submitted program.""" + + @property + def program_bytes(self) -> bytes: + """The exact bytes of the submitted program.""" + + @property + def num_shots(self) -> int: + """The requested number of shots.""" + + def __eq__(self, arg: object, /) -> bool: + """Return whether two objects refer to the same job.""" + + def __ne__(self, arg: object, /) -> bool: + """Return whether two objects refer to different jobs.""" + + class Status(enum.Enum): + """Status values defined by QDMI.""" + + CREATED = 0 + + SUBMITTED = 1 + + QUEUED = 2 + + RUNNING = 3 + + DONE = 4 + + CANCELED = 5 + + FAILED = 6 + +class ProgramFormat(enum.Enum): + """Program formats defined by QDMI.""" + + QASM2 = 0 + + QASM3 = 1 + + QIR_BASE_STRING = 2 + + QIR_BASE_MODULE = 3 + + QIR_ADAPTIVE_STRING = 4 + + QIR_ADAPTIVE_MODULE = 5 + + CALIBRATION = 6 + + QPY = 7 + + IQM_JSON = 8 + + BATCH_JOB = 9 + + CUSTOM1 = 999999995 + + CUSTOM2 = 999999996 + + CUSTOM3 = 999999997 + + CUSTOM4 = 999999998 + + CUSTOM5 = 999999999 + +class CustomProperty(enum.Enum): + """An implementation-defined custom property or result slot.""" + + CUSTOM1 = 1 + + CUSTOM2 = 2 + + CUSTOM3 = 3 + + CUSTOM4 = 4 + + CUSTOM5 = 5 + +class Device: + """One initialized QDMI device session. + + The object owns the native library and session state required by its sites, + operations, child devices, and jobs. + """ + + class Status(enum.Enum): + """Status values defined by QDMI.""" + + OFFLINE = 0 + + IDLE = 1 + + BUSY = 2 + + ERROR = 3 + + MAINTENANCE = 4 + + CALIBRATION = 5 + + def name(self) -> str: + """Return the device name reported by its implementation.""" + + def version(self) -> str: + """Return the device version reported by its implementation.""" + + def status(self) -> Status: + """Return the current QDMI device status.""" + + def library_version(self) -> str: + """Return the device library version.""" + + def qubits_num(self) -> int: + """Return the number of qubits available on the device.""" + + def sites(self) -> list[Site]: + """Return all regular sites and zones.""" + + def regular_sites(self) -> list[Site]: + """Return sites that are not zones.""" + + def zones(self) -> list[Site]: + """Return sites that represent zones.""" + + def operations(self) -> list[Operation]: + """Return operations supported by the device.""" + + def coupling_map(self) -> list[tuple[Site, Site]] | None: + """Return the optional coupling map as site pairs.""" + + def needs_calibration(self) -> int | None: + """Return the optional calibration requirement.""" + + def length_unit(self) -> str | None: + """Return the optional device length unit.""" + + def length_scale_factor(self) -> float | None: + """Return the optional length scale factor.""" + + def duration_unit(self) -> str | None: + """Return the optional device duration unit.""" + + def duration_scale_factor(self) -> float | None: + """Return the optional duration scale factor.""" + + def min_atom_distance(self) -> int | None: + """Return the optional minimum atom distance.""" + + def supported_program_formats(self) -> list[ProgramFormat]: + """Return the QDMI program formats accepted by the device.""" + + def child_devices(self) -> list[Device]: + """Return directly managed child devices.""" + + @overload + def query_custom_property(self, custom_property: CustomProperty, value_type: type[str]) -> str | None: ... + @overload + def query_custom_property(self, custom_property: CustomProperty, value_type: type[bool]) -> bool | None: ... + @overload + def query_custom_property(self, custom_property: CustomProperty, value_type: type[int]) -> int | None: ... + @overload + def query_custom_property(self, custom_property: CustomProperty, value_type: type[float]) -> float | None: ... + @overload + def query_custom_property(self, custom_property: CustomProperty, value_type: type[bytes]) -> bytes | None: ... + @overload + def query_custom_property( + self, custom_property: CustomProperty, value_type: type[str | bool | int | float | bytes] + ) -> str | bool | int | float | bytes | None: + """Query an implementation-defined custom device property. + + The caller must provide the type documented by the device implementation. + Use ``bytes`` to retrieve the value without interpretation. + + Args: + custom_property: Custom property slot to query. + value_type: Expected Python type of the property value. + + Returns: + The typed property value, or ``None`` when the slot is unsupported. + """ + + def submit_job( + self, + program: str | bytes, + program_format: ProgramFormat, + num_shots: int, + *, + custom1: str | bool | int | float | None = None, # ruff:ignore[redundant-numeric-union] + custom2: str | bool | int | float | None = None, # ruff:ignore[redundant-numeric-union] + custom3: str | bool | int | float | None = None, # ruff:ignore[redundant-numeric-union] + custom4: str | bool | int | float | None = None, # ruff:ignore[redundant-numeric-union] + custom5: str | bool | int | float | None = None, # ruff:ignore[redundant-numeric-union] + ) -> Job: + """Submit a quantum program to the device. + + Args: + program: Text submitted with a terminating null byte, or exact bytes. + program_format: QDMI format of ``program``. + num_shots: Number of requested executions. + custom1: First implementation-defined job parameter. + custom2: Second implementation-defined job parameter. + custom3: Third implementation-defined job parameter. + custom4: Fourth implementation-defined job parameter. + custom5: Fifth implementation-defined job parameter. + + Returns: + A job retaining the device session. + """ + + def __eq__(self, arg: object, /) -> bool: + """Return whether two objects refer to the same device.""" + + def __ne__(self, arg: object, /) -> bool: + """Return whether two objects refer to different devices.""" + + class Site: + """A physical site or zone belonging to a device.""" + + def index(self) -> int: + """Return the device-assigned site index.""" + + def t1(self) -> int | None: + """Return the optional T1 coherence time.""" + + def t2(self) -> int | None: + """Return the optional T2 coherence time.""" + + def name(self) -> str | None: + """Return the optional site name.""" + + def x_coordinate(self) -> int | None: + """Return the optional x coordinate.""" + + def y_coordinate(self) -> int | None: + """Return the optional y coordinate.""" + + def z_coordinate(self) -> int | None: + """Return the optional z coordinate.""" + + def is_zone(self) -> bool: + """Return whether this site represents a zone.""" + + def x_extent(self) -> int | None: + """Return the optional x extent of the zone.""" + + def y_extent(self) -> int | None: + """Return the optional y extent of the zone.""" + + def z_extent(self) -> int | None: + """Return the optional z extent of the zone.""" + + def module_index(self) -> int | None: + """Return the optional module index.""" + + def submodule_index(self) -> int | None: + """Return the optional submodule index.""" + + @overload + def query_custom_property(self, custom_property: CustomProperty, value_type: type[str]) -> str | None: ... + @overload + def query_custom_property(self, custom_property: CustomProperty, value_type: type[bool]) -> bool | None: ... + @overload + def query_custom_property(self, custom_property: CustomProperty, value_type: type[int]) -> int | None: ... + @overload + def query_custom_property(self, custom_property: CustomProperty, value_type: type[float]) -> float | None: ... + @overload + def query_custom_property(self, custom_property: CustomProperty, value_type: type[bytes]) -> bytes | None: ... + @overload + def query_custom_property( + self, custom_property: CustomProperty, value_type: type[str | bool | int | float | bytes] + ) -> str | bool | int | float | bytes | None: + """Query an implementation-defined custom site property. + + The caller must provide the type documented by the device implementation. + Use ``bytes`` to retrieve the value without interpretation. + + Args: + custom_property: Custom property slot to query. + value_type: Expected Python type of the property value. + + Returns: + The typed property value, or ``None`` when the slot is unsupported. + """ + + def __eq__(self, arg: object, /) -> bool: + """Return whether two objects refer to the same site.""" + + def __ne__(self, arg: object, /) -> bool: + """Return whether two objects refer to different sites.""" + + class Operation: + """A quantum operation supported by a device.""" + + def name(self, sites: Sequence[Device.Site] = ..., params: Sequence[float] = ...) -> str: + """Return the operation name for the given sites and parameters.""" + + def qubits_num(self, sites: Sequence[Device.Site] = ..., params: Sequence[float] = ...) -> int | None: + """Return the optional operation arity.""" + + def parameters_num(self, sites: Sequence[Device.Site] = ..., params: Sequence[float] = ...) -> int: + """Return the number of operation parameters.""" + + def duration(self, sites: Sequence[Device.Site] = ..., params: Sequence[float] = ...) -> int | None: + """Return the optional duration for this operation instance.""" + + def fidelity(self, sites: Sequence[Device.Site] = ..., params: Sequence[float] = ...) -> float | None: + """Return the optional fidelity for this operation instance.""" + + def interaction_radius(self, sites: Sequence[Device.Site] = ..., params: Sequence[float] = ...) -> int | None: + """Return the optional interaction radius.""" + + def blocking_radius(self, sites: Sequence[Device.Site] = ..., params: Sequence[float] = ...) -> int | None: + """Return the optional blocking radius.""" + + def idling_fidelity(self, sites: Sequence[Device.Site] = ..., params: Sequence[float] = ...) -> float | None: + """Return the optional idling fidelity.""" + + def is_zoned(self) -> bool: + """Return whether the operation is restricted to zones.""" + + def sites(self) -> list[Device.Site] | None: + """Return sites on which the operation is available.""" + + def site_pairs(self) -> list[tuple[Device.Site, Device.Site]] | None: + """Return supported site pairs for a local two-site operation.""" + + def mean_shuttling_speed(self, sites: Sequence[Device.Site] = ..., params: Sequence[float] = ...) -> int | None: + """Return the optional mean shuttling speed.""" + + @overload + def query_custom_property( + self, + custom_property: CustomProperty, + value_type: type[str], + sites: Sequence[Device.Site] = ..., + params: Sequence[float] = ..., + ) -> str | None: ... + @overload + def query_custom_property( + self, + custom_property: CustomProperty, + value_type: type[bool], + sites: Sequence[Device.Site] = ..., + params: Sequence[float] = ..., + ) -> bool | None: ... + @overload + def query_custom_property( + self, + custom_property: CustomProperty, + value_type: type[int], + sites: Sequence[Device.Site] = ..., + params: Sequence[float] = ..., + ) -> int | None: ... + @overload + def query_custom_property( + self, + custom_property: CustomProperty, + value_type: type[float], + sites: Sequence[Device.Site] = ..., + params: Sequence[float] = ..., + ) -> float | None: ... + @overload + def query_custom_property( + self, + custom_property: CustomProperty, + value_type: type[bytes], + sites: Sequence[Device.Site] = ..., + params: Sequence[float] = ..., + ) -> bytes | None: ... + @overload + def query_custom_property( + self, + custom_property: CustomProperty, + value_type: type[str | bool | int | float | bytes], + sites: Sequence[Device.Site] = ..., + params: Sequence[float] = ..., + ) -> str | bool | int | float | bytes | None: + """Query an implementation-defined custom operation property. + + The caller must provide the type documented by the device implementation. + Use ``bytes`` to retrieve the value without interpretation. + + Args: + custom_property: Custom property slot to query. + value_type: Expected Python type of the property value. + sites: Sites for the operation instance. + params: Parameters for the operation instance. + + Returns: + The typed property value, or ``None`` when the slot is unsupported. + """ + + def __eq__(self, arg: object, /) -> bool: + """Return whether two objects refer to the same operation.""" + + def __ne__(self, arg: object, /) -> bool: + """Return whether two objects refer to different operations.""" + +class OpenAllResult: + """Devices and per-ID errors produced by bulk opening.""" + + @property + def devices(self) -> dict[str, Device]: + """Successfully opened devices keyed by stable ID.""" + + @property + def errors(self) -> dict[str, str]: + """Error messages for failed definitions keyed by stable ID.""" + +class DeviceManager: + """Discover and lazily open QDMI devices. + + Definitions are discovered without loading native libraries. Opening a device + creates an independent session while compatible devices may share a loaded + library. + """ + + @overload + def __init__(self) -> None: + """Create a manager from the standard configuration sources.""" + + @overload + def __init__(self, registry: DeviceRegistry) -> None: + """Create a manager from an immutable registry snapshot.""" + + @property + def definitions(self) -> list[DeviceDefinition]: + """The manager's immutable device definitions.""" + + def open(self, device_id: str, *, session_overrides: SessionParameters = ...) -> Device: + """Open one device by stable ID. + + The supplied session values override the definition defaults field by field. + The native library is loaded only when this method is called. + """ + + def open_all(self, *, session_overrides: SessionParameters = ...) -> OpenAllResult: + """Open a snapshot of all definitions independently. + + Failures are retained by device ID and do not prevent other definitions from + opening. + """ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 08161ca5bc..da6ac3c706 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -80,9 +80,6 @@ add_subdirectory(na) # add QDMI package library add_subdirectory(qdmi) -# add FoMaC package library -add_subdirectory(fomac) - # add QIR package library add_subdirectory(qir) diff --git a/src/fomac/CMakeLists.txt b/src/fomac/CMakeLists.txt deleted file mode 100644 index 631141c998..0000000000 --- a/src/fomac/CMakeLists.txt +++ /dev/null @@ -1,32 +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}-fomac) - -if(NOT TARGET ${TARGET_NAME}) - # Add FoMaC library - add_mqt_core_library(${TARGET_NAME} ALIAS_NAME FoMaC) - - # Add sources to target - target_sources(${TARGET_NAME} PRIVATE FoMaC.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}/fomac/FoMaC.hpp) - - # Add link libraries - target_link_libraries( - ${TARGET_NAME} - PUBLIC qdmi::qdmi MQT::CoreQDMICommon MQT::CoreQDMIDriver - PRIVATE spdlog::spdlog) - - # add to list of MQT core targets - set(MQT_CORE_TARGETS - ${MQT_CORE_TARGETS} ${TARGET_NAME} - PARENT_SCOPE) -endif() diff --git a/src/fomac/FoMaC.cpp b/src/fomac/FoMaC.cpp deleted file mode 100644 index fb3e7156a6..0000000000 --- a/src/fomac/FoMaC.cpp +++ /dev/null @@ -1,870 +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 "fomac/FoMaC.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 fomac { -namespace { -[[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; -} - -[[nodiscard]] constexpr bool -hasNoGenericProgramPayload(const QDMI_Program_Format format) noexcept { - return format == QDMI_PROGRAM_FORMAT_CALIBRATION || - format == QDMI_PROGRAM_FORMAT_BATCHJOB; -} -} // 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); - std::vector operations; - operations.reserve(qdmiOperations.size()); - std::ranges::transform( - qdmiOperations, std::back_inserter(operations), - [this](const QDMI_Operation& op) -> Operation { return {device_, op}; }); - return operations; -} - -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::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"); - } - if (hasNoGenericProgramPayload(format)) { - throw std::invalid_argument( - "Calibration and batch jobs do not use a generic program payload"); - } - - 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 { - if (hasNoGenericProgramPayload(format)) { - throw std::invalid_argument( - "Calibration and batch jobs do not use a generic program payload"); - } - - 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"); - qdmi::throwIfError(QDMI_job_set_parameter(jobWrapper, - QDMI_JOB_PARAMETER_PROGRAM, - program.size(), program.data()), - "Setting program"); - 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; -} - -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::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 fomac diff --git a/src/na/CMakeLists.txt b/src/na/CMakeLists.txt index 6efa4404cf..fdaba3d290 100644 --- a/src/na/CMakeLists.txt +++ b/src/na/CMakeLists.txt @@ -6,7 +6,7 @@ # # Licensed under the MIT License -add_subdirectory(fomac) +add_subdirectory(qdmi) if(NOT TARGET ${MQT_CORE_TARGET_NAME}-na) # collect headers and source files diff --git a/src/na/fomac/CMakeLists.txt b/src/na/qdmi/CMakeLists.txt similarity index 81% rename from src/na/fomac/CMakeLists.txt rename to src/na/qdmi/CMakeLists.txt index 58f39850a5..1d6b3f68e9 100644 --- a/src/na/fomac/CMakeLists.txt +++ b/src/na/qdmi/CMakeLists.txt @@ -7,25 +7,25 @@ # Licensed under the MIT License # Set target name -set(TARGET_NAME ${MQT_CORE_TARGET_NAME}-na-fomac) +set(TARGET_NAME ${MQT_CORE_TARGET_NAME}-na-qdmi) # If the target is not already defined if(NOT TARGET ${TARGET_NAME}) # Add library - add_mqt_core_library(${TARGET_NAME} ALIAS_NAME NAFoMaC) + add_mqt_core_library(${TARGET_NAME} ALIAS_NAME NAQDMI) # add sources to target target_sources(${TARGET_NAME} PRIVATE Device.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}/na/fomac/Device.hpp) + FILES ${MQT_CORE_INCLUDE_BUILD_DIR}/na/qdmi/Device.hpp) # Link nlohmann_json, spdlog target_link_libraries( ${TARGET_NAME} - PUBLIC MQT::CoreFoMaC nlohmann_json::nlohmann_json + PUBLIC MQT::CoreQDMI nlohmann_json::nlohmann_json PRIVATE spdlog::spdlog MQT::CoreQDMINaDeviceGen) # add to list of MQT core targets diff --git a/src/na/fomac/Device.cpp b/src/na/qdmi/Device.cpp similarity index 92% rename from src/na/fomac/Device.cpp rename to src/na/qdmi/Device.cpp index 474035413f..48d3d1b1cf 100644 --- a/src/na/fomac/Device.cpp +++ b/src/na/qdmi/Device.cpp @@ -8,10 +8,10 @@ * Licensed under the MIT License */ -#include "na/fomac/Device.hpp" +#include "na/qdmi/Device.hpp" -#include "fomac/FoMaC.hpp" #include "ir/Definitions.hpp" +#include "qdmi/Device.hpp" #include "qdmi/devices/na/Generator.hpp" #include @@ -36,14 +36,14 @@ #include #include -namespace na { +namespace na::qdmi { namespace { /** - * @brief Calculate the rectangular extent covering all given Session sites. - * @param sites is a vector of Session sites + * @brief Calculate the rectangular extent covering all given QDMI sites. + * @param sites is a vector of QDMI sites * @return the extent covering all given sites */ -auto calculateExtentFromSites(const std::vector& sites) +auto calculateExtentFromSites(const std::vector<::qdmi::Site>& sites) -> Device::Region { auto minX = std::numeric_limits::max(); auto maxX = std::numeric_limits::min(); @@ -62,13 +62,13 @@ auto calculateExtentFromSites(const std::vector& sites) .height = static_cast(maxY - minY)}}; } /** - * @brief Calculate the rectangular extent covering all given Session site + * @brief Calculate the rectangular extent covering all given QDMI site * pairs. - * @param sitePairs is a vector of Session site pairs + * @param sitePairs is a vector of QDMI site pairs * @return the extent covering all sites in the pairs */ auto calculateExtentFromSites( - const std::vector>& sitePairs) + const std::vector>& sitePairs) -> Device::Region { auto minX = std::numeric_limits::max(); auto maxX = std::numeric_limits::min(); @@ -110,8 +110,8 @@ class MinHeap } }; } // namespace -auto Session::Device::initNameFromDevice() -> void { name = getName(); } -auto Session::Device::initMinAtomDistanceFromDevice() -> bool { +auto Device::initNameFromDevice() -> void { name = getName(); } +auto Device::initMinAtomDistanceFromDevice() -> bool { const auto& d = getMinAtomDistance(); if (!d.has_value()) { SPDLOG_INFO("Minimal atom distance not set"); @@ -120,11 +120,9 @@ auto Session::Device::initMinAtomDistanceFromDevice() -> bool { minAtomDistance = *d; return true; } -auto Session::Device::initQubitsNumFromDevice() -> void { - numQubits = getQubitsNum(); -} -auto Session::Device::initLengthUnitFromDevice() -> bool { - const auto& u = fomac::Device::getLengthUnit(); +auto Device::initQubitsNumFromDevice() -> void { numQubits = getQubitsNum(); } +auto Device::initLengthUnitFromDevice() -> bool { + const auto& u = ::qdmi::Device::getLengthUnit(); if (!u.has_value()) { SPDLOG_INFO("Length unit not set"); return false; @@ -133,8 +131,8 @@ auto Session::Device::initLengthUnitFromDevice() -> bool { lengthUnit.scaleFactor = getLengthScaleFactor().value_or(1.0); return true; } -auto Session::Device::initDurationUnitFromDevice() -> bool { - const auto& u = fomac::Device::getDurationUnit(); +auto Device::initDurationUnitFromDevice() -> bool { + const auto& u = ::qdmi::Device::getDurationUnit(); if (!u.has_value()) { SPDLOG_INFO("Duration unit not set"); return false; @@ -143,7 +141,7 @@ auto Session::Device::initDurationUnitFromDevice() -> bool { durationUnit.scaleFactor = getDurationScaleFactor().value_or(1.0); return true; } -auto Session::Device::initDecoherenceTimesFromDevice() -> bool { +auto Device::initDecoherenceTimesFromDevice() -> bool { const auto regularSites = getRegularSites(); if (regularSites.empty()) { SPDLOG_INFO("Device has no regular sites with decoherence data"); @@ -170,7 +168,7 @@ auto Session::Device::initDecoherenceTimesFromDevice() -> bool { decoherenceTimes.t2 = sumT2 / count; return true; } -auto Session::Device::initTrapsfromDevice() -> bool { +auto Device::initTrapsfromDevice() -> bool { traps.clear(); const auto regularSites = getRegularSites(); if (regularSites.empty()) { @@ -294,10 +292,10 @@ auto Session::Device::initTrapsfromDevice() -> bool { } return true; } -auto Session::Device::initOperationsFromDevice() -> bool { +auto Device::initOperationsFromDevice() -> bool { std::map>> shuttlingUnitsPerId; - for (const fomac::Operation& op : getOperations()) { + for (const ::qdmi::Operation& op : getOperations()) { const auto zoned = op.isZoned(); const auto& nq = op.getQubitsNum(); const auto& opName = op.getName(); @@ -307,7 +305,7 @@ auto Session::Device::initOperationsFromDevice() -> bool { return false; } if (zoned) { - if (std::ranges::any_of(*sitesOpt, [](const fomac::Site& site) -> bool { + if (std::ranges::any_of(*sitesOpt, [](const ::qdmi::Site& site) -> bool { return !site.isZone(); })) { SPDLOG_INFO("Operation marked as zoned but has non-zone sites"); @@ -576,14 +574,4 @@ auto Session::Device::initOperationsFromDevice() -> bool { return true; } -auto Session::getDevices() -> std::vector { - std::vector devices; - fomac::Session session; - for (const auto& d : session.getDevices()) { - if (auto r = Device::tryCreateFromDevice(d); r.has_value()) { - devices.emplace_back(r.value()); - } - } - return devices; -} -} // namespace na +} // namespace na::qdmi diff --git a/src/qdmi/CMakeLists.txt b/src/qdmi/CMakeLists.txt index 6beb81f7ad..6145bdff30 100644 --- a/src/qdmi/CMakeLists.txt +++ b/src/qdmi/CMakeLists.txt @@ -8,7 +8,42 @@ 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 Device.cpp + DeviceManager.cpp + DeviceRegistry.cpp + DeviceState.cpp + DeviceState.h + DeviceApi.cpp + DeviceApi.h) + + target_sources( + ${TARGET_NAME} + PUBLIC FILE_SET + HEADERS + BASE_DIRS + ${MQT_CORE_INCLUDE_BUILD_DIR} + FILES + ${MQT_CORE_INCLUDE_BUILD_DIR}/qdmi/Device.hpp + ${MQT_CORE_INCLUDE_BUILD_DIR}/qdmi/DeviceManager.hpp + ${MQT_CORE_INCLUDE_BUILD_DIR}/qdmi/DeviceRegistry.hpp) + + target_link_libraries( + ${TARGET_NAME} + PUBLIC nlohmann_json::nlohmann_json qdmi::qdmi + PRIVATE MQT::CoreQDMICommon spdlog::spdlog ${CMAKE_DL_LIBS}) + target_include_directories(${TARGET_NAME} SYSTEM + PRIVATE ${PROJECT_SOURCE_DIR}/vendor/tomlplusplus) + + list(APPEND MQT_CORE_TARGETS ${TARGET_NAME}) +endif() set(MQT_CORE_TARGETS ${MQT_CORE_TARGETS} diff --git a/src/qdmi/Device.cpp b/src/qdmi/Device.cpp new file mode 100644 index 0000000000..c51b795c71 --- /dev/null +++ b/src/qdmi/Device.cpp @@ -0,0 +1,879 @@ +/* + * 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.h" +#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 + +namespace qdmi { +namespace { +[[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; +} + +[[nodiscard]] constexpr bool +hasNoGenericProgramPayload(const QDMI_Program_Format format) noexcept { + return format == QDMI_PROGRAM_FORMAT_CALIBRATION || + format == QDMI_PROGRAM_FORMAT_BATCHJOB; +} + +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(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::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"); + } + if (hasNoGenericProgramPayload(format)) { + throw std::invalid_argument( + "Calibration and batch jobs do not use a generic program payload"); + } + + 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 { + if (hasNoGenericProgramPayload(format)) { + throw std::invalid_argument( + "Calibration and batch jobs do not use a generic program payload"); + } + + 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"); + throwIfError(jobState->device->api().device_job_set_parameter( + jobState->job, QDMI_DEVICE_JOB_PARAMETER_PROGRAM, + program.size(), program.data()), + "Setting program"); + 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)); +} + +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::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..a5d52cef2f --- /dev/null +++ b/src/qdmi/DeviceApi.cpp @@ -0,0 +1,173 @@ +/* + * 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.h" + +#include "qdmi/common/Common.hpp" + +#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 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; +} +} // 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); + 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); + // The shared pointer immediately adopts this allocation and its deleter + // signals completion only after finalization and unloading. + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + auto* api = new DeviceApi(canonicalLibrary, prefix); + auto loaded = std::shared_ptr( + api, [finalized](const DeviceApi* instance) noexcept { + delete instance; + finalized->count_down(); + }); + cache.libraries.emplace(key, + DeviceApiCacheEntry{loaded, std::move(finalized)}); + return loaded; + } +} + +} // namespace qdmi::detail diff --git a/src/qdmi/DeviceApi.h b/src/qdmi/DeviceApi.h new file mode 100644 index 0000000000..81fe361b57 --- /dev/null +++ b/src/qdmi/DeviceApi.h @@ -0,0 +1,73 @@ +/* + * 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_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..558b048cf1 --- /dev/null +++ b/src/qdmi/DeviceManager.cpp @@ -0,0 +1,86 @@ +/* + * 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 "DeviceApi.h" +#include "DeviceState.h" +#include "qdmi/Device.hpp" +#include "qdmi/DeviceRegistry.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(SessionParameters& target, const SessionParameters& 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.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() = default; + +DeviceManager::DeviceManager(DeviceRegistry registry) + : registry_(std::move(registry)) {} + +Device DeviceManager::open(const std::string_view id, + const SessionParameters& 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("No QDMI device is registered with 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 SessionParameters& 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; +} +} // namespace qdmi diff --git a/src/qdmi/driver/DeviceRegistry.cpp b/src/qdmi/DeviceRegistry.cpp similarity index 88% rename from src/qdmi/driver/DeviceRegistry.cpp rename to src/qdmi/DeviceRegistry.cpp index bc3537a683..44eb20d583 100644 --- a/src/qdmi/driver/DeviceRegistry.cpp +++ b/src/qdmi/DeviceRegistry.cpp @@ -8,9 +8,7 @@ * Licensed under the MIT License */ -#include "DeviceRegistry.hpp" - -#include "qdmi/driver/Driver.hpp" +#include "qdmi/DeviceRegistry.hpp" #include // NOLINT(misc-include-cleaner) #include @@ -21,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -37,7 +36,7 @@ #include #endif -namespace qdmi::detail { +namespace qdmi { namespace { using Json = nlohmann::json; // NOLINT(misc-include-cleaner) @@ -115,8 +114,8 @@ void rejectUnknownKeys(const Json& value, 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 {}; } @@ -392,7 +391,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); @@ -443,7 +442,7 @@ void appendFragments(std::vector& files, } [[nodiscard]] auto materialize(const DefinitionPatch& patch) - -> std::optional { + -> std::optional { if (!patch.enabled.value_or(true)) { return std::nullopt; } @@ -455,15 +454,13 @@ 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; definition.session.baseUrl = patch.session.baseUrl; definition.session.token = patch.session.token; - if (patch.session.authFile) { - definition.session.authFile = patch.session.authFile; - } + definition.session.authFile = patch.session.authFile; definition.session.authUrl = patch.session.authUrl; definition.session.username = patch.session.username; definition.session.password = patch.session.password; @@ -510,14 +507,68 @@ DeviceRegistry::DeviceRegistry() { error.what()); } } - for (auto& [unused, patch] : merged) { + for (const auto& [unused, patch] : merged) { static_cast(unused); if (!patch.enabled.value_or(true)) { - disabledIds_.emplace_back(std::move(patch.id)); + disabledIds_.emplace(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)); + } +} + +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"); + } +} +} // 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()) { + if (!replace) { + throw std::invalid_argument("QDMI device ID '" + definition.id + + "' is already registered"); + } + *existing = std::move(definition); + return; + } + definitions_.emplace_back(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; +} + +} // namespace qdmi diff --git a/src/qdmi/DeviceState.cpp b/src/qdmi/DeviceState.cpp new file mode 100644 index 0000000000..7d1c5bf8ee --- /dev/null +++ b/src/qdmi/DeviceState.cpp @@ -0,0 +1,141 @@ +/* + * 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.h" + +#include "DeviceApi.h" +#include "qdmi/DeviceRegistry.hpp" +#include "qdmi/common/Common.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace qdmi::detail { +namespace { +[[nodiscard]] auto openSession(const DeviceApi& api, + const SessionParameters& parameters, + QDMI_Child_Device child) -> QDMI_Device_Session { + 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); + 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 SessionParameters& 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 SessionParameters& sessionParameters, + QDMI_Child_Device child, + std::shared_ptr parentSession) + : sessionState(std::make_shared(std::move(deviceApi), + sessionParameters, child, + std::move(parentSession))), + parameters(sessionParameters) { + 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() { + if (job != nullptr) { + device->api().device_job_free(job); + } +} +} // namespace qdmi::detail diff --git a/src/qdmi/DeviceState.h b/src/qdmi/DeviceState.h new file mode 100644 index 0000000000..b25bde1960 --- /dev/null +++ b/src/qdmi/DeviceState.h @@ -0,0 +1,88 @@ +/* + * 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.h" +#include "qdmi/Device.hpp" +#include "qdmi/DeviceRegistry.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 SessionParameters& 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; + SessionParameters parameters; + std::vector> children; + + DeviceState(std::shared_ptr deviceApi, + const SessionParameters& 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(); + 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 auto create(std::shared_ptr api, + const SessionParameters& parameters = {}) + -> Device { + return Device(std::make_shared(std::move(api), parameters)); + } +}; + +} // namespace qdmi::detail diff --git a/src/qdmi/driver/CMakeLists.txt b/src/qdmi/driver/CMakeLists.txt deleted file mode 100644 index a83cddd967..0000000000 --- a/src/qdmi/driver/CMakeLists.txt +++ /dev/null @@ -1,52 +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) - - # Add link libraries - target_link_libraries( - ${TARGET_NAME} - PUBLIC qdmi::qdmi MQT::CoreQDMICommon - PRIVATE qdmi::qdmi_project_warnings nlohmann_json::nlohmann_json spdlog::spdlog - ${CMAKE_DL_LIBS}) - target_include_directories(${TARGET_NAME} SYSTEM - PRIVATE ${PROJECT_SOURCE_DIR}/vendor/tomlplusplus) - - 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 9bc695e16b..0000000000 --- a/src/qdmi/driver/Driver.cpp +++ /dev/null @@ -1,848 +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 - -#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); \ - } \ - } - //===----------------------------------------------------------------------===// - - 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_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; - } -} - -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.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); - 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::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_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"); - } -} -} // 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::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); -} - -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/CMakeLists.txt b/test/CMakeLists.txt index a51f3a6127..eccf7f16cc 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -15,9 +15,6 @@ add_subdirectory(na) add_subdirectory(zx) add_subdirectory(qir) add_subdirectory(qdmi) -if(BUILD_MQT_CORE_QDMI_NA_DEVICE) - add_subdirectory(fomac) -endif() # copy test circuits to build directory file(COPY ${PROJECT_SOURCE_DIR}/test/circuits DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/test/fomac/CMakeLists.txt b/test/fomac/CMakeLists.txt deleted file mode 100644 index 5d435c3927..0000000000 --- a/test/fomac/CMakeLists.txt +++ /dev/null @@ -1,14 +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-fomac-test) - -if(TARGET MQT::CoreFoMaC) - package_add_test(${TARGET_NAME} MQT::CoreFoMaC test_fomac.cpp) - mqt_copy_qdmi_runtime(${TARGET_NAME}) -endif() diff --git a/test/na/CMakeLists.txt b/test/na/CMakeLists.txt index 017ad42d0d..33774496c7 100644 --- a/test/na/CMakeLists.txt +++ b/test/na/CMakeLists.txt @@ -13,5 +13,5 @@ if(TARGET MQT::CoreNA) endif() if(BUILD_MQT_CORE_QDMI_NA_DEVICE) - add_subdirectory(fomac) + add_subdirectory(qdmi) endif() diff --git a/test/na/fomac/CMakeLists.txt b/test/na/fomac/CMakeLists.txt deleted file mode 100644 index 05228857df..0000000000 --- a/test/na/fomac/CMakeLists.txt +++ /dev/null @@ -1,18 +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 - -if(TARGET MQT::CoreNAFoMaC) - # Set test target name - set(TARGET_NAME ${MQT_CORE_TARGET_NAME}-na-fomac-test) - # Add the test executable - package_add_test(${TARGET_NAME} MQT::CoreNAFoMaC test_fomac.cpp) - # Set the device json path - target_compile_definitions(${TARGET_NAME} - PRIVATE NA_DEVICE_JSON="${PROJECT_SOURCE_DIR}/json/na/device.json") - mqt_copy_qdmi_runtime(${TARGET_NAME}) -endif() diff --git a/test/na/qdmi/CMakeLists.txt b/test/na/qdmi/CMakeLists.txt new file mode 100644 index 0000000000..6df90f767b --- /dev/null +++ b/test/na/qdmi/CMakeLists.txt @@ -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 + +if(TARGET MQT::CoreNAQDMI) + # Set test target name + set(TARGET_NAME ${MQT_CORE_TARGET_NAME}-na-qdmi-test) + # Add the test executable + package_add_test(${TARGET_NAME} MQT::CoreNAQDMI test_qdmi.cpp) + # Set the device json path + target_compile_definitions( + ${TARGET_NAME} PRIVATE NA_DEVICE_JSON="${PROJECT_SOURCE_DIR}/json/na/device.json" + NA_DEVICE_LIBRARY="$") + mqt_copy_qdmi_runtime(${TARGET_NAME}) +endif() diff --git a/test/na/fomac/test_fomac.cpp b/test/na/qdmi/test_qdmi.cpp similarity index 62% rename from test/na/fomac/test_fomac.cpp rename to test/na/qdmi/test_qdmi.cpp index 384fc6e7de..f8d5773867 100644 --- a/test/na/fomac/test_fomac.cpp +++ b/test/na/qdmi/test_qdmi.cpp @@ -8,12 +8,15 @@ * Licensed under the MIT License */ -#include "na/fomac/Device.hpp" +#include "na/qdmi/Device.hpp" +#include "qdmi/DeviceManager.hpp" +#include "qdmi/DeviceRegistry.hpp" #include #include #include +#include #include namespace na { @@ -28,27 +31,42 @@ auto canonicallyOrderLatticeVectors(nlohmann::json& device) -> void { } } } + +auto getDevice() -> qdmi::Device { + ::qdmi::DeviceRegistry registry({{ + .id = "mqt.na.default", + .library = NA_DEVICE_LIBRARY, + .prefix = "MQT_NA", + }}); + auto device = + ::qdmi::DeviceManager(std::move(registry)).open("mqt.na.default"); + auto converted = qdmi::Device::tryCreateFromDevice(device); + if (!converted) { + throw std::runtime_error("Built-in NA device is missing required metadata"); + } + return *std::move(converted); +} } // namespace // ignore the linter warning regarding nlohmann::json and the compile time // definitions // NOLINTBEGIN(misc-include-cleaner) -TEST(TestNAFoMaC, TrapsJSONRoundTrip) { - nlohmann::json fomacDevice; +TEST(TestNAQDMI, TrapsJSONRoundTrip) { + nlohmann::json expectedDevice; // Open the file std::ifstream file(NA_DEVICE_JSON); ASSERT_TRUE(file.is_open()) << "Failed to open json file: " NA_DEVICE_JSON; // Parse the JSON file try { - fomacDevice = nlohmann::json::parse(file); + expectedDevice = nlohmann::json::parse(file); } catch (const nlohmann::json::parse_error& e) { GTEST_FAIL() << "JSON parsing error: " << e.what(); } - nlohmann::json qdmiDevice = Session::getDevices().front(); - canonicallyOrderLatticeVectors(fomacDevice); - canonicallyOrderLatticeVectors(qdmiDevice); - EXPECT_EQ(fomacDevice["traps"], qdmiDevice["traps"]); + nlohmann::json actualDevice = getDevice(); + canonicallyOrderLatticeVectors(expectedDevice); + canonicallyOrderLatticeVectors(actualDevice); + EXPECT_EQ(expectedDevice["traps"], actualDevice["traps"]); } -TEST(TestNAFoMaC, FullJSONRoundTrip) { +TEST(TestNAQDMI, FullJSONRoundTrip) { nlohmann::json jsonDevice; // Open the file std::ifstream file(NA_DEVICE_JSON); @@ -59,10 +77,10 @@ TEST(TestNAFoMaC, FullJSONRoundTrip) { } catch (const nlohmann::json::parse_error& e) { GTEST_FAIL() << "JSON parsing error: " << e.what(); } - nlohmann::json fomacDevice = Session::getDevices().front(); + nlohmann::json qdmiDevice = getDevice(); canonicallyOrderLatticeVectors(jsonDevice); - canonicallyOrderLatticeVectors(fomacDevice); - EXPECT_EQ(jsonDevice, fomacDevice); + canonicallyOrderLatticeVectors(qdmiDevice); + EXPECT_EQ(jsonDevice, qdmiDevice); } // NOLINTEND(misc-include-cleaner) } // namespace na diff --git a/test/python/conftest.py b/test/python/conftest.py new file mode 100644 index 0000000000..23cd8f1068 --- /dev/null +++ b/test/python/conftest.py @@ -0,0 +1,39 @@ +# 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 + +"""Shared fixtures for installed-package QDMI integration tests.""" + +from __future__ import annotations + +import pytest + +from mqt.core import qdmi + + +def _open_isolated(device_id: str) -> qdmi.Device: + discovery = qdmi.DeviceManager() + definition = next( + (candidate for candidate in discovery.definitions if candidate.device_id == device_id), + None, + ) + if definition is None: + pytest.skip(f"QDMI device '{device_id}' is not installed") + registry = qdmi.DeviceRegistry([definition]) + return qdmi.DeviceManager(registry).open(device_id) + + +@pytest.fixture(scope="session") +def ddsim_device() -> qdmi.Device: + """Return the packaged DDSIM device through an isolated definition.""" + return _open_isolated("mqt.ddsim.default") + + +@pytest.fixture(scope="session") +def na_device() -> qdmi.Device: + """Return the packaged neutral-atom device through an isolated definition.""" + return _open_isolated("mqt.na.default") diff --git a/test/python/na/test_na_fomac.py b/test/python/na/test_na_qdmi.py similarity index 97% rename from test/python/na/test_na_fomac.py rename to test/python/na/test_na_qdmi.py index 85cc896158..90fed20cc0 100644 --- a/test/python/na/test_na_fomac.py +++ b/test/python/na/test_na_qdmi.py @@ -16,20 +16,21 @@ import pytest -from mqt.core.na.fomac import devices +from mqt.core.na.qdmi import Device +from mqt.core.qdmi import DeviceManager if TYPE_CHECKING: from collections.abc import Mapping - from mqt.core.na.fomac import Device - @pytest.fixture def device_tuple() -> tuple[Device, Mapping[str, Any]]: - """Return a neutral atom FoMaC device instance.""" + """Return a neutral atom QDMI device instance.""" with pathlib.Path("json/na/device.json").open(encoding="utf-8") as f: device_dict = load(f) - return next(iter(devices())), device_dict + device = Device.try_create_from_device(DeviceManager().open("mqt.na.default")) + assert device is not None + return device, device_dict def test_name(device_tuple: tuple[Device, Mapping[str, Any]]) -> None: diff --git a/test/python/plugins/qiskit/test_backend.py b/test/python/plugins/qiskit/test_backend.py index 6f0a5143b6..5a93ae736e 100644 --- a/test/python/plugins/qiskit/test_backend.py +++ b/test/python/plugins/qiskit/test_backend.py @@ -19,7 +19,6 @@ from qiskit.circuit.library import UnitaryGate from qiskit.providers import JobStatus -from mqt.core import fomac from mqt.core.plugins.qiskit import ( CircuitValidationError, QDMIBackend, @@ -28,8 +27,9 @@ from mqt.core.plugins.qiskit.exceptions import UnsupportedDeviceError if TYPE_CHECKING: + from mqt.core import qdmi - class _FomacDeviceLike(Protocol): # pragma: no cover - typing helper to fix mypy errors + class _QDMIDeviceLike(Protocol): # pragma: no cover - typing helper to fix mypy errors def name(self) -> str: ... def version(self) -> str: ... @@ -40,24 +40,19 @@ def operations(self) -> list[object]: ... def coupling_map(self) -> object: ... - SiteSpecificDevice = _FomacDeviceLike - MisconfiguredDevice = _FomacDeviceLike - ZonedDevice = _FomacDeviceLike + SiteSpecificDevice = _QDMIDeviceLike + MisconfiguredDevice = _QDMIDeviceLike + ZonedDevice = _QDMIDeviceLike @pytest.fixture -def ddsim_backend() -> QDMIBackend: +def ddsim_backend(ddsim_device: qdmi.Device) -> QDMIBackend: """Get a QDMIBackend for the DDSIM device. Returns: A QDMIBackend instance wrapping the DDSIM device. """ - session = fomac.Session() - devices = session.get_devices() - for device in devices: - if "DDSIM" in device.name(): - return QDMIBackend(device=device, provider=None) - pytest.skip("DDSIM device not available") + return QDMIBackend(device=ddsim_device, provider=None) def test_backend_instantiation(ddsim_backend: QDMIBackend) -> None: @@ -613,13 +608,7 @@ def test_backend_openqasm3_translation_works_for_native_gates(ddsim_backend: QDM assert sum(counts.values()) == 100 -def test_zoned_operation_rejected_at_backend_init() -> None: +def test_zoned_operation_rejected_at_backend_init(na_device: qdmi.Device) -> None: """Backend rejects devices exposing zoned operations.""" - session = fomac.Session() - devices = session.get_devices() - for device in devices: - if device.name().startswith("MQT NA"): - with pytest.raises(UnsupportedDeviceError, match="cannot be represented in Qiskit's Target model"): - QDMIBackend(device) - return - pytest.skip("NA device not available") + with pytest.raises(UnsupportedDeviceError, match="cannot be represented in Qiskit's Target model"): + QDMIBackend(na_device) diff --git a/test/python/plugins/qiskit/test_estimator.py b/test/python/plugins/qiskit/test_estimator.py index f296803c08..93c5f08d21 100644 --- a/test/python/plugins/qiskit/test_estimator.py +++ b/test/python/plugins/qiskit/test_estimator.py @@ -10,6 +10,8 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import numpy as np import pytest from qiskit import QuantumCircuit @@ -17,20 +19,17 @@ from qiskit.primitives.containers.estimator_pub import EstimatorPub from qiskit.quantum_info import SparsePauliOp -from mqt.core import fomac from mqt.core.plugins.qiskit import QDMIBackend, QDMIEstimator +if TYPE_CHECKING: + from mqt.core import qdmi + @pytest.fixture -def estimator() -> QDMIEstimator: +def estimator(ddsim_device: qdmi.Device) -> QDMIEstimator: """Returns a QDMIEstimator based on the DDSIM backend.""" - session = fomac.Session() - devices = session.get_devices() - for device in devices: - if "DDSIM" in device.name(): - backend = QDMIBackend(device=device, provider=None) - return QDMIEstimator(backend) - pytest.skip("DDSIM device not available") + backend = QDMIBackend(device=ddsim_device, provider=None) + return QDMIEstimator(backend) def test_estimator_run_simple_observable(estimator: QDMIEstimator) -> None: diff --git a/test/python/plugins/qiskit/test_mock_backend.py b/test/python/plugins/qiskit/test_mock_backend.py index 52922f21ef..1460d131ca 100644 --- a/test/python/plugins/qiskit/test_mock_backend.py +++ b/test/python/plugins/qiskit/test_mock_backend.py @@ -15,6 +15,7 @@ import secrets import string import warnings +from types import SimpleNamespace from typing import TYPE_CHECKING, NoReturn import numpy as np @@ -22,7 +23,7 @@ from qiskit import qasm2, qasm3 from qiskit.circuit import Clbit, Parameter, QuantumCircuit -from mqt.core import fomac +from mqt.core import qdmi from mqt.core.plugins.qiskit import ( MoveGate, QDMIBackend, @@ -40,7 +41,7 @@ class MockQDMIDevice: """Mock QDMI device for testing with configurable properties and job execution. - This class implements the FoMaC device interface for testing purposes, + This class implements the QDMI device interface for testing purposes, providing configurable device properties and mock job execution. """ @@ -138,7 +139,7 @@ def is_zoned(self) -> bool: return self._zoned class MockJob: - """Mock FoMaC job with simulated results.""" + """Mock QDMI job with simulated results.""" def __init__(self, num_clbits: int, shots: int) -> None: """Initialize mock job with number of classical bits and shots.""" @@ -146,7 +147,7 @@ def __init__(self, num_clbits: int, shots: int) -> None: self._shots = shots alphabet = string.ascii_lowercase + string.digits self._id = "mock-job-" + "".join(secrets.choice(alphabet) for _ in range(8)) - self._status = fomac.Job.Status.DONE + self._status = qdmi.Job.Status.DONE self._counts: dict[str, int] | None = None @property @@ -159,7 +160,7 @@ def num_shots(self) -> int: """The number of shots.""" return self._shots - def check(self) -> fomac.Job.Status: + def check(self) -> qdmi.Job.Status: """Return job status.""" return self._status @@ -262,11 +263,11 @@ def coupling_map(self) -> list[tuple[MockSite, MockSite]] | None: return self._coupling_map @staticmethod - def supported_program_formats() -> list[fomac.ProgramFormat]: + def supported_program_formats() -> list[qdmi.ProgramFormat]: """Return list of supported program formats.""" - return [fomac.ProgramFormat.QASM2, fomac.ProgramFormat.QASM3] + return [qdmi.ProgramFormat.QASM2, qdmi.ProgramFormat.QASM3] - def submit_job(self, program: str, program_format: fomac.ProgramFormat, num_shots: int) -> MockJob: # ruff:ignore[unused-method-argument] + def submit_job(self, program: str, program_format: qdmi.ProgramFormat, num_shots: int) -> MockJob: """Submit a mock job to the device. Args: @@ -277,6 +278,7 @@ def submit_job(self, program: str, program_format: fomac.ProgramFormat, num_shot Returns: A mock job with simulated results. """ + del program_format # Parse the number of classical bits from a QASM program. # Look for "creg [];" pattern in QASM2 @@ -318,13 +320,43 @@ def test_custom_device(mock_qdmi_device_factory): return MockQDMIDevice -def _patch_session_devices(monkeypatch: pytest.MonkeyPatch, devices: list[MockQDMIDevice]) -> None: - """Helper to monkeypatch fomac.Session.get_devices to return the given devices list.""" +def _patch_manager_devices( + monkeypatch: pytest.MonkeyPatch, + devices: list[MockQDMIDevice], + errors: dict[str, str] | None = None, +) -> None: + """Patch QDMI device discovery to return the supplied mock devices.""" - def _mock_get_devices(_self: object) -> list[MockQDMIDevice]: - return devices + class MockManager: + def __init__(self) -> None: + self.definitions = [SimpleNamespace(device_id=f"mock-{index}") for index in range(len(devices))] + self.devices = devices - monkeypatch.setattr(fomac.Session, "get_devices", _mock_get_devices) + def open(self, device_id: str, **_kwargs: object) -> MockQDMIDevice: + return self.devices[int(device_id.removeprefix("mock-"))] + + def open_all(self, **_kwargs: object) -> SimpleNamespace: + return SimpleNamespace( + devices={f"mock-{index}": device for index, device in enumerate(self.devices)}, errors=errors or {} + ) + + monkeypatch.setattr(qdmi, "DeviceManager", MockManager) + + +def test_provider_keeps_openable_devices_after_partial_discovery_failure( + monkeypatch: pytest.MonkeyPatch, mock_qdmi_device_factory: type[MockQDMIDevice] +) -> None: + """Provider retains successful bulk-open results when another definition fails.""" + mock_device = mock_qdmi_device_factory( + name="Available Device", + num_qubits=2, + operations=["h", "measure"], + ) + _patch_manager_devices(monkeypatch, [mock_device], {"unavailable": "provider failed"}) + + provider = QDMIProvider() + + assert provider.get_backend("Available Device").name == "Available Device" def test_backend_warns_on_unmappable_operation( @@ -338,8 +370,7 @@ def test_backend_warns_on_unmappable_operation( operations=["cz", "custom_unmappable_gate", "measure"], ) - # Use helper to patch Session.get_devices - _patch_session_devices(monkeypatch, [mock_device]) + _patch_manager_devices(monkeypatch, [mock_device]) # Creating backend should trigger warning about unmappable operation with warnings.catch_warnings(record=True) as w: @@ -365,7 +396,7 @@ def test_backend_exposes_move_operation( num_qubits=2, operations=["move", "measure"], ) - _patch_session_devices(monkeypatch, [mock_device]) + _patch_manager_devices(monkeypatch, [mock_device]) provider = QDMIProvider() backend = provider.get_backend("Test Device") @@ -384,8 +415,7 @@ def test_backend_warns_on_missing_measurement_operation( operations=["cz"], # No measure operation ) - # Use helper to patch Session.get_devices - _patch_session_devices(monkeypatch, [mock_device]) + _patch_manager_devices(monkeypatch, [mock_device]) # Creating backend should trigger warning about missing measurement operation with warnings.catch_warnings(record=True) as w: @@ -412,7 +442,7 @@ def test_backend_exposes_move_gate( operations=["move", "cz", "measure"], ) - _patch_session_devices(monkeypatch, [mock_device]) + _patch_manager_devices(monkeypatch, [mock_device]) provider = QDMIProvider() backend = provider.get_backend("Test Device with MOVE") @@ -446,9 +476,11 @@ def test_backend_qasm3_conversion_success(mock_qdmi_device_factory: type[MockQDM device = mock_qdmi_device_factory(num_qubits=2, operations=["h", "cx", "measure"]) backend = QDMIBackend(device) # ty: ignore[invalid-argument-type] - program, fmt = backend._convert_circuit(qc, [fomac.ProgramFormat.QASM3]) # ruff:ignore[private-member-access] + program, fmt = backend._convert_circuit( # ruff:ignore[private-member-access] + qc, [qdmi.ProgramFormat.QASM3] + ) - assert fmt == fomac.ProgramFormat.QASM3 + assert fmt == qdmi.ProgramFormat.QASM3 assert "OPENQASM 3" in program assert "h q[0]" in program assert "cx q[0], q[1]" in program @@ -464,9 +496,11 @@ def test_backend_qasm2_conversion_success(mock_qdmi_device_factory: type[MockQDM device = mock_qdmi_device_factory(num_qubits=2, operations=["h", "cx", "measure"]) backend = QDMIBackend(device) # ty: ignore[invalid-argument-type] - program, fmt = backend._convert_circuit(qc, [fomac.ProgramFormat.QASM2]) # ruff:ignore[private-member-access] + program, fmt = backend._convert_circuit( # ruff:ignore[private-member-access] + qc, [qdmi.ProgramFormat.QASM2] + ) - assert fmt == fomac.ProgramFormat.QASM2 + assert fmt == qdmi.ProgramFormat.QASM2 assert "OPENQASM 2.0" in program assert "h q[0]" in program assert "cx q[0],q[1]" in program @@ -482,9 +516,11 @@ def test_backend_qasm3_preferred_over_qasm2(mock_qdmi_device_factory: type[MockQ backend = QDMIBackend(device) # ty: ignore[invalid-argument-type] # When both formats are available, QASM3 should be chosen - program, fmt = backend._convert_circuit(qc, [fomac.ProgramFormat.QASM2, fomac.ProgramFormat.QASM3]) # ruff:ignore[private-member-access] + program, fmt = backend._convert_circuit( # ruff:ignore[private-member-access] + qc, [qdmi.ProgramFormat.QASM2, qdmi.ProgramFormat.QASM3] + ) - assert fmt == fomac.ProgramFormat.QASM3 + assert fmt == qdmi.ProgramFormat.QASM3 assert "OPENQASM 3" in program @@ -492,12 +528,13 @@ def test_backend_uses_iqm_json_when_supported(mock_qdmi_device_factory: type[Moc """Test that backend uses IQM JSON format when supported.""" device = mock_qdmi_device_factory(num_qubits=2, operations=["r", "cz", "measure"]) - submitted_format: fomac.ProgramFormat | None = None + submitted_format: qdmi.ProgramFormat | None = None - def mock_supported_formats() -> list[fomac.ProgramFormat]: - return [fomac.ProgramFormat.IQM_JSON, fomac.ProgramFormat.QASM3] + def mock_supported_formats() -> list[qdmi.ProgramFormat]: + return [qdmi.ProgramFormat.IQM_JSON, qdmi.ProgramFormat.QASM3] - def mock_submit_job(program: str, program_format: fomac.ProgramFormat, num_shots: int) -> MockQDMIDevice.MockJob: # ruff:ignore[unused-function-argument] + def mock_submit_job(program: str, program_format: qdmi.ProgramFormat, num_shots: int) -> MockQDMIDevice.MockJob: + del program nonlocal submitted_format submitted_format = program_format return device.MockJob(num_clbits=2, shots=num_shots) @@ -513,19 +550,20 @@ def mock_submit_job(program: str, program_format: fomac.ProgramFormat, num_shots backend.run(qc, shots=100) - assert submitted_format == fomac.ProgramFormat.IQM_JSON + assert submitted_format == qdmi.ProgramFormat.IQM_JSON def test_backend_iqm_json_preferred_over_qasm(mock_qdmi_device_factory: type[MockQDMIDevice]) -> None: """Test that IQM JSON takes priority over QASM formats.""" device = mock_qdmi_device_factory(num_qubits=2, operations=["r", "cz", "measure"]) - submitted_format: fomac.ProgramFormat | None = None + submitted_format: qdmi.ProgramFormat | None = None - def mock_supported_formats() -> list[fomac.ProgramFormat]: - return [fomac.ProgramFormat.QASM2, fomac.ProgramFormat.QASM3, fomac.ProgramFormat.IQM_JSON] + def mock_supported_formats() -> list[qdmi.ProgramFormat]: + return [qdmi.ProgramFormat.QASM2, qdmi.ProgramFormat.QASM3, qdmi.ProgramFormat.IQM_JSON] - def mock_submit_job(program: str, program_format: fomac.ProgramFormat, num_shots: int) -> MockQDMIDevice.MockJob: # ruff:ignore[unused-function-argument] + def mock_submit_job(program: str, program_format: qdmi.ProgramFormat, num_shots: int) -> MockQDMIDevice.MockJob: + del program nonlocal submitted_format submitted_format = program_format return device.MockJob(num_clbits=2, shots=num_shots) @@ -541,20 +579,20 @@ def mock_submit_job(program: str, program_format: fomac.ProgramFormat, num_shots backend.run(qc, shots=100) - assert submitted_format == fomac.ProgramFormat.IQM_JSON + assert submitted_format == qdmi.ProgramFormat.IQM_JSON @pytest.mark.parametrize( ("qasm_module_name", "program_format"), [ - ("qasm3", fomac.ProgramFormat.QASM3), - ("qasm2", fomac.ProgramFormat.QASM2), + ("qasm3", qdmi.ProgramFormat.QASM3), + ("qasm2", qdmi.ProgramFormat.QASM2), ], ) def test_backend_qasm_conversion_failure( monkeypatch: pytest.MonkeyPatch, qasm_module_name: str, - program_format: fomac.ProgramFormat, + program_format: qdmi.ProgramFormat, mock_qdmi_device_factory: type[MockQDMIDevice], ) -> None: """Backend should raise TranslationError when QASM conversion fails.""" @@ -591,14 +629,31 @@ def test_backend_unsupported_format_error(mock_qdmi_device_factory: type[MockQDM with pytest.raises( UnsupportedFormatError, match="No conversion from Qiskit to any of the supported program formats" ): - backend._convert_circuit(qc, [fomac.ProgramFormat.QPY]) # ruff:ignore[private-member-access] + backend._convert_circuit( # ruff:ignore[private-member-access] + qc, [qdmi.ProgramFormat.QPY] + ) def test_map_operation_returns_none_for_unknown() -> None: - """Unknown FoMaC operations cannot be mapped to Qiskit gates.""" - assert QDMIBackend._map_operation_to_gate("unknown_gate") is None # ruff:ignore[private-member-access] - assert QDMIBackend._map_operation_to_gate("custom_op") is None # ruff:ignore[private-member-access] - assert QDMIBackend._map_operation_to_gate("") is None # ruff:ignore[private-member-access] + """Unknown QDMI operations cannot be mapped to Qiskit gates.""" + assert ( + QDMIBackend._map_operation_to_gate( # ruff:ignore[private-member-access] + "unknown_gate" + ) + is None + ) + assert ( + QDMIBackend._map_operation_to_gate( # ruff:ignore[private-member-access] + "custom_op" + ) + is None + ) + assert ( + QDMIBackend._map_operation_to_gate( # ruff:ignore[private-member-access] + "" + ) + is None + ) def test_map_operation_to_move_gate() -> None: @@ -658,8 +713,7 @@ def test_backend_validation_uses_inverse_mapping( operations=["prx", "cz", "measure"], # Uses 'prx' instead of 'r' ) - # Use helper to patch Session.get_devices - _patch_session_devices(monkeypatch, [mock_device]) + _patch_manager_devices(monkeypatch, [mock_device]) provider = QDMIProvider() backend = provider.get_backend("Test Device with PRX") diff --git a/test/python/plugins/qiskit/test_provider.py b/test/python/plugins/qiskit/test_provider.py index f6e6480b6f..3dd055b03a 100644 --- a/test/python/plugins/qiskit/test_provider.py +++ b/test/python/plugins/qiskit/test_provider.py @@ -10,14 +10,17 @@ from __future__ import annotations -import tempfile -from pathlib import Path +from types import SimpleNamespace +from typing import TYPE_CHECKING import pytest -from mqt.core import fomac +from mqt.core import qdmi from mqt.core.plugins.qiskit import QDMIProvider +if TYPE_CHECKING: + from pathlib import Path + def test_provider_backends_filter_by_name() -> None: """Provider can filter backends by name substring.""" @@ -79,11 +82,15 @@ def test_provider_get_backend_nonexistent() -> None: def test_provider_get_backend_no_devices(monkeypatch: pytest.MonkeyPatch) -> None: """Provider raises ValueError when no devices available.""" - # Monkeypatch to return empty device list - def mock_get_devices(_self: object) -> list[object]: - return [] + class EmptyManager: + def __init__(self) -> None: + self.definitions: list[object] = [] + + @staticmethod + def open_all(**_kwargs: object) -> object: + return SimpleNamespace(devices={}, errors={}) - monkeypatch.setattr(fomac.Session, "get_devices", mock_get_devices) + monkeypatch.setattr(qdmi, "DeviceManager", EmptyManager) provider = QDMIProvider() with pytest.raises(ValueError, match="No backend found with name"): @@ -119,25 +126,28 @@ def test_provider_with_token_parameter() -> None: pass -def test_provider_with_auth_file_parameter() -> None: - """Provider accepts auth_file parameter.""" - # Create a temporary file for testing - with tempfile.NamedTemporaryFile(delete=False, mode="w", encoding="utf-8") as tmp_file: - tmp_file.write("test_auth_content") - tmp_path = tmp_file.name +def test_provider_with_auth_file_parameter(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + """Provider accepts and forwards a path-like auth_file parameter.""" + received_parameters: qdmi.SessionParameters | None = None - try: - # Should not raise an error when creating provider with auth_file - # Note: The currently available QDMI devices don't support authentication. - try: - provider = QDMIProvider(auth_file=tmp_path) - assert provider is not None - except RuntimeError: - # If not supported, that's okay for now - pass - finally: - # Clean up - Path(tmp_path).unlink(missing_ok=True) + class EmptyManager: + def __init__(self) -> None: + self.definitions: list[object] = [] + + @staticmethod + def open_all(*, session_overrides: qdmi.SessionParameters) -> object: + nonlocal received_parameters + received_parameters = session_overrides + return SimpleNamespace(devices={}, errors={}) + + monkeypatch.setattr(qdmi, "DeviceManager", EmptyManager) + auth_file = tmp_path / "credentials.json" + + provider = QDMIProvider(auth_file=auth_file) + + assert provider.backends() == [] + assert received_parameters is not None + assert received_parameters.auth_file == auth_file def test_provider_with_auth_url_parameter() -> None: @@ -164,18 +174,6 @@ def test_provider_with_username_password_parameters() -> None: pass -def test_provider_with_project_id_parameter() -> None: - """Provider accepts project_id parameter.""" - # Should not raise an error when creating provider with project_id - # Note: The currently available QDMI devices don't support authentication. - try: - provider = QDMIProvider(project_id="test_project") - assert provider is not None - except RuntimeError: - # If not supported, that's okay for now - pass - - def test_provider_with_multiple_auth_parameters() -> None: """Provider accepts multiple authentication parameters.""" # Should not raise an error when creating provider with multiple auth parameters @@ -185,7 +183,6 @@ def test_provider_with_multiple_auth_parameters() -> None: token="test_token", # ruff:ignore[hardcoded-password-func-arg] username="test_user", password="test_pass", # ruff:ignore[hardcoded-password-func-arg] - project_id="test_project", ) assert provider is not None except RuntimeError: @@ -239,7 +236,6 @@ def test_provider_with_custom_parameters() -> None: provider = QDMIProvider( token="test_token", # ruff:ignore[hardcoded-password-func-arg] custom1="custom_value", - project_id="project_id", ) assert provider is not None except (RuntimeError, ValueError): diff --git a/test/python/plugins/qiskit/test_sampler.py b/test/python/plugins/qiskit/test_sampler.py index 63cfc5f4d2..8be7b31854 100644 --- a/test/python/plugins/qiskit/test_sampler.py +++ b/test/python/plugins/qiskit/test_sampler.py @@ -11,26 +11,24 @@ from __future__ import annotations from collections import Counter +from typing import TYPE_CHECKING import numpy as np import pytest from qiskit import QuantumCircuit from qiskit.circuit import ClassicalRegister, Parameter -from mqt.core import fomac from mqt.core.plugins.qiskit import QDMIBackend, QDMISampler +if TYPE_CHECKING: + from mqt.core import qdmi + @pytest.fixture -def sampler() -> QDMISampler: +def sampler(ddsim_device: qdmi.Device) -> QDMISampler: """Returns a QDMISampler based on the DDSIM backend.""" - session = fomac.Session() - devices = session.get_devices() - for device in devices: - if "DDSIM" in device.name(): - backend = QDMIBackend(device=device, provider=None) - return QDMISampler(backend) - pytest.skip("DDSIM device not available") + backend = QDMIBackend(device=ddsim_device, provider=None) + return QDMISampler(backend) def test_sampler_run_simple_circuit(sampler: QDMISampler) -> None: diff --git a/test/python/fomac/test_fomac.py b/test/python/qdmi/test_qdmi.py similarity index 74% rename from test/python/fomac/test_fomac.py rename to test/python/qdmi/test_qdmi.py index 4604c1e67c..1116a5439c 100644 --- a/test/python/fomac/test_fomac.py +++ b/test/python/qdmi/test_qdmi.py @@ -10,35 +10,97 @@ from __future__ import annotations -import tempfile from pathlib import Path from typing import cast import pytest -from mqt.core.fomac import ( +from mqt.core.qdmi import ( CustomProperty, Device, DeviceDefinition, + DeviceManager, + DeviceRegistry, Job, + OpenAllResult, ProgramFormat, - Session, - open_device, - register_device, - register_device_if_absent, + SessionParameters, ) CustomValueType = type[str] | type[bool] | type[int] | type[float] | type[bytes] def _get_devices() -> list[Device]: - """Get all available devices from a Session. + """Open each available QDMI device. Returns: List of all available QDMI devices. """ - session = Session() - return session.get_devices() + result = DeviceManager().open_all() + return list(result.devices.values()) + + +def test_device_ids_and_bulk_open_result() -> None: + """Stable Python device IDs support named bulk-open results.""" + discovery = DeviceManager() + definition = next( + (candidate for candidate in discovery.definitions if candidate.device_id == "mqt.sc.default"), + None, + ) + if definition is None: + pytest.skip("SC device not installed") + bad = DeviceDefinition("missing", "does-not-exist", "MISSING") + manager = DeviceManager(DeviceRegistry([definition, bad])) + result = manager.open_all() + assert isinstance(result, OpenAllResult) + assert definition.device_id in result.devices + assert "missing" in result.errors + + +def test_device_definition_accepts_path_like_library() -> None: + """Device definitions preserve filesystem paths without loading them.""" + library = Path("/nonexistent/lib.so") + definition = DeviceDefinition("python.missing", library, "PREFIX") + assert definition.library == library + + registry = DeviceRegistry([definition]) + with pytest.raises(RuntimeError): + DeviceManager(registry).open(definition.device_id) + + +def test_session_parameters_support_keyword_construction() -> None: + """Session parameters accept optional keywords and preserve path values.""" + auth_file = Path("/path/to/auth.json") + credential = auth_file.name + parameters = SessionParameters( + base_url="https://example.com", + token=credential, + auth_file=auth_file, + auth_url="https://example.com/auth", + username="username", + password=credential, + custom1="one", + custom2="two", + custom3="three", + custom4="four", + custom5="five", + ) + + assert parameters.base_url == "https://example.com" + assert parameters.token == credential + assert parameters.auth_file == auth_file + assert parameters.auth_url == "https://example.com/auth" + assert parameters.username == "username" + assert parameters.password == credential + assert parameters.custom1 == "one" + assert parameters.custom2 == "two" + assert parameters.custom3 == "three" + assert parameters.custom4 == "four" + assert parameters.custom5 == "five" + + empty = SessionParameters() + assert empty.base_url is None + assert empty.auth_file is None @pytest.fixture(params=_get_devices()) @@ -554,6 +616,8 @@ def test_device_executes_binary_qir_program(ddsim_device: Device) -> None: def test_device_submit_job_handles_custom_parameters(ddsim_device: Device) -> None: """Test that submit_job forwards custom job parameters to DDSIM.""" + with pytest.raises(RuntimeError, match=r"Setting custom parameter: Not supported\."): + ddsim_device.submit_job("OPENQASM 3.0;", ProgramFormat.QASM3, 1, custom1=7) with pytest.raises(RuntimeError, match=r"Setting custom parameter: Not supported\."): ddsim_device.submit_job("OPENQASM 3.0;", ProgramFormat.QASM3, 1, custom1="value") with pytest.raises(RuntimeError, match=r"Setting custom parameter: Not supported\."): @@ -749,256 +813,3 @@ def test_simulator_job_get_sparse_probabilities_returns_valid_probabilities(simu assert "11" in sparse_probabilities assert sparse_probabilities["11"] == pytest.approx(0.5) - - -def test_session_construction_with_token() -> None: - """Test Session construction with a token parameter. - - Unsupported parameters are skipped during Session initialization, - so Session construction succeeds unless there's a critical error. - """ - # Empty token should be accepted - session = Session(token="") - assert session is not None - - # Non-empty token should be accepted - session = Session(token="test_token_123") # ruff:ignore[hardcoded-password-func-arg] - assert session is not None - - # Token with special characters should be accepted - session = Session(token="very_long_token_with_special_characters_!@#$%^&*()") # ruff:ignore[hardcoded-password-func-arg] - assert session is not None - - -def test_session_construction_with_auth_url() -> None: - """Test Session construction with auth URL parameter. - - Valid URLs should pass validation and Session construction should succeed - (even if the parameter is unsupported and skipped). Invalid URLs should - fail validation before attempting to set the parameter. - """ - # Valid HTTPS URL - session = Session(auth_url="https://example.com") - assert session is not None - - # Valid HTTP URL with port and path - session = Session(auth_url="http://auth.server.com:8080/api") - assert session is not None - - # Valid HTTPS URL with query parameters - session = Session(auth_url="https://auth.example.com/token?param=value") - assert session is not None - - # Valid localhost URL - session = Session(auth_url="http://localhost") - assert session is not None - - # Valid localhost URL with port - session = Session(auth_url="http://localhost:8080") - assert session is not None - - # Valid localhost URL with port and path - session = Session(auth_url="https://localhost:3000/auth/api") - assert session is not None - - # Invalid URL - not a URL at all - with pytest.raises(RuntimeError): - Session(auth_url="not-a-url") - - # Invalid URL - unsupported protocol - with pytest.raises(RuntimeError): - Session(auth_url="ftp://invalid.com") - - # Invalid URL - missing protocol - with pytest.raises(RuntimeError): - Session(auth_url="example.com") - - -def test_session_construction_with_auth_file() -> None: - """Test Session construction with auth file parameter. - - Existing files should pass validation and Session construction should succeed. - Non-existent files should fail validation before attempting to set the parameter. - """ - # Test with non-existent file - with pytest.raises(RuntimeError): - Session(auth_file="/nonexistent/path/to/file.txt") - - # Test with existing file - with tempfile.NamedTemporaryFile(encoding="utf-8", mode="w", delete=False, suffix=".txt") as tmp_file: - tmp_file.write("test_token_content") - tmp_path = tmp_file.name - - try: - # Both string and pathlib paths should be accepted. - string_session = Session(auth_file=tmp_path) - path_session = Session(auth_file=Path(tmp_path)) - assert string_session is not None - assert path_session is not None - finally: - # Clean up - Path(tmp_path).unlink(missing_ok=True) - - -def test_session_construction_with_username_password() -> None: - """Test Session construction with username and password parameters. - - Unsupported parameters are skipped, so construction should succeed. - """ - # Username only - session = Session(username="user123") - assert session is not None - - # Password only - session = Session(password="secure_password") # ruff:ignore[hardcoded-password-func-arg] - assert session is not None - - # Both username and password - session = Session(username="user123", password="secure_password") # ruff:ignore[hardcoded-password-func-arg] - assert session is not None - - -def test_session_construction_with_project_id() -> None: - """Test Session construction with project ID parameter. - - Unsupported parameters are skipped, so construction should succeed. - """ - session = Session(project_id="project-123-abc") - assert session is not None - - -def test_session_construction_with_multiple_parameters() -> None: - """Test Session construction with multiple authentication parameters. - - Unsupported parameters are skipped, so construction should succeed. - """ - session = Session( - token="test_token", # ruff:ignore[hardcoded-password-func-arg] - username="test_user", - password="test_pass", # ruff:ignore[hardcoded-password-func-arg] - project_id="test_project", - ) - assert session is not None - - -def test_session_construction_with_custom_parameters() -> None: - """Test Session construction with custom configuration parameters. - - 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 - try: - session = Session(custom1="custom_value_1") - assert session is not None - except (RuntimeError, ValueError): - pass - - # Test custom2 - try: - session = Session(custom2="custom_value_2") - assert session is not None - except (RuntimeError, ValueError): - pass - - # Test all custom parameters together - try: - session = Session( - custom1="value1", - custom2="value2", - custom3="value3", - custom4="value4", - custom5="value5", - ) - assert session is not None - except (RuntimeError, ValueError): - pass - - # Test mixing custom parameters with standard authentication - try: - session = Session( - token="test_token", # ruff:ignore[hardcoded-password-func-arg] - custom1="custom_value", - project_id="project_id", - ) - assert session is not None - except (RuntimeError, ValueError): - pass - - -def test_session_get_devices_returns_list() -> None: - """Test that get_devices() returns a list of Device objects.""" - session = Session() - devices = session.get_devices() - - assert isinstance(devices, list) - assert len(devices) > 0 - - # All elements should be Device instances - for device in devices: - assert isinstance(device, Device) - # Device should have a name - assert len(device.name()) > 0 - - -def test_session_multiple_instances() -> None: - """Test that multiple Session instances can be created independently.""" - session1 = Session() - session2 = Session() - - devices1 = session1.get_devices() - devices2 = session2.get_devices() - - # Both should return devices - assert len(devices1) > 0 - assert len(devices2) > 0 - - # Should return the same number of devices - assert len(devices1) == len(devices2) - - -def test_register_device_does_not_load_nonexistent_library() -> None: - """Registration stores metadata and opening performs native loading.""" - library_path = Path("/nonexistent/lib.so") - definition = DeviceDefinition("python.missing", library_path, "PREFIX") - assert definition.device_id == "python.missing" - assert definition.library_path == library_path - assert definition.prefix == "PREFIX" - register_device(definition) - with pytest.raises(RuntimeError): - open_device("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) - with pytest.raises(ValueError, match="library must not be empty"): - register_device_if_absent(DeviceDefinition("python.if-absent", "", "PREFIX")) - - -def test_open_device_rejects_unknown_id() -> None: - """Opening requires a stable registered ID.""" - with pytest.raises(IndexError, match="Unknown QDMI device ID"): - open_device("python.unknown") - - -def test_open_device_creates_a_fresh_session() -> None: - """Stable-ID opens should return separately owned sessions.""" - first = open_device("mqt.na.default") - second = open_device("mqt.na.default") - assert first != second - - -def test_site_keeps_fresh_session_alive() -> None: - """A site should remain usable after its device wrapper is destroyed.""" - site = open_device("mqt.na.default").sites()[0] - assert site.index() == 0 - - -def test_operation_keeps_fresh_session_alive() -> None: - """An operation should remain usable after its device wrapper is destroyed.""" - operation = open_device("mqt.na.default").operations()[0] - assert operation.name() diff --git a/test/qdmi/CMakeLists.txt b/test/qdmi/CMakeLists.txt index e7408fe449..648516673f 100644 --- a/test/qdmi/CMakeLists.txt +++ b/test/qdmi/CMakeLists.txt @@ -10,6 +10,9 @@ add_subdirectory(devices) if(BUILD_MQT_CORE_QDMI_DDSIM_DEVICE AND BUILD_MQT_CORE_QDMI_NA_DEVICE AND BUILD_MQT_CORE_QDMI_SC_DEVICE) - add_subdirectory(driver) + add_subdirectory(device) +endif() +if(BUILD_MQT_CORE_QDMI_SC_DEVICE) + add_subdirectory(manager) endif() add_subdirectory(registry) diff --git a/test/qdmi/driver/CMakeLists.txt b/test/qdmi/device/CMakeLists.txt similarity index 53% rename from test/qdmi/driver/CMakeLists.txt rename to test/qdmi/device/CMakeLists.txt index 3e373d44ee..4c3d6d79bc 100644 --- a/test/qdmi/driver/CMakeLists.txt +++ b/test/qdmi/device/CMakeLists.txt @@ -6,9 +6,19 @@ # # Licensed under the MIT License -set(TARGET_NAME mqt-core-qdmi-driver-test) +set(TARGET_NAME mqt-core-qdmi-object-model-test) + +if(TARGET MQT::CoreQDMI) + 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) -if(TARGET MQT::CoreQDMIDriver) add_library(mqt-core-qdmi-metadata-device SHARED metadata_device.cpp) set_target_properties( mqt-core-qdmi-metadata-device @@ -24,37 +34,21 @@ if(TARGET MQT::CoreQDMIDriver) "${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::CoreFoMaC) - 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.na.default\", \"library\": \"$\", \"prefix\": \"MQT_NA\"},\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" - ) - string(MAKE_C_IDENTIFIER "${TARGET_NAME}-mqt-core-qdmi-metadata-device" metadata_manifest_stem) - set(metadata_manifest_file - "$/${metadata_manifest_stem}.qdmi.json") + package_add_test(${TARGET_NAME} MQT::CoreQDMI test_device.cpp test_device_api.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 - "MQT_CORE_QDMI_TEST_CONFIG_FILE=\"${config_file}\"" - "MQT_CORE_QDMI_METADATA_MANIFEST=\"${metadata_manifest_file}\"" - "MQT_CORE_QDMI_SESSION_DEVICE=\"$\"" - "TEST_DEVICE_LIBRARIES=std::array{ std::pair{\"$\", \"MQT_NA\"}, std::pair{\"$\", \"MQT_SC\"}, std::pair{\"$\", \"MQT_DDSIM\"} }" - ) - + PRIVATE DDSIM_DEVICE_LIBRARY="$" + LIFECYCLE_DEVICE_LIBRARY="$" + NA_DEVICE_LIBRARY="$" + SC_DEVICE_LIBRARY="$") + add_dependencies(${TARGET_NAME} MQT::CoreQDMI_DDSIM_Device MQT::CoreQDMINaDevice + MQT::CoreQDMIScDevice) 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 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..459a62a94d --- /dev/null +++ b/test/qdmi/device/lifecycle_device.cpp @@ -0,0 +1,186 @@ +/* + * 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 { +std::mutex stateMutex; +std::condition_variable stateChanged; +bool blockFinalize = false; +bool finalizeStarted = false; +bool releaseFinalize = false; +std::size_t initializeCount = 0; +std::size_t finalizeCount = 0; +std::size_t overlappingInitializeCount = 0; +} // namespace + +extern "C" { + +TEST_LIFECYCLE_QDMI_EXPORT void TEST_LIFECYCLE_prepare_blocking_finalize() { + const std::scoped_lock lock(stateMutex); + blockFinalize = true; + finalizeStarted = false; + releaseFinalize = false; + initializeCount = 0; + finalizeCount = 0; + overlappingInitializeCount = 0; +} + +TEST_LIFECYCLE_QDMI_EXPORT bool +TEST_LIFECYCLE_wait_for_finalize(const std::size_t timeoutMs) { + std::unique_lock lock(stateMutex); + return stateChanged.wait_for(lock, std::chrono::milliseconds(timeoutMs), + [] { return finalizeStarted; }); +} + +TEST_LIFECYCLE_QDMI_EXPORT bool +TEST_LIFECYCLE_wait_for_initializations(const std::size_t expected, + const std::size_t timeoutMs) { + std::unique_lock lock(stateMutex); + return stateChanged.wait_for( + lock, std::chrono::milliseconds(timeoutMs), + [expected] { return initializeCount >= expected; }); +} + +TEST_LIFECYCLE_QDMI_EXPORT void TEST_LIFECYCLE_release_finalize() { + { + const std::scoped_lock lock(stateMutex); + releaseFinalize = true; + } + stateChanged.notify_all(); +} + +TEST_LIFECYCLE_QDMI_EXPORT std::size_t TEST_LIFECYCLE_initialize_count() { + const std::scoped_lock lock(stateMutex); + return initializeCount; +} + +TEST_LIFECYCLE_QDMI_EXPORT std::size_t TEST_LIFECYCLE_finalize_count() { + const std::scoped_lock lock(stateMutex); + return finalizeCount; +} + +TEST_LIFECYCLE_QDMI_EXPORT std::size_t +TEST_LIFECYCLE_overlapping_initialize_count() { + const std::scoped_lock lock(stateMutex); + return overlappingInitializeCount; +} + +TEST_LIFECYCLE_QDMI_EXPORT int TEST_LIFECYCLE_QDMI_device_initialize() { + { + const std::scoped_lock lock(stateMutex); + ++initializeCount; + if (finalizeStarted) { + ++overlappingInitializeCount; + } + } + stateChanged.notify_all(); + return QDMI_SUCCESS; +} + +TEST_LIFECYCLE_QDMI_EXPORT int TEST_LIFECYCLE_QDMI_device_finalize() { + std::unique_lock lock(stateMutex); + finalizeStarted = true; + stateChanged.notify_all(); + if (blockFinalize) { + stateChanged.wait(lock, [] { return releaseFinalize; }); + } + finalizeStarted = false; + ++finalizeCount; + stateChanged.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, + std::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, + std::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, std::size_t, + void*, std::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, + std::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, std::size_t, void*, + std::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, std::size_t, + void*, std::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, std::size_t, void*, std::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, + std::size_t, const TEST_LIFECYCLE_QDMI_Site*, std::size_t, const double*, + QDMI_Operation_Property, std::size_t, void*, std::size_t*) { + return QDMI_ERROR_NOTSUPPORTED; +} +// NOLINTEND(readability-named-parameter) + +} // extern "C" 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/fomac/test_fomac.cpp b/test/qdmi/device/test_device.cpp similarity index 66% rename from test/fomac/test_fomac.cpp rename to test/qdmi/device/test_device.cpp index 07d16ef437..1a67c59731 100644 --- a/test/fomac/test_fomac.cpp +++ b/test/qdmi/device/test_device.cpp @@ -8,55 +8,62 @@ * Licensed under the MIT License */ -#include "fomac/FoMaC.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 -namespace fomac { +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.na.default", + .library = NA_DEVICE_LIBRARY, + .prefix = "MQT_NA"}, + {.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 +93,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,132 +134,57 @@ 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"), + EXPECT_THROW(std::ignore = detail::decodeCustomValue(bytesOf(true), + "test property"), std::invalid_argument); } -TEST(FoMaCTest, StatusToString) { +TEST(QDMITest, StatusToString) { EXPECT_STREQ(qdmi::toString(QDMI_WARN_GENERAL), "General warning"); EXPECT_STREQ(qdmi::toString(QDMI_SUCCESS), "Success"); EXPECT_STREQ(qdmi::toString(QDMI_ERROR_FATAL), "A fatal error"); @@ -275,7 +201,7 @@ TEST(FoMaCTest, StatusToString) { EXPECT_STREQ(qdmi::toString(QDMI_ERROR_TIMEOUT), "Timeout"); } -TEST(FoMaCTest, SitePropertyToString) { +TEST(QDMITest, SitePropertyToString) { EXPECT_STREQ(qdmi::toString(QDMI_SITE_PROPERTY_INDEX), "INDEX"); EXPECT_STREQ(qdmi::toString(QDMI_SITE_PROPERTY_T1), "T1"); EXPECT_STREQ(qdmi::toString(QDMI_SITE_PROPERTY_T2), "T2"); @@ -298,7 +224,7 @@ TEST(FoMaCTest, SitePropertyToString) { EXPECT_STREQ(qdmi::toString(QDMI_SITE_PROPERTY_CUSTOM5), "CUSTOM5"); } -TEST(FoMaCTest, OperationPropertyToString) { +TEST(QDMITest, OperationPropertyToString) { EXPECT_STREQ(qdmi::toString(QDMI_OPERATION_PROPERTY_NAME), "NAME"); EXPECT_STREQ(qdmi::toString(QDMI_OPERATION_PROPERTY_QUBITSNUM), "QUBITS NUM"); EXPECT_STREQ(qdmi::toString(QDMI_OPERATION_PROPERTY_PARAMETERSNUM), @@ -322,7 +248,7 @@ TEST(FoMaCTest, OperationPropertyToString) { EXPECT_STREQ(qdmi::toString(QDMI_OPERATION_PROPERTY_CUSTOM5), "CUSTOM5"); } -TEST(FoMaCTest, DevicePropertyToString) { +TEST(QDMITest, DevicePropertyToString) { EXPECT_STREQ(qdmi::toString(QDMI_DEVICE_PROPERTY_NAME), "NAME"); EXPECT_STREQ(qdmi::toString(QDMI_DEVICE_PROPERTY_VERSION), "VERSION"); EXPECT_STREQ(qdmi::toString(QDMI_DEVICE_PROPERTY_STATUS), "STATUS"); @@ -346,8 +272,6 @@ TEST(FoMaCTest, 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_MAX), "MAX"); EXPECT_STREQ(qdmi::toString(QDMI_DEVICE_PROPERTY_CUSTOM1), "CUSTOM1"); EXPECT_STREQ(qdmi::toString(QDMI_DEVICE_PROPERTY_CUSTOM2), "CUSTOM2"); @@ -356,16 +280,7 @@ TEST(FoMaCTest, DevicePropertyToString) { EXPECT_STREQ(qdmi::toString(QDMI_DEVICE_PROPERTY_CUSTOM5), "CUSTOM5"); } -TEST(FoMaCTest, SessionPropertyToString) { - EXPECT_STREQ(qdmi::toString(QDMI_SESSION_PROPERTY_DEVICES), "DEVICES"); -} - -TEST(FoMaCTest, DeviceSessionParameterToString) { - EXPECT_STREQ(qdmi::toString(QDMI_DEVICE_SESSION_PARAMETER_CHILDDEVICE), - "CHILD DEVICE"); -} - -TEST(FoMaCTest, ThrowIfError) { +TEST(QDMITest, ThrowIfError) { EXPECT_NO_THROW(qdmi::throwIfError(QDMI_SUCCESS, "Test")); EXPECT_NO_THROW(qdmi::throwIfError(QDMI_WARN_GENERAL, "Test")); EXPECT_THROW(qdmi::throwIfError(QDMI_ERROR_FATAL, "Test"), @@ -898,7 +813,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; } } @@ -999,224 +915,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 / ("fomac_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()); @@ -1227,12 +927,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()); @@ -1242,41 +939,17 @@ TEST(AuthenticationTest, SessionMultipleInstances) { EXPECT_EQ(devices1.size(), devices2.size()); } -TEST(DeviceOwnershipTest, SiteKeepsFreshSessionAlive) { - const auto site = [] { - auto device = Session::openDevice("mqt.na.default"); - return device.getSites().front(); - }(); - - EXPECT_EQ(site.getIndex(), 0); -} - -TEST(DeviceOwnershipTest, OperationKeepsFreshSessionAlive) { - const auto operation = [] { - auto device = Session::openDevice("mqt.na.default"); - return device.getOperations().front(); - }(); - - EXPECT_FALSE(operation.getName().empty()); -} - -TEST(DeviceOwnershipTest, SiteFromOperationKeepsFreshSessionAlive) { - const auto site = [] { - auto device = Session::openDevice("mqt.na.default"); - const auto operation = device.getOperations().front(); - return operation.getSites().value().front(); - }(); - - EXPECT_TRUE(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 naDevice = manager.open("mqt.na.default"); + const auto operations = scDevice.getOperations(); + const auto sites = naDevice.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 @@ -1320,4 +993,4 @@ INSTANTIATE_TEST_SUITE_P( return name; }); -} // namespace fomac +} // namespace qdmi diff --git a/test/qdmi/device/test_device_api.cpp b/test/qdmi/device/test_device_api.cpp new file mode 100644 index 0000000000..eb882a4946 --- /dev/null +++ b/test/qdmi/device/test_device_api.cpp @@ -0,0 +1,457 @@ +/* + * 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.h" +#include "DeviceState.h" +#include "qdmi/Device.hpp" +#include "test_lifecycle_qdmi/device.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +TEST_LIFECYCLE_QDMI_EXPORT void TEST_LIFECYCLE_prepare_blocking_finalize(); +TEST_LIFECYCLE_QDMI_EXPORT bool +TEST_LIFECYCLE_wait_for_finalize(std::size_t timeoutMs); +TEST_LIFECYCLE_QDMI_EXPORT bool +TEST_LIFECYCLE_wait_for_initializations(std::size_t expected, + std::size_t timeoutMs); +TEST_LIFECYCLE_QDMI_EXPORT void TEST_LIFECYCLE_release_finalize(); +TEST_LIFECYCLE_QDMI_EXPORT std::size_t TEST_LIFECYCLE_initialize_count(); +TEST_LIFECYCLE_QDMI_EXPORT std::size_t TEST_LIFECYCLE_finalize_count(); +TEST_LIFECYCLE_QDMI_EXPORT std::size_t +TEST_LIFECYCLE_overlapping_initialize_count(); +} + +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) { + TEST_LIFECYCLE_prepare_blocking_finalize(); + auto first = loadDeviceApi(LIFECYCLE_DEVICE_LIBRARY, "TEST_LIFECYCLE"); + ASSERT_EQ(TEST_LIFECYCLE_initialize_count(), 1); + + std::thread finalize([api = std::move(first)]() mutable { api.reset(); }); + if (!TEST_LIFECYCLE_wait_for_finalize(1000)) { + TEST_LIFECYCLE_release_finalize(); + 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 = + TEST_LIFECYCLE_wait_for_initializations(2, 250); + TEST_LIFECYCLE_release_finalize(); + finalize.join(); + reopen.join(); + + EXPECT_FALSE(initializedBeforeFinalize); + EXPECT_EQ(TEST_LIFECYCLE_overlapping_initialize_count(), 0); + EXPECT_EQ(TEST_LIFECYCLE_initialize_count(), 2); + EXPECT_EQ(TEST_LIFECYCLE_finalize_count(), 1); + reopened.reset(); + EXPECT_EQ(TEST_LIFECYCLE_finalize_count(), 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/session_device.cpp b/test/qdmi/driver/session_device.cpp deleted file mode 100644 index 5f91e81268..0000000000 --- a/test/qdmi/driver/session_device.cpp +++ /dev/null @@ -1,261 +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 - -#include -#include -#include -#include -#include -#include - -struct QDMI_Child_Device_impl_d {}; - -struct QDMI_Device_Session_impl_d { - std::unordered_map parameters; - QDMI_Child_Device child = nullptr; - bool initialized = false; -}; - -struct QDMI_Device_Job_impl_d { - QDMI_Device_Session session = nullptr; -}; - -namespace { -[[nodiscard]] auto activeSessions() -> std::atomic_size_t& { - static std::atomic_size_t sessions = 0; - return sessions; -} - -[[nodiscard]] auto parameter(const QDMI_Device_Session_impl_d* const session, - const QDMI_Device_Session_Parameter key) - -> std::string { - if (const auto entry = session->parameters.find(key); - entry != session->parameters.end()) { - return entry->second; - } - return ""; -} - -[[nodiscard]] auto childDeviceHandle() -> QDMI_Child_Device { - static QDMI_Child_Device_impl_d child; - return &child; -} - -auto queryString(const std::string& result, const size_t size, void* value, - size_t* sizeRet) -> int { - const auto required = result.size() + 1; - if (sizeRet != nullptr) { - *sizeRet = required; - } - if (value == nullptr) { - return QDMI_SUCCESS; - } - if (size < required) { - return QDMI_ERROR_INVALIDARGUMENT; - } - std::memcpy(value, result.c_str(), required); - return QDMI_SUCCESS; -} -} // namespace - -// QDMI requires these exported C symbols to use the configured device prefix. -// NOLINTBEGIN(readability-identifier-naming) -extern "C" int TEST_SESSION_QDMI_device_initialize() { return QDMI_SUCCESS; } - -extern "C" int TEST_SESSION_QDMI_device_finalize() { return QDMI_SUCCESS; } - -extern "C" int -TEST_SESSION_QDMI_device_session_alloc(QDMI_Device_Session* session) { - if (session == nullptr) { - return QDMI_ERROR_INVALIDARGUMENT; - } - // The QDMI C API transfers this allocation through an opaque raw handle. - // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - *session = new (std::nothrow) QDMI_Device_Session_impl_d; - if (*session == nullptr) { - return QDMI_ERROR_OUTOFMEM; - } - ++activeSessions(); - return QDMI_SUCCESS; -} - -extern "C" int TEST_SESSION_QDMI_device_session_set_parameter( - QDMI_Device_Session session, const QDMI_Device_Session_Parameter param, - const size_t size, const void* value) { - if (session == nullptr || (value != nullptr && size == 0)) { - return QDMI_ERROR_INVALIDARGUMENT; - } - if (session->initialized) { - return QDMI_ERROR_BADSTATE; - } - if (param == QDMI_DEVICE_SESSION_PARAMETER_CHILDDEVICE) { - if (value == nullptr || size != sizeof(QDMI_Child_Device)) { - return QDMI_ERROR_INVALIDARGUMENT; - } - QDMI_Child_Device child = nullptr; - std::memcpy(static_cast(&child), value, sizeof(QDMI_Child_Device)); - if (child != childDeviceHandle()) { - return QDMI_ERROR_INVALIDARGUMENT; - } - session->child = child; - return QDMI_SUCCESS; - } - if (value != nullptr) { - session->parameters[param] = static_cast(value); - } - return QDMI_SUCCESS; -} - -extern "C" int -TEST_SESSION_QDMI_device_session_init(QDMI_Device_Session session) { - if (session == nullptr) { - return QDMI_ERROR_INVALIDARGUMENT; - } - if (session->initialized) { - return QDMI_ERROR_BADSTATE; - } - session->initialized = true; - return QDMI_SUCCESS; -} - -extern "C" void -TEST_SESSION_QDMI_device_session_free(QDMI_Device_Session session) { - if (session == nullptr) { - return; - } - --activeSessions(); - // This releases the opaque handle allocated by device_session_alloc. - // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - delete session; -} - -extern "C" int TEST_SESSION_QDMI_device_session_query_device_property( - QDMI_Device_Session session, const QDMI_Device_Property prop, - const size_t size, void* value, size_t* sizeRet) { - if (session == nullptr || !session->initialized) { - return QDMI_ERROR_BADSTATE; - } - if (prop == QDMI_DEVICE_PROPERTY_CHILDDEVICES) { - if (session->child != nullptr || - parameter(session, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM5) != - "with-child") { - return QDMI_ERROR_NOTSUPPORTED; - } - constexpr auto required = sizeof(QDMI_Child_Device); - if (sizeRet != nullptr) { - *sizeRet = required; - } - if (value == nullptr) { - return QDMI_SUCCESS; - } - if (size < required) { - return QDMI_ERROR_INVALIDARGUMENT; - } - auto* const child = childDeviceHandle(); - std::memcpy(value, static_cast(&child), - sizeof(QDMI_Child_Device)); - return QDMI_SUCCESS; - } - if (prop != QDMI_DEVICE_PROPERTY_NAME) { - return QDMI_ERROR_NOTSUPPORTED; - } - if (session->child != nullptr) { - return queryString("child;active=" + - std::to_string(activeSessions().load()), - size, value, sizeRet); - } - const auto name = - "base=" + parameter(session, QDMI_DEVICE_SESSION_PARAMETER_BASEURL) + - ";token=" + parameter(session, QDMI_DEVICE_SESSION_PARAMETER_TOKEN) + - ";custom1=" + parameter(session, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM1) + - ";custom2=" + parameter(session, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM2) + - ";active=" + std::to_string(activeSessions().load()); - return queryString(name, size, value, sizeRet); -} - -extern "C" int TEST_SESSION_QDMI_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 QDMI_ERROR_NOTSUPPORTED; -} - -extern "C" int TEST_SESSION_QDMI_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 QDMI_ERROR_NOTSUPPORTED; -} - -extern "C" int -TEST_SESSION_QDMI_device_session_create_device_job(QDMI_Device_Session session, - QDMI_Device_Job* job) { - if (session == nullptr || !session->initialized || job == nullptr) { - return QDMI_ERROR_INVALIDARGUMENT; - } - // The QDMI C API transfers this allocation through an opaque raw handle. - // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - *job = new (std::nothrow) QDMI_Device_Job_impl_d{session}; - return *job == nullptr ? QDMI_ERROR_OUTOFMEM : QDMI_SUCCESS; -} - -extern "C" int TEST_SESSION_QDMI_device_job_set_parameter( - QDMI_Device_Job job, QDMI_Device_Job_Parameter /*parameter*/, - size_t /*size*/, const void* /*value*/) { - return job == nullptr ? QDMI_ERROR_INVALIDARGUMENT : QDMI_SUCCESS; -} - -extern "C" int TEST_SESSION_QDMI_device_job_query_property( - QDMI_Device_Job job, const QDMI_Device_Job_Property prop, const size_t size, - void* value, size_t* sizeRet) { - if (job == nullptr || job->session == nullptr || - prop != QDMI_DEVICE_JOB_PROPERTY_ID) { - return QDMI_ERROR_INVALIDARGUMENT; - } - return queryString("session-job", size, value, sizeRet); -} - -extern "C" int TEST_SESSION_QDMI_device_job_submit(QDMI_Device_Job job) { - if (job == nullptr || job->session == nullptr) { - return QDMI_ERROR_INVALIDARGUMENT; - } - return QDMI_SUCCESS; -} - -extern "C" int TEST_SESSION_QDMI_device_job_cancel(QDMI_Device_Job /*job*/) { - return QDMI_ERROR_NOTSUPPORTED; -} - -extern "C" int TEST_SESSION_QDMI_device_job_check(QDMI_Device_Job /*job*/, - QDMI_Job_Status* /*status*/) { - return QDMI_ERROR_NOTSUPPORTED; -} - -extern "C" int TEST_SESSION_QDMI_device_job_wait(QDMI_Device_Job /*job*/, - size_t /*timeout*/) { - return QDMI_ERROR_NOTSUPPORTED; -} - -extern "C" int TEST_SESSION_QDMI_device_job_get_results( - QDMI_Device_Job /*job*/, QDMI_Job_Result /*result*/, size_t /*size*/, - void* /*value*/, size_t* /*sizeRet*/) { - return QDMI_ERROR_NOTSUPPORTED; -} - -extern "C" void TEST_SESSION_QDMI_device_job_free(QDMI_Device_Job job) { - // This releases the opaque handle allocated by - // device_session_create_device_job. - // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - delete job; -} -// NOLINTEND(readability-identifier-naming) diff --git a/test/qdmi/driver/test_driver.cpp b/test/qdmi/driver/test_driver.cpp deleted file mode 100644 index ba3c075d69..0000000000 --- a/test/qdmi/driver/test_driver.cpp +++ /dev/null @@ -1,1252 +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 "fomac/FoMaC.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 fomacChildren = - fomac::Session::createSessionlessDevice(&parent).getChildDevices(); - ASSERT_EQ(fomacChildren.size(), 2); - EXPECT_EQ(fomacChildren[0].getName(), "child-0"); - EXPECT_EQ(fomacChildren[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); - } - - 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, 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 NA Default QDMI Device", - "MQT Core DDSIM QDMI Device", - "MQT SC Default 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.na.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, 3 * sizeof(QDMI_Device)); - std::array devices{}; - 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("MQT NA Default QDMI Device", - "MQT Core DDSIM QDMI Device", - "MQT SC Default 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.custom1 = "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, 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 = - fomac::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 = - fomac::Session::openDevice("test.session-overrides", probeOverrides); - EXPECT_THAT(queryName(probe), testing::HasSubstr("active=1")); - EXPECT_EQ(clientCatalogSize(), catalogSizeBefore); -} - -TEST(DeviceRegistrationTest, FreshOpenCreatesDistinctSessions) { - registerSessionTestDevice(); - const auto first = fomac::Session::openDevice("test.session-overrides"); - const auto second = fomac::Session::openDevice("test.session-overrides"); - EXPECT_NE(first, second); -} - -TEST(DeviceRegistrationTest, FreshJobRetainsItsDeviceSession) { - registerSessionTestDevice(); - std::optional job; - { - auto device = fomac::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 = fomac::Session::openDevice("test.session-overrides"); - EXPECT_THAT(queryName(probe), testing::HasSubstr("active=1")); -} - -TEST(DeviceRegistrationTest, FreshChildDeviceRetainsItsRootSession) { - registerSessionTestDevice(); - std::optional child; - { - qdmi::DeviceSessionConfig overrides; - overrides.custom5 = "with-child"; - auto root = fomac::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 = fomac::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.custom1 = "RESONANCE_COCOS_V1"; - config.custom2 = "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, OpenWithAllParameters) { - 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.custom1 = "value1"; - config.custom2 = "value2"; - 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.custom1 = "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, {.custom1 = "first-session"}); - const auto equivalentLibrary = std::filesystem::path(library).parent_path() / - "." / - std::filesystem::path(library).filename(); - auto* const second = openTestDevice(equivalentLibrary.string(), prefix, - {.custom1 = "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..773830452a --- /dev/null +++ b/test/qdmi/manager/CMakeLists.txt @@ -0,0 +1,15 @@ +# 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) + package_add_test(${TARGET_NAME} MQT::CoreQDMI test_device_manager.cpp) + target_compile_definitions(${TARGET_NAME} + PRIVATE SC_DEVICE_LIBRARY="$") +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..870aa8185f --- /dev/null +++ b/test/qdmi/manager/test_device_manager.cpp @@ -0,0 +1,144 @@ +/* + * 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 + +#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))})); +} + +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); +} + +} // namespace diff --git a/test/qdmi/registry/CMakeLists.txt b/test/qdmi/registry/CMakeLists.txt index 918784ccd0..9c9b71d231 100644 --- a/test/qdmi/registry/CMakeLists.txt +++ b/test/qdmi/registry/CMakeLists.txt @@ -8,9 +8,8 @@ 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) +if(TARGET MQT::CoreQDMI) + package_add_test(${TARGET_NAME} MQT::CoreQDMI test_device_registry.cpp) target_compile_definitions( ${TARGET_NAME} PRIVATE diff --git a/test/qdmi/registry/test_device_registry.cpp b/test/qdmi/registry/test_device_registry.cpp index 5cb0499f26..a3b637157d 100644 --- a/test/qdmi/registry/test_device_registry.cpp +++ b/test/qdmi/registry/test_device_registry.cpp @@ -8,8 +8,7 @@ * Licensed under the MIT License */ -#include "DeviceRegistry.hpp" -#include "qdmi/driver/Driver.hpp" +#include "qdmi/DeviceRegistry.hpp" #include @@ -41,9 +40,8 @@ class TemporaryDirectory { [[nodiscard]] const std::filesystem::path& path() const { return path_; } - [[nodiscard]] std::filesystem::path - write(const std::filesystem::path& relative, - const std::string& contents) const { + std::filesystem::path write(const std::filesystem::path& relative, + const std::string& contents) const { const auto path = path_ / relative; std::filesystem::create_directories(path.parent_path()); std::ofstream output(path); @@ -122,7 +120,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(); @@ -150,7 +148,7 @@ TEST(DeviceRegistry, ParsesEnvironmentConfigurationWithoutLoadingLibraries) { }]} })"); - const qdmi::detail::DeviceRegistry registry; + qdmi::DeviceRegistry registry; const auto* definition = findDefinition(registry, "example.device"); ASSERT_NE(definition, nullptr); EXPECT_EQ(std::filesystem::weakly_canonical(definition->library), @@ -174,7 +172,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); } { @@ -182,7 +180,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); } } @@ -205,7 +203,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"); @@ -229,10 +227,14 @@ 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", .prefix = "FALLBACK"})); + EXPECT_THROW( + registry.registerDevice( + {.id = "masked", .library = "fallback", .prefix = "FALLBACK"}), + std::invalid_argument); } TEST(DeviceRegistry, HigherPrecedenceDefinitionMustExplicitlyReenableDevice) { @@ -251,10 +253,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", .prefix = "FALLBACK"})); } { @@ -265,16 +267,33 @@ 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()); } } +TEST(DeviceRegistry, ExplicitReplacementCanReenableConfiguredId) { + const TemporaryDirectory directory; + const auto path = directory.write("disabled.json", R"({ + "schema-version": 1, + "qdmi": {"devices": [{"id": "masked", "enabled": false}]} + })"); + const ScopedEnvironmentVariable configFile("MQT_CORE_QDMI_CONFIG_FILE", + path.string()); + const ScopedEnvironmentVariable configJson("MQT_CORE_QDMI_CONFIG_JSON", ""); + qdmi::DeviceRegistry registry; + + registry.registerDevice( + {.id = "masked", .library = "explicit", .prefix = "EXPLICIT"}, true); + const auto* definition = findDefinition(registry, "masked"); + ASSERT_NE(definition, nullptr); + EXPECT_EQ(definition->prefix, "EXPLICIT"); +} + TEST(DeviceRegistry, ResolvesRelativeConfigurationPathsBeforeCwdChanges) { const TemporaryDirectory directory; directory.write("config/device.json", R"({ @@ -292,7 +311,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; @@ -316,7 +335,7 @@ TEST(DeviceRegistry, DiscoversGeneratedBuildTreeManifests) { const auto configFile = emptyConfig(directory); const ScopedEnvironmentVariable configJson("MQT_CORE_QDMI_CONFIG_JSON", ""); - const qdmi::detail::DeviceRegistry registry; + const qdmi::DeviceRegistry registry; ASSERT_EQ(registry.definitions().size(), 3); EXPECT_EQ(registry.definitions().at(0).id, "mqt.ddsim.default"); EXPECT_EQ(registry.definitions().at(1).id, "mqt.na.default"); @@ -344,7 +363,7 @@ TEST(DeviceRegistry, ReadsProjectConfigurationFromPyprojectToml) { directory.path().string()); #endif - const qdmi::detail::DeviceRegistry registry; + const qdmi::DeviceRegistry registry; const auto* definition = findDefinition(registry, "toml"); ASSERT_NE(definition, nullptr); EXPECT_EQ(std::filesystem::weakly_canonical(definition->library), @@ -367,7 +386,7 @@ TEST(DeviceRegistry, DedicatedProjectFileWinsOverPyproject) { 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; EXPECT_NE(findDefinition(registry, "json"), nullptr); EXPECT_EQ(findDefinition(registry, "toml"), nullptr); } @@ -398,7 +417,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, @@ -423,7 +442,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); } } @@ -434,21 +453,20 @@ TEST(DeviceRegistry, ReportsInvalidExplicitJsonAndToml) { 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); } { directory.write("pyproject.toml", "[tool.qdmi\n"); const ScopedCurrentPath currentPath(directory.path()); const ScopedEnvironmentVariable configFile("MQT_CORE_QDMI_CONFIG_FILE", ""); - EXPECT_THROW(static_cast(qdmi::detail::DeviceRegistry()), + EXPECT_THROW(static_cast(qdmi::DeviceRegistry()), std::invalid_argument); } }