From adf97ebd8f50feeded7381a123af123c1277b3d6 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Tue, 14 Jul 2026 23:36:28 +0200 Subject: [PATCH 01/16] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Redesign=20QDMI=20de?= =?UTF-8?q?vice=20management?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assisted-by: GPT-5 via Codex --- CHANGELOG.md | 32 +- CMakeLists.txt | 3 +- UPGRADING.md | 180 ++++- bindings/CMakeLists.txt | 2 +- bindings/na/CMakeLists.txt | 2 +- bindings/na/register_na.cpp | 6 +- .../{register_fomac.cpp => register_qdmi.cpp} | 33 +- bindings/patterns.txt | 6 +- bindings/{fomac => qdmi}/CMakeLists.txt | 8 +- bindings/{fomac/fomac.cpp => qdmi/qdmi.cpp} | 461 +++++------- cmake/AddMQTQDMIDevice.cmake | 52 ++ cmake/ExternalDependencies.cmake | 14 + docs/qdmi/configuration.md | 66 ++ docs/qdmi/driver.md | 85 +-- docs/qdmi/index.md | 5 +- docs/qdmi/qdmi_backend.md | 12 +- doxygen/namespaces.md | 4 +- include/mqt-core/na/fomac/Device.hpp | 175 ----- include/mqt-core/na/qdmi/Device.hpp | 156 ++++ .../{fomac/FoMaC.hpp => qdmi/Device.hpp} | 158 ++-- include/mqt-core/qdmi/DeviceManager.hpp | 120 +++ include/mqt-core/qdmi/common/Common.hpp | 4 + noxfile.py | 2 +- pyproject.toml | 3 +- python/mqt/core/na/__init__.pyi | 2 +- python/mqt/core/na/{fomac.pyi => qdmi.pyi} | 15 +- python/mqt/core/plugins/qiskit/backend.py | 42 +- python/mqt/core/plugins/qiskit/converters.py | 6 +- python/mqt/core/plugins/qiskit/estimator.py | 2 +- python/mqt/core/plugins/qiskit/job.py | 30 +- python/mqt/core/plugins/qiskit/provider.py | 43 +- python/mqt/core/{fomac.pyi => qdmi.pyi} | 227 +++--- src/CMakeLists.txt | 3 - src/fomac/CMakeLists.txt | 32 - src/na/CMakeLists.txt | 2 +- src/na/{fomac => qdmi}/CMakeLists.txt | 8 +- src/na/{fomac => qdmi}/Device.cpp | 56 +- src/qdmi/CMakeLists.txt | 26 + src/{fomac/FoMaC.cpp => qdmi/Device.cpp} | 143 +--- src/qdmi/DeviceManager.cpp | 698 ++++++++++++++++++ src/qdmi/devices/dd/CMakeLists.txt | 6 + src/qdmi/devices/na/CMakeLists.txt | 6 + src/qdmi/devices/sc/CMakeLists.txt | 6 + src/qdmi/driver/CMakeLists.txt | 44 +- src/qdmi/driver/Driver.cpp | 11 +- test/CMakeLists.txt | 1 - test/na/CMakeLists.txt | 2 +- test/na/{fomac => qdmi}/CMakeLists.txt | 12 +- .../test_fomac.cpp => qdmi/test_qdmi.cpp} | 41 +- .../na/{test_na_fomac.py => test_na_qdmi.py} | 13 +- test/python/plugins/qiskit/test_backend.py | 20 +- test/python/plugins/qiskit/test_estimator.py | 7 +- .../plugins/qiskit/test_mock_backend.py | 81 +- test/python/plugins/qiskit/test_provider.py | 8 +- test/python/plugins/qiskit/test_sampler.py | 7 +- .../test_fomac.py => qdmi/test_qdmi.py} | 221 +----- test/qdmi/CMakeLists.txt | 3 +- test/{fomac => qdmi/device}/CMakeLists.txt | 13 +- .../device/test_device.cpp} | 275 ++----- test/qdmi/driver/CMakeLists.txt | 2 +- test/qdmi/driver/test_driver.cpp | 8 +- test/qdmi/manager/CMakeLists.txt | 15 + test/qdmi/manager/test_device_manager.cpp | 185 +++++ 63 files changed, 2264 insertions(+), 1647 deletions(-) rename bindings/na/{register_fomac.cpp => register_qdmi.cpp} (83%) rename bindings/{fomac => qdmi}/CMakeLists.txt (82%) rename bindings/{fomac/fomac.cpp => qdmi/qdmi.cpp} (50%) create mode 100644 cmake/AddMQTQDMIDevice.cmake create mode 100644 docs/qdmi/configuration.md delete mode 100644 include/mqt-core/na/fomac/Device.hpp create mode 100644 include/mqt-core/na/qdmi/Device.hpp rename include/mqt-core/{fomac/FoMaC.hpp => qdmi/Device.hpp} (87%) create mode 100644 include/mqt-core/qdmi/DeviceManager.hpp rename python/mqt/core/na/{fomac.pyi => qdmi.pyi} (87%) rename python/mqt/core/{fomac.pyi => qdmi.pyi} (77%) delete mode 100644 src/fomac/CMakeLists.txt rename src/na/{fomac => qdmi}/CMakeLists.txt (81%) rename src/na/{fomac => qdmi}/Device.cpp (92%) rename src/{fomac/FoMaC.cpp => qdmi/Device.cpp} (82%) create mode 100644 src/qdmi/DeviceManager.cpp rename test/na/{fomac => qdmi}/CMakeLists.txt (71%) rename test/na/{fomac/test_fomac.cpp => qdmi/test_qdmi.cpp} (62%) rename test/python/na/{test_na_fomac.py => test_na_qdmi.py} (96%) rename test/python/{fomac/test_fomac.py => qdmi/test_qdmi.py} (77%) rename test/{fomac => qdmi/device}/CMakeLists.txt (64%) rename test/{fomac/test_fomac.cpp => qdmi/device/test_device.cpp} (81%) create mode 100644 test/qdmi/manager/CMakeLists.txt create mode 100644 test/qdmi/manager/test_device_manager.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index b1ecfbd17a..5f5c28b486 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,11 +14,14 @@ releases may include breaking changes. - ✨ Add Python bindings for the MQT Compiler Collection ([#1815]) ([**@burgholzer**], [**@denialhaag**]) -- ✨ Add support for QDMI child devices to the driver and FoMaC libraries +- ✨ Add versioned JSON/TOML QDMI configuration discovery, relocatable built-in + manifests, and lazy `qdmi::DeviceManager`/`mqt.core.qdmi` APIs with per-device + sessions and failure isolation. +- ✨ Add support for QDMI child devices to the driver and QDMI libraries ([#1897]) ([**@burgholzer**]) -- ✨ Add typed custom property and result queries to the C++ and Python FoMaC +- ✨ Add typed custom property and result queries to the C++ and Python QDMI libraries ([#1895]) ([**@burgholzer**]) -- ✨ Add support for custom job parameters to C++ and Python FoMaC library +- ✨ Add support for custom job parameters to C++ and Python QDMI library ([#1887]) ([**@flowerthrower**], [**@burgholzer**]) - ✨ Add QIR Output Schemas support to the QIR runtime ([#1877]) ([**@rturrado**]) @@ -64,6 +67,13 @@ releases may include breaking changes. ### Changed +- ♻️ Replace the legacy 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 `openAll` isolates failures + by stable device ID. +- ♻️ Migrate the Qiskit provider and neutral-atom adapter to lazily opened + configured QDMI devices and the unified `mqt.core.qdmi` API. - ⬆️ Raise the minimum supported QDMI version to 1.3.2 ([#1897]) ([**@burgholzer**]) - ⬆️ Require LLVM 22.1 for C++ library builds ([#1549]) ([**@burgholzer**], @@ -73,6 +83,10 @@ releases may include breaking changes. ### Removed +- 🔥 Remove the legacy device-management namespace, Python module, global + session API, source/include/test trees, and compatibility CMake targets. +- 🔥 Remove central built-in device enumeration and compiled-in library names; + generated relocatable manifest fragments now register built-in devices. - 🔥 Remove the density matrix support from the MQT Core DD package ([#1466]) ([**@burgholzer**]) - 🔥 Remove `datastructures` (`ds`) (sub)library from MQT Core ([#1458]) @@ -98,7 +112,7 @@ _If you are upgrading: please see [`UPGRADING.md`](UPGRADING.md#370)._ ### Changed - ⬆️ Update QDMI to version 1.3.2 ([#1873]) ([**@denialhaag**]) -- ♻️ Improve implementation and usability of FoMaC classes ([#1849]) +- ♻️ Improve implementation and usability of QDMI classes ([#1849]) ([**@MatthiasReumann**]) - ⬆️ Update `nanobind` to version 2.13.0 ([#1817]) - ⬆️ Update [munich-quantum-toolkit/workflows] to version `v2.0.1` ([#1660], @@ -230,7 +244,7 @@ _If you are upgrading: please see [`UPGRADING.md`](UPGRADING.md#340)._ value ([#1310]) ([**@MatthiasReumann**]) - ✨ Add `OptionalDependencyTester` to lazily handle optional Python dependencies like Qiskit ([#1243]) ([**@marcelwa**], [**@burgholzer**]) -- ✨ Expose the QDMI job interface through FoMaC ([#1243]) ([**@marcelwa**], +- ✨ Expose the QDMI job interface through QDMI ([#1243]) ([**@marcelwa**], [**@burgholzer**]) - ✨ Add Qiskit backend wrapper with job submission support for QDMI devices through a provider interface ([#1243], [#1385]) ([**@marcelwa**], @@ -268,8 +282,8 @@ _If you are upgrading: please see [`UPGRADING.md`](UPGRADING.md#340)._ session configurations ([#1355]) ([**@burgholzer**]) - ♻️ Enable thread-safe reference counting for QDMI devices singletons ([#1355]) ([**@burgholzer**]) -- ♻️ Refactor `FoMaC` singleton to instantiable `Session` class with - configurable authentication parameters ([#1355]) ([**@marcelwa**]) +- ♻️ Refactor `QDMI` singleton to instantiable `Session` class with configurable + authentication parameters ([#1355]) ([**@marcelwa**]) - 👷 Stop testing on `ubuntu-22.04` and `ubuntu-22.04-arm` runners ([#1359]) ([**@denialhaag**], [**@burgholzer**]) - 👷 Stop testing with `clang-19` and start testing with `clang-21` ([#1359]) @@ -360,7 +374,7 @@ _If you are upgrading: please see [`UPGRADING.md`](UPGRADING.md#330)._ - 👷 Enable testing on Python 3.14 ([#1246]) ([**@denialhaag**]) - ✨ Add dedicated `PlacementPass` to MLIR transpilation routines ([#1232]) ([**@MatthiasReumann**]) -- ✨ Add an NA-specific FoMaC implementation ([#1223], [#1236]) ([**@ystade**], +- ✨ Add an NA-specific QDMI implementation ([#1223], [#1236]) ([**@ystade**], [**@burgholzer**]) - ✨ Enable import of BarrierOp into MQTRef ([#1224]) ([**@denialhaag**]) - ✨ Add naive quantum program routing MLIR pass ([#1148]) @@ -376,7 +390,7 @@ _If you are upgrading: please see [`UPGRADING.md`](UPGRADING.md#330)._ ([**@denialhaag**]) - ✨ Add support for translating `IfElseOperation`s to the `MQTRef` MLIR dialect ([#1164]) ([**@denialhaag**], [**@burgholzer**]) -- ✨ Add MQT's implementation of a generic FoMaC with Python bindings ([#1150], +- ✨ Add MQT's implementation of a generic QDMI with Python bindings ([#1150], [#1186], [#1223]) ([**@ystade**]) - ✨ Add new MLIR pass `ElidePermutations` for SWAP gate elimination ([#1151]) ([**@taminob**]) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0bc895e4c7..8aa82e7b1a 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,6 +29,7 @@ include(PreventInSourceBuilds) include(PackageAddTest) include(Cache) include(AddMQTCoreLibrary) +include(AddMQTQDMIDevice) option(BUILD_MQT_CORE_BINDINGS "Build the MQT Core Python bindings" OFF) if(BUILD_MQT_CORE_BINDINGS) @@ -163,7 +164,7 @@ if(BUILD_MQT_CORE_MLIR) if(BUILD_MQT_CORE_DOCUMENTATION) add_custom_target(mqt-core-docs DEPENDS mlir-doc) - 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() diff --git a/UPGRADING.md b/UPGRADING.md index 043fd920e2..5a76142908 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -6,6 +6,172 @@ of changes including minor and patch releases, please refer to the ## [Unreleased] +### QDMI device management has been redesigned + +The legacy device-management namespace, Python module, global session object, +and compatibility CMake target have been removed. The public API is now +consistently named `qdmi`: + +| Concern | C++ | Python | +| --- | --- | --- | +| Discover registrations | `qdmi::DeviceRegistry` | `qdmi.DeviceManager.definitions` | +| Open one device | `qdmi::DeviceManager::open` | `DeviceManager.open` | +| Open every device independently | `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. Replace imports from the +removed device-management module with `mqt.core.qdmi`, includes with +`qdmi/Device.hpp` or `qdmi/DeviceManager.hpp`, and the removed CMake target with +`MQT::CoreQDMI`. + +#### Opening devices + +Code that previously created a global session and enumerated its devices should +create a manager and open stable device IDs. Discovery parses configuration but +does not load native libraries; loading and session initialization happen only +when `open` or `open_all` is called. + +Python: + +```python +from mqt.core import qdmi + +manager = qdmi.DeviceManager() +print([definition.id for definition in manager.definitions]) + +device = manager.open("mqt.ddsim.default") +print(device.name()) + +devices, errors = manager.open_all() +for device_id, error in errors.items(): + print(f"{device_id} could not be opened: {error}") +``` + +C++: + +```cpp +#include "qdmi/DeviceManager.hpp" + +qdmi::DeviceManager manager; +const auto device = manager.open("mqt.ddsim.default"); + +const auto opened = manager.openAll(); +for (const auto& [id, error] : opened.errors) { + // Handle one failed provider without losing successfully opened devices. +} +``` + +`openAll` isolates failures by device ID. This differs from the removed global +session, where one library or initialization failure could affect discovery as a +whole. + +#### Per-device session parameters + +Authentication and provider 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() +parameters.token = obtain_token() +parameters.project_id = "research-project" + +device = qdmi.DeviceManager().open( + "vendor.qpu.production", + session_overrides=parameters, +) +``` + +```cpp +qdmi::SessionParameters parameters; +parameters.token = obtainToken(); +parameters.projectId = "research-project"; + +auto device = manager.open("vendor.qpu.production", parameters); +``` + +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. Unregistering a definition likewise +does not invalidate already opened devices. + +#### 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", + "abi": "qdmi-v1", + "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 `mqt-core.json`; +3. user or XDG `mqt-core.json`; +4. the nearest project `mqt-core.json` or `[tool.mqt-core.qdmi]` table in + `pyproject.toml`; +5. `MQT_CORE_QDMI_CONFIG_JSON`; +6. C++ or Python runtime overrides. + +A dedicated `mqt-core.json` wins over `pyproject.toml` in the same directory. +`MQT_CORE_QDMI_CONFIG_FILE` or `ConfigOptions.explicitFile` replaces the system, +user, and project layers while retaining packaged devices. Set `isolated` to +exclude packaged manifests as well. See the +[configuration reference](docs/qdmi/configuration.md) for complete schemas and +administrator, project, environment, and runtime examples. + +Static C++ executables must set `ConfigOptions.configRoot`: unlike a shared +library or Python extension, a fully static executable has no portable module +location from which built-in manifests can be discovered. + +#### QDMI child devices + +`qdmi::Device::getChildDevices()` and `Device.child_devices()` return direct +child devices as ordinary device objects. Each child owns the provider session +and library state required by its QDMI v1 handle; retaining a child is therefore +safe even after discarding its parent or manager. Providers 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 inject runtime +definitions or mock `DeviceManager.open_all`. + ### MLIR enabled by default for C++ and Python package builds The MLIR-based functionality within MQT Core has long been experimental and @@ -65,14 +231,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`. @@ -216,9 +374,9 @@ result = job.result() ``` The backend automatically converts circuits to QASM, introspects device -capabilities, validates circuits, and formats results. The existing FoMaC -interface (`mqt.core.fomac`) remains fully supported for direct, low-level -access to QDMI devices. +capabilities, validates circuits, and formats results. The existing QDMI +interface (`mqt.core.qdmi`) remains fully supported for direct, low-level access +to QDMI devices. Install with Qiskit support: `uv pip install "mqt-core[qiskit]"` diff --git a/bindings/CMakeLists.txt b/bindings/CMakeLists.txt index 117f19e477..d88ad59a66 100644 --- a/bindings/CMakeLists.txt +++ b/bindings/CMakeLists.txt @@ -8,7 +8,7 @@ add_subdirectory(ir) add_subdirectory(dd) -add_subdirectory(fomac) +add_subdirectory(qdmi) add_subdirectory(na) if(BUILD_MQT_CORE_MLIR) 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..869062a942 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( 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/fomac/fomac.cpp b/bindings/qdmi/qdmi.cpp similarity index 50% rename from bindings/fomac/fomac.cpp rename to bindings/qdmi/qdmi.cpp index fcdfd0ddf8..3ef5798871 100644 --- a/bindings/fomac/fomac.cpp +++ b/bindings/qdmi/qdmi.cpp @@ -8,19 +8,19 @@ * Licensed under the MIT License */ -#include "fomac/FoMaC.hpp" - -#include "qdmi/driver/Driver.hpp" +#include "qdmi/Device.hpp" +#include "qdmi/DeviceManager.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 // 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 @@ -73,97 +73,80 @@ template } // 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 independent authentication settings. -All authentication parameters are optional and can be provided as keyword arguments to the constructor.)pb"); - - session.def( + auto sessionParameters = nb::class_( + m, "SessionParameters", "Parameters for one QDMI device session."); + sessionParameters.def(nb::init<>()) + .def_rw("base_url", &qdmi::SessionParameters::baseUrl) + .def_rw("token", &qdmi::SessionParameters::token) + .def_rw("auth_file", &qdmi::SessionParameters::authFile) + .def_rw("auth_url", &qdmi::SessionParameters::authUrl) + .def_rw("username", &qdmi::SessionParameters::username) + .def_rw("password", &qdmi::SessionParameters::password) + .def_rw("project_id", &qdmi::SessionParameters::projectId) + .def_rw("custom1", &qdmi::SessionParameters::custom1) + .def_rw("custom2", &qdmi::SessionParameters::custom2) + .def_rw("custom3", &qdmi::SessionParameters::custom3) + .def_rw("custom4", &qdmi::SessionParameters::custom4) + .def_rw("custom5", &qdmi::SessionParameters::custom5); + + auto definition = nb::class_( + m, "DeviceDefinition", "A side-effect-free QDMI device registration."); + definition + .def( + "__init__", + [](qdmi::DeviceDefinition* self, std::string id, + std::filesystem::path library, std::string prefix, std::string abi, + bool enabled, qdmi::SessionParameters session) { + new (self) qdmi::DeviceDefinition{.id = std::move(id), + .library = std::move(library), + .abi = std::move(abi), + .prefix = std::move(prefix), + .enabled = enabled, + .session = std::move(session)}; + }, + "id"_a, "library"_a, "prefix"_a, nb::kw_only(), "abi"_a = "qdmi-v1", + "enabled"_a = true, "session"_a = qdmi::SessionParameters{}) + .def_rw("id", &qdmi::DeviceDefinition::id) + .def_rw("library", &qdmi::DeviceDefinition::library) + .def_rw("abi", &qdmi::DeviceDefinition::abi) + .def_rw("prefix", &qdmi::DeviceDefinition::prefix) + .def_rw("enabled", &qdmi::DeviceDefinition::enabled) + .def_rw("session", &qdmi::DeviceDefinition::session) + .def_ro("source", &qdmi::DeviceDefinition::source); + + auto configOptions = nb::class_( + m, "ConfigOptions", "Controls QDMI configuration discovery."); + configOptions.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); + [](qdmi::ConfigOptions* self, + std::optional configRoot, + std::optional explicitFile, + std::optional baseDirectory, bool isolated, + std::optional inlineJson, + std::vector runtimeOverrides) { + qdmi::ConfigOptions options{.configRoot = std::move(configRoot), + .explicitFile = std::move(explicitFile), + .baseDirectory = std::move(baseDirectory), + .isolated = isolated, + .runtimeOverrides = + std::move(runtimeOverrides)}; + if (inlineJson) { + options.inlineOverrides = nlohmann::json::parse(*inlineJson); + } + new (self) qdmi::ConfigOptions(std::move(options)); }, - 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"); + nb::kw_only(), "config_root"_a = std::nullopt, + "explicit_file"_a = std::nullopt, "base_directory"_a = std::nullopt, + "isolated"_a = false, "inline_json"_a = std::nullopt, + "runtime_overrides"_a = std::vector{}); // Job class - auto job = nb::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("check", &qdmi::Job::check, "Returns the current status of the job."); - job.def("wait", &fomac::Job::wait, "timeout"_a = 0, + job.def("wait", &qdmi::Job::wait, "timeout"_a = 0, R"pb(Waits for the job to complete. Args: @@ -172,36 +155,36 @@ All authentication parameters are optional and can be provided as keyword argume Returns: True if the job completed within the timeout, False otherwise.)pb"); - job.def("cancel", &fomac::Job::cancel, "Cancels the job."); + job.def("cancel", &qdmi::Job::cancel, "Cancels the job."); - job.def("get_shots", &fomac::Job::getShots, + job.def("get_shots", &qdmi::Job::getShots, "Returns the raw shot results from the job."); - job.def("get_counts", &fomac::Job::getCounts, + job.def("get_counts", &qdmi::Job::getCounts, "Returns the measurement counts from the job."); - job.def("get_dense_statevector", &fomac::Job::getDenseStateVector, + job.def("get_dense_statevector", &qdmi::Job::getDenseStateVector, "Returns the dense statevector from the job (typically only " "available from simulator devices)."); - job.def("get_dense_probabilities", &fomac::Job::getDenseProbabilities, + job.def("get_dense_probabilities", &qdmi::Job::getDenseProbabilities, "Returns the dense probabilities from the job (typically only " "available from simulator devices)."); - job.def("get_sparse_statevector", &fomac::Job::getSparseStateVector, + job.def("get_sparse_statevector", &qdmi::Job::getSparseStateVector, "Returns the sparse statevector from the job (typically only " "available from simulator devices)."); - job.def("get_sparse_probabilities", &fomac::Job::getSparseProbabilities, + job.def("get_sparse_probabilities", &qdmi::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 qdmi::Job& self, const qdmi::CustomProperty customProperty, const nb::handle valueType) { return queryCustomValue( - [&self, customProperty]() { + [&self, customProperty]() { return self.queryCustomProperty(customProperty); }, valueType); @@ -219,10 +202,10 @@ when the custom slot is unsupported.)pb"); job.def( "get_custom_result", - [](const fomac::Job& self, const fomac::CustomProperty customProperty, + [](const qdmi::Job& self, const qdmi::CustomProperty customProperty, const nb::handle valueType) { return queryCustomValue( - [&self, customProperty]() { + [&self, customProperty]() { return self.getCustomResult(customProperty); }, valueType); @@ -237,15 +220,14 @@ 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("id", &qdmi::Job::getId, "The job ID."); - job.def_prop_ro("program_format", &fomac::Job::getProgramFormat, + job.def_prop_ro("program_format", &qdmi::Job::getProgramFormat, "The format of the submitted program."); - job.def_prop_ro("program", &fomac::Job::getProgram, "The submitted program."); + job.def_prop_ro("program", &qdmi::Job::getProgram, "The submitted program."); - job.def_prop_ro("num_shots", &fomac::Job::getNumShots, - "The number of shots."); + job.def_prop_ro("num_shots", &qdmi::Job::getNumShots, "The number of shots."); job.def(nb::self == nb::self, nb::sig("def __eq__(self, arg: object, /) -> bool")); @@ -280,17 +262,17 @@ when the custom slot is unsupported.)pb"); .value("CUSTOM4", QDMI_PROGRAM_FORMAT_CUSTOM4) .value("CUSTOM5", QDMI_PROGRAM_FORMAT_CUSTOM5); - nb::enum_( + 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); + .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_( + auto device = nb::class_( m, "Device", "A device represents a quantum device with its properties and " "capabilities."); @@ -304,70 +286,69 @@ when the custom slot is unsupported.)pb"); .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("name", &qdmi::Device::getName, "Returns the name of the device."); - device.def("version", &fomac::Device::getVersion, + device.def("version", &qdmi::Device::getVersion, "Returns the version of the device."); - device.def("status", &fomac::Device::getStatus, + device.def("status", &qdmi::Device::getStatus, "Returns the current status of the device."); - device.def("library_version", &fomac::Device::getLibraryVersion, + device.def("library_version", &qdmi::Device::getLibraryVersion, "Returns the version of the library used to define the device."); - device.def("qubits_num", &fomac::Device::getQubitsNum, + device.def("qubits_num", &qdmi::Device::getQubitsNum, "Returns the number of qubits available on the device."); - device.def("sites", &fomac::Device::getSites, + device.def("sites", &qdmi::Device::getSites, "Returns the list of all sites (zone and regular sites) available " "on the device."); - device.def("regular_sites", &fomac::Device::getRegularSites, + device.def("regular_sites", &qdmi::Device::getRegularSites, "Returns the list of regular sites (without zone sites) available " "on the device."); - device.def("zones", &fomac::Device::getZones, + device.def("zones", &qdmi::Device::getZones, "Returns the list of zone sites (without regular sites) available " "on the device."); - device.def("operations", &fomac::Device::getOperations, + device.def("operations", &qdmi::Device::getOperations, "Returns the list of operations supported by the device."); - device.def("coupling_map", &fomac::Device::getCouplingMap, + device.def("coupling_map", &qdmi::Device::getCouplingMap, "Returns the coupling map of the device as a list of site pairs."); - device.def("needs_calibration", &fomac::Device::getNeedsCalibration, + device.def("needs_calibration", &qdmi::Device::getNeedsCalibration, "Returns whether the device needs calibration."); - device.def("length_unit", &fomac::Device::getLengthUnit, + device.def("length_unit", &qdmi::Device::getLengthUnit, "Returns the unit of length used by the device."); - device.def("length_scale_factor", &fomac::Device::getLengthScaleFactor, + device.def("length_scale_factor", &qdmi::Device::getLengthScaleFactor, "Returns the scale factor for length used by the device."); - device.def("duration_unit", &fomac::Device::getDurationUnit, + device.def("duration_unit", &qdmi::Device::getDurationUnit, "Returns the unit of duration used by the device."); - device.def("duration_scale_factor", &fomac::Device::getDurationScaleFactor, + device.def("duration_scale_factor", &qdmi::Device::getDurationScaleFactor, "Returns the scale factor for duration used by the device."); - device.def("min_atom_distance", &fomac::Device::getMinAtomDistance, + device.def("min_atom_distance", &qdmi::Device::getMinAtomDistance, "Returns the minimum atom distance on the device."); device.def("supported_program_formats", - &fomac::Device::getSupportedProgramFormats, + &qdmi::Device::getSupportedProgramFormats, "Returns the list of program formats supported by the device."); - device.def("child_devices", &fomac::Device::getChildDevices, + device.def("child_devices", &qdmi::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 qdmi::Device& self, const qdmi::CustomProperty customProperty, const nb::handle valueType) { return queryCustomValue( - [&self, customProperty]() { + [&self, customProperty]() { return self.queryCustomProperty(customProperty); }, valueType); @@ -383,14 +364,14 @@ 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", &fomac::Device::submitJob, "program"_a, + device.def("submit_job", &qdmi::Device::submitJob, "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 job to the device."); - device.def("__repr__", [](const fomac::Device& dev) { + device.def("__repr__", [](const qdmi::Device& dev) { return ""; }); @@ -400,53 +381,53 @@ when the custom slot is unsupported.)pb"); nb::sig("def __ne__(self, arg: object, /) -> bool")); // Site class - auto site = nb::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("index", &qdmi::Site::getIndex, "Returns the index of the site."); - site.def("t1", &fomac::Site::getT1, + site.def("t1", &qdmi::Site::getT1, "Returns the T1 coherence time of the site."); - site.def("t2", &fomac::Site::getT2, + site.def("t2", &qdmi::Site::getT2, "Returns the T2 coherence time of the site."); - site.def("name", &fomac::Site::getName, "Returns the name of the site."); + site.def("name", &qdmi::Site::getName, "Returns the name of the site."); - site.def("x_coordinate", &fomac::Site::getXCoordinate, + site.def("x_coordinate", &qdmi::Site::getXCoordinate, "Returns the x coordinate of the site."); - site.def("y_coordinate", &fomac::Site::getYCoordinate, + site.def("y_coordinate", &qdmi::Site::getYCoordinate, "Returns the y coordinate of the site."); - site.def("z_coordinate", &fomac::Site::getZCoordinate, + site.def("z_coordinate", &qdmi::Site::getZCoordinate, "Returns the z coordinate of the site."); - site.def("is_zone", &fomac::Site::isZone, + site.def("is_zone", &qdmi::Site::isZone, "Returns whether the site is a zone."); - site.def("x_extent", &fomac::Site::getXExtent, + site.def("x_extent", &qdmi::Site::getXExtent, "Returns the x extent of the site."); - site.def("y_extent", &fomac::Site::getYExtent, + site.def("y_extent", &qdmi::Site::getYExtent, "Returns the y extent of the site."); - site.def("z_extent", &fomac::Site::getZExtent, + site.def("z_extent", &qdmi::Site::getZExtent, "Returns the z extent of the site."); - site.def("module_index", &fomac::Site::getModuleIndex, + site.def("module_index", &qdmi::Site::getModuleIndex, "Returns the index of the module the site belongs to."); - site.def("submodule_index", &fomac::Site::getSubmoduleIndex, + site.def("submodule_index", &qdmi::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 qdmi::Site& self, const qdmi::CustomProperty customProperty, const nb::handle valueType) { return queryCustomValue( - [&self, customProperty]() { + [&self, customProperty]() { return self.queryCustomProperty(customProperty); }, valueType); @@ -462,7 +443,7 @@ 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) { + site.def("__repr__", [](const qdmi::Site& s) { return ""; }); @@ -472,87 +453,85 @@ when the custom slot is unsupported.)pb"); nb::sig("def __ne__(self, arg: object, /) -> bool")); // Operation class - auto operation = nb::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{}, + operation.def("name", &qdmi::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{}, + operation.def("qubits_num", &qdmi::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{}, + operation.def("parameters_num", &qdmi::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{}, + operation.def("duration", &qdmi::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{}, + operation.def("fidelity", &qdmi::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{}, + operation.def("interaction_radius", &qdmi::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{}, + operation.def("blocking_radius", &qdmi::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{}, + operation.def("idling_fidelity", &qdmi::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, + operation.def("is_zoned", &qdmi::Operation::isZoned, "Returns whether the operation is zoned."); - operation.def("sites", &fomac::Operation::getSites, + operation.def("sites", &qdmi::Operation::getSites, "Returns the list of sites the operation can be performed on."); - operation.def("site_pairs", &fomac::Operation::getSitePairs, + operation.def("site_pairs", &qdmi::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{}, + operation.def("mean_shuttling_speed", &qdmi::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 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]() { + ¶ms]() { return self.queryCustomProperty(customProperty, sites, params); }, valueType); }, "custom_property"_a, "value_type"_a, - "sites"_a.sig("...") = std::vector{}, + "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] = " + "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. @@ -561,7 +540,7 @@ 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) { + operation.def("__repr__", [](const qdmi::Operation& op) { return ""; }); @@ -570,80 +549,40 @@ when the custom slot is unsupported.)pb"); operation.def(nb::self != nb::self, nb::sig("def __ne__(self, arg: object, /) -> bool")); - // Module-level function to add dynamic device libraries - m.def( - "add_dynamic_device_library", - [](const std::string& libraryPath, const 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) -> fomac::Device { - const qdmi::DeviceSessionConfig config{.baseUrl = baseUrl, - .token = token, - .authFile = authFile, - .authUrl = authUrl, - .username = username, - .password = password, - .custom1 = custom1, - .custom2 = custom2, - .custom3 = custom3, - .custom4 = custom4, - .custom5 = custom5}; - auto* const qdmiDevice = qdmi::Driver::get().addDynamicDeviceLibrary( - libraryPath, prefix, config); - return fomac::Session::createSessionlessDevice(qdmiDevice); - }, - "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(Load a dynamic device library into the QDMI driver. - -This function loads a shared library (.so, .dll, or .dylib) that implements a QDMI device interface and makes it available for use in sessions. - -Args: - library_path: Path to the shared library file to load. - prefix: Function prefix used by the library (e.g., "MY_DEVICE"). - base_url: Optional base URL for the device API endpoint. - token: Optional authentication token. - auth_file: Optional path to authentication file. - auth_url: Optional authentication server URL. - username: Optional username for authentication. - password: Optional password for authentication. - 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. - -Returns: - Device: The newly loaded device that can be used to create backends. - -Raises: - RuntimeError: If library loading fails or configuration is invalid. - -Examples: - Load a device library with configuration: - - >>> import mqt.core.fomac as fomac - >>> device = fomac.add_dynamic_device_library( - ... "/path/to/libmy_device.so", "MY_DEVICE", base_url="http://localhost:8080", custom1="API_V2" - ... ) - - Now the device can be used directly: - - >>> from mqt.core.plugins.qiskit import QDMIBackend - >>> backend = QDMIBackend(device=device))pb"); + auto manager = nb::class_( + m, "DeviceManager", "Discovers and lazily opens QDMI devices."); + manager + .def(nb::init(), + "options"_a = qdmi::ConfigOptions{}) + .def_prop_ro( + "definitions", + [](const qdmi::DeviceManager& self) { return self.definitions(); }) + .def("register_device", &qdmi::DeviceManager::registerDevice, + "definition"_a, nb::kw_only(), "replace"_a = false) + .def( + "unregister_device", + [](qdmi::DeviceManager& self, const std::string& id) { + return self.unregisterDevice(id); + }, + "id"_a) + .def( + "open", + [](qdmi::DeviceManager& self, const std::string& id, + const qdmi::SessionParameters& sessionOverrides) { + return self.open(id, sessionOverrides); + }, + "id"_a, nb::kw_only(), + "session_overrides"_a = qdmi::SessionParameters{}) + .def( + "open_all", + [](qdmi::DeviceManager& self, + const qdmi::SessionParameters& sessionOverrides) { + auto result = self.openAll(sessionOverrides); + return nb::make_tuple(std::move(result.devices), + std::move(result.errors)); + }, + nb::kw_only(), "session_overrides"_a = qdmi::SessionParameters{}, + "Open all definitions independently and return (devices, errors)."); } } // namespace mqt diff --git a/cmake/AddMQTQDMIDevice.cmake b/cmake/AddMQTQDMIDevice.cmake new file mode 100644 index 0000000000..50c1c05e46 --- /dev/null +++ b/cmake/AddMQTQDMIDevice.cmake @@ -0,0 +1,52 @@ +# 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(GNUInstallDirs) + +# Register a relocatable built-in QDMI device. The generated fragment is emitted beside the runtime +# library in both build and install trees. +function(mqt_register_qdmi_device target) + cmake_parse_arguments(ARG "" "ID;PREFIX" "SESSION" ${ARGN}) + if(NOT TARGET ${target}) + message(FATAL_ERROR "Unknown QDMI device target: ${target}") + endif() + if(NOT ARG_ID OR NOT ARG_PREFIX) + message(FATAL_ERROR "mqt_register_qdmi_device requires ID and PREFIX") + endif() + + set(session_json "") + foreach(pair IN LISTS ARG_SESSION) + string(REPLACE "=" ";" parts "${pair}") + list(LENGTH parts count) + if(NOT count EQUAL 2) + message(FATAL_ERROR "Invalid QDMI session default '${pair}'") + endif() + list(GET parts 0 key) + list(GET parts 1 value) + string(APPEND session_json "\n \"${key}\": \"${value}\",") + endforeach() + string(REGEX REPLACE ",$" "" session_json "${session_json}") + + set(fragment "${CMAKE_CURRENT_BINARY_DIR}/$/${target}.qdmi.json") + file( + GENERATE + OUTPUT "${fragment}" + CONTENT + "{\n \"schema-version\": 1,\n \"qdmi\": {\n \"devices\": [\n {\n \"id\": \"${ARG_ID}\",\n \"library\": \"$\",\n \"abi\": \"qdmi-v1\",\n \"prefix\": \"${ARG_PREFIX}\",\n \"enabled\": true,\n \"session\": {${session_json}\n }\n }\n ]\n }\n}\n" + ) + + add_custom_command( + TARGET ${target} + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${fragment}" + "$/${target}.qdmi.json") + install( + FILES "${fragment}" + DESTINATION ${CMAKE_INSTALL_LIBDIR} + COMPONENT ${MQT_CORE_TARGET_NAME}_Runtime) +endfunction() diff --git a/cmake/ExternalDependencies.cmake b/cmake/ExternalDependencies.cmake index b361a7e9ff..b7cf7f107e 100644 --- a/cmake/ExternalDependencies.cmake +++ b/cmake/ExternalDependencies.cmake @@ -47,6 +47,20 @@ set(JSON_Install FetchContent_Declare(nlohmann_json URL ${JSON_URL} FIND_PACKAGE_ARGS ${JSON_VERSION}) list(APPEND FETCH_PACKAGES nlohmann_json) +set(TOMLPLUSPLUS_VERSION + 3.4.0 + CACHE STRING "toml++ version") +set(TOMLPLUSPLUS_URL + https://github.com/marzer/tomlplusplus/archive/refs/tags/v${TOMLPLUSPLUS_VERSION}.tar.gz) +set(TOMLPLUSPLUS_BUILD_TESTS + OFF + CACHE BOOL "Disable toml++ tests" FORCE) +set(TOMLPLUSPLUS_BUILD_EXAMPLES + OFF + CACHE BOOL "Disable toml++ examples" FORCE) +FetchContent_Declare(tomlplusplus URL ${TOMLPLUSPLUS_URL} FIND_PACKAGE_ARGS ${TOMLPLUSPLUS_VERSION}) +list(APPEND FETCH_PACKAGES tomlplusplus) + option(USE_SYSTEM_BOOST "Whether to try to use the system Boost installation" OFF) set(BOOST_MIN_VERSION 1.80.0 diff --git a/docs/qdmi/configuration.md b/docs/qdmi/configuration.md new file mode 100644 index 0000000000..eb9ce4f1ac --- /dev/null +++ b/docs/qdmi/configuration.md @@ -0,0 +1,66 @@ +# QDMI configuration + +QDMI configuration is versioned and trusted: discovering it only parses device +definitions, while opening a configured library executes native code. + +```json +{ + "schema-version": 1, + "qdmi": { + "devices": [ + { + "id": "example.device", + "library": "libexample-device.so", + "abi": "qdmi-v1", + "prefix": "EXAMPLE", + "enabled": true, + "session": { + "base-url": "https://device.example", + "auth-file": "credentials.json", + "custom1": "provider-specific" + } + } + ] + } +} +``` + +Each enabled QDMI v1 definition requires a unique `id`, `library`, and `prefix`; +`abi` defaults to `qdmi-v1`. Relative library and authentication-file paths are +resolved against the file that declared them. Unknown keys, invalid types, +duplicate IDs within one source, and incomplete enabled definitions are errors +with source and property-path diagnostics. + +Sources are merged field-by-field by ID in this order: + +1. generated manifest fragments packaged beside the device libraries; +2. the system `mqt-core.json`; +3. the user/XDG `mqt-core.json`; +4. the nearest project `mqt-core.json`, or `[tool.mqt-core.qdmi]` in + `pyproject.toml` when no dedicated file exists in that directory; +5. `MQT_CORE_QDMI_CONFIG_JSON`; +6. constructor/runtime overrides. + +`MQT_CORE_QDMI_CONFIG_FILE` or `ConfigOptions.explicitFile` replaces the system, +user, and project levels. Packaged definitions remain available unless +`isolated` is enabled. Setting `enabled` to `false` masks an inherited +definition. + +For a static C++ consumer, pass the directory containing generated `*.qdmi.json` +fragments explicitly because a fully static executable has no portable module +origin: + +```cpp +qdmi::ConfigOptions options; +options.configRoot = "/opt/site/lib"; +qdmi::DeviceManager manager(options); +``` + +Runtime registration is also available without modifying configuration files: + +```cpp +manager.registerDevice({.id = "site.device", + .library = "/opt/site/lib/libdevice.so", + .prefix = "SITE"}); +auto device = manager.open("site.device"); +``` diff --git a/docs/qdmi/driver.md b/docs/qdmi/driver.md index f52d4b37d7..52882b0780 100644 --- a/docs/qdmi/driver.md +++ b/docs/qdmi/driver.md @@ -1,55 +1,34 @@ ---- -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 that can be -used directly. Other devices can be loaded dynamically at runtime via -{cpp-api:func}`qdmi::Driver::addDynamicDeviceLibrary`. - -## 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()) +# 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()) { + auto device = manager.open(definition.id); +} +``` +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: + device = manager.open(definition.id) + print(device.name()) ``` + +Opening one device does not open any other definition. `open_all()` returns a +pair of successful devices and per-ID error messages so one unavailable provider +does not hide the remaining devices. + +See [QDMI configuration](configuration.md) for discovery, precedence, and +registration examples. diff --git a/docs/qdmi/index.md b/docs/qdmi/index.md index d26d2cbe52..c1d831f01d 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](driver.md), a [QDMI device for Neutral Atom Systems](na_device.md), and a [QDMI device for a Classical Quantum Circuit Simulator](ddsim_device). @@ -14,6 +14,7 @@ components, such as a [QDMI driver](driver.md), a NA QDMI Device DDSIM QDMI Device -QDMI Driver +QDMI Device Management +QDMI Configuration QDMI-Qiskit Backend ``` diff --git a/docs/qdmi/qdmi_backend.md b/docs/qdmi/qdmi_backend.md index 619cbf1d71..64eacc9ade 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 @@ -216,7 +216,7 @@ except RuntimeError as e: ## Device Capabilities and Target -The backend automatically introspects the FoMaC (QDMI) device and constructs a +The backend automatically introspects the QDMI (QDMI) device and constructs a Qiskit {py:class}`~qiskit.transpiler.Target` object describing device capabilities. @@ -306,8 +306,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 (QDMI) job and +provides status tracking: ```python from qiskit.providers import JobStatus @@ -535,7 +535,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 (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/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/fomac/FoMaC.hpp b/include/mqt-core/qdmi/Device.hpp similarity index 87% rename from include/mqt-core/fomac/FoMaC.hpp rename to include/mqt-core/qdmi/Device.hpp index 25f07ce68f..30049c1378 100644 --- a/include/mqt-core/fomac/FoMaC.hpp +++ b/include/mqt-core/qdmi/Device.hpp @@ -8,8 +8,8 @@ * Licensed under the MIT License */ -/** @file FoMaC.hpp - * @brief FoMaC C++ device-management interface. +/** @file Device.hpp + * @brief QDMI C++ device-management interface. */ #pragma once @@ -38,7 +38,7 @@ #include #include -namespace fomac { +namespace qdmi { using CustomJobParameter = std::variant; /** @@ -302,95 +302,11 @@ 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 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()); +class DeviceManager; /** * @brief Class representing a quantum device. @@ -398,7 +314,7 @@ static_assert(std::is_move_assignable()); * This class provides methods to query properties of the device, * its sites, and its operations. * - * The class can only be constructed by Session instances. + * The class can only be constructed by a DeviceManager. * * @see QDMI_Device */ @@ -514,14 +430,21 @@ class Device { const std::optional& custom4 = std::nullopt, const std::optional& custom5 = std::nullopt) const; - auto operator<=>(const Device&) const noexcept = default; + auto operator<=>(const Device& other) const noexcept { + return device_ <=> other.device_; + } + + bool operator==(const Device& other) const noexcept { + return device_ == other.device_; + } 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) {} + explicit Device(QDMI_Device device, std::shared_ptr lifetime = {}) + : device_(device), lifetime_(std::move(lifetime)) {} /// Query a device property. template @@ -587,7 +510,10 @@ class Device { /// @brief The underlying device pointer. QDMI_Device device_; - friend class Session; + /// Shared ownership of the library and initialized device session. + std::shared_ptr lifetime_; + + friend class DeviceManager; }; /** @@ -721,7 +647,11 @@ class Job { * @brief Constructs a Job object from a QDMI_Job handle. * @param job The QDMI_Job handle to wrap. */ - explicit Job(QDMI_Job job) : job_(job, QDMI_job_free) {} + explicit Job(QDMI_Job job, std::shared_ptr lifetime = {}) + : lifetime_(std::move(lifetime)), job_(job, QDMI_job_free) {} + + /// Shared ownership of the device session used by this job. + std::shared_ptr lifetime_; std::unique_ptr job_{ nullptr, QDMI_job_free}; @@ -801,7 +731,7 @@ class Site { 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_, site_, qdmiProperty, + return QDMI_device_query_site_property(device_, site_, qdmiProperty, size, value, sizeRet); }, "custom site property " + @@ -816,14 +746,15 @@ class Site { * @param device The device that owns the site. * @param site The QDMI_Site handle to wrap. */ - Site(const Device* device, QDMI_Site site) : device_(device), site_(site) {} + Site(QDMI_Device device, QDMI_Site site, std::shared_ptr lifetime = {}) + : device_(device), site_(site), lifetime_(std::move(lifetime)) {} /// 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_, site_, prop, + const auto result = QDMI_device_query_site_property(device_, site_, prop, 0, nullptr, &size); if constexpr (is_optional) { if (result == QDMI_ERROR_NOTSUPPORTED) { @@ -833,15 +764,14 @@ class Site { qdmi::throwIfError(result, std::string("Querying size") + qdmi::toString(prop)); std::string value(size - 1, '\0'); - qdmi::throwIfError(QDMI_device_query_site_property(*device_, site_, prop, - size, value.data(), - nullptr), + qdmi::throwIfError(QDMI_device_query_site_property( + device_, 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_, site_, prop, sizeof(remove_optional_t), &value, nullptr); + device_, site_, prop, sizeof(remove_optional_t), &value, nullptr); if constexpr (is_optional) { if (result == QDMI_ERROR_NOTSUPPORTED) { return std::nullopt; @@ -854,11 +784,13 @@ class Site { } /// @brief A pointer to the device that owns the site. - Device const* device_; + QDMI_Device device_; /// @brief The underlying QDMI_Site object. QDMI_Site site_; + std::shared_ptr lifetime_; + friend class Device; friend class Operation; }; @@ -967,7 +899,7 @@ class Operation { [this, qdmiProperty, &qdmiSites, ¶ms](const size_t size, void* value, size_t* sizeRet) { return QDMI_device_query_operation_property( - *device_, operation_, qdmiSites.size(), qdmiSites.data(), + device_, operation_, qdmiSites.size(), qdmiSites.data(), params.size(), params.data(), qdmiProperty, size, value, sizeRet); }, "custom operation property " + @@ -982,8 +914,10 @@ class Operation { * @param device The device that owns the site. * @param operation The QDMI_Operation handle to wrap. */ - Operation(const Device* device, QDMI_Operation operation) - : device_(device), operation_(operation) {} + Operation(QDMI_Device device, QDMI_Operation operation, + std::shared_ptr lifetime = {}) + : device_(device), operation_(operation), lifetime_(std::move(lifetime)) { + } /// Query an operation property. template @@ -999,7 +933,7 @@ class Operation { if constexpr (string_or_optional_string) { size_t size = 0; auto result = QDMI_device_query_operation_property( - *device_, operation_, sites.size(), qdmiSites.data(), params.size(), + device_, operation_, sites.size(), qdmiSites.data(), params.size(), params.data(), prop, 0, nullptr, &size); if constexpr (is_optional) { if (result == QDMI_ERROR_NOTSUPPORTED) { @@ -1009,7 +943,7 @@ class Operation { qdmi::throwIfError(result, msg); std::string value(size - 1, '\0'); result = QDMI_device_query_operation_property( - *device_, operation_, sites.size(), qdmiSites.data(), params.size(), + device_, operation_, sites.size(), qdmiSites.data(), params.size(), params.data(), prop, size, value.data(), nullptr); qdmi::throwIfError(result, msg); return value; @@ -1017,7 +951,7 @@ class Operation { T>) { size_t size = 0; auto result = QDMI_device_query_operation_property( - *device_, operation_, sites.size(), qdmiSites.data(), params.size(), + device_, operation_, sites.size(), qdmiSites.data(), params.size(), params.data(), prop, 0, nullptr, &size); if constexpr (is_optional) { if (result == QDMI_ERROR_NOTSUPPORTED) { @@ -1028,14 +962,14 @@ class Operation { remove_optional_t value( size / sizeof(typename remove_optional_t::value_type)); result = QDMI_device_query_operation_property( - *device_, operation_, sites.size(), qdmiSites.data(), params.size(), + device_, 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_, operation_, sites.size(), qdmiSites.data(), params.size(), + device_, 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) { @@ -1048,11 +982,13 @@ class Operation { } /// @brief A pointer to the device that owns the operation. - Device const* device_; + QDMI_Device device_; /// @brief The underlying QDMI_Operation object. QDMI_Operation operation_; + std::shared_ptr lifetime_; + friend class Device; }; -} // namespace fomac +} // namespace qdmi diff --git a/include/mqt-core/qdmi/DeviceManager.hpp b/include/mqt-core/qdmi/DeviceManager.hpp new file mode 100644 index 0000000000..2dc232b745 --- /dev/null +++ b/include/mqt-core/qdmi/DeviceManager.hpp @@ -0,0 +1,120 @@ +/* + * 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 Side-effect-free QDMI discovery and lazy device management. + */ + +#pragma once + +#include "qdmi/Device.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace qdmi { + +/** Parameters applied to one device session before it is initialized. */ +struct SessionParameters { + std::optional baseUrl; + 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; +}; + +/** A parsed, side-effect-free registration for one QDMI device. */ +struct DeviceDefinition { + std::string id; + std::filesystem::path library; + std::string abi = "qdmi-v1"; + std::string prefix; + bool enabled = true; + SessionParameters session; + std::filesystem::path source; +}; + +/** Controls configuration discovery. */ +struct ConfigOptions { + std::optional configRoot; + std::optional explicitFile; + std::optional baseDirectory; + bool isolated = false; + std::optional inlineOverrides; + std::vector runtimeOverrides; +}; + +/** Discovers and merges QDMI device definitions without loading libraries. */ +class DeviceRegistry { +public: + explicit DeviceRegistry(const ConfigOptions& options = {}); + + /** Returns enabled definitions in stable ID order. */ + [[nodiscard]] const std::vector& definitions() const { + return definitions_; + } + + /** Registers a complete definition. */ + void registerDevice(DeviceDefinition definition, bool replace = false); + + /** Removes a definition, returning whether it existed. */ + bool unregisterDevice(std::string_view id); + +private: + std::vector definitions_; +}; + +/** Result of independently opening every enabled definition. */ +struct OpenAllResult { + std::map devices; + std::map errors; +}; + +/** Lazily opens configured QDMI devices and shares loaded v1 libraries. */ +class DeviceManager { +public: + explicit DeviceManager(const ConfigOptions& options = {}); + explicit DeviceManager(DeviceRegistry registry); + ~DeviceManager(); + + DeviceManager(DeviceManager&&) noexcept; + DeviceManager& operator=(DeviceManager&&) noexcept; + DeviceManager(const DeviceManager&) = delete; + DeviceManager& operator=(const DeviceManager&) = delete; + + [[nodiscard]] const std::vector& definitions() const; + void registerDevice(DeviceDefinition definition, bool replace = false); + bool unregisterDevice(std::string_view id); + [[nodiscard]] Device + open(std::string_view id, + const SessionParameters& sessionOverrides = SessionParameters{}); + [[nodiscard]] OpenAllResult + openAll(const SessionParameters& sessionOverrides = SessionParameters{}); + +private: + struct Impl; + std::unique_ptr impl_; +}; + +} // namespace qdmi diff --git a/include/mqt-core/qdmi/common/Common.hpp b/include/mqt-core/qdmi/common/Common.hpp index c3586e6013..0e21332eea 100644 --- a/include/mqt-core/qdmi/common/Common.hpp +++ b/include/mqt-core/qdmi/common/Common.hpp @@ -249,6 +249,8 @@ constexpr auto toString(const QDMI_Device_Session_Parameter param) -> const return "USERNAME"; case QDMI_DEVICE_SESSION_PARAMETER_PASSWORD: return "PASSWORD"; + case QDMI_DEVICE_SESSION_PARAMETER_CHILDDEVICE: + return "CHILD DEVICE"; case QDMI_DEVICE_SESSION_PARAMETER_MAX: return "MAX"; case QDMI_DEVICE_SESSION_PARAMETER_CUSTOM1: @@ -386,6 +388,8 @@ constexpr auto toString(const QDMI_Device_Property prop) -> const char* { return "PULSE SUPPORT"; case QDMI_DEVICE_PROPERTY_SUPPORTEDPROGRAMFORMATS: return "SUPPORTED PROGRAM FORMATS"; + case QDMI_DEVICE_PROPERTY_CHILDDEVICES: + return "CHILD DEVICES"; case QDMI_DEVICE_PROPERTY_MAX: return "MAX"; case QDMI_DEVICE_PROPERTY_CUSTOM1: diff --git a/noxfile.py b/noxfile.py index f2f6e6c478..5355a28bfa 100755 --- a/noxfile.py +++ b/noxfile.py @@ -222,7 +222,7 @@ def stubs(session: nox.Session) -> None: "--module", "mqt.core.dd", "--module", - "mqt.core.fomac", + "mqt.core.qdmi", "--module", "mqt.core.mlir", "--module", diff --git a/pyproject.toml b/pyproject.toml index 6157ec6fc8..9a02e5091a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -93,7 +93,7 @@ build.targets = [ "mqt-core-na", "mqt-core-ir-bindings", "mqt-core-dd-bindings", - "mqt-core-fomac-bindings", + "mqt-core-qdmi-bindings", "mqt-core-na-bindings", "mqt-core-mlir-bindings", "mqt-core-qdmi-ddsim-device", @@ -251,7 +251,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] 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 54a0f59cf7..9dae288d97 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 via QDMI. 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: # noq 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 ebf5823f1e..d05f0e7a91 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/estimator.py b/python/mqt/core/plugins/qiskit/estimator.py index a58c141e1f..07f6ac66a3 100644 --- a/python/mqt/core/plugins/qiskit/estimator.py +++ b/python/mqt/core/plugins/qiskit/estimator.py @@ -234,7 +234,7 @@ def _get_observable_circuits( pauli_terms = cast("Mapping[str, int | float | complex]", observable) observable = SparsePauliOp.from_list(pauli_terms.items()) else: - observable = SparsePauliOp(observable) + observable = SparsePauliOp(cast("Any", observable)) paulis = observable.paulis coeffs = observable.coeffs diff --git a/python/mqt/core/plugins/qiskit/job.py b/python/mqt/core/plugins/qiskit/job.py index 2a957331ee..8153f91ae5 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/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..aa297904c5 100644 --- a/python/mqt/core/plugins/qiskit/provider.py +++ b/python/mqt/core/plugins/qiskit/provider.py @@ -14,7 +14,7 @@ from __future__ import annotations -from ... import fomac +from ... import qdmi from .backend import QDMIBackend __all__ = ["QDMIProvider"] @@ -25,10 +25,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: @@ -70,23 +70,28 @@ def __init__( 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 + parameters.project_id = project_id + 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_by_id, _errors = self._manager.open_all(session_overrides=parameters) + devices = devices_by_id.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/fomac.pyi b/python/mqt/core/qdmi.pyi similarity index 77% rename from python/mqt/core/fomac.pyi rename to python/mqt/core/qdmi.pyi index 3e8bd79642..41b700ab22 100644 --- a/python/mqt/core/fomac.pyi +++ b/python/mqt/core/qdmi.pyi @@ -7,77 +7,117 @@ # 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. +class SessionParameters: + """Parameters for one QDMI device session.""" - Allows creating isolated sessions with independent authentication settings. - All authentication parameters are optional and can be provided as keyword arguments to the constructor. - """ + def __init__(self) -> None: ... + @property + def base_url(self) -> str | None: ... + @base_url.setter + def base_url(self, arg: str | None) -> None: ... + @property + def token(self) -> str | None: ... + @token.setter + def token(self, arg: str | None) -> None: ... + @property + def auth_file(self) -> pathlib.Path | None: ... + @auth_file.setter + def auth_file(self, arg: str | os.PathLike | None) -> None: ... + @property + def auth_url(self) -> str | None: ... + @auth_url.setter + def auth_url(self, arg: str | None) -> None: ... + @property + def username(self) -> str | None: ... + @username.setter + def username(self, arg: str | None) -> None: ... + @property + def password(self) -> str | None: ... + @password.setter + def password(self, arg: str | None) -> None: ... + @property + def project_id(self) -> str | None: ... + @project_id.setter + def project_id(self, arg: str | None) -> None: ... + @property + def custom1(self) -> str | None: ... + @custom1.setter + def custom1(self, arg: str | None) -> None: ... + @property + def custom2(self) -> str | None: ... + @custom2.setter + def custom2(self, arg: str | None) -> None: ... + @property + def custom3(self) -> str | None: ... + @custom3.setter + def custom3(self, arg: str | None) -> None: ... + @property + def custom4(self) -> str | None: ... + @custom4.setter + def custom4(self, arg: str | None) -> None: ... + @property + def custom5(self) -> str | None: ... + @custom5.setter + def custom5(self, arg: str | None) -> None: ... + +class DeviceDefinition: + """A side-effect-free QDMI device registration.""" def __init__( self, + id: str, + library: str | os.PathLike, + prefix: str, *, - token: str | None = None, - auth_file: str | 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() - """ + abi: str = "qdmi-v1", + enabled: bool = True, + session: SessionParameters = ..., + ) -> None: ... + @property + def id(self) -> str: ... + @id.setter + def id(self, arg: str, /) -> None: ... + @property + def library(self) -> pathlib.Path: ... + @library.setter + def library(self, arg: str | os.PathLike, /) -> None: ... + @property + def abi(self) -> str: ... + @abi.setter + def abi(self, arg: str, /) -> None: ... + @property + def prefix(self) -> str: ... + @prefix.setter + def prefix(self, arg: str, /) -> None: ... + @property + def enabled(self) -> bool: ... + @enabled.setter + def enabled(self, arg: bool, /) -> None: ... + @property + def session(self) -> SessionParameters: ... + @session.setter + def session(self, arg: SessionParameters, /) -> None: ... + @property + def source(self) -> pathlib.Path: ... - def get_devices(self) -> list[Device]: - """Get available devices from this session. +class ConfigOptions: + """Controls QDMI configuration discovery.""" - Returns: - List of available devices. - """ + def __init__( + self, + *, + config_root: str | os.PathLike | None = None, + explicit_file: str | os.PathLike | None = None, + base_directory: str | os.PathLike | None = None, + isolated: bool = False, + inline_json: str | None = None, + runtime_overrides: Sequence[DeviceDefinition] = [], + ) -> None: ... class Job: """A job represents a submitted quantum program execution.""" @@ -511,57 +551,14 @@ class Device: def __eq__(self, arg: object, /) -> bool: ... def __ne__(self, arg: object, /) -> bool: ... -def add_dynamic_device_library( - library_path: str, - prefix: str, - *, - base_url: str | None = None, - token: str | None = None, - auth_file: str | 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: - """Load a dynamic device library into the QDMI driver. - - This function loads a shared library (.so, .dll, or .dylib) that implements a QDMI device interface and makes it available for use in sessions. - - Args: - library_path: Path to the shared library file to load. - prefix: Function prefix used by the library (e.g., "MY_DEVICE"). - base_url: Optional base URL for the device API endpoint. - token: Optional authentication token. - auth_file: Optional path to authentication file. - auth_url: Optional authentication server URL. - username: Optional username for authentication. - password: Optional password for authentication. - 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. - - Returns: - Device: The newly loaded device that can be used to create backends. - - Raises: - RuntimeError: If library loading fails or configuration is invalid. - - Examples: - Load a device library with configuration: - - >>> import mqt.core.fomac as fomac - >>> device = fomac.add_dynamic_device_library( - ... "/path/to/libmy_device.so", "MY_DEVICE", base_url="http://localhost:8080", custom1="API_V2" - ... ) - - Now the device can be used directly: - - >>> from mqt.core.plugins.qiskit import QDMIBackend - >>> backend = QDMIBackend(device=device) - """ +class DeviceManager: + """Discovers and lazily opens QDMI devices.""" + + def __init__(self, options: ConfigOptions = ...) -> None: ... + @property + def definitions(self) -> list[DeviceDefinition]: ... + def register_device(self, definition: DeviceDefinition, *, replace: bool = False) -> None: ... + def unregister_device(self, id: str) -> bool: ... + def open(self, id: str, *, session_overrides: SessionParameters = ...) -> Device: ... + def open_all(self, *, session_overrides: SessionParameters = ...) -> tuple: + """Open all definitions independently and return (devices, errors).""" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 095203ac15..c92a93d603 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/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..71eb29aa80 100644 --- a/src/qdmi/CMakeLists.txt +++ b/src/qdmi/CMakeLists.txt @@ -10,6 +10,32 @@ 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) + + 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) + + target_link_libraries( + ${TARGET_NAME} + PUBLIC qdmi::qdmi MQT::CoreQDMICommon nlohmann_json::nlohmann_json + PRIVATE MQT::CoreQDMIDriver spdlog::spdlog ${CMAKE_DL_LIBS}) + target_include_directories(${TARGET_NAME} SYSTEM PRIVATE ${tomlplusplus_SOURCE_DIR}/include) + + list(APPEND MQT_CORE_TARGETS ${TARGET_NAME}) +endif() + set(MQT_CORE_TARGETS ${MQT_CORE_TARGETS} PARENT_SCOPE) diff --git a/src/fomac/FoMaC.cpp b/src/qdmi/Device.cpp similarity index 82% rename from src/fomac/FoMaC.cpp rename to src/qdmi/Device.cpp index 4b42160f9b..efa9b7bc5a 100644 --- a/src/fomac/FoMaC.cpp +++ b/src/qdmi/Device.cpp @@ -8,23 +8,20 @@ * Licensed under the MIT License */ -#include "fomac/FoMaC.hpp" +#include "qdmi/Device.hpp" #include "qdmi/common/Common.hpp" #include -#include #include #include #include #include -#include #include #include #include #include -#include #include #include #include @@ -33,7 +30,7 @@ #include #include -namespace fomac { +namespace qdmi { size_t Site::getIndex() const { return queryProperty(QDMI_SITE_PROPERTY_INDEX); } @@ -134,10 +131,11 @@ std::optional> Operation::getSites() const { } std::vector returnedSites; returnedSites.reserve(qdmiSites->size()); - std::ranges::transform(*qdmiSites, std::back_inserter(returnedSites), - [device = device_](const QDMI_Site& site) -> Site { - return {device, site}; - }); + std::ranges::transform( + *qdmiSites, std::back_inserter(returnedSites), + [device = device_, lifetime = lifetime_](const QDMI_Site& site) -> Site { + return {device, site, lifetime}; + }); return returnedSites; } std::optional>> @@ -197,9 +195,10 @@ std::vector Device::getSites() const { 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 {this, site}; }); + std::ranges::transform(qdmiSites, std::back_inserter(sites), + [this](const QDMI_Site& site) -> Site { + return {device_, site, lifetime_}; + }); return sites; } @@ -225,9 +224,10 @@ std::vector Device::getOperations() const { 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 {this, op}; }); + std::ranges::transform(qdmiOperations, std::back_inserter(operations), + [this](const QDMI_Operation& op) -> Operation { + return {device_, op, lifetime_}; + }); return operations; } @@ -245,8 +245,8 @@ Device::getCouplingMap() const { std::ranges::transform(*qdmiCouplingMap, std::back_inserter(couplingMap), [this](const std::pair& pair) -> std::pair { - return {Site{this, pair.first}, - Site{this, pair.second}}; + return {Site{device_, pair.first, lifetime_}, + Site{device_, pair.second, lifetime_}}; }); return couplingMap; } @@ -310,7 +310,9 @@ std::vector Device::getChildDevices() const { devices.reserve(handles.size()); std::ranges::transform( handles, std::back_inserter(devices), - [](QDMI_Device_impl_d* const handle) { return Device(handle); }); + [lifetime = lifetime_](QDMI_Device_impl_d* const handle) { + return Device(handle, lifetime); + }); return devices; } @@ -323,7 +325,7 @@ Job Device::submitJob(const std::string& program, const std::optional& custom5) const { QDMI_Job job = nullptr; qdmi::throwIfError(QDMI_device_create_job(device_, &job), "Creating job"); - Job jobWrapper{job}; + Job jobWrapper{job, lifetime_}; qdmi::throwIfError(QDMI_job_set_parameter(jobWrapper, QDMI_JOB_PARAMETER_PROGRAMFORMAT, @@ -681,105 +683,4 @@ std::map Job::getSparseProbabilities() const { return probabilities; } -Device Session::createSessionlessDevice(QDMI_Device device) { - return Device(device); -} - -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); - } - } - // 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); - setParameter(config.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 +} // namespace qdmi diff --git a/src/qdmi/DeviceManager.cpp b/src/qdmi/DeviceManager.cpp new file mode 100644 index 0000000000..aab323f65d --- /dev/null +++ b/src/qdmi/DeviceManager.cpp @@ -0,0 +1,698 @@ +/* + * 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 "qdmi/driver/Driver.hpp" + +#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 + +namespace qdmi { +namespace { +using Json = nlohmann::json; + +struct SessionPatch { + std::optional baseUrl; + 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; +}; + +struct DefinitionPatch { + std::string id; + std::optional library; + std::optional abi; + std::optional prefix; + std::optional enabled; + SessionPatch session; + std::filesystem::path source; +}; + +[[nodiscard]] auto sourceLabel(const std::filesystem::path& source, + const std::string_view path) -> std::string { + return source.string() + ":" + std::string(path); +} + +void requireObject(const Json& value, const std::filesystem::path& source, + const std::string_view path) { + if (!value.is_object()) { + throw std::invalid_argument(sourceLabel(source, path) + + " must be an object"); + } +} + +void rejectUnknownKeys(const Json& value, + const std::initializer_list allowed, + const std::filesystem::path& source, + const std::string_view path) { + const std::set known(allowed); + for (const auto& [key, unused] : value.items()) { + static_cast(unused); + if (!known.contains(key)) { + throw std::invalid_argument(sourceLabel(source, path) + + " contains unknown key '" + key + "'"); + } + } +} + +[[nodiscard]] auto optionalString(const Json& value, const std::string& key, + const std::filesystem::path& source, + const std::string& path) + -> std::optional { + const auto it = value.find(key); + if (it == value.end()) { + return std::nullopt; + } + if (!it->is_string()) { + throw std::invalid_argument(sourceLabel(source, path + "." + key) + + " must be a string"); + } + return it->get(); +} + +[[nodiscard]] auto resolvePath(std::filesystem::path path, + const std::filesystem::path& base) + -> std::filesystem::path { + if (path.is_relative()) { + path = base / path; + } + return path.lexically_normal(); +} + +[[nodiscard]] auto +parseSessionPatch(const Json& value, const std::filesystem::path& source, + const std::string& path, const std::filesystem::path& base) + -> SessionPatch { + requireObject(value, source, path); + rejectUnknownKeys(value, + {"base-url", "token", "auth-file", "auth-url", "username", + "password", "project-id", "custom1", "custom2", "custom3", + "custom4", "custom5"}, + source, path); + SessionPatch patch; + patch.baseUrl = optionalString(value, "base-url", source, path); + patch.token = optionalString(value, "token", source, path); + patch.authUrl = optionalString(value, "auth-url", source, path); + patch.username = optionalString(value, "username", source, path); + patch.password = optionalString(value, "password", source, path); + patch.projectId = optionalString(value, "project-id", source, path); + patch.custom1 = optionalString(value, "custom1", source, path); + patch.custom2 = optionalString(value, "custom2", source, path); + patch.custom3 = optionalString(value, "custom3", source, path); + patch.custom4 = optionalString(value, "custom4", source, path); + patch.custom5 = optionalString(value, "custom5", source, path); + if (auto authFile = optionalString(value, "auth-file", source, path)) { + patch.authFile = resolvePath(*authFile, base); + } + return patch; +} + +[[nodiscard]] auto +parseDevicePatch(const Json& value, const std::filesystem::path& source, + const std::string& path, const std::filesystem::path& base) + -> DefinitionPatch { + requireObject(value, source, path); + rejectUnknownKeys(value, + {"id", "library", "abi", "prefix", "enabled", "session"}, + source, path); + const auto id = optionalString(value, "id", source, path); + if (!id || id->empty()) { + throw std::invalid_argument(sourceLabel(source, path + ".id") + + " must be a non-empty string"); + } + DefinitionPatch patch; + patch.id = *id; + patch.source = source; + if (auto library = optionalString(value, "library", source, path)) { + patch.library = resolvePath(*library, base); + } + patch.abi = optionalString(value, "abi", source, path); + patch.prefix = optionalString(value, "prefix", source, path); + if (const auto it = value.find("enabled"); it != value.end()) { + if (!it->is_boolean()) { + throw std::invalid_argument(sourceLabel(source, path + ".enabled") + + " must be a boolean"); + } + patch.enabled = it->get(); + } + if (const auto it = value.find("session"); it != value.end()) { + patch.session = parseSessionPatch(*it, source, path + ".session", base); + } + return patch; +} + +[[nodiscard]] auto parseConfiguration(const Json& root, + const std::filesystem::path& source, + const std::filesystem::path& base) + -> std::vector { + requireObject(root, source, "$"); + rejectUnknownKeys(root, {"schema-version", "qdmi"}, source, "$"); + const auto version = root.find("schema-version"); + if (version == root.end() || !version->is_number_integer() || + version->get() != 1) { + throw std::invalid_argument(sourceLabel(source, "$.schema-version") + + " must be the integer 1"); + } + const auto qdmiConfig = root.find("qdmi"); + if (qdmiConfig == root.end()) { + return {}; + } + requireObject(*qdmiConfig, source, "$.qdmi"); + rejectUnknownKeys(*qdmiConfig, {"devices", "device-config"}, source, + "$.qdmi"); + if (const auto deviceConfig = qdmiConfig->find("device-config"); + deviceConfig != qdmiConfig->end()) { + requireObject(*deviceConfig, source, "$.qdmi.device-config"); + } + const auto devices = qdmiConfig->find("devices"); + if (devices == qdmiConfig->end()) { + return {}; + } + if (!devices->is_array()) { + throw std::invalid_argument(sourceLabel(source, "$.qdmi.devices") + + " must be an array"); + } + std::set ids; + std::vector patches; + patches.reserve(devices->size()); + for (size_t i = 0; i < devices->size(); ++i) { + auto patch = + parseDevicePatch((*devices)[i], source, + "$.qdmi.devices[" + std::to_string(i) + "]", base); + if (!ids.emplace(patch.id).second) { + throw std::invalid_argument(sourceLabel(source, "$.qdmi.devices") + + " contains duplicate id '" + patch.id + "'"); + } + patches.emplace_back(std::move(patch)); + } + return patches; +} + +[[nodiscard]] auto readJson(const std::filesystem::path& path) -> Json { + std::ifstream stream(path); + if (!stream) { + throw std::runtime_error("Cannot open QDMI configuration file: " + + path.string()); + } + try { + return Json::parse(stream); + } catch (const Json::parse_error& error) { + throw std::invalid_argument(path.string() + + ": invalid JSON: " + error.what()); + } +} + +[[nodiscard]] auto readPyproject(const std::filesystem::path& path) + -> std::optional { + try { + const auto table = toml::parse_file(path.string()); + const auto* qdmiTable = table["tool"]["mqt-core"]["qdmi"].as_table(); + if (qdmiTable == nullptr) { + return std::nullopt; + } + std::ostringstream formatted; + formatted << toml::json_formatter{*qdmiTable}; + return Json{{"schema-version", 1}, {"qdmi", Json::parse(formatted.str())}}; + } catch (const toml::parse_error& error) { + throw std::invalid_argument( + path.string() + ": invalid TOML: " + std::string(error.description())); + } +} + +template +void mergeOptional(std::optional& target, const std::optional& source) { + if (source) { + target = source; + } +} + +void mergeSession(SessionPatch& target, const SessionPatch& source) { + mergeOptional(target.baseUrl, source.baseUrl); + mergeOptional(target.token, source.token); + mergeOptional(target.authFile, source.authFile); + mergeOptional(target.authUrl, source.authUrl); + mergeOptional(target.username, source.username); + mergeOptional(target.password, source.password); + mergeOptional(target.projectId, source.projectId); + mergeOptional(target.custom1, source.custom1); + mergeOptional(target.custom2, source.custom2); + mergeOptional(target.custom3, source.custom3); + mergeOptional(target.custom4, source.custom4); + mergeOptional(target.custom5, source.custom5); +} + +void mergePatch(DefinitionPatch& target, const DefinitionPatch& source) { + mergeOptional(target.library, source.library); + mergeOptional(target.abi, source.abi); + mergeOptional(target.prefix, source.prefix); + mergeOptional(target.enabled, source.enabled); + mergeSession(target.session, source.session); + target.source = source.source; +} + +[[nodiscard]] auto moduleDirectory() -> std::filesystem::path { +#ifdef _WIN32 + HMODULE module = nullptr; + if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(&moduleDirectory), + &module) == 0) { + return {}; + } + std::wstring buffer(MAX_PATH, L'\0'); + while (true) { + const auto size = GetModuleFileNameW(module, buffer.data(), + static_cast(buffer.size())); + if (size == 0) { + return {}; + } + if (size < buffer.size()) { + buffer.resize(size); + return std::filesystem::path(buffer).parent_path(); + } + buffer.resize(buffer.size() * 2); + } +#else + Dl_info info{}; + if (dladdr(reinterpret_cast(&moduleDirectory), &info) == 0 || + info.dli_fname == nullptr) { + return {}; + } + return std::filesystem::path(info.dli_fname).parent_path(); +#endif +} + +[[nodiscard]] auto environment(const char* name) -> std::optional { + if (const auto* value = std::getenv(name); + value != nullptr && *value != '\0') { + return std::string(value); + } + return std::nullopt; +} + +void appendIfFile(std::vector& files, + const std::filesystem::path& path) { + std::error_code error; + if (std::filesystem::is_regular_file(path, error)) { + files.emplace_back(path); + } +} + +void appendFragments(std::vector& files, + const std::filesystem::path& directory) { + std::error_code error; + if (!std::filesystem::is_directory(directory, error)) { + return; + } + std::vector found; + for (const auto& entry : std::filesystem::directory_iterator(directory)) { + if (entry.is_regular_file() && + entry.path().filename().string().ends_with(".qdmi.json")) { + found.emplace_back(entry.path()); + } + } + std::ranges::sort(found); + files.insert(files.end(), found.begin(), found.end()); +} + +[[nodiscard]] auto nearestProjectConfiguration(std::filesystem::path directory) + -> std::optional { + while (!directory.empty()) { + const auto dedicated = directory / "mqt-core.json"; + if (std::filesystem::is_regular_file(dedicated)) { + return dedicated; + } + const auto pyproject = directory / "pyproject.toml"; + if (std::filesystem::is_regular_file(pyproject)) { + if (readPyproject(pyproject)) { + return pyproject; + } + } + const auto parent = directory.parent_path(); + if (parent == directory) { + break; + } + directory = parent; + } + return std::nullopt; +} + +[[nodiscard]] auto discoverFiles(const ConfigOptions& options) + -> std::vector { + std::vector files; + if (!options.isolated) { + const auto root = options.configRoot.value_or(moduleDirectory()); + appendFragments(files, root); + appendFragments(files, root / "lib"); + appendFragments(files, root / "mqt-core" / "qdmi"); + appendFragments(files, root / "qdmi"); + } + + auto explicitFile = options.explicitFile; + if (!explicitFile) { + if (auto value = environment("MQT_CORE_QDMI_CONFIG_FILE")) { + explicitFile = *value; + } + } + if (explicitFile) { + const auto resolved = resolvePath( + *explicitFile, + options.baseDirectory.value_or(std::filesystem::current_path())); + if (!std::filesystem::is_regular_file(resolved)) { + throw std::runtime_error("Explicit QDMI configuration file does not " + "exist: " + + resolved.string()); + } + files.emplace_back(resolved); + return files; + } + +#ifdef _WIN32 + if (auto programData = environment("PROGRAMDATA")) { + appendIfFile(files, std::filesystem::path(*programData) / "mqt-core" / + "mqt-core.json"); + } + if (auto appData = environment("APPDATA")) { + appendIfFile(files, std::filesystem::path(*appData) / "mqt-core" / + "mqt-core.json"); + } +#else + appendIfFile(files, "/etc/mqt-core/mqt-core.json"); + if (auto xdg = environment("XDG_CONFIG_HOME")) { + appendIfFile(files, + std::filesystem::path(*xdg) / "mqt-core" / "mqt-core.json"); + } else if (auto home = environment("HOME")) { + appendIfFile(files, std::filesystem::path(*home) / ".config" / "mqt-core" / + "mqt-core.json"); + } +#endif + if (auto project = nearestProjectConfiguration( + options.baseDirectory.value_or(std::filesystem::current_path()))) { + files.emplace_back(std::move(*project)); + } + return files; +} + +[[nodiscard]] auto toPatch(const DeviceDefinition& definition, + const std::filesystem::path& base) + -> DefinitionPatch { + DefinitionPatch patch; + patch.id = definition.id; + if (!definition.library.empty()) { + patch.library = resolvePath(definition.library, base); + } + patch.abi = definition.abi; + patch.prefix = definition.prefix; + patch.enabled = definition.enabled; + patch.source = definition.source.empty() ? std::filesystem::path("") + : definition.source; + patch.session.baseUrl = definition.session.baseUrl; + patch.session.token = definition.session.token; + if (definition.session.authFile) { + patch.session.authFile = resolvePath(*definition.session.authFile, base); + } + patch.session.authUrl = definition.session.authUrl; + patch.session.username = definition.session.username; + patch.session.password = definition.session.password; + patch.session.projectId = definition.session.projectId; + patch.session.custom1 = definition.session.custom1; + patch.session.custom2 = definition.session.custom2; + patch.session.custom3 = definition.session.custom3; + patch.session.custom4 = definition.session.custom4; + patch.session.custom5 = definition.session.custom5; + return patch; +} + +[[nodiscard]] auto materialize(const DefinitionPatch& patch) + -> std::optional { + if (!patch.enabled.value_or(true)) { + return std::nullopt; + } + const auto abi = patch.abi.value_or("qdmi-v1"); + if (abi != "qdmi-v1") { + throw std::invalid_argument(patch.source.string() + ": device '" + + patch.id + "' uses unsupported ABI '" + abi + + "'"); + } + if (!patch.library || patch.library->empty()) { + throw std::invalid_argument(patch.source.string() + ": enabled device '" + + patch.id + "' is missing library"); + } + if (!patch.prefix || patch.prefix->empty()) { + throw std::invalid_argument(patch.source.string() + ": enabled device '" + + patch.id + "' is missing prefix"); + } + DeviceDefinition definition; + definition.id = patch.id; + definition.library = *patch.library; + definition.abi = abi; + definition.prefix = *patch.prefix; + definition.enabled = true; + definition.source = patch.source; + definition.session.baseUrl = patch.session.baseUrl; + definition.session.token = patch.session.token; + definition.session.authFile = patch.session.authFile; + definition.session.authUrl = patch.session.authUrl; + definition.session.username = patch.session.username; + definition.session.password = patch.session.password; + definition.session.projectId = patch.session.projectId; + definition.session.custom1 = patch.session.custom1; + definition.session.custom2 = patch.session.custom2; + definition.session.custom3 = patch.session.custom3; + definition.session.custom4 = patch.session.custom4; + definition.session.custom5 = patch.session.custom5; + return definition; +} + +void overlay(SessionParameters& target, const SessionParameters& source) { + mergeOptional(target.baseUrl, source.baseUrl); + mergeOptional(target.token, source.token); + mergeOptional(target.authFile, source.authFile); + mergeOptional(target.authUrl, source.authUrl); + mergeOptional(target.username, source.username); + mergeOptional(target.password, source.password); + mergeOptional(target.projectId, source.projectId); + mergeOptional(target.custom1, source.custom1); + mergeOptional(target.custom2, source.custom2); + mergeOptional(target.custom3, source.custom3); + mergeOptional(target.custom4, source.custom4); + mergeOptional(target.custom5, source.custom5); +} + +[[nodiscard]] auto toV1Config(const SessionParameters& parameters) + -> DeviceSessionConfig { + DeviceSessionConfig config; + config.baseUrl = parameters.baseUrl; + config.token = parameters.token; + if (parameters.authFile) { + config.authFile = parameters.authFile->string(); + } + config.authUrl = parameters.authUrl; + config.username = parameters.username; + config.password = parameters.password; + config.custom1 = parameters.custom1; + config.custom2 = parameters.custom2; + config.custom3 = parameters.custom3; + config.custom4 = parameters.custom4; + config.custom5 = parameters.custom5; + return config; +} +} // namespace + +DeviceRegistry::DeviceRegistry(const ConfigOptions& options) { + std::map merged; + const auto mergePatches = [&merged](std::vector patches) { + for (auto& patch : patches) { + if (auto it = merged.find(patch.id); it != merged.end()) { + mergePatch(it->second, patch); + } else { + merged.emplace(patch.id, std::move(patch)); + } + } + }; + + for (const auto& file : discoverFiles(options)) { + if (file.filename() == "pyproject.toml") { + if (auto config = readPyproject(file)) { + mergePatches(parseConfiguration(*config, file, file.parent_path())); + } + } else { + mergePatches( + parseConfiguration(readJson(file), file, file.parent_path())); + } + } + const auto inlineBase = + options.baseDirectory.value_or(std::filesystem::current_path()); + if (auto inlineJson = environment("MQT_CORE_QDMI_CONFIG_JSON")) { + try { + mergePatches(parseConfiguration( + Json::parse(*inlineJson), "", inlineBase)); + } catch (const Json::parse_error& error) { + throw std::invalid_argument( + std::string(": invalid JSON: ") + + error.what()); + } + } + if (options.inlineOverrides) { + mergePatches( + parseConfiguration(*options.inlineOverrides, "", inlineBase)); + } + for (const auto& definition : options.runtimeOverrides) { + auto patch = toPatch(definition, inlineBase); + if (auto it = merged.find(patch.id); it != merged.end()) { + mergePatch(it->second, patch); + } else { + merged.emplace(patch.id, std::move(patch)); + } + } + for (const auto& [unused, patch] : merged) { + static_cast(unused); + if (auto definition = materialize(patch)) { + definitions_.emplace_back(std::move(*definition)); + } + } +} + +void DeviceRegistry::registerDevice(DeviceDefinition definition, + const bool replace) { + if (definition.id.empty() || definition.library.empty() || + definition.prefix.empty()) { + throw std::invalid_argument( + "A device definition requires a non-empty id, library, and prefix"); + } + const auto it = + std::ranges::find(definitions_, definition.id, &DeviceDefinition::id); + if (it != definitions_.end()) { + if (!replace) { + throw std::invalid_argument("Device '" + definition.id + + "' is already registered"); + } + *it = std::move(definition); + } else if (definition.enabled) { + definitions_.emplace_back(std::move(definition)); + } + std::ranges::sort(definitions_, {}, &DeviceDefinition::id); +} + +bool DeviceRegistry::unregisterDevice(const std::string_view id) { + const auto oldSize = definitions_.size(); + std::erase_if(definitions_, [id](const DeviceDefinition& definition) { + return definition.id == id; + }); + return definitions_.size() != oldSize; +} + +struct DeviceManager::Impl { + explicit Impl(DeviceRegistry initialRegistry) + : registry(std::move(initialRegistry)) {} + + DeviceRegistry registry; + std::mutex mutex; + std::map> libraries; +}; + +DeviceManager::DeviceManager(const ConfigOptions& options) + : DeviceManager(DeviceRegistry(options)) {} + +DeviceManager::DeviceManager(DeviceRegistry registry) + : impl_(std::make_unique(std::move(registry))) {} + +DeviceManager::~DeviceManager() = default; +DeviceManager::DeviceManager(DeviceManager&&) noexcept = default; +DeviceManager& DeviceManager::operator=(DeviceManager&&) noexcept = default; + +const std::vector& DeviceManager::definitions() const { + return impl_->registry.definitions(); +} + +void DeviceManager::registerDevice(DeviceDefinition definition, + const bool replace) { + impl_->registry.registerDevice(std::move(definition), replace); +} + +bool DeviceManager::unregisterDevice(const std::string_view id) { + return impl_->registry.unregisterDevice(id); +} + +Device DeviceManager::open(const std::string_view id, + const SessionParameters& sessionOverrides) { + const auto& definitions = impl_->registry.definitions(); + const auto definition = + std::ranges::find(definitions, id, &DeviceDefinition::id); + if (definition == definitions.end()) { + throw std::out_of_range("No QDMI device is registered with id '" + + std::string(id) + "'"); + } + const auto key = definition->library.string() + "\n" + definition->prefix; + std::shared_ptr library; + { + const std::scoped_lock lock(impl_->mutex); + library = impl_->libraries[key].lock(); + if (!library) { + library = std::make_shared( + definition->library.string(), definition->prefix); + impl_->libraries[key] = library; + } + } + auto parameters = definition->session; + overlay(parameters, sessionOverrides); + auto state = + std::make_shared(library, toV1Config(parameters)); + auto* const handle = state.get(); + return Device(handle, std::move(state)); +} + +OpenAllResult +DeviceManager::openAll(const SessionParameters& sessionOverrides) { + 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/devices/dd/CMakeLists.txt b/src/qdmi/devices/dd/CMakeLists.txt index ee4a2a3458..f2129b8e92 100644 --- a/src/qdmi/devices/dd/CMakeLists.txt +++ b/src/qdmi/devices/dd/CMakeLists.txt @@ -17,6 +17,10 @@ if(NOT TARGET ${TARGET_NAME}) # Add library add_mqt_core_library(${TARGET_NAME} FORCE_SHARED HIDDEN_VISIBILITY ALIAS_NAME QDMI_DDSIM_Device) + set_target_properties( + ${TARGET_NAME} + PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR}") # Generate prefixed QDMI headers generate_prefixed_qdmi_headers(${QDMI_PREFIX}) @@ -51,6 +55,8 @@ if(NOT TARGET ${TARGET_NAME}) # Add to list of MQT Core targets list(APPEND MQT_CORE_TARGETS ${TARGET_NAME}) + + mqt_register_qdmi_device(${TARGET_NAME} ID mqt.ddsim.default PREFIX ${QDMI_PREFIX}) endif() set(MQT_CORE_TARGETS diff --git a/src/qdmi/devices/na/CMakeLists.txt b/src/qdmi/devices/na/CMakeLists.txt index fb77c9164c..41e89735e7 100644 --- a/src/qdmi/devices/na/CMakeLists.txt +++ b/src/qdmi/devices/na/CMakeLists.txt @@ -98,6 +98,10 @@ if(NOT TARGET ${TARGET_NAME}) # Add library add_mqt_core_library(${TARGET_NAME} FORCE_SHARED HIDDEN_VISIBILITY ALIAS_NAME QDMINaDevice) + set_target_properties( + ${TARGET_NAME} + PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR}") add_dependencies(${TARGET_NAME} generate_qdmi_na_device_header) # Generate prefixed QDMI headers @@ -129,6 +133,8 @@ if(NOT TARGET ${TARGET_NAME}) # add to list of MQT core targets list(APPEND MQT_CORE_TARGETS ${TARGET_NAME}) + + mqt_register_qdmi_device(${TARGET_NAME} ID mqt.na.default PREFIX ${QDMI_PREFIX}) endif() set(MQT_CORE_TARGETS diff --git a/src/qdmi/devices/sc/CMakeLists.txt b/src/qdmi/devices/sc/CMakeLists.txt index d6650f8799..1f01de4bce 100644 --- a/src/qdmi/devices/sc/CMakeLists.txt +++ b/src/qdmi/devices/sc/CMakeLists.txt @@ -95,6 +95,10 @@ if(NOT TARGET ${TARGET_NAME}) # Add library add_mqt_core_library(${TARGET_NAME} FORCE_SHARED HIDDEN_VISIBILITY ALIAS_NAME QDMIScDevice) + set_target_properties( + ${TARGET_NAME} + PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR}") add_dependencies(${TARGET_NAME} generate_qdmi_sc_device_header) # Generate prefixed QDMI headers @@ -126,6 +130,8 @@ if(NOT TARGET ${TARGET_NAME}) # add to list of MQT core targets list(APPEND MQT_CORE_TARGETS ${TARGET_NAME}) + + mqt_register_qdmi_device(${TARGET_NAME} ID mqt.sc.default PREFIX ${QDMI_PREFIX}) endif() set(MQT_CORE_TARGETS diff --git a/src/qdmi/driver/CMakeLists.txt b/src/qdmi/driver/CMakeLists.txt index 3712a4b386..2cefe92919 100644 --- a/src/qdmi/driver/CMakeLists.txt +++ b/src/qdmi/driver/CMakeLists.txt @@ -12,12 +12,9 @@ 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 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) + # The v1 implementation is private to the public DeviceManager abstraction. + target_sources(${TARGET_NAME} PRIVATE Driver.cpp + ${MQT_CORE_INCLUDE_BUILD_DIR}/qdmi/driver/Driver.hpp) # Add link libraries target_link_libraries( @@ -25,41 +22,6 @@ if(NOT TARGET ${TARGET_NAME}) PUBLIC qdmi::qdmi MQT::CoreQDMICommon PRIVATE qdmi::qdmi_project_warnings spdlog::spdlog ${CMAKE_DL_LIBS}) - add_dependencies(${TARGET_NAME} MQT::CoreQDMINaDevice MQT::CoreQDMIScDevice - MQT::CoreQDMI_DDSIM_Device) - target_compile_definitions( - ${TARGET_NAME} - PRIVATE - "DYN_DEV_LIBS=std::pair{\"$\", \"MQT_NA\"}, std::pair{\"$\", \"MQT_SC\"}, std::pair{\"$\", \"MQT_DDSIM\"}" - ) - - # Ensure the driver can find the device libraries at runtime - if(WIN32) - # On Windows, we need to copy the device DLLs to the library directory - add_custom_command( - TARGET ${TARGET_NAME} - PRE_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different $ - $) - add_custom_command( - TARGET ${TARGET_NAME} - PRE_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different $ - $) - add_custom_command( - TARGET ${TARGET_NAME} - PRE_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different $ - $) - else() - target_link_options( - ${TARGET_NAME} - INTERFACE - $> - $> - $>) - endif() - # add to list of MQT core targets list(APPEND MQT_CORE_TARGETS ${TARGET_NAME}) endif() diff --git a/src/qdmi/driver/Driver.cpp b/src/qdmi/driver/Driver.cpp index 2562f338c3..2d369fc33e 100644 --- a/src/qdmi/driver/Driver.cpp +++ b/src/qdmi/driver/Driver.cpp @@ -496,16 +496,7 @@ auto QDMI_Session_impl_d::querySessionProperty(QDMI_Session_Property prop, } namespace qdmi { -Driver::Driver() { - for (const auto& [lib, prefix] : std::array{DYN_DEV_LIBS}) { - try { - addDynamicDeviceLibrary(lib, prefix); - } catch (const std::exception& ex) { - SPDLOG_WARN("Skipping builtin QDMI device library '{}': {}", lib, - ex.what()); - } - } -} +Driver::Driver() = default; auto Driver::addDynamicDeviceLibrary(const std::string& libName, const std::string& prefix, diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 6203a14e01..eccf7f16cc 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -15,7 +15,6 @@ add_subdirectory(na) add_subdirectory(zx) add_subdirectory(qir) add_subdirectory(qdmi) -add_subdirectory(fomac) # copy test circuits to build directory file(COPY ${PROJECT_SOURCE_DIR}/test/circuits DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/test/na/CMakeLists.txt b/test/na/CMakeLists.txt index fa743e93b1..b3a5736d59 100644 --- a/test/na/CMakeLists.txt +++ b/test/na/CMakeLists.txt @@ -12,4 +12,4 @@ if(TARGET MQT::CoreNA) target_link_libraries(mqt-core-na-test PRIVATE MQT::CoreQASM) endif() -add_subdirectory(fomac) +add_subdirectory(qdmi) diff --git a/test/na/fomac/CMakeLists.txt b/test/na/qdmi/CMakeLists.txt similarity index 71% rename from test/na/fomac/CMakeLists.txt rename to test/na/qdmi/CMakeLists.txt index b30219f250..7a45c9949f 100644 --- a/test/na/fomac/CMakeLists.txt +++ b/test/na/qdmi/CMakeLists.txt @@ -6,14 +6,16 @@ # # Licensed under the MIT License -if(TARGET MQT::CoreNAFoMaC) +if(TARGET MQT::CoreNAQDMI) # Set test target name - set(TARGET_NAME ${MQT_CORE_TARGET_NAME}-na-fomac-test) + set(TARGET_NAME ${MQT_CORE_TARGET_NAME}-na-qdmi-test) # Add the test executable - package_add_test(${TARGET_NAME} MQT::CoreNAFoMaC test_fomac.cpp) + 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") + target_compile_definitions( + ${TARGET_NAME} PRIVATE NA_DEVICE_JSON="${PROJECT_SOURCE_DIR}/json/na/device.json" + NA_DEVICE_LIBRARY="$") + add_dependencies(${TARGET_NAME} MQT::CoreQDMINaDevice) if(WIN32) # On Windows, we need to copy the device DLLs to the test directory 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..285da40e88 100644 --- a/test/na/fomac/test_fomac.cpp +++ b/test/na/qdmi/test_qdmi.cpp @@ -8,7 +8,8 @@ * Licensed under the MIT License */ -#include "na/fomac/Device.hpp" +#include "na/qdmi/Device.hpp" +#include "qdmi/DeviceManager.hpp" #include #include @@ -28,27 +29,43 @@ auto canonicallyOrderLatticeVectors(nlohmann::json& device) -> void { } } } + +auto getDevice() -> qdmi::Device { + ::qdmi::ConfigOptions options; + options.isolated = true; + options.runtimeOverrides = {{ + .id = "mqt.na.default", + .library = NA_DEVICE_LIBRARY, + .prefix = "MQT_NA", + }}; + auto device = ::qdmi::DeviceManager(options).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 +76,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/na/test_na_fomac.py b/test/python/na/test_na_qdmi.py similarity index 96% rename from test/python/na/test_na_fomac.py rename to test/python/na/test_na_qdmi.py index 85cc896158..bddba4ff9c 100644 --- a/test/python/na/test_na_fomac.py +++ b/test/python/na/test_na_qdmi.py @@ -16,20 +16,23 @@ 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 + devices, errors = DeviceManager().open_all() + assert not errors + converted = (Device.try_create_from_device(candidate) for candidate in devices.values()) + device = next(candidate for candidate in converted if candidate 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 4db8440785..b90c987599 100644 --- a/test/python/plugins/qiskit/test_backend.py +++ b/test/python/plugins/qiskit/test_backend.py @@ -19,7 +19,7 @@ from qiskit.circuit.library import UnitaryGate from qiskit.providers import JobStatus -from mqt.core import fomac +from mqt.core import qdmi from mqt.core.plugins.qiskit import ( CircuitValidationError, QDMIBackend, @@ -29,7 +29,7 @@ if TYPE_CHECKING: - 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,9 +40,9 @@ def operations(self) -> list[object]: ... def coupling_map(self) -> object: ... - SiteSpecificDevice = _FomacDeviceLike - MisconfiguredDevice = _FomacDeviceLike - ZonedDevice = _FomacDeviceLike + SiteSpecificDevice = _QDMIDeviceLike + MisconfiguredDevice = _QDMIDeviceLike + ZonedDevice = _QDMIDeviceLike @pytest.fixture @@ -52,9 +52,8 @@ def ddsim_backend() -> QDMIBackend: Returns: A QDMIBackend instance wrapping the DDSIM device. """ - session = fomac.Session() - devices = session.get_devices() - for device in devices: + devices, _errors = qdmi.DeviceManager().open_all() + for device in devices.values(): if "DDSIM" in device.name(): return QDMIBackend(device=device, provider=None) pytest.skip("DDSIM device not available") @@ -600,9 +599,8 @@ def test_backend_openqasm3_translation_works_for_native_gates(ddsim_backend: QDM def test_zoned_operation_rejected_at_backend_init() -> None: """Backend rejects devices exposing zoned operations.""" - session = fomac.Session() - devices = session.get_devices() - for device in devices: + devices, _errors = qdmi.DeviceManager().open_all() + for device in devices.values(): if device.name().startswith("MQT NA"): with pytest.raises(UnsupportedDeviceError, match="cannot be represented in Qiskit's Target model"): QDMIBackend(device) diff --git a/test/python/plugins/qiskit/test_estimator.py b/test/python/plugins/qiskit/test_estimator.py index f296803c08..80bf8f89b2 100644 --- a/test/python/plugins/qiskit/test_estimator.py +++ b/test/python/plugins/qiskit/test_estimator.py @@ -17,16 +17,15 @@ from qiskit.primitives.containers.estimator_pub import EstimatorPub from qiskit.quantum_info import SparsePauliOp -from mqt.core import fomac +from mqt.core import qdmi from mqt.core.plugins.qiskit import QDMIBackend, QDMIEstimator @pytest.fixture def estimator() -> QDMIEstimator: """Returns a QDMIEstimator based on the DDSIM backend.""" - session = fomac.Session() - devices = session.get_devices() - for device in devices: + devices, _errors = qdmi.DeviceManager().open_all() + for device in devices.values(): if "DDSIM" in device.name(): backend = QDMIBackend(device=device, provider=None) return QDMIEstimator(backend) diff --git a/test/python/plugins/qiskit/test_mock_backend.py b/test/python/plugins/qiskit/test_mock_backend.py index b5b60af6c0..9b1bd3cd5e 100644 --- a/test/python/plugins/qiskit/test_mock_backend.py +++ b/test/python/plugins/qiskit/test_mock_backend.py @@ -22,7 +22,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 +40,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 +138,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 +146,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 +159,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 +262,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: # noqa: ARG002 + def submit_job(self, program: str, program_format: qdmi.ProgramFormat, num_shots: int) -> MockJob: # noqa: ARG002 """Submit a mock job to the device. Args: @@ -318,13 +318,13 @@ 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]) -> None: + """Patch QDMI device discovery to return the supplied mock devices.""" - def _mock_get_devices(_self: object) -> list[MockQDMIDevice]: - return devices + def _mock_open_all(_self: object, **_kwargs: object) -> tuple[dict[str, MockQDMIDevice], dict[str, str]]: + return {f"mock-{index}": device for index, device in enumerate(devices)}, {} - monkeypatch.setattr(fomac.Session, "get_devices", _mock_get_devices) + monkeypatch.setattr(qdmi.DeviceManager, "open_all", _mock_open_all) def test_backend_warns_on_unmappable_operation( @@ -338,8 +338,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 +364,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 +383,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 +410,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 +444,9 @@ 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]) # noqa: SLF001 + program, fmt = backend._convert_circuit(qc, [qdmi.ProgramFormat.QASM3]) # noqa: SLF001 - 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 +462,9 @@ 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]) # noqa: SLF001 + program, fmt = backend._convert_circuit(qc, [qdmi.ProgramFormat.QASM2]) # noqa: SLF001 - 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 +480,9 @@ 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]) # noqa: SLF001 + program, fmt = backend._convert_circuit(qc, [qdmi.ProgramFormat.QASM2, qdmi.ProgramFormat.QASM3]) # noqa: SLF001 - assert fmt == fomac.ProgramFormat.QASM3 + assert fmt == qdmi.ProgramFormat.QASM3 assert "OPENQASM 3" in program @@ -492,12 +490,12 @@ 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: # noqa: ARG001 + def mock_submit_job(program: str, program_format: qdmi.ProgramFormat, num_shots: int) -> MockQDMIDevice.MockJob: # noqa: ARG001 nonlocal submitted_format submitted_format = program_format return device.MockJob(num_clbits=2, shots=num_shots) @@ -513,19 +511,19 @@ 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: # noqa: ARG001 + def mock_submit_job(program: str, program_format: qdmi.ProgramFormat, num_shots: int) -> MockQDMIDevice.MockJob: # noqa: ARG001 nonlocal submitted_format submitted_format = program_format return device.MockJob(num_clbits=2, shots=num_shots) @@ -541,20 +539,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,11 +589,11 @@ 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]) # noqa: SLF001 + backend._convert_circuit(qc, [qdmi.ProgramFormat.QPY]) # noqa: SLF001 def test_map_operation_returns_none_for_unknown() -> None: - """Unknown FoMaC operations cannot be mapped to Qiskit gates.""" + """Unknown QDMI operations cannot be mapped to Qiskit gates.""" assert QDMIBackend._map_operation_to_gate("unknown_gate") is None # noqa: SLF001 assert QDMIBackend._map_operation_to_gate("custom_op") is None # noqa: SLF001 assert QDMIBackend._map_operation_to_gate("") is None # noqa: SLF001 @@ -658,8 +656,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 8bc1136269..a1c42ddd98 100644 --- a/test/python/plugins/qiskit/test_provider.py +++ b/test/python/plugins/qiskit/test_provider.py @@ -15,7 +15,7 @@ import pytest -from mqt.core import fomac +from mqt.core import qdmi from mqt.core.plugins.qiskit import QDMIProvider @@ -80,10 +80,10 @@ def test_provider_get_backend_no_devices(monkeypatch: pytest.MonkeyPatch) -> Non """Provider raises ValueError when no devices available.""" # Monkeypatch to return empty device list - def mock_get_devices(_self: object) -> list[object]: - return [] + def mock_open_all(_self: object, **_kwargs: object) -> tuple[dict[str, object], dict[str, str]]: + return {}, {} - monkeypatch.setattr(fomac.Session, "get_devices", mock_get_devices) + monkeypatch.setattr(qdmi.DeviceManager, "open_all", mock_open_all) provider = QDMIProvider() with pytest.raises(ValueError, match="No backend found with name"): diff --git a/test/python/plugins/qiskit/test_sampler.py b/test/python/plugins/qiskit/test_sampler.py index 63cfc5f4d2..9b2a788292 100644 --- a/test/python/plugins/qiskit/test_sampler.py +++ b/test/python/plugins/qiskit/test_sampler.py @@ -17,16 +17,15 @@ from qiskit import QuantumCircuit from qiskit.circuit import ClassicalRegister, Parameter -from mqt.core import fomac +from mqt.core import qdmi from mqt.core.plugins.qiskit import QDMIBackend, QDMISampler @pytest.fixture def sampler() -> QDMISampler: """Returns a QDMISampler based on the DDSIM backend.""" - session = fomac.Session() - devices = session.get_devices() - for device in devices: + devices, _errors = qdmi.DeviceManager().open_all() + for device in devices.values(): if "DDSIM" in device.name(): backend = QDMIBackend(device=device, provider=None) return QDMISampler(backend) diff --git a/test/python/fomac/test_fomac.py b/test/python/qdmi/test_qdmi.py similarity index 77% rename from test/python/fomac/test_fomac.py rename to test/python/qdmi/test_qdmi.py index 6cdc5c63d9..fb0ad72c75 100644 --- a/test/python/fomac/test_fomac.py +++ b/test/python/qdmi/test_qdmi.py @@ -10,25 +10,23 @@ from __future__ import annotations -import tempfile -from pathlib import Path from typing import cast import pytest -from mqt.core.fomac import CustomProperty, Device, Job, ProgramFormat, Session, add_dynamic_device_library +from mqt.core.qdmi import CustomProperty, Device, DeviceManager, Job, ProgramFormat CustomValueType = type[str] | type[bool] | type[int] | type[float] | type[bytes] def _get_devices() -> list[Device]: - """Get all available devices from a Session. + """Open all available QDMI devices. Returns: List of all available QDMI devices. """ - session = Session() - return session.get_devices() + devices, _errors = DeviceManager().open_all() + return list(devices.values()) @pytest.fixture(params=_get_devices()) @@ -686,214 +684,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") # noqa: S106 - assert session is not None - - # Token with special characters should be accepted - session = Session(token="very_long_token_with_special_characters_!@#$%^&*()") # noqa: S106 - 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: - # Existing file should be accepted (validation passes, parameter may be skipped) - session = Session(auth_file=tmp_path) - assert 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") # noqa: S106 - assert session is not None - - # Both username and password - session = Session(username="user123", password="secure_password") # noqa: S106 - 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", # noqa: S106 - username="test_user", - password="test_pass", # noqa: S106 - 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", # noqa: S106 - 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_add_dynamic_device_library_nonexistent_library() -> None: - """Test that loading a non-existent library raises an error.""" - with pytest.raises(RuntimeError): - add_dynamic_device_library("/nonexistent/lib.so", "PREFIX") diff --git a/test/qdmi/CMakeLists.txt b/test/qdmi/CMakeLists.txt index 595fc88d15..82214c5a5d 100644 --- a/test/qdmi/CMakeLists.txt +++ b/test/qdmi/CMakeLists.txt @@ -7,4 +7,5 @@ # Licensed under the MIT License add_subdirectory(devices) -add_subdirectory(driver) +add_subdirectory(device) +add_subdirectory(manager) diff --git a/test/fomac/CMakeLists.txt b/test/qdmi/device/CMakeLists.txt similarity index 64% rename from test/fomac/CMakeLists.txt rename to test/qdmi/device/CMakeLists.txt index dd6542126f..0c88cf675e 100644 --- a/test/fomac/CMakeLists.txt +++ b/test/qdmi/device/CMakeLists.txt @@ -6,10 +6,17 @@ # # Licensed under the MIT License -set(TARGET_NAME mqt-core-fomac-test) +set(TARGET_NAME mqt-core-qdmi-object-model-test) -if(TARGET MQT::CoreFoMaC) - package_add_test(${TARGET_NAME} MQT::CoreFoMaC test_fomac.cpp) +if(TARGET MQT::CoreQDMI) + package_add_test(${TARGET_NAME} MQT::CoreQDMI test_device.cpp) + target_compile_definitions( + ${TARGET_NAME} + PRIVATE DDSIM_DEVICE_LIBRARY="$" + NA_DEVICE_LIBRARY="$" + SC_DEVICE_LIBRARY="$") + add_dependencies(${TARGET_NAME} MQT::CoreQDMI_DDSIM_Device MQT::CoreQDMINaDevice + MQT::CoreQDMIScDevice) if(WIN32) # On Windows, we need to copy the device DLLs to the test directory diff --git a/test/fomac/test_fomac.cpp b/test/qdmi/device/test_device.cpp similarity index 81% rename from test/fomac/test_fomac.cpp rename to test/qdmi/device/test_device.cpp index 935091d847..d4484c0207 100644 --- a/test/fomac/test_fomac.cpp +++ b/test/qdmi/device/test_device.cpp @@ -8,7 +8,8 @@ * Licensed under the MIT License */ -#include "fomac/FoMaC.hpp" +#include "qdmi/Device.hpp" +#include "qdmi/DeviceManager.hpp" #include "qdmi/common/Common.hpp" #include @@ -32,7 +33,7 @@ #include #include -namespace fomac { +namespace qdmi { namespace { @@ -57,6 +58,32 @@ template auto bytesOf(const T& value) { return bytes; } +auto configuredManager() -> DeviceManager { + ConfigOptions options; + options.isolated = true; + options.runtimeOverrides = { + {.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"}, + }; + return DeviceManager(options); +} + +auto getDevices() -> std::vector { + auto opened = configuredManager().openAll(); + std::vector devices; + devices.reserve(opened.devices.size()); + std::ranges::move(opened.devices | std::views::values, + std::back_inserter(devices)); + return devices; +} + class DeviceTest : public testing::TestWithParam { protected: Device device; @@ -86,13 +113,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"); } }; @@ -258,7 +279,7 @@ TEST(CustomPropertyTest, RejectsIncompatibleRepresentations) { 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 +296,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 +319,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 +343,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"); @@ -354,11 +375,11 @@ TEST(FoMaCTest, DevicePropertyToString) { EXPECT_STREQ(qdmi::toString(QDMI_DEVICE_PROPERTY_CUSTOM5), "CUSTOM5"); } -TEST(FoMaCTest, SessionPropertyToString) { +TEST(QDMITest, SessionPropertyToString) { EXPECT_STREQ(qdmi::toString(QDMI_SESSION_PROPERTY_DEVICES), "DEVICES"); } -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"), @@ -992,209 +1013,8 @@ TEST(AuthenticationTest, SessionParameterToString) { 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.string(); - 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, OpenAllReturnsDevices) { + auto devices = getDevices(); EXPECT_FALSE(devices.empty()); @@ -1205,12 +1025,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()); @@ -1220,14 +1037,6 @@ TEST(AuthenticationTest, SessionMultipleInstances) { EXPECT_EQ(devices1.size(), devices2.size()); } -namespace { -// Helper function to get all devices for parameterized tests -auto getDevices() -> std::vector { - Session session; - return session.getDevices(); -} -} // namespace - INSTANTIATE_TEST_SUITE_P( // Custom instantiation name DeviceTest, @@ -1270,4 +1079,4 @@ INSTANTIATE_TEST_SUITE_P( return name; }); -} // namespace fomac +} // namespace qdmi diff --git a/test/qdmi/driver/CMakeLists.txt b/test/qdmi/driver/CMakeLists.txt index 6fc3089a23..ec91fd6fcf 100644 --- a/test/qdmi/driver/CMakeLists.txt +++ b/test/qdmi/driver/CMakeLists.txt @@ -10,7 +10,7 @@ set(TARGET_NAME mqt-core-qdmi-driver-test) if(TARGET MQT::CoreQDMIDriver) package_add_test(${TARGET_NAME} MQT::CoreQDMIDriver test_driver.cpp) - target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreFoMaC) + target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQDMI) target_compile_definitions( ${TARGET_NAME} PRIVATE diff --git a/test/qdmi/driver/test_driver.cpp b/test/qdmi/driver/test_driver.cpp index 9c3737fdf6..64c017b2e7 100644 --- a/test/qdmi/driver/test_driver.cpp +++ b/test/qdmi/driver/test_driver.cpp @@ -8,7 +8,7 @@ * Licensed under the MIT License */ -#include "fomac/FoMaC.hpp" +#include "qdmi/Device.hpp" #include "qdmi/driver/Driver.hpp" #include @@ -323,12 +323,6 @@ TEST(ChildDeviceTest, WrapsOpaqueHandlesInStableClientDevices) { 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, 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..12a6763ad5 --- /dev/null +++ b/test/qdmi/manager/test_device_manager.cpp @@ -0,0 +1,185 @@ +/* + * 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 +#include + +#include +#include +#include +#include + +namespace { +class TemporaryDirectory { +public: + TemporaryDirectory() { + path_ = + std::filesystem::temp_directory_path() / "mqt-core-qdmi-manager-test"; + std::filesystem::remove_all(path_); + std::filesystem::create_directories(path_); + } + + ~TemporaryDirectory() { std::filesystem::remove_all(path_); } + + [[nodiscard]] const std::filesystem::path& path() const { return path_; } + + void write(const std::filesystem::path& relative, + const std::string& contents) const { + std::ofstream output(path_ / relative); + output << contents; + } + +private: + std::filesystem::path path_; +}; + +TEST(DeviceRegistry, ParsesInlineConfigurationWithoutLoadingLibraries) { + qdmi::ConfigOptions options; + options.isolated = true; + options.baseDirectory = "/configuration"; + options.inlineOverrides = nlohmann::json::parse(R"({ + "schema-version": 1, + "qdmi": { + "devices": [{ + "id": "example.device", + "library": "libexample.so", + "prefix": "EXAMPLE", + "session": {"auth-file": "secret.json", "custom1": "value"} + }] + } + })"); + + const qdmi::DeviceRegistry registry(options); + ASSERT_EQ(registry.definitions().size(), 1); + const auto& definition = registry.definitions().front(); + EXPECT_EQ(definition.id, "example.device"); + EXPECT_EQ(definition.library, + std::filesystem::path("/configuration/libexample.so")); + ASSERT_TRUE(definition.session.authFile.has_value()); + EXPECT_EQ(*definition.session.authFile, + std::filesystem::path("/configuration/secret.json")); + EXPECT_EQ(definition.session.custom1, "value"); +} + +TEST(DeviceRegistry, RejectsDuplicateIdsAndUnknownKeys) { + qdmi::ConfigOptions options; + options.isolated = true; + options.inlineOverrides = nlohmann::json::parse(R"({ + "schema-version": 1, + "qdmi": {"devices": [ + {"id": "duplicate", "library": "one", "prefix": "ONE"}, + {"id": "duplicate", "library": "two", "prefix": "TWO"} + ]} + })"); + EXPECT_THROW(static_cast(qdmi::DeviceRegistry(options)), + std::invalid_argument); + + options.inlineOverrides = nlohmann::json::parse(R"({ + "schema-version": 1, + "qdmi": {"devices": [ + {"id": "invalid", "library": "one", "prefix": "ONE", "typo": true} + ]} + })"); + EXPECT_THROW(static_cast(qdmi::DeviceRegistry(options)), + std::invalid_argument); +} + +TEST(DeviceRegistry, DisabledOverrideMasksInheritedDefinition) { + qdmi::DeviceDefinition definition{ + .id = "masked", .library = "unused", .prefix = "UNUSED"}; + qdmi::ConfigOptions options; + options.isolated = true; + options.runtimeOverrides.emplace_back(definition); + options.inlineOverrides = nlohmann::json::parse(R"({ + "schema-version": 1, + "qdmi": {"devices": [{"id": "masked", "enabled": false}]} + })"); + + // Runtime overrides have the highest precedence and deliberately re-enable + // complete definitions. + EXPECT_EQ(qdmi::DeviceRegistry(options).definitions().size(), 1); + + options.runtimeOverrides.clear(); + EXPECT_TRUE(qdmi::DeviceRegistry(options).definitions().empty()); +} + +TEST(DeviceRegistry, MergesExplicitConfigurationOverBuiltInFragments) { + const TemporaryDirectory directory; + directory.write("builtin.qdmi.json", R"({ + "schema-version": 1, + "qdmi": {"devices": [{ + "id": "merged", "library": "device.so", "prefix": "DEVICE", + "session": {"base-url": "https://default.invalid", "custom1": "one"} + }]} + })"); + directory.write("override.json", R"({ + "schema-version": 1, + "qdmi": {"devices": [{ + "id": "merged", "session": {"custom1": "two"} + }]} + })"); + qdmi::ConfigOptions options; + options.configRoot = directory.path(); + options.explicitFile = directory.path() / "override.json"; + + const qdmi::DeviceRegistry registry(options); + const auto& definitions = registry.definitions(); + ASSERT_EQ(definitions.size(), 1); + EXPECT_EQ(definitions.front().library, directory.path() / "device.so"); + EXPECT_EQ(definitions.front().session.baseUrl, "https://default.invalid"); + EXPECT_EQ(definitions.front().session.custom1, "two"); +} + +TEST(DeviceRegistry, ReadsProjectConfigurationFromPyprojectToml) { + const TemporaryDirectory directory; + directory.write("pyproject.toml", R"( + [tool.mqt-core.qdmi] + devices = [ + { id = "toml", library = "device.so", prefix = "TOML" } + ] + )"); + qdmi::ConfigOptions options; + options.isolated = true; + options.baseDirectory = directory.path(); + + const qdmi::DeviceRegistry registry(options); + const auto& definitions = registry.definitions(); + ASSERT_EQ(definitions.size(), 1); + EXPECT_EQ(definitions.front().id, "toml"); + EXPECT_EQ(definitions.front().library, directory.path() / "device.so"); +} + +TEST(DeviceManager, LazilyOpensAndKeepsDeviceAlive) { + qdmi::ConfigOptions options; + options.isolated = true; + options.runtimeOverrides.emplace_back(qdmi::DeviceDefinition{ + .id = "mqt.sc.test", .library = SC_DEVICE_LIBRARY, .prefix = "MQT_SC"}); + + auto device = qdmi::DeviceManager(options).open("mqt.sc.test"); + EXPECT_EQ(device.getName(), "MQT SC Default QDMI Device"); + EXPECT_FALSE(device.getSites().empty()); +} + +TEST(DeviceManager, OpenAllIsolatesFailures) { + qdmi::ConfigOptions options; + options.isolated = true; + options.runtimeOverrides.emplace_back(qdmi::DeviceDefinition{ + .id = "good", .library = SC_DEVICE_LIBRARY, .prefix = "MQT_SC"}); + options.runtimeOverrides.emplace_back(qdmi::DeviceDefinition{ + .id = "bad", .library = "does-not-exist", .prefix = "MISSING"}); + + qdmi::DeviceManager manager(options); + auto result = manager.openAll(); + EXPECT_TRUE(result.devices.contains("good")); + EXPECT_TRUE(result.errors.contains("bad")); +} +} // namespace From fd50c53cec48f180c663d858b3695eae9f638337 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Wed, 15 Jul 2026 01:03:50 +0200 Subject: [PATCH 02/16] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Complete=20QDMI=20ob?= =?UTF-8?q?ject=20model=20redesign?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assisted-by: GPT-5 via Codex --- .github/workflows/ci.yml | 1 + .pre-commit-config.yaml | 3 + CHANGELOG.md | 15 +- UPGRADING.md | 119 +- bindings/qdmi/qdmi.cpp | 75 +- cmake/ExternalDependencies.cmake | 14 - docs/qdmi/{driver.md => device_management.md} | 19 +- docs/qdmi/index.md | 4 +- docs/qdmi/na_device.md | 6 +- docs/qdmi/qdmi_backend.md | 17 +- include/mqt-core/qdmi/Device.hpp | 1092 +- include/mqt-core/qdmi/DeviceManager.hpp | 10 - include/mqt-core/qdmi/common/Common.hpp | 54 +- include/mqt-core/qdmi/driver/Driver.hpp | 443 - python/mqt/core/plugins/qiskit/provider.py | 24 +- python/mqt/core/qdmi.pyi | 8 +- src/qdmi/CMakeLists.txt | 10 +- src/qdmi/Device.cpp | 1305 +- src/qdmi/DeviceApi.hpp | 138 + src/qdmi/DeviceManager.cpp | 72 +- src/qdmi/V1DeviceApi.cpp | 351 + src/qdmi/driver/CMakeLists.txt | 31 - src/qdmi/driver/Driver.cpp | 654 - test/python/na/test_na_qdmi.py | 6 +- test/python/plugins/qiskit/test_backend.py | 10 +- test/python/plugins/qiskit/test_estimator.py | 5 +- .../plugins/qiskit/test_mock_backend.py | 12 +- test/python/plugins/qiskit/test_provider.py | 22 +- test/python/plugins/qiskit/test_sampler.py | 5 +- test/python/qdmi/test_qdmi.py | 6 +- test/qdmi/device/CMakeLists.txt | 4 +- test/qdmi/device/test_device.cpp | 224 +- test/qdmi/device/test_device_api.cpp | 238 + test/qdmi/driver/CMakeLists.txt | 38 - test/qdmi/driver/test_driver.cpp | 972 - test/qdmi/manager/test_device_manager.cpp | 184 +- vendor/tomlplusplus/README.md | 10 + vendor/tomlplusplus/toml.hpp | 17899 ++++++++++++++++ 38 files changed, 20050 insertions(+), 4050 deletions(-) rename docs/qdmi/{driver.md => device_management.md} (58%) delete mode 100644 include/mqt-core/qdmi/driver/Driver.hpp create mode 100644 src/qdmi/DeviceApi.hpp create mode 100644 src/qdmi/V1DeviceApi.cpp delete mode 100644 src/qdmi/driver/CMakeLists.txt delete mode 100644 src/qdmi/driver/Driver.cpp create mode 100644 test/qdmi/device/test_device_api.cpp delete mode 100644 test/qdmi/driver/CMakeLists.txt delete mode 100644 test/qdmi/driver/test_driver.cpp create mode 100644 vendor/tomlplusplus/README.md create mode 100644 vendor/tomlplusplus/toml.hpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 501a6c00d6..5b3716a3ea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -165,6 +165,7 @@ jobs: setup-python: true install-pkgs: "nanobind==2.13.0" cpp-linter-extra-args: "-std=c++20" + cpp-linter-ignore-extra: "vendor/**" setup-mlir: true llvm-version: 22.1.7 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 240aa80111..7ad5a5b33f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -74,6 +74,7 @@ repos: rev: v1.48.0 hooks: - id: typos + exclude: ^vendor/ priority: 3 ## Check license headers @@ -81,6 +82,7 @@ repos: rev: v2.9.0 hooks: - id: license-tools + exclude: ^vendor/ priority: 4 ## Format BibTeX files with bibtex-tidy @@ -136,6 +138,7 @@ repos: hooks: - id: clang-format types_or: [c++, c, cuda] + exclude: ^vendor/ priority: 5 - id: clang-format name: clang-format (TableGen) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f5c28b486..82a16b521e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ releases may include breaking changes. - ✨ Add versioned JSON/TOML QDMI configuration discovery, relocatable built-in manifests, and lazy `qdmi::DeviceManager`/`mqt.core.qdmi` APIs with per-device sessions and failure isolation. -- ✨ Add support for QDMI child devices to the driver and QDMI libraries +- ✨ Add support for QDMI child devices to the QDMI object model and libraries ([#1897]) ([**@burgholzer**]) - ✨ Add typed custom property and result queries to the C++ and Python QDMI libraries ([#1895]) ([**@burgholzer**]) @@ -70,8 +70,13 @@ releases may include breaking changes. - ♻️ Replace the legacy 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 `openAll` isolates failures - by stable device ID. + objects retain their required runtime state, and definitions can be inspected + without executing provider code. +- ♻️ Adapt QDMI v1.3 provider libraries through a private function table. The + public C++ and Python object model no longer exposes QDMI v1 client handles or + depends on process-global client state. +- 📦 Vendor the current toml++ single-header distribution used to parse project + configuration, including its embedded upstream license and pinned provenance. - ♻️ Migrate the Qiskit provider and neutral-atom adapter to lazily opened configured QDMI devices and the unified `mqt.core.qdmi` API. - ⬆️ Raise the minimum supported QDMI version to 1.3.2 ([#1897]) @@ -85,6 +90,10 @@ releases may include breaking changes. - 🔥 Remove the legacy device-management namespace, Python module, global session API, source/include/test trees, and compatibility CMake targets. +- 🔥 Remove the QDMI client-interface implementation, the `Driver` singleton, + `addDynamicDeviceLibrary`, and the public `openAll`/`open_all` convenience. + Devices are opened explicitly by stable ID and failures are handled at each + call site. - 🔥 Remove central built-in device enumeration and compiled-in library names; generated relocatable manifest fragments now register built-in devices. - 🔥 Remove the density matrix support from the MQT Core DD package ([#1466]) diff --git a/UPGRADING.md b/UPGRADING.md index 5a76142908..21a48af583 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -8,31 +8,85 @@ of changes including minor and patch releases, please refer to the ### QDMI device management has been redesigned -The legacy device-management namespace, Python module, global session object, -and compatibility CMake target have been removed. The public API is now -consistently named `qdmi`: +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 provider library is loaded and a session is +initialized only when its stable device ID is passed to `open`. The QDMI v1.3 +ABI is now an implementation detail behind a private function-table adapter; +public MQT objects no longer expose QDMI handles. | Concern | C++ | Python | | --- | --- | --- | | Discover registrations | `qdmi::DeviceRegistry` | `qdmi.DeviceManager.definitions` | | Open one device | `qdmi::DeviceManager::open` | `DeviceManager.open` | -| Open every device independently | `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. Replace imports from the -removed device-management module with `mqt.core.qdmi`, includes with -`qdmi/Device.hpp` or `qdmi/DeviceManager.hpp`, and the removed CMake target with -`MQT::CoreQDMI`. +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; and +- `DeviceManager.openAll()` / `DeviceManager.open_all()`. + +Replace old includes with `qdmi/Device.hpp` or `qdmi/DeviceManager.hpp`, and +link `MQT::CoreQDMI`. + +#### 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.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" +auto& driver = qdmi::Driver::get(); +const auto device = fomac::Device(driver.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 previously created a global session and enumerated its devices should -create a manager and open stable device IDs. Discovery parses configuration but -does not load native libraries; loading and session initialization happen only -when `open` or `open_all` is called. +Code that needs every configured device should iterate definitions and decide +how to handle each failure. This keeps discovery usable even when a provider is +temporarily unavailable and avoids an arbitrary bulk-open policy. Python: @@ -42,12 +96,13 @@ from mqt.core import qdmi manager = qdmi.DeviceManager() print([definition.id for definition in manager.definitions]) -device = manager.open("mqt.ddsim.default") -print(device.name()) - -devices, errors = manager.open_all() -for device_id, error in errors.items(): - print(f"{device_id} could not be opened: {error}") +for definition in manager.definitions: + try: + device = manager.open(definition.id) + except RuntimeError as error: + print(f"{definition.id} could not be opened: {error}") + continue + print(device.name()) ``` C++: @@ -56,18 +111,15 @@ C++: #include "qdmi/DeviceManager.hpp" qdmi::DeviceManager manager; -const auto device = manager.open("mqt.ddsim.default"); - -const auto opened = manager.openAll(); -for (const auto& [id, error] : opened.errors) { - // Handle one failed provider without losing successfully opened devices. +for (const auto& definition : manager.definitions()) { + try { + auto device = manager.open(definition.id); + } catch (const std::exception& error) { + // Record this provider failure and continue with other definitions. + } } ``` -`openAll` isolates failures by device ID. This differs from the removed global -session, where one library or initialization failure could affect discovery as a -whole. - #### Per-device session parameters Authentication and provider settings no longer belong to a process-wide session. @@ -78,7 +130,7 @@ from mqt.core import qdmi parameters = qdmi.SessionParameters() parameters.token = obtain_token() -parameters.project_id = "research-project" +parameters.custom1 = "provider-specific-value" device = qdmi.DeviceManager().open( "vendor.qpu.production", @@ -89,11 +141,16 @@ device = qdmi.DeviceManager().open( ```cpp qdmi::SessionParameters parameters; parameters.token = obtainToken(); -parameters.projectId = "research-project"; +parameters.custom1 = "provider-specific-value"; auto device = manager.open("vendor.qpu.production", parameters); ``` +QDMI v1 has no standard project-ID session parameter. Provider-specific project +or organization identifiers must use the custom slot documented by that +provider. 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 @@ -145,7 +202,7 @@ A dedicated `mqt-core.json` wins over `pyproject.toml` in the same directory. `MQT_CORE_QDMI_CONFIG_FILE` or `ConfigOptions.explicitFile` replaces the system, user, and project layers while retaining packaged devices. Set `isolated` to exclude packaged manifests as well. See the -[configuration reference](docs/qdmi/configuration.md) for complete schemas and +{doc}`configuration reference ` for complete schemas and administrator, project, environment, and runtime examples. Static C++ executables must set `ConfigOptions.configRoot`: unlike a shared @@ -170,7 +227,7 @@ 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 inject runtime -definitions or mock `DeviceManager.open_all`. +definitions or mock `DeviceManager.definitions` and `DeviceManager.open`. ### MLIR enabled by default for C++ and Python package builds diff --git a/bindings/qdmi/qdmi.cpp b/bindings/qdmi/qdmi.cpp index 3ef5798871..f99c551754 100644 --- a/bindings/qdmi/qdmi.cpp +++ b/bindings/qdmi/qdmi.cpp @@ -21,7 +21,6 @@ #include // NOLINT(misc-include-cleaner) #include // NOLINT(misc-include-cleaner) #include // NOLINT(misc-include-cleaner) -#include #include #include @@ -82,7 +81,6 @@ NB_MODULE(MQT_CORE_MODULE_NAME, m) { .def_rw("auth_url", &qdmi::SessionParameters::authUrl) .def_rw("username", &qdmi::SessionParameters::username) .def_rw("password", &qdmi::SessionParameters::password) - .def_rw("project_id", &qdmi::SessionParameters::projectId) .def_rw("custom1", &qdmi::SessionParameters::custom1) .def_rw("custom2", &qdmi::SessionParameters::custom2) .def_rw("custom3", &qdmi::SessionParameters::custom3) @@ -235,32 +233,33 @@ when the custom slot is unsupported.)pb"); 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); + nb::enum_(job, "Status", "Enumeration of job status.") + .value("CREATED", qdmi::JobStatus::Created) + .value("SUBMITTED", qdmi::JobStatus::Submitted) + .value("QUEUED", qdmi::JobStatus::Queued) + .value("RUNNING", qdmi::JobStatus::Running) + .value("DONE", qdmi::JobStatus::Done) + .value("CANCELED", qdmi::JobStatus::Canceled) + .value("FAILED", qdmi::JobStatus::Failed); // ProgramFormat enum - nb::enum_(m, "ProgramFormat", + 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("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); + .value("QASM2", qdmi::ProgramFormat::Qasm2) + .value("QASM3", qdmi::ProgramFormat::Qasm3) + .value("QIR_BASE_STRING", qdmi::ProgramFormat::QirBaseString) + .value("QIR_BASE_MODULE", qdmi::ProgramFormat::QirBaseModule) + .value("QIR_ADAPTIVE_STRING", qdmi::ProgramFormat::QirAdaptiveString) + .value("QIR_ADAPTIVE_MODULE", qdmi::ProgramFormat::QirAdaptiveModule) + .value("CALIBRATION", qdmi::ProgramFormat::Calibration) + .value("QPY", qdmi::ProgramFormat::Qpy) + .value("IQM_JSON", qdmi::ProgramFormat::IqmJson) + .value("BATCH_JOB", qdmi::ProgramFormat::BatchJob) + .value("CUSTOM1", qdmi::ProgramFormat::Custom1) + .value("CUSTOM2", qdmi::ProgramFormat::Custom2) + .value("CUSTOM3", qdmi::ProgramFormat::Custom3) + .value("CUSTOM4", qdmi::ProgramFormat::Custom4) + .value("CUSTOM5", qdmi::ProgramFormat::Custom5); nb::enum_( m, "CustomProperty", @@ -277,14 +276,14 @@ when the custom slot is unsupported.)pb"); "A device represents a quantum device with its properties and " "capabilities."); - nb::enum_(device, "Status", + 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); + .value("OFFLINE", qdmi::DeviceStatus::Offline) + .value("IDLE", qdmi::DeviceStatus::Idle) + .value("BUSY", qdmi::DeviceStatus::Busy) + .value("ERROR", qdmi::DeviceStatus::Error) + .value("MAINTENANCE", qdmi::DeviceStatus::Maintenance) + .value("CALIBRATION", qdmi::DeviceStatus::Calibration); device.def("name", &qdmi::Device::getName, "Returns the name of the device."); @@ -572,17 +571,7 @@ when the custom slot is unsupported.)pb"); return self.open(id, sessionOverrides); }, "id"_a, nb::kw_only(), - "session_overrides"_a = qdmi::SessionParameters{}) - .def( - "open_all", - [](qdmi::DeviceManager& self, - const qdmi::SessionParameters& sessionOverrides) { - auto result = self.openAll(sessionOverrides); - return nb::make_tuple(std::move(result.devices), - std::move(result.errors)); - }, - nb::kw_only(), "session_overrides"_a = qdmi::SessionParameters{}, - "Open all definitions independently and return (devices, errors)."); + "session_overrides"_a = qdmi::SessionParameters{}); } } // namespace mqt diff --git a/cmake/ExternalDependencies.cmake b/cmake/ExternalDependencies.cmake index b7cf7f107e..b361a7e9ff 100644 --- a/cmake/ExternalDependencies.cmake +++ b/cmake/ExternalDependencies.cmake @@ -47,20 +47,6 @@ set(JSON_Install FetchContent_Declare(nlohmann_json URL ${JSON_URL} FIND_PACKAGE_ARGS ${JSON_VERSION}) list(APPEND FETCH_PACKAGES nlohmann_json) -set(TOMLPLUSPLUS_VERSION - 3.4.0 - CACHE STRING "toml++ version") -set(TOMLPLUSPLUS_URL - https://github.com/marzer/tomlplusplus/archive/refs/tags/v${TOMLPLUSPLUS_VERSION}.tar.gz) -set(TOMLPLUSPLUS_BUILD_TESTS - OFF - CACHE BOOL "Disable toml++ tests" FORCE) -set(TOMLPLUSPLUS_BUILD_EXAMPLES - OFF - CACHE BOOL "Disable toml++ examples" FORCE) -FetchContent_Declare(tomlplusplus URL ${TOMLPLUSPLUS_URL} FIND_PACKAGE_ARGS ${TOMLPLUSPLUS_VERSION}) -list(APPEND FETCH_PACKAGES tomlplusplus) - option(USE_SYSTEM_BOOST "Whether to try to use the system Boost installation" OFF) set(BOOST_MIN_VERSION 1.80.0 diff --git a/docs/qdmi/driver.md b/docs/qdmi/device_management.md similarity index 58% rename from docs/qdmi/driver.md rename to docs/qdmi/device_management.md index 52882b0780..e138b3d758 100644 --- a/docs/qdmi/driver.md +++ b/docs/qdmi/device_management.md @@ -11,7 +11,12 @@ the underlying library/session lifetime. qdmi::DeviceManager manager; for (const auto& definition : manager.definitions()) { - auto device = manager.open(definition.id); + try { + auto device = manager.open(definition.id); + // Use this device independently of the manager. + } catch (const std::exception& error) { + // An unavailable provider does not affect other definitions. + } } ``` @@ -22,13 +27,17 @@ from mqt.core.qdmi import DeviceManager manager = DeviceManager() for definition in manager.definitions: - device = manager.open(definition.id) + try: + device = manager.open(definition.id) + except RuntimeError as error: + print(f"{definition.id}: {error}") + continue print(device.name()) ``` -Opening one device does not open any other definition. `open_all()` returns a -pair of successful devices and per-ID error messages so one unavailable provider -does not hide the remaining devices. +Discovery is side-effect free. Each `open` call loads and initializes only the +selected definition, and separate definitions can share one loaded provider +library while retaining independent sessions. See [QDMI configuration](configuration.md) for discovery, precedence, and registration examples. diff --git a/docs/qdmi/index.md b/docs/qdmi/index.md index c1d831f01d..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 [QDMI device management](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 [QDMI device management](driver.md), a NA QDMI Device DDSIM QDMI Device -QDMI Device Management +QDMI Device Management QDMI Configuration QDMI-Qiskit Backend ``` diff --git a/docs/qdmi/na_device.md b/docs/qdmi/na_device.md index 18b5f4b30d..9b2cadb58c 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 provider 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 64eacc9ade..fc60ed64b3 100644 --- a/docs/qdmi/qdmi_backend.md +++ b/docs/qdmi/qdmi_backend.md @@ -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 +### Provider-Specific Session Parameters -Associate your session with a specific project or organization: +QDMI v1 reserves five custom session slots for provider-specific settings. +Consult the provider 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="provider-project") ``` ### Combining Authentication Parameters @@ -196,7 +196,7 @@ provider = QDMIProvider( token="your_api_token", username="your_username", password="your_password", - project_id="your_project_id", + custom1="provider-specific-value", auth_url="https://custom-auth.example.com", ) ``` @@ -216,9 +216,8 @@ except RuntimeError as e: ## Device Capabilities and Target -The backend automatically introspects the QDMI (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,7 +305,7 @@ job = backend.run(circuits, parameter_values=param_values, shots=100) ### Job Status -The {py:class}`~mqt.core.plugins.qiskit.job.QDMIJob` wraps a QDMI (QDMI) job and +The {py:class}`~mqt.core.plugins.qiskit.job.QDMIJob` wraps a QDMI job and provides status tracking: ```python diff --git a/include/mqt-core/qdmi/Device.hpp b/include/mqt-core/qdmi/Device.hpp index 30049c1378..1b7bf8931b 100644 --- a/include/mqt-core/qdmi/Device.hpp +++ b/include/mqt-core/qdmi/Device.hpp @@ -9,43 +9,76 @@ */ /** @file Device.hpp - * @brief QDMI C++ device-management interface. + * @brief ABI-neutral C++ interface for QDMI devices. */ #pragma once -#include "qdmi/common/Common.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 namespace qdmi { +namespace detail { +struct DeviceState; +struct JobState; +struct DeviceFactory; +} // namespace detail + 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. - */ +/** Device availability state independent of the provider ABI. */ +enum class DeviceStatus : std::uint8_t { + Offline, + Idle, + Busy, + Error, + Maintenance, + Calibration, +}; + +/** Lifecycle state of a submitted job. */ +enum class JobStatus : std::uint8_t { + Created, + Submitted, + Queued, + Running, + Done, + Canceled, + Failed, +}; + +/** Program formats understood by the QDMI v1 adapter. */ +enum class ProgramFormat : std::uint32_t { + Qasm2 = 0, + Qasm3 = 1, + QirBaseString = 2, + QirBaseModule = 3, + QirAdaptiveString = 4, + QirAdaptiveModule = 5, + Calibration = 6, + Qpy = 7, + IqmJson = 8, + BatchJob = 9, + Custom1 = 999999995, + Custom2 = 999999996, + Custom3 = 999999997, + Custom4 = 999999998, + Custom5 = 999999999, +}; + +/** Identifies an implementation-defined custom property or result slot. */ enum class CustomProperty : std::uint8_t { Custom1 = 1, Custom2 = 2, @@ -54,11 +87,6 @@ enum class CustomProperty : std::uint8_t { 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 || @@ -66,929 +94,265 @@ concept custom_property_value = 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) { +template +[[nodiscard]] auto +decodeCustomValue(const std::optional>& bytes, + const std::string& description) -> std::optional { + if (!bytes) { 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; + 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) + + 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); + 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())); + if (bytes->size() != sizeof(T)) { + throw std::invalid_argument("Cannot decode " + description + + ": unexpected byte size"); } T value{}; - std::memcpy(&value, bytes.data(), sizeof(T)); + 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>; - class Job; class Site; -class Device; class Operation; class DeviceManager; -/** - * @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 a DeviceManager. - * - * @see QDMI_Device - */ +/** One initialized quantum-device session. */ class Device { public: - // NOLINTNEXTLINE(google-explicit-constructor, *-explicit-conversions) - operator QDMI_Device() const { return device_; } + [[nodiscard]] auto getName() const -> std::string; + [[nodiscard]] auto getVersion() const -> std::string; + [[nodiscard]] auto getStatus() const -> DeviceStatus; + [[nodiscard]] auto getLibraryVersion() const -> std::string; + [[nodiscard]] auto getQubitsNum() const -> size_t; + [[nodiscard]] auto getSites() const -> std::vector; + [[nodiscard]] auto getRegularSites() const -> std::vector; + [[nodiscard]] auto getZones() const -> std::vector; + [[nodiscard]] auto getOperations() const -> std::vector; + [[nodiscard]] auto getCouplingMap() const + -> std::optional>>; + [[nodiscard]] auto getNeedsCalibration() const -> std::optional; + [[nodiscard]] auto getLengthUnit() const -> std::optional; + [[nodiscard]] auto getLengthScaleFactor() const -> std::optional; + [[nodiscard]] auto getDurationUnit() const -> std::optional; + [[nodiscard]] auto getDurationScaleFactor() const -> std::optional; + [[nodiscard]] auto getMinAtomDistance() const -> std::optional; + [[nodiscard]] auto getSupportedProgramFormats() const + -> std::vector; + [[nodiscard]] auto getChildDevices() const -> std::vector; - /// @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_, qdmiProperty, size, - value, sizeRet); - }, - "custom device property " + - std::to_string(static_cast(property))); + [[nodiscard]] auto queryCustomProperty(const CustomProperty property) const + -> std::optional { + return detail::decodeCustomValue(queryCustomPropertyBytes(property), + "custom device property"); } - /// @see QDMI_job_submit - [[nodiscard]] Job submitJob( - const std::string& program, QDMI_Program_Format format, size_t numShots, + [[nodiscard]] auto submitJob( + const std::string& program, ProgramFormat 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; + const std::optional& custom5 = std::nullopt) const + -> Job; - auto operator<=>(const Device& other) const noexcept { - return device_ <=> other.device_; + [[nodiscard]] auto operator<=>(const Device& other) const noexcept { + return state_.get() <=> other.state_.get(); } - - bool operator==(const Device& other) const noexcept { - return device_ == other.device_; + [[nodiscard]] auto operator==(const Device& other) const noexcept -> bool { + return state_ == other.state_; } private: - /** - * @brief Constructs a Device object from a QDMI_Device handle. - * @param device The QDMI_Device handle to wrap. - */ - explicit Device(QDMI_Device device, std::shared_ptr lifetime = {}) - : device_(device), lifetime_(std::move(lifetime)) {} - - /// 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_, 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_, 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_, 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_, 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_, 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. - QDMI_Device device_; - - /// Shared ownership of the library and initialized device session. - std::shared_ptr lifetime_; - + explicit Device(std::shared_ptr state) + : state_(std::move(state)) {} + [[nodiscard]] auto queryCustomPropertyBytes(CustomProperty property) const + -> std::optional>; + std::shared_ptr state_; friend class DeviceManager; + friend struct detail::DeviceFactory; }; -/** - * @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 - */ +/** A submitted job retaining its device session. */ class Job { public: - // 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 + [[nodiscard]] auto check() const -> JobStatus; + [[nodiscard]] auto wait(size_t timeout = 0) const -> bool; void cancel() const; + [[nodiscard]] auto getId() const -> std::string; + [[nodiscard]] auto getProgramFormat() const -> ProgramFormat; + [[nodiscard]] auto getProgram() const -> std::string; + [[nodiscard]] auto getNumShots() const -> size_t; - /// Get the job ID - [[nodiscard]] std::string getId() const; - - /// Get the program format - [[nodiscard]] QDMI_Program_Format getProgramFormat() const; - - /// Get the program to be executed - [[nodiscard]] std::string getProgram() 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))); + [[nodiscard]] auto queryCustomProperty(const CustomProperty property) const + -> std::optional { + return detail::decodeCustomValue(queryCustomPropertyBytes(property), + "custom job 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))); + [[nodiscard]] auto getCustomResult(const CustomProperty property) const + -> std::optional { + return detail::decodeCustomValue(getCustomResultBytes(property), + "custom job result"); } - /** - * @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; + [[nodiscard]] auto getShots() const -> std::vector; + [[nodiscard]] auto getCounts() const -> std::map; + [[nodiscard]] auto getDenseStateVector() const + -> std::vector>; + [[nodiscard]] auto getDenseProbabilities() const -> std::vector; + [[nodiscard]] auto getSparseStateVector() const + -> std::map>; + [[nodiscard]] auto getSparseProbabilities() const + -> std::map; + + [[nodiscard]] auto operator<=>(const Job& other) const noexcept { + return state_.get() <=> other.state_.get(); + } + [[nodiscard]] auto operator==(const Job& other) const noexcept -> bool { + return state_ == other.state_; + } private: - /** - * @brief Constructs a Job object from a QDMI_Job handle. - * @param job The QDMI_Job handle to wrap. - */ - explicit Job(QDMI_Job job, std::shared_ptr lifetime = {}) - : lifetime_(std::move(lifetime)), job_(job, QDMI_job_free) {} - - /// Shared ownership of the device session used by this job. - std::shared_ptr lifetime_; - - std::unique_ptr job_{ - nullptr, QDMI_job_free}; - + explicit Job(std::shared_ptr state) + : state_(std::move(state)) {} + [[nodiscard]] auto queryCustomPropertyBytes(CustomProperty property) const + -> std::optional>; + [[nodiscard]] auto getCustomResultBytes(CustomProperty property) const + -> std::optional>; + std::shared_ptr state_; 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 - */ +/** A physical site or zone belonging to a device. */ 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; + [[nodiscard]] auto getIndex() const -> size_t; + [[nodiscard]] auto getT1() const -> std::optional; + [[nodiscard]] auto getT2() const -> std::optional; + [[nodiscard]] auto getName() const -> std::optional; + [[nodiscard]] auto getXCoordinate() const -> std::optional; + [[nodiscard]] auto getYCoordinate() const -> std::optional; + [[nodiscard]] auto getZCoordinate() const -> std::optional; + [[nodiscard]] auto isZone() const -> bool; + [[nodiscard]] auto getXExtent() const -> std::optional; + [[nodiscard]] auto getYExtent() const -> std::optional; + [[nodiscard]] auto getZExtent() const -> std::optional; + [[nodiscard]] auto getModuleIndex() const -> std::optional; + [[nodiscard]] auto getSubmoduleIndex() const -> std::optional; - /// @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_, site_, qdmiProperty, - size, value, sizeRet); - }, - "custom site property " + - std::to_string(static_cast(property))); + [[nodiscard]] auto queryCustomProperty(const CustomProperty property) const + -> std::optional { + return detail::decodeCustomValue(queryCustomPropertyBytes(property), + "custom site property"); } - auto operator<=>(const Site&) const noexcept = default; - -private: - /** - * @brief Constructs a Site object from a QDMI_Site handle. - * @param device The device that owns the site. - * @param site The QDMI_Site handle to wrap. - */ - Site(QDMI_Device device, QDMI_Site site, std::shared_ptr lifetime = {}) - : device_(device), site_(site), lifetime_(std::move(lifetime)) {} - - /// 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_, 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_, 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_, 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; + [[nodiscard]] auto operator<=>(const Site& other) const noexcept { + if (const auto device = state_.get() <=> other.state_.get(); device != 0) { + return device; } + return handle_ <=> other.handle_; + } + [[nodiscard]] auto operator==(const Site& other) const noexcept -> bool { + return state_ == other.state_ && handle_ == other.handle_; } - /// @brief A pointer to the device that owns the site. - QDMI_Device device_; - - /// @brief The underlying QDMI_Site object. - QDMI_Site site_; - - std::shared_ptr lifetime_; - +private: + Site(std::shared_ptr state, void* handle) + : state_(std::move(state)), handle_(handle) {} + [[nodiscard]] auto queryCustomPropertyBytes(CustomProperty property) const + -> std::optional>; + std::shared_ptr state_; + void* handle_ = nullptr; 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 - */ +/** An operation supported by a device. */ 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 + [[nodiscard]] auto getName(const std::vector& sites = {}, + const std::vector& params = {}) const + -> std::string; + [[nodiscard]] auto getQubitsNum(const std::vector& sites = {}, + const std::vector& params = {}) const + -> std::optional; + [[nodiscard]] auto 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 + const std::vector& params = {}) const -> size_t; + [[nodiscard]] auto getDuration(const std::vector& sites = {}, + const std::vector& params = {}) const + -> std::optional; + [[nodiscard]] auto getFidelity(const std::vector& sites = {}, + const std::vector& params = {}) const + -> std::optional; + [[nodiscard]] auto getInteractionRadius(const std::vector& sites = {}, - const std::vector& params = {}) const; - - /// @see QDMI_OPERATION_PROPERTY_BLOCKINGRADIUS - [[nodiscard]] std::optional + const std::vector& params = {}) const + -> std::optional; + [[nodiscard]] auto getBlockingRadius(const std::vector& sites = {}, - const std::vector& params = {}) const; - - /// @see QDMI_OPERATION_PROPERTY_IDLINGFIDELITY - [[nodiscard]] std::optional + const std::vector& params = {}) const + -> std::optional; + [[nodiscard]] auto 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 + const std::vector& params = {}) const + -> std::optional; + [[nodiscard]] auto isZoned() const -> bool; + [[nodiscard]] auto getSites() const -> std::optional>; + [[nodiscard]] auto getSitePairs() const + -> std::optional>>; + [[nodiscard]] auto getMeanShuttlingSpeed(const std::vector& sites = {}, - const std::vector& params = {}) const; + const std::vector& params = {}) const + -> std::optional; - /** - * @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 + [[nodiscard]] auto 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_, 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 device that owns the site. - * @param operation The QDMI_Operation handle to wrap. - */ - Operation(QDMI_Device device, QDMI_Operation operation, - std::shared_ptr lifetime = {}) - : device_(device), operation_(operation), lifetime_(std::move(lifetime)) { + const std::vector& params = {}) const + -> std::optional { + return detail::decodeCustomValue( + queryCustomPropertyBytes(property, sites, params), + "custom operation property"); } - /// 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_, 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_, 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_, 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_, 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_, 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; + [[nodiscard]] auto operator<=>(const Operation& other) const noexcept { + if (const auto device = state_.get() <=> other.state_.get(); device != 0) { + return device; } + return handle_ <=> other.handle_; + } + [[nodiscard]] auto operator==(const Operation& other) const noexcept -> bool { + return state_ == other.state_ && handle_ == other.handle_; } - /// @brief A pointer to the device that owns the operation. - QDMI_Device device_; - - /// @brief The underlying QDMI_Operation object. - QDMI_Operation operation_; - - std::shared_ptr lifetime_; - +private: + [[nodiscard]] static auto siteHandles(const std::vector& sites) + -> std::vector; + Operation(std::shared_ptr state, void* handle) + : state_(std::move(state)), handle_(handle) {} + [[nodiscard]] auto + queryCustomPropertyBytes(CustomProperty property, + const std::vector& sites, + const std::vector& params) const + -> std::optional>; + 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 index 2dc232b745..4d2b7c5088 100644 --- a/include/mqt-core/qdmi/DeviceManager.hpp +++ b/include/mqt-core/qdmi/DeviceManager.hpp @@ -19,7 +19,6 @@ #include #include -#include #include #include #include @@ -36,7 +35,6 @@ struct SessionParameters { std::optional authUrl; std::optional username; std::optional password; - std::optional projectId; std::optional custom1; std::optional custom2; std::optional custom3; @@ -85,12 +83,6 @@ class DeviceRegistry { std::vector definitions_; }; -/** Result of independently opening every enabled definition. */ -struct OpenAllResult { - std::map devices; - std::map errors; -}; - /** Lazily opens configured QDMI devices and shares loaded v1 libraries. */ class DeviceManager { public: @@ -109,8 +101,6 @@ class DeviceManager { [[nodiscard]] Device open(std::string_view id, const SessionParameters& sessionOverrides = SessionParameters{}); - [[nodiscard]] OpenAllResult - openAll(const SessionParameters& sessionOverrides = SessionParameters{}); private: struct Impl; 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 27c7f864bf..0000000000 --- a/include/mqt-core/qdmi/driver/Driver.hpp +++ /dev/null @@ -1,443 +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 - -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 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 A pointer to the list of all devices. - const std::vector>* devices_; - -public: - /// @brief Constructor for the QDMI session. - explicit QDMI_Session_impl_d( - const std::vector>& devices) - : devices_(&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 loads all statically known and linked QDMI device - * libraries. Additional devices can be added dynamically. - * @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; - - /// @brief Private constructor to enforce the singleton pattern. - Driver(); - - /** - * @brief Vector of unique pointers to QDMI_Device_impl_d objects. - */ - std::vector> devices_; - - /** - * @brief Map of sessions to their corresponding unique pointers to - * QDMI_Session_impl_d objects. - */ - std::unordered_map> - sessions_; - -public: - /** - * @brief Loads a dynamic device library and adds it to the driver. - * - * @param libName The path to the dynamic library to load. - * @param prefix The prefix used for the device interface functions in the - * library. - * @param config Configuration for device session parameters. - * - * @return A pointer to the newly created device. - * - * @throws std::runtime_error If the device cannot be initialized. - * @throws std::bad_alloc If memory allocation fails during the process. - */ - auto addDynamicDeviceLibrary(const std::string& libName, - const std::string& prefix, - const DeviceSessionConfig& config = {}) - -> 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/python/mqt/core/plugins/qiskit/provider.py b/python/mqt/core/plugins/qiskit/provider.py index aa297904c5..a59ff27972 100644 --- a/python/mqt/core/plugins/qiskit/provider.py +++ b/python/mqt/core/plugins/qiskit/provider.py @@ -24,6 +24,20 @@ def __dir__() -> list[str]: return __all__ +def _open_device( + manager: qdmi.DeviceManager, definition: qdmi.DeviceDefinition, parameters: qdmi.SessionParameters +) -> qdmi.Device | None: + """Open one configured device, ignoring an unavailable provider. + + Returns: + The opened device, or ``None`` if its provider is unavailable. + """ + try: + return manager.open(definition.id, session_overrides=parameters) + except RuntimeError: + return None + + class QDMIProvider: """Provider for devices discovered by the QDMI device manager. @@ -58,7 +72,6 @@ def __init__( 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,7 +82,6 @@ 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 provider-specific session parameters. Raises: @@ -81,7 +93,6 @@ def __init__( parameters.auth_url = auth_url parameters.username = username parameters.password = password - parameters.project_id = project_id for key, value in session_kwargs.items(): if not hasattr(parameters, key): msg = f"Unknown QDMI session parameter: {key}" @@ -89,8 +100,11 @@ def __init__( setattr(parameters, key, value) self._manager = qdmi.DeviceManager() - devices_by_id, _errors = self._manager.open_all(session_overrides=parameters) - devices = devices_by_id.values() + devices = [ + device + for definition in self._manager.definitions + if (device := _open_device(self._manager, definition, parameters)) is not None + ] 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]: diff --git a/python/mqt/core/qdmi.pyi b/python/mqt/core/qdmi.pyi index 41b700ab22..af4baf0aca 100644 --- a/python/mqt/core/qdmi.pyi +++ b/python/mqt/core/qdmi.pyi @@ -41,10 +41,6 @@ class SessionParameters: @password.setter def password(self, arg: str | None) -> None: ... @property - def project_id(self) -> str | None: ... - @project_id.setter - def project_id(self, arg: str | None) -> None: ... - @property def custom1(self) -> str | None: ... @custom1.setter def custom1(self, arg: str | None) -> None: ... @@ -255,6 +251,8 @@ class ProgramFormat(enum.Enum): IQM_JSON = 8 + BATCH_JOB = 9 + CUSTOM1 = 999999995 CUSTOM2 = 999999996 @@ -560,5 +558,3 @@ class DeviceManager: def register_device(self, definition: DeviceDefinition, *, replace: bool = False) -> None: ... def unregister_device(self, id: str) -> bool: ... def open(self, id: str, *, session_overrides: SessionParameters = ...) -> Device: ... - def open_all(self, *, session_overrides: SessionParameters = ...) -> tuple: - """Open all definitions independently and return (devices, errors).""" diff --git a/src/qdmi/CMakeLists.txt b/src/qdmi/CMakeLists.txt index 71eb29aa80..4dd5f38d9a 100644 --- a/src/qdmi/CMakeLists.txt +++ b/src/qdmi/CMakeLists.txt @@ -8,14 +8,13 @@ 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) + target_sources(${TARGET_NAME} PRIVATE Device.cpp DeviceApi.hpp DeviceManager.cpp V1DeviceApi.cpp) target_sources( ${TARGET_NAME} @@ -29,9 +28,10 @@ if(NOT TARGET ${TARGET_NAME}) target_link_libraries( ${TARGET_NAME} - PUBLIC qdmi::qdmi MQT::CoreQDMICommon nlohmann_json::nlohmann_json - PRIVATE MQT::CoreQDMIDriver spdlog::spdlog ${CMAKE_DL_LIBS}) - target_include_directories(${TARGET_NAME} SYSTEM PRIVATE ${tomlplusplus_SOURCE_DIR}/include) + PUBLIC nlohmann_json::nlohmann_json + PRIVATE qdmi::qdmi 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() diff --git a/src/qdmi/Device.cpp b/src/qdmi/Device.cpp index efa9b7bc5a..96bf6cbc5a 100644 --- a/src/qdmi/Device.cpp +++ b/src/qdmi/Device.cpp @@ -10,677 +10,788 @@ #include "qdmi/Device.hpp" +#include "DeviceApi.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 qdmi { -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); +namespace { +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; } -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); + +template +[[nodiscard]] auto queryValue(Query&& query, const std::string& description) + -> T { + T value{}; + throwIfError(query(sizeof(T), &value, nullptr), "Querying " + description); + return value; } -std::optional> Operation::getSites() const { - const auto& qdmiSites = queryProperty>>( - QDMI_OPERATION_PROPERTY_SITES, {}, {}); - if (!qdmiSites.has_value()) { + +template +[[nodiscard]] auto queryOptionalValue(Query&& query, + const std::string& description) + -> std::optional { + T value{}; + const auto result = query(sizeof(T), &value, nullptr); + if (result == QDMI_ERROR_NOTSUPPORTED) { return std::nullopt; } - std::vector returnedSites; - returnedSites.reserve(qdmiSites->size()); - std::ranges::transform( - *qdmiSites, std::back_inserter(returnedSites), - [device = device_, lifetime = lifetime_](const QDMI_Site& site) -> Site { - return {device, site, lifetime}; - }); - return returnedSites; + throwIfError(result, "Querying " + description); + return value; } -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 + +template +[[nodiscard]] auto queryVector(Query&& query, const std::string& description) + -> std::vector { + const auto bytes = queryBytes(std::forward(query), description); + if (!bytes) { + throw std::runtime_error("Querying " + description + ": Not supported."); + } + if (bytes->size() % sizeof(T) != 0) { + throw std::runtime_error("Invalid byte size while querying " + description); + } + std::vector values(bytes->size() / sizeof(T)); + if (!bytes->empty()) { + std::memcpy(values.data(), bytes->data(), bytes->size()); } + return values; +} - const auto sitesOpt = getSites(); - if (!sitesOpt.has_value()) { +template +[[nodiscard]] auto queryOptionalVector(Query&& query, + const std::string& description) + -> std::optional> { + const auto bytes = queryBytes(std::forward(query), description); + if (!bytes) { 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 + if (bytes->size() % sizeof(T) != 0) { + throw std::runtime_error("Invalid byte size while querying " + description); } - - 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]); + std::vector values(bytes->size() / sizeof(T)); + if (!bytes->empty()) { + std::memcpy(values.data(), bytes->data(), bytes->size()); } - - 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); + return values; } -std::string Device::getVersion() const { - return queryProperty(QDMI_DEVICE_PROPERTY_VERSION); +template +[[nodiscard]] auto queryString(Query&& query, const std::string& description) + -> std::string { + const auto bytes = queryBytes(std::forward(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}; } -QDMI_Device_Status Device::getStatus() const { - return queryProperty(QDMI_DEVICE_PROPERTY_STATUS); +template +[[nodiscard]] auto queryOptionalString(Query&& query, + const std::string& description) + -> std::optional { + const auto bytes = queryBytes(std::forward(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); } -std::string Device::getLibraryVersion() const { - return queryProperty(QDMI_DEVICE_PROPERTY_LIBRARYVERSION); +[[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; } -size_t Device::getQubitsNum() const { - return queryProperty(QDMI_DEVICE_PROPERTY_QUBITSNUM); +[[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; } -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, lifetime_}; - }); - return sites; +[[nodiscard]] constexpr auto deviceCustom(const CustomProperty property) + -> QDMI_Device_Property { + return static_cast(QDMI_DEVICE_PROPERTY_CUSTOM1 + + customOffset(property)); } - -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; +[[nodiscard]] constexpr auto siteCustom(const CustomProperty property) + -> QDMI_Site_Property { + return static_cast(QDMI_SITE_PROPERTY_CUSTOM1 + + customOffset(property)); } - -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; +[[nodiscard]] constexpr auto operationCustom(const CustomProperty property) + -> QDMI_Operation_Property { + return static_cast(QDMI_OPERATION_PROPERTY_CUSTOM1 + + customOffset(property)); } - -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, lifetime_}; - }); - return operations; +[[nodiscard]] constexpr auto jobCustom(const CustomProperty property) + -> QDMI_Device_Job_Property { + return static_cast( + QDMI_DEVICE_JOB_PROPERTY_CUSTOM1 + customOffset(property)); } - -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, lifetime_}, - Site{device_, pair.second, lifetime_}}; - }); - return couplingMap; +[[nodiscard]] constexpr auto resultCustom(const CustomProperty property) + -> QDMI_Job_Result { + return static_cast(QDMI_JOB_RESULT_CUSTOM1 + + customOffset(property)); } -std::optional Device::getNeedsCalibration() const { - return queryProperty>( - QDMI_DEVICE_PROPERTY_NEEDSCALIBRATION); +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->setJobParameter( + state.job, parameter, typed.size() + 1, typed.c_str()); + } else { + return state.device->api->setJobParameter(state.job, parameter, + sizeof(T), &typed); + } + }, + value); + throwIfError(result, "Setting custom parameter"); } +} // namespace -std::optional Device::getLengthUnit() const { - return queryProperty>( - QDMI_DEVICE_PROPERTY_LENGTHUNIT); +auto Device::getName() const -> std::string { + return queryString( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->api->queryDevice( + state_->session, QDMI_DEVICE_PROPERTY_NAME, size, value, sizeRet); + }, + "device name"); +} +auto Device::getVersion() const -> std::string { + return queryString( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->api->queryDevice(state_->session, + QDMI_DEVICE_PROPERTY_VERSION, size, + value, sizeRet); + }, + "device version"); } - -std::optional Device::getLengthScaleFactor() const { - return queryProperty>( - QDMI_DEVICE_PROPERTY_LENGTHSCALEFACTOR); +auto Device::getStatus() const -> DeviceStatus { + const auto status = queryValue( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->api->queryDevice( + state_->session, QDMI_DEVICE_PROPERTY_STATUS, size, value, sizeRet); + }, + "device status"); + return static_cast(status); +} +auto Device::getLibraryVersion() const -> std::string { + return queryString( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->api->queryDevice(state_->session, + QDMI_DEVICE_PROPERTY_LIBRARYVERSION, + size, value, sizeRet); + }, + "device library version"); +} +auto Device::getQubitsNum() const -> size_t { + return queryValue( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->api->queryDevice(state_->session, + QDMI_DEVICE_PROPERTY_QUBITSNUM, size, + value, sizeRet); + }, + "device qubit count"); } - -std::optional Device::getDurationUnit() const { - return queryProperty>( - QDMI_DEVICE_PROPERTY_DURATIONUNIT); +auto Device::getSites() const -> std::vector { + const auto handles = queryVector( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->api->queryDevice( + 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::optional Device::getDurationScaleFactor() const { - return queryProperty>( - QDMI_DEVICE_PROPERTY_DURATIONSCALEFACTOR); +auto Device::getRegularSites() const -> std::vector { + auto sites = getSites(); + std::erase_if(sites, [](const Site& site) { return site.isZone(); }); + return sites; } - -std::optional Device::getMinAtomDistance() const { - return queryProperty>( - QDMI_DEVICE_PROPERTY_MINATOMDISTANCE); +auto Device::getZones() const -> std::vector { + auto sites = getSites(); + std::erase_if(sites, [](const Site& site) { return !site.isZone(); }); + return sites; } - -std::vector Device::getSupportedProgramFormats() const { - return queryProperty>( - QDMI_DEVICE_PROPERTY_SUPPORTEDPROGRAMFORMATS); +auto Device::getOperations() const -> std::vector { + const auto handles = queryVector( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->api->queryDevice(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::vector Device::getChildDevices() const { - size_t size = 0; - auto result = QDMI_device_query_device_property( - device_, 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_, QDMI_DEVICE_PROPERTY_CHILDDEVICES, size, - static_cast(handles.data()), nullptr); - qdmi::throwIfError(result, "Querying child devices"); +auto Device::getCouplingMap() const + -> std::optional>> { + using Pair = std::pair; + const auto pairs = queryOptionalVector( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->api->queryDevice(state_->session, + QDMI_DEVICE_PROPERTY_COUPLINGMAP, size, + value, sizeRet); + }, + "device coupling map"); + if (!pairs) { + return std::nullopt; } - - std::vector devices; - devices.reserve(handles.size()); + std::vector> result; + result.reserve(pairs->size()); std::ranges::transform( - handles, std::back_inserter(devices), - [lifetime = lifetime_](QDMI_Device_impl_d* const handle) { - return Device(handle, lifetime); + *pairs, std::back_inserter(result), [this](const Pair& pair) { + return std::pair{Site(state_, pair.first), Site(state_, pair.second)}; }); - 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 { - QDMI_Job job = nullptr; - qdmi::throwIfError(QDMI_device_create_job(device_, &job), "Creating job"); - Job jobWrapper{job, lifetime_}; - - 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() + 1, program.c_str()), - "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"); - } + 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->queryDevice(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 + +auto Device::getLengthUnit() const -> std::optional { + return queryOptionalString( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->api->queryDevice(state_->session, + QDMI_DEVICE_PROPERTY_LENGTHUNIT, size, + value, sizeRet); }, - 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; + "device length unit"); +} +auto Device::getDurationUnit() const -> std::optional { + return queryOptionalString( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->api->queryDevice(state_->session, + QDMI_DEVICE_PROPERTY_DURATIONUNIT, size, + value, sizeRet); + }, + "device duration unit"); +} +auto Device::getSupportedProgramFormats() const -> std::vector { + const auto formats = queryVector( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->api->queryDevice( + state_->session, QDMI_DEVICE_PROPERTY_SUPPORTEDPROGRAMFORMATS, size, + value, sizeRet); + }, + "supported program formats"); + std::vector result; + result.reserve(formats.size()); + std::ranges::transform(formats, std::back_inserter(result), [](const auto f) { + return static_cast(f); + }); + return result; +} +auto Device::getChildDevices() const -> std::vector { + 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; +} +auto Device::queryCustomPropertyBytes(const CustomProperty property) const + -> std::optional> { + return queryBytes( + [this, property](const size_t size, void* value, size_t* sizeRet) { + return state_->api->queryDevice(state_->session, deviceCustom(property), + size, value, sizeRet); + }, + "custom device property"); +} +auto Device::submitJob(const std::string& program, const ProgramFormat 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 + -> Job { + auto jobState = std::make_shared(state_); + const auto qdmiFormat = static_cast(format); + throwIfError(jobState->device->api->setJobParameter( + jobState->job, QDMI_DEVICE_JOB_PARAMETER_PROGRAMFORMAT, + sizeof(qdmiFormat), &qdmiFormat), + "Setting program format"); + throwIfError(jobState->device->api->setJobParameter( + jobState->job, QDMI_DEVICE_JOB_PARAMETER_PROGRAM, + program.size() + 1, program.c_str()), + "Setting program"); + throwIfError(jobState->device->api->setJobParameter( + 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]); + } } - qdmi::throwIfError(ret, "Waiting for job"); - qdmi::unreachable(); -} - -void Job::cancel() const { - qdmi::throwIfError(QDMI_job_cancel(job_.get()), "Cancelling job"); + jobState->device->api->submitJob(jobState->job); + return Job(std::move(jobState)); } -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; +auto Job::check() const -> JobStatus { + return static_cast(state_->device->api->checkJob(state_->job)); } - -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; +auto Job::wait(const size_t timeout) const -> bool { + return state_->device->api->waitJob(state_->job, timeout); } - -std::string Job::getProgram() const { - size_t size = 0; - qdmi::throwIfError(QDMI_job_query_property(job_.get(), - QDMI_JOB_PROPERTY_PROGRAM, 0, - nullptr, &size), - "Querying program size"); - - std::string program(size - 1, '\0'); - qdmi::throwIfError(QDMI_job_query_property(job_.get(), - QDMI_JOB_PROPERTY_PROGRAM, size, - program.data(), nullptr), - "Querying program"); - return program; -} - -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; +void Job::cancel() const { state_->device->api->cancelJob(state_->job); } +auto Job::getId() const -> std::string { + return queryString( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->device->api->queryJobProperty( + state_->job, QDMI_DEVICE_JOB_PROPERTY_ID, size, value, sizeRet); + }, + "job ID"); +} +auto Job::getProgramFormat() const -> ProgramFormat { + const auto format = queryValue( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->device->api->queryJobProperty( + state_->job, QDMI_DEVICE_JOB_PROPERTY_PROGRAMFORMAT, size, value, + sizeRet); + }, + "job program format"); + return static_cast(format); +} +auto Job::getProgram() const -> std::string { + return queryString( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->device->api->queryJobProperty( + state_->job, QDMI_DEVICE_JOB_PROPERTY_PROGRAM, size, value, + sizeRet); + }, + "job program"); +} +auto Job::getNumShots() const -> size_t { + return queryValue( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->device->api->queryJobProperty( + state_->job, QDMI_DEVICE_JOB_PROPERTY_SHOTSNUM, size, value, + sizeRet); + }, + "job shot count"); +} +auto Job::queryCustomPropertyBytes(const CustomProperty property) const + -> std::optional> { + return queryBytes( + [this, property](const size_t size, void* value, size_t* sizeRet) { + return state_->device->api->queryJobProperty( + state_->job, jobCustom(property), size, value, sizeRet); + }, + "custom job property"); +} +auto Job::getCustomResultBytes(const CustomProperty property) const + -> std::optional> { + return queryBytes( + [this, property](const size_t size, void* value, size_t* sizeRet) { + return state_->device->api->getJobResult( + state_->job, resultCustom(property), size, value, sizeRet); + }, + "custom job result"); } - -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) { +auto Job::getShots() const -> std::vector { + const auto bytes = queryBytes( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->device->api->getJobResult( + 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 {}; } - - 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"); + if (bytes->back() != std::byte{0}) { + throw std::runtime_error("Invalid string while querying job shots"); } - - return shotsVec; + return splitCommaSeparated(std::string( + reinterpret_cast(bytes->data()), bytes->size() - 1)); } - -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; +auto Job::getCounts() const -> std::map { + const auto keys = splitCommaSeparated(queryString( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->device->api->getJobResult( + 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->getJobResult( + 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; +} +auto Job::getDenseStateVector() const -> std::vector> { + return queryVector>( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->device->api->getJobResult( + state_->job, QDMI_JOB_RESULT_STATEVECTOR_DENSE, size, value, + sizeRet); + }, + "dense state vector"); +} +auto Job::getDenseProbabilities() const -> std::vector { + return queryVector( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->device->api->getJobResult( + state_->job, QDMI_JOB_RESULT_PROBABILITIES_DENSE, size, value, + sizeRet); + }, + "dense probabilities"); +} +auto Job::getSparseStateVector() const + -> std::map> { + const auto keys = splitCommaSeparated(queryString( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->device->api->getJobResult( + 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->getJobResult( + 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; +} +auto Job::getSparseProbabilities() const -> std::map { + const auto keys = splitCommaSeparated(queryString( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->device->api->getJobResult( + 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->getJobResult( + 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->querySite(state_->session, \ + static_cast(handle_), \ + property, size, value, sizeRet); \ + }, \ + description); \ + } +auto Site::getIndex() const -> size_t { + return queryValue( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->api->querySite( + 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 +auto Site::getName() const -> std::optional { + return queryOptionalString( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->api->querySite( + state_->session, static_cast(handle_), + QDMI_SITE_PROPERTY_NAME, size, value, sizeRet); + }, + "site name"); +} +auto Site::isZone() const -> bool { + return queryOptionalValue( + [this](const size_t size, void* value, size_t* sizeRet) { + return state_->api->querySite( + state_->session, static_cast(handle_), + QDMI_SITE_PROPERTY_ISZONE, size, value, sizeRet); + }, + "site zone flag") + .value_or(false); } - -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; +auto Site::queryCustomPropertyBytes(const CustomProperty property) const + -> std::optional> { + return queryBytes( + [this, property](const size_t size, void* value, size_t* sizeRet) { + return state_->api->querySite( + state_->session, static_cast(handle_), + siteCustom(property), size, value, sizeRet); + }, + "custom site property"); } -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; +auto Operation::siteHandles(const std::vector& sites) + -> std::vector { + std::vector handles; + handles.reserve(sites.size()); + std::ranges::transform( + sites, std::back_inserter(handles), + [](const Site& site) { 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->queryOperation( \ + state_->session, static_cast(handle_), \ + opaqueHandles.size(), handles, params.size(), params.data(), \ + property, size, value, sizeRet); \ + }, \ + description); \ + } +auto Operation::getName(const std::vector& sites, + const std::vector& params) const + -> std::string { + 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->queryOperation( + 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 +auto Operation::getParametersNum(const std::vector& sites, + const std::vector& params) const + -> size_t { + 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->queryOperation( + state_->session, static_cast(handle_), + opaqueHandles.size(), handles, params.size(), params.data(), + QDMI_OPERATION_PROPERTY_PARAMETERSNUM, size, value, sizeRet); + }, + "operation parameter count"); +} +auto Operation::isZoned() const -> bool { + 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->queryOperation( + 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::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"); +auto Operation::getSites() const -> std::optional> { + 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->queryOperation( + 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; } - return stateVector; + 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::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"); +auto Operation::getSitePairs() const + -> std::optional>> { + if (isZoned() || getQubitsNum().value_or(0) != 2) { + return std::nullopt; } - - 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; + const auto sites = getSites(); + if (!sites || sites->empty() || sites->size() % 2 != 0) { + return std::nullopt; } - if (idx != values.size()) { - throw std::runtime_error("Sparse probabilities key/value count mismatch"); + 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 probabilities; + return pairs; +} +auto Operation::queryCustomPropertyBytes( + const CustomProperty property, const std::vector& sites, + const std::vector& params) const + -> std::optional> { + 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->queryOperation( + 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.hpp b/src/qdmi/DeviceApi.hpp new file mode 100644 index 0000000000..084069a7a9 --- /dev/null +++ b/src/qdmi/DeviceApi.hpp @@ -0,0 +1,138 @@ +/* + * 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/DeviceManager.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +namespace qdmi::detail { + +/** Private, ABI-neutral interface used by the public QDMI object model. */ +class DeviceApi { +public: + DeviceApi() = default; + DeviceApi(const DeviceApi&) = delete; + DeviceApi& operator=(const DeviceApi&) = delete; + DeviceApi(DeviceApi&&) = delete; + DeviceApi& operator=(DeviceApi&&) = delete; + virtual ~DeviceApi() = default; + + [[nodiscard]] virtual auto + openSession(const SessionParameters& parameters, + QDMI_Child_Device child = nullptr) const + -> QDMI_Device_Session = 0; + virtual void closeSession(QDMI_Device_Session session) const noexcept = 0; + + [[nodiscard]] virtual auto createJob(QDMI_Device_Session session) const + -> QDMI_Device_Job = 0; + virtual void freeJob(QDMI_Device_Job job) const noexcept = 0; + [[nodiscard]] virtual auto + setJobParameter(QDMI_Device_Job job, QDMI_Device_Job_Parameter parameter, + size_t size, const void* value) const -> int = 0; + [[nodiscard]] virtual auto queryJobProperty(QDMI_Device_Job job, + QDMI_Device_Job_Property property, + size_t size, void* value, + size_t* sizeRet) const -> int = 0; + virtual void submitJob(QDMI_Device_Job job) const = 0; + virtual void cancelJob(QDMI_Device_Job job) const = 0; + [[nodiscard]] virtual auto checkJob(QDMI_Device_Job job) const + -> QDMI_Job_Status = 0; + [[nodiscard]] virtual auto waitJob(QDMI_Device_Job job, size_t timeout) const + -> bool = 0; + [[nodiscard]] virtual auto getJobResult(QDMI_Device_Job job, + QDMI_Job_Result result, size_t size, + void* data, size_t* sizeRet) const + -> int = 0; + + [[nodiscard]] virtual auto queryDevice(QDMI_Device_Session session, + QDMI_Device_Property property, + size_t size, void* value, + size_t* sizeRet) const -> int = 0; + [[nodiscard]] virtual auto querySite(QDMI_Device_Session session, + QDMI_Site site, + QDMI_Site_Property property, size_t size, + void* value, size_t* sizeRet) const + -> int = 0; + [[nodiscard]] virtual auto + queryOperation(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) const -> int = 0; +}; + +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; +}; + +struct DeviceState { + std::shared_ptr lifetime; + std::shared_ptr api; + QDMI_Device_Session session = nullptr; + SessionParameters parameters; + std::vector> children; + + DeviceState(std::shared_ptr deviceApi, + const SessionParameters& sessionParameters, + QDMI_Child_Device child = nullptr, + std::shared_ptr parentSession = nullptr); + ~DeviceState(); + DeviceState(const DeviceState&) = delete; + DeviceState& operator=(const DeviceState&) = delete; + DeviceState(DeviceState&&) = delete; + DeviceState& operator=(DeviceState&&) = delete; +}; + +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/private construction access for ABI adapters. */ +struct DeviceFactory { + [[nodiscard]] static auto create(std::shared_ptr api, + const SessionParameters& parameters = {}) + -> Device { + return Device(std::make_shared(std::move(api), parameters)); + } +}; + +[[nodiscard]] auto makeV1DeviceApi(const std::filesystem::path& library, + const std::string& prefix) + -> std::shared_ptr; + +} // namespace qdmi::detail diff --git a/src/qdmi/DeviceManager.cpp b/src/qdmi/DeviceManager.cpp index aab323f65d..455b80fe9d 100644 --- a/src/qdmi/DeviceManager.cpp +++ b/src/qdmi/DeviceManager.cpp @@ -10,10 +10,10 @@ #include "qdmi/DeviceManager.hpp" -#include "qdmi/driver/Driver.hpp" +#include "DeviceApi.hpp" #include -#include +#include #include #include @@ -50,7 +50,6 @@ struct SessionPatch { std::optional authUrl; std::optional username; std::optional password; - std::optional projectId; std::optional custom1; std::optional custom2; std::optional custom3; @@ -126,8 +125,8 @@ parseSessionPatch(const Json& value, const std::filesystem::path& source, requireObject(value, source, path); rejectUnknownKeys(value, {"base-url", "token", "auth-file", "auth-url", "username", - "password", "project-id", "custom1", "custom2", "custom3", - "custom4", "custom5"}, + "password", "custom1", "custom2", "custom3", "custom4", + "custom5"}, source, path); SessionPatch patch; patch.baseUrl = optionalString(value, "base-url", source, path); @@ -135,7 +134,6 @@ parseSessionPatch(const Json& value, const std::filesystem::path& source, patch.authUrl = optionalString(value, "auth-url", source, path); patch.username = optionalString(value, "username", source, path); patch.password = optionalString(value, "password", source, path); - patch.projectId = optionalString(value, "project-id", source, path); patch.custom1 = optionalString(value, "custom1", source, path); patch.custom2 = optionalString(value, "custom2", source, path); patch.custom3 = optionalString(value, "custom3", source, path); @@ -273,7 +271,6 @@ void mergeSession(SessionPatch& target, const SessionPatch& source) { mergeOptional(target.authUrl, source.authUrl); mergeOptional(target.username, source.username); mergeOptional(target.password, source.password); - mergeOptional(target.projectId, source.projectId); mergeOptional(target.custom1, source.custom1); mergeOptional(target.custom2, source.custom2); mergeOptional(target.custom3, source.custom3); @@ -323,11 +320,24 @@ void mergePatch(DefinitionPatch& target, const DefinitionPatch& source) { } [[nodiscard]] auto environment(const char* name) -> std::optional { +#ifdef _WIN32 + char* raw = nullptr; + size_t size = 0; + if (_dupenv_s(&raw, &size, name) != 0 || raw == nullptr) { + return std::nullopt; + } + const std::unique_ptr value(raw, &std::free); + if (*value == '\0') { + return std::nullopt; + } + return std::string(value.get()); +#else if (const auto* value = std::getenv(name); value != nullptr && *value != '\0') { return std::string(value); } return std::nullopt; +#endif } void appendIfFile(std::vector& files, @@ -454,7 +464,6 @@ void appendFragments(std::vector& files, patch.session.authUrl = definition.session.authUrl; patch.session.username = definition.session.username; patch.session.password = definition.session.password; - patch.session.projectId = definition.session.projectId; patch.session.custom1 = definition.session.custom1; patch.session.custom2 = definition.session.custom2; patch.session.custom3 = definition.session.custom3; @@ -495,7 +504,6 @@ void appendFragments(std::vector& files, definition.session.authUrl = patch.session.authUrl; definition.session.username = patch.session.username; definition.session.password = patch.session.password; - definition.session.projectId = patch.session.projectId; definition.session.custom1 = patch.session.custom1; definition.session.custom2 = patch.session.custom2; definition.session.custom3 = patch.session.custom3; @@ -511,7 +519,6 @@ void overlay(SessionParameters& target, const SessionParameters& source) { mergeOptional(target.authUrl, source.authUrl); mergeOptional(target.username, source.username); mergeOptional(target.password, source.password); - mergeOptional(target.projectId, source.projectId); mergeOptional(target.custom1, source.custom1); mergeOptional(target.custom2, source.custom2); mergeOptional(target.custom3, source.custom3); @@ -519,24 +526,6 @@ void overlay(SessionParameters& target, const SessionParameters& source) { mergeOptional(target.custom5, source.custom5); } -[[nodiscard]] auto toV1Config(const SessionParameters& parameters) - -> DeviceSessionConfig { - DeviceSessionConfig config; - config.baseUrl = parameters.baseUrl; - config.token = parameters.token; - if (parameters.authFile) { - config.authFile = parameters.authFile->string(); - } - config.authUrl = parameters.authUrl; - config.username = parameters.username; - config.password = parameters.password; - config.custom1 = parameters.custom1; - config.custom2 = parameters.custom2; - config.custom3 = parameters.custom3; - config.custom4 = parameters.custom4; - config.custom5 = parameters.custom5; - return config; -} } // namespace DeviceRegistry::DeviceRegistry(const ConfigOptions& options) { @@ -628,7 +617,7 @@ struct DeviceManager::Impl { DeviceRegistry registry; std::mutex mutex; - std::map> libraries; + std::map> libraries; }; DeviceManager::DeviceManager(const ConfigOptions& options) @@ -664,35 +653,18 @@ Device DeviceManager::open(const std::string_view id, std::string(id) + "'"); } const auto key = definition->library.string() + "\n" + definition->prefix; - std::shared_ptr library; + std::shared_ptr library; { const std::scoped_lock lock(impl_->mutex); library = impl_->libraries[key].lock(); if (!library) { - library = std::make_shared( - definition->library.string(), definition->prefix); + library = + detail::makeV1DeviceApi(definition->library, definition->prefix); impl_->libraries[key] = library; } } auto parameters = definition->session; overlay(parameters, sessionOverrides); - auto state = - std::make_shared(library, toV1Config(parameters)); - auto* const handle = state.get(); - return Device(handle, std::move(state)); -} - -OpenAllResult -DeviceManager::openAll(const SessionParameters& sessionOverrides) { - 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; + return Device(std::make_shared(library, parameters)); } } // namespace qdmi diff --git a/src/qdmi/V1DeviceApi.cpp b/src/qdmi/V1DeviceApi.cpp new file mode 100644 index 0000000000..fc59bd8471 --- /dev/null +++ b/src/qdmi/V1DeviceApi.cpp @@ -0,0 +1,351 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "DeviceApi.hpp" +#include "qdmi/common/Common.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#endif + +namespace qdmi::detail { +namespace { +#ifdef _WIN32 +[[nodiscard]] auto openLibrary(const std::filesystem::path& path) -> void* { + return LoadLibraryExW(path.wstring().c_str(), nullptr, + LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | + LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); +} +[[nodiscard]] auto loadSymbol(void* library, const std::string& symbol) + -> void* { + return reinterpret_cast( + GetProcAddress(static_cast(library), symbol.c_str())); +} +void closeLibrary(void* library) { + static_cast(FreeLibrary(static_cast(library))); +} +#else +[[nodiscard]] auto openLibrary(const std::filesystem::path& path) -> void* { + return dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL); +} +[[nodiscard]] auto loadSymbol(void* library, const std::string& symbol) + -> void* { + return dlsym(library, symbol.c_str()); +} +void closeLibrary(void* library) { static_cast(dlclose(library)); } +#endif + +template +[[nodiscard]] auto resolve(void* library, const std::string& prefix, + const std::string& suffix) -> Function* { + 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 v1 symbol '" + name + "'"); + } + return function; +} + +class V1DeviceApi final : public DeviceApi { +public: + V1DeviceApi(const std::filesystem::path& path, const std::string& prefix) + : library_(openLibrary(path)) { + if (library_ == nullptr) { + throw std::runtime_error("Could not open QDMI device library: " + + path.string()); + } + try { + initialize_ = resolve( + library_, prefix, "device_initialize"); + finalize_ = resolve(library_, prefix, + "device_finalize"); + sessionAlloc_ = resolve( + library_, prefix, "device_session_alloc"); + sessionInit_ = resolve( + library_, prefix, "device_session_init"); + sessionFree_ = resolve( + library_, prefix, "device_session_free"); + sessionSetParameter_ = + resolve( + library_, prefix, "device_session_set_parameter"); + createJob_ = resolve( + library_, prefix, "device_session_create_device_job"); + jobFree_ = resolve(library_, prefix, + "device_job_free"); + jobSetParameter_ = resolve( + library_, prefix, "device_job_set_parameter"); + jobQueryProperty_ = resolve( + library_, prefix, "device_job_query_property"); + jobSubmit_ = resolve( + library_, prefix, "device_job_submit"); + jobCancel_ = resolve( + library_, prefix, "device_job_cancel"); + jobCheck_ = resolve(library_, prefix, + "device_job_check"); + jobWait_ = resolve(library_, prefix, + "device_job_wait"); + jobGetResults_ = resolve( + library_, prefix, "device_job_get_results"); + queryDevice_ = + resolve( + library_, prefix, "device_session_query_device_property"); + querySite_ = resolve( + library_, prefix, "device_session_query_site_property"); + queryOperation_ = + resolve( + library_, prefix, "device_session_query_operation_property"); + throwIfError(initialize_(), "Initializing QDMI device library"); + initialized_ = true; + } catch (...) { + closeLibrary(library_); + library_ = nullptr; + throw; + } + } + + ~V1DeviceApi() override { + if (initialized_) { + static_cast(finalize_()); + } + if (library_ != nullptr) { + closeLibrary(library_); + } + } + + [[nodiscard]] auto openSession(const SessionParameters& parameters, + const QDMI_Child_Device child) const + -> QDMI_Device_Session override { + QDMI_Device_Session session = nullptr; + throwIfError(sessionAlloc_(&session), "Allocating QDMI device session"); + try { + const auto set = [this, + session](const std::optional& value, + const QDMI_Device_Session_Parameter key) { + if (!value) { + return; + } + const auto result = sessionSetParameter_( + 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(sessionSetParameter_( + session, QDMI_DEVICE_SESSION_PARAMETER_CHILDDEVICE, + sizeof(child), &child), + "Selecting QDMI child device"); + } + throwIfError(sessionInit_(session), "Initializing QDMI device session"); + return session; + } catch (...) { + sessionFree_(session); + throw; + } + } + + void closeSession(const QDMI_Device_Session session) const noexcept override { + if (session != nullptr) { + sessionFree_(session); + } + } + + [[nodiscard]] auto createJob(const QDMI_Device_Session session) const + -> QDMI_Device_Job override { + QDMI_Device_Job job = nullptr; + throwIfError(createJob_(session, &job), "Creating QDMI device job"); + return job; + } + void freeJob(const QDMI_Device_Job job) const noexcept override { + if (job != nullptr) { + jobFree_(job); + } + } + [[nodiscard]] auto setJobParameter(const QDMI_Device_Job job, + const QDMI_Device_Job_Parameter parameter, + const size_t size, const void* value) const + -> int override { + return jobSetParameter_(job, parameter, size, value); + } + [[nodiscard]] auto queryJobProperty(const QDMI_Device_Job job, + const QDMI_Device_Job_Property property, + const size_t size, void* value, + size_t* sizeRet) const -> int override { + return jobQueryProperty_(job, property, size, value, sizeRet); + } + void submitJob(const QDMI_Device_Job job) const override { + throwIfError(jobSubmit_(job), "Submitting QDMI job"); + } + void cancelJob(const QDMI_Device_Job job) const override { + throwIfError(jobCancel_(job), "Canceling QDMI job"); + } + [[nodiscard]] auto checkJob(const QDMI_Device_Job job) const + -> QDMI_Job_Status override { + QDMI_Job_Status status{}; + throwIfError(jobCheck_(job, &status), "Checking QDMI job"); + return status; + } + [[nodiscard]] auto waitJob(const QDMI_Device_Job job, + const size_t timeout) const -> bool override { + const auto result = jobWait_(job, timeout); + if (result == QDMI_ERROR_TIMEOUT) { + return false; + } + throwIfError(result, "Waiting for QDMI job"); + return true; + } + [[nodiscard]] auto getJobResult(const QDMI_Device_Job job, + const QDMI_Job_Result result, + const size_t size, void* data, + size_t* sizeRet) const -> int override { + return jobGetResults_(job, result, size, data, sizeRet); + } + [[nodiscard]] auto queryDevice(const QDMI_Device_Session session, + const QDMI_Device_Property property, + const size_t size, void* value, + size_t* sizeRet) const -> int override { + return queryDevice_(session, property, size, value, sizeRet); + } + [[nodiscard]] auto querySite(const QDMI_Device_Session session, + const QDMI_Site site, + const QDMI_Site_Property property, + const size_t size, void* value, + size_t* sizeRet) const -> int override { + return querySite_(session, site, property, size, value, sizeRet); + } + [[nodiscard]] auto queryOperation( + const QDMI_Device_Session session, const QDMI_Operation operation, + const size_t numSites, const QDMI_Site* sites, const size_t numParams, + const double* params, const QDMI_Operation_Property property, + const size_t size, void* value, size_t* sizeRet) const -> int override { + return queryOperation_(session, operation, numSites, sites, numParams, + params, property, size, value, sizeRet); + } + +private: + void* library_ = nullptr; + bool initialized_ = false; + decltype(QDMI_device_initialize)* initialize_ = nullptr; + decltype(QDMI_device_finalize)* finalize_ = nullptr; + decltype(QDMI_device_session_alloc)* sessionAlloc_ = nullptr; + decltype(QDMI_device_session_init)* sessionInit_ = nullptr; + decltype(QDMI_device_session_free)* sessionFree_ = nullptr; + decltype(QDMI_device_session_set_parameter)* sessionSetParameter_ = nullptr; + decltype(QDMI_device_session_create_device_job)* createJob_ = nullptr; + decltype(QDMI_device_job_free)* jobFree_ = nullptr; + decltype(QDMI_device_job_set_parameter)* jobSetParameter_ = nullptr; + decltype(QDMI_device_job_query_property)* jobQueryProperty_ = nullptr; + decltype(QDMI_device_job_submit)* jobSubmit_ = nullptr; + decltype(QDMI_device_job_cancel)* jobCancel_ = nullptr; + decltype(QDMI_device_job_check)* jobCheck_ = nullptr; + decltype(QDMI_device_job_wait)* jobWait_ = nullptr; + decltype(QDMI_device_job_get_results)* jobGetResults_ = nullptr; + decltype(QDMI_device_session_query_device_property)* queryDevice_ = nullptr; + decltype(QDMI_device_session_query_site_property)* querySite_ = nullptr; + decltype(QDMI_device_session_query_operation_property)* queryOperation_ = + nullptr; +}; +} // namespace + +SessionState::SessionState(std::shared_ptr deviceApi, + const SessionParameters& sessionParameters, + const QDMI_Child_Device child, + std::shared_ptr parentSession) + : api(std::move(deviceApi)), parent(std::move(parentSession)) { + session = api->openSession(sessionParameters, child); +} + +SessionState::~SessionState() { api->closeSession(session); } + +DeviceState::DeviceState(std::shared_ptr deviceApi, + const SessionParameters& sessionParameters, + const QDMI_Child_Device child, + std::shared_ptr parentSession) + : lifetime(std::make_shared(deviceApi, sessionParameters, + child, std::move(parentSession))), + api(std::move(deviceApi)), session(lifetime->session), + parameters(sessionParameters) { + try { + size_t size = 0; + const auto result = api->queryDevice( + 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->queryDevice(session, QDMI_DEVICE_PROPERTY_CHILDDEVICES, + size, handles.data(), nullptr), + "Querying QDMI child devices"); + } + children.reserve(handles.size()); + for (auto* handle : handles) { + children.emplace_back( + std::make_shared(api, parameters, handle, lifetime)); + } + } catch (...) { + children.clear(); + lifetime.reset(); + session = nullptr; + throw; + } +} + +DeviceState::~DeviceState() { + children.clear(); + lifetime.reset(); +} + +JobState::JobState(std::shared_ptr deviceState) + : device(std::move(deviceState)), + job(device->api->createJob(device->session)) {} +JobState::~JobState() { device->api->freeJob(job); } + +auto makeV1DeviceApi(const std::filesystem::path& library, + const std::string& prefix) -> std::shared_ptr { + return std::make_shared(library, prefix); +} +} // namespace qdmi::detail diff --git a/src/qdmi/driver/CMakeLists.txt b/src/qdmi/driver/CMakeLists.txt deleted file mode 100644 index 2cefe92919..0000000000 --- a/src/qdmi/driver/CMakeLists.txt +++ /dev/null @@ -1,31 +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) - - # The v1 implementation is private to the public DeviceManager abstraction. - target_sources(${TARGET_NAME} PRIVATE Driver.cpp - ${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 spdlog::spdlog ${CMAKE_DL_LIBS}) - - # 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/Driver.cpp b/src/qdmi/driver/Driver.cpp deleted file mode 100644 index 2d369fc33e..0000000000 --- a/src/qdmi/driver/Driver.cpp +++ /dev/null @@ -1,654 +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 "qdmi/common/Common.hpp" - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef _WIN32 -#include - -#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); - - if (requested.has_parent_path()) { - // A directory component was supplied: Directly load the library. - return LoadLibraryW(requested.wstring().c_str()); - } - - // Bare filename: resolve relative to the driver's own directory so that - // builtin device DLLs installed next to the driver are found reliably. - const auto path = getDriverDirectory() / requested; - 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) - } catch (const std::exception&) { - DL_CLOSE(libHandle_); - throw; - } - // initialize the device - device_initialize(); -} - -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_); - } -} - -#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); - setParameter(config.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_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 { -Driver::Driver() = default; - -auto Driver::addDynamicDeviceLibrary(const std::string& libName, - const std::string& prefix, - const DeviceSessionConfig& config) - -> QDMI_Device { - devices_.emplace_back(std::make_unique( - std::make_shared(libName, prefix), config)); - return devices_.back().get(); -} - -auto Driver::sessionAlloc(QDMI_Session* session) -> int { - if (session == nullptr) { - return QDMI_ERROR_INVALIDARGUMENT; - } - auto uniqueSession = std::make_unique(devices_); - *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/python/na/test_na_qdmi.py b/test/python/na/test_na_qdmi.py index bddba4ff9c..164560a107 100644 --- a/test/python/na/test_na_qdmi.py +++ b/test/python/na/test_na_qdmi.py @@ -28,9 +28,9 @@ def device_tuple() -> tuple[Device, Mapping[str, Any]]: """Return a neutral atom QDMI device instance.""" with pathlib.Path("json/na/device.json").open(encoding="utf-8") as f: device_dict = load(f) - devices, errors = DeviceManager().open_all() - assert not errors - converted = (Device.try_create_from_device(candidate) for candidate in devices.values()) + manager = DeviceManager() + devices = (manager.open(definition.id) for definition in manager.definitions) + converted = (Device.try_create_from_device(candidate) for candidate in devices) device = next(candidate for candidate in converted if candidate is not None) return device, device_dict diff --git a/test/python/plugins/qiskit/test_backend.py b/test/python/plugins/qiskit/test_backend.py index b90c987599..b3b232da6d 100644 --- a/test/python/plugins/qiskit/test_backend.py +++ b/test/python/plugins/qiskit/test_backend.py @@ -52,8 +52,9 @@ def ddsim_backend() -> QDMIBackend: Returns: A QDMIBackend instance wrapping the DDSIM device. """ - devices, _errors = qdmi.DeviceManager().open_all() - for device in devices.values(): + manager = qdmi.DeviceManager() + for definition in manager.definitions: + device = manager.open(definition.id) if "DDSIM" in device.name(): return QDMIBackend(device=device, provider=None) pytest.skip("DDSIM device not available") @@ -599,8 +600,9 @@ def test_backend_openqasm3_translation_works_for_native_gates(ddsim_backend: QDM def test_zoned_operation_rejected_at_backend_init() -> None: """Backend rejects devices exposing zoned operations.""" - devices, _errors = qdmi.DeviceManager().open_all() - for device in devices.values(): + manager = qdmi.DeviceManager() + for definition in manager.definitions: + device = manager.open(definition.id) if device.name().startswith("MQT NA"): with pytest.raises(UnsupportedDeviceError, match="cannot be represented in Qiskit's Target model"): QDMIBackend(device) diff --git a/test/python/plugins/qiskit/test_estimator.py b/test/python/plugins/qiskit/test_estimator.py index 80bf8f89b2..0b5573d45c 100644 --- a/test/python/plugins/qiskit/test_estimator.py +++ b/test/python/plugins/qiskit/test_estimator.py @@ -24,8 +24,9 @@ @pytest.fixture def estimator() -> QDMIEstimator: """Returns a QDMIEstimator based on the DDSIM backend.""" - devices, _errors = qdmi.DeviceManager().open_all() - for device in devices.values(): + manager = qdmi.DeviceManager() + for definition in manager.definitions: + device = manager.open(definition.id) if "DDSIM" in device.name(): backend = QDMIBackend(device=device, provider=None) return QDMIEstimator(backend) diff --git a/test/python/plugins/qiskit/test_mock_backend.py b/test/python/plugins/qiskit/test_mock_backend.py index 9b1bd3cd5e..dc793b5123 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 @@ -321,10 +322,15 @@ def test_custom_device(mock_qdmi_device_factory): def _patch_manager_devices(monkeypatch: pytest.MonkeyPatch, devices: list[MockQDMIDevice]) -> None: """Patch QDMI device discovery to return the supplied mock devices.""" - def _mock_open_all(_self: object, **_kwargs: object) -> tuple[dict[str, MockQDMIDevice], dict[str, str]]: - return {f"mock-{index}": device for index, device in enumerate(devices)}, {} + class MockManager: + def __init__(self) -> None: + self.definitions = [SimpleNamespace(id=f"mock-{index}") for index in range(len(devices))] + self.devices = devices - monkeypatch.setattr(qdmi.DeviceManager, "open_all", _mock_open_all) + def open(self, device_id: str, **_kwargs: object) -> MockQDMIDevice: + return self.devices[int(device_id.removeprefix("mock-"))] + + monkeypatch.setattr(qdmi, "DeviceManager", MockManager) def test_backend_warns_on_unmappable_operation( diff --git a/test/python/plugins/qiskit/test_provider.py b/test/python/plugins/qiskit/test_provider.py index a1c42ddd98..0094b8f7d2 100644 --- a/test/python/plugins/qiskit/test_provider.py +++ b/test/python/plugins/qiskit/test_provider.py @@ -79,11 +79,11 @@ 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_open_all(_self: object, **_kwargs: object) -> tuple[dict[str, object], dict[str, str]]: - return {}, {} + class EmptyManager: + def __init__(self) -> None: + self.definitions: list[object] = [] - monkeypatch.setattr(qdmi.DeviceManager, "open_all", mock_open_all) + monkeypatch.setattr(qdmi, "DeviceManager", EmptyManager) provider = QDMIProvider() with pytest.raises(ValueError, match="No backend found with name"): @@ -164,18 +164,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 +173,6 @@ def test_provider_with_multiple_auth_parameters() -> None: token="test_token", # noqa: S106 username="test_user", password="test_pass", # noqa: S106 - project_id="test_project", ) assert provider is not None except RuntimeError: @@ -239,7 +226,6 @@ def test_provider_with_custom_parameters() -> None: provider = QDMIProvider( token="test_token", # noqa: S106 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 9b2a788292..48907df1fe 100644 --- a/test/python/plugins/qiskit/test_sampler.py +++ b/test/python/plugins/qiskit/test_sampler.py @@ -24,8 +24,9 @@ @pytest.fixture def sampler() -> QDMISampler: """Returns a QDMISampler based on the DDSIM backend.""" - devices, _errors = qdmi.DeviceManager().open_all() - for device in devices.values(): + manager = qdmi.DeviceManager() + for definition in manager.definitions: + device = manager.open(definition.id) if "DDSIM" in device.name(): backend = QDMIBackend(device=device, provider=None) return QDMISampler(backend) diff --git a/test/python/qdmi/test_qdmi.py b/test/python/qdmi/test_qdmi.py index fb0ad72c75..17c6a99dc0 100644 --- a/test/python/qdmi/test_qdmi.py +++ b/test/python/qdmi/test_qdmi.py @@ -20,13 +20,13 @@ def _get_devices() -> list[Device]: - """Open all available QDMI devices. + """Open each available QDMI device. Returns: List of all available QDMI devices. """ - devices, _errors = DeviceManager().open_all() - return list(devices.values()) + manager = DeviceManager() + return [manager.open(definition.id) for definition in manager.definitions] @pytest.fixture(params=_get_devices()) diff --git a/test/qdmi/device/CMakeLists.txt b/test/qdmi/device/CMakeLists.txt index 0c88cf675e..503632efc6 100644 --- a/test/qdmi/device/CMakeLists.txt +++ b/test/qdmi/device/CMakeLists.txt @@ -9,7 +9,9 @@ set(TARGET_NAME mqt-core-qdmi-object-model-test) if(TARGET MQT::CoreQDMI) - package_add_test(${TARGET_NAME} MQT::CoreQDMI test_device.cpp) + 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) + target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQDMICommon qdmi::qdmi) target_compile_definitions( ${TARGET_NAME} PRIVATE DDSIM_DEVICE_LIBRARY="$" diff --git a/test/qdmi/device/test_device.cpp b/test/qdmi/device/test_device.cpp index d4484c0207..a45b920c54 100644 --- a/test/qdmi/device/test_device.cpp +++ b/test/qdmi/device/test_device.cpp @@ -14,7 +14,7 @@ #include #include -#include +#include #include #include @@ -37,21 +37,6 @@ 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)); @@ -76,11 +61,12 @@ auto configuredManager() -> DeviceManager { } auto getDevices() -> std::vector { - auto opened = configuredManager().openAll(); + auto manager = configuredManager(); std::vector devices; - devices.reserve(opened.devices.size()); - std::ranges::move(opened.devices | std::views::values, - std::back_inserter(devices)); + devices.reserve(manager.definitions().size()); + for (const auto& definition : manager.definitions()) { + devices.emplace_back(manager.open(definition.id)); + } return devices; } @@ -131,7 +117,7 @@ bit[1] c; h q[0]; c[0] = measure q[0]; )"; - return device.submitJob(qasm3Program, QDMI_PROGRAM_FORMAT_QASM3, 10); + return device.submitJob(qasm3Program, ProgramFormat::Qasm3, 10); } }; @@ -148,134 +134,59 @@ qubit[2] q; h q[0]; cx q[0], q[1]; )"; - return device.submitJob(qasm3Program, QDMI_PROGRAM_FORMAT_QASM3, 0); + return device.submitJob(qasm3Program, ProgramFormat::Qasm3, 0); } }; } // 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); } @@ -375,10 +286,6 @@ TEST(QDMITest, DevicePropertyToString) { EXPECT_STREQ(qdmi::toString(QDMI_DEVICE_PROPERTY_CUSTOM5), "CUSTOM5"); } -TEST(QDMITest, SessionPropertyToString) { - EXPECT_STREQ(qdmi::toString(QDMI_SESSION_PROPERTY_DEVICES), "DEVICES"); -} - TEST(QDMITest, ThrowIfError) { EXPECT_NO_THROW(qdmi::throwIfError(QDMI_SUCCESS, "Test")); EXPECT_NO_THROW(qdmi::throwIfError(QDMI_WARN_GENERAL, "Test")); @@ -742,15 +649,14 @@ h q[0]; cx q[0], q[1]; c = measure q;)"; - const auto job = - device.submitJob(qasm3Program, QDMI_PROGRAM_FORMAT_QASM3, 100); + const auto job = device.submitJob(qasm3Program, ProgramFormat::Qasm3, 100); EXPECT_FALSE(job.getId().empty()); - EXPECT_EQ(job.getProgramFormat(), QDMI_PROGRAM_FORMAT_QASM3); + EXPECT_EQ(job.getProgramFormat(), ProgramFormat::Qasm3); EXPECT_STREQ(job.getProgram().c_str(), qasm3Program.c_str()); EXPECT_EQ(job.getNumShots(), 100); EXPECT_TRUE(job.wait()); - EXPECT_EQ(job.check(), QDMI_JOB_STATUS_DONE); + EXPECT_EQ(job.check(), JobStatus::Done); } TEST_F(DDSimulatorDeviceTest, SubmitJobCustomSupportedTypes) { @@ -760,24 +666,23 @@ TEST_F(DDSimulatorDeviceTest, SubmitJobCustomSupportedTypes) { try { switch (which) { case 1: - device.submitJob(qasm3Program, QDMI_PROGRAM_FORMAT_QASM3, 10, custom); + device.submitJob(qasm3Program, ProgramFormat::Qasm3, 10, custom); break; case 2: - device.submitJob(qasm3Program, QDMI_PROGRAM_FORMAT_QASM3, 10, - std::nullopt, custom); + device.submitJob(qasm3Program, ProgramFormat::Qasm3, 10, std::nullopt, + custom); break; case 3: - device.submitJob(qasm3Program, QDMI_PROGRAM_FORMAT_QASM3, 10, - std::nullopt, std::nullopt, custom); + device.submitJob(qasm3Program, ProgramFormat::Qasm3, 10, std::nullopt, + std::nullopt, custom); break; case 4: - device.submitJob(qasm3Program, QDMI_PROGRAM_FORMAT_QASM3, 10, - std::nullopt, std::nullopt, std::nullopt, custom); + device.submitJob(qasm3Program, ProgramFormat::Qasm3, 10, std::nullopt, + std::nullopt, std::nullopt, custom); break; case 5: - device.submitJob(qasm3Program, QDMI_PROGRAM_FORMAT_QASM3, 10, - std::nullopt, std::nullopt, std::nullopt, std::nullopt, - custom); + device.submitJob(qasm3Program, ProgramFormat::Qasm3, 10, std::nullopt, + std::nullopt, std::nullopt, std::nullopt, custom); break; default: throw std::invalid_argument("Invalid 'which' value"); @@ -804,16 +709,13 @@ bit[1] c; c[0] = measure q[0]; )"; - const auto job1 = - device.submitJob(qasm3Program, QDMI_PROGRAM_FORMAT_QASM3, 10); + const auto job1 = device.submitJob(qasm3Program, ProgramFormat::Qasm3, 10); EXPECT_EQ(job1.getNumShots(), 10); - const auto job2 = - device.submitJob(qasm3Program, QDMI_PROGRAM_FORMAT_QASM3, 100); + const auto job2 = device.submitJob(qasm3Program, ProgramFormat::Qasm3, 100); EXPECT_EQ(job2.getNumShots(), 100); - const auto job3 = - device.submitJob(qasm3Program, QDMI_PROGRAM_FORMAT_QASM3, 1000); + const auto job3 = device.submitJob(qasm3Program, ProgramFormat::Qasm3, 1000); EXPECT_EQ(job3.getNumShots(), 1000); } @@ -824,8 +726,7 @@ qubit[1] q; bit[1] c; c[0] = measure q[0]; )"; - const auto job2 = - device.submitJob(qasm3Program, QDMI_PROGRAM_FORMAT_QASM3, 10); + const auto job2 = device.submitJob(qasm3Program, ProgramFormat::Qasm3, 10); EXPECT_NE(job.getId(), job2.getId()); } @@ -844,8 +745,7 @@ TEST_F(JobTest, StatusProgresses) { EXPECT_TRUE(job.wait()); const auto finalStatus = job.check(); - EXPECT_THAT(finalStatus, - testing::AnyOf(QDMI_JOB_STATUS_DONE, QDMI_JOB_STATUS_FAILED)); + EXPECT_THAT(finalStatus, testing::AnyOf(JobStatus::Done, JobStatus::Failed)); } TEST_F(JobTest, GetCountsReturnsValidHistogram) { @@ -897,7 +797,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; } } @@ -909,7 +810,7 @@ bit[1] c; c[0] = measure q[0]; )"; const auto jobToCancel = - device.submitJob(qasm3Program, QDMI_PROGRAM_FORMAT_QASM3, 10); + device.submitJob(qasm3Program, ProgramFormat::Qasm3, 10); // Fast-executing jobs (like the DD simulator) may complete before // cancel is called, which should throw an exception. @@ -918,12 +819,11 @@ c[0] = measure q[0]; jobToCancel.cancel(); // If cancel succeeded, the job should be in CANCELED state const auto status = jobToCancel.check(); - EXPECT_EQ(status, QDMI_JOB_STATUS_CANCELED); + EXPECT_EQ(status, JobStatus::Canceled); } catch (const std::invalid_argument&) { // If cancel threw an exception, the job should already be done const auto status = jobToCancel.check(); - EXPECT_THAT(status, - testing::AnyOf(QDMI_JOB_STATUS_DONE, QDMI_JOB_STATUS_FAILED)); + EXPECT_THAT(status, testing::AnyOf(JobStatus::Done, JobStatus::Failed)); } } @@ -931,8 +831,7 @@ TEST_F(JobTest, CancelCompletedJobThrows) { EXPECT_TRUE(job.wait()); const auto statusBefore = job.check(); - EXPECT_THAT(statusBefore, - testing::AnyOf(QDMI_JOB_STATUS_DONE, QDMI_JOB_STATUS_FAILED)); + EXPECT_THAT(statusBefore, testing::AnyOf(JobStatus::Done, JobStatus::Failed)); EXPECT_THROW(job.cancel(), std::invalid_argument); } @@ -998,22 +897,7 @@ 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(DeviceManagerTest, OpenAllReturnsDevices) { +TEST(DeviceManagerTest, DiscoversAndOpensDevices) { auto devices = getDevices(); EXPECT_FALSE(devices.empty()); diff --git a/test/qdmi/device/test_device_api.cpp b/test/qdmi/device/test_device_api.cpp new file mode 100644 index 0000000000..f1f5a4f755 --- /dev/null +++ b/test/qdmi/device/test_device_api.cpp @@ -0,0 +1,238 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "DeviceApi.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace qdmi::detail { +namespace { +class ScriptedDeviceApi final : public DeviceApi { + struct Child { + size_t id; + }; + struct Session { + QDMI_Child_Device child = nullptr; + }; + + std::array children_{{{0}, {1}}}; + + [[nodiscard]] static auto asSession(const QDMI_Device_Session session) + -> Session* { + return reinterpret_cast(session); + } + +public: + enum class ChildBehavior : std::uint8_t { + Supported, + Unsupported, + Malformed, + QueryFailure, + SelectionFailure, + }; + + ChildBehavior behavior = ChildBehavior::Supported; + size_t opened = 0; + size_t closed = 0; + std::vector closeOrder; + + [[nodiscard]] auto openSession(const SessionParameters&, + const QDMI_Child_Device child) const + -> QDMI_Device_Session override { + auto* mutableThis = const_cast(this); + ++mutableThis->opened; + if (child != nullptr && behavior == ChildBehavior::SelectionFailure) { + ++mutableThis->closed; + throw std::runtime_error("child selection failed"); + } + return reinterpret_cast(new Session{child}); + } + + void closeSession(const QDMI_Device_Session session) const noexcept override { + if (session == nullptr) { + return; + } + auto* mutableThis = const_cast(this); + const auto* typed = asSession(session); + if (typed->child == nullptr) { + mutableThis->closeOrder.emplace_back("parent"); + } else { + const auto* child = reinterpret_cast(typed->child); + mutableThis->closeOrder.emplace_back("child-" + + std::to_string(child->id)); + } + ++mutableThis->closed; + delete typed; + } + + [[nodiscard]] auto createJob(QDMI_Device_Session) const + -> QDMI_Device_Job override { + throw std::runtime_error("jobs are not scripted"); + } + void freeJob(QDMI_Device_Job) const noexcept override {} + [[nodiscard]] auto setJobParameter(QDMI_Device_Job, QDMI_Device_Job_Parameter, + size_t, const void*) const + -> int override { + return QDMI_ERROR_NOTSUPPORTED; + } + [[nodiscard]] auto queryJobProperty(QDMI_Device_Job, QDMI_Device_Job_Property, + size_t, void*, size_t*) const + -> int override { + return QDMI_ERROR_NOTSUPPORTED; + } + void submitJob(QDMI_Device_Job) const override { + throw std::runtime_error("jobs are not scripted"); + } + void cancelJob(QDMI_Device_Job) const override { + throw std::runtime_error("jobs are not scripted"); + } + [[nodiscard]] auto checkJob(QDMI_Device_Job) const + -> QDMI_Job_Status override { + return QDMI_JOB_STATUS_FAILED; + } + [[nodiscard]] auto waitJob(QDMI_Device_Job, size_t) const -> bool override { + return false; + } + [[nodiscard]] auto getJobResult(QDMI_Device_Job, QDMI_Job_Result, size_t, + void*, size_t*) const -> int override { + return QDMI_ERROR_NOTSUPPORTED; + } + + [[nodiscard]] auto queryDevice(const QDMI_Device_Session session, + const QDMI_Device_Property property, + const size_t size, void* value, + size_t* sizeRet) const -> int override { + 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; + } + std::array handles{ + reinterpret_cast( + const_cast(&children_[0])), + reinterpret_cast( + const_cast(&children_[1]))}; + std::memcpy(value, handles.data(), required); + } + 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]] auto querySite(QDMI_Device_Session, QDMI_Site, + QDMI_Site_Property, size_t, void*, size_t*) const + -> int override { + return QDMI_ERROR_NOTSUPPORTED; + } + [[nodiscard]] auto queryOperation(QDMI_Device_Session, QDMI_Operation, size_t, + const QDMI_Site*, size_t, const double*, + QDMI_Operation_Property, size_t, void*, + size_t*) const -> int override { + return QDMI_ERROR_NOTSUPPORTED; + } +}; + +TEST(DeviceApiTest, ChildRetainsParentSessionAndLibrary) { + const auto api = std::make_shared(); + std::optional retainedChild; + { + const auto parent = DeviceFactory::create(api); + 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, HandlesUnsupportedAndInvalidChildLists) { + const auto unsupported = std::make_shared(); + unsupported->behavior = ScriptedDeviceApi::ChildBehavior::Unsupported; + EXPECT_TRUE(DeviceFactory::create(unsupported).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)), + 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)), + 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)), + 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); + constexpr auto invalid = static_cast(0); + EXPECT_THROW(static_cast(device.queryCustomProperty(invalid)), + std::invalid_argument); +} +} // namespace +} // namespace qdmi::detail diff --git a/test/qdmi/driver/CMakeLists.txt b/test/qdmi/driver/CMakeLists.txt deleted file mode 100644 index ec91fd6fcf..0000000000 --- a/test/qdmi/driver/CMakeLists.txt +++ /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 - -set(TARGET_NAME mqt-core-qdmi-driver-test) - -if(TARGET MQT::CoreQDMIDriver) - package_add_test(${TARGET_NAME} MQT::CoreQDMIDriver test_driver.cpp) - target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQDMI) - target_compile_definitions( - ${TARGET_NAME} - PRIVATE - "DYN_DEV_LIBS=std::array{ std::pair{\"$\", \"MQT_NA\"}, std::pair{\"$\", \"MQT_SC\"}, std::pair{\"$\", \"MQT_DDSIM\"} }" - ) - - if(WIN32) - # On Windows, we need to copy the device DLLs to the test directory - add_custom_command( - TARGET ${TARGET_NAME} - PRE_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different $ - $) - add_custom_command( - TARGET ${TARGET_NAME} - PRE_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different $ - $) - add_custom_command( - TARGET ${TARGET_NAME} - PRE_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different $ - $) - endif() -endif() diff --git a/test/qdmi/driver/test_driver.cpp b/test/qdmi/driver/test_driver.cpp deleted file mode 100644 index 64c017b2e7..0000000000 --- a/test/qdmi/driver/test_driver.cpp +++ /dev/null @@ -1,972 +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/Device.hpp" -#include "qdmi/driver/Driver.hpp" - -#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 { - -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; -} - -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"); - - 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"}; -// 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(DeviceSessionConfigTest, AddDynamicDeviceLibraryWithBaseUrl) { - qdmi::DeviceSessionConfig config; - config.baseUrl = "http://localhost:8080"; - - for (const auto& [lib, prefix] : DYN_DEV_LIBS) { - EXPECT_NO_THROW( - { qdmi::Driver::get().addDynamicDeviceLibrary(lib, prefix, config); }); - } -} - -TEST(DeviceSessionConfigTest, AddDynamicDeviceLibraryWithCustomParameters) { - qdmi::DeviceSessionConfig config; - config.custom1 = "RESONANCE_COCOS_V1"; - config.custom2 = "test_value"; - config.baseUrl = "http://localhost:9090"; - - for (const auto& [lib, prefix] : DYN_DEV_LIBS) { - // Custom parameters may fail with validation errors or succeed/return false - try { - qdmi::Driver::get().addDynamicDeviceLibrary(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, AddDynamicDeviceLibraryWithAuthToken) { - qdmi::DeviceSessionConfig config; - config.token = "test_token_123"; - config.baseUrl = "https://api.example.com"; - - for (const auto& [lib, prefix] : DYN_DEV_LIBS) { - EXPECT_NO_THROW( - { qdmi::Driver::get().addDynamicDeviceLibrary(lib, prefix, config); }); - } -} - -TEST(DeviceSessionConfigTest, AddDynamicDeviceLibraryWithAuthFile) { - qdmi::DeviceSessionConfig config; - config.authFile = "/nonexistent/auth.json"; - - for (const auto& [lib, prefix] : DYN_DEV_LIBS) { - // This should not throw even with non-existent file because - // if the auth file parameter is not supported, it's skipped - EXPECT_NO_THROW( - { qdmi::Driver::get().addDynamicDeviceLibrary(lib, prefix, config); }); - } -} - -TEST(DeviceSessionConfigTest, AddDynamicDeviceLibraryWithUsernamePassword) { - qdmi::DeviceSessionConfig config; - config.authUrl = "https://auth.example.com"; - config.username = "quantum_user"; - config.password = "secret_password"; - - for (const auto& [lib, prefix] : DYN_DEV_LIBS) { - EXPECT_NO_THROW( - { qdmi::Driver::get().addDynamicDeviceLibrary(lib, prefix, config); }); - } -} - -TEST(DeviceSessionConfigTest, AddDynamicDeviceLibraryWithAllParameters) { - 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] : DYN_DEV_LIBS) { - try { - qdmi::Driver::get().addDynamicDeviceLibrary(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 (DYN_DEV_LIBS.empty()) { - GTEST_SKIP() << "No dynamic device libraries to test"; - } - auto& driver = qdmi::Driver::get(); - for (const auto& [lib, prefix] : DYN_DEV_LIBS) { - // Config 1: baseUrl - { - qdmi::DeviceSessionConfig config; - config.baseUrl = "http://localhost:8080"; - EXPECT_NO_THROW(driver.addDynamicDeviceLibrary(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(driver.addDynamicDeviceLibrary(lib, prefix, config);); - } - - // Config 3: authentication parameters - { - qdmi::DeviceSessionConfig config; - config.token = "new_token"; - config.authUrl = "https://auth.example.com"; - EXPECT_NO_THROW(driver.addDynamicDeviceLibrary(lib, prefix, config);); - } - } -} - -TEST(DynamicDeviceLibraryTest, addDynamicDeviceLibraryReturnsDevice) { - // Test that addDynamicDeviceLibrary returns a valid device pointer - if constexpr (DYN_DEV_LIBS.empty()) { - GTEST_SKIP() << "No dynamic device libraries configured for testing."; - } - auto& driver = qdmi::Driver::get(); - for (const auto& [lib, prefix] : DYN_DEV_LIBS) { - const qdmi::DeviceSessionConfig config; - QDMI_Device device = nullptr; - ASSERT_NO_THROW( - { device = driver.addDynamicDeviceLibrary(lib, prefix, config); }); - ASSERT_NE(device, nullptr) - << "addDynamicDeviceLibrary 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/test_device_manager.cpp b/test/qdmi/manager/test_device_manager.cpp index 12a6763ad5..6bf6554662 100644 --- a/test/qdmi/manager/test_device_manager.cpp +++ b/test/qdmi/manager/test_device_manager.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #include @@ -22,8 +23,9 @@ namespace { class TemporaryDirectory { public: TemporaryDirectory() { - path_ = - std::filesystem::temp_directory_path() / "mqt-core-qdmi-manager-test"; + path_ = std::filesystem::temp_directory_path() / + ("mqt-core-qdmi-manager-test-" + + std::to_string(std::random_device{}())); std::filesystem::remove_all(path_); std::filesystem::create_directories(path_); } @@ -34,6 +36,7 @@ class TemporaryDirectory { void write(const std::filesystem::path& relative, const std::string& contents) const { + std::filesystem::create_directories((path_ / relative).parent_path()); std::ofstream output(path_ / relative); output << contents; } @@ -158,6 +161,137 @@ TEST(DeviceRegistry, ReadsProjectConfigurationFromPyprojectToml) { EXPECT_EQ(definitions.front().library, directory.path() / "device.so"); } +TEST(DeviceRegistry, DedicatedProjectFileWinsOverPyproject) { + const TemporaryDirectory directory; + directory.write("pyproject.toml", R"( + [tool.mqt-core.qdmi] + devices = [{ id = "toml", library = "toml.so", prefix = "TOML" }] + )"); + directory.write("mqt-core.json", R"({ + "schema-version": 1, + "qdmi": {"devices": [ + {"id": "json", "library": "json.so", "prefix": "JSON"} + ]} + })"); + qdmi::ConfigOptions options; + options.isolated = true; + options.baseDirectory = directory.path(); + + const qdmi::DeviceRegistry registry(options); + ASSERT_EQ(registry.definitions().size(), 1); + EXPECT_EQ(registry.definitions().front().id, "json"); +} + +TEST(DeviceRegistry, ReportsInvalidDocumentsAndSchemaPaths) { + qdmi::ConfigOptions options; + options.isolated = true; + options.inlineOverrides = nlohmann::json::object(); + EXPECT_THROW(static_cast(qdmi::DeviceRegistry(options)), + std::invalid_argument); + + options.inlineOverrides = nlohmann::json::parse(R"({ + "schema-version": 2, + "qdmi": {} + })"); + EXPECT_THROW(static_cast(qdmi::DeviceRegistry(options)), + std::invalid_argument); + + options.inlineOverrides = nlohmann::json::parse(R"({ + "schema-version": 1, + "qdmi": {"devices": {}} + })"); + EXPECT_THROW(static_cast(qdmi::DeviceRegistry(options)), + std::invalid_argument); +} + +TEST(DeviceRegistry, ValidatesDefinitionAndSessionTypes) { + qdmi::ConfigOptions options; + options.isolated = true; + options.inlineOverrides = nlohmann::json::parse(R"({ + "schema-version": 1, + "qdmi": {"devices": [{"id": 4}]} + })"); + EXPECT_THROW(static_cast(qdmi::DeviceRegistry(options)), + std::invalid_argument); + + options.inlineOverrides = nlohmann::json::parse(R"({ + "schema-version": 1, + "qdmi": {"devices": [{ + "id": "invalid", "library": "device", "prefix": "P", + "enabled": "yes" + }]} + })"); + EXPECT_THROW(static_cast(qdmi::DeviceRegistry(options)), + std::invalid_argument); + + options.inlineOverrides = nlohmann::json::parse(R"({ + "schema-version": 1, + "qdmi": {"devices": [{ + "id": "invalid", "library": "device", "prefix": "P", + "session": {"token": 42} + }]} + })"); + EXPECT_THROW(static_cast(qdmi::DeviceRegistry(options)), + std::invalid_argument); +} + +TEST(DeviceRegistry, RejectsIncompleteAndUnsupportedEnabledDefinitions) { + qdmi::ConfigOptions options; + options.isolated = true; + options.inlineOverrides = nlohmann::json::parse(R"({ + "schema-version": 1, + "qdmi": {"devices": [{"id": "missing", "prefix": "P"}]} + })"); + EXPECT_THROW(static_cast(qdmi::DeviceRegistry(options)), + std::invalid_argument); + + options.inlineOverrides = nlohmann::json::parse(R"({ + "schema-version": 1, + "qdmi": {"devices": [{ + "id": "future", "library": "device", "prefix": "P", "abi": "qdmi-v2" + }]} + })"); + EXPECT_THROW(static_cast(qdmi::DeviceRegistry(options)), + std::invalid_argument); +} + +TEST(DeviceRegistry, ReportsInvalidExplicitJsonAndToml) { + const TemporaryDirectory directory; + qdmi::ConfigOptions options; + options.isolated = true; + options.explicitFile = directory.path() / "missing.json"; + EXPECT_THROW(static_cast(qdmi::DeviceRegistry(options)), + std::runtime_error); + + directory.write("invalid.json", "{"); + options.explicitFile = directory.path() / "invalid.json"; + EXPECT_THROW(static_cast(qdmi::DeviceRegistry(options)), + std::invalid_argument); + + directory.write("pyproject.toml", "[tool.mqt-core.qdmi\n"); + options.explicitFile.reset(); + options.baseDirectory = directory.path(); + EXPECT_THROW(static_cast(qdmi::DeviceRegistry(options)), + std::invalid_argument); +} + +TEST(DeviceRegistry, RegistersReplacesAndUnregistersDefinitions) { + qdmi::ConfigOptions options; + options.isolated = true; + options.baseDirectory = std::filesystem::temp_directory_path(); + qdmi::DeviceRegistry registry(options); + registry.registerDevice({.id = "example", .library = "one", .prefix = "ONE"}); + EXPECT_THROW(registry.registerDevice( + {.id = "example", .library = "two", .prefix = "TWO"}), + std::invalid_argument); + registry.registerDevice({.id = "example", .library = "two", .prefix = "TWO"}, + true); + EXPECT_EQ(registry.definitions().front().prefix, "TWO"); + EXPECT_TRUE(registry.unregisterDevice("example")); + EXPECT_FALSE(registry.unregisterDevice("example")); + EXPECT_THROW(registry.registerDevice({}), std::invalid_argument); +} + TEST(DeviceManager, LazilyOpensAndKeepsDeviceAlive) { qdmi::ConfigOptions options; options.isolated = true; @@ -169,7 +303,7 @@ TEST(DeviceManager, LazilyOpensAndKeepsDeviceAlive) { EXPECT_FALSE(device.getSites().empty()); } -TEST(DeviceManager, OpenAllIsolatesFailures) { +TEST(DeviceManager, OpensDefinitionsIndividually) { qdmi::ConfigOptions options; options.isolated = true; options.runtimeOverrides.emplace_back(qdmi::DeviceDefinition{ @@ -178,8 +312,46 @@ TEST(DeviceManager, OpenAllIsolatesFailures) { .id = "bad", .library = "does-not-exist", .prefix = "MISSING"}); qdmi::DeviceManager manager(options); - auto result = manager.openAll(); - EXPECT_TRUE(result.devices.contains("good")); - EXPECT_TRUE(result.errors.contains("bad")); + 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, SharesLibrariesButKeepsSessionsIndependent) { + qdmi::ConfigOptions options; + options.isolated = true; + options.runtimeOverrides = { + {.id = "first", .library = SC_DEVICE_LIBRARY, .prefix = "MQT_SC"}, + {.id = "second", .library = SC_DEVICE_LIBRARY, .prefix = "MQT_SC"}, + }; + qdmi::DeviceManager manager(options); + const auto first = manager.open("first"); + const auto second = manager.open("second"); + EXPECT_EQ(first.getName(), second.getName()); + EXPECT_EQ(first.getSites().size(), second.getSites().size()); +} + +TEST(DeviceManager, OpenDevicesOutliveRegistrationsAndManager) { + qdmi::ConfigOptions options; + options.isolated = true; + options.runtimeOverrides.emplace_back(qdmi::DeviceDefinition{ + .id = "persistent", .library = SC_DEVICE_LIBRARY, .prefix = "MQT_SC"}); + qdmi::Device device = [&options] { + qdmi::DeviceManager manager(options); + auto opened = manager.open("persistent"); + EXPECT_TRUE(manager.unregisterDevice("persistent")); + return opened; + }(); + EXPECT_EQ(device.getName(), "MQT SC Default QDMI Device"); +} + +TEST(DeviceManager, RejectsMalformedV1FunctionTable) { + qdmi::ConfigOptions options; + options.isolated = true; + options.runtimeOverrides.emplace_back(qdmi::DeviceDefinition{ + .id = "wrong-prefix", .library = SC_DEVICE_LIBRARY, .prefix = "MISSING"}); + qdmi::DeviceManager manager(options); + EXPECT_THROW(static_cast(manager.open("wrong-prefix")), + std::runtime_error); } } // namespace diff --git a/vendor/tomlplusplus/README.md b/vendor/tomlplusplus/README.md new file mode 100644 index 0000000000..6e5384047b --- /dev/null +++ b/vendor/tomlplusplus/README.md @@ -0,0 +1,10 @@ +# toml++ single-header distribution + +`toml.hpp` is the upstream single-header distribution of +[toml++](https://github.com/marzer/tomlplusplus), pinned to commit +`a43ad3787293f4a46b1d70c0924b5a25d10e79fc` from 24 May 2026. + +The unmodified file has SHA-256 digest +`e8a56bd3ae26d71c7414615f4d85bce5072077b2e1ca455289211170a541e026`. Its preamble +contains the MIT license, as prescribed for upstream's single-header +distribution. diff --git a/vendor/tomlplusplus/toml.hpp b/vendor/tomlplusplus/toml.hpp new file mode 100644 index 0000000000..caf87c4c21 --- /dev/null +++ b/vendor/tomlplusplus/toml.hpp @@ -0,0 +1,17899 @@ +//---------------------------------------------------------------------------------------------------------------------- +// +// toml++ v3.4.0 +// https://github.com/marzer/tomlplusplus +// SPDX-License-Identifier: MIT +// +//---------------------------------------------------------------------------------------------------------------------- +// +// - THIS FILE WAS ASSEMBLED FROM MULTIPLE HEADER FILES BY A SCRIPT - PLEASE DON'T EDIT IT DIRECTLY - +// +// If you wish to submit a contribution to toml++, hooray and thanks! Before you crack on, please be aware that this +// file was assembled from a number of smaller files by a python script, and code contributions should not be made +// against it directly. You should instead make your changes in the relevant source file(s). The file names of the files +// that contributed to this header can be found at the beginnings and ends of the corresponding sections of this file. +// +//---------------------------------------------------------------------------------------------------------------------- +// +// TOML Language Specifications: +// latest: https://github.com/toml-lang/toml/blob/master/README.md +// v1.0.0: https://toml.io/en/v1.0.0 +// v0.5.0: https://toml.io/en/v0.5.0 +// changelog: https://github.com/toml-lang/toml/blob/master/CHANGELOG.md +// +//---------------------------------------------------------------------------------------------------------------------- +// +// MIT License +// +// Copyright (c) Mark Gillard +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the +// Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +//---------------------------------------------------------------------------------------------------------------------- +#ifndef TOMLPLUSPLUS_HPP +#define TOMLPLUSPLUS_HPP + +#define INCLUDE_TOMLPLUSPLUS_H // old guard name used pre-v3 +#define TOMLPLUSPLUS_H // guard name used in the legacy toml.h + +//******** impl/preprocessor.hpp ************************************************************************************* + +#ifndef __cplusplus +#error toml++ is a C++ library. +#endif + +#ifndef TOML_CPP +#ifdef _MSVC_LANG +#if _MSVC_LANG > __cplusplus +#define TOML_CPP _MSVC_LANG +#endif +#endif +#ifndef TOML_CPP +#define TOML_CPP __cplusplus +#endif +#if TOML_CPP >= 202900L +#undef TOML_CPP +#define TOML_CPP 29 +#elif TOML_CPP >= 202600L +#undef TOML_CPP +#define TOML_CPP 26 +#elif TOML_CPP >= 202302L +#undef TOML_CPP +#define TOML_CPP 23 +#elif TOML_CPP >= 202002L +#undef TOML_CPP +#define TOML_CPP 20 +#elif TOML_CPP >= 201703L +#undef TOML_CPP +#define TOML_CPP 17 +#elif TOML_CPP >= 201402L +#undef TOML_CPP +#define TOML_CPP 14 +#elif TOML_CPP >= 201103L +#undef TOML_CPP +#define TOML_CPP 11 +#else +#undef TOML_CPP +#define TOML_CPP 0 +#endif +#endif + +#if !TOML_CPP +#error toml++ requires C++17 or higher. For a pre-C++11 TOML library see https://github.com/ToruNiina/Boost.toml +#elif TOML_CPP < 17 +#error toml++ requires C++17 or higher. For a C++11 TOML library see https://github.com/ToruNiina/toml11 +#endif + +#ifndef TOML_MAKE_VERSION +#define TOML_MAKE_VERSION(major, minor, patch) (((major)*10000) + ((minor)*100) + ((patch))) +#endif + +#ifndef TOML_INTELLISENSE +#ifdef __INTELLISENSE__ +#define TOML_INTELLISENSE 1 +#else +#define TOML_INTELLISENSE 0 +#endif +#endif + +#ifndef TOML_DOXYGEN +#if defined(DOXYGEN) || defined(__DOXYGEN) || defined(__DOXYGEN__) || defined(__doxygen__) || defined(__POXY__) \ + || defined(__poxy__) +#define TOML_DOXYGEN 1 +#else +#define TOML_DOXYGEN 0 +#endif +#endif + +#ifndef TOML_CLANG +#ifdef __clang__ +#define TOML_CLANG __clang_major__ +#else +#define TOML_CLANG 0 +#endif + +// special handling for apple clang; see: +// - https://github.com/marzer/tomlplusplus/issues/189 +// - https://en.wikipedia.org/wiki/Xcode +// - +// https://stackoverflow.com/questions/19387043/how-can-i-reliably-detect-the-version-of-clang-at-preprocessing-time +#if TOML_CLANG && defined(__apple_build_version__) +#undef TOML_CLANG +#define TOML_CLANG_VERSION TOML_MAKE_VERSION(__clang_major__, __clang_minor__, __clang_patchlevel__) +#if TOML_CLANG_VERSION >= TOML_MAKE_VERSION(15, 0, 0) +#define TOML_CLANG 16 +#elif TOML_CLANG_VERSION >= TOML_MAKE_VERSION(14, 3, 0) +#define TOML_CLANG 15 +#elif TOML_CLANG_VERSION >= TOML_MAKE_VERSION(14, 0, 0) +#define TOML_CLANG 14 +#elif TOML_CLANG_VERSION >= TOML_MAKE_VERSION(13, 1, 6) +#define TOML_CLANG 13 +#elif TOML_CLANG_VERSION >= TOML_MAKE_VERSION(13, 0, 0) +#define TOML_CLANG 12 +#elif TOML_CLANG_VERSION >= TOML_MAKE_VERSION(12, 0, 5) +#define TOML_CLANG 11 +#elif TOML_CLANG_VERSION >= TOML_MAKE_VERSION(12, 0, 0) +#define TOML_CLANG 10 +#elif TOML_CLANG_VERSION >= TOML_MAKE_VERSION(11, 0, 3) +#define TOML_CLANG 9 +#elif TOML_CLANG_VERSION >= TOML_MAKE_VERSION(11, 0, 0) +#define TOML_CLANG 8 +#elif TOML_CLANG_VERSION >= TOML_MAKE_VERSION(10, 0, 1) +#define TOML_CLANG 7 +#else +#define TOML_CLANG 6 // not strictly correct but doesn't matter below this +#endif +#undef TOML_CLANG_VERSION +#endif +#endif + +#ifndef TOML_ICC +#ifdef __INTEL_COMPILER +#define TOML_ICC __INTEL_COMPILER +#ifdef __ICL +#define TOML_ICC_CL TOML_ICC +#else +#define TOML_ICC_CL 0 +#endif +#else +#define TOML_ICC 0 +#define TOML_ICC_CL 0 +#endif +#endif + +#ifndef TOML_MSVC_LIKE +#ifdef _MSC_VER +#define TOML_MSVC_LIKE _MSC_VER +#else +#define TOML_MSVC_LIKE 0 +#endif +#endif + +#ifndef TOML_MSVC +#if TOML_MSVC_LIKE && !TOML_CLANG && !TOML_ICC +#define TOML_MSVC TOML_MSVC_LIKE +#else +#define TOML_MSVC 0 +#endif +#endif + +#ifndef TOML_GCC_LIKE +#ifdef __GNUC__ +#define TOML_GCC_LIKE __GNUC__ +#else +#define TOML_GCC_LIKE 0 +#endif +#endif + +#ifndef TOML_GCC +#if TOML_GCC_LIKE && !TOML_CLANG && !TOML_ICC +#define TOML_GCC TOML_GCC_LIKE +#else +#define TOML_GCC 0 +#endif +#endif + +#ifndef TOML_CUDA +#if defined(__CUDACC__) || defined(__CUDA_ARCH__) || defined(__CUDA_LIBDEVICE__) +#define TOML_CUDA 1 +#else +#define TOML_CUDA 0 +#endif +#endif + +#ifndef TOML_NVCC +#ifdef __NVCOMPILER_MAJOR__ +#define TOML_NVCC __NVCOMPILER_MAJOR__ +#else +#define TOML_NVCC 0 +#endif +#endif + +#ifndef TOML_ARCH_ITANIUM +#if defined(__ia64__) || defined(__ia64) || defined(_IA64) || defined(__IA64__) || defined(_M_IA64) +#define TOML_ARCH_ITANIUM 1 +#define TOML_ARCH_BITNESS 64 +#else +#define TOML_ARCH_ITANIUM 0 +#endif +#endif + +#ifndef TOML_ARCH_AMD64 +#if defined(__amd64__) || defined(__amd64) || defined(__x86_64__) || defined(__x86_64) || defined(_M_AMD64) +#define TOML_ARCH_AMD64 1 +#define TOML_ARCH_BITNESS 64 +#else +#define TOML_ARCH_AMD64 0 +#endif +#endif + +#ifndef TOML_ARCH_X86 +#if defined(__i386__) || defined(_M_IX86) +#define TOML_ARCH_X86 1 +#define TOML_ARCH_BITNESS 32 +#else +#define TOML_ARCH_X86 0 +#endif +#endif + +#ifndef TOML_ARCH_ARM +#if defined(__aarch64__) || defined(__ARM_ARCH_ISA_A64) || defined(_M_ARM64) || defined(__ARM_64BIT_STATE) \ + || defined(_M_ARM64EC) +#define TOML_ARCH_ARM32 0 +#define TOML_ARCH_ARM64 1 +#define TOML_ARCH_ARM 1 +#define TOML_ARCH_BITNESS 64 +#elif defined(__arm__) || defined(_M_ARM) || defined(__ARM_32BIT_STATE) +#define TOML_ARCH_ARM32 1 +#define TOML_ARCH_ARM64 0 +#define TOML_ARCH_ARM 1 +#define TOML_ARCH_BITNESS 32 +#else +#define TOML_ARCH_ARM32 0 +#define TOML_ARCH_ARM64 0 +#define TOML_ARCH_ARM 0 +#endif +#endif + +#ifndef TOML_ARCH_BITNESS +#define TOML_ARCH_BITNESS 0 +#endif + +#ifndef TOML_ARCH_X64 +#if TOML_ARCH_BITNESS == 64 +#define TOML_ARCH_X64 1 +#else +#define TOML_ARCH_X64 0 +#endif +#endif + +#if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__) || defined(__CYGWIN__) +#define TOML_WINDOWS 1 +#else +#define TOML_WINDOWS 0 +#endif + +#ifdef __unix__ +#define TOML_UNIX 1 +#else +#define TOML_UNIX 0 +#endif + +#ifdef __linux__ +#define TOML_LINUX 1 +#else +#define TOML_LINUX 0 +#endif + +// TOML_HAS_INCLUDE +#ifndef TOML_HAS_INCLUDE +#ifdef __has_include +#define TOML_HAS_INCLUDE(header) __has_include(header) +#else +#define TOML_HAS_INCLUDE(header) 0 +#endif +#endif + +// TOML_HAS_BUILTIN +#ifndef TOML_HAS_BUILTIN +#ifdef __has_builtin +#define TOML_HAS_BUILTIN(name) __has_builtin(name) +#else +#define TOML_HAS_BUILTIN(name) 0 +#endif +#endif + +// TOML_HAS_FEATURE +#ifndef TOML_HAS_FEATURE +#ifdef __has_feature +#define TOML_HAS_FEATURE(name) __has_feature(name) +#else +#define TOML_HAS_FEATURE(name) 0 +#endif +#endif + +// TOML_HAS_ATTR +#ifndef TOML_HAS_ATTR +#ifdef __has_attribute +#define TOML_HAS_ATTR(attr) __has_attribute(attr) +#else +#define TOML_HAS_ATTR(attr) 0 +#endif +#endif + +// TOML_HAS_CPP_ATTR +#ifndef TOML_HAS_CPP_ATTR +#ifdef __has_cpp_attribute +#define TOML_HAS_CPP_ATTR(attr) __has_cpp_attribute(attr) +#else +#define TOML_HAS_CPP_ATTR(attr) 0 +#endif +#endif + +// TOML_ATTR (gnu attributes) +#ifndef TOML_ATTR +#if TOML_CLANG || TOML_GCC_LIKE +#define TOML_ATTR(...) __attribute__((__VA_ARGS__)) +#else +#define TOML_ATTR(...) +#endif +#endif + +// TOML_DECLSPEC (msvc attributes) +#ifndef TOML_DECLSPEC +#if TOML_MSVC_LIKE +#define TOML_DECLSPEC(...) __declspec(__VA_ARGS__) +#else +#define TOML_DECLSPEC(...) +#endif +#endif + +// TOML_COMPILER_HAS_EXCEPTIONS +#ifndef TOML_COMPILER_HAS_EXCEPTIONS +#if defined(__EXCEPTIONS) || defined(_CPPUNWIND) || defined(__cpp_exceptions) +#define TOML_COMPILER_HAS_EXCEPTIONS 1 +#else +#define TOML_COMPILER_HAS_EXCEPTIONS 0 +#endif +#endif + +// TOML_COMPILER_HAS_RTTI +#ifndef TOML_COMPILER_HAS_RTTI +#if defined(_CPPRTTI) || defined(__GXX_RTTI) || TOML_HAS_FEATURE(cxx_rtti) +#define TOML_COMPILER_HAS_RTTI 1 +#else +#define TOML_COMPILER_HAS_RTTI 0 +#endif +#endif + +// TOML_CONCAT +#define TOML_CONCAT_1(x, y) x##y +#define TOML_CONCAT(x, y) TOML_CONCAT_1(x, y) + +// TOML_MAKE_STRING +#define TOML_MAKE_STRING_1(s) #s +#define TOML_MAKE_STRING(s) TOML_MAKE_STRING_1(s) + +// TOML_PRAGMA_XXXX (compiler-specific pragmas) +#if TOML_CLANG +#define TOML_PRAGMA_CLANG(decl) _Pragma(TOML_MAKE_STRING(clang decl)) +#else +#define TOML_PRAGMA_CLANG(decl) +#endif +#if TOML_CLANG >= 8 +#define TOML_PRAGMA_CLANG_GE_8(decl) TOML_PRAGMA_CLANG(decl) +#else +#define TOML_PRAGMA_CLANG_GE_8(decl) +#endif +#if TOML_CLANG >= 9 +#define TOML_PRAGMA_CLANG_GE_9(decl) TOML_PRAGMA_CLANG(decl) +#else +#define TOML_PRAGMA_CLANG_GE_9(decl) +#endif +#if TOML_CLANG >= 10 +#define TOML_PRAGMA_CLANG_GE_10(decl) TOML_PRAGMA_CLANG(decl) +#else +#define TOML_PRAGMA_CLANG_GE_10(decl) +#endif +#if TOML_CLANG >= 11 +#define TOML_PRAGMA_CLANG_GE_11(decl) TOML_PRAGMA_CLANG(decl) +#else +#define TOML_PRAGMA_CLANG_GE_11(decl) +#endif +#if TOML_GCC +#define TOML_PRAGMA_GCC(decl) _Pragma(TOML_MAKE_STRING(GCC decl)) +#else +#define TOML_PRAGMA_GCC(decl) +#endif +#if TOML_MSVC +#define TOML_PRAGMA_MSVC(...) __pragma(__VA_ARGS__) +#else +#define TOML_PRAGMA_MSVC(...) +#endif +#if TOML_ICC +#define TOML_PRAGMA_ICC(...) __pragma(__VA_ARGS__) +#else +#define TOML_PRAGMA_ICC(...) +#endif + +// TOML_ALWAYS_INLINE +#ifndef TOML_ALWAYS_INLINE +#ifdef _MSC_VER +#define TOML_ALWAYS_INLINE __forceinline +#elif TOML_GCC || TOML_CLANG || TOML_HAS_ATTR(__always_inline__) +#define TOML_ALWAYS_INLINE \ + TOML_ATTR(__always_inline__) \ + inline +#else +#define TOML_ALWAYS_INLINE inline +#endif +#endif + +// TOML_NEVER_INLINE +#ifndef TOML_NEVER_INLINE +#ifdef _MSC_VER +#define TOML_NEVER_INLINE TOML_DECLSPEC(noinline) +#elif TOML_CUDA // https://gitlab.gnome.org/GNOME/glib/-/issues/2555 +#define TOML_NEVER_INLINE TOML_ATTR(noinline) +#else +#if TOML_GCC || TOML_CLANG || TOML_HAS_ATTR(__noinline__) +#define TOML_NEVER_INLINE TOML_ATTR(__noinline__) +#endif +#endif +#ifndef TOML_NEVER_INLINE +#define TOML_NEVER_INLINE +#endif +#endif + +// MSVC attributes +#ifndef TOML_ABSTRACT_INTERFACE +#define TOML_ABSTRACT_INTERFACE TOML_DECLSPEC(novtable) +#endif +#ifndef TOML_EMPTY_BASES +#define TOML_EMPTY_BASES TOML_DECLSPEC(empty_bases) +#endif + +// TOML_TRIVIAL_ABI +#ifndef TOML_TRIVIAL_ABI +#if TOML_CLANG || TOML_HAS_ATTR(__trivial_abi__) +#define TOML_TRIVIAL_ABI TOML_ATTR(__trivial_abi__) +#else +#define TOML_TRIVIAL_ABI +#endif +#endif + +// TOML_NODISCARD +#ifndef TOML_NODISCARD +#if TOML_CPP >= 17 && TOML_HAS_CPP_ATTR(nodiscard) >= 201603 +#define TOML_NODISCARD [[nodiscard]] +#elif TOML_CLANG || TOML_GCC || TOML_HAS_ATTR(__warn_unused_result__) +#define TOML_NODISCARD TOML_ATTR(__warn_unused_result__) +#else +#define TOML_NODISCARD +#endif +#endif + +// TOML_NODISCARD_CTOR +#ifndef TOML_NODISCARD_CTOR +#if TOML_CPP >= 17 && TOML_HAS_CPP_ATTR(nodiscard) >= 201907 +#define TOML_NODISCARD_CTOR [[nodiscard]] +#else +#define TOML_NODISCARD_CTOR +#endif +#endif + +// pure + const +#ifndef TOML_PURE +#ifdef NDEBUG +#define TOML_PURE \ + TOML_DECLSPEC(noalias) \ + TOML_ATTR(pure) +#else +#define TOML_PURE +#endif +#endif +#ifndef TOML_CONST +#ifdef NDEBUG +#define TOML_CONST \ + TOML_DECLSPEC(noalias) \ + TOML_ATTR(const) +#else +#define TOML_CONST +#endif +#endif +#ifndef TOML_INLINE_GETTER +#define TOML_INLINE_GETTER \ + TOML_NODISCARD \ + TOML_ALWAYS_INLINE +#endif +#ifndef TOML_PURE_GETTER +#define TOML_PURE_GETTER \ + TOML_NODISCARD \ + TOML_PURE +#endif +#ifndef TOML_PURE_INLINE_GETTER +#define TOML_PURE_INLINE_GETTER \ + TOML_NODISCARD \ + TOML_ALWAYS_INLINE \ + TOML_PURE +#endif +#ifndef TOML_CONST_GETTER +#define TOML_CONST_GETTER \ + TOML_NODISCARD \ + TOML_CONST +#endif +#ifndef TOML_CONST_INLINE_GETTER +#define TOML_CONST_INLINE_GETTER \ + TOML_NODISCARD \ + TOML_ALWAYS_INLINE \ + TOML_CONST +#endif + +// TOML_ASSUME +#ifndef TOML_ASSUME +#ifdef _MSC_VER +#define TOML_ASSUME(expr) __assume(expr) +#elif TOML_ICC || TOML_CLANG || TOML_HAS_BUILTIN(__builtin_assume) +#define TOML_ASSUME(expr) __builtin_assume(expr) +#elif TOML_HAS_CPP_ATTR(assume) >= 202207 +#define TOML_ASSUME(expr) [[assume(expr)]] +#elif TOML_HAS_ATTR(__assume__) +#define TOML_ASSUME(expr) __attribute__((__assume__(expr))) +#else +#define TOML_ASSUME(expr) static_cast(0) +#endif +#endif + +// TOML_UNREACHABLE +#ifndef TOML_UNREACHABLE +#ifdef _MSC_VER +#define TOML_UNREACHABLE __assume(0) +#elif TOML_ICC || TOML_CLANG || TOML_GCC || TOML_HAS_BUILTIN(__builtin_unreachable) +#define TOML_UNREACHABLE __builtin_unreachable() +#else +#define TOML_UNREACHABLE static_cast(0) +#endif +#endif + +// TOML_LIKELY +#if TOML_CPP >= 20 && TOML_HAS_CPP_ATTR(likely) >= 201803 +#define TOML_LIKELY(...) (__VA_ARGS__) [[likely]] +#define TOML_LIKELY_CASE [[likely]] +#elif TOML_GCC || TOML_CLANG || TOML_HAS_BUILTIN(__builtin_expect) +#define TOML_LIKELY(...) (__builtin_expect(!!(__VA_ARGS__), 1)) +#else +#define TOML_LIKELY(...) (__VA_ARGS__) +#endif +#ifndef TOML_LIKELY_CASE +#define TOML_LIKELY_CASE +#endif + +// TOML_UNLIKELY +#if TOML_CPP >= 20 && TOML_HAS_CPP_ATTR(unlikely) >= 201803 +#define TOML_UNLIKELY(...) (__VA_ARGS__) [[unlikely]] +#define TOML_UNLIKELY_CASE [[unlikely]] +#elif TOML_GCC || TOML_CLANG || TOML_HAS_BUILTIN(__builtin_expect) +#define TOML_UNLIKELY(...) (__builtin_expect(!!(__VA_ARGS__), 0)) +#else +#define TOML_UNLIKELY(...) (__VA_ARGS__) +#endif +#ifndef TOML_UNLIKELY_CASE +#define TOML_UNLIKELY_CASE +#endif + +// TOML_FLAGS_ENUM +#if TOML_CLANG || TOML_HAS_ATTR(flag_enum) +#define TOML_FLAGS_ENUM __attribute__((flag_enum)) +#else +#define TOML_FLAGS_ENUM +#endif + +// TOML_OPEN_ENUM + TOML_CLOSED_ENUM +#if TOML_CLANG || TOML_HAS_ATTR(enum_extensibility) +#define TOML_OPEN_ENUM __attribute__((enum_extensibility(open))) +#define TOML_CLOSED_ENUM __attribute__((enum_extensibility(closed))) +#else +#define TOML_OPEN_ENUM +#define TOML_CLOSED_ENUM +#endif + +// TOML_OPEN_FLAGS_ENUM + TOML_CLOSED_FLAGS_ENUM +#define TOML_OPEN_FLAGS_ENUM TOML_OPEN_ENUM TOML_FLAGS_ENUM +#define TOML_CLOSED_FLAGS_ENUM TOML_CLOSED_ENUM TOML_FLAGS_ENUM + +// TOML_MAKE_FLAGS +#define TOML_MAKE_FLAGS_2(T, op, linkage) \ + TOML_CONST_INLINE_GETTER \ + linkage constexpr T operator op(T lhs, T rhs) noexcept \ + { \ + using under = std::underlying_type_t; \ + return static_cast(static_cast(lhs) op static_cast(rhs)); \ + } \ + \ + linkage constexpr T& operator TOML_CONCAT(op, =)(T & lhs, T rhs) noexcept \ + { \ + return lhs = (lhs op rhs); \ + } \ + \ + static_assert(true) +#define TOML_MAKE_FLAGS_1(T, linkage) \ + static_assert(std::is_enum_v); \ + \ + TOML_MAKE_FLAGS_2(T, &, linkage); \ + TOML_MAKE_FLAGS_2(T, |, linkage); \ + TOML_MAKE_FLAGS_2(T, ^, linkage); \ + \ + TOML_CONST_INLINE_GETTER \ + linkage constexpr T operator~(T val) noexcept \ + { \ + using under = std::underlying_type_t; \ + return static_cast(~static_cast(val)); \ + } \ + \ + TOML_CONST_INLINE_GETTER \ + linkage constexpr bool operator!(T val) noexcept \ + { \ + using under = std::underlying_type_t; \ + return !static_cast(val); \ + } \ + \ + static_assert(true) +#define TOML_MAKE_FLAGS(T) TOML_MAKE_FLAGS_1(T, ) + +#define TOML_UNUSED(...) static_cast(__VA_ARGS__) + +#define TOML_DELETE_DEFAULTS(T) \ + T(const T&) = delete; \ + T(T&&) = delete; \ + T& operator=(const T&) = delete; \ + T& operator=(T&&) = delete + +#define TOML_ASYMMETRICAL_EQUALITY_OPS(LHS, RHS, ...) \ + __VA_ARGS__ TOML_NODISCARD \ + friend bool operator==(RHS rhs, LHS lhs) noexcept \ + { \ + return lhs == rhs; \ + } \ + __VA_ARGS__ TOML_NODISCARD \ + friend bool operator!=(LHS lhs, RHS rhs) noexcept \ + { \ + return !(lhs == rhs); \ + } \ + __VA_ARGS__ TOML_NODISCARD \ + friend bool operator!=(RHS rhs, LHS lhs) noexcept \ + { \ + return !(lhs == rhs); \ + } \ + static_assert(true) + +#define TOML_EVAL_BOOL_1(T, F) T +#define TOML_EVAL_BOOL_0(T, F) F + +#if !defined(__POXY__) && !defined(POXY_IMPLEMENTATION_DETAIL) +#define POXY_IMPLEMENTATION_DETAIL(...) __VA_ARGS__ +#endif + +// COMPILER-SPECIFIC WARNING MANAGEMENT + +#if TOML_CLANG + +#define TOML_PUSH_WARNINGS \ + TOML_PRAGMA_CLANG(diagnostic push) \ + TOML_PRAGMA_CLANG(diagnostic ignored "-Wunknown-warning-option") \ + static_assert(true) + +#define TOML_DISABLE_SWITCH_WARNINGS \ + TOML_PRAGMA_CLANG(diagnostic ignored "-Wswitch") \ + static_assert(true) + +#define TOML_DISABLE_ARITHMETIC_WARNINGS \ + TOML_PRAGMA_CLANG_GE_10(diagnostic ignored "-Wimplicit-int-float-conversion") \ + TOML_PRAGMA_CLANG(diagnostic ignored "-Wfloat-equal") \ + TOML_PRAGMA_CLANG(diagnostic ignored "-Wdouble-promotion") \ + TOML_PRAGMA_CLANG(diagnostic ignored "-Wchar-subscripts") \ + TOML_PRAGMA_CLANG(diagnostic ignored "-Wshift-sign-overflow") \ + static_assert(true) + +#define TOML_DISABLE_SPAM_WARNINGS \ + TOML_PRAGMA_CLANG_GE_8(diagnostic ignored "-Wdefaulted-function-deleted") \ + TOML_PRAGMA_CLANG_GE_9(diagnostic ignored "-Wctad-maybe-unsupported") \ + TOML_PRAGMA_CLANG_GE_10(diagnostic ignored "-Wzero-as-null-pointer-constant") \ + TOML_PRAGMA_CLANG_GE_11(diagnostic ignored "-Wsuggest-destructor-override") \ + TOML_PRAGMA_CLANG(diagnostic ignored "-Wweak-vtables") \ + TOML_PRAGMA_CLANG(diagnostic ignored "-Wweak-template-vtables") \ + TOML_PRAGMA_CLANG(diagnostic ignored "-Wdouble-promotion") \ + TOML_PRAGMA_CLANG(diagnostic ignored "-Wchar-subscripts") \ + TOML_PRAGMA_CLANG(diagnostic ignored "-Wmissing-field-initializers") \ + TOML_PRAGMA_CLANG(diagnostic ignored "-Wpadded") \ + static_assert(true) + +#define TOML_POP_WARNINGS \ + TOML_PRAGMA_CLANG(diagnostic pop) \ + static_assert(true) + +#define TOML_DISABLE_WARNINGS \ + TOML_PRAGMA_CLANG(diagnostic push) \ + TOML_PRAGMA_CLANG(diagnostic ignored "-Weverything") \ + static_assert(true, "") + +#define TOML_ENABLE_WARNINGS \ + TOML_PRAGMA_CLANG(diagnostic pop) \ + static_assert(true) + +#define TOML_SIMPLE_STATIC_ASSERT_MESSAGES 1 + +#elif TOML_MSVC + +#define TOML_PUSH_WARNINGS \ + __pragma(warning(push)) \ + static_assert(true) + +#if TOML_HAS_INCLUDE() +#pragma warning(push, 0) +#include +#pragma warning(pop) +#define TOML_DISABLE_CODE_ANALYSIS_WARNINGS \ + __pragma(warning(disable : ALL_CODE_ANALYSIS_WARNINGS)) \ + static_assert(true) +#else +#define TOML_DISABLE_CODE_ANALYSIS_WARNINGS static_assert(true) +#endif + +#define TOML_DISABLE_SWITCH_WARNINGS \ + __pragma(warning(disable : 4061)) \ + __pragma(warning(disable : 4062)) \ + __pragma(warning(disable : 4063)) \ + __pragma(warning(disable : 5262)) /* switch-case implicit fallthrough (false-positive) */ \ + __pragma(warning(disable : 26819)) /* cg: unannotated fallthrough */ \ + static_assert(true) + +#define TOML_DISABLE_SPAM_WARNINGS \ + __pragma(warning(disable : 4127)) /* conditional expr is constant */ \ + __pragma(warning(disable : 4324)) /* structure was padded due to alignment specifier */ \ + __pragma(warning(disable : 4348)) \ + __pragma(warning(disable : 4464)) /* relative include path contains '..' */ \ + __pragma(warning(disable : 4505)) /* unreferenced local function removed */ \ + __pragma(warning(disable : 4514)) /* unreferenced inline function has been removed */ \ + __pragma(warning(disable : 4582)) /* constructor is not implicitly called */ \ + __pragma(warning(disable : 4619)) /* there is no warning number 'XXXX' */ \ + __pragma(warning(disable : 4623)) /* default constructor was implicitly defined as deleted */ \ + __pragma(warning(disable : 4625)) /* copy constructor was implicitly defined as deleted */ \ + __pragma(warning(disable : 4626)) /* assignment operator was implicitly defined as deleted */ \ + __pragma(warning(disable : 4710)) /* function not inlined */ \ + __pragma(warning(disable : 4711)) /* function selected for automatic expansion */ \ + __pragma(warning(disable : 4820)) /* N bytes padding added */ \ + __pragma(warning(disable : 5026)) /* move constructor was implicitly defined as deleted */ \ + __pragma(warning(disable : 5027)) /* move assignment operator was implicitly defined as deleted */ \ + __pragma(warning(disable : 5039)) /* potentially throwing function passed to 'extern "C"' function */ \ + __pragma(warning(disable : 5045)) /* Compiler will insert Spectre mitigation */ \ + __pragma(warning(disable : 5264)) /* const variable is not used (false-positive) */ \ + __pragma(warning(disable : 26451)) \ + __pragma(warning(disable : 26490)) \ + __pragma(warning(disable : 26495)) \ + __pragma(warning(disable : 26812)) \ + __pragma(warning(disable : 26819)) \ + static_assert(true) + +#define TOML_DISABLE_ARITHMETIC_WARNINGS \ + __pragma(warning(disable : 4365)) /* argument signed/unsigned mismatch */ \ + __pragma(warning(disable : 4738)) /* storing 32-bit float result in memory */ \ + __pragma(warning(disable : 5219)) /* implicit conversion from integral to float */ \ + static_assert(true) + +#define TOML_POP_WARNINGS \ + __pragma(warning(pop)) \ + static_assert(true) + +#define TOML_DISABLE_WARNINGS \ + __pragma(warning(push, 0)) \ + __pragma(warning(disable : 4348)) \ + __pragma(warning(disable : 4668)) \ + __pragma(warning(disable : 5105)) \ + __pragma(warning(disable : 5264)) \ + TOML_DISABLE_CODE_ANALYSIS_WARNINGS; \ + TOML_DISABLE_SWITCH_WARNINGS; \ + TOML_DISABLE_SPAM_WARNINGS; \ + TOML_DISABLE_ARITHMETIC_WARNINGS; \ + static_assert(true) + +#define TOML_ENABLE_WARNINGS TOML_POP_WARNINGS + +#elif TOML_ICC + +#define TOML_PUSH_WARNINGS \ + __pragma(warning(push)) \ + static_assert(true) + +#define TOML_DISABLE_SPAM_WARNINGS \ + __pragma(warning(disable : 82)) /* storage class is not first */ \ + __pragma(warning(disable : 111)) /* statement unreachable (false-positive) */ \ + __pragma(warning(disable : 869)) /* unreferenced parameter */ \ + __pragma(warning(disable : 1011)) /* missing return (false-positive) */ \ + __pragma(warning(disable : 2261)) /* assume expr side-effects discarded */ \ + static_assert(true) + +#define TOML_POP_WARNINGS \ + __pragma(warning(pop)) \ + static_assert(true) + +#define TOML_DISABLE_WARNINGS \ + __pragma(warning(push, 0)) \ + TOML_DISABLE_SPAM_WARNINGS + +#define TOML_ENABLE_WARNINGS \ + __pragma(warning(pop)) \ + static_assert(true) + +#elif TOML_GCC + +#define TOML_PUSH_WARNINGS \ + TOML_PRAGMA_GCC(diagnostic push) \ + static_assert(true) + +#define TOML_DISABLE_SWITCH_WARNINGS \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wswitch") \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wswitch-enum") \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wswitch-default") \ + static_assert(true) + +#define TOML_DISABLE_ARITHMETIC_WARNINGS \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wfloat-equal") \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wsign-conversion") \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wchar-subscripts") \ + static_assert(true) + +#define TOML_DISABLE_SUGGEST_ATTR_WARNINGS \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wsuggest-attribute=const") \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wsuggest-attribute=pure") \ + static_assert(true) + +#define TOML_DISABLE_SPAM_WARNINGS \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wpadded") \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wcast-align") \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wcomment") \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wtype-limits") \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wuseless-cast") \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wchar-subscripts") \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wsubobject-linkage") \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wmissing-field-initializers") \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wmaybe-uninitialized") \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wnoexcept") \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wnull-dereference") \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wduplicated-branches") \ + static_assert(true) + +#define TOML_POP_WARNINGS \ + TOML_PRAGMA_GCC(diagnostic pop) \ + static_assert(true) + +#define TOML_DISABLE_WARNINGS \ + TOML_PRAGMA_GCC(diagnostic push) \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wall") \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wextra") \ + TOML_PRAGMA_GCC(diagnostic ignored "-Wpedantic") \ + TOML_DISABLE_SWITCH_WARNINGS; \ + TOML_DISABLE_ARITHMETIC_WARNINGS; \ + TOML_DISABLE_SUGGEST_ATTR_WARNINGS; \ + TOML_DISABLE_SPAM_WARNINGS; \ + static_assert(true) + +#define TOML_ENABLE_WARNINGS \ + TOML_PRAGMA_GCC(diagnostic pop) \ + static_assert(true) + +#endif + +#ifndef TOML_PUSH_WARNINGS +#define TOML_PUSH_WARNINGS static_assert(true) +#endif +#ifndef TOML_DISABLE_CODE_ANALYSIS_WARNINGS +#define TOML_DISABLE_CODE_ANALYSIS_WARNINGS static_assert(true) +#endif +#ifndef TOML_DISABLE_SWITCH_WARNINGS +#define TOML_DISABLE_SWITCH_WARNINGS static_assert(true) +#endif +#ifndef TOML_DISABLE_SUGGEST_ATTR_WARNINGS +#define TOML_DISABLE_SUGGEST_ATTR_WARNINGS static_assert(true) +#endif +#ifndef TOML_DISABLE_SPAM_WARNINGS +#define TOML_DISABLE_SPAM_WARNINGS static_assert(true) +#endif +#ifndef TOML_DISABLE_ARITHMETIC_WARNINGS +#define TOML_DISABLE_ARITHMETIC_WARNINGS static_assert(true) +#endif +#ifndef TOML_POP_WARNINGS +#define TOML_POP_WARNINGS static_assert(true) +#endif +#ifndef TOML_DISABLE_WARNINGS +#define TOML_DISABLE_WARNINGS static_assert(true) +#endif +#ifndef TOML_ENABLE_WARNINGS +#define TOML_ENABLE_WARNINGS static_assert(true) +#endif +#ifndef TOML_SIMPLE_STATIC_ASSERT_MESSAGES +#define TOML_SIMPLE_STATIC_ASSERT_MESSAGES 0 +#endif + +#ifdef TOML_CONFIG_HEADER +#include TOML_CONFIG_HEADER +#endif + +// is the library being built as a shared lib/dll using meson and friends? +#ifndef TOML_SHARED_LIB +#define TOML_SHARED_LIB 0 +#endif + +// header-only mode +#if !defined(TOML_HEADER_ONLY) && defined(TOML_ALL_INLINE) // was TOML_ALL_INLINE pre-2.0 +#define TOML_HEADER_ONLY TOML_ALL_INLINE +#endif +#if !defined(TOML_HEADER_ONLY) || (defined(TOML_HEADER_ONLY) && TOML_HEADER_ONLY) || TOML_INTELLISENSE +#undef TOML_HEADER_ONLY +#define TOML_HEADER_ONLY 1 +#endif +#if TOML_DOXYGEN || TOML_SHARED_LIB +#undef TOML_HEADER_ONLY +#define TOML_HEADER_ONLY 0 +#endif + +// internal implementation switch +#if defined(TOML_IMPLEMENTATION) || TOML_HEADER_ONLY +#undef TOML_IMPLEMENTATION +#define TOML_IMPLEMENTATION 1 +#else +#define TOML_IMPLEMENTATION 0 +#endif + +// dll/shared lib function exports (legacy - TOML_API was the old name for this setting) +#if !defined(TOML_EXPORTED_MEMBER_FUNCTION) && !defined(TOML_EXPORTED_STATIC_FUNCTION) \ + && !defined(TOML_EXPORTED_FREE_FUNCTION) && !defined(TOML_EXPORTED_CLASS) && defined(TOML_API) +#define TOML_EXPORTED_MEMBER_FUNCTION TOML_API +#define TOML_EXPORTED_STATIC_FUNCTION TOML_API +#define TOML_EXPORTED_FREE_FUNCTION TOML_API +#endif + +// dll/shared lib exports +#if TOML_SHARED_LIB +#undef TOML_API +#undef TOML_EXPORTED_CLASS +#undef TOML_EXPORTED_MEMBER_FUNCTION +#undef TOML_EXPORTED_STATIC_FUNCTION +#undef TOML_EXPORTED_FREE_FUNCTION +#if TOML_WINDOWS +#if TOML_IMPLEMENTATION +#define TOML_EXPORTED_CLASS __declspec(dllexport) +#define TOML_EXPORTED_FREE_FUNCTION __declspec(dllexport) +#else +#define TOML_EXPORTED_CLASS __declspec(dllimport) +#define TOML_EXPORTED_FREE_FUNCTION __declspec(dllimport) +#endif +#ifndef TOML_CALLCONV +#define TOML_CALLCONV __cdecl +#endif +#elif defined(__GNUC__) && __GNUC__ >= 4 +#define TOML_EXPORTED_CLASS __attribute__((visibility("default"))) +#define TOML_EXPORTED_MEMBER_FUNCTION __attribute__((visibility("default"))) +#define TOML_EXPORTED_STATIC_FUNCTION __attribute__((visibility("default"))) +#define TOML_EXPORTED_FREE_FUNCTION __attribute__((visibility("default"))) +#endif +#endif +#ifndef TOML_EXPORTED_CLASS +#define TOML_EXPORTED_CLASS +#endif +#ifndef TOML_EXPORTED_MEMBER_FUNCTION +#define TOML_EXPORTED_MEMBER_FUNCTION +#endif +#ifndef TOML_EXPORTED_STATIC_FUNCTION +#define TOML_EXPORTED_STATIC_FUNCTION +#endif +#ifndef TOML_EXPORTED_FREE_FUNCTION +#define TOML_EXPORTED_FREE_FUNCTION +#endif + +// experimental language features +#if !defined(TOML_ENABLE_UNRELEASED_FEATURES) && defined(TOML_UNRELEASED_FEATURES) // was TOML_UNRELEASED_FEATURES + // pre-3.0 +#define TOML_ENABLE_UNRELEASED_FEATURES TOML_UNRELEASED_FEATURES +#endif +#if (defined(TOML_ENABLE_UNRELEASED_FEATURES) && TOML_ENABLE_UNRELEASED_FEATURES) || TOML_INTELLISENSE +#undef TOML_ENABLE_UNRELEASED_FEATURES +#define TOML_ENABLE_UNRELEASED_FEATURES 1 +#endif +#ifndef TOML_ENABLE_UNRELEASED_FEATURES +#define TOML_ENABLE_UNRELEASED_FEATURES 0 +#endif + +// parser +#if !defined(TOML_ENABLE_PARSER) && defined(TOML_PARSER) // was TOML_PARSER pre-3.0 +#define TOML_ENABLE_PARSER TOML_PARSER +#endif +#if !defined(TOML_ENABLE_PARSER) || (defined(TOML_ENABLE_PARSER) && TOML_ENABLE_PARSER) || TOML_INTELLISENSE +#undef TOML_ENABLE_PARSER +#define TOML_ENABLE_PARSER 1 +#endif + +// formatters +#if !defined(TOML_ENABLE_FORMATTERS) || (defined(TOML_ENABLE_FORMATTERS) && TOML_ENABLE_FORMATTERS) || TOML_INTELLISENSE +#undef TOML_ENABLE_FORMATTERS +#define TOML_ENABLE_FORMATTERS 1 +#endif + +// SIMD +#if !defined(TOML_ENABLE_SIMD) || (defined(TOML_ENABLE_SIMD) && TOML_ENABLE_SIMD) || TOML_INTELLISENSE +#undef TOML_ENABLE_SIMD +#define TOML_ENABLE_SIMD 1 +#endif + +// windows compat +#if !defined(TOML_ENABLE_WINDOWS_COMPAT) && defined(TOML_WINDOWS_COMPAT) // was TOML_WINDOWS_COMPAT pre-3.0 +#define TOML_ENABLE_WINDOWS_COMPAT TOML_WINDOWS_COMPAT +#endif +#if !defined(TOML_ENABLE_WINDOWS_COMPAT) || (defined(TOML_ENABLE_WINDOWS_COMPAT) && TOML_ENABLE_WINDOWS_COMPAT) \ + || TOML_INTELLISENSE +#undef TOML_ENABLE_WINDOWS_COMPAT +#define TOML_ENABLE_WINDOWS_COMPAT 1 +#endif + +#if !TOML_WINDOWS +#undef TOML_ENABLE_WINDOWS_COMPAT +#define TOML_ENABLE_WINDOWS_COMPAT 0 +#endif + +#ifndef TOML_INCLUDE_WINDOWS_H +#define TOML_INCLUDE_WINDOWS_H 0 +#endif + +// custom optional +#ifdef TOML_OPTIONAL_TYPE +#define TOML_HAS_CUSTOM_OPTIONAL_TYPE 1 +#else +#define TOML_HAS_CUSTOM_OPTIONAL_TYPE 0 +#endif + +// exceptions (library use) +#if TOML_COMPILER_HAS_EXCEPTIONS +#if !defined(TOML_EXCEPTIONS) || (defined(TOML_EXCEPTIONS) && TOML_EXCEPTIONS) +#undef TOML_EXCEPTIONS +#define TOML_EXCEPTIONS 1 +#endif +#else +#if defined(TOML_EXCEPTIONS) && TOML_EXCEPTIONS +#error TOML_EXCEPTIONS was explicitly enabled but exceptions are disabled/unsupported by the compiler. +#endif +#undef TOML_EXCEPTIONS +#define TOML_EXCEPTIONS 0 +#endif + +// calling convention for static/free/friend functions +#ifndef TOML_CALLCONV +#define TOML_CALLCONV +#endif + +#ifndef TOML_UNDEF_MACROS +#define TOML_UNDEF_MACROS 1 +#endif + +#ifndef TOML_MAX_NESTED_VALUES +#define TOML_MAX_NESTED_VALUES 128 +// this refers to the depth of nested values, e.g. inline tables and arrays. +// 128 is very generous; real TOML files rarely exceed single-digit nesting. +// keep this value low enough to avoid stack overflows in sanitizer-instrumented builds +// where each recursion cycle may consume ~3KB of stack. +#endif + +#ifndef TOML_MAX_DOTTED_KEYS_DEPTH +#define TOML_MAX_DOTTED_KEYS_DEPTH 1024 +#endif + +#ifdef TOML_CHAR_8_STRINGS +#if TOML_CHAR_8_STRINGS +#error TOML_CHAR_8_STRINGS was removed in toml++ 2.0.0; all value setters and getters now work with char8_t strings implicitly. +#endif +#endif + +#ifdef TOML_LARGE_FILES +#if !TOML_LARGE_FILES +#error Support for !TOML_LARGE_FILES (i.e. 'small files') was removed in toml++ 3.0.0. +#endif +#endif + +#ifndef TOML_LIFETIME_HOOKS +#define TOML_LIFETIME_HOOKS 0 +#endif + +#ifdef NDEBUG +#undef TOML_ASSERT +#define TOML_ASSERT(expr) static_assert(true) +#endif +#ifndef TOML_ASSERT +#ifndef assert +TOML_DISABLE_WARNINGS; +#include +TOML_ENABLE_WARNINGS; +#endif +#define TOML_ASSERT(expr) assert(expr) +#endif +#ifdef NDEBUG +#define TOML_ASSERT_ASSUME(expr) TOML_ASSUME(expr) +#else +#define TOML_ASSERT_ASSUME(expr) TOML_ASSERT(expr) +#endif + +#ifndef TOML_ENABLE_FLOAT16 +#define TOML_ENABLE_FLOAT16 0 +#endif + +#ifndef TOML_DISABLE_CONDITIONAL_NOEXCEPT_LAMBDA +#define TOML_DISABLE_CONDITIONAL_NOEXCEPT_LAMBDA 0 +#endif + +#ifndef TOML_DISABLE_NOEXCEPT_NOEXCEPT +#define TOML_DISABLE_NOEXCEPT_NOEXCEPT 0 + #ifdef _MSC_VER + #if _MSC_VER <= 1943 // Up to Visual Studio 2022 Version 17.13.6 + #undef TOML_DISABLE_NOEXCEPT_NOEXCEPT + #define TOML_DISABLE_NOEXCEPT_NOEXCEPT 1 + #endif + #endif +#endif + +#if !defined(TOML_FLOAT_CHARCONV) && (TOML_GCC || TOML_CLANG || (TOML_ICC && !TOML_ICC_CL)) +// not supported by any version of GCC or Clang as of 26/11/2020 +// not supported by any version of ICC on Linux as of 11/01/2021 +#define TOML_FLOAT_CHARCONV 0 +#endif +#if !defined(TOML_INT_CHARCONV) && (defined(__EMSCRIPTEN__) || defined(__APPLE__)) +// causes link errors on emscripten +// causes Mac OS SDK version errors on some versions of Apple Clang +#define TOML_INT_CHARCONV 0 +#endif +#ifndef TOML_INT_CHARCONV +#define TOML_INT_CHARCONV 1 +#endif +#ifndef TOML_FLOAT_CHARCONV +#define TOML_FLOAT_CHARCONV 1 +#endif +#if (TOML_INT_CHARCONV || TOML_FLOAT_CHARCONV) && !TOML_HAS_INCLUDE() +#undef TOML_INT_CHARCONV +#undef TOML_FLOAT_CHARCONV +#define TOML_INT_CHARCONV 0 +#define TOML_FLOAT_CHARCONV 0 +#endif + +#if defined(__cpp_concepts) && __cpp_concepts >= 201907 +#define TOML_REQUIRES(...) requires(__VA_ARGS__) +#else +#define TOML_REQUIRES(...) +#endif +#define TOML_ENABLE_IF(...) , typename std::enable_if<(__VA_ARGS__), int>::type = 0 +#define TOML_CONSTRAINED_TEMPLATE(condition, ...) \ + template <__VA_ARGS__ TOML_ENABLE_IF(condition)> \ + TOML_REQUIRES(condition) +#define TOML_HIDDEN_CONSTRAINT(condition, ...) TOML_CONSTRAINED_TEMPLATE(condition, __VA_ARGS__) + +#if defined(__SIZEOF_FLOAT128__) && defined(__FLT128_MANT_DIG__) && defined(__LDBL_MANT_DIG__) \ + && __FLT128_MANT_DIG__ > __LDBL_MANT_DIG__ +#define TOML_FLOAT128 __float128 +#endif + +#ifdef __SIZEOF_INT128__ +#define TOML_INT128 __int128_t +#define TOML_UINT128 __uint128_t +#endif + +// clang-format off + +//******** impl/version.hpp ****************************************************************************************** + +#define TOML_LIB_MAJOR 3 +#define TOML_LIB_MINOR 4 +#define TOML_LIB_PATCH 0 + +#define TOML_LANG_MAJOR 1 +#define TOML_LANG_MINOR 0 +#define TOML_LANG_PATCH 0 + +//******** impl/preprocessor.hpp ************************************************************************************* + +#define TOML_LIB_SINGLE_HEADER 1 + +#if TOML_ENABLE_UNRELEASED_FEATURES + #define TOML_LANG_EFFECTIVE_VERSION \ + TOML_MAKE_VERSION(TOML_LANG_MAJOR, TOML_LANG_MINOR, TOML_LANG_PATCH+1) +#else + #define TOML_LANG_EFFECTIVE_VERSION \ + TOML_MAKE_VERSION(TOML_LANG_MAJOR, TOML_LANG_MINOR, TOML_LANG_PATCH) +#endif + +#define TOML_LANG_HIGHER_THAN(major, minor, patch) \ + (TOML_LANG_EFFECTIVE_VERSION > TOML_MAKE_VERSION(major, minor, patch)) + +#define TOML_LANG_AT_LEAST(major, minor, patch) \ + (TOML_LANG_EFFECTIVE_VERSION >= TOML_MAKE_VERSION(major, minor, patch)) + +#define TOML_LANG_UNRELEASED \ + TOML_LANG_HIGHER_THAN(TOML_LANG_MAJOR, TOML_LANG_MINOR, TOML_LANG_PATCH) + +#ifndef TOML_ABI_NAMESPACES + #if TOML_DOXYGEN + #define TOML_ABI_NAMESPACES 0 + #else + #define TOML_ABI_NAMESPACES 1 + #endif +#endif +#if TOML_ABI_NAMESPACES + #define TOML_NAMESPACE_START namespace toml { inline namespace TOML_CONCAT(v, TOML_LIB_MAJOR) + #define TOML_NAMESPACE_END } static_assert(true) + #define TOML_NAMESPACE ::toml::TOML_CONCAT(v, TOML_LIB_MAJOR) + #define TOML_ABI_NAMESPACE_START(name) inline namespace name { static_assert(true) + #define TOML_ABI_NAMESPACE_BOOL(cond, T, F) TOML_ABI_NAMESPACE_START(TOML_CONCAT(TOML_EVAL_BOOL_, cond)(T, F)) + #define TOML_ABI_NAMESPACE_END } static_assert(true) +#else + #define TOML_NAMESPACE_START namespace toml + #define TOML_NAMESPACE_END static_assert(true) + #define TOML_NAMESPACE toml + #define TOML_ABI_NAMESPACE_START(...) static_assert(true) + #define TOML_ABI_NAMESPACE_BOOL(...) static_assert(true) + #define TOML_ABI_NAMESPACE_END static_assert(true) +#endif +#define TOML_IMPL_NAMESPACE_START TOML_NAMESPACE_START { namespace impl +#define TOML_IMPL_NAMESPACE_END } TOML_NAMESPACE_END +#if TOML_HEADER_ONLY + #define TOML_ANON_NAMESPACE_START static_assert(TOML_IMPLEMENTATION); TOML_IMPL_NAMESPACE_START + #define TOML_ANON_NAMESPACE_END TOML_IMPL_NAMESPACE_END + #define TOML_ANON_NAMESPACE TOML_NAMESPACE::impl + #define TOML_EXTERNAL_LINKAGE inline + #define TOML_INTERNAL_LINKAGE inline +#else + #define TOML_ANON_NAMESPACE_START static_assert(TOML_IMPLEMENTATION); \ + using namespace toml; \ + namespace + #define TOML_ANON_NAMESPACE_END static_assert(true) + #define TOML_ANON_NAMESPACE + #define TOML_EXTERNAL_LINKAGE + #define TOML_INTERNAL_LINKAGE static +#endif + +// clang-format on + +// clang-format off + +#if TOML_SIMPLE_STATIC_ASSERT_MESSAGES + + #define TOML_SA_NEWLINE " " + #define TOML_SA_LIST_SEP ", " + #define TOML_SA_LIST_BEG " (" + #define TOML_SA_LIST_END ")" + #define TOML_SA_LIST_NEW " " + #define TOML_SA_LIST_NXT ", " + +#else + + #define TOML_SA_NEWLINE "\n| " + #define TOML_SA_LIST_SEP TOML_SA_NEWLINE " - " + #define TOML_SA_LIST_BEG TOML_SA_LIST_SEP + #define TOML_SA_LIST_END + #define TOML_SA_LIST_NEW TOML_SA_NEWLINE TOML_SA_NEWLINE + #define TOML_SA_LIST_NXT TOML_SA_LIST_NEW + +#endif + +#define TOML_SA_NATIVE_VALUE_TYPE_LIST \ + TOML_SA_LIST_BEG "std::string" \ + TOML_SA_LIST_SEP "int64_t" \ + TOML_SA_LIST_SEP "double" \ + TOML_SA_LIST_SEP "bool" \ + TOML_SA_LIST_SEP "toml::date" \ + TOML_SA_LIST_SEP "toml::time" \ + TOML_SA_LIST_SEP "toml::date_time" \ + TOML_SA_LIST_END + +#define TOML_SA_NODE_TYPE_LIST \ + TOML_SA_LIST_BEG "toml::table" \ + TOML_SA_LIST_SEP "toml::array" \ + TOML_SA_LIST_SEP "toml::value" \ + TOML_SA_LIST_SEP "toml::value" \ + TOML_SA_LIST_SEP "toml::value" \ + TOML_SA_LIST_SEP "toml::value" \ + TOML_SA_LIST_SEP "toml::value" \ + TOML_SA_LIST_SEP "toml::value" \ + TOML_SA_LIST_SEP "toml::value" \ + TOML_SA_LIST_END + +#define TOML_SA_UNWRAPPED_NODE_TYPE_LIST \ + TOML_SA_LIST_NEW "A native TOML value type" \ + TOML_SA_NATIVE_VALUE_TYPE_LIST \ + \ + TOML_SA_LIST_NXT "A TOML node type" \ + TOML_SA_NODE_TYPE_LIST + +// clang-format on + +TOML_PUSH_WARNINGS; +TOML_DISABLE_SPAM_WARNINGS; +TOML_DISABLE_SWITCH_WARNINGS; +TOML_DISABLE_SUGGEST_ATTR_WARNINGS; + +// misc warning false-positives +#if TOML_MSVC +#pragma warning(disable : 5031) // #pragma warning(pop): likely mismatch +#if TOML_SHARED_LIB +#pragma warning(disable : 4251) // dll exports for std lib types +#endif +#elif TOML_CLANG +TOML_PRAGMA_CLANG(diagnostic ignored "-Wheader-hygiene") +#if TOML_CLANG >= 12 +TOML_PRAGMA_CLANG(diagnostic ignored "-Wc++20-extensions") +#endif +#if TOML_CLANG == 13 +TOML_PRAGMA_CLANG(diagnostic ignored "-Wreserved-identifier") +#endif +#endif + +//******** impl/std_new.hpp ****************************************************************************************** + +TOML_DISABLE_WARNINGS; +#include +TOML_ENABLE_WARNINGS; + +#if (!defined(__apple_build_version__) && TOML_CLANG >= 8) || TOML_GCC >= 7 || TOML_ICC >= 1910 || TOML_MSVC >= 1914 +#define TOML_LAUNDER(x) __builtin_launder(x) +#elif defined(__cpp_lib_launder) && __cpp_lib_launder >= 201606 +#define TOML_LAUNDER(x) std::launder(x) +#else +#define TOML_LAUNDER(x) x +#endif + +//******** impl/std_string.hpp *************************************************************************************** + +TOML_DISABLE_WARNINGS; +#include +#include +TOML_ENABLE_WARNINGS; + +#if TOML_DOXYGEN \ + || (defined(__cpp_char8_t) && __cpp_char8_t >= 201811 && defined(__cpp_lib_char8_t) \ + && __cpp_lib_char8_t >= 201907) +#define TOML_HAS_CHAR8 1 +#else +#define TOML_HAS_CHAR8 0 +#endif + +namespace toml // non-abi namespace; this is not an error +{ + using namespace std::string_literals; + using namespace std::string_view_literals; +} + +#if TOML_ENABLE_WINDOWS_COMPAT + +TOML_IMPL_NAMESPACE_START +{ + TOML_NODISCARD + TOML_EXPORTED_FREE_FUNCTION + std::string narrow(std::wstring_view); + + TOML_NODISCARD + TOML_EXPORTED_FREE_FUNCTION + std::wstring widen(std::string_view); + +#if TOML_HAS_CHAR8 + + TOML_NODISCARD + TOML_EXPORTED_FREE_FUNCTION + std::wstring widen(std::u8string_view); + +#endif +} +TOML_IMPL_NAMESPACE_END; + +#endif // TOML_ENABLE_WINDOWS_COMPAT + +//******** impl/std_optional.hpp ************************************************************************************* + +TOML_DISABLE_WARNINGS; +#if !TOML_HAS_CUSTOM_OPTIONAL_TYPE +#include +#endif +TOML_ENABLE_WARNINGS; + +TOML_NAMESPACE_START +{ +#if TOML_HAS_CUSTOM_OPTIONAL_TYPE + + template + using optional = TOML_OPTIONAL_TYPE; + +#else + + template + using optional = std::optional; + +#endif +} +TOML_NAMESPACE_END; + +//******** impl/forward_declarations.hpp ***************************************************************************** + +TOML_DISABLE_WARNINGS; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +TOML_ENABLE_WARNINGS; +TOML_PUSH_WARNINGS; +#ifdef _MSC_VER +#ifndef __clang__ +#pragma inline_recursion(on) +#endif +#pragma push_macro("min") +#pragma push_macro("max") +#undef min +#undef max +#endif + +#ifndef TOML_DISABLE_ENVIRONMENT_CHECKS +#define TOML_ENV_MESSAGE \ + "If you're seeing this error it's because you're building toml++ for an environment that doesn't conform to " \ + "one of the 'ground truths' assumed by the library. Essentially this just means that I don't have the " \ + "resources to test on more platforms, but I wish I did! You can try disabling the checks by defining " \ + "TOML_DISABLE_ENVIRONMENT_CHECKS, but your mileage may vary. Please consider filing an issue at " \ + "https://github.com/marzer/tomlplusplus/issues to help me improve support for your target environment. " \ + "Thanks!" + +static_assert(CHAR_BIT == 8, TOML_ENV_MESSAGE); +#ifdef FLT_RADIX +static_assert(FLT_RADIX == 2, TOML_ENV_MESSAGE); +#endif +static_assert('A' == 65, TOML_ENV_MESSAGE); +static_assert(sizeof(double) == 8, TOML_ENV_MESSAGE); +static_assert(std::numeric_limits::is_iec559, TOML_ENV_MESSAGE); +static_assert(std::numeric_limits::digits == 53, TOML_ENV_MESSAGE); +static_assert(std::numeric_limits::digits10 == 15, TOML_ENV_MESSAGE); +static_assert(std::numeric_limits::radix == 2, TOML_ENV_MESSAGE); + +#undef TOML_ENV_MESSAGE +#endif // !TOML_DISABLE_ENVIRONMENT_CHECKS + +// undocumented forward declarations are hidden from doxygen because they fuck it up =/ + +namespace toml // non-abi namespace; this is not an error +{ + using ::std::size_t; + using ::std::intptr_t; + using ::std::uintptr_t; + using ::std::ptrdiff_t; + using ::std::nullptr_t; + using ::std::int8_t; + using ::std::int16_t; + using ::std::int32_t; + using ::std::int64_t; + using ::std::uint8_t; + using ::std::uint16_t; + using ::std::uint32_t; + using ::std::uint64_t; + using ::std::uint_least32_t; + using ::std::uint_least64_t; +} + +TOML_NAMESPACE_START +{ + struct date; + struct time; + struct time_offset; + + TOML_ABI_NAMESPACE_BOOL(TOML_HAS_CUSTOM_OPTIONAL_TYPE, custopt, stdopt); + struct date_time; + TOML_ABI_NAMESPACE_END; + + struct source_position; + struct source_region; + + class node; + template + class node_view; + + class key; + class array; + class table; + template + class value; + + class path; + + class toml_formatter; + class json_formatter; + class yaml_formatter; + + TOML_ABI_NAMESPACE_BOOL(TOML_EXCEPTIONS, ex, noex); +#if TOML_EXCEPTIONS + using parse_result = table; +#else + class parse_result; +#endif + TOML_ABI_NAMESPACE_END; // TOML_EXCEPTIONS +} +TOML_NAMESPACE_END; + +TOML_IMPL_NAMESPACE_START +{ + using node_ptr = std::unique_ptr; + + TOML_ABI_NAMESPACE_BOOL(TOML_EXCEPTIONS, impl_ex, impl_noex); + class parser; + TOML_ABI_NAMESPACE_END; // TOML_EXCEPTIONS + + // clang-format off + + inline constexpr std::string_view control_char_escapes[] = + { + "\\u0000"sv, + "\\u0001"sv, + "\\u0002"sv, + "\\u0003"sv, + "\\u0004"sv, + "\\u0005"sv, + "\\u0006"sv, + "\\u0007"sv, + "\\b"sv, + "\\t"sv, + "\\n"sv, + "\\u000B"sv, + "\\f"sv, + "\\r"sv, + "\\u000E"sv, + "\\u000F"sv, + "\\u0010"sv, + "\\u0011"sv, + "\\u0012"sv, + "\\u0013"sv, + "\\u0014"sv, + "\\u0015"sv, + "\\u0016"sv, + "\\u0017"sv, + "\\u0018"sv, + "\\u0019"sv, + "\\u001A"sv, + "\\u001B"sv, + "\\u001C"sv, + "\\u001D"sv, + "\\u001E"sv, + "\\u001F"sv, + }; + + inline constexpr std::string_view node_type_friendly_names[] = + { + "none"sv, + "table"sv, + "array"sv, + "string"sv, + "integer"sv, + "floating-point"sv, + "boolean"sv, + "date"sv, + "time"sv, + "date-time"sv + }; + + // clang-format on +} +TOML_IMPL_NAMESPACE_END; + +#if TOML_ABI_NAMESPACES +#if TOML_EXCEPTIONS +#define TOML_PARSER_TYPENAME TOML_NAMESPACE::impl::impl_ex::parser +#else +#define TOML_PARSER_TYPENAME TOML_NAMESPACE::impl::impl_noex::parser +#endif +#else +#define TOML_PARSER_TYPENAME TOML_NAMESPACE::impl::parser +#endif + +namespace toml +{ +} + +TOML_NAMESPACE_START // abi namespace +{ + inline namespace literals + { + } + + enum class TOML_CLOSED_ENUM node_type : uint8_t + { + none, + table, + array, + string, + integer, + floating_point, + boolean, + date, + time, + date_time + }; + + template + inline std::basic_ostream& operator<<(std::basic_ostream& lhs, node_type rhs) + { + const auto str = impl::node_type_friendly_names[static_cast>(rhs)]; + using str_char_t = decltype(str)::value_type; + if constexpr (std::is_same_v) + return lhs << str; + else + { + if constexpr (sizeof(Char) == sizeof(str_char_t)) + return lhs << std::basic_string_view{ reinterpret_cast(str.data()), str.length() }; + else + return lhs << str.data(); + } + } + + enum class TOML_OPEN_FLAGS_ENUM value_flags : uint16_t // being an "OPEN" flags enum is not an error + { + none, + format_as_binary = 1, + format_as_octal = 2, + format_as_hexadecimal = 3, + }; + TOML_MAKE_FLAGS(value_flags); + + inline constexpr value_flags preserve_source_value_flags = + POXY_IMPLEMENTATION_DETAIL(value_flags{ static_cast>(-1) }); + + enum class TOML_CLOSED_FLAGS_ENUM format_flags : uint64_t + { + none, + quote_dates_and_times = (1ull << 0), + quote_infinities_and_nans = (1ull << 1), + allow_literal_strings = (1ull << 2), + allow_multi_line_strings = (1ull << 3), + allow_real_tabs_in_strings = (1ull << 4), + allow_unicode_strings = (1ull << 5), + allow_binary_integers = (1ull << 6), + allow_octal_integers = (1ull << 7), + allow_hexadecimal_integers = (1ull << 8), + indent_sub_tables = (1ull << 9), + indent_array_elements = (1ull << 10), + indentation = indent_sub_tables | indent_array_elements, + relaxed_float_precision = (1ull << 11), + terse_key_value_pairs = (1ull << 12), + force_multiline_arrays = (1ull << 13), + }; + TOML_MAKE_FLAGS(format_flags); + + template + struct TOML_TRIVIAL_ABI inserter + { + static_assert(std::is_reference_v); + + T value; + }; + template + inserter(T&&) -> inserter; + template + inserter(T&) -> inserter; + + using default_formatter = toml_formatter; +} +TOML_NAMESPACE_END; + +TOML_IMPL_NAMESPACE_START +{ + template + using remove_cvref = std::remove_cv_t>; + + template + using common_signed_type = std::common_type_t...>; + + template + inline constexpr bool is_one_of = (false || ... || std::is_same_v); + + template + inline constexpr bool all_integral = (std::is_integral_v && ...); + + template + inline constexpr bool is_cvref = std::is_reference_v || std::is_const_v || std::is_volatile_v; + + template + inline constexpr bool is_wide_string = + is_one_of, const wchar_t*, wchar_t*, std::wstring_view, std::wstring>; + + template + inline constexpr bool value_retrieval_is_nothrow = !std::is_same_v, std::string> +#if TOML_HAS_CHAR8 + && !std::is_same_v, std::u8string> +#endif + + && !is_wide_string; + + template + struct copy_ref_; + template + using copy_ref = typename copy_ref_::type; + + template + struct copy_ref_ + { + using type = Dest; + }; + + template + struct copy_ref_ + { + using type = std::add_lvalue_reference_t; + }; + + template + struct copy_ref_ + { + using type = std::add_rvalue_reference_t; + }; + + template + struct copy_cv_; + template + using copy_cv = typename copy_cv_::type; + + template + struct copy_cv_ + { + using type = Dest; + }; + + template + struct copy_cv_ + { + using type = std::add_const_t; + }; + + template + struct copy_cv_ + { + using type = std::add_volatile_t; + }; + + template + struct copy_cv_ + { + using type = std::add_cv_t; + }; + + template + using copy_cvref = + copy_ref, std::remove_reference_t>, Dest>, Src>; + + template + inline constexpr bool always_false = false; + + template + inline constexpr bool first_is_same = false; + template + inline constexpr bool first_is_same = true; + + template > + struct underlying_type_ + { + using type = std::underlying_type_t; + }; + template + struct underlying_type_ + { + using type = T; + }; + template + using underlying_type = typename underlying_type_::type; + + // general value traits + // (as they relate to their equivalent native TOML type) + struct default_value_traits + { + using native_type = void; + static constexpr bool is_native = false; + static constexpr bool is_losslessly_convertible_to_native = false; + static constexpr bool can_represent_native = false; + static constexpr bool can_partially_represent_native = false; + static constexpr auto type = node_type::none; + }; + + template + struct value_traits; + + template > + struct value_traits_base_selector + { + static_assert(!is_cvref); + + using type = default_value_traits; + }; + template + struct value_traits_base_selector + { + static_assert(!is_cvref); + + using type = value_traits>; + }; + + template + struct value_traits : value_traits_base_selector::type + {}; + template + struct value_traits : value_traits + {}; + template + struct value_traits : value_traits + {}; + template + struct value_traits : value_traits + {}; + template + struct value_traits : value_traits + {}; + template + struct value_traits : value_traits + {}; + + // integer value_traits specializations - standard types + template + struct integer_limits + { + static constexpr T min = T{ (std::numeric_limits>::min)() }; + static constexpr T max = T{ (std::numeric_limits>::max)() }; + }; + template + struct integer_traits_base : integer_limits + { + using native_type = int64_t; + static constexpr bool is_native = std::is_same_v, native_type>; + static constexpr bool is_signed = static_cast>(-1) < underlying_type{}; + static constexpr auto type = node_type::integer; + static constexpr bool can_partially_represent_native = true; + }; + template + struct unsigned_integer_traits : integer_traits_base + { + static constexpr bool is_losslessly_convertible_to_native = + integer_limits>::max <= 9223372036854775807ULL; + static constexpr bool can_represent_native = false; + }; + template + struct signed_integer_traits : integer_traits_base + { + using native_type = int64_t; + static constexpr bool is_losslessly_convertible_to_native = + integer_limits>::min >= (-9223372036854775807LL - 1LL) + && integer_limits>::max <= 9223372036854775807LL; + static constexpr bool can_represent_native = + integer_limits>::min <= (-9223372036854775807LL - 1LL) + && integer_limits>::max >= 9223372036854775807LL; + }; + template ::is_signed> + struct integer_traits : signed_integer_traits + {}; + template + struct integer_traits : unsigned_integer_traits + {}; + template <> + struct value_traits : integer_traits + {}; + template <> + struct value_traits : integer_traits + {}; + template <> + struct value_traits : integer_traits + {}; + template <> + struct value_traits : integer_traits + {}; + template <> + struct value_traits : integer_traits + {}; + template <> + struct value_traits : integer_traits + {}; + template <> + struct value_traits : integer_traits + {}; + template <> + struct value_traits : integer_traits + {}; + template <> + struct value_traits : integer_traits + {}; + template <> + struct value_traits : integer_traits + {}; + static_assert(value_traits::is_native); + static_assert(value_traits::is_signed); + static_assert(value_traits::is_losslessly_convertible_to_native); + static_assert(value_traits::can_represent_native); + static_assert(value_traits::can_partially_represent_native); + + // integer value_traits specializations - non-standard types +#ifdef TOML_INT128 + template <> + struct integer_limits + { + static constexpr TOML_INT128 max = + static_cast((TOML_UINT128{ 1u } << ((__SIZEOF_INT128__ * CHAR_BIT) - 1)) - 1); + static constexpr TOML_INT128 min = -max - TOML_INT128{ 1 }; + }; + template <> + struct integer_limits + { + static constexpr TOML_UINT128 min = TOML_UINT128{}; + static constexpr TOML_UINT128 max = (2u * static_cast(integer_limits::max)) + 1u; + }; + template <> + struct value_traits : integer_traits + {}; + template <> + struct value_traits : integer_traits + {}; +#endif +#ifdef TOML_SMALL_INT_TYPE + template <> + struct value_traits : signed_integer_traits + {}; +#endif + + // floating-point traits base + template + struct float_traits_base + { + static constexpr auto type = node_type::floating_point; + using native_type = double; + static constexpr bool is_native = std::is_same_v; + static constexpr bool is_signed = true; + + static constexpr int bits = static_cast(sizeof(T) * CHAR_BIT); + static constexpr int digits = MantissaDigits; + static constexpr int digits10 = DecimalDigits; + + static constexpr bool is_losslessly_convertible_to_native = bits <= 64 // + && digits <= 53 // DBL_MANT_DIG + && digits10 <= 15; // DBL_DIG + + static constexpr bool can_represent_native = digits >= 53 // DBL_MANT_DIG + && digits10 >= 15; // DBL_DIG + + static constexpr bool can_partially_represent_native = digits > 0 && digits10 > 0; + }; + template + struct float_traits : float_traits_base::digits, std::numeric_limits::digits10> + {}; +#if TOML_ENABLE_FLOAT16 + template <> + struct float_traits<_Float16> : float_traits_base<_Float16, __FLT16_MANT_DIG__, __FLT16_DIG__> + {}; +#endif +#ifdef TOML_FLOAT128 + template <> + struct float_traits : float_traits_base + {}; +#endif + + // floating-point traits + template <> + struct value_traits : float_traits + {}; + template <> + struct value_traits : float_traits + {}; + template <> + struct value_traits : float_traits + {}; +#if TOML_ENABLE_FLOAT16 + template <> + struct value_traits<_Float16> : float_traits<_Float16> + {}; +#endif +#ifdef TOML_FLOAT128 + template <> + struct value_traits : float_traits + {}; +#endif +#ifdef TOML_SMALL_FLOAT_TYPE + template <> + struct value_traits : float_traits + {}; +#endif + static_assert(value_traits::is_native); + static_assert(value_traits::is_losslessly_convertible_to_native); + static_assert(value_traits::can_represent_native); + static_assert(value_traits::can_partially_represent_native); + + // string value_traits specializations - char-based strings + template + struct string_traits + { + using native_type = std::string; + static constexpr bool is_native = std::is_same_v; + static constexpr bool is_losslessly_convertible_to_native = true; + static constexpr bool can_represent_native = + !std::is_array_v && (!std::is_pointer_v || std::is_const_v>); + static constexpr bool can_partially_represent_native = can_represent_native; + static constexpr auto type = node_type::string; + }; + template <> + struct value_traits : string_traits + {}; + template <> + struct value_traits : string_traits + {}; + template <> + struct value_traits : string_traits + {}; + template + struct value_traits : string_traits + {}; + template <> + struct value_traits : string_traits + {}; + template + struct value_traits : string_traits + {}; + + // string value_traits specializations - char8_t-based strings +#if TOML_HAS_CHAR8 + template <> + struct value_traits : string_traits + {}; + template <> + struct value_traits : string_traits + {}; + template <> + struct value_traits : string_traits + {}; + template + struct value_traits : string_traits + {}; + template <> + struct value_traits : string_traits + {}; + template + struct value_traits : string_traits + {}; +#endif + + // string value_traits specializations - wchar_t-based strings on Windows +#if TOML_ENABLE_WINDOWS_COMPAT + template + struct wstring_traits + { + using native_type = std::string; + static constexpr bool is_native = false; + static constexpr bool is_losslessly_convertible_to_native = true; // narrow + static constexpr bool can_represent_native = std::is_same_v; // widen + static constexpr bool can_partially_represent_native = can_represent_native; + static constexpr auto type = node_type::string; + }; + template <> + struct value_traits : wstring_traits + {}; + template <> + struct value_traits : wstring_traits + {}; + template <> + struct value_traits : wstring_traits + {}; + template + struct value_traits : wstring_traits + {}; + template <> + struct value_traits : wstring_traits + {}; + template + struct value_traits : wstring_traits + {}; +#endif + + // other 'native' value_traits specializations + template + struct native_value_traits + { + using native_type = T; + static constexpr bool is_native = true; + static constexpr bool is_losslessly_convertible_to_native = true; + static constexpr bool can_represent_native = true; + static constexpr bool can_partially_represent_native = true; + static constexpr auto type = NodeType; + }; + template <> + struct value_traits : native_value_traits + {}; + template <> + struct value_traits : native_value_traits + {}; + template <> + struct value_traits