From 95327dca47a330de29aeecdac34485ebaca3d828 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Fri, 14 Aug 2026 17:55:23 +0200 Subject: [PATCH 01/35] =?UTF-8?q?=F0=9F=93=9D=20Plan=20the=20remaining=20M?= =?UTF-8?q?QT=20Core=20v3=20QDMI=20backports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Record the compatible scope, architecture exclusions, validation boundaries, and combined publication strategy for the remaining QDMI and Slurm functionality. Assisted-by: GPT-5.6 via Codex --- .agent/plans/backport-qdmi-slurm-v3.md | 201 +++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 .agent/plans/backport-qdmi-slurm-v3.md diff --git a/.agent/plans/backport-qdmi-slurm-v3.md b/.agent/plans/backport-qdmi-slurm-v3.md new file mode 100644 index 0000000000..53adb4e89a --- /dev/null +++ b/.agent/plans/backport-qdmi-slurm-v3.md @@ -0,0 +1,201 @@ +# Backport the remaining QDMI and Slurm functionality to v3.x + +This ExecPlan is a living document. The sections `Progress`, +`Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must +be kept up to date as work proceeds. + +This ExecPlan must be maintained in accordance with `.agent/PLANS.md` from the +repository root. + +## Purpose / Big Picture + +MQT Core 3 already exposes the QDMI Python namespace. This backport completes +the compatible QDMI feature set that merged into `main` after the previous +combined v3 backport. Users can inspect device queues, retrieve provider jobs, +query provider-specific operation lists, and select a registered device from a +Slurm license environment. Administrators also get a two-node Slurm fixture and +a concise tutorial that demonstrate cluster-wide admission without presenting +license metadata as authorization. + +## Progress + +- [x] (2026-08-14 18:20Z) Compared every first-parent `main` change merged + after pull request #2024 with the current `v3.x` architecture. +- [x] (2026-08-14 18:25Z) Selected pull requests #2010, #2008, #2042, #2025, + and #2043 as one coherent, v3-compatible functionality range. +- [ ] Port queue telemetry and job retrieval while preserving the v3 QDMI + driver and Python namespace. +- [ ] Port generic custom-operation list queries. +- [ ] Port the Slurm license selector and its system fixture and tutorial. +- [ ] Regenerate stubs, validate the aggregate range, and complete an + adversarial review. +- [ ] Publish one combined pull request against `v3.x` and verify its exact + revision, metadata, signatures, and replacement CI. + +## Surprises & Discoveries + +- Observation: The first namespace backport, pull request #2113, already + contains the compatibility surface required by the later Slurm layer. + Evidence: the current `v3.x` tip exports `mqt.core.qdmi`, + `mqt.core.qdmi.driver`, and the deprecated v3 aliases. +- Observation: The QDMI features form a dependency chain even though they + merged as separate pull requests on `main`. Queue telemetry updates the QDMI + revision and low-level driver contract; job retrieval and custom operation + lists build on that contract; the Slurm adapter builds on the namespace. + +## Decision Log + +- Decision: Use one pull request with separate signed commits for each upstream + functionality. Rationale: this saves stable-branch CI capacity while keeping + provenance, review boundaries, and possible future reverts clear. + Date/Author: 2026-08-14 / Codex. +- Decision: Exclude compiler and QIR changes, breaking removals, dependency + churn, and CI-only optimizations merged after #2024. Rationale: those changes + either depend on the LLVM/MLIR 22 architecture, conflict with the v3 + compatibility promise, or do not justify widening a functional backport. + Date/Author: 2026-08-14 / Codex. +- Decision: Keep one local Slurm license with an optional count of one as the + selector grammar. Rationale: this is the reviewed `main` contract; Slurm + licenses are admission controls and mutable environment metadata, not access + credentials or allocation attestation. + Date/Author: 2026-08-14 / Codex. + +## Outcomes & Retrospective + +Implementation and final validation are pending. + +## Context and Orientation + +QDMI is the low-level device interface. Device providers implement its device +side. `src/qdmi/driver` loads registered provider libraries and implements the +QDMI client interface. `src/fomac` and `include/mqt-core/fomac` provide owning +C++ wrappers, while `bindings/qdmi` exposes the Python namespace. The current +v3 Python API uses `mqt.core.qdmi` for device objects and +`mqt.core.qdmi.driver` for registration and opening. + +Queue telemetry consists of an optional device queue length and an optional job +queue position. Job retrieval opens an existing provider job by its stable job +identifier. A custom operation list is a provider-defined QDMI property whose +payload is an array of operation handles; FoMaC converts those handles to the +same owning `Operation` objects used by the standard operation property. + +The Slurm adapter reads `SLURM_JOB_LICENSES`, accepts one registered local +device ID with an omitted count or count one, opens a fresh device session, and +accepts device states `IDLE` and `BUSY`. The environment variable is mutable. +The adapter therefore selects a device but does not authenticate the user or +prove an allocation. Provider credentials or operating-system isolation remain +the authorization boundary. + +## Plan of Work + +First adapt the implementation and tests from #2010 to the v3 driver and the +new QDMI Python namespace. Update the bundled QDMI revision to the version that +defines the queue and retrieval client interfaces. Preserve optional-provider +loading so old provider libraries remain usable and report unsupported +retrieval rather than failing to load. + +Next add the high-level retrieval and custom-operation APIs from #2008 and +#2042. Keep returned devices, jobs, sites, and operations owning their provider +sessions. Return `None` or `std::nullopt` for unsupported optional properties, +reject malformed byte counts, and preserve provider error codes. + +Then adapt #2025 and #2043 directly to the v3 namespace established by #2113. +Keep the mechanism-specific `fomac::slurm::openDeviceFromLicense()` C++ API and +`mqt.core.qdmi.slurm.open_device_from_license()` Python API. Add the modern +Slurm 25.11 fixture, wheel-based container installation, DDSIM and SC license +scenarios, and the trust-boundary tutorial. + +Keep one signed commit per upstream pull request. Resolve the combined +`CHANGELOG.md` additions semantically and do not carry obsolete intermediate +plans or superseded API names from development history. + +## Concrete Steps + +Run all commands from the repository root through the worktree-local wrapper +when they create caches or build artifacts. + +Apply the five upstream changes in dependency order and inspect each staged +range before committing. Regenerate recursive stubs after all binding changes: + + ./.agent/run.sh uvx nox -s stubs + +Configure and build the native release tree, then run the focused driver, +FoMaC, DDSIM, and Slurm-selector tests: + + ./.agent/run.sh cmake --preset release + ./.agent/run.sh cmake --build --preset release + ./.agent/run.sh ctest --preset release + +Install the Python package and run the QDMI tests: + + ./.agent/run.sh uv sync --inexact --only-group build --only-group test + ./.agent/run.sh uv sync --inexact --no-dev --no-build-isolation-package mqt-core + ./.agent/run.sh uv run --no-sync pytest test/python/qdmi -q + +Run strict documentation and repository validation: + + ./.agent/run.sh uvx nox --non-interactive -s docs + ./.agent/run.sh uvx nox -s lint + git diff --check origin/v3.x...HEAD + +The privileged Slurm workflow is executed by GitHub Actions because it requires +Docker, systemd, cgroup v2, and two Slurm compute containers. + +## Validation and Acceptance + +The driver tests must prove absent and present queue values, a fresh status +query for each job queue-position request, supported and unsupported retrieval, +correct job ownership, custom operation handles, malformed custom payloads, and +operation-property queries. Python tests must prove equivalent `None`, error, +and lifetime behavior through `mqt.core.qdmi`. + +The selector tests must accept only `` and +`:1`. They must reject a missing value, whitespace, +unknown IDs, remote licenses, malformed or non-unit counts, and AND or OR +expressions. It must open a fresh device in `IDLE` or `BUSY` state and reject +all other states with the ID and status in the diagnostic. + +The privileged fixture must run two held DDSIM Bell jobs on separate nodes, +leave a third pending for the DDSIM license while CPUs remain available, run an +SC job independently, release one DDSIM job, admit the pending job, and validate +256 samples containing only `00` and `11`. It must also prove that the selector +rejects compound and non-unit expressions. The container must import the built +wheel without `PYTHONPATH`, and the packaged `slurmd` unit must report +`Delegate=yes`. + +## Idempotence and Recovery + +Builds, stub generation, tests, and documentation commands are repeatable. A +failed source adaptation is repaired in the current logical commit rather than +discarding unrelated work. If `v3.x` advances before publication, rebase the +complete sequence, resolve changelog conflicts semantically, regenerate stubs, +and rerun the affected tests. Never modify another task worktree. + +## Artifacts and Notes + +The selected upstream commits are the squash commits for #2010, #2008, #2042, +#2025, and #2043 on `main`. Pull request #2113 is the authoritative v3 prefix. +Final exact heads, test counts, and any environment-limited checks will be added +here after validation. + +## Interfaces and Dependencies + +The final public additions are: + +- optional C++ and Python device queue-length queries and job queue-position + queries; +- `fomac::Device::retrieveJobById(std::string_view)` and + `mqt.core.qdmi.Device.retrieve_job_by_id(str)`; +- `fomac::Device::queryCustomOperations(fomac::CustomProperty)` and + `mqt.core.qdmi.Device.query_custom_operations(CustomProperty)`; +- `fomac::slurm::openDeviceFromLicense()` and + `mqt.core.qdmi.slurm.open_device_from_license()`. + +The bundled QDMI revision must expose the matching queue and retrieval enums and +functions. The Slurm fixture requires Slurm 25.11 or newer, Ubuntu 26.04, +systemd, Munge, cgroup v2, and Docker Compose. Runtime device authorization is +provider-specific and remains outside the Slurm selector. + +Revision note: created this plan after the first namespace backport merged and +after classifying every `main` change merged since the previous combined v3 +backport. From 46a1159b55cacb26d4f90e68fc86eaa664ae730b Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Fri, 14 Aug 2026 17:56:02 +0200 Subject: [PATCH 02/35] =?UTF-8?q?=E2=9C=A8=20Backport=20QDMI=20queue=20tel?= =?UTF-8?q?emetry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expose optional device queue length and job queue position through the v3 driver, C++ wrappers, and QDMI Python namespace. Update the bundled QDMI revision and retain optional low-level job retrieval for provider compatibility. Backports #2010. Assisted-by: GPT-5.6 via Codex --- CHANGELOG.md | 4 ++ bindings/qdmi/qdmi.cpp | 8 ++++ cmake/ExternalDependencies.cmake | 6 +-- include/mqt-core/fomac/FoMaC.hpp | 22 ++++++++++ include/mqt-core/qdmi/common/Common.hpp | 2 + include/mqt-core/qdmi/driver/Driver.hpp | 9 ++++ python/mqt/core/qdmi/__init__.pyi | 7 +++ src/fomac/FoMaC.cpp | 12 ++++++ src/qdmi/driver/Driver.cpp | 41 ++++++++++++++++++ test/fomac/test_fomac.cpp | 27 ++++++++++++ test/python/qdmi/test_qdmi.py | 19 +++++++++ test/qdmi/devices/dd/job_parameters_test.cpp | 5 +++ test/qdmi/driver/session_device.cpp | 32 ++++++++++++-- test/qdmi/driver/test_driver.cpp | 45 ++++++++++++++++++++ 14 files changed, 232 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 105ebe9a77..bf1e6e8586 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ releases may include breaking changes. ### Added +- ✨ Support retrieving existing jobs by ID through the QDMI client API and + expose optional device queue length and job queue position through the C++ and + Python FoMaC APIs ([#2010]) ([**@burgholzer**]) - ✨ Add PennyLane support for gate-based QDMI devices ([#2005]) ([**@burgholzer**]) - ✨ Expose compressed vector and matrix DD serialization through bytes-based @@ -634,6 +637,7 @@ changelogs._ [#2074]: https://github.com/munich-quantum-toolkit/core/pull/2074 +[#2010]: https://github.com/munich-quantum-toolkit/core/pull/2010 [#2005]: https://github.com/munich-quantum-toolkit/core/pull/2005 [#1984]: https://github.com/munich-quantum-toolkit/core/pull/1984 [#1983]: https://github.com/munich-quantum-toolkit/core/pull/1983 diff --git a/bindings/qdmi/qdmi.cpp b/bindings/qdmi/qdmi.cpp index 0aac6d9a95..c8477384b8 100644 --- a/bindings/qdmi/qdmi.cpp +++ b/bindings/qdmi/qdmi.cpp @@ -309,6 +309,11 @@ when the custom slot is unsupported.)pb"); job.def_prop_ro("num_shots", &fomac::Job::getNumShots, "The number of shots."); + job.def_prop_ro( + "queue_position", &fomac::Job::getQueuePosition, + "The number of jobs ahead in the queue, or None if unavailable or not " + "applicable in the current state."); + job.def(nb::self == nb::self, nb::sig("def __eq__(self, arg: object, /) -> bool")); job.def(nb::self != nb::self, @@ -403,6 +408,9 @@ when the custom slot is unsupported.)pb"); device.def("needs_calibration", &fomac::Device::getNeedsCalibration, "Returns whether the device needs calibration."); + device.def("queue_length", &fomac::Device::getQueueLength, + "Returns the current queue length, or None if unavailable."); + device.def("length_unit", &fomac::Device::getLengthUnit, "Returns the unit of length used by the device."); diff --git a/cmake/ExternalDependencies.cmake b/cmake/ExternalDependencies.cmake index daf04df5c1..c10e2334dd 100644 --- a/cmake/ExternalDependencies.cmake +++ b/cmake/ExternalDependencies.cmake @@ -75,11 +75,11 @@ if(BUILD_MQT_CORE_TESTS) endif() # cmake-format: off -set(QDMI_MINIMUM_VERSION 1.3.2 +set(QDMI_MINIMUM_VERSION 1.3.3 CACHE STRING "Minimum QDMI version") -set(QDMI_VERSION 1.3.2 +set(QDMI_VERSION 1.3.3 CACHE STRING "QDMI version") -set(QDMI_REV "d05a0b418f42e54e9585d2e00af8ce23e745fd83" # v1.3.2 +set(QDMI_REV "e80020f7ace5c0a716142378c812f30f86263c4e" # QDMI develop CACHE STRING "QDMI identifier (tag, branch or commit hash)") set(QDMI_REPO_OWNER "Munich-Quantum-Software-Stack" CACHE STRING "QDMI repository owner (change when using a fork)") diff --git a/include/mqt-core/fomac/FoMaC.hpp b/include/mqt-core/fomac/FoMaC.hpp index 67e88d84a1..0a770a84f6 100644 --- a/include/mqt-core/fomac/FoMaC.hpp +++ b/include/mqt-core/fomac/FoMaC.hpp @@ -65,6 +65,15 @@ concept custom_property_value = std::same_as>; namespace detail { +[[nodiscard]] inline std::optional +queuePositionFromResult(const int result, const size_t queuePosition) { + if (result == QDMI_ERROR_NOTSUPPORTED || result == QDMI_ERROR_BADSTATE) { + return std::nullopt; + } + qdmi::throwIfError(result, "Querying job queue position"); + return queuePosition; +} + template [[nodiscard]] std::optional queryCustomValue(Query query, const std::string_view description) { @@ -466,6 +475,9 @@ class Device { /// @see QDMI_DEVICE_PROPERTY_NEEDSCALIBRATION [[nodiscard]] std::optional getNeedsCalibration() const; + /// @see QDMI_DEVICE_PROPERTY_QUEUELENGTH + [[nodiscard]] std::optional getQueueLength() const; + /// @see QDMI_DEVICE_PROPERTY_LENGTHUNIT [[nodiscard]] std::optional getLengthUnit() const; @@ -685,6 +697,16 @@ class Job { /// Get the number of shots [[nodiscard]] size_t getNumShots() const; + /** + * @brief Gets the current number of jobs ahead of this job in its queue. + * @return The queue position, or `std::nullopt` if it is unavailable or not + * applicable in the job's current state. + * @throws std::runtime_error If the provider status refresh or property query + * fails for another reason. + * @see QDMI_JOB_PROPERTY_QUEUEPOSITION + */ + [[nodiscard]] std::optional getQueuePosition() const; + /** * @brief Queries an implementation-defined custom job property. * @tparam T Expected value type. Use `std::vector` to retrieve the diff --git a/include/mqt-core/qdmi/common/Common.hpp b/include/mqt-core/qdmi/common/Common.hpp index 0e21332eea..d68eb8d275 100644 --- a/include/mqt-core/qdmi/common/Common.hpp +++ b/include/mqt-core/qdmi/common/Common.hpp @@ -390,6 +390,8 @@ constexpr auto toString(const QDMI_Device_Property prop) -> const char* { return "SUPPORTED PROGRAM FORMATS"; case QDMI_DEVICE_PROPERTY_CHILDDEVICES: return "CHILD DEVICES"; + case QDMI_DEVICE_PROPERTY_QUEUELENGTH: + return "QUEUE LENGTH"; case QDMI_DEVICE_PROPERTY_MAX: return "MAX"; case QDMI_DEVICE_PROPERTY_CUSTOM1: diff --git a/include/mqt-core/qdmi/driver/Driver.hpp b/include/mqt-core/qdmi/driver/Driver.hpp index d67a362aa0..e09b99d8a1 100644 --- a/include/mqt-core/qdmi/driver/Driver.hpp +++ b/include/mqt-core/qdmi/driver/Driver.hpp @@ -119,6 +119,9 @@ struct DeviceLibrary { /// Function pointer to @ref QDMI_device_session_create_device_job. decltype(QDMI_device_session_create_device_job)* device_session_create_device_job{}; + /// Function pointer to @ref QDMI_device_session_retrieve_device_job_by_id. + decltype(QDMI_device_session_retrieve_device_job_by_id)* + device_session_retrieve_device_job_by_id{}; /// Function pointer to @ref QDMI_device_job_free. decltype(QDMI_device_job_free)* device_job_free{}; /// Function pointer to @ref QDMI_device_job_set_parameter. @@ -265,6 +268,12 @@ struct QDMI_Device_impl_d { */ auto createJob(QDMI_Job* job) -> int; + /** + * @brief Retrieves an existing job by its ID. + * @see QDMI_session_retrieve_job_by_id + */ + auto retrieveJobById(const char* jobId, QDMI_Job* job) -> int; + /** * @brief Frees the job associated with the device. * @see QDMI_job_free diff --git a/python/mqt/core/qdmi/__init__.pyi b/python/mqt/core/qdmi/__init__.pyi index 28a96d0c5a..59b5d7a2c0 100644 --- a/python/mqt/core/qdmi/__init__.pyi +++ b/python/mqt/core/qdmi/__init__.pyi @@ -113,6 +113,10 @@ class Job: def num_shots(self) -> int: """The number of shots.""" + @property + def queue_position(self) -> int | None: + """The number of jobs ahead in the queue, or None if unavailable or not applicable in the current state.""" + def __eq__(self, arg: object, /) -> bool: ... def __ne__(self, arg: object, /) -> bool: ... @@ -230,6 +234,9 @@ class Device: def needs_calibration(self) -> int | None: """Returns whether the device needs calibration.""" + def queue_length(self) -> int | None: + """Returns the current queue length, or None if unavailable.""" + def length_unit(self) -> str | None: """Returns the unit of length used by the device.""" diff --git a/src/fomac/FoMaC.cpp b/src/fomac/FoMaC.cpp index fb3e7156a6..16c0ab75ed 100644 --- a/src/fomac/FoMaC.cpp +++ b/src/fomac/FoMaC.cpp @@ -275,6 +275,10 @@ std::optional Device::getNeedsCalibration() const { QDMI_DEVICE_PROPERTY_NEEDSCALIBRATION); } +std::optional Device::getQueueLength() const { + return queryProperty>(QDMI_DEVICE_PROPERTY_QUEUELENGTH); +} + std::optional Device::getLengthUnit() const { return queryProperty>( QDMI_DEVICE_PROPERTY_LENGTHUNIT); @@ -524,6 +528,14 @@ size_t Job::getNumShots() const { return numShots; } +std::optional Job::getQueuePosition() const { + size_t queuePosition = 0; + const auto result = + QDMI_job_query_property(job_.get(), QDMI_JOB_PROPERTY_QUEUEPOSITION, + sizeof(queuePosition), &queuePosition, nullptr); + return detail::queuePositionFromResult(result, queuePosition); +} + std::vector Job::getShots() const { size_t shotsSize = 0; qdmi::throwIfError(QDMI_job_get_results(job_.get(), QDMI_JOB_RESULT_SHOTS, 0, diff --git a/src/qdmi/driver/Driver.cpp b/src/qdmi/driver/Driver.cpp index 5c49064462..30a5767f3e 100644 --- a/src/qdmi/driver/Driver.cpp +++ b/src/qdmi/driver/Driver.cpp @@ -122,6 +122,13 @@ DynamicDeviceLibrary::DynamicDeviceLibrary(const std::string& libName, throw std::runtime_error("Failed to load symbol: " + symbolName); \ } \ } + +#define LOAD_OPTIONAL_DYNAMIC_SYMBOL(symbol) \ + { \ + const std::string symbolName = std::string(prefix) + "_QDMI_" + #symbol; \ + (symbol) = reinterpret_cast( \ + DL_SYM(libHandle_, symbolName.c_str())); \ + } //===----------------------------------------------------------------------===// try { @@ -136,6 +143,7 @@ DynamicDeviceLibrary::DynamicDeviceLibrary(const std::string& libName, LOAD_DYNAMIC_SYMBOL(device_session_set_parameter) // device job interface LOAD_DYNAMIC_SYMBOL(device_session_create_device_job) + LOAD_OPTIONAL_DYNAMIC_SYMBOL(device_session_retrieve_device_job_by_id) LOAD_DYNAMIC_SYMBOL(device_job_free) LOAD_DYNAMIC_SYMBOL(device_job_set_parameter) LOAD_DYNAMIC_SYMBOL(device_job_query_property) @@ -159,6 +167,9 @@ DynamicDeviceLibrary::DynamicDeviceLibrary(const std::string& libName, } } +#undef LOAD_OPTIONAL_DYNAMIC_SYMBOL +#undef LOAD_DYNAMIC_SYMBOL + DynamicDeviceLibrary::~DynamicDeviceLibrary() { // Check if QDMI_device_finalize is not NULL before calling it. if (device_finalize != nullptr) { @@ -397,6 +408,26 @@ auto QDMI_Device_impl_d::createJob(QDMI_Job* job) -> int { return QDMI_SUCCESS; } +auto QDMI_Device_impl_d::retrieveJobById(const char* const jobId, + QDMI_Job* const job) -> int { + if (jobId == nullptr || *jobId == '\0' || job == nullptr) { + return QDMI_ERROR_INVALIDARGUMENT; + } + if (library_->device_session_retrieve_device_job_by_id == nullptr) { + return QDMI_ERROR_NOTSUPPORTED; + } + QDMI_Device_Job deviceJob = nullptr; + const auto result = library_->device_session_retrieve_device_job_by_id( + deviceSession_, jobId, &deviceJob); + if (result != QDMI_SUCCESS) { + return result; + } + auto uniqueJob = std::make_unique(deviceJob, this); + const auto it = jobs_.emplace(uniqueJob.get(), std::move(uniqueJob)).first; + *job = it->first; + return QDMI_SUCCESS; +} + auto QDMI_Device_impl_d::freeJob(QDMI_Job job) -> void { if (job != nullptr) { jobs_.erase(job); @@ -508,6 +539,8 @@ namespace { return QDMI_DEVICE_JOB_PROPERTY_PROGRAMFORMAT; case QDMI_JOB_PROPERTY_SHOTSNUM: return QDMI_DEVICE_JOB_PROPERTY_SHOTSNUM; + case QDMI_JOB_PROPERTY_QUEUEPOSITION: + return QDMI_DEVICE_JOB_PROPERTY_QUEUEPOSITION; case QDMI_JOB_PROPERTY_CUSTOM1: return QDMI_DEVICE_JOB_PROPERTY_CUSTOM1; case QDMI_JOB_PROPERTY_CUSTOM2: @@ -798,6 +831,14 @@ int QDMI_device_create_job(QDMI_Device dev, QDMI_Job* job) { return dev->createJob(job); } +int QDMI_session_retrieve_job_by_id(QDMI_Device device, const char* jobId, + QDMI_Job* job) { + if (device == nullptr) { + return QDMI_ERROR_INVALIDARGUMENT; + } + return device->retrieveJobById(jobId, job); +} + void QDMI_job_free(QDMI_Job job) { if (job != nullptr) { job->free(); diff --git a/test/fomac/test_fomac.cpp b/test/fomac/test_fomac.cpp index 3922479f6d..4da3ff1d76 100644 --- a/test/fomac/test_fomac.cpp +++ b/test/fomac/test_fomac.cpp @@ -258,6 +258,23 @@ TEST(CustomPropertyTest, RejectsIncompatibleRepresentations) { std::invalid_argument); } +TEST(QueuePositionTest, ReturnsPositionOnSuccess) { + EXPECT_EQ(detail::queuePositionFromResult(QDMI_SUCCESS, 3), 3); +} + +TEST(QueuePositionTest, ReturnsNulloptWhenUnavailable) { + EXPECT_EQ(detail::queuePositionFromResult(QDMI_ERROR_NOTSUPPORTED, 0), + std::nullopt); + EXPECT_EQ(detail::queuePositionFromResult(QDMI_ERROR_BADSTATE, 0), + std::nullopt); +} + +TEST(QueuePositionTest, PropagatesOtherQueryErrors) { + EXPECT_THROW(std::ignore = detail::queuePositionFromResult( + QDMI_ERROR_INVALIDARGUMENT, 0), + std::invalid_argument); +} + TEST(FoMaCTest, StatusToString) { EXPECT_STREQ(qdmi::toString(QDMI_WARN_GENERAL), "General warning"); EXPECT_STREQ(qdmi::toString(QDMI_SUCCESS), "Success"); @@ -348,6 +365,8 @@ TEST(FoMaCTest, DevicePropertyToString) { "SUPPORTED PROGRAM FORMATS"); EXPECT_STREQ(qdmi::toString(QDMI_DEVICE_PROPERTY_CHILDDEVICES), "CHILD DEVICES"); + EXPECT_STREQ(qdmi::toString(QDMI_DEVICE_PROPERTY_QUEUELENGTH), + "QUEUE LENGTH"); EXPECT_STREQ(qdmi::toString(QDMI_DEVICE_PROPERTY_MAX), "MAX"); EXPECT_STREQ(qdmi::toString(QDMI_DEVICE_PROPERTY_CUSTOM1), "CUSTOM1"); EXPECT_STREQ(qdmi::toString(QDMI_DEVICE_PROPERTY_CUSTOM2), "CUSTOM2"); @@ -423,6 +442,10 @@ TEST_P(DeviceTest, NeedsCalibration) { EXPECT_NO_THROW(std::ignore = device.getNeedsCalibration()); } +TEST_F(DDSimulatorDeviceTest, QueueLengthIsUnavailable) { + EXPECT_EQ(device.getQueueLength(), std::nullopt); +} + TEST_P(DeviceTest, LengthUnit) { EXPECT_NO_THROW(std::ignore = device.getLengthUnit()); } @@ -833,6 +856,10 @@ c[0] = measure q[0]; EXPECT_NE(job.getId(), job2.getId()); } +TEST_F(JobTest, QueuePositionIsUnavailable) { + EXPECT_EQ(job.getQueuePosition(), std::nullopt); +} + TEST_F(JobTest, UnsupportedCustomPropertyAndResultReturnNullopt) { EXPECT_EQ( job.queryCustomProperty>(CustomProperty::Custom1), diff --git a/test/python/qdmi/test_qdmi.py b/test/python/qdmi/test_qdmi.py index c2b0b0c71e..eab0481ea2 100644 --- a/test/python/qdmi/test_qdmi.py +++ b/test/python/qdmi/test_qdmi.py @@ -173,6 +173,14 @@ def test_device_needs_calibration(device: Device) -> None: assert isinstance(needs_cal, int) +def test_device_queue_length(device: Device) -> None: + """Test that the optional device queue length is a non-negative integer.""" + queue_length = device.queue_length() + if queue_length is not None: + assert isinstance(queue_length, int) + assert queue_length >= 0 + + def test_device_length_unit(device: Device) -> None: """Test that the device length unit is a non-empty string.""" lu = device.length_unit() @@ -623,6 +631,17 @@ def test_job_ids_are_unique(ddsim_device: Device) -> None: assert job1.id != job2.id +def test_job_queue_position_is_unavailable(submitted_job: Job) -> None: + """Test that DDSIM does not manufacture a queue position.""" + assert submitted_job.queue_position is None + + +def test_job_queue_position_is_none_after_completion(submitted_job: Job) -> None: + """Test that queue position is None when it no longer applies.""" + assert submitted_job.wait() + assert submitted_job.queue_position is None + + def test_job_custom_property_and_result_unsupported(submitted_job: Job) -> None: """Test custom job-property and result queries for unsupported slots.""" property_value: bytes | None = submitted_job.query_custom_property(CustomProperty.CUSTOM1, bytes) diff --git a/test/qdmi/devices/dd/job_parameters_test.cpp b/test/qdmi/devices/dd/job_parameters_test.cpp index da57756f37..39979ad245 100644 --- a/test/qdmi/devices/dd/job_parameters_test.cpp +++ b/test/qdmi/devices/dd/job_parameters_test.cpp @@ -65,6 +65,11 @@ TEST(JobParameters, SetAndQueryBasics) { QDMI_SUCCESS); EXPECT_EQ(shotsOut, shots); + EXPECT_EQ( + MQT_DDSIM_QDMI_device_job_query_property( + j.job, QDMI_DEVICE_JOB_PROPERTY_QUEUEPOSITION, 0, nullptr, nullptr), + QDMI_ERROR_NOTSUPPORTED); + EXPECT_EQ(MQT_DDSIM_QDMI_device_job_query_property( j.job, QDMI_DEVICE_JOB_PROPERTY_ID, 0, nullptr, &size), QDMI_SUCCESS); diff --git a/test/qdmi/driver/session_device.cpp b/test/qdmi/driver/session_device.cpp index 5f91e81268..1d8a6eea76 100644 --- a/test/qdmi/driver/session_device.cpp +++ b/test/qdmi/driver/session_device.cpp @@ -27,6 +27,7 @@ struct QDMI_Device_Session_impl_d { struct QDMI_Device_Job_impl_d { QDMI_Device_Session session = nullptr; + bool retrieved = false; }; namespace { @@ -205,23 +206,46 @@ TEST_SESSION_QDMI_device_session_create_device_job(QDMI_Device_Session session, } // The QDMI C API transfers this allocation through an opaque raw handle. // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - *job = new (std::nothrow) QDMI_Device_Job_impl_d{session}; + *job = new (std::nothrow) QDMI_Device_Job_impl_d{.session = session}; + return *job == nullptr ? QDMI_ERROR_OUTOFMEM : QDMI_SUCCESS; +} + +extern "C" int TEST_SESSION_QDMI_device_session_retrieve_device_job_by_id( + QDMI_Device_Session session, const char* jobId, QDMI_Device_Job* job) { + if (session == nullptr || !session->initialized || jobId == nullptr || + *jobId == '\0' || job == nullptr) { + return QDMI_ERROR_INVALIDARGUMENT; + } + if (std::strcmp(jobId, "session-job") != 0) { + return QDMI_ERROR_NOTFOUND; + } + // The QDMI C API transfers this allocation through an opaque raw handle. + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + *job = new (std::nothrow) + QDMI_Device_Job_impl_d{.session = session, .retrieved = true}; return *job == nullptr ? QDMI_ERROR_OUTOFMEM : QDMI_SUCCESS; } extern "C" int TEST_SESSION_QDMI_device_job_set_parameter( QDMI_Device_Job job, QDMI_Device_Job_Parameter /*parameter*/, size_t /*size*/, const void* /*value*/) { - return job == nullptr ? QDMI_ERROR_INVALIDARGUMENT : QDMI_SUCCESS; + if (job == nullptr) { + return QDMI_ERROR_INVALIDARGUMENT; + } + return job->retrieved ? QDMI_ERROR_BADSTATE : QDMI_SUCCESS; } extern "C" int TEST_SESSION_QDMI_device_job_query_property( QDMI_Device_Job job, const QDMI_Device_Job_Property prop, const size_t size, void* value, size_t* sizeRet) { if (job == nullptr || job->session == nullptr || - prop != QDMI_DEVICE_JOB_PROPERTY_ID) { + (prop != QDMI_DEVICE_JOB_PROPERTY_ID && + prop != QDMI_DEVICE_JOB_PROPERTY_QUEUEPOSITION)) { return QDMI_ERROR_INVALIDARGUMENT; } + if (prop == QDMI_DEVICE_JOB_PROPERTY_QUEUEPOSITION) { + return QDMI_ERROR_NOTSUPPORTED; + } return queryString("session-job", size, value, sizeRet); } @@ -229,7 +253,7 @@ extern "C" int TEST_SESSION_QDMI_device_job_submit(QDMI_Device_Job job) { if (job == nullptr || job->session == nullptr) { return QDMI_ERROR_INVALIDARGUMENT; } - return QDMI_SUCCESS; + return job->retrieved ? QDMI_ERROR_BADSTATE : QDMI_SUCCESS; } extern "C" int TEST_SESSION_QDMI_device_job_cancel(QDMI_Device_Job /*job*/) { diff --git a/test/qdmi/driver/test_driver.cpp b/test/qdmi/driver/test_driver.cpp index ef9f034e2e..3bea7e9ac4 100644 --- a/test/qdmi/driver/test_driver.cpp +++ b/test/qdmi/driver/test_driver.cpp @@ -523,6 +523,10 @@ TEST_P(DriverJobTest, JobQueryProperty) { EXPECT_EQ(numShots, 1); } + EXPECT_EQ(QDMI_job_query_property(job, QDMI_JOB_PROPERTY_QUEUEPOSITION, 0, + nullptr, nullptr), + QDMI_ERROR_NOTSUPPORTED); + constexpr std::array customProperties{ QDMI_JOB_PROPERTY_CUSTOM1, QDMI_JOB_PROPERTY_CUSTOM2, QDMI_JOB_PROPERTY_CUSTOM3, QDMI_JOB_PROPERTY_CUSTOM4, @@ -777,6 +781,21 @@ TEST_P(DriverTest, SessionAlloc) { EXPECT_EQ(QDMI_session_alloc(nullptr), QDMI_ERROR_INVALIDARGUMENT); } +TEST_P(DriverTest, RetrieveJobById) { + QDMI_Job job = nullptr; + EXPECT_EQ(QDMI_session_retrieve_job_by_id(nullptr, "job-id", &job), + QDMI_ERROR_INVALIDARGUMENT); + EXPECT_EQ(QDMI_session_retrieve_job_by_id(device, nullptr, &job), + QDMI_ERROR_INVALIDARGUMENT); + EXPECT_EQ(QDMI_session_retrieve_job_by_id(device, "", &job), + QDMI_ERROR_INVALIDARGUMENT); + EXPECT_EQ(QDMI_session_retrieve_job_by_id(device, "job-id", nullptr), + QDMI_ERROR_INVALIDARGUMENT); + EXPECT_EQ(QDMI_session_retrieve_job_by_id(device, "job-id", &job), + QDMI_ERROR_NOTSUPPORTED); + EXPECT_EQ(job, nullptr); +} + TEST_P(DriverTest, SessionInit) { EXPECT_EQ(QDMI_session_init(nullptr), QDMI_ERROR_INVALIDARGUMENT) << "`session == nullptr` is not a valid argument."; @@ -1099,6 +1118,32 @@ TEST(DeviceRegistrationTest, FreshJobRetainsItsDeviceSession) { EXPECT_THAT(queryName(probe), testing::HasSubstr("active=1")); } +TEST(DeviceRegistrationTest, RetrievesExistingJobs) { + registerSessionTestDevice(); + const auto device = fomac::Session::openDevice("test.session-overrides"); + + QDMI_Job job = nullptr; + ASSERT_EQ(QDMI_session_retrieve_job_by_id(device, "session-job", &job), + QDMI_SUCCESS); + ASSERT_NE(job, nullptr); + + std::array id{}; + EXPECT_EQ(QDMI_job_query_property(job, QDMI_JOB_PROPERTY_ID, id.size(), + id.data(), nullptr), + QDMI_SUCCESS); + EXPECT_STREQ(id.data(), "session-job"); + EXPECT_EQ(QDMI_job_set_parameter(job, QDMI_JOB_PARAMETER_SHOTSNUM, + sizeof(size_t), nullptr), + QDMI_ERROR_BADSTATE); + EXPECT_EQ(QDMI_job_submit(job), QDMI_ERROR_BADSTATE); + QDMI_job_free(job); + + job = nullptr; + EXPECT_EQ(QDMI_session_retrieve_job_by_id(device, "missing", &job), + QDMI_ERROR_NOTFOUND); + EXPECT_EQ(job, nullptr); +} + TEST(DeviceRegistrationTest, FreshChildDeviceRetainsItsRootSession) { registerSessionTestDevice(); std::optional child; From 29ac67b1818fa63a206e6159b715b5b96c371c71 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Fri, 14 Aug 2026 17:56:52 +0200 Subject: [PATCH 03/35] =?UTF-8?q?=E2=9C=A8=20Backport=20existing=20QDMI=20?= =?UTF-8?q?job=20retrieval?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expose provider job retrieval by stable job ID through the v3 C++ wrappers and QDMI Python namespace. Keep the built-in devices explicit about their unsupported retrieval contract. Backports #2008. Assisted-by: GPT-5.6 via Codex --- CHANGELOG.md | 7 ++++--- bindings/qdmi/qdmi.cpp | 8 ++++++++ include/mqt-core/fomac/FoMaC.hpp | 10 ++++++++++ python/mqt/core/qdmi/__init__.pyi | 3 +++ src/fomac/FoMaC.cpp | 9 +++++++++ src/qdmi/devices/dd/Device.cpp | 7 +++++++ src/qdmi/devices/na/Device.cpp | 13 ++++++++++++- src/qdmi/devices/sc/Device.cpp | 13 ++++++++++++- test/python/qdmi/test_qdmi.py | 8 ++++++++ test/qdmi/driver/test_driver.cpp | 3 +++ 10 files changed, 76 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf1e6e8586..d809af7a84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,9 +12,9 @@ releases may include breaking changes. ### Added -- ✨ Support retrieving existing jobs by ID through the QDMI client API and - expose optional device queue length and job queue position through the C++ and - Python FoMaC APIs ([#2010]) ([**@burgholzer**]) +- ✨ Support retrieving existing jobs by ID through the QDMI client API and C++ + and Python FoMaC APIs, and expose optional device queue length and job queue + position ([#2008], [#2010]) ([**@burgholzer**]) - ✨ Add PennyLane support for gate-based QDMI devices ([#2005]) ([**@burgholzer**]) - ✨ Expose compressed vector and matrix DD serialization through bytes-based @@ -638,6 +638,7 @@ changelogs._ [#2074]: https://github.com/munich-quantum-toolkit/core/pull/2074 [#2010]: https://github.com/munich-quantum-toolkit/core/pull/2010 +[#2008]: https://github.com/munich-quantum-toolkit/core/pull/2008 [#2005]: https://github.com/munich-quantum-toolkit/core/pull/2005 [#1984]: https://github.com/munich-quantum-toolkit/core/pull/1984 [#1983]: https://github.com/munich-quantum-toolkit/core/pull/1983 diff --git a/bindings/qdmi/qdmi.cpp b/bindings/qdmi/qdmi.cpp index c8477384b8..74c614b6eb 100644 --- a/bindings/qdmi/qdmi.cpp +++ b/bindings/qdmi/qdmi.cpp @@ -492,6 +492,14 @@ when the custom slot is unsupported.)pb"); "custom5"_a = nb::none(), nb::rv_policy::reference_internal, "Submits an exact byte payload to the device."); + device.def( + "retrieve_job_by_id", + [](const fomac::Device& self, const std::string& jobId) { + return self.retrieveJobById(jobId); + }, + "job_id"_a, nb::rv_policy::reference_internal, + "Retrieves an existing job by its device-provided ID."); + device.def("__repr__", [](const fomac::Device& dev) { return ""; }); diff --git a/include/mqt-core/fomac/FoMaC.hpp b/include/mqt-core/fomac/FoMaC.hpp index 0a770a84f6..b92292ed6d 100644 --- a/include/mqt-core/fomac/FoMaC.hpp +++ b/include/mqt-core/fomac/FoMaC.hpp @@ -559,6 +559,16 @@ class Device { const std::optional& custom4 = std::nullopt, const std::optional& custom5 = std::nullopt) const; + /** + * @brief Retrieves an existing job by its device-provided ID. + * @details Opening a job does not submit, clone, or modify the remote job. + * The returned handle can be used to query its state and retrieve results. + * @param jobId The nonempty opaque ID returned by @ref Job::getId. + * @throws std::runtime_error If the driver or device cannot retrieve the job. + * @see QDMI_session_retrieve_job_by_id + */ + [[nodiscard]] Job retrieveJobById(std::string_view jobId) const; + auto operator<=>(const Device&) const noexcept = default; private: diff --git a/python/mqt/core/qdmi/__init__.pyi b/python/mqt/core/qdmi/__init__.pyi index 59b5d7a2c0..813cc801ee 100644 --- a/python/mqt/core/qdmi/__init__.pyi +++ b/python/mqt/core/qdmi/__init__.pyi @@ -309,6 +309,9 @@ class Device: ) -> Job: """Submits an exact byte payload to the device.""" + def retrieve_job_by_id(self, job_id: str) -> Job: + """Retrieves an existing job by its device-provided ID.""" + def __eq__(self, arg: object, /) -> bool: ... def __ne__(self, arg: object, /) -> bool: ... diff --git a/src/fomac/FoMaC.cpp b/src/fomac/FoMaC.cpp index 16c0ab75ed..6ce81b931d 100644 --- a/src/fomac/FoMaC.cpp +++ b/src/fomac/FoMaC.cpp @@ -411,6 +411,15 @@ Job Device::submitJob(const std::span program, return jobWrapper; } +Job Device::retrieveJobById(const std::string_view jobId) const { + const std::string id{jobId}; + QDMI_Job job = nullptr; + qdmi::throwIfError( + QDMI_session_retrieve_job_by_id(device_.get(), id.c_str(), &job), + "Retrieving job"); + return Job{job, device_}; +} + void Device::setCustomJobParam(QDMI_Job job, const QDMI_Job_Parameter param, const CustomJobParameter& value) { std::visit( diff --git a/src/qdmi/devices/dd/Device.cpp b/src/qdmi/devices/dd/Device.cpp index 2e90af27e9..3cac4359e7 100644 --- a/src/qdmi/devices/dd/Device.cpp +++ b/src/qdmi/devices/dd/Device.cpp @@ -849,6 +849,13 @@ int MQT_DDSIM_QDMI_device_session_create_device_job( return session->createDeviceJob(job); } +int MQT_DDSIM_QDMI_device_session_retrieve_device_job_by_id( + [[maybe_unused]] MQT_DDSIM_QDMI_Device_Session session, + [[maybe_unused]] const char* jobId, + [[maybe_unused]] MQT_DDSIM_QDMI_Device_Job* job) { + return QDMI_ERROR_NOTSUPPORTED; +} + void MQT_DDSIM_QDMI_device_job_free(MQT_DDSIM_QDMI_Device_Job job) { job->free(); } diff --git a/src/qdmi/devices/na/Device.cpp b/src/qdmi/devices/na/Device.cpp index 5bf591e1b7..bfa227de42 100644 --- a/src/qdmi/devices/na/Device.cpp +++ b/src/qdmi/devices/na/Device.cpp @@ -604,7 +604,18 @@ int MQT_NA_QDMI_device_session_create_device_job( return session->createDeviceJob(job); } -void MQT_NA_QDMI_device_job_free(MQT_NA_QDMI_Device_Job job) { job->free(); } +int MQT_NA_QDMI_device_session_retrieve_device_job_by_id( + [[maybe_unused]] MQT_NA_QDMI_Device_Session session, + [[maybe_unused]] const char* jobId, + [[maybe_unused]] MQT_NA_QDMI_Device_Job* job) { + return QDMI_ERROR_NOTSUPPORTED; +} + +void MQT_NA_QDMI_device_job_free(MQT_NA_QDMI_Device_Job job) { + if (job != nullptr) { + job->free(); + } +} int MQT_NA_QDMI_device_job_set_parameter(MQT_NA_QDMI_Device_Job job, const QDMI_Device_Job_Parameter param, diff --git a/src/qdmi/devices/sc/Device.cpp b/src/qdmi/devices/sc/Device.cpp index d8f37eda95..5f236e69d9 100644 --- a/src/qdmi/devices/sc/Device.cpp +++ b/src/qdmi/devices/sc/Device.cpp @@ -433,7 +433,18 @@ int MQT_SC_QDMI_device_session_create_device_job( return session->createDeviceJob(job); } -void MQT_SC_QDMI_device_job_free(MQT_SC_QDMI_Device_Job job) { job->free(); } +int MQT_SC_QDMI_device_session_retrieve_device_job_by_id( + [[maybe_unused]] MQT_SC_QDMI_Device_Session session, + [[maybe_unused]] const char* jobId, + [[maybe_unused]] MQT_SC_QDMI_Device_Job* job) { + return QDMI_ERROR_NOTSUPPORTED; +} + +void MQT_SC_QDMI_device_job_free(MQT_SC_QDMI_Device_Job job) { + if (job != nullptr) { + job->free(); + } +} int MQT_SC_QDMI_device_job_set_parameter(MQT_SC_QDMI_Device_Job job, const QDMI_Device_Job_Parameter param, diff --git a/test/python/qdmi/test_qdmi.py b/test/python/qdmi/test_qdmi.py index eab0481ea2..1b839bd788 100644 --- a/test/python/qdmi/test_qdmi.py +++ b/test/python/qdmi/test_qdmi.py @@ -600,6 +600,14 @@ def test_device_submit_job_preserves_num_shots(ddsim_device: Device) -> None: assert job3.num_shots == 1000 +def test_device_retrieve_job_by_id_reports_unsupported_provider( + ddsim_device: Device, +) -> None: + """Expose job retrieval through Python without requiring DDSIM support.""" + with pytest.raises(RuntimeError, match=r"Retrieving job: Not supported\."): + ddsim_device.retrieve_job_by_id("unknown") + + @pytest.fixture def submitted_job(ddsim_device: Device) -> Job: """Fixture that provides a submitted job for testing. diff --git a/test/qdmi/driver/test_driver.cpp b/test/qdmi/driver/test_driver.cpp index 3bea7e9ac4..7b70754e6a 100644 --- a/test/qdmi/driver/test_driver.cpp +++ b/test/qdmi/driver/test_driver.cpp @@ -1142,6 +1142,9 @@ TEST(DeviceRegistrationTest, RetrievesExistingJobs) { EXPECT_EQ(QDMI_session_retrieve_job_by_id(device, "missing", &job), QDMI_ERROR_NOTFOUND); EXPECT_EQ(job, nullptr); + + const auto retrievedJob = device.retrieveJobById("session-job"); + EXPECT_EQ(retrievedJob.getId(), "session-job"); } TEST(DeviceRegistrationTest, FreshChildDeviceRetainsItsRootSession) { From 91c9303b35e6eebefa12161fc1471b4f3a93fc8b Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Fri, 14 Aug 2026 17:58:00 +0200 Subject: [PATCH 04/35] =?UTF-8?q?=E2=9C=A8=20Backport=20custom=20QDMI=20op?= =?UTF-8?q?eration=20lists?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expose provider-specific operation-handle arrays through the v3 C++ wrappers and QDMI Python namespace. Preserve unsupported, empty, malformed-payload, shared-handle, and lifetime semantics. Backports #2042. Assisted-by: GPT-5.6 via Codex --- CHANGELOG.md | 3 + bindings/qdmi/qdmi.cpp | 8 +++ include/mqt-core/fomac/FoMaC.hpp | 40 ++++++++++++ python/mqt/core/qdmi/__init__.pyi | 7 +++ src/fomac/FoMaC.cpp | 29 +++++++-- test/python/qdmi/test_qdmi.py | 5 ++ test/qdmi/driver/session_device.cpp | 95 +++++++++++++++++++++++++++-- test/qdmi/driver/test_driver.cpp | 68 +++++++++++++++++++++ 8 files changed, 247 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d809af7a84..da5f5c1fef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ releases may include breaking changes. ### Added +- ✨ Add generic C++ and Python FoMaC support for custom device properties that + contain operation handles ([#2042]) ([**@burgholzer**]) - ✨ Support retrieving existing jobs by ID through the QDMI client API and C++ and Python FoMaC APIs, and expose optional device queue length and job queue position ([#2008], [#2010]) ([**@burgholzer**]) @@ -637,6 +639,7 @@ changelogs._ [#2074]: https://github.com/munich-quantum-toolkit/core/pull/2074 +[#2042]: https://github.com/munich-quantum-toolkit/core/pull/2042 [#2010]: https://github.com/munich-quantum-toolkit/core/pull/2010 [#2008]: https://github.com/munich-quantum-toolkit/core/pull/2008 [#2005]: https://github.com/munich-quantum-toolkit/core/pull/2005 diff --git a/bindings/qdmi/qdmi.cpp b/bindings/qdmi/qdmi.cpp index 74c614b6eb..aeca9f0eff 100644 --- a/bindings/qdmi/qdmi.cpp +++ b/bindings/qdmi/qdmi.cpp @@ -433,6 +433,14 @@ when the custom slot is unsupported.)pb"); device.def("child_devices", &fomac::Device::getChildDevices, "Returns the direct child devices managed by this device."); + device.def( + "query_custom_operations", &fomac::Device::queryCustomOperations, + "custom_property"_a, + R"pb(Query a custom device property that contains operation handles. + +Returns normal :class:`Device.Operation` objects, or ``None`` when the custom +slot is unsupported. A supported empty list is returned as an empty list.)pb"); + device.def( "query_custom_property", [](const fomac::Device& self, const fomac::CustomProperty customProperty, diff --git a/include/mqt-core/fomac/FoMaC.hpp b/include/mqt-core/fomac/FoMaC.hpp index b92292ed6d..db22e05f13 100644 --- a/include/mqt-core/fomac/FoMaC.hpp +++ b/include/mqt-core/fomac/FoMaC.hpp @@ -113,6 +113,31 @@ queryCustomValue(Query query, const std::string_view description) { } } +template +[[nodiscard]] std::optional> +queryHandleArray(Query query, const std::string_view description) { + size_t size = 0; + const auto sizeResult = query(0, nullptr, &size); + if (sizeResult == QDMI_ERROR_NOTSUPPORTED) { + return std::nullopt; + } + qdmi::throwIfError(sizeResult, + "Querying " + std::string(description) + " size"); + if (size % sizeof(Handle) != 0) { + throw std::invalid_argument( + "Cannot decode " + std::string(description) + ": the device reported " + + std::to_string(size) + " bytes, which is not a multiple of " + + std::to_string(sizeof(Handle))); + } + + std::vector handles(size / sizeof(Handle)); + if (size != 0) { + qdmi::throwIfError(query(size, static_cast(handles.data()), nullptr), + "Querying " + std::string(description)); + } + return handles; +} + [[nodiscard]] constexpr QDMI_Device_Property toDeviceProperty(const CustomProperty property) { switch (property) { @@ -526,6 +551,17 @@ class Device { std::to_string(static_cast(property))); } + /** + * @brief Queries a custom device property containing operation handles. + * @param property Custom property slot to query. + * @return Normal FoMaC operation wrappers, or `std::nullopt` if the slot is + * unsupported. A supported empty list is returned as an engaged optional. + * @throws std::invalid_argument If the returned byte count is not a multiple + * of `sizeof(QDMI_Operation)`. + */ + [[nodiscard]] std::optional> + queryCustomOperations(CustomProperty property) const; + /** * @brief Submits a textual program. * @details The terminating null byte required by QDMI text formats is @@ -586,6 +622,10 @@ class Device { explicit Device(std::shared_ptr device) : device_(std::move(device)) {} + /// Wrap operation handles while retaining their owning device session. + [[nodiscard]] std::vector + wrapOperations(std::span operations) const; + /// Query a device property. template [[nodiscard]] T queryProperty(const QDMI_Device_Property prop) const { diff --git a/python/mqt/core/qdmi/__init__.pyi b/python/mqt/core/qdmi/__init__.pyi index 813cc801ee..f41de97db0 100644 --- a/python/mqt/core/qdmi/__init__.pyi +++ b/python/mqt/core/qdmi/__init__.pyi @@ -258,6 +258,13 @@ class Device: def child_devices(self) -> list[Device]: """Returns the direct child devices managed by this device.""" + def query_custom_operations(self, custom_property: CustomProperty) -> list[Operation] | None: + """Query a custom device property that contains operation handles. + + Returns normal :class:`Device.Operation` objects, or ``None`` when the custom + slot is unsupported. A supported empty list is returned as an empty list. + """ + @overload def query_custom_property(self, custom_property: CustomProperty, value_type: type[str]) -> str | None: ... @overload diff --git a/src/fomac/FoMaC.cpp b/src/fomac/FoMaC.cpp index 6ce81b931d..b87e23fa8d 100644 --- a/src/fomac/FoMaC.cpp +++ b/src/fomac/FoMaC.cpp @@ -240,12 +240,33 @@ std::vector Device::getZones() const { std::vector Device::getOperations() const { const auto& qdmiOperations = queryProperty>( QDMI_DEVICE_PROPERTY_OPERATIONS); - std::vector operations; - operations.reserve(qdmiOperations.size()); + return wrapOperations(qdmiOperations); +} + +std::optional> +Device::queryCustomOperations(const CustomProperty property) const { + const auto qdmiProperty = detail::toDeviceProperty(property); + const auto handles = detail::queryHandleArray( + [this, qdmiProperty](const size_t size, void* value, size_t* sizeRet) { + return QDMI_device_query_device_property(device_.get(), qdmiProperty, + size, value, sizeRet); + }, + "custom operation list " + + std::to_string(static_cast(property))); + if (!handles.has_value()) { + return std::nullopt; + } + return wrapOperations(*handles); +} + +std::vector +Device::wrapOperations(const std::span operations) const { + std::vector wrappedOperations; + wrappedOperations.reserve(operations.size()); std::ranges::transform( - qdmiOperations, std::back_inserter(operations), + operations, std::back_inserter(wrappedOperations), [this](const QDMI_Operation& op) -> Operation { return {device_, op}; }); - return operations; + return wrappedOperations; } std::optional>> diff --git a/test/python/qdmi/test_qdmi.py b/test/python/qdmi/test_qdmi.py index 1b839bd788..a77101b847 100644 --- a/test/python/qdmi/test_qdmi.py +++ b/test/python/qdmi/test_qdmi.py @@ -243,6 +243,11 @@ def test_device_custom_property_rejects_invalid_type(device: Device) -> None: device.query_custom_property(CustomProperty.CUSTOM1, cast("type[bytes]", list)) +def test_device_custom_operations_unsupported(device: Device) -> None: + """Unsupported custom operation lists return None instead of raw bytes.""" + assert device.query_custom_operations(CustomProperty.CUSTOM5) is None + + def test_site_index(device_and_site: tuple[Device, Device.Site]) -> None: """Test that the site index is a non-negative integer.""" _device, site = device_and_site diff --git a/test/qdmi/driver/session_device.cpp b/test/qdmi/driver/session_device.cpp index 1d8a6eea76..c7af106d74 100644 --- a/test/qdmi/driver/session_device.cpp +++ b/test/qdmi/driver/session_device.cpp @@ -10,6 +10,7 @@ #include +#include #include #include #include @@ -19,6 +20,12 @@ struct QDMI_Child_Device_impl_d {}; +struct QDMI_Operation_impl_d { + const char* name; + size_t qubitsNum; + size_t parametersNum; +}; + struct QDMI_Device_Session_impl_d { std::unordered_map parameters; QDMI_Child_Device child = nullptr; @@ -51,6 +58,27 @@ namespace { return &child; } +[[nodiscard]] auto customOperationHandles() + -> const std::array& { + static QDMI_Operation_impl_d rotate{ + .name = "custom-rx", .qubitsNum = 1, .parametersNum = 1}; + static QDMI_Operation_impl_d controlledNot{ + .name = "custom-cx", .qubitsNum = 2, .parametersNum = 0}; + static const std::array OPERATIONS{&rotate, + &controlledNot}; + return OPERATIONS; +} + +[[nodiscard]] auto findCustomOperation(QDMI_Operation operation) + -> const QDMI_Operation_impl_d* { + for (auto* const handle : customOperationHandles()) { + if (operation == handle) { + return handle; + } + } + return nullptr; +} + auto queryString(const std::string& result, const size_t size, void* value, size_t* sizeRet) -> int { const auto required = result.size() + 1; @@ -66,6 +94,22 @@ auto queryString(const std::string& result, const size_t size, void* value, std::memcpy(value, result.c_str(), required); return QDMI_SUCCESS; } + +template +auto queryValue(const T& result, const size_t size, void* value, + size_t* sizeRet) -> int { + if (sizeRet != nullptr) { + *sizeRet = sizeof(T); + } + if (value == nullptr) { + return QDMI_SUCCESS; + } + if (size < sizeof(T)) { + return QDMI_ERROR_INVALIDARGUMENT; + } + std::memcpy(value, &result, sizeof(T)); + return QDMI_SUCCESS; +} } // namespace // QDMI requires these exported C symbols to use the configured device prefix. @@ -166,6 +210,33 @@ extern "C" int TEST_SESSION_QDMI_device_session_query_device_property( sizeof(QDMI_Child_Device)); return QDMI_SUCCESS; } + if (prop == QDMI_DEVICE_PROPERTY_CUSTOM1) { + const auto& operations = customOperationHandles(); + const auto required = operations.size() * sizeof(QDMI_Operation); + if (sizeRet != nullptr) { + *sizeRet = required; + } + if (value == nullptr) { + return QDMI_SUCCESS; + } + if (size < required) { + return QDMI_ERROR_INVALIDARGUMENT; + } + std::memcpy(value, static_cast(operations.data()), required); + return QDMI_SUCCESS; + } + if (prop == QDMI_DEVICE_PROPERTY_CUSTOM2) { + if (sizeRet != nullptr) { + *sizeRet = 0; + } + return QDMI_SUCCESS; + } + if (prop == QDMI_DEVICE_PROPERTY_CUSTOM3) { + if (sizeRet != nullptr) { + *sizeRet = sizeof(QDMI_Operation) + 1; + } + return value == nullptr ? QDMI_SUCCESS : QDMI_ERROR_INVALIDARGUMENT; + } if (prop != QDMI_DEVICE_PROPERTY_NAME) { return QDMI_ERROR_NOTSUPPORTED; } @@ -191,10 +262,26 @@ extern "C" int TEST_SESSION_QDMI_device_session_query_site_property( } extern "C" int TEST_SESSION_QDMI_device_session_query_operation_property( - QDMI_Device_Session /*session*/, QDMI_Operation /*operation*/, - size_t /*numSites*/, const QDMI_Site* /*sites*/, size_t /*numParams*/, - const double* /*params*/, QDMI_Operation_Property /*property*/, - size_t /*size*/, void* /*value*/, size_t* /*sizeRet*/) { + QDMI_Device_Session session, QDMI_Operation operation, size_t /*numSites*/, + const QDMI_Site* /*sites*/, size_t /*numParams*/, const double* /*params*/, + const QDMI_Operation_Property property, const size_t size, void* value, + size_t* sizeRet) { + if (session == nullptr || !session->initialized) { + return QDMI_ERROR_BADSTATE; + } + const auto* const customOperation = findCustomOperation(operation); + if (customOperation == nullptr) { + return QDMI_ERROR_INVALIDARGUMENT; + } + if (property == QDMI_OPERATION_PROPERTY_NAME) { + return queryString(customOperation->name, size, value, sizeRet); + } + if (property == QDMI_OPERATION_PROPERTY_QUBITSNUM) { + return queryValue(customOperation->qubitsNum, size, value, sizeRet); + } + if (property == QDMI_OPERATION_PROPERTY_PARAMETERSNUM) { + return queryValue(customOperation->parametersNum, size, value, sizeRet); + } return QDMI_ERROR_NOTSUPPORTED; } diff --git a/test/qdmi/driver/test_driver.cpp b/test/qdmi/driver/test_driver.cpp index 7b70754e6a..d4dbd57545 100644 --- a/test/qdmi/driver/test_driver.cpp +++ b/test/qdmi/driver/test_driver.cpp @@ -1101,6 +1101,74 @@ TEST(DeviceRegistrationTest, FreshOpenCreatesDistinctSessions) { EXPECT_NE(first, second); } +TEST(DeviceRegistrationTest, CustomOperationListSupportsRawAndFoMaCQueries) { + registerSessionTestDevice(); + const auto device = fomac::Session::openDevice("test.session-overrides"); + + size_t size = 0; + ASSERT_EQ(QDMI_device_query_device_property( + device, QDMI_DEVICE_PROPERTY_CUSTOM1, 0, nullptr, &size), + QDMI_SUCCESS); + ASSERT_EQ(size, 2 * sizeof(QDMI_Operation)); + std::vector rawOperations(size / sizeof(QDMI_Operation)); + EXPECT_EQ(QDMI_device_query_device_property( + device, QDMI_DEVICE_PROPERTY_CUSTOM1, + size - sizeof(QDMI_Operation), + static_cast(rawOperations.data()), nullptr), + QDMI_ERROR_INVALIDARGUMENT); + ASSERT_EQ(QDMI_device_query_device_property( + device, QDMI_DEVICE_PROPERTY_CUSTOM1, size, + static_cast(rawOperations.data()), nullptr), + QDMI_SUCCESS); + + const auto operations = + device.queryCustomOperations(fomac::CustomProperty::Custom1); + ASSERT_TRUE(operations.has_value()); + ASSERT_EQ(operations->size(), rawOperations.size()); + EXPECT_EQ(static_cast(operations->at(0)), + rawOperations.at(0)); + EXPECT_EQ(static_cast(operations->at(1)), + rawOperations.at(1)); + EXPECT_EQ(operations->at(0).getName(), "custom-rx"); + EXPECT_EQ(operations->at(0).getQubitsNum(), 1); + EXPECT_EQ(operations->at(0).getParametersNum(), 1); + EXPECT_EQ(operations->at(1).getName(), "custom-cx"); + EXPECT_EQ(operations->at(1).getQubitsNum(), 2); + EXPECT_EQ(operations->at(1).getParametersNum(), 0); +} + +TEST(DeviceRegistrationTest, + CustomOperationListDistinguishesUnsupportedEmptyAndMalformed) { + registerSessionTestDevice(); + const auto device = fomac::Session::openDevice("test.session-overrides"); + + EXPECT_EQ(device.queryCustomOperations(fomac::CustomProperty::Custom4), + std::nullopt); + const auto empty = + device.queryCustomOperations(fomac::CustomProperty::Custom2); + ASSERT_TRUE(empty.has_value()); + EXPECT_TRUE(empty->empty()); + EXPECT_THROW(static_cast(device.queryCustomOperations( + fomac::CustomProperty::Custom3)), + std::invalid_argument); +} + +TEST(DeviceRegistrationTest, CustomOperationRetainsOwningDeviceSession) { + registerSessionTestDevice(); + const auto operation = [] { + const auto operations = + fomac::Session::openDevice("test.session-overrides") + .queryCustomOperations(fomac::CustomProperty::Custom1); + if (!operations.has_value() || operations->empty()) { + throw std::runtime_error("Test provider returned no custom operations"); + } + return operations->front(); + }(); + + EXPECT_EQ(operation.getName(), "custom-rx"); + EXPECT_EQ(operation.getQubitsNum(), 1); +} + TEST(DeviceRegistrationTest, FreshJobRetainsItsDeviceSession) { registerSessionTestDevice(); std::optional job; From fb4fa309bddafca27f3753bf743dd3d8c232d363 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Fri, 14 Aug 2026 17:58:49 +0200 Subject: [PATCH 05/35] =?UTF-8?q?=E2=9C=A8=20Backport=20the=20QDMI=20Slurm?= =?UTF-8?q?=20license=20selector?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the mechanism-specific C++ and Python adapters that open the registered device named by one local Slurm license environment value. Preserve the selection-only trust boundary and provider-owned authorization. Backports #2025. Assisted-by: GPT-5.6 via Codex --- CHANGELOG.md | 3 + bindings/qdmi/qdmi.cpp | 5 + bindings/qdmi/slurm.cpp | 46 ++++++ docs/qdmi/configuration.md | 42 ++++++ include/mqt-core/fomac/Slurm.hpp | 39 +++++ include/mqt-core/qdmi/devices/dd/Device.hpp | 2 +- python/mqt/core/qdmi/__init__.pyi | 1 + python/mqt/core/qdmi/slurm.pyi | 34 +++++ src/fomac/CMakeLists.txt | 13 +- src/fomac/Slurm.cpp | 142 ++++++++++++++++++ test/fomac/CMakeLists.txt | 6 +- test/fomac/test_slurm.cpp | 158 ++++++++++++++++++++ test/python/qdmi/test_slurm.py | 37 +++++ test/qdmi/devices/dd/device_status_test.cpp | 9 +- test/qdmi/driver/session_device.cpp | 28 ++++ 15 files changed, 555 insertions(+), 10 deletions(-) create mode 100644 bindings/qdmi/slurm.cpp create mode 100644 include/mqt-core/fomac/Slurm.hpp create mode 100644 python/mqt/core/qdmi/slurm.pyi create mode 100644 src/fomac/Slurm.cpp create mode 100644 test/fomac/test_slurm.cpp create mode 100644 test/python/qdmi/test_slurm.py diff --git a/CHANGELOG.md b/CHANGELOG.md index da5f5c1fef..6adccbca49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ releases may include breaking changes. ### Added +- ✨ Add C++ and Python adapters that open the QDMI device named by one local + Slurm license environment value ([#2025]) ([**@burgholzer**]) - ✨ Add generic C++ and Python FoMaC support for custom device properties that contain operation handles ([#2042]) ([**@burgholzer**]) - ✨ Support retrieving existing jobs by ID through the QDMI client API and C++ @@ -640,6 +642,7 @@ changelogs._ [#2074]: https://github.com/munich-quantum-toolkit/core/pull/2074 [#2042]: https://github.com/munich-quantum-toolkit/core/pull/2042 +[#2025]: https://github.com/munich-quantum-toolkit/core/pull/2025 [#2010]: https://github.com/munich-quantum-toolkit/core/pull/2010 [#2008]: https://github.com/munich-quantum-toolkit/core/pull/2008 [#2005]: https://github.com/munich-quantum-toolkit/core/pull/2005 diff --git a/bindings/qdmi/qdmi.cpp b/bindings/qdmi/qdmi.cpp index aeca9f0eff..1b070b6545 100644 --- a/bindings/qdmi/qdmi.cpp +++ b/bindings/qdmi/qdmi.cpp @@ -36,6 +36,10 @@ namespace mqt { namespace nb = nanobind; using namespace nb::literals; +namespace bindings { +void registerSlurm(nb::module_& qdmiModule); +} + namespace { void warnAboutLegacySession() { constexpr auto message = @@ -124,6 +128,7 @@ NB_MODULE(MQT_CORE_MODULE_NAME, qdmiModule) { qdmiModule.doc() = "QDMI entities and access to MQT Core's QDMI driver."; auto driver = qdmiModule.def_submodule( "driver", "Register, discover, and open QDMI devices through MQT Core."); + bindings::registerSlurm(qdmiModule); // Session class auto session = nb::class_(driver, "Session", diff --git a/bindings/qdmi/slurm.cpp b/bindings/qdmi/slurm.cpp new file mode 100644 index 0000000000..ad4b8a36bf --- /dev/null +++ b/bindings/qdmi/slurm.cpp @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "fomac/Slurm.hpp" + +#include + +namespace nb = nanobind; + +namespace mqt::bindings { + +// NOLINTNEXTLINE(misc-use-internal-linkage) +void registerSlurm(nb::module_& qdmiModule) { + auto slurm = qdmiModule.def_submodule( + "slurm", "Open a QDMI device named by the Slurm license environment."); + slurm.def("open_device_from_license", &fomac::slurm::openDeviceFromLicense, + R"pb(Open the QDMI device named by the Slurm license environment. + +``SLURM_JOB_LICENSES`` must contain one local license whose name equals a +registered QDMI device ID. The optional count must be one. The function opens a +fresh device session from the persistent definition and accepts device status +``IDLE`` or ``BUSY``. It does not apply job-specific QDMI configuration or +credentials. + +Warning: + ``SLURM_JOB_LICENSES`` is process-mutable. This function uses it only for + device selection. It does not verify a Slurm allocation, authenticate the + caller, or authorize device access. The provider or operating system must + enforce access independently. + +Returns: + mqt.core.qdmi.Device: The fresh device session. + +Raises: + RuntimeError: If the license value or named device does not satisfy this + contract.)pb"); +} + +} // namespace mqt::bindings diff --git a/docs/qdmi/configuration.md b/docs/qdmi/configuration.md index 1bfc3a948e..0c15ba1e2d 100644 --- a/docs/qdmi/configuration.md +++ b/docs/qdmi/configuration.md @@ -171,6 +171,48 @@ Multiple definitions may refer to the same library and prefix. MQT Core reuses the initialized library while creating a fresh QDMI device session, with its own session parameters, for every definition. +## Selecting a device from a Slurm license environment + +MQT Core provides a mechanism-specific adapter for jobs that use local Slurm +licenses for cluster-wide admission. The license name must equal one registered +QDMI device ID. Each job must request one license. For example: + +```bash +sbatch --licenses=mqt.ddsim.default:1 simulation.sh +``` + +The job can then open the named device: + +```python +from mqt.core.qdmi import slurm + +device = slurm.open_device_from_license() +``` + +The equivalent C++ function is `fomac::slurm::openDeviceFromLicense()` from +`fomac/Slurm.hpp`. Both functions read `SLURM_JOB_LICENSES`. They accept only +`` or `:1`. They reject remote, +compound, and non-unit license values. + +The adapter opens a fresh device session from the persistent definition. It does +not replace configuration or inject credentials. Each provider defines its own +credential sources. The adapter accepts QDMI device status `IDLE` and `BUSY`. It +rejects all other device states. + +`SLURM_JOB_LICENSES` is process-mutable. The adapter uses this value only for +device selection. It does not verify that Slurm allocated the license. It does +not authenticate the caller or authorize access to the device. Provider +credentials must authorize remote devices. The operating system must isolate a +local device when access requires enforcement. A caller can also bypass this +adapter and call {py:func}`~mqt.core.qdmi.driver.open_device` with a stable +device ID. A different Slurm lookup would therefore not make MQT Core an access +control boundary. + +A cluster can configure more than one license for a device. For example, +`mqt.ddsim.default:2` permits two independent jobs to request one license each. +The count is a Slurm admission limit. It is not an access permission, a provider +availability check, or a provider queue length. + ## Relocatable packages and static consumers Built-in targets generate manifests beside their runtime libraries in both build diff --git a/include/mqt-core/fomac/Slurm.hpp b/include/mqt-core/fomac/Slurm.hpp new file mode 100644 index 0000000000..7b2348d5e2 --- /dev/null +++ b/include/mqt-core/fomac/Slurm.hpp @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +/** @file Slurm.hpp + * @brief FoMaC adapter for selecting a QDMI device from a Slurm license + * environment value. + */ + +#pragma once + +#include "fomac/FoMaC.hpp" + +namespace fomac::slurm { + +/** + * @brief Opens the QDMI device named by the Slurm license environment. + * @return A fresh device session using the registered device definition. + * @details The @c SLURM_JOB_LICENSES value must contain exactly one local + * license. Its name must equal a registered QDMI device ID. The optional + * license count must be one. The device must report @c QDMI_DEVICE_STATUS_IDLE + * or @c QDMI_DEVICE_STATUS_BUSY. + * @warning This function uses process-mutable environment data for device + * selection. It does not verify a Slurm allocation, authenticate the caller, + * or authorize access to the device. The provider or operating system must + * enforce access independently. + * @throws std::runtime_error If the license value is missing, malformed, + * compound, remote, has a non-unit count, names an unknown device, or names a + * device in another state. + */ +[[nodiscard]] Device openDeviceFromLicense(); + +} // namespace fomac::slurm diff --git a/include/mqt-core/qdmi/devices/dd/Device.hpp b/include/mqt-core/qdmi/devices/dd/Device.hpp index 38b7c5e203..fef3a9cec4 100644 --- a/include/mqt-core/qdmi/devices/dd/Device.hpp +++ b/include/mqt-core/qdmi/devices/dd/Device.hpp @@ -45,7 +45,7 @@ class Device final : public Singleton { size_t qubitsNum_ = 0; /// The status of the device. - std::atomic status_{QDMI_DEVICE_STATUS_OFFLINE}; + std::atomic status_{QDMI_DEVICE_STATUS_IDLE}; /// The list of device sessions. std::unordered_map mqt.core.qdmi.Device: + """Open the QDMI device named by the Slurm license environment. + + ``SLURM_JOB_LICENSES`` must contain one local license whose name equals a + registered QDMI device ID. The optional count must be one. The function opens a + fresh device session from the persistent definition and accepts device status + ``IDLE`` or ``BUSY``. It does not apply job-specific QDMI configuration or + credentials. + + Warning: + ``SLURM_JOB_LICENSES`` is process-mutable. This function uses it only for + device selection. It does not verify a Slurm allocation, authenticate the + caller, or authorize device access. The provider or operating system must + enforce access independently. + + Returns: + mqt.core.qdmi.Device: The fresh device session. + + Raises: + RuntimeError: If the license value or named device does not satisfy this + contract. + """ diff --git a/src/fomac/CMakeLists.txt b/src/fomac/CMakeLists.txt index 631141c998..91ac5bb0aa 100644 --- a/src/fomac/CMakeLists.txt +++ b/src/fomac/CMakeLists.txt @@ -13,11 +13,18 @@ if(NOT TARGET ${TARGET_NAME}) add_mqt_core_library(${TARGET_NAME} ALIAS_NAME FoMaC) # Add sources to target - target_sources(${TARGET_NAME} PRIVATE FoMaC.cpp) + target_sources(${TARGET_NAME} PRIVATE FoMaC.cpp Slurm.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) + target_sources( + ${TARGET_NAME} + PUBLIC FILE_SET + HEADERS + BASE_DIRS + ${MQT_CORE_INCLUDE_BUILD_DIR} + FILES + ${MQT_CORE_INCLUDE_BUILD_DIR}/fomac/FoMaC.hpp + ${MQT_CORE_INCLUDE_BUILD_DIR}/fomac/Slurm.hpp) # Add link libraries target_link_libraries( diff --git a/src/fomac/Slurm.cpp b/src/fomac/Slurm.cpp new file mode 100644 index 0000000000..55c7a7b06c --- /dev/null +++ b/src/fomac/Slurm.cpp @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "fomac/Slurm.hpp" + +#include "fomac/FoMaC.hpp" +#include "qdmi/driver/Driver.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fomac::slurm { +namespace { + +[[nodiscard]] auto statusName(const QDMI_Device_Status status) + -> std::string_view { + switch (status) { + case QDMI_DEVICE_STATUS_OFFLINE: + return "OFFLINE"; + case QDMI_DEVICE_STATUS_IDLE: + return "IDLE"; + case QDMI_DEVICE_STATUS_BUSY: + return "BUSY"; + case QDMI_DEVICE_STATUS_ERROR: + return "ERROR"; + case QDMI_DEVICE_STATUS_MAINTENANCE: + return "MAINTENANCE"; + case QDMI_DEVICE_STATUS_CALIBRATION: + return "CALIBRATION"; + case QDMI_DEVICE_STATUS_MAX: + return "UNKNOWN"; + } + return "UNKNOWN"; +} + +[[nodiscard]] auto parseLicense(const std::string& licenseSpec, + const std::vector& registeredIds) + -> std::string { + if (licenseSpec.empty()) { + throw std::runtime_error( + "SLURM_JOB_LICENSES is not set or empty; no QDMI device license is " + "available"); + } + if (std::ranges::any_of(licenseSpec, [](const unsigned char character) { + return std::isspace(character) != 0; + })) { + throw std::runtime_error("SLURM_JOB_LICENSES must not contain whitespace"); + } + if (licenseSpec.find(',') != std::string::npos) { + throw std::runtime_error( + "SLURM_JOB_LICENSES uses a compound AND expression; exactly one QDMI " + "device license is required"); + } + if (licenseSpec.find('|') != std::string::npos) { + throw std::runtime_error( + "SLURM_JOB_LICENSES uses a compound OR expression; exactly one QDMI " + "device license is required"); + } + if (licenseSpec.find('@') != std::string::npos) { + throw std::runtime_error( + "A remote Slurm license cannot select a QDMI device"); + } + + const auto countSeparator = licenseSpec.find(':'); + if (countSeparator != std::string::npos && + licenseSpec.find(':', countSeparator + 1) != std::string::npos) { + throw std::runtime_error("SLURM_JOB_LICENSES contains a malformed license"); + } + const auto deviceId = licenseSpec.substr(0, countSeparator); + if (deviceId.empty()) { + throw std::runtime_error("SLURM_JOB_LICENSES contains a malformed license"); + } + + if (countSeparator != std::string::npos) { + const std::string countText = licenseSpec.substr(countSeparator + 1); + if (countText.empty()) { + throw std::runtime_error( + "SLURM_JOB_LICENSES contains a malformed license count"); + } + size_t count = 0; + const char* const countBegin = countText.data(); + const auto countLength = static_cast(countText.size()); + const char* const countEnd = std::next(countBegin, countLength); + const auto [parsedEnd, error] = + std::from_chars(countBegin, countEnd, count); + if (error != std::errc{} || parsedEnd != countEnd) { + throw std::runtime_error( + "SLURM_JOB_LICENSES contains an invalid license count"); + } + if (count != 1) { + throw std::runtime_error( + "A QDMI device job must request exactly one Slurm license"); + } + } + + if (std::ranges::find(registeredIds, deviceId) == registeredIds.end()) { + throw std::runtime_error("Slurm license '" + deviceId + + "' is not a registered QDMI device ID"); + } + return deviceId; +} + +} // namespace + +Device openDeviceFromLicense() { + // The job can modify its environment. Use this value only to select a + // registered device; the provider or operating system must authorize access. + const auto* const environmentValue = std::getenv("SLURM_JOB_LICENSES"); + const std::string licenseSpec = + environmentValue == nullptr ? std::string{} : environmentValue; + const auto deviceId = + parseLicense(licenseSpec, qdmi::Driver::get().registeredDeviceIds()); + + auto device = Session::openDevice(deviceId); + const auto status = device.getStatus(); + if (status != QDMI_DEVICE_STATUS_IDLE && status != QDMI_DEVICE_STATUS_BUSY) { + throw std::runtime_error("SLURM_JOB_LICENSES names QDMI device '" + + deviceId + "' with status " + + std::string(statusName(status))); + } + return device; +} + +} // namespace fomac::slurm diff --git a/test/fomac/CMakeLists.txt b/test/fomac/CMakeLists.txt index 5d435c3927..18de5002d4 100644 --- a/test/fomac/CMakeLists.txt +++ b/test/fomac/CMakeLists.txt @@ -9,6 +9,10 @@ set(TARGET_NAME mqt-core-fomac-test) if(TARGET MQT::CoreFoMaC) - package_add_test(${TARGET_NAME} MQT::CoreFoMaC test_fomac.cpp) + package_add_test(${TARGET_NAME} MQT::CoreFoMaC test_fomac.cpp test_slurm.cpp) + target_compile_definitions( + ${TARGET_NAME} + PRIVATE "MQT_CORE_FOMAC_SLURM_TEST_DEVICE=\"$\"") + add_dependencies(${TARGET_NAME} mqt-core-qdmi-session-device) mqt_copy_qdmi_runtime(${TARGET_NAME}) endif() diff --git a/test/fomac/test_slurm.cpp b/test/fomac/test_slurm.cpp new file mode 100644 index 0000000000..28847dc6be --- /dev/null +++ b/test/fomac/test_slurm.cpp @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "fomac/Slurm.hpp" +#include "qdmi/driver/Driver.hpp" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace fomac::slurm { +namespace { + +class ScopedSlurmLicenses { +public: + explicit ScopedSlurmLicenses(const std::optional& value) + : originalValue(getValue()) { + setValue(value); + } + + ~ScopedSlurmLicenses() { setValue(originalValue); } + + ScopedSlurmLicenses(const ScopedSlurmLicenses&) = delete; + ScopedSlurmLicenses& operator=(const ScopedSlurmLicenses&) = delete; + ScopedSlurmLicenses(ScopedSlurmLicenses&&) = delete; + ScopedSlurmLicenses& operator=(ScopedSlurmLicenses&&) = delete; + +private: + [[nodiscard]] static auto getValue() -> std::optional { + if (const auto* const value = std::getenv("SLURM_JOB_LICENSES")) { + return value; + } + return std::nullopt; + } + + static void setValue(const std::optional& value) { +#ifdef _WIN32 + if (_putenv_s("SLURM_JOB_LICENSES", value.value_or("").c_str()) != 0) { + std::abort(); + } +#else + int result = 0; + if (value.has_value()) { + // NOLINTNEXTLINE(misc-include-cleaner) + result = setenv("SLURM_JOB_LICENSES", value->c_str(), 1); + } else { + // NOLINTNEXTLINE(misc-include-cleaner) + result = unsetenv("SLURM_JOB_LICENSES"); + } + if (result != 0) { + std::abort(); + } +#endif + } + + std::optional originalValue; +}; + +void registerStatusDevice(const std::string& id, + const std::string& configuredStatus) { + static_cast(qdmi::Driver::get().registerDeviceIfAbsent( + {.id = id, + .library = MQT_CORE_FOMAC_SLURM_TEST_DEVICE, + .prefix = "TEST_SESSION", + .session = {.custom4 = configuredStatus}})); +} + +} // namespace + +TEST(SlurmAdapterTest, AcceptsImplicitAndExplicitUnitCounts) { + registerStatusDevice("test.slurm.idle", "idle"); + for (const auto* const value : {"test.slurm.idle", "test.slurm.idle:1"}) { + const ScopedSlurmLicenses licenses(value); + EXPECT_EQ(openDeviceFromLicense().getStatus(), QDMI_DEVICE_STATUS_IDLE); + } +} + +TEST(SlurmAdapterTest, AcceptsBusyDevice) { + registerStatusDevice("test.slurm.busy", "busy"); + const ScopedSlurmLicenses licenses("test.slurm.busy"); + EXPECT_EQ(openDeviceFromLicense().getStatus(), QDMI_DEVICE_STATUS_BUSY); +} + +TEST(SlurmAdapterTest, RejectsMissingAndMalformedValues) { + registerStatusDevice("test.slurm.grammar", "idle"); + const std::array, 13> invalidValues{ + std::nullopt, + "", + " test.slurm.grammar", + "test.slurm.grammar ", + ":1", + "test.slurm.grammar:", + "test.slurm.grammar:+1", + "test.slurm.grammar:-1", + "test.slurm.grammar:1x", + "test.slurm.grammar:0", + "test.slurm.grammar:2", + "test.slurm.grammar:184467440737095516160", + "test.slurm.grammar:1:1", + }; + + for (const auto& value : invalidValues) { + const ScopedSlurmLicenses licenses(value); + EXPECT_THROW(static_cast(openDeviceFromLicense()), std::runtime_error) + << "value: " << value.value_or(""); + } +} + +TEST(SlurmAdapterTest, RejectsUnknownRemoteAndCompoundLicenses) { + registerStatusDevice("test.slurm.single", "idle"); + constexpr std::array invalidValues{ + "test.slurm.unknown", "test.slurm.single@license-server:1", + "test.slurm.single,unrelated", "unrelated,test.slurm.single", + "test.slurm.single|unrelated", "unrelated|test.slurm.single", + }; + + for (const auto* const value : invalidValues) { + const ScopedSlurmLicenses licenses(value); + EXPECT_THROW(static_cast(openDeviceFromLicense()), std::runtime_error) + << "value: " << value; + } +} + +TEST(SlurmAdapterTest, RejectsUnavailableDeviceWithIdAndStatus) { + constexpr std::array rejectedStates{ + std::pair{"offline", "OFFLINE"}, + std::pair{"error", "ERROR"}, + std::pair{"maintenance", "MAINTENANCE"}, + std::pair{"calibration", "CALIBRATION"}, + std::pair{"max", "UNKNOWN"}, + }; + + for (const auto& [configuredStatus, reportedStatus] : rejectedStates) { + const auto id = std::string{"test.slurm."} + configuredStatus; + registerStatusDevice(id, configuredStatus); + const ScopedSlurmLicenses licenses(id); + EXPECT_THAT( + [] { return openDeviceFromLicense(); }, + testing::ThrowsMessage(testing::AllOf( + testing::HasSubstr(id), testing::HasSubstr(reportedStatus)))); + } +} + +} // namespace fomac::slurm diff --git a/test/python/qdmi/test_slurm.py b/test/python/qdmi/test_slurm.py new file mode 100644 index 0000000000..2551b3c0f7 --- /dev/null +++ b/test/python/qdmi/test_slurm.py @@ -0,0 +1,37 @@ +# 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 + +"""Test the mechanism-specific Slurm adapter.""" + +from __future__ import annotations + +import pytest + +from mqt.core.qdmi import slurm + + +def test_open_device_from_license(monkeypatch: pytest.MonkeyPatch) -> None: + """Open a fresh registered DDSIM device from a unit license.""" + monkeypatch.setenv("SLURM_JOB_LICENSES", "mqt.ddsim.default:1") + device = slurm.open_device_from_license() + assert device.name() == "MQT Core DDSIM QDMI Device" + + +@pytest.mark.parametrize( + "value", + [ + "mqt.ddsim.default:2", + "mqt.ddsim.default,mqt.sc.default", + "mqt.ddsim.default|mqt.sc.default", + ], +) +def test_reject_non_unit_and_compound_licenses(monkeypatch: pytest.MonkeyPatch, value: str) -> None: + """Reject license requests that cannot select one QDMI device.""" + monkeypatch.setenv("SLURM_JOB_LICENSES", value) + with pytest.raises(RuntimeError): + slurm.open_device_from_license() diff --git a/test/qdmi/devices/dd/device_status_test.cpp b/test/qdmi/devices/dd/device_status_test.cpp index 2943ebf93c..58e144847d 100644 --- a/test/qdmi/devices/dd/device_status_test.cpp +++ b/test/qdmi/devices/dd/device_status_test.cpp @@ -8,9 +8,7 @@ * Licensed under the MIT License */ -/* - * DDSIM QDMI Device - Device status transitions (OFFLINE/BUSY/IDLE) - */ +/* DDSIM QDMI device status transitions. */ #include "helpers/circuits.hpp" #include "helpers/test_utils.hpp" #include "mqt_ddsim_qdmi/constants.h" @@ -35,8 +33,9 @@ QDMI_Device_Status queryStatus(MQT_DDSIM_QDMI_Device_Session session) { TEST(DeviceStatus, TransitionsBusyThenIdleAfterJob) { const qdmi_test::SessionGuard s{}; - // Initial status can be OFFLINE depending on implementation; do not assert - // it. Submit a job to force BUSY then completion to IDLE. + EXPECT_EQ(queryStatus(s.session), QDMI_DEVICE_STATUS_IDLE); + + // Submit a job to force BUSY, then wait for the return to IDLE. const qdmi_test::JobGuard j{s.session}; ASSERT_EQ(qdmi_test::setProgram(j.job, QDMI_PROGRAM_FORMAT_QASM3, qdmi_test::QASM3_HEAVY_SAMPLING), diff --git a/test/qdmi/driver/session_device.cpp b/test/qdmi/driver/session_device.cpp index c7af106d74..17e7b32d28 100644 --- a/test/qdmi/driver/session_device.cpp +++ b/test/qdmi/driver/session_device.cpp @@ -53,6 +53,29 @@ namespace { return ""; } +[[nodiscard]] auto deviceStatus(const std::string& configuredStatus) + -> QDMI_Device_Status { + if (configuredStatus == "busy") { + return QDMI_DEVICE_STATUS_BUSY; + } + if (configuredStatus == "offline") { + return QDMI_DEVICE_STATUS_OFFLINE; + } + if (configuredStatus == "error") { + return QDMI_DEVICE_STATUS_ERROR; + } + if (configuredStatus == "maintenance") { + return QDMI_DEVICE_STATUS_MAINTENANCE; + } + if (configuredStatus == "calibration") { + return QDMI_DEVICE_STATUS_CALIBRATION; + } + if (configuredStatus == "max") { + return QDMI_DEVICE_STATUS_MAX; + } + return QDMI_DEVICE_STATUS_IDLE; +} + [[nodiscard]] auto childDeviceHandle() -> QDMI_Child_Device { static QDMI_Child_Device_impl_d child; return &child; @@ -237,6 +260,11 @@ extern "C" int TEST_SESSION_QDMI_device_session_query_device_property( } return value == nullptr ? QDMI_SUCCESS : QDMI_ERROR_INVALIDARGUMENT; } + if (prop == QDMI_DEVICE_PROPERTY_STATUS) { + return queryValue( + deviceStatus(parameter(session, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM4)), + size, value, sizeRet); + } if (prop != QDMI_DEVICE_PROPERTY_NAME) { return QDMI_ERROR_NOTSUPPORTED; } From a938de553ab639f5399cf90310795db1931986c1 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Fri, 14 Aug 2026 17:59:40 +0200 Subject: [PATCH 06/35] =?UTF-8?q?=F0=9F=A7=AA=20Backport=20the=20QDMI=20Sl?= =?UTF-8?q?urm=20integration=20fixture?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the modern two-node Slurm admission workflow and trust-boundary tutorial for the v3 QDMI namespace. Build the test wheel with the v3 LLVM and MLIR 21 toolchain. Backports #2043. Assisted-by: GPT-5.6 via Codex --- .github/workflows/slurm.yml | 69 +++++ CHANGELOG.md | 3 + docs/qdmi/index.md | 1 + docs/qdmi/slurm.md | 218 +++++++++++++++ pyproject.toml | 1 + test/slurm/.dockerignore | 1 + test/slurm/.gitignore | 2 + test/slurm/Dockerfile | 46 ++++ test/slurm/bell_job.py | 75 ++++++ test/slurm/cgroup.conf | 12 + test/slurm/compose.yml | 107 ++++++++ test/slurm/ddsim-job.sh | 12 + test/slurm/mqt-slurm-prepare | 17 ++ test/slurm/mqt-slurm-prepare.service | 20 ++ test/slurm/run_integration.py | 385 +++++++++++++++++++++++++++ test/slurm/sc-job.sh | 12 + test/slurm/sc_job.py | 35 +++ test/slurm/slurm.conf | 34 +++ 18 files changed, 1050 insertions(+) create mode 100644 .github/workflows/slurm.yml create mode 100644 docs/qdmi/slurm.md create mode 100644 test/slurm/.dockerignore create mode 100644 test/slurm/.gitignore create mode 100644 test/slurm/Dockerfile create mode 100644 test/slurm/bell_job.py create mode 100644 test/slurm/cgroup.conf create mode 100644 test/slurm/compose.yml create mode 100755 test/slurm/ddsim-job.sh create mode 100755 test/slurm/mqt-slurm-prepare create mode 100644 test/slurm/mqt-slurm-prepare.service create mode 100644 test/slurm/run_integration.py create mode 100755 test/slurm/sc-job.sh create mode 100644 test/slurm/sc_job.py create mode 100644 test/slurm/slurm.conf diff --git a/.github/workflows/slurm.yml b/.github/workflows/slurm.yml new file mode 100644 index 0000000000..04c282574d --- /dev/null +++ b/.github/workflows/slurm.yml @@ -0,0 +1,69 @@ +name: Slurm integration + +on: + push: + branches: + - main + paths: + - ".github/workflows/slurm.yml" + - "bindings/qdmi/**" + - "cmake/**" + - "include/mqt-core/fomac/**" + - "include/mqt-core/qdmi/**" + - "pyproject.toml" + - "python/mqt/core/qdmi/**" + - "src/fomac/**" + - "src/qdmi/**" + - "test/slurm/**" + - "uv.lock" + pull_request: + paths: + - ".github/workflows/slurm.yml" + - "bindings/qdmi/**" + - "cmake/**" + - "include/mqt-core/fomac/**" + - "include/mqt-core/qdmi/**" + - "pyproject.toml" + - "python/mqt/core/qdmi/**" + - "src/fomac/**" + - "src/qdmi/**" + - "test/slurm/**" + - "uv.lock" + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + slurm: + name: Slurm 25.11 and QDMI devices + runs-on: ubuntu-26.04 + timeout-minutes: 45 + env: + UV_NO_PROGRESS: "1" + steps: + - name: Check out MQT Core + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + + - name: Set up uv and Python + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + with: + enable-cache: true + python-version: "3.14" + + - name: Set up MLIR + uses: munich-quantum-software/setup-mlir@fc08baf0a172e98008e6528d465de3b5dd99bc68 # v1.4.1 + with: + llvm-version: 21.1.8 + + - name: Build the MQT Core wheel + run: uv build --wheel --out-dir test/slurm/dist + + - name: Test Slurm admission and QDMI execution + run: uv run --no-project --python 3.14 test/slurm/run_integration.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 6adccbca49..e62d5049bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ releases may include breaking changes. ### Added +- 🧪 Test static Slurm license admission and QDMI execution with DDSIM and SC, + and document the cluster setup ([#2043]) ([**@burgholzer**]) - ✨ Add C++ and Python adapters that open the QDMI device named by one local Slurm license environment value ([#2025]) ([**@burgholzer**]) - ✨ Add generic C++ and Python FoMaC support for custom device properties that @@ -641,6 +643,7 @@ changelogs._ [#2074]: https://github.com/munich-quantum-toolkit/core/pull/2074 +[#2043]: https://github.com/munich-quantum-toolkit/core/pull/2043 [#2042]: https://github.com/munich-quantum-toolkit/core/pull/2042 [#2025]: https://github.com/munich-quantum-toolkit/core/pull/2025 [#2010]: https://github.com/munich-quantum-toolkit/core/pull/2010 diff --git a/docs/qdmi/index.md b/docs/qdmi/index.md index db2228ee36..3479f88080 100644 --- a/docs/qdmi/index.md +++ b/docs/qdmi/index.md @@ -16,6 +16,7 @@ NA QDMI Device DDSIM QDMI Device QDMI Driver QDMI device configuration +Slurm integration QDMI-Qiskit Backend PennyLane interface for QDMI devices ``` diff --git a/docs/qdmi/slurm.md b/docs/qdmi/slurm.md new file mode 100644 index 0000000000..580283b3bd --- /dev/null +++ b/docs/qdmi/slurm.md @@ -0,0 +1,218 @@ +# Use QDMI devices with Slurm + +This example uses Slurm 25.11 or newer on Ubuntu 26.04. It has one controller, +two compute nodes, and two CPUs on each compute node. + +A Slurm license controls admission to a cluster-wide resource. In this setup, +the license name is a stable QDMI device ID. A license does not show provider +availability. It does not show the device queue. The QDMI provider supplies that +information when its interface supports it. + +## Understand the control boundaries + +This example is suitable for admission and accounting tests on a cooperative +cluster. It does not make a Slurm license an access-control credential. The +controls are independent: + +- Slurm admits jobs and accounts for the configured license count. +- The MQT Core adapter uses the license environment to select a registered QDMI + device. +- The QDMI provider reports device availability and queue data. +- The provider or the operating system authorizes access to the device. + +`SLURM_JOB_LICENSES` is process-mutable. A job can change it before it calls +`slurm.open_device_from_license()`. Thus, the function does not prove that Slurm +allocated the named license. It does not authenticate the user. It does not +authorize access. A lookup through another Slurm interface would not make MQT +Core an access-control boundary because a program can also call +`driver.open_device(device_id)` directly. + +## Install the software + +Install the same MQT Core package on each compute node. The package contains the +MQT Core QDMI interface and the bundled QDMI devices. You can use a shared +software environment or install the same wheel on each node. + +Install Slurm, Munge, and systemd. Start Munge before Slurm. Use the same Munge +key on all nodes. Keep this key outside the QDMI device configuration. + +Use the unified cgroup v2 hierarchy. Add these settings to `slurm.conf`: + +```ini +ProctrackType=proctrack/cgroup +TaskPlugin=task/cgroup,task/affinity +JobAcctGatherType=jobacct_gather/cgroup +SelectType=select/cons_tres +SelectTypeParameters=CR_CPU +``` + +Use the cgroup plugin to constrain processors and memory. For example, use this +`cgroup.conf`: + +```ini +CgroupPlugin=autodetect +ConstrainCores=yes +ConstrainRAMSpace=yes +ConstrainSwapSpace=yes +``` + +This fixture has no local device file. For strict isolation of a local device, +configure it as a Slurm GRES with a `File=` entry. Also set +`ConstrainDevices=yes` for the cgroup task plugin. Slurm can then restrict the +device files that a job can open. See the [Slurm GRES configuration] +documentation. A remote QPU has no local device file, so the provider must +enforce its authorization. + +Run `slurmd -C` on each compute node and use its output for the `NodeName` +record. The MQT Core test uses two CPUs on `node1` and `node2`. + +## Register the devices + +MQT Core installs persistent definitions for `mqt.ddsim.default` and +`mqt.sc.default`. You do not have to add another registry file for these two +devices. You can verify the stable IDs before you configure Slurm: + +```console +python -c "from mqt.core.qdmi import driver; print(*driver.registered_device_ids(), sep='\n')" +``` + +For an external provider, install its shared library and QDMI manifest. You can +also add one trusted system registry file at `/etc/mqt-core/qdmi.json`. The `id` +field in that file is the stable device ID. Use the same ID as the local Slurm +license name. Do not put short-lived access tokens in this file. Use the +authentication method that the provider documents for batch jobs. + +The Slurm adapter does not supply credentials. IQM can use its configured token +source. Amazon Braket uses the AWS credential provider chain, such as an +instance role, workload identity, or AWS profile. A job can also export provider +configuration. Use provider-scoped credentials with minimum permissions. Do not +store access tokens or AWS access keys in a persistent device definition. + +Add the licenses to `slurm.conf` on the controller: + +```ini +Licenses=mqt.ddsim.default:2,mqt.sc.default:1 +``` + +The DDSIM count is two. Therefore, Slurm can admit two jobs that each request +one DDSIM license. These jobs can run on different nodes. The count is a local +cluster policy. It is not a DDSIM property and it is not a per-node count. + +Restart `slurmctld` after you first add the licenses. Reconfigure the compute +nodes as required by your Slurm installation. Then inspect the configured +resources: + +```console +scontrol show lic +``` + +The initial report must contain these values: + +```text +LicenseName=mqt.ddsim.default Total=2 Used=0 Free=2 Remote=no +LicenseName=mqt.sc.default Total=1 Used=0 Free=1 Remote=no +``` + +## Submit a DDSIM job + +Save this program as `bell.py` in a location that all compute nodes can read: + +```python +from mqt.core.qdmi import ProgramFormat, slurm + +program = """OPENQASM 2.0; +include "qelib1.inc"; +qreg q[2]; +creg c[2]; +h q[0]; +cx q[0], q[1]; +measure q -> c; +""" + +device = slurm.open_device_from_license() +job = device.submit_job(program, ProgramFormat.QASM2, num_shots=256) +if not job.wait(60): + raise RuntimeError("DDSIM did not finish within 60 seconds") + +counts = job.get_counts() +if sum(counts.values()) != 256 or not set(counts) <= {"00", "11"}: + raise RuntimeError(f"Invalid Bell results: {counts}") +print(counts) +``` + +Save this batch script as `bell.sbatch`: + +```bash +#!/bin/bash +#SBATCH --nodes=1 +#SBATCH --ntasks=1 +#SBATCH --cpus-per-task=1 +#SBATCH --licenses=mqt.ddsim.default:1 +#SBATCH --output=bell-%j.out + +set -euo pipefail +python bell.py +``` + +The adapter reads `SLURM_JOB_LICENSES` and selects the persistent device +definition with the same ID. It requires one unambiguous QDMI device license. It +opens a fresh device session and checks the device status. The function accepts +`IDLE` and `BUSY`. This check is not authorization. The provider can still +reject a later submission or put the quantum task in its device queue. + +Submit the job with this command: + +```console +sbatch bell.sbatch +``` + +## Check concurrent jobs + +For a scheduling test, add a sufficiently long classical post-processing step +after the Python command. For example, add `sleep 120` to `bell.sbatch`. Then +submit two jobs: + +```console +sbatch --nodelist=node1 bell.sbatch +sbatch --nodelist=node2 bell.sbatch +``` + +Both jobs can run because two DDSIM licenses exist. Each job uses one CPU. One +CPU remains free on each node. Submit a third DDSIM job. Slurm keeps it pending +until one DDSIM license becomes free. + +Use these commands to inspect the state: + +```console +squeue --format="%.18i %.9T %.20R %.12N %.20L" +scontrol show lic mqt.ddsim.default +scontrol show node node1 +scontrol show node node2 +``` + +The third job must have state `PENDING` and reason `Licenses`. The license +report must show `Total=2 Used=2 Free=0`. The node records must still show one +free CPU on each node. A job that requests `mqt.sc.default:1` can use one of +these CPUs because it uses a different license. + +When one DDSIM job ends, Slurm returns its license. The pending job can then +start without a change to the device registry or `slurm.conf`. + +## Diagnose a failure + +First, run `scontrol show job `. Check `JobState`, `Reason`, `Licenses`, +and `NodeList`. Use `scontrol show lic` to compare the total, used, and free +counts. Use `scontrol show node` to check CPU allocation. + +If a node is down, check `systemctl status munge slurmd` and the Slurm journal +on that node. Check that `/sys/fs/cgroup/cgroup.controllers` exists. Check that +all nodes use the same Munge key and the same `slurm.conf`. + +If MQT Core cannot select a device, print `SLURM_JOB_LICENSES` inside the batch +job and list the registered QDMI IDs. Use this value only to diagnose selection. +It is not proof of the Slurm allocation. The license name and stable ID must +match exactly. Do not add a generic device license. Do not use a Slurm OR +license expression for device selection because the environment does not +identify a single selected device in that case. + +[Slurm GRES configuration]: https://slurm.schedmd.com/gres.conf.html diff --git a/pyproject.toml b/pyproject.toml index 47c217c595..29328b10fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -239,6 +239,7 @@ known-first-party = ["mqt.core"] [tool.ruff.lint.per-file-ignores] "test/python/**" = ["T20", "INP001"] +"test/slurm/**" = ["DOC201", "DOC501", "INP001"] "docs/**" = ["T20", "INP001"] "noxfile.py" = ["T20", "TID251", "PLC0415"] "*.pyi" = ["D418", "E501", "PYI021"] diff --git a/test/slurm/.dockerignore b/test/slurm/.dockerignore new file mode 100644 index 0000000000..712f264ddd --- /dev/null +++ b/test/slurm/.dockerignore @@ -0,0 +1 @@ +runtime/ diff --git a/test/slurm/.gitignore b/test/slurm/.gitignore new file mode 100644 index 0000000000..2b9e435bc3 --- /dev/null +++ b/test/slurm/.gitignore @@ -0,0 +1,2 @@ +dist/ +runtime/ diff --git a/test/slurm/Dockerfile b/test/slurm/Dockerfile new file mode 100644 index 0000000000..ad58e91a19 --- /dev/null +++ b/test/slurm/Dockerfile @@ -0,0 +1,46 @@ +# 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 + +FROM ghcr.io/astral-sh/uv:0.12.1 AS uv + +FROM ubuntu:26.04 AS common + +ENV container=docker + +RUN apt-get update \ + && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \ + ca-certificates \ + dbus \ + munge \ + python3 \ + slurm-wlm \ + systemd \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=uv /uv /uvx /bin/ +COPY dist/*.whl /tmp/dist/ +COPY mqt-slurm-prepare /usr/local/sbin/mqt-slurm-prepare +COPY mqt-slurm-prepare.service /etc/systemd/system/mqt-slurm-prepare.service + +RUN uv pip install --system --no-deps --break-system-packages /tmp/dist/*.whl \ + && rm -rf /root/.cache/uv /tmp/dist \ + && chmod 0755 /usr/local/sbin/mqt-slurm-prepare \ + && systemctl enable mqt-slurm-prepare.service munge.service + +STOPSIGNAL SIGRTMIN+3 + +CMD ["/usr/lib/systemd/systemd"] + +FROM common AS controller + +RUN systemctl enable slurmctld.service + +FROM common AS compute + +RUN systemctl enable slurmd.service diff --git a/test/slurm/bell_job.py b/test/slurm/bell_job.py new file mode 100644 index 0000000000..e16cca6cff --- /dev/null +++ b/test/slurm/bell_job.py @@ -0,0 +1,75 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Execute a Bell circuit on the DDSIM device named by the license environment.""" + +from __future__ import annotations + +import argparse +import json +import os +import time +from pathlib import Path + +from mqt.core.qdmi import ProgramFormat, slurm + +BELL_PROGRAM = """OPENQASM 2.0; +include "qelib1.inc"; +qreg q[2]; +creg c[2]; +h q[0]; +cx q[0], q[1]; +measure q -> c; +""" +SHOTS = 256 + + +def main() -> None: + """Run the circuit, write its result, and optionally retain the allocation.""" + parser = argparse.ArgumentParser() + parser.add_argument("--hold", action="store_true") + args = parser.parse_args() + + job_id = os.environ["SLURM_JOB_ID"] + device = slurm.open_device_from_license() + job = device.submit_job(BELL_PROGRAM, ProgramFormat.QASM2, SHOTS) + if not job.wait(60): + msg = "DDSIM did not complete the Bell circuit within 60 seconds" + raise RuntimeError(msg) + + counts = job.get_counts() + if sum(counts.values()) != SHOTS: + msg = f"Expected {SHOTS} Bell samples, got {sum(counts.values())}" + raise RuntimeError(msg) + if set(counts) != {"00", "11"}: + msg = f"Expected only Bell outcomes 00 and 11, got {sorted(counts)}" + raise RuntimeError(msg) + + result = { + "counts": counts, + "device": device.name(), + "job_id": job_id, + "licenses": os.environ["SLURM_JOB_LICENSES"], + "node": os.environ["SLURM_JOB_NODELIST"], + "shots": SHOTS, + } + runtime = Path("/runtime") + (runtime / f"ddsim-{job_id}.json").write_text(json.dumps(result, sort_keys=True), encoding="utf-8") + + if args.hold: + release = runtime / f"release-{job_id}" + deadline = time.monotonic() + 180 + while not release.exists(): + if time.monotonic() >= deadline: + msg = f"Release marker for Slurm job {job_id} did not appear" + raise TimeoutError(msg) + time.sleep(0.2) + + +if __name__ == "__main__": + main() diff --git a/test/slurm/cgroup.conf b/test/slurm/cgroup.conf new file mode 100644 index 0000000000..ec18dafa62 --- /dev/null +++ b/test/slurm/cgroup.conf @@ -0,0 +1,12 @@ +# 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 + +CgroupPlugin=autodetect +ConstrainCores=yes +ConstrainRAMSpace=yes +ConstrainSwapSpace=yes diff --git a/test/slurm/compose.yml b/test/slurm/compose.yml new file mode 100644 index 0000000000..5f0bb556ea --- /dev/null +++ b/test/slurm/compose.yml @@ -0,0 +1,107 @@ +# 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 + +services: + controller: + build: + context: . + target: controller + hostname: controller + privileged: true + cgroup: host + tmpfs: + - /run + - /run/lock + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:rw + - ../..:/workspace:ro + - ./runtime:/runtime + - ./slurm.conf:/etc/slurm/slurm.conf:ro + - ./cgroup.conf:/etc/slurm/cgroup.conf:ro + healthcheck: + test: + [ + "CMD", + "systemctl", + "is-active", + "--quiet", + "munge.service", + "slurmctld.service", + ] + interval: 2s + timeout: 2s + retries: 30 + start_period: 5s + + node1: + build: + context: . + target: compute + hostname: node1 + privileged: true + cgroup: host + depends_on: + controller: + condition: service_healthy + tmpfs: + - /run + - /run/lock + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:rw + - ../..:/workspace:ro + - ./runtime:/runtime + - ./slurm.conf:/etc/slurm/slurm.conf:ro + - ./cgroup.conf:/etc/slurm/cgroup.conf:ro + healthcheck: + test: + [ + "CMD", + "systemctl", + "is-active", + "--quiet", + "munge.service", + "slurmd.service", + ] + interval: 2s + timeout: 2s + retries: 30 + start_period: 5s + + node2: + build: + context: . + target: compute + hostname: node2 + privileged: true + cgroup: host + depends_on: + controller: + condition: service_healthy + tmpfs: + - /run + - /run/lock + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:rw + - ../..:/workspace:ro + - ./runtime:/runtime + - ./slurm.conf:/etc/slurm/slurm.conf:ro + - ./cgroup.conf:/etc/slurm/cgroup.conf:ro + healthcheck: + test: + [ + "CMD", + "systemctl", + "is-active", + "--quiet", + "munge.service", + "slurmd.service", + ] + interval: 2s + timeout: 2s + retries: 30 + start_period: 5s diff --git a/test/slurm/ddsim-job.sh b/test/slurm/ddsim-job.sh new file mode 100755 index 0000000000..3ff43536b3 --- /dev/null +++ b/test/slurm/ddsim-job.sh @@ -0,0 +1,12 @@ +#!/bin/sh +# 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 -eu + +exec python3 /workspace/test/slurm/bell_job.py "$@" diff --git a/test/slurm/mqt-slurm-prepare b/test/slurm/mqt-slurm-prepare new file mode 100755 index 0000000000..a106b55932 --- /dev/null +++ b/test/slurm/mqt-slurm-prepare @@ -0,0 +1,17 @@ +#!/bin/sh +# 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 -eu + +test -s /runtime/munge.key + +install -d -o munge -g munge -m 0755 /etc/munge /run/munge +install -o munge -g munge -m 0400 /runtime/munge.key /etc/munge/munge.key +install -d -o slurm -g slurm -m 0755 /var/spool/slurmctld /var/log/slurm +install -d -o root -g root -m 0755 /var/spool/slurmd diff --git a/test/slurm/mqt-slurm-prepare.service b/test/slurm/mqt-slurm-prepare.service new file mode 100644 index 0000000000..ea26c9d04e --- /dev/null +++ b/test/slurm/mqt-slurm-prepare.service @@ -0,0 +1,20 @@ +# 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 + +[Unit] +Description=Prepare the MQT Core Slurm test node +RequiresMountsFor=/runtime +Before=munge.service slurmctld.service slurmd.service + +[Service] +Type=oneshot +ExecStart=/usr/local/sbin/mqt-slurm-prepare +RemainAfterExit=yes + +[Install] +WantedBy=multi-user.target diff --git a/test/slurm/run_integration.py b/test/slurm/run_integration.py new file mode 100644 index 0000000000..a8d173d670 --- /dev/null +++ b/test/slurm/run_integration.py @@ -0,0 +1,385 @@ +# 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 + +"""Run the real Slurm admission and QDMI execution integration test.""" + +from __future__ import annotations + +import json +import logging +import re +import secrets +import shlex +import subprocess +import time +from pathlib import Path +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from collections.abc import Callable, Sequence + +ROOT = Path(__file__).resolve().parents[2] +FIXTURE = ROOT / "test" / "slurm" +DIST = FIXTURE / "dist" +RUNTIME = FIXTURE / "runtime" +COMPOSE = ( + "docker", + "compose", + "--project-name", + "mqt-core-slurm-test", + "--file", + str(FIXTURE / "compose.yml"), +) +TIMEOUT = 120.0 +RESULT_VISIBILITY_GRACE_PERIOD = 5.0 +LOGGER = logging.getLogger(__name__) + + +def run(command: Sequence[str], *, check: bool = True) -> subprocess.CompletedProcess[str]: + """Run a command and retain output for assertions and diagnostics.""" + LOGGER.info("+ %s", shlex.join(command)) + result = subprocess.run(command, check=False, capture_output=True, text=True) # ruff: ignore[subprocess-without-shell-equals-true] + if result.stdout: + LOGGER.info("%s", result.stdout.rstrip()) + if result.stderr: + LOGGER.info("%s", result.stderr.rstrip()) + if check and result.returncode != 0: + raise subprocess.CalledProcessError(result.returncode, command, output=result.stdout, stderr=result.stderr) + return result + + +def compose(*arguments: str, check: bool = True) -> subprocess.CompletedProcess[str]: + """Run Docker Compose for the fixed, isolated test project.""" + return run((*COMPOSE, *arguments), check=check) + + +def controller(*command: str, check: bool = True) -> subprocess.CompletedProcess[str]: + """Run a Slurm client command in the controller container.""" + return compose("exec", "-T", "controller", *command, check=check) + + +def compute(node: str, *command: str, check: bool = True) -> subprocess.CompletedProcess[str]: + """Run a diagnostic command in one compute container.""" + return compose("exec", "-T", node, *command, check=check) + + +def wait_for(description: str, predicate: Callable[[], bool], timeout: float = TIMEOUT) -> None: + """Poll a condition and report a precise timeout.""" + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + if predicate(): + return + time.sleep(0.5) + msg = f"Timed out while waiting for {description}" + raise TimeoutError(msg) + + +def node_record(node: str) -> str: + """Return one machine-readable Slurm node record.""" + return controller("scontrol", "show", "node", node, "--oneliner").stdout.strip() + + +def node_is_idle(node: str) -> bool: + """Return whether a compute node has registered and is idle.""" + record = node_record(node) + return "State=IDLE" in record and "CPUTot=2" in record + + +def job_record(job_id: str) -> tuple[str, str, str] | None: + """Return state, reason, and node for an active job.""" + output = controller("squeue", "--noheader", "--jobs", job_id, "--format=%T|%R|%N").stdout.strip() + if not output: + return None + state, reason, node = output.split("|", maxsplit=2) + # squeue renders a pending reason in parentheses even with a custom format. + if reason.startswith("(") and reason.endswith(")"): + reason = reason[1:-1] + return state, reason, node + + +def job_matches(job_id: str, state: str, *, node: str | None = None, reason: str | None = None) -> bool: + """Return whether an active job has the requested observable state.""" + record = job_record(job_id) + if record is None: + return False + actual_state, actual_reason, actual_node = record + return ( + actual_state == state and (node is None or actual_node == node) and (reason is None or actual_reason == reason) + ) + + +def submit(script: str, license_expression: str, *, node: str | None = None, hold: bool = False) -> str: + """Submit one single-processor batch job and return its numeric ID.""" + command = [ + "sbatch", + "--parsable", + "--nodes=1", + "--ntasks=1", + "--cpus-per-task=1", + f"--licenses={license_expression}", + "--chdir=/workspace", + "--output=/runtime/slurm-%j.out", + ] + if node is not None: + command.append(f"--nodelist={node}") + command.append(f"/workspace/test/slurm/{script}") + if hold: + command.append("--hold") + job_id = controller(*command).stdout.strip().split(";", maxsplit=1)[0] + if not job_id.isdecimal(): + msg = f"sbatch returned an invalid job ID: {job_id!r}" + raise RuntimeError(msg) + return job_id + + +def license_record(name: str) -> dict[str, str]: + """Parse one `scontrol show lic` record.""" + output = controller("scontrol", "show", "lic", name, "--oneliner").stdout + return dict(field.split("=", maxsplit=1) for field in output.split() if "=" in field) + + +def assert_license(name: str, *, total: int, used: int, free: int) -> None: + """Require the exact static Slurm license counters.""" + record = license_record(name) + expected = {"LicenseName": name, "Total": str(total), "Used": str(used), "Free": str(free)} + if not expected.items() <= record.items(): + msg = f"Unexpected license record for {name}: {record}; expected {expected}" + raise AssertionError(msg) + + +def load_result(kind: str, job_id: str) -> dict[str, Any]: + """Load one batch-job result from the shared runtime directory.""" + return json.loads((RUNTIME / f"{kind}-{job_id}.json").read_text(encoding="utf-8")) + + +def wait_for_result(kind: str, job_id: str, description: str) -> None: + """Wait for a result and allow bounded shared-file visibility delay.""" + result_path = RUNTIME / f"{kind}-{job_id}.json" + left_queue_at: float | None = None + + def result_exists_or_raise() -> bool: + nonlocal left_queue_at + if result_path.exists(): + return True + if job_record(job_id) is not None: + left_queue_at = None + return False + + now = time.monotonic() + if left_queue_at is None: + left_queue_at = now + return False + if now - left_queue_at < RESULT_VISIBILITY_GRACE_PERIOD: + return False + + output_path = RUNTIME / f"slurm-{job_id}.out" + output = output_path.read_text(encoding="utf-8") if output_path.exists() else "" + msg = f"Slurm job {job_id} exited before producing {result_path.name}:\n{output.rstrip()}" + raise RuntimeError(msg) + + wait_for(description, result_exists_or_raise) + + +def wait_for_failed_adapter(job_id: str, diagnostic: str) -> None: + """Require an adapter diagnostic and a failed batch job without a result.""" + output_path = RUNTIME / f"slurm-{job_id}.out" + + def failed_with_diagnostic() -> bool: + if not output_path.exists() or job_record(job_id) is not None: + return False + return diagnostic in output_path.read_text(encoding="utf-8") + + wait_for(f"Slurm job {job_id} to fail with {diagnostic!r}", failed_with_diagnostic) + if (RUNTIME / f"ddsim-{job_id}.json").exists(): + msg = f"Rejected Slurm job {job_id} unexpectedly produced a DDSIM result" + raise AssertionError(msg) + + +def assert_bell_result(job_id: str, expected_node: str | None = None) -> None: + """Recheck the Bell result outside the batch job.""" + result = load_result("ddsim", job_id) + if result["shots"] != 256 or sum(result["counts"].values()) != 256: + msg = f"Slurm job {job_id} did not return 256 Bell samples: {result}" + raise AssertionError(msg) + if set(result["counts"]) != {"00", "11"}: + msg = f"Slurm job {job_id} returned non-Bell outcomes: {result}" + raise AssertionError(msg) + if expected_node is not None and result["node"] != expected_node: + msg = f"Slurm job {job_id} ran on {result['node']}, expected {expected_node}" + raise AssertionError(msg) + if result["licenses"] not in {"mqt.ddsim.default", "mqt.ddsim.default:1"}: + msg = f"Slurm exposed an unexpected license string: {result['licenses']}" + raise AssertionError(msg) + + +def clean_runtime() -> None: + """Remove only result artifacts created by an earlier fixture run.""" + RUNTIME.mkdir(parents=True, exist_ok=True) + for pattern in ("ddsim-*.json", "sc-*.json", "release-*", "slurm-*.out"): + for path in RUNTIME.glob(pattern): + path.unlink() + key = RUNTIME / "munge.key" + key.write_bytes(secrets.token_bytes(1024)) + key.chmod(0o600) + + +def print_diagnostics() -> None: + """Print cluster state without hiding the original test failure.""" + controller("squeue", "--all", check=False) + controller( + "sacct", + "--allusers", + "--starttime=now-1hour", + "--format=JobID,State,ExitCode,Reason,NodeList", + check=False, + ) + controller("scontrol", "show", "node", check=False) + controller("scontrol", "show", "lic", check=False) + for output in sorted(RUNTIME.glob("slurm-*.out")): + LOGGER.info("=== %s ===", output.name) + try: + LOGGER.info("%s", output.read_text(encoding="utf-8").rstrip()) + except OSError as error: + LOGGER.info("Could not read %s: %s", output, error) + for service, units in ( + ("controller", ("munge.service", "slurmctld.service")), + ("node1", ("munge.service", "slurmd.service")), + ("node2", ("munge.service", "slurmd.service")), + ): + compose("exec", "-T", service, "systemctl", "status", "--no-pager", *units, check=False) + compose( + "exec", + "-T", + service, + "journalctl", + "--no-pager", + "--lines=100", + *(argument for unit in units for argument in ("--unit", unit)), + check=False, + ) + compose("logs", "--no-color", check=False) + + +def main() -> None: + """Build the cluster and verify Slurm admission and DDSIM execution.""" + clean_runtime() + wheels = tuple(DIST.glob("*.whl")) + if len(wheels) != 1: + msg = f"Build exactly one MQT Core wheel in {DIST}, found {len(wheels)}" + raise RuntimeError(msg) + + success = False + started = False + try: + cgroup_version = run(("docker", "info", "--format", "{{.CgroupVersion}}")).stdout.strip() + if cgroup_version != "2": + msg = f"The Slurm integration requires Docker on cgroup v2, got {cgroup_version!r}" + raise RuntimeError(msg) + + started = True + compose("up", "--build", "--detach", "--wait") + + version_output = controller("scontrol", "--version").stdout.strip() + version_match = re.search(r"^slurm(?:-wlm)?\s+(\d+)\.(\d+)\b", version_output, flags=re.IGNORECASE) + if version_match is None or tuple(map(int, version_match.groups())) < (25, 11): + msg = f"The fixture requires Slurm 25.11 or newer, got {version_output!r}" + raise RuntimeError(msg) + + for node in ("node1", "node2"): + compute(node, "test", "-r", "/sys/fs/cgroup/cgroup.controllers") + delegate = compute( + node, + "systemctl", + "show", + "slurmd.service", + "--property=Delegate", + "--value", + ).stdout.strip() + if delegate != "yes": + msg = f"The packaged slurmd.service on {node} must set Delegate=yes, got {delegate!r}" + raise RuntimeError(msg) + wait_for(f"{node} to become IDLE with two processors", lambda node=node: node_is_idle(node)) + + registry_check = ( + "from pathlib import Path; " + "import mqt.core; " + "from mqt.core.qdmi import driver; " + "module_path = Path(mqt.core.__file__).resolve(); " + "assert not any(module_path.is_relative_to(root) for root in ('/workspace', '/runtime')), module_path; " + "ids = driver.registered_device_ids(); " + "assert 'mqt.ddsim.default' in ids and 'mqt.sc.default' in ids, ids" + ) + controller("python3", "-c", registry_check) + assert_license("mqt.ddsim.default", total=2, used=0, free=2) + assert_license("mqt.sc.default", total=1, used=0, free=1) + + non_unit = submit("ddsim-job.sh", "mqt.ddsim.default:2") + wait_for_failed_adapter(non_unit, "must request exactly one Slurm license") + compound = submit("ddsim-job.sh", "mqt.ddsim.default:1,mqt.sc.default:1") + wait_for_failed_adapter(compound, "uses a compound AND expression") + alternative = submit("ddsim-job.sh", "mqt.ddsim.default:1|mqt.sc.default:1") + wait_for_failed_adapter(alternative, "uses a compound OR expression") + assert_license("mqt.ddsim.default", total=2, used=0, free=2) + assert_license("mqt.sc.default", total=1, used=0, free=1) + + first = submit("ddsim-job.sh", "mqt.ddsim.default:1", node="node1", hold=True) + second = submit("ddsim-job.sh", "mqt.ddsim.default:1", node="node2", hold=True) + wait_for_result("ddsim", first, "the first DDSIM Bell result") + wait_for_result("ddsim", second, "the second DDSIM Bell result") + wait_for("the first DDSIM job to hold on node1", lambda: job_matches(first, "RUNNING", node="node1")) + wait_for("the second DDSIM job to hold on node2", lambda: job_matches(second, "RUNNING", node="node2")) + if "CPUAlloc=1" not in node_record("node1") or "CPUAlloc=1" not in node_record("node2"): + msg = "Each held DDSIM job must leave one processor free on its compute node" + raise AssertionError(msg) + + third = submit("ddsim-job.sh", "mqt.ddsim.default:1") + wait_for( + "the third DDSIM job to wait for its license", + lambda: job_matches(third, "PENDING", reason="Licenses"), + ) + assert_license("mqt.ddsim.default", total=2, used=2, free=0) + + sc_job = submit("sc-job.sh", "mqt.sc.default:1") + wait_for_result("sc", sc_job, "the SC job to execute on a free CPU") + wait_for("the SC job to leave the queue", lambda: job_record(sc_job) is None) + sc_result = load_result("sc", sc_job) + if sc_result["node"] not in {"node1", "node2"} or sc_result["qubits"] <= 0: + msg = f"Unexpected SC job result: {sc_result}" + raise AssertionError(msg) + if not job_matches(first, "RUNNING", node="node1") or not job_matches(second, "RUNNING", node="node2"): + msg = "The SC job did not complete while both DDSIM licenses remained held" + raise AssertionError(msg) + + (RUNTIME / f"release-{first}").touch() + wait_for("the released first DDSIM job to finish", lambda: job_record(first) is None) + wait_for_result("ddsim", third, "the pending third DDSIM job to execute") + wait_for("the third DDSIM job to finish", lambda: job_record(third) is None) + + assert_bell_result(first, "node1") + assert_bell_result(second, "node2") + assert_bell_result(third) + assert_license("mqt.ddsim.default", total=2, used=1, free=1) + + (RUNTIME / f"release-{second}").touch() + wait_for("the released second DDSIM job to finish", lambda: job_record(second) is None) + assert_license("mqt.ddsim.default", total=2, used=0, free=2) + + LOGGER.info( + "Slurm 25.11+ admitted two held DDSIM jobs, blocked the third for Licenses, " + "ran the SC job on a free CPU, and executed the third Bell job after release." + ) + success = True + finally: + if started and not success: + print_diagnostics() + compose("down", "--volumes", "--remove-orphans", check=False) + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO, format="%(message)s") + main() diff --git a/test/slurm/sc-job.sh b/test/slurm/sc-job.sh new file mode 100755 index 0000000000..791dc7bcab --- /dev/null +++ b/test/slurm/sc-job.sh @@ -0,0 +1,12 @@ +#!/bin/sh +# 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 -eu + +exec python3 /workspace/test/slurm/sc_job.py diff --git a/test/slurm/sc_job.py b/test/slurm/sc_job.py new file mode 100644 index 0000000000..e7e7ebe397 --- /dev/null +++ b/test/slurm/sc_job.py @@ -0,0 +1,35 @@ +# 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 + +"""Open the SC device named by the license environment on a free processor.""" + +from __future__ import annotations + +import json +import os +from pathlib import Path + +from mqt.core.qdmi import slurm + + +def main() -> None: + """Query the license-selected SC device and record the compute node.""" + job_id = os.environ["SLURM_JOB_ID"] + device = slurm.open_device_from_license() + result = { + "device": device.name(), + "job_id": job_id, + "licenses": os.environ["SLURM_JOB_LICENSES"], + "node": os.environ["SLURM_JOB_NODELIST"], + "qubits": device.qubits_num(), + } + Path(f"/runtime/sc-{job_id}.json").write_text(json.dumps(result, sort_keys=True), encoding="utf-8") + + +if __name__ == "__main__": + main() diff --git a/test/slurm/slurm.conf b/test/slurm/slurm.conf new file mode 100644 index 0000000000..8bb1182833 --- /dev/null +++ b/test/slurm/slurm.conf @@ -0,0 +1,34 @@ +# 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 + +ClusterName=mqt-core-test +SlurmctldHost=controller +SlurmUser=slurm +AuthType=auth/munge +StateSaveLocation=/var/spool/slurmctld +SlurmdSpoolDir=/var/spool/slurmd +SlurmctldPidFile=/run/slurmctld.pid +SlurmdPidFile=/run/slurmd.pid + +ProctrackType=proctrack/cgroup +TaskPlugin=task/cgroup,task/affinity +JobAcctGatherType=jobacct_gather/cgroup +SelectType=select/cons_tres +SelectTypeParameters=CR_CPU +PrologFlags=Contain + +SchedulerType=sched/backfill +ReturnToService=2 +SlurmdParameters=config_overrides +SlurmctldParameters=reconfig_on_restart + +Licenses=mqt.ddsim.default:2,mqt.sc.default:1 + +NodeName=node1 NodeAddr=node1 CPUs=2 Boards=1 SocketsPerBoard=1 CoresPerSocket=2 ThreadsPerCore=1 RealMemory=512 State=UNKNOWN +NodeName=node2 NodeAddr=node2 CPUs=2 Boards=1 SocketsPerBoard=1 CoresPerSocket=2 ThreadsPerCore=1 RealMemory=512 State=UNKNOWN +PartitionName=compute Nodes=node1,node2 Default=YES MaxTime=INFINITE State=UP From 6150b62fe1ce7f7c3021c4ca6dac37214e9b4ba5 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Fri, 14 Aug 2026 18:04:26 +0200 Subject: [PATCH 07/35] =?UTF-8?q?=E2=9C=A8=20Backport=20runtime-configurab?= =?UTF-8?q?le=20neutral-atom=20devices?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace generated neutral-atom provider state with strict session-owned JSON configuration. Preserve the v3 QDMI namespace, typed registry transport, job retrieval contract, and embedded-build device options. Backports #1974. Assisted-by: GPT-5.6 via Codex --- UPGRADING.md | 23 + bindings/na/register_qdmi.cpp | 2 +- docs/qdmi/na_device.md | 74 +- include/mqt-core/na/fomac/Device.hpp | 2 +- .../qdmi/common/DeviceConfiguration.hpp | 53 ++ .../na/{Generator.hpp => Configuration.hpp} | 88 +-- include/mqt-core/qdmi/devices/na/Device.hpp | 243 ++----- ...vice.json => mqt-core-qdmi-na-device.json} | 1 + src/na/fomac/CMakeLists.txt | 2 +- src/na/fomac/Device.cpp | 2 +- src/qdmi/common/CMakeLists.txt | 15 +- src/qdmi/common/DeviceConfiguration.cpp | 201 ++++++ src/qdmi/devices/na/App.cpp | 452 ------------ src/qdmi/devices/na/CMakeLists.txt | 79 +-- src/qdmi/devices/na/Configuration.cpp | 668 ++++++++++++++++++ src/qdmi/devices/na/Device.cpp | 560 ++++++++------- src/qdmi/devices/na/Generator.cpp | 512 -------------- test/na/fomac/CMakeLists.txt | 5 +- test/python/na/test_na_qdmi.py | 2 +- test/qdmi/devices/na/CMakeLists.txt | 26 +- test/qdmi/devices/na/test_app.cpp | 372 ---------- test/qdmi/devices/na/test_configuration.cpp | 181 +++++ test/qdmi/devices/na/test_device.cpp | 292 +++++++- test/qdmi/devices/na/test_generator.cpp | 101 --- test/qdmi/driver/CMakeLists.txt | 12 + test/qdmi/driver/test_driver.cpp | 73 +- 26 files changed, 2005 insertions(+), 2036 deletions(-) create mode 100644 include/mqt-core/qdmi/common/DeviceConfiguration.hpp rename include/mqt-core/qdmi/devices/na/{Generator.hpp => Configuration.hpp} (86%) rename json/na/{device.json => mqt-core-qdmi-na-device.json} (99%) create mode 100644 src/qdmi/common/DeviceConfiguration.cpp delete mode 100644 src/qdmi/devices/na/App.cpp create mode 100644 src/qdmi/devices/na/Configuration.cpp delete mode 100644 src/qdmi/devices/na/Generator.cpp delete mode 100644 test/qdmi/devices/na/test_app.cpp create mode 100644 test/qdmi/devices/na/test_configuration.cpp delete mode 100644 test/qdmi/devices/na/test_generator.cpp diff --git a/UPGRADING.md b/UPGRADING.md index e3c2f7e351..fcf9182111 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -6,6 +6,29 @@ of changes including minor and patch releases, please refer to the ## [Unreleased] +### Runtime-configurable neutral-atom QDMI device + +The built-in neutral-atom QDMI provider now parses its device description when +each session is initialized. The `mqt-core-qdmi-na-device-gen` target, +`mqt-core-qdmi-na-device-generator` executable, `na::writeHeader`, and generated +`DeviceMemberInitializers.hpp` file have been removed. Replace generator API use +with the `na::Device` configuration type and the `na::readJSON` functions in +`qdmi/devices/na/Configuration.hpp`. + +At runtime, use the registry `session.device-config` field or Python +`device_config` and `device_config_file` arguments. Direct QDMI v1 clients pass +inline JSON through CUSTOM1 or a file path through CUSTOM2. + +### Bundled QDMI devices in embedded builds + +The bundled QDMI devices now have individual CMake options: +`BUILD_MQT_CORE_QDMI_DDSIM_DEVICE`, `BUILD_MQT_CORE_QDMI_NA_DEVICE`, and +`BUILD_MQT_CORE_QDMI_SC_DEVICE`. All three remain enabled by default in a +standalone MQT Core build. They default to disabled when MQT Core is consumed +through CMake's `FetchContent` or `add_subdirectory`; embedded consumers can +enable only the devices they need before making MQT Core available. The QDMI +driver and FoMaC libraries remain available independently. + ### QDMI Python namespace The native Python module has moved from `mqt.core.fomac` to `mqt.core.qdmi`. diff --git a/bindings/na/register_qdmi.cpp b/bindings/na/register_qdmi.cpp index 9c38b5e988..5ebe25c891 100644 --- a/bindings/na/register_qdmi.cpp +++ b/bindings/na/register_qdmi.cpp @@ -10,7 +10,7 @@ #include "fomac/FoMaC.hpp" #include "na/fomac/Device.hpp" -#include "qdmi/devices/na/Generator.hpp" +#include "qdmi/devices/na/Configuration.hpp" #include #include diff --git a/docs/qdmi/na_device.md b/docs/qdmi/na_device.md index dd6e3fcb3f..ad48234dcc 100644 --- a/docs/qdmi/na_device.md +++ b/docs/qdmi/na_device.md @@ -1,28 +1,46 @@ ---- -file_format: mystnb -kernelspec: - name: python3 -mystnb: - number_source_lines: true ---- - -# MQT Core Neutral Atom QDMI Device - -## Objective - -Compilation passes throughout MQT need information about the target device. The -Neutral Atom [QDMI](https://munich-quantum-software-stack.github.io/QDMI/) -device provides a uniform way to provide the necessary information for neutral -atom-based quantum devices. It defines a representation to easily provide static -information in the form of a JSON file. - -## Describing a Device - -The basis of a such device implementation in MQT is a specification in a JSON -file. The structure of this JSON file is defined by the {cpp:class}`na::Device` -struct. The struct defines functions to serialize 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`. +# Neutral-atom QDMI device + +The MQT Core neutral-atom (NA) provider materializes its complete device model +when a QDMI session is initialized. Sites, zones, operations, units, and +calibration are owned by that session. Multiple stable IDs may therefore open +the same provider library and prefix with different device descriptions at the +same time. + +## Configuration sources + +The provider selects the first available source in this order: + +1. QDMI CUSTOM1 containing NUL-terminated inline JSON; +2. QDMI CUSTOM2 containing a NUL-terminated JSON file path; +3. `MQT_CORE_QDMI_NA_CONFIG_JSON`; +4. `MQT_CORE_QDMI_NA_CONFIG_FILE`; +5. `mqt-core-qdmi-na-device.json` beside the provider shared library. + +When direct QDMI callers populate both explicit slots, inline JSON wins. Setting +both NA environment sources is an error. Configuration cannot be changed after +successful initialization. A failed initialization does not commit partial +state, so the same allocated session can be corrected and initialized again. + +Driver users should use the typed `device-config` registry field or the +`device_config` and `device_config_file` Python arguments described in +{doc}`configuration`. Direct QDMI v1 clients use CUSTOM1 and CUSTOM2. + +## Schema and validation + +Every description is a strict JSON object with `"schema-version": 1`. The +bundled example is `json/na/mqt-core-qdmi-na-device.json`, and the C++ value +model is {cpp-api:class}`na::Device`. Required top-level and nested fields +cannot be omitted, unknown fields are rejected, and validation covers: + +- non-empty names and positive device capacity; +- finite positive unit scales and supported units; +- lattice geometry, unique generated coordinates, sufficient sites, and bounded + site/pair expansion; +- operation arity, regions, fidelities, and interaction data; +- shuttling units and decoherence values. + +Applications that need to validate a description without opening a provider +session can call `na::readJSON`; it is the same strict parser used at runtime. + +Handles returned by a session are valid only for that session. Queries reject +sites or operations owned by another session. diff --git a/include/mqt-core/na/fomac/Device.hpp b/include/mqt-core/na/fomac/Device.hpp index e5698661d4..d69c26195e 100644 --- a/include/mqt-core/na/fomac/Device.hpp +++ b/include/mqt-core/na/fomac/Device.hpp @@ -11,7 +11,7 @@ #pragma once #include "fomac/FoMaC.hpp" -#include "qdmi/devices/na/Generator.hpp" +#include "qdmi/devices/na/Configuration.hpp" // NOLINTNEXTLINE(misc-include-cleaner) #include diff --git a/include/mqt-core/qdmi/common/DeviceConfiguration.hpp b/include/mqt-core/qdmi/common/DeviceConfiguration.hpp new file mode 100644 index 0000000000..2048fd36bb --- /dev/null +++ b/include/mqt-core/qdmi/common/DeviceConfiguration.hpp @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include + +#include +#include +#include +#include + +namespace qdmi::detail { + +/// JSON text selected for a provider session together with a safe source label. +struct LoadedDeviceConfiguration { + std::string json; + std::string source; +}; + +/** + * @brief Validate and store a CUSTOM1/CUSTOM2 string parameter. + * + * A null value with size zero is a capability probe. Assignments contain one + * trailing NUL and no embedded NUL. A single NUL clears the selected value. + */ +int setDeviceConfigurationParameter(QDMI_Device_Session_Parameter parameter, + size_t size, const void* value, + std::optional& inlineJson, + std::optional& file); + +/** + * @brief Select and load one runtime device description. + * + * A non-empty explicit inline value wins over an explicit file value. Without + * an explicit source, exactly one technology-specific environment variable may + * select inline JSON or a file. The final fallback is a file beside the shared + * module containing @p anchor. + */ +std::optional loadDeviceConfiguration( + const std::optional& inlineJson, + const std::optional& file, + std::string_view inlineEnvironment, std::string_view fileEnvironment, + std::string_view bundledFilename, const void* anchor, int& status); + +} // namespace qdmi::detail diff --git a/include/mqt-core/qdmi/devices/na/Generator.hpp b/include/mqt-core/qdmi/devices/na/Configuration.hpp similarity index 86% rename from include/mqt-core/qdmi/devices/na/Generator.hpp rename to include/mqt-core/qdmi/devices/na/Configuration.hpp index 3c08d0378b..dcddd02804 100644 --- a/include/mqt-core/qdmi/devices/na/Generator.hpp +++ b/include/mqt-core/qdmi/devices/na/Configuration.hpp @@ -8,6 +8,10 @@ * Licensed under the MIT License */ +/** @file Configuration.hpp + * @brief Neutral-atom QDMI device configuration. + */ + #pragma once #include // NOLINT(misc-include-cleaner) @@ -18,10 +22,10 @@ #include #include #include -#include #include #include #include +#include #include namespace na { @@ -35,6 +39,8 @@ namespace na { * the length unit, respectively. */ struct Device { + /// @brief Version of this device-description schema. + uint64_t schemaVersion = 1; /// @brief The name of the device. std::string name; /// @brief The number of qubits in the device. @@ -267,22 +273,30 @@ struct Device { Unit durationUnit = {.unit = "us", ///< Default is microseconds (us). .scaleFactor = 1.0}; - // Before we used the macro NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT here, - // too. Now, we added an id to shuttling units that must be initialized - // in a custom routine, so we can only use the serialize macro. Additionally, - // we check here whether the units are valid SI units. - // NOLINTNEXTLINE(misc-include-cleaner) - NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE( - Device, name, numQubits, traps, minAtomDistance, - globalSingleQubitOperations, globalMultiQubitOperations, - localSingleQubitOperations, localMultiQubitOperations, shuttlingUnits, - decoherenceTimes, lengthUnit, durationUnit) + /// Serialize the versioned runtime configuration. + // NOLINTNEXTLINE(readability-identifier-naming,misc-include-cleaner) + friend void to_json(nlohmann::json& json, const Device& device) { + json = nlohmann::json{ + {"schema-version", device.schemaVersion}, + {"name", device.name}, + {"numQubits", device.numQubits}, + {"traps", device.traps}, + {"minAtomDistance", device.minAtomDistance}, + {"globalSingleQubitOperations", device.globalSingleQubitOperations}, + {"globalMultiQubitOperations", device.globalMultiQubitOperations}, + {"localSingleQubitOperations", device.localSingleQubitOperations}, + {"localMultiQubitOperations", device.localMultiQubitOperations}, + {"shuttlingUnits", device.shuttlingUnits}, + {"decoherenceTimes", device.decoherenceTimes}, + {"lengthUnit", device.lengthUnit}, + {"durationUnit", device.durationUnit}}; + } // the name of the following function is given by the nlohmann::json library // and must not be changed template // NOLINTNEXTLINE(readability-identifier-naming) - friend auto from_json(const BasicJsonType& json, Device& device) -> void { + friend void from_json(const BasicJsonType& json, Device& device) { const Device defaultDevice{}; device.name = !json.is_null() ? json.value("name", defaultDevice.name) : defaultDevice.name; @@ -354,23 +368,6 @@ struct Device { } }; -/** - * @brief Writes a JSON schema with default values for the device configuration - * to the specified output stream. - * @param os is the output stream to write the JSON schema to. - * @throws std::runtime_error if the JSON conversion fails. - */ -auto writeJSONSchema(std::ostream& os) -> void; - -/** - * @brief Writes a JSON schema with default values for the device configuration - * to the specified path. - * @param path The path to write the JSON schema to. - * @throws std::runtime_error if the JSON conversion fails or the file cannot be - * opened. - */ -auto writeJSONSchema(const std::string& path) -> void; - /** * @brief Parses the device configuration from an input stream. * @param is is the input stream containing the JSON representation of the @@ -378,7 +375,10 @@ auto writeJSONSchema(const std::string& path) -> void; * @returns The parsed device configuration as a Protobuf message. * @throws std::runtime_error if the JSON cannot be parsed. */ -[[nodiscard]] auto readJSON(std::istream& is) -> Device; +[[nodiscard]] Device readJSON(std::istream& is); + +/// Parse and validate JSON text while using @p source in diagnostics. +[[nodiscard]] Device readJSON(std::string_view json, std::string_view source); /** * @brief Parses the device configuration from a JSON file. @@ -387,29 +387,7 @@ auto writeJSONSchema(const std::string& path) -> void; * @throws std::runtime_error if the JSON file does not exist, or the JSON file * cannot be parsed. */ -[[nodiscard]] auto readJSON(const std::string& path) -> Device; - -/** - * @brief Writes a header file with the device configuration to the specified - * output stream. - * @param device is the protobuf representation of the device. - * @param os is the output stream to write the header file to. - * @throws std::runtime_error if the file cannot be opened or written to. - * @note This implementation only supports multi-qubit gates up to two - * qubits. - */ -auto writeHeader(const Device& device, std::ostream& os) -> void; - -/** - * @brief Writes a header file with the device configuration to the specified - * path. - * @param device is the protobuf representation of the device. - * @param path is the path to write the header file to. - * @throws std::runtime_error if the file cannot be opened or written to. - * @note This implementation only supports multi-qubit gates up to two - * qubits. - */ -auto writeHeader(const Device& device, const std::string& path) -> void; +[[nodiscard]] Device readJSON(const std::string& path); /** * @brief Information about a regular site in a lattice. @@ -440,8 +418,8 @@ struct SiteInfo { * determinant of the lattice vector matrix is near zero, causing the system * of equations to have no unique solution). */ -auto forEachRegularSites(const std::vector& lattices, +void forEachRegularSites(const std::vector& lattices, const std::function& f, - size_t startId = 0) -> void; + size_t startId = 0); } // namespace na diff --git a/include/mqt-core/qdmi/devices/na/Device.hpp b/include/mqt-core/qdmi/devices/na/Device.hpp index 5a8b90c442..bb128c2a4c 100644 --- a/include/mqt-core/qdmi/devices/na/Device.hpp +++ b/include/mqt-core/qdmi/devices/na/Device.hpp @@ -16,9 +16,11 @@ #include "mqt_na_qdmi/device.h" #include "qdmi/common/Common.hpp" +#include "qdmi/devices/na/Configuration.hpp" #include #include +#include #include #include #include @@ -27,72 +29,6 @@ #include #include -namespace qdmi::na { -class Device final : public Singleton { - friend class Singleton; - - /// @brief Provides access to the device name. - std::string name_; - - /// @brief The number of qubits in the device. - size_t qubitsNum_ = 0; - - /// @brief A struct representing a unit. - struct Unit { - /// @brief The unit used to interpret values. - std::string unit; - /** - * @brief The scale factor of the unit. - * @details This factor must be multiplied with all values before - * interpreting them in the unit specified by @ref Unit::unit. - */ - double scaleFactor = 1.0; - }; - /// @brief The unit used to interpret length values. - Unit lengthUnit_; - - /// @brief The unit used to interpret duration values. - Unit durationUnit_; - - /// @brief The minimum atom distance that must be maintained. - uint64_t minAtomDistance_; - - /// @brief The list of sites. - std::vector> sites_; - - /// @brief The list of operations. - std::vector> operations_; - - /// @brief The list of device sessions. - std::unordered_map> - sessions_; - - /// @brief Private constructor to enforce the singleton pattern. - Device(); - -public: - /** - * @brief Allocates a new device session. - * @see MQT_NA_QDMI_device_session_alloc - */ - auto sessionAlloc(MQT_NA_QDMI_Device_Session* session) -> int; - - /** - * @brief Frees a device session. - * @see MQT_NA_QDMI_device_session_free - */ - auto sessionFree(MQT_NA_QDMI_Device_Session session) -> void; - - /** - * @brief Query a device property. - * @see MQT_NA_QDMI_device_session_query_device_property - */ - auto queryProperty(QDMI_Device_Property prop, size_t size, void* value, - size_t* sizeRet) -> int; -}; -} // namespace qdmi::na - /** * @brief Implementation of the MQT_NA_QDMI_Device_Session structure. */ @@ -105,6 +41,17 @@ struct MQT_NA_QDMI_Device_Session_impl_d { }; /// @brief The current status of the session. Status status_ = Status::ALLOCATED; + std::optional inlineConfiguration_; + std::optional fileConfiguration_; + std::string name_; + size_t qubitsNum_ = 0; + na::Device::Unit lengthUnit_; + na::Device::Unit durationUnit_; + uint64_t minAtomDistance_ = 0; + std::vector> siteStorage_; + std::vector sites_; + std::vector> operationStorage_; + std::vector operations_; /// @brief The device jobs associated with this session. std::unordered_map> @@ -115,51 +62,49 @@ struct MQT_NA_QDMI_Device_Session_impl_d { * @brief Initializes the device session. * @see MQT_NA_QDMI_device_session_init */ - auto init() -> int; + int init(); /** * @brief Sets a parameter for the device session. * @see MQT_NA_QDMI_device_session_set_parameter */ - auto setParameter(QDMI_Device_Session_Parameter param, size_t size, - const void* value) const -> int; + int setParameter(QDMI_Device_Session_Parameter param, size_t size, + const void* value); /** * @brief Create a new device job. * @see MQT_NA_QDMI_device_session_create_device_job */ - auto createDeviceJob(MQT_NA_QDMI_Device_Job* job) -> int; + int createDeviceJob(MQT_NA_QDMI_Device_Job* job); /** * @brief Frees the device job. * @see MQT_NA_QDMI_device_job_free */ - auto freeDeviceJob(MQT_NA_QDMI_Device_Job job) -> void; + void freeDeviceJob(MQT_NA_QDMI_Device_Job job); /** * @brief Forwards a query of a device property to the device. * @see MQT_NA_QDMI_device_session_query_device_property */ - auto queryDeviceProperty(QDMI_Device_Property prop, size_t size, void* value, - size_t* sizeRet) const -> int; + int queryDeviceProperty(QDMI_Device_Property prop, size_t size, void* value, + size_t* sizeRet) const; /** * @brief Forwards a query of a site property to the site. * @see MQT_NA_QDMI_device_session_query_site_property */ - auto querySiteProperty(MQT_NA_QDMI_Site site, QDMI_Site_Property prop, - size_t size, void* value, size_t* sizeRet) const - -> int; + int querySiteProperty(MQT_NA_QDMI_Site site, QDMI_Site_Property prop, + size_t size, void* value, size_t* sizeRet) const; /** * @brief Forwards a query of an operation property to the operation. * @see MQT_NA_QDMI_device_session_query_operation_property */ - auto queryOperationProperty(MQT_NA_QDMI_Operation operation, size_t numSites, - const MQT_NA_QDMI_Site* sites, size_t numParams, - const double* params, - QDMI_Operation_Property prop, size_t size, - void* value, size_t* sizeRet) const -> int; + int queryOperationProperty(MQT_NA_QDMI_Operation operation, size_t numSites, + const MQT_NA_QDMI_Site* sites, size_t numParams, + const double* params, QDMI_Operation_Property prop, + size_t size, void* value, size_t* sizeRet) const; }; /** @@ -183,52 +128,52 @@ struct MQT_NA_QDMI_Device_Job_impl_d { * is private. * @see QDMI_job_free */ - auto free() -> void; + void free(); /** * @brief Sets a parameter for the job. * @see MQT_NA_QDMI_device_job_set_parameter */ - auto setParameter(QDMI_Device_Job_Parameter param, size_t size, - const void* value) -> int; + int setParameter(QDMI_Device_Job_Parameter param, size_t size, + const void* value); /** * @brief Queries a property of the job. * @see MQT_NA_QDMI_device_job_query_property */ - auto queryProperty(QDMI_Device_Job_Property prop, size_t size, void* value, - size_t* sizeRet) -> int; + int queryProperty(QDMI_Device_Job_Property prop, size_t size, void* value, + size_t* sizeRet); /** * @brief Submits the job to the device. * @see MQT_NA_QDMI_device_job_submit */ - auto submit() -> int; + int submit(); /** * @brief Cancels the job. * @see MQT_NA_QDMI_device_job_cancel */ - auto cancel() -> int; + int cancel(); /** * @brief Checks the status of the job. * @see MQT_NA_QDMI_device_job_check */ - auto check(QDMI_Job_Status* status) -> int; + int check(QDMI_Job_Status* status); /** * @brief Waits for the job to complete but at most for the specified timeout. * @see MQT_NA_QDMI_device_job_wait */ - auto wait(size_t timeout) -> int; + int wait(size_t timeout); /** * @brief Gets the results of the job. * @see MQT_NA_QDMI_device_job_get_results */ - auto getResults(QDMI_Job_Result result, size_t size, void* data, - [[maybe_unused]] size_t* sizeRet) -> int; + int getResults(QDMI_Job_Result result, size_t size, void* data, + [[maybe_unused]] size_t* sizeRet); }; /** @@ -238,6 +183,7 @@ struct MQT_NA_QDMI_Site_impl_d { friend MQT_NA_QDMI_Operation_impl_d; private: + MQT_NA_QDMI_Device_Session_impl_d* owner_ = nullptr; uint64_t id_ = 0; ///< Unique identifier of the site uint64_t moduleId_ = 0; ///< Identifier of the module the site belongs to /// Identifier of the submodule the site belongs to @@ -255,29 +201,26 @@ struct MQT_NA_QDMI_Site_impl_d { DecoherenceTimes decoherenceTimes_{}; bool isZone = false; ///< Indicates if the site is a zone site +public: /// @brief Constructor for regular sites. - MQT_NA_QDMI_Site_impl_d(uint64_t id, uint64_t moduleId, uint64_t subModuleId, - int64_t x, int64_t y); + MQT_NA_QDMI_Site_impl_d(MQT_NA_QDMI_Device_Session_impl_d* owner, uint64_t id, + uint64_t moduleId, uint64_t subModuleId, int64_t x, + int64_t y, uint64_t t1, uint64_t t2); /// @brief Constructor for zone sites. - MQT_NA_QDMI_Site_impl_d(uint64_t id, int64_t x, int64_t y, uint64_t width, - uint64_t height); + MQT_NA_QDMI_Site_impl_d(MQT_NA_QDMI_Device_Session_impl_d* owner, uint64_t id, + int64_t x, int64_t y, uint64_t width, uint64_t height, + uint64_t t1, uint64_t t2); -public: - /// @brief Factory function for regular sites. - [[nodiscard]] static auto makeUniqueSite(uint64_t id, uint64_t moduleId, - uint64_t subModuleId, int64_t x, - int64_t y) - -> std::unique_ptr; - /// @brief Factory function for zone sites. - [[nodiscard]] static auto makeUniqueZone(uint64_t id, int64_t x, int64_t y, - uint64_t width, uint64_t height) - -> std::unique_ptr; /** * @brief Queries a property of the site. * @see MQT_NA_QDMI_device_session_query_site_property */ - auto queryProperty(QDMI_Site_Property prop, size_t size, void* value, - size_t* sizeRet) const -> int; + int queryProperty(QDMI_Site_Property prop, size_t size, void* value, + size_t* sizeRet) const; + [[nodiscard]] bool + ownedBy(const MQT_NA_QDMI_Device_Session_impl_d* session) const { + return owner_ == session; + } }; /** @@ -285,6 +228,7 @@ struct MQT_NA_QDMI_Site_impl_d { */ struct MQT_NA_QDMI_Operation_impl_d { private: + MQT_NA_QDMI_Device_Session_impl_d* owner_ = nullptr; std::string name_; ///< Name of the operation size_t numParameters_; ///< Number of parameters for the operation /** @@ -324,89 +268,58 @@ struct MQT_NA_QDMI_Operation_impl_d { /// Indicates if this operation is zoned (global) bool isZoned_ = false; +public: /// @brief Constructor for the global single-qubit. - MQT_NA_QDMI_Operation_impl_d(std::string name, size_t numParameters, + MQT_NA_QDMI_Operation_impl_d(MQT_NA_QDMI_Device_Session_impl_d* owner, + std::string name, size_t numParameters, size_t numQubits, uint64_t duration, double fidelity, MQT_NA_QDMI_Site zone); /// @brief Constructor for the global multi-qubit operations. - MQT_NA_QDMI_Operation_impl_d(std::string name, size_t numParameters, + MQT_NA_QDMI_Operation_impl_d(MQT_NA_QDMI_Device_Session_impl_d* owner, + std::string name, size_t numParameters, size_t numQubits, uint64_t duration, double fidelity, uint64_t interactionRadius, uint64_t blockingRadius, double idlingFidelity, MQT_NA_QDMI_Site zone); /// @brief Constructor for the single-qubit operations. - MQT_NA_QDMI_Operation_impl_d(std::string name, size_t numParameters, + MQT_NA_QDMI_Operation_impl_d(MQT_NA_QDMI_Device_Session_impl_d* owner, + std::string name, size_t numParameters, uint64_t duration, double fidelity, - const std::vector& sites); + std::vector sites); /// @brief Constructor for the local two-qubit operations. MQT_NA_QDMI_Operation_impl_d( - std::string name, size_t numParameters, size_t numQubits, - uint64_t duration, double fidelity, uint64_t interactionRadius, - uint64_t blockingRadius, - const std::vector>& sites); + MQT_NA_QDMI_Device_Session_impl_d* owner, std::string name, + size_t numParameters, size_t numQubits, uint64_t duration, + double fidelity, uint64_t interactionRadius, uint64_t blockingRadius, + std::vector> sites); /// @brief Constructor for load and store operations. - MQT_NA_QDMI_Operation_impl_d(std::string name, size_t numParameters, + MQT_NA_QDMI_Operation_impl_d(MQT_NA_QDMI_Device_Session_impl_d* owner, + std::string name, size_t numParameters, uint64_t duration, double fidelity, MQT_NA_QDMI_Site zone); /// @brief Constructor for the shuttling operations. - MQT_NA_QDMI_Operation_impl_d(std::string name, size_t numParameters, + MQT_NA_QDMI_Operation_impl_d(MQT_NA_QDMI_Device_Session_impl_d* owner, + std::string name, size_t numParameters, MQT_NA_QDMI_Site zone, uint64_t meanShuttlingSpeed); +private: /// @brief Sort the sites such that the occurrence of a given site can be /// determined in O(log n) time. - auto sortSites() -> void; + void sortSites(); public: - /// @brief Factory function for the global single-qubit operations. - [[nodiscard]] static auto - makeUniqueGlobalSingleQubit(const std::string& name, size_t numParameters, - uint64_t duration, double fidelity, - MQT_NA_QDMI_Site zone) - -> std::unique_ptr; - /// @brief Factory function for the global multi-qubit operations. - [[nodiscard]] static auto makeUniqueGlobalMultiQubit( - const std::string& name, size_t numParameters, size_t numQubits, - uint64_t duration, double fidelity, uint64_t interactionRadius, - uint64_t blockingRadius, double idlingFidelity, MQT_NA_QDMI_Site zone) - -> std::unique_ptr; - /// @brief Factory function for the local single-qubit operations. - [[nodiscard]] static auto - makeUniqueLocalSingleQubit(const std::string& name, size_t numParameters, - uint64_t duration, double fidelity, - const std::vector& sites) - -> std::unique_ptr; - /// @brief Factory function for the local multi-qubit operations. - [[nodiscard]] static auto makeUniqueLocalTwoQubit( - const std::string& name, size_t numParameters, size_t numQubits, - uint64_t duration, double fidelity, uint64_t interactionRadius, - uint64_t blockingRadius, - const std::vector>& sites) - -> std::unique_ptr; - /// @brief Factory function for the shuttling load operations. - [[nodiscard]] static auto - makeUniqueShuttlingLoad(const std::string& name, size_t numParameters, - uint64_t duration, double fidelity, - MQT_NA_QDMI_Site zone) - -> std::unique_ptr; - /// @brief Factory function for the shuttling move operations. - [[nodiscard]] static auto - makeUniqueShuttlingMove(const std::string& name, size_t numParameters, - MQT_NA_QDMI_Site zone, uint64_t meanShuttlingSpeed) - -> std::unique_ptr; - /// @brief Factory function for the shuttling store operations. - [[nodiscard]] static auto - makeUniqueShuttlingStore(const std::string& name, size_t numParameters, - uint64_t duration, double fidelity, - MQT_NA_QDMI_Site zone) - -> std::unique_ptr; + [[nodiscard]] bool + ownedBy(const MQT_NA_QDMI_Device_Session_impl_d* session) const { + return owner_ == session; + } /** * @brief Queries a property of the operation. * @see MQT_NA_QDMI_device_session_query_operation_property */ - auto queryProperty(size_t numSites, const MQT_NA_QDMI_Site* sites, - size_t numParams, const double* params, - QDMI_Operation_Property prop, size_t size, void* value, - size_t* sizeRet) const -> int; + int queryProperty(size_t numSites, const MQT_NA_QDMI_Site* sites, + size_t numParams, const double* params, + QDMI_Operation_Property prop, size_t size, void* value, + size_t* sizeRet) const; }; diff --git a/json/na/device.json b/json/na/mqt-core-qdmi-na-device.json similarity index 99% rename from json/na/device.json rename to json/na/mqt-core-qdmi-na-device.json index 1c1531dfc5..114557c9f5 100644 --- a/json/na/device.json +++ b/json/na/mqt-core-qdmi-na-device.json @@ -1,4 +1,5 @@ { + "schema-version": 1, "name": "MQT NA Default QDMI Device", "numQubits": 100, "traps": [ diff --git a/src/na/fomac/CMakeLists.txt b/src/na/fomac/CMakeLists.txt index 58f39850a5..89763ecd11 100644 --- a/src/na/fomac/CMakeLists.txt +++ b/src/na/fomac/CMakeLists.txt @@ -26,7 +26,7 @@ if(NOT TARGET ${TARGET_NAME}) target_link_libraries( ${TARGET_NAME} PUBLIC MQT::CoreFoMaC nlohmann_json::nlohmann_json - PRIVATE spdlog::spdlog MQT::CoreQDMINaDeviceGen) + PRIVATE spdlog::spdlog MQT::CoreQDMINaDeviceConfig) # add to list of MQT core targets set(MQT_CORE_TARGETS diff --git a/src/na/fomac/Device.cpp b/src/na/fomac/Device.cpp index 474035413f..5e59bb5573 100644 --- a/src/na/fomac/Device.cpp +++ b/src/na/fomac/Device.cpp @@ -12,7 +12,7 @@ #include "fomac/FoMaC.hpp" #include "ir/Definitions.hpp" -#include "qdmi/devices/na/Generator.hpp" +#include "qdmi/devices/na/Configuration.hpp" #include diff --git a/src/qdmi/common/CMakeLists.txt b/src/qdmi/common/CMakeLists.txt index cc8518339f..f942940657 100644 --- a/src/qdmi/common/CMakeLists.txt +++ b/src/qdmi/common/CMakeLists.txt @@ -13,17 +13,24 @@ if(NOT TARGET ${TARGET_NAME}) add_mqt_core_library(${TARGET_NAME} ALIAS_NAME QDMICommon) # Add sources to target - target_sources(${TARGET_NAME} PRIVATE Common.cpp) + target_sources(${TARGET_NAME} PRIVATE Common.cpp DeviceConfiguration.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/common/Common.hpp) + target_sources( + ${TARGET_NAME} + PUBLIC FILE_SET + HEADERS + BASE_DIRS + ${MQT_CORE_INCLUDE_BUILD_DIR} + FILES + ${MQT_CORE_INCLUDE_BUILD_DIR}/qdmi/common/Common.hpp + ${MQT_CORE_INCLUDE_BUILD_DIR}/qdmi/common/DeviceConfiguration.hpp) # Add link libraries target_link_libraries( ${TARGET_NAME} PUBLIC qdmi::qdmi - PRIVATE qdmi::qdmi_project_warnings) + PRIVATE qdmi::qdmi_project_warnings spdlog::spdlog ${CMAKE_DL_LIBS}) # add to list of MQT core targets list(APPEND MQT_CORE_TARGETS ${TARGET_NAME}) diff --git a/src/qdmi/common/DeviceConfiguration.cpp b/src/qdmi/common/DeviceConfiguration.cpp new file mode 100644 index 0000000000..d417dbb712 --- /dev/null +++ b/src/qdmi/common/DeviceConfiguration.cpp @@ -0,0 +1,201 @@ +/* + * 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/common/DeviceConfiguration.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include + +#include +#else +#include +#endif + +namespace qdmi::detail { +namespace { +[[nodiscard]] std::optional +environment(const std::string_view name) { +#ifdef _WIN32 + char* raw = nullptr; + size_t size = 0; + const std::string ownedName(name); + if (_dupenv_s(&raw, &size, ownedName.c_str()) != 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 + const std::string ownedName(name); + const auto* value = std::getenv(ownedName.c_str()); + if (value == nullptr || *value == '\0') { + return std::nullopt; + } + return std::string(value); +#endif +} + +[[nodiscard]] std::filesystem::path moduleDirectory(const void* anchor) { +#ifdef _WIN32 + HMODULE module = nullptr; + if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(anchor), &module) == 0) { + return {}; + } + std::wstring buffer(MAX_PATH, L'\0'); + while (true) { + const auto length = GetModuleFileNameW(module, buffer.data(), + static_cast(buffer.size())); + if (length == 0) { + return {}; + } + if (length < buffer.size()) { + buffer.resize(length); + return std::filesystem::path(buffer).parent_path(); + } + buffer.resize(buffer.size() * 2); + } +#else + Dl_info info{}; + if (dladdr(anchor, &info) == 0 || info.dli_fname == nullptr) { + return {}; + } + return std::filesystem::path(info.dli_fname).parent_path(); +#endif +} + +[[nodiscard]] std::optional +readFile(const std::filesystem::path& path, const bool bundled, int& status) { + std::error_code error; + const auto exists = std::filesystem::exists(path, error); + if (error) { + status = bundled ? QDMI_ERROR_FATAL : QDMI_ERROR_PERMISSIONDENIED; + SPDLOG_ERROR("Cannot inspect QDMI device configuration '{}': {}", + path.string(), error.message()); + return std::nullopt; + } + if (!exists) { + status = bundled ? QDMI_ERROR_FATAL : QDMI_ERROR_NOTFOUND; + SPDLOG_ERROR("QDMI device configuration '{}' does not exist", + path.string()); + return std::nullopt; + } + errno = 0; + std::ifstream input(path, std::ios::binary); + if (!input) { + status = bundled ? QDMI_ERROR_FATAL : QDMI_ERROR_PERMISSIONDENIED; + SPDLOG_ERROR("Cannot read QDMI device configuration '{}': {}", + path.string(), std::strerror(errno)); + return std::nullopt; + } + std::string json{std::istreambuf_iterator(input), + std::istreambuf_iterator()}; + if (!input.eof() && input.fail()) { + status = bundled ? QDMI_ERROR_FATAL : QDMI_ERROR_PERMISSIONDENIED; + SPDLOG_ERROR("Failed while reading QDMI device configuration '{}'", + path.string()); + return std::nullopt; + } + status = QDMI_SUCCESS; + return LoadedDeviceConfiguration{.json = std::move(json), + .source = path.string()}; +} +} // namespace + +int setDeviceConfigurationParameter( + const QDMI_Device_Session_Parameter parameter, const size_t size, + const void* value, std::optional& inlineJson, + std::optional& file) { + if (parameter != QDMI_DEVICE_SESSION_PARAMETER_CUSTOM1 && + parameter != QDMI_DEVICE_SESSION_PARAMETER_CUSTOM2) { + return QDMI_ERROR_NOTSUPPORTED; + } + if (value == nullptr) { + return size == 0 ? QDMI_SUCCESS : QDMI_ERROR_INVALIDARGUMENT; + } + if (size == 0) { + return QDMI_ERROR_INVALIDARGUMENT; + } + const std::span bytes{static_cast(value), size}; + if (bytes.back() != '\0' || + std::memchr(bytes.data(), '\0', bytes.size() - 1) != nullptr) { + return QDMI_ERROR_INVALIDARGUMENT; + } + const std::string stringValue(bytes.data(), bytes.size() - 1); + if (parameter == QDMI_DEVICE_SESSION_PARAMETER_CUSTOM1) { + inlineJson = + stringValue.empty() ? std::nullopt : std::optional{stringValue}; + } else { + file = stringValue.empty() + ? std::nullopt + : std::optional{std::filesystem::path(stringValue)}; + } + return QDMI_SUCCESS; +} + +std::optional +loadDeviceConfiguration(const std::optional& inlineJson, + const std::optional& file, + const std::string_view inlineEnvironment, + const std::string_view fileEnvironment, + const std::string_view bundledFilename, + const void* anchor, int& status) { + if (inlineJson) { + status = QDMI_SUCCESS; + return LoadedDeviceConfiguration{.json = *inlineJson, + .source = "inline session configuration"}; + } + if (file) { + return readFile(*file, false, status); + } + const auto environmentJson = environment(inlineEnvironment); + const auto environmentFile = environment(fileEnvironment); + if (environmentJson && environmentFile) { + SPDLOG_ERROR("Both {} and {} are set", inlineEnvironment, fileEnvironment); + status = QDMI_ERROR_INVALIDARGUMENT; + return std::nullopt; + } + if (environmentJson) { + status = QDMI_SUCCESS; + return LoadedDeviceConfiguration{.json = *environmentJson, + .source = std::string(inlineEnvironment)}; + } + if (environmentFile) { + return readFile(std::filesystem::path(*environmentFile), false, status); + } + const auto directory = moduleDirectory(anchor); + if (directory.empty()) { + SPDLOG_ERROR("Cannot locate the QDMI provider module"); + status = QDMI_ERROR_FATAL; + return std::nullopt; + } + return readFile(directory / bundledFilename, true, status); +} +} // namespace qdmi::detail diff --git a/src/qdmi/devices/na/App.cpp b/src/qdmi/devices/na/App.cpp deleted file mode 100644 index 50bbb9fd93..0000000000 --- a/src/qdmi/devices/na/App.cpp +++ /dev/null @@ -1,452 +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 App.cpp - * @brief Implementation of the generator tool for neutral atom devices. - */ - -#include "qdmi/devices/na/Generator.hpp" - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace { -/** - * Prints the usage information for the command line tool. - * @param programName is the name of the program executable. - */ -auto printUsage(const std::string& programName) -> void { - std::cout - << "Generator for turning neutral atom computer JSON specifications into " - "header files to be used as part of a neutral atom QDMI device " - "implementation.\n" - "\n" - "Usage: " - << programName - << " [OPTIONS] [ARGS]\n" - "\n" - "Commands:\n" - " schema Generate a default JSON schema.\n" - " validate Validate a JSON specification.\n" - " generate Generate a header file from a JSON specification.\n" - "\n" - "Options:\n" - " -h, --help Show this help message and exit.\n" - " -v, --version Show version information and exit.\n"; -} - -/** - * Prints the usage information for the schema sub-command. - * @param programName is the name of the program executable. - */ -auto printSchemaUsage(const std::string& programName) -> void { - std::cout << "Generates a JSON schema with default values.\n" - "\n" - "Usage: " - << programName - << " schema [options]\n" - "\n" - "Options:\n" - " -h, --help Show this help message and exit.\n" - " -o, --output Specify the output file. If not\n" - " specified, prints to stdout.\n"; -} - -/** - * Prints the usage information for the validate sub-command. - * @param programName is the name of the program executable. - */ -auto printValidateUsage(const std::string& programName) -> void { - std::cout << "Validates a JSON specification against the schema.\n" - "\n" - "Usage: " - << programName - << " validate [options] []\n" - "\n" - "Arguments:\n" - " json_file the path to the JSON file to validate. If\n" - " not specified, the JSON is read from stdin.\n" - "\n" - "Options:\n" - " -h, --help Show this help message and exit.\n"; -} - -/** - * Prints the usage information for the generate sub-command. - * @param programName is the name of the program executable. - */ -auto printGenerateUsage(const std::string& programName) -> void { - std::cout << "Generates a header file from a JSON specification.\n" - "\n" - "Usage: " - << programName - << " generate [options] \n" - "\n" - "Arguments:\n" - " json_file the path to the JSON file to generate the\n" - " header file from. If not specified, the\n" - " JSON is read from stdin.\n" - "\n" - "Options:\n" - " -h, --help Show this help message and exit.\n" - " -o, --output Specify the output file for the\n" - " generated header file. If no output\n" - " file is specified, the header file is\n" - " printed to stdout.\n"; -} - -/** - * Prints the version information for the command line tool. - */ -auto printVersion() -> void { - // NOLINTNEXTLINE(misc-include-cleaner) - std::cout << "MQT QDMI NA Device Generator (MQT Version " MQT_CORE_VERSION - ")\n"; -} - -/// Enum to represent the different commands that can be executed. -enum class Command : uint8_t { - Schema, ///< Command to generate a JSON schema - Validate, ///< Command to validate a JSON specification - Generate ///< Command to generate a header file from a JSON specification -}; - -/// Struct to hold the parsed command line arguments. -struct Arguments { - std::string programName; ///< Name of the program executable - bool help = false; ///< Flag to indicate if help is requested - /// Flag to indicate if version information is requested - bool version = false; - std::optional command; ///< Command to execute -}; - -/// Struct to hold the parsed schema command line arguments. -struct SchemaArguments { - bool help = false; ///< Flag to indicate if help is requested - /// Optional output file for the schema - std::optional outputFile; -}; - -/// Struct to hold the parsed validate command line arguments. -struct ValidateArguments { - bool help = false; ///< Flag to indicate if help is requested - /// Optional JSON file to validate - std::optional jsonFile; -}; - -/// Struct to hold the parsed generate command line arguments. -struct GenerateArguments { - bool help = false; ///< Flag to indicate if help is requested - /// Optional output file for the generated header file - std::optional outputFile; - /// Optional JSON file to parse the device configuration - std::optional jsonFile; -}; - -/** - * Parses the command line arguments and returns an Arguments struct. - * @param args is the vector of command line arguments. - * @returns the parsed arguments as an Arguments struct and an index indicating - * the position of the first sub-command argument. - * @throws std::invalid_argument if the value after an option is missing. - */ -auto parseArguments(const std::vector& args) - -> std::pair { - Arguments arguments; - arguments.programName = - args.empty() ? "mqt-core-na-device-gen" : args.front(); - size_t i = 1; - while (i < args.size()) { - if (const std::string& arg = args.at(i); arg == "-h" || arg == "--help") { - arguments.help = true; - } else if (arg == "-v" || arg == "--version") { - arguments.version = true; - } else if (arg == "schema") { - arguments.command = Command::Schema; - break; // No more arguments for schema command - } else if (arg == "validate") { - arguments.command = Command::Validate; - break; // No more arguments for validate command - } else if (arg == "generate") { - arguments.command = Command::Generate; - break; // No more arguments for generate command - } else { - throw std::invalid_argument("Unknown argument: " + arg); - } - ++i; - } - return {arguments, i + 1}; -} - -/** - * Parses the command line arguments for the schema command and returns a - * SchemaArguments struct. - * @param args is the vector of command line arguments. - * @param i is the index to the first sub-command argument within @p args - * @return Parsed schema arguments as a SchemaArguments struct. - */ -auto parseSchemaArguments(const std::vector& args, size_t i) - -> SchemaArguments { - SchemaArguments schemaArgs; - while (i < args.size()) { - if (const std::string& arg = args.at(i); arg == "-h" || arg == "--help") { - schemaArgs.help = true; - } else if (arg == "-o" || arg == "--output") { - if (++i >= args.size()) { - throw std::invalid_argument("Missing value for output option."); - } - schemaArgs.outputFile = args.at(i); - } else { - throw std::invalid_argument("Unknown argument: " + arg); - } - ++i; - } - return schemaArgs; -} - -/** - * Parses the command line arguments for the validate command and returns a - * ValidateArguments struct. - * @param args is the vector of command line arguments. - * @param i is the index to the first sub-command argument within @p args - * @return Parsed validate arguments as a ValidateArguments struct. - */ -auto parseValidateArguments(const std::vector& args, size_t i) - -> ValidateArguments { - ValidateArguments validateArgs; - while (i < args.size()) { - if (const std::string& arg = args.at(i); arg == "-h" || arg == "--help") { - validateArgs.help = true; - } else { - validateArgs.jsonFile = arg; - } - ++i; - } - return validateArgs; -} - -/** - * Parses the command line arguments for the generate command and returns a - * GenerateArguments struct. - * @param args is the vector of command line arguments. - * @param i is the index to the first sub-command argument within @p args - * @return Parsed generate arguments as a GenerateArguments struct. - */ -auto parseGenerateArguments(const std::vector& args, size_t i) - -> GenerateArguments { - GenerateArguments generateArgs; - while (i < args.size()) { - if (const std::string& arg = args.at(i); arg == "-h" || arg == "--help") { - generateArgs.help = true; - } else if (arg == "-o" || arg == "--output") { - if (++i >= args.size()) { - throw std::invalid_argument("Missing value for output option."); - } - generateArgs.outputFile = args.at(i); - } else { - generateArgs.jsonFile = arg; - } - ++i; - } - return generateArgs; -} - -/** - * Executes the schema command, generating a JSON schema and writing it to the - * specified output file or stdout. - * @param progName is the name of the program executable. - * @param argVec is the vector of command line arguments. - * @param i is the index to the first sub-command argument within @p argVec - * @return 0 on success, 1 on error. - */ -auto executeSchemaCommand(const std::string& progName, - const std::vector& argVec, - const size_t i) -> int { - SchemaArguments schemaArgs; - // parse the rest of the command line arguments for the schema command - try { - schemaArgs = parseSchemaArguments(argVec, i); - } catch (const std::exception& e) { - SPDLOG_ERROR("Error parsing schema arguments: {}", e.what()); - printSchemaUsage(progName); - return 1; - } - // if the help flag is set, print the schema usage information and exit - if (schemaArgs.help) { - printSchemaUsage(progName); - return 0; - } - // generate the JSON schema and write it to the output file or stdout - try { - if (schemaArgs.outputFile.has_value()) { - na::writeJSONSchema(schemaArgs.outputFile.value()); - } else { - na::writeJSONSchema(std::cout); - } - } catch (const std::exception& e) { - SPDLOG_ERROR("Error generating JSON schema: {}", e.what()); - return 1; - } - return 0; -} - -/** - * Executes the validate command, validating a JSON file or JSON string from - * stdin. - * @param progName is the name of the program executable. - * @param argVec is the vector of command line arguments. - * @param i is the index to the first sub-command argument within @p argVec - * @return 0 on success, 1 on error. - */ -auto executeValidateCommand(const std::string& progName, - const std::vector& argVec, - const size_t i) -> int { - // parse the rest of the command line arguments for the validate command - const ValidateArguments validateArgs = parseValidateArguments(argVec, i); - // - if (validateArgs.help) { - printValidateUsage(progName); - return 0; - } - // validate the JSON file or the JSON string from stdin - try { - if (validateArgs.jsonFile.has_value()) { - std::ignore = na::readJSON(validateArgs.jsonFile.value()); - } else { - std::ignore = na::readJSON(std::cin); - } - } catch (const std::exception& e) { - SPDLOG_ERROR("Error validating JSON: {}", e.what()); - return 1; - } - return 0; -} - -/** - * Executes the generate command, generating a header file from a JSON file or - * JSON string from stdin. - * @param progName is the name of the program executable. - * @param argVec is the vector of command line arguments. - * @param i is the index to the first sub-command argument within @p argVec - * @return 0 on success, 1 on error. - */ -auto executeGenerateCommand(const std::string& progName, - const std::vector& argVec, - const size_t i) -> int { - GenerateArguments generateArgs; - // parse the rest of the command line arguments for the generate command - try { - generateArgs = parseGenerateArguments(argVec, i); - } catch (const std::exception& e) { - SPDLOG_ERROR("Error parsing generate arguments: {}", e.what()); - printGenerateUsage(progName); - return 1; - } - // if the help flag is set, print the generate usage information and exit - if (generateArgs.help) { - printGenerateUsage(progName); - return 0; - } - // generate the header file from the JSON specification - try { - na::Device device; - // read the JSON file or the JSON string from stdin - if (generateArgs.jsonFile.has_value()) { - device = na::readJSON(generateArgs.jsonFile.value()); - } else { - device = na::readJSON(std::cin); - } - // write the header file to the output file or stdout - if (generateArgs.outputFile.has_value()) { - na::writeHeader(device, generateArgs.outputFile.value()); - } else { - na::writeHeader(device, std::cout); - } - } catch (const std::exception& e) { - SPDLOG_ERROR("Error generating header file: {}", e.what()); - return 1; - } - return 0; -} -} // namespace - -/** - * @brief Main function that parses command-line-arguments and processes the - * JSON. - * @details This function handles the command line arguments, checks for help - * and version flags, and processes the JSON file or schema file as specified by - * the user. Either a JSON file or a schema file must be provided. If no output - * file is specified, the JSON file is parsed but no header file is generated. - * - * @param argc is the number of command line arguments. - * @param argv is the array of command line arguments. - */ -int main(int argc, char* argv[]) { - std::vector argVec; - std::pair parsedArgs; - // `main` functions should not throw exceptions. Apparently, the - // initialization of a vector can throw exceptions, so we catch them here. - try { - argVec.reserve(static_cast(argc)); - for (const auto& arg : std::span(argv, static_cast(argc))) { - argVec.emplace_back(arg); - } - } catch (std::exception& e) { - SPDLOG_ERROR("Error parsing arguments into vector: {}", e.what()); - return 1; - } - // parse the command line arguments up to the first sub-command - try { - parsedArgs = parseArguments(argVec); - } catch (const std::exception& e) { - SPDLOG_ERROR("Error parsing arguments: {}", e.what()); - printUsage(argVec.empty() ? "mqt-core-na-device-gen" : argVec.front()); - return 1; - } - // unpack the parsed arguments and the index of the first sub-command here - // because structured bindings only work with fresh variables - const auto& [args, i] = parsedArgs; - // print help or version information if requested - if (args.help) { - printUsage(args.programName); - return 0; - } - // if the version flag is set, print the version information and exit - if (args.version) { - printVersion(); - return 0; - } - // if no command is specified, print the usage information - if (!args.command.has_value()) { - printUsage(args.programName); - return 1; - } - switch (*args.command) { - case Command::Schema: - return executeSchemaCommand(args.programName, argVec, i); - case Command::Validate: - return executeValidateCommand(args.programName, argVec, i); - case Command::Generate: - return executeGenerateCommand(args.programName, argVec, i); - } - return 0; -} diff --git a/src/qdmi/devices/na/CMakeLists.txt b/src/qdmi/devices/na/CMakeLists.txt index b8c4781969..06722575d3 100644 --- a/src/qdmi/devices/na/CMakeLists.txt +++ b/src/qdmi/devices/na/CMakeLists.txt @@ -7,32 +7,28 @@ # Licensed under the MIT License # Set target name -set(TARGET_NAME ${MQT_CORE_TARGET_NAME}-qdmi-na-device-gen) +set(TARGET_NAME ${MQT_CORE_TARGET_NAME}-qdmi-na-device-config) # If the target is not already defined if(NOT TARGET ${TARGET_NAME}) - # Add library for device generation - add_mqt_core_library(${TARGET_NAME} ALIAS_NAME QDMINaDeviceGen) + # Add the runtime configuration parser library + add_mqt_core_library(${TARGET_NAME} ALIAS_NAME QDMINaDeviceConfig) # add sources to target - target_sources(${TARGET_NAME} PRIVATE Generator.cpp) + target_sources(${TARGET_NAME} PRIVATE Configuration.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/devices/na/Generator.hpp) + ${MQT_CORE_INCLUDE_BUILD_DIR}/qdmi/devices/na/Configuration.hpp) - # Link nlohmann_json and spdlog - target_link_libraries( - ${TARGET_NAME} - PUBLIC nlohmann_json::nlohmann_json - PRIVATE spdlog::spdlog) + target_link_libraries(${TARGET_NAME} PUBLIC nlohmann_json::nlohmann_json) # add to list of MQT core targets list(APPEND MQT_CORE_TARGETS ${TARGET_NAME}) - # place the generated library in a predictable location where the executable can find it + # Place the library in the common output directories. set_target_properties( ${TARGET_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}" @@ -47,38 +43,6 @@ if(NOT BUILD_MQT_CORE_QDMI_NA_DEVICE) return() endif() -# Set target name -set(TARGET_NAME mqt-core-qdmi-na-device-generator) - -# If the target is not already defined -if(NOT TARGET ${TARGET_NAME}) - # Add executable for device generation - add_executable(${TARGET_NAME}) - - # add sources to target - target_sources(${TARGET_NAME} PRIVATE App.cpp) - - # Link Generator library - target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQDMINaDeviceGen MQT::ProjectOptions - MQT::ProjectWarnings spdlog::spdlog) - - # set versioning information - set_target_properties(${TARGET_NAME} PROPERTIES VERSION ${PROJECT_VERSION} EXPORT_NAME - CoreNaDeviceGen) - # place in the bin directory - set_target_properties(${TARGET_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY - "${CMAKE_BINARY_DIR}/bin") - - # set c++ standard - target_compile_features(${TARGET_NAME} PRIVATE cxx_std_20) - - # Make version available - target_compile_definitions(${TARGET_NAME} PRIVATE MQT_CORE_VERSION="${MQT_CORE_VERSION}") - - # Create an alias for the target - add_executable(MQT::CoreQDMINaDeviceGenerator ALIAS ${TARGET_NAME}) -endif() - # Set target name set(TARGET_NAME ${MQT_CORE_TARGET_NAME}-qdmi-na-device) @@ -88,24 +52,8 @@ set(QDMI_PREFIX "MQT_NA") # If the target is not already defined if(NOT TARGET ${TARGET_NAME}) - # Set paths - set(JSON_FILE ${PROJECT_SOURCE_DIR}/json/na/device.json) - set(DEVICE_HDR ${CMAKE_CURRENT_BINARY_DIR}/include/qdmi/na/DeviceMemberInitializers.hpp) - - # Create include directory - file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/include/qdmi/na) - - # Generate definitions for device - add_custom_command( - OUTPUT ${DEVICE_HDR} - COMMAND MQT::CoreQDMINaDeviceGenerator ARGS generate --output ${DEVICE_HDR} ${JSON_FILE} - DEPENDS ${JSON_FILE} MQT::CoreQDMINaDeviceGenerator - COMMENT "Generating C++ header from ${JSON_FILE}") - add_custom_target(generate_qdmi_na_device_header DEPENDS ${DEVICE_HDR}) - # Add library add_mqt_core_library(${TARGET_NAME} FORCE_SHARED HIDDEN_VISIBILITY ALIAS_NAME QDMINaDevice) - add_dependencies(${TARGET_NAME} generate_qdmi_na_device_header) # Generate prefixed QDMI headers generate_prefixed_qdmi_headers(${QDMI_PREFIX}) @@ -123,14 +71,21 @@ if(NOT TARGET ${TARGET_NAME}) ${MQT_CORE_INCLUDE_BUILD_DIR} ${CMAKE_CURRENT_BINARY_DIR}/include FILES - ${DEVICE_HDR} ${MQT_CORE_INCLUDE_BUILD_DIR}/qdmi/devices/na/Device.hpp ${QDMI_HDRS}) # add link libraries - target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQDMICommon spdlog::spdlog) + target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQDMICommon MQT::CoreQDMINaDeviceConfig + spdlog::spdlog) - mqt_configure_qdmi_device(${TARGET_NAME} ID mqt.na.default PREFIX ${QDMI_PREFIX}) + mqt_configure_qdmi_device( + ${TARGET_NAME} + ID + mqt.na.default + PREFIX + ${QDMI_PREFIX} + RUNTIME_FILES + ${PROJECT_SOURCE_DIR}/json/na/mqt-core-qdmi-na-device.json) list(APPEND MQT_CORE_TARGETS ${TARGET_NAME}) endif() diff --git a/src/qdmi/devices/na/Configuration.cpp b/src/qdmi/devices/na/Configuration.cpp new file mode 100644 index 0000000000..4d256b2382 --- /dev/null +++ b/src/qdmi/devices/na/Configuration.cpp @@ -0,0 +1,668 @@ +/* + * 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 Configuration.cpp + * @brief Runtime configuration parsing for neutral-atom QDMI devices. + */ + +#include "qdmi/devices/na/Configuration.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace na { +namespace { +using Json = nlohmann::json; + +[[noreturn]] void validationError(const std::string_view source, + const std::string_view pointer, + const std::string_view message) { + throw std::invalid_argument(std::string(source) + ":" + std::string(pointer) + + " " + std::string(message)); +} + +void rejectUnknownKeys(const Json& value, + const std::initializer_list allowed, + const std::string_view source, + const std::string_view pointer) { + for (const auto& [key, unused] : value.items()) { + static_cast(unused); + if (std::ranges::find(allowed, std::string_view{key}) == allowed.end()) { + validationError(source, pointer, "contains unknown key '" + key + "'"); + } + } +} + +void requireObjectKeys(const Json& value, + const std::initializer_list required, + const std::string_view source, + const std::string& pointer) { + if (!value.is_object()) { + validationError(source, pointer, "must be an object"); + } + rejectUnknownKeys(value, required, source, pointer); + for (const auto key : required) { + if (!value.contains(key)) { + validationError(source, pointer + "/" + std::string(key), "is required"); + } + } +} + +void requireUnsigned(const Json& value, const std::string_view key, + const std::string_view source, + const std::string& pointer) { + const auto& field = value.at(key); + if (!field.is_number_unsigned() && + (!field.is_number_integer() || field.get() < 0)) { + validationError(source, pointer + "/" + std::string(key), + "must be a non-negative integer"); + } +} + +void requireNumber(const Json& value, const std::string_view key, + const std::string_view source, const std::string& pointer) { + if (!value.at(key).is_number()) { + validationError(source, pointer + "/" + std::string(key), + "must be a number"); + } +} + +void requireString(const Json& value, const std::string_view key, + const std::string_view source, const std::string& pointer) { + if (!value.at(key).is_string()) { + validationError(source, pointer + "/" + std::string(key), + "must be a string"); + } +} + +void validateVector(const Json& value, const std::string_view source, + const std::string& pointer) { + requireObjectKeys(value, {"x", "y"}, source, pointer); + for (const auto* const key : {"x", "y"}) { + const auto& coordinate = value.at(key); + const auto outOfRangeUnsigned = + coordinate.is_number_unsigned() && + coordinate.get() > + static_cast(std::numeric_limits::max()); + if (!coordinate.is_number_integer() || outOfRangeUnsigned) { + validationError(source, pointer + "/" + key, + "must be a signed 64-bit integer"); + } + } +} + +void validateRegion(const Json& value, const std::string_view source, + const std::string& pointer) { + requireObjectKeys(value, {"origin", "size"}, source, pointer); + validateVector(value.at("origin"), source, pointer + "/origin"); + requireObjectKeys(value.at("size"), {"width", "height"}, source, + pointer + "/size"); + requireUnsigned(value.at("size"), "width", source, pointer + "/size"); + requireUnsigned(value.at("size"), "height", source, pointer + "/size"); + const auto width = value.at("size").at("width").get(); + const auto height = value.at("size").at("height").get(); + if (width == 0 || height == 0 || + width > static_cast(std::numeric_limits::max()) || + height > static_cast(std::numeric_limits::max())) { + validationError(source, pointer + "/size", + "width and height must be positive signed 64-bit values"); + } + const auto originX = value.at("origin").at("x").get(); + const auto originY = value.at("origin").at("y").get(); + if (originX > + std::numeric_limits::max() - static_cast(width) || + originY > + std::numeric_limits::max() - static_cast(height)) { + validationError(source, pointer, + "origin plus size exceeds the coordinate range"); + } +} + +void validateOperation(const Json& value, const std::string_view source, + const std::string& pointer, const bool multiQubit) { + if (multiQubit) { + requireObjectKeys(value, + {"name", "region", "duration", "fidelity", + "numParameters", "interactionRadius", "blockingRadius", + "numQubits"}, + source, pointer); + } else { + requireObjectKeys( + value, {"name", "region", "duration", "fidelity", "numParameters"}, + source, pointer); + } + validateRegion(value.at("region"), source, pointer + "/region"); + requireString(value, "name", source, pointer); + requireUnsigned(value, "duration", source, pointer); + requireNumber(value, "fidelity", source, pointer); + requireUnsigned(value, "numParameters", source, pointer); + if (multiQubit) { + requireUnsigned(value, "interactionRadius", source, pointer); + requireUnsigned(value, "blockingRadius", source, pointer); + requireUnsigned(value, "numQubits", source, pointer); + } +} + +void validateNestedSchema(const Json& json, const std::string_view source) { + const auto validateArray = [&](const std::string_view key, + const auto& validateElement) { + const auto& array = json.at(key); + if (!array.is_array()) { + validationError(source, "$/" + std::string(key), "must be an array"); + } + for (size_t i = 0; i < array.size(); ++i) { + validateElement(array.at(i), + "$/" + std::string(key) + "/" + std::to_string(i)); + } + }; + + validateArray("traps", [&](const Json& trap, const std::string& pointer) { + requireObjectKeys(trap, + {"latticeOrigin", "latticeVector1", "latticeVector2", + "sublatticeOffsets", "extent"}, + source, pointer); + validateVector(trap.at("latticeOrigin"), source, + pointer + "/latticeOrigin"); + validateVector(trap.at("latticeVector1"), source, + pointer + "/latticeVector1"); + validateVector(trap.at("latticeVector2"), source, + pointer + "/latticeVector2"); + const auto& offsets = trap.at("sublatticeOffsets"); + if (!offsets.is_array() || offsets.empty()) { + validationError(source, pointer + "/sublatticeOffsets", + "must be a non-empty array"); + } + for (size_t i = 0; i < offsets.size(); ++i) { + validateVector(offsets.at(i), source, + pointer + "/sublatticeOffsets/" + std::to_string(i)); + } + validateRegion(trap.at("extent"), source, pointer + "/extent"); + }); + + const auto validateOperations = [&](const std::string_view key, + const bool multiQubit) { + validateArray(key, [&](const Json& operation, const std::string& pointer) { + validateOperation(operation, source, pointer, multiQubit); + }); + }; + validateOperations("globalSingleQubitOperations", false); + validateOperations("localSingleQubitOperations", false); + validateOperations("localMultiQubitOperations", true); + + validateArray("globalMultiQubitOperations", [&](const Json& operation, + const std::string& pointer) { + requireObjectKeys(operation, + {"name", "region", "duration", "fidelity", + "numParameters", "interactionRadius", "blockingRadius", + "idlingFidelity", "numQubits"}, + source, pointer); + validateRegion(operation.at("region"), source, pointer + "/region"); + requireString(operation, "name", source, pointer); + requireUnsigned(operation, "duration", source, pointer); + requireNumber(operation, "fidelity", source, pointer); + requireUnsigned(operation, "numParameters", source, pointer); + requireUnsigned(operation, "interactionRadius", source, pointer); + requireUnsigned(operation, "blockingRadius", source, pointer); + requireNumber(operation, "idlingFidelity", source, pointer); + requireUnsigned(operation, "numQubits", source, pointer); + }); + + validateArray( + "shuttlingUnits", [&](const Json& unit, const std::string& pointer) { + requireObjectKeys(unit, + {"region", "loadDuration", "storeDuration", + "loadFidelity", "storeFidelity", "numParameters", + "meanShuttlingSpeed"}, + source, pointer); + validateRegion(unit.at("region"), source, pointer + "/region"); + for (const auto* const key : {"loadDuration", "storeDuration", + "numParameters", "meanShuttlingSpeed"}) { + requireUnsigned(unit, key, source, pointer); + } + requireNumber(unit, "loadFidelity", source, pointer); + requireNumber(unit, "storeFidelity", source, pointer); + }); + requireObjectKeys(json.at("decoherenceTimes"), {"t1", "t2"}, source, + "$/decoherenceTimes"); + requireUnsigned(json.at("decoherenceTimes"), "t1", source, + "$/decoherenceTimes"); + requireUnsigned(json.at("decoherenceTimes"), "t2", source, + "$/decoherenceTimes"); + requireObjectKeys(json.at("lengthUnit"), {"scaleFactor", "unit"}, source, + "$/lengthUnit"); + requireNumber(json.at("lengthUnit"), "scaleFactor", source, "$/lengthUnit"); + requireString(json.at("lengthUnit"), "unit", source, "$/lengthUnit"); + requireObjectKeys(json.at("durationUnit"), {"scaleFactor", "unit"}, source, + "$/durationUnit"); + requireNumber(json.at("durationUnit"), "scaleFactor", source, + "$/durationUnit"); + requireString(json.at("durationUnit"), "unit", source, "$/durationUnit"); +} + +[[nodiscard]] Device parseAndValidate(const Json& json, + const std::string_view source) { + if (!json.is_object()) { + validationError(source, "$", "must be an object"); + } + requireObjectKeys(json, + {"schema-version", "name", "numQubits", "traps", + "minAtomDistance", "globalSingleQubitOperations", + "globalMultiQubitOperations", "localSingleQubitOperations", + "localMultiQubitOperations", "shuttlingUnits", + "decoherenceTimes", "lengthUnit", "durationUnit"}, + source, "$"); + validateNestedSchema(json, source); + requireString(json, "name", source, "$"); + requireUnsigned(json, "numQubits", source, "$"); + requireUnsigned(json, "minAtomDistance", source, "$"); + const auto& schemaVersion = json.at("schema-version"); + if (!schemaVersion.is_number_unsigned() && + !schemaVersion.is_number_integer()) { + validationError(source, "$/schema-version", "must be the integer 1"); + } + const auto supportedSchemaVersion = schemaVersion.is_number_unsigned() + ? schemaVersion.get() == 1 + : schemaVersion.get() == 1; + if (!supportedSchemaVersion) { + validationError(source, "$/schema-version", "must be 1"); + } + Device device; + try { + device = json.get(); + } catch (const Json::exception& error) { + validationError(source, "$", + "has an invalid value: " + std::string(error.what())); + } catch (const std::exception& error) { + validationError(source, "$", + "has an invalid value: " + std::string(error.what())); + } + device.schemaVersion = 1; + if (device.name.empty() || device.numQubits == 0 || + device.minAtomDistance == 0) { + validationError(source, "$", + "requires a non-empty name, positive numQubits, and " + "positive minAtomDistance"); + } + const auto validUnit = [](const Device::Unit& unit) { + return std::isfinite(unit.scaleFactor) && unit.scaleFactor > 0.; + }; + if (!validUnit(device.lengthUnit) || !validUnit(device.durationUnit)) { + validationError(source, "$/*Unit", + "scaleFactor must be positive and finite"); + } + const auto validFidelity = [](const double fidelity) { + return std::isfinite(fidelity) && fidelity >= 0. && fidelity <= 1.; + }; + const auto validateOperations = [&](const auto& operations, + const std::string_view pointer) { + std::set names; + for (const auto& operation : operations) { + if (operation.name.empty() || operation.duration == 0 || + !validFidelity(operation.fidelity) || + !names.emplace(operation.name).second) { + validationError(source, pointer, + "operations require a unique name, positive duration, " + "and fidelity in [0, 1]"); + } + } + }; + validateOperations(device.globalSingleQubitOperations, + "$/globalSingleQubitOperations"); + validateOperations(device.globalMultiQubitOperations, + "$/globalMultiQubitOperations"); + validateOperations(device.localSingleQubitOperations, + "$/localSingleQubitOperations"); + validateOperations(device.localMultiQubitOperations, + "$/localMultiQubitOperations"); + for (const auto& operation : device.globalMultiQubitOperations) { + if (!validFidelity(operation.idlingFidelity) || operation.numQubits == 0 || + operation.interactionRadius == 0 || operation.blockingRadius == 0) { + validationError(source, "$/globalMultiQubitOperations", + "requires positive arity and radii and valid " + "idlingFidelity"); + } + } + for (const auto& operation : device.localMultiQubitOperations) { + if (operation.numQubits != 2 || operation.interactionRadius == 0 || + operation.blockingRadius == 0) { + validationError(source, "$/localMultiQubitOperations", + "requires positive radii and exactly two qubits"); + } + } + for (const auto& unit : device.shuttlingUnits) { + if (!validFidelity(unit.loadFidelity) || + !validFidelity(unit.storeFidelity) || unit.loadDuration == 0 || + unit.storeDuration == 0 || unit.meanShuttlingSpeed == 0) { + validationError(source, "$/shuttlingUnits", + "durations and speed must be positive and fidelities " + "must be finite and in [0, 1]"); + } + } + if (device.decoherenceTimes.t1 == 0 || device.decoherenceTimes.t2 == 0) { + validationError(source, "$/decoherenceTimes", "t1 and t2 must be positive"); + } + std::set> uniqueCoordinates; + std::vector> coordinates; + size_t generatedSites = 0; + try { + forEachRegularSites(device.traps, [&](const SiteInfo& site) { + if (!uniqueCoordinates.emplace(site.x, site.y).second) { + validationError(source, "$/traps", + "generates duplicate site coordinates"); + } + coordinates.emplace_back(site.x, site.y); + ++generatedSites; + }); + } catch (const std::invalid_argument&) { + throw; + } catch (const std::exception& error) { + validationError(source, "$/traps", error.what()); + } + if (generatedSites < device.numQubits) { + validationError(source, "$/traps", + "does not generate enough sites for numQubits"); + } + constexpr size_t maxLocalPairCandidates = 10'000'000; + size_t remainingPairCandidates = maxLocalPairCandidates; + for (const auto& operation : device.localMultiQubitOperations) { + for (size_t first = 0; first < coordinates.size(); ++first) { + const auto& [x, y] = coordinates[first]; + if (operation.region.origin.x <= x && + x <= operation.region.origin.x + + static_cast(operation.region.size.width) && + operation.region.origin.y <= y && + y <= operation.region.origin.y + + static_cast(operation.region.size.height)) { + const auto candidates = coordinates.size() - first - 1; + if (candidates > remainingPairCandidates) { + validationError(source, "$/localMultiQubitOperations", + "expands to more than 10000000 candidate site pairs"); + } + remainingPairCandidates -= candidates; + } + } + } + return device; +} + +/** + * @brief Solves a 2D linear equation system. + * @details The equation has the following form: + * @code + * x1 * i + x2 * j = x0 + * y1 * i + y2 * j = y0 + * @endcode + * The free variables are i and j. + * @param x1 Coefficient for x in the first equation. + * @param x2 Coefficient for y in the first equation. + * @param y1 Coefficient for x in the second equation. + * @param y2 Coefficient for y in the second equation. + * @param x0 Right-hand side of the first equation. + * @param y0 Right-hand side of the second equation. + * @returns A pair containing the solution (i, j). + * @throws std::runtime_error if the system has no unique solution. + */ +[[noreturn]] void arithmeticOverflow() { + throw std::overflow_error( + "lattice arithmetic exceeds the signed 64-bit range"); +} + +[[nodiscard]] int64_t checkedAdd(const int64_t left, const int64_t right) { + if ((right > 0 && left > std::numeric_limits::max() - right) || + (right < 0 && left < std::numeric_limits::min() - right)) { + arithmeticOverflow(); + } + return left + right; +} + +[[nodiscard]] int64_t checkedSubtract(const int64_t left, const int64_t right) { + if ((right > 0 && left < std::numeric_limits::min() + right) || + (right < 0 && left > std::numeric_limits::max() + right)) { + arithmeticOverflow(); + } + return left - right; +} + +[[nodiscard]] int64_t checkedMultiply(const int64_t left, const int64_t right) { + if (left == 0 || right == 0) { + return 0; + } + if ((left == -1 && right == std::numeric_limits::min()) || + (right == -1 && left == std::numeric_limits::min())) { + arithmeticOverflow(); + } + bool overflow = false; + if (left > 0) { + overflow = right > 0 ? left > std::numeric_limits::max() / right + : right < std::numeric_limits::min() / left; + } else { + overflow = right > 0 ? left < std::numeric_limits::min() / right + : left < std::numeric_limits::max() / right; + } + if (overflow) { + arithmeticOverflow(); + } + return left * right; +} + +// Preserve the extended precision offered by platforms where it is available. +using LatticeFloat = long double; // NOLINT(google-runtime-float) + +[[nodiscard]] std::pair +solve2DLinearEquation(const int64_t x1, const int64_t x2, const int64_t y1, + const int64_t y2, const int64_t x0, const int64_t y0) { + const auto asLatticeFloat = [](const int64_t value) { + return static_cast(value); + }; + const auto det = (asLatticeFloat(x1) * asLatticeFloat(y2)) - + (asLatticeFloat(x2) * asLatticeFloat(y1)); + if (constexpr auto epsilon = 1e-10; std::abs(det) < epsilon) { + throw std::runtime_error("The system of equations has no unique solution."); + } + const auto detX = (asLatticeFloat(x0) * asLatticeFloat(y2)) - + (asLatticeFloat(x2) * asLatticeFloat(y0)); + const auto detY = (asLatticeFloat(x1) * asLatticeFloat(y0)) - + (asLatticeFloat(x0) * asLatticeFloat(y1)); + return {static_cast(detX / det), static_cast(detY / det)}; +} + +[[nodiscard]] int64_t floorToInt64(const double value) { + const auto floored = std::floor(value); + constexpr auto minInt64 = -0x1p63; + constexpr auto maxInt64Exclusive = 0x1p63; + if (!std::isfinite(floored) || floored < minInt64 || + floored >= maxInt64Exclusive) { + throw std::overflow_error("lattice index exceeds the signed 64-bit range"); + } + return static_cast(floored); +} + +[[nodiscard]] int64_t coordinate(const int64_t origin, const int64_t offset, + const int64_t firstIndex, + const int64_t firstVector, + const int64_t secondIndex, + const int64_t secondVector) { + return checkedAdd(checkedAdd(origin, offset), + checkedAdd(checkedMultiply(firstIndex, firstVector), + checkedMultiply(secondIndex, secondVector))); +} + +/** + * @brief Increments the indices in lexicographic order. + * @details This function increments the first index that is less than its + * limit, resets all previous indices to their minimum values. + * @param indices The vector of indices to increment. + * @param minima The minimum values for each index (used when resetting). + * @param limits The limits for each index. + * @returns true if the increment was successful, false if all indices have + * reached their limits. + */ +[[nodiscard]] bool increment(std::vector& indices, + const std::vector& minima, + const std::vector& limits) { + size_t i = 0; + for (; i < indices.size() && indices[i] == limits[i]; ++i) { + } + if (i == indices.size()) { + // all indices are at their limits + return false; + } + for (size_t j = 0; j < i; ++j) { + indices[j] = minima[j]; // Reset all previous indices to their minima + } + ++indices[i]; // Increment the next index + return true; +} +} // namespace + +[[nodiscard]] Device readJSON(std::istream& is) { + // Read the device configuration from the input stream + nlohmann::json json; + try { + is >> json; + // NOLINTNEXTLINE(misc-include-cleaner) + } catch (const nlohmann::detail::parse_error& e) { + std::stringstream ss; + ss << "Failed to parse JSON string: " << e.what(); + throw std::runtime_error(ss.str()); + } + return parseAndValidate(json, "input"); +} + +Device readJSON(const std::string_view json, const std::string_view source) { + try { + return parseAndValidate(Json::parse(json), source); + } catch (const Json::parse_error& error) { + throw std::invalid_argument(std::string(source) + + ": invalid JSON: " + error.what()); + } +} + +[[nodiscard]] Device readJSON(const std::string& path) { + // Read the device configuration from a JSON file + std::ifstream ifs(path); + if (!ifs.good()) { + throw std::runtime_error("Failed to open JSON file: " + std::string(path)); + } + const auto& device = readJSON(ifs); + ifs.close(); + return device; +} + +void forEachRegularSites(const std::vector& lattices, + const std::function& f, + const size_t startId) { + size_t count = startId; + size_t moduleCount = 0; + for (const auto& [latticeOrigin, latticeVector1, latticeVector2, + sublatticeOffsets, extent] : lattices) { + size_t subModuleCount = 0; + const auto& [origin, size] = extent; + const auto extentWidth = static_cast(size.width); + const auto extentHeight = static_cast(size.height); + const auto solve = [&](const int64_t x, const int64_t y) { + return solve2DLinearEquation(latticeVector1.x, latticeVector2.x, + latticeVector1.y, latticeVector2.y, x, y); + }; + const auto maximumX = checkedAdd(origin.x, extentWidth); + const auto maximumY = checkedAdd(origin.y, extentHeight); + + // indices of the bottom left corner + const auto& [bottomLeftI, bottomLeftJ] = + solve(checkedSubtract(origin.x, latticeOrigin.x), + checkedSubtract(origin.y, latticeOrigin.y)); + + // indices of the bottom right corner + const auto& [bottomRightI, bottomRightJ] = + solve(checkedSubtract(maximumX, latticeOrigin.x), + checkedSubtract(origin.y, latticeOrigin.y)); + + // indices of the top left corner + const auto& [topLeftI, topLeftJ] = + solve(checkedSubtract(origin.x, latticeOrigin.x), + checkedSubtract(maximumY, latticeOrigin.y)); + + // indices of the top right corner + const auto& [topRightI, topRightJ] = + solve(checkedSubtract(maximumX, latticeOrigin.x), + checkedSubtract(maximumY, latticeOrigin.y)); + + const auto minI = floorToInt64( + std::min({bottomLeftI, bottomRightI, topLeftI, topRightI})); + const auto minJ = floorToInt64( + std::min({bottomLeftJ, bottomRightJ, topLeftJ, topRightJ})); + const auto maxI = floorToInt64( + std::max({bottomLeftI, bottomRightI, topLeftI, topRightI})); + const auto maxJ = floorToInt64( + std::max({bottomLeftJ, bottomRightJ, topLeftJ, topRightJ})); + + constexpr size_t maxCandidateSites = 10'000'000; + const auto spanI = checkedAdd(checkedSubtract(maxI, minI), 1); + const auto spanJ = checkedAdd(checkedSubtract(maxJ, minJ), 1); + if (std::cmp_greater(spanI, maxCandidateSites) || + std::cmp_greater(spanJ, maxCandidateSites) || + spanI > static_cast(maxCandidateSites) / spanJ || + sublatticeOffsets.size() > + maxCandidateSites / + (static_cast(spanI) * static_cast(spanJ))) { + throw std::length_error( + "lattice expands to more than 10000000 candidate sites"); + } + + const std::vector minima{minI, minJ}; + const std::vector limits{maxI, maxJ}; + std::vector indices{minI, minJ}; + for (bool loop = true; loop; + loop = increment(indices, minima, limits), ++subModuleCount) { + // For every sublattice offset, add a site for repetition indices + for (const auto& [xOffset, yOffset] : sublatticeOffsets) { + if (count == std::numeric_limits::max()) { + throw std::overflow_error("generated site identifier overflow"); + } + const auto id = count++; + const auto x = + coordinate(latticeOrigin.x, xOffset, indices[0], latticeVector1.x, + indices[1], latticeVector2.x); + const auto y = + coordinate(latticeOrigin.y, yOffset, indices[0], latticeVector1.y, + indices[1], latticeVector2.y); + if (origin.x <= x && x <= origin.x + extentWidth && origin.y <= y && + y <= origin.y + extentHeight) { + // Only add the site if it is within the extent of the lattice + f(SiteInfo{.id = id, + .x = x, + .y = y, + .moduleId = moduleCount, + .subModuleId = subModuleCount}); + } + } + } + ++moduleCount; + } +} +} // namespace na diff --git a/src/qdmi/devices/na/Device.cpp b/src/qdmi/devices/na/Device.cpp index bfa227de42..1618a064dd 100644 --- a/src/qdmi/devices/na/Device.cpp +++ b/src/qdmi/devices/na/Device.cpp @@ -16,74 +16,272 @@ #include "mqt_na_qdmi/device.h" #include "qdmi/common/Common.hpp" -#include "qdmi/na/DeviceMemberInitializers.hpp" +#include "qdmi/common/DeviceConfiguration.hpp" +#include "qdmi/devices/na/Configuration.hpp" + +#include #include -#include +#include #include #include #include +#include #include +#include #include +#include #include +#include +#include #include #include #include #include -namespace qdmi::na { -Device::Device() { - // NOLINTBEGIN(cppcoreguidelines-prefer-member-initializer) - INITIALIZE_NAME(name_); - INITIALIZE_QUBITSNUM(qubitsNum_); - INITIALIZE_MINATOMDISTANCE(minAtomDistance_); - // NOLINTEND(cppcoreguidelines-prefer-member-initializer) - INITIALIZE_LENGTHUNIT(lengthUnit_); - INITIALIZE_DURATIONUNIT(durationUnit_); - // NOLINTNEXTLINE(misc-const-correctness) - INITIALIZE_SITES(sites_); - INITIALIZE_OPERATIONS(operations_); -} -auto Device::sessionAlloc(MQT_NA_QDMI_Device_Session* session) -> int { - if (session == nullptr) { +namespace { +[[nodiscard]] bool inside(const na::Device::Region& region, const int64_t x, + const int64_t y) { + return region.origin.x <= x && + x <= region.origin.x + static_cast(region.size.width) && + region.origin.y <= y && + y <= region.origin.y + static_cast(region.size.height); +} + +[[nodiscard]] uint64_t magnitude(const int64_t value) { + return value >= 0 + ? static_cast(value) + : static_cast(-(value + 1)) + static_cast(1); +} + +[[nodiscard]] uint64_t coordinateDistance(const int64_t first, + const int64_t second) { + if ((first < 0) == (second < 0)) { + return first >= second ? static_cast(first - second) + : static_cast(second - first); + } + return magnitude(first) + magnitude(second); +} + +[[nodiscard]] bool withinRadius(const int64_t firstX, const int64_t firstY, + const int64_t secondX, const int64_t secondY, + const uint64_t radius) { + const auto deltaX = coordinateDistance(firstX, secondX); + const auto deltaY = coordinateDistance(firstY, secondY); + return std::hypot(static_cast(deltaX), static_cast(deltaY)) <= + static_cast(radius); +} +} // namespace + +int MQT_NA_QDMI_Device_Session_impl_d::init() { + if (status_ != Status::ALLOCATED) { + return QDMI_ERROR_BADSTATE; + } + int loadStatus = QDMI_SUCCESS; + const auto loaded = qdmi::detail::loadDeviceConfiguration( + inlineConfiguration_, fileConfiguration_, "MQT_CORE_QDMI_NA_CONFIG_JSON", + "MQT_CORE_QDMI_NA_CONFIG_FILE", "mqt-core-qdmi-na-device.json", + reinterpret_cast(&MQT_NA_QDMI_device_initialize), + loadStatus); + if (!loaded) { + return loadStatus; + } + try { + const auto configuration = na::readJSON(loaded->json, loaded->source); + std::vector> newSiteStorage; + std::vector newSites; + std::vector globalMultiZones; + std::vector globalSingleZones; + std::vector shuttlingZones; + const auto addZone = [&](const na::Device::Region& region) { + auto site = std::make_unique( + this, newSiteStorage.size(), region.origin.x, region.origin.y, + region.size.width, region.size.height, + configuration.decoherenceTimes.t1, configuration.decoherenceTimes.t2); + auto* const handle = site.get(); + newSiteStorage.emplace_back(std::move(site)); + newSites.emplace_back(handle); + return handle; + }; + globalMultiZones.reserve(configuration.globalMultiQubitOperations.size()); + for (const auto& operation : configuration.globalMultiQubitOperations) { + globalMultiZones.emplace_back(addZone(operation.region)); + } + globalSingleZones.reserve(configuration.globalSingleQubitOperations.size()); + for (const auto& operation : configuration.globalSingleQubitOperations) { + globalSingleZones.emplace_back(addZone(operation.region)); + } + shuttlingZones.reserve(configuration.shuttlingUnits.size()); + for (const auto& unit : configuration.shuttlingUnits) { + shuttlingZones.emplace_back(addZone(unit.region)); + } + + struct RegularSite { + MQT_NA_QDMI_Site handle; + int64_t x; + int64_t y; + }; + std::vector regularSites; + na::forEachRegularSites(configuration.traps, [&](const na::SiteInfo& info) { + auto site = std::make_unique( + this, newSiteStorage.size(), info.moduleId, info.subModuleId, info.x, + info.y, configuration.decoherenceTimes.t1, + configuration.decoherenceTimes.t2); + auto* const handle = site.get(); + newSiteStorage.emplace_back(std::move(site)); + newSites.emplace_back(handle); + regularSites.push_back({.handle = handle, .x = info.x, .y = info.y}); + }); + + std::vector> + newOperationStorage; + const auto addOperation = [&](Args&&... args) { + newOperationStorage.emplace_back( + std::make_unique( + this, std::forward(args)...)); + }; + for (size_t i = 0; i < configuration.globalSingleQubitOperations.size(); + ++i) { + const auto& operation = configuration.globalSingleQubitOperations[i]; + addOperation(operation.name, operation.numParameters, 1U, + operation.duration, operation.fidelity, + globalSingleZones[i]); + } + for (size_t i = 0; i < configuration.globalMultiQubitOperations.size(); + ++i) { + const auto& operation = configuration.globalMultiQubitOperations[i]; + addOperation(operation.name, operation.numParameters, operation.numQubits, + operation.duration, operation.fidelity, + operation.interactionRadius, operation.blockingRadius, + operation.idlingFidelity, globalMultiZones[i]); + } + for (const auto& operation : configuration.localSingleQubitOperations) { + std::vector supported; + for (const auto& site : regularSites) { + if (inside(operation.region, site.x, site.y)) { + supported.emplace_back(site.handle); + } + } + addOperation(operation.name, operation.numParameters, operation.duration, + operation.fidelity, std::move(supported)); + } + for (const auto& operation : configuration.localMultiQubitOperations) { + std::vector> supported; + for (size_t first = 0; first < regularSites.size(); ++first) { + if (!inside(operation.region, regularSites[first].x, + regularSites[first].y)) { + continue; + } + for (size_t second = first + 1; second < regularSites.size(); + ++second) { + if (inside(operation.region, regularSites[second].x, + regularSites[second].y) && + withinRadius(regularSites[first].x, regularSites[first].y, + regularSites[second].x, regularSites[second].y, + operation.interactionRadius)) { + supported.emplace_back(regularSites[first].handle, + regularSites[second].handle); + } + } + } + addOperation(operation.name, operation.numParameters, operation.numQubits, + operation.duration, operation.fidelity, + operation.interactionRadius, operation.blockingRadius, + std::move(supported)); + } + for (size_t i = 0; i < configuration.shuttlingUnits.size(); ++i) { + const auto& unit = configuration.shuttlingUnits[i]; + addOperation("load<" + std::to_string(unit.id) + ">", unit.numParameters, + unit.loadDuration, unit.loadFidelity, shuttlingZones[i]); + addOperation("move<" + std::to_string(unit.id) + ">", unit.numParameters, + shuttlingZones[i], unit.meanShuttlingSpeed); + addOperation("store<" + std::to_string(unit.id) + ">", unit.numParameters, + unit.storeDuration, unit.storeFidelity, shuttlingZones[i]); + } + std::vector newOperations; + newOperations.reserve(newOperationStorage.size()); + std::ranges::transform( + newOperationStorage, std::back_inserter(newOperations), + [](const auto& operation) { return operation.get(); }); + + name_ = configuration.name; + qubitsNum_ = configuration.numQubits; + lengthUnit_ = configuration.lengthUnit; + durationUnit_ = configuration.durationUnit; + minAtomDistance_ = configuration.minAtomDistance; + siteStorage_ = std::move(newSiteStorage); + sites_ = std::move(newSites); + operationStorage_ = std::move(newOperationStorage); + operations_ = std::move(newOperations); + status_ = Status::INITIALIZED; + return QDMI_SUCCESS; + } catch (const std::bad_alloc&) { + SPDLOG_ERROR("Out of memory while initializing NA device from {}", + loaded->source); + return QDMI_ERROR_OUTOFMEM; + } catch (const std::invalid_argument& error) { + SPDLOG_ERROR("Invalid NA device configuration from {}: {}", loaded->source, + error.what()); + return QDMI_ERROR_INVALIDARGUMENT; + } catch (const std::exception& error) { + SPDLOG_ERROR("Failed to initialize NA device from {}: {}", loaded->source, + error.what()); + return QDMI_ERROR_FATAL; + } +} +int MQT_NA_QDMI_Device_Session_impl_d::setParameter( + QDMI_Device_Session_Parameter param, const size_t size, const void* value) { + if ((value != nullptr && size == 0) || + IS_INVALID_ARGUMENT(param, QDMI_DEVICE_SESSION_PARAMETER)) { return QDMI_ERROR_INVALIDARGUMENT; } - auto uniqueSession = std::make_unique(); - const auto& it = - sessions_.emplace(uniqueSession.get(), std::move(uniqueSession)).first; - // get the key, i.e., the raw pointer to the session from the map iterator - *session = it->first; + if (status_ != Status::ALLOCATED) { + return QDMI_ERROR_BADSTATE; + } + return qdmi::detail::setDeviceConfigurationParameter( + param, size, value, inlineConfiguration_, fileConfiguration_); +} +int MQT_NA_QDMI_Device_Session_impl_d::createDeviceJob( + // NOLINTNEXTLINE(readability-non-const-parameter) + MQT_NA_QDMI_Device_Job* job) { + if (job == nullptr) { + return QDMI_ERROR_INVALIDARGUMENT; + } + if (status_ == Status::ALLOCATED) { + return QDMI_ERROR_BADSTATE; + } + auto uniqueJob = std::make_unique(this); + *job = jobs_.emplace(uniqueJob.get(), std::move(uniqueJob)).first->first; return QDMI_SUCCESS; } -auto Device::sessionFree(MQT_NA_QDMI_Device_Session session) -> void { - if (session != nullptr) { - if (const auto& it = sessions_.find(session); it != sessions_.end()) { - sessions_.erase(it); - } +void MQT_NA_QDMI_Device_Session_impl_d::freeDeviceJob( + MQT_NA_QDMI_Device_Job job) { + if (job != nullptr) { + jobs_.erase(job); } } -auto Device::queryProperty(const QDMI_Device_Property prop, const size_t size, - void* value, size_t* sizeRet) -> int { +int MQT_NA_QDMI_Device_Session_impl_d::queryDeviceProperty( + const QDMI_Device_Property prop, const size_t size, void* value, + size_t* sizeRet) const { + if (status_ != Status::INITIALIZED) { + return QDMI_ERROR_BADSTATE; + } if ((value != nullptr && size == 0) || IS_INVALID_ARGUMENT(prop, QDMI_DEVICE_PROPERTY)) { return QDMI_ERROR_INVALIDARGUMENT; } ADD_STRING_PROPERTY(QDMI_DEVICE_PROPERTY_NAME, name_.c_str(), prop, size, value, sizeRet) - // NOLINTNEXTLINE(misc-include-cleaner) ADD_STRING_PROPERTY(QDMI_DEVICE_PROPERTY_VERSION, MQT_CORE_VERSION, prop, size, value, sizeRet) - // NOLINTNEXTLINE(misc-include-cleaner) ADD_STRING_PROPERTY(QDMI_DEVICE_PROPERTY_LIBRARYVERSION, QDMI_VERSION, prop, size, value, sizeRet) ADD_SINGLE_VALUE_PROPERTY(QDMI_DEVICE_PROPERTY_STATUS, QDMI_Device_Status, QDMI_DEVICE_STATUS_IDLE, prop, size, value, sizeRet) ADD_SINGLE_VALUE_PROPERTY(QDMI_DEVICE_PROPERTY_QUBITSNUM, size_t, qubitsNum_, prop, size, value, sizeRet) - // This device never needs calibration ADD_SINGLE_VALUE_PROPERTY(QDMI_DEVICE_PROPERTY_NEEDSCALIBRATION, size_t, 0, prop, size, value, sizeRet) - // This device does not support pulse-level control ADD_SINGLE_VALUE_PROPERTY( QDMI_DEVICE_PROPERTY_PULSESUPPORT, QDMI_Device_Pulse_Support_Level, QDMI_DEVICE_PULSE_SUPPORT_LEVEL_NONE, prop, size, value, sizeRet) @@ -113,58 +311,11 @@ auto Device::queryProperty(const QDMI_Device_Property prop, const size_t size, } return QDMI_ERROR_NOTSUPPORTED; } -} // namespace qdmi::na - -auto MQT_NA_QDMI_Device_Session_impl_d::init() -> int { - if (status_ != Status::ALLOCATED) { - return QDMI_ERROR_BADSTATE; - } - status_ = Status::INITIALIZED; - return QDMI_SUCCESS; -} -auto MQT_NA_QDMI_Device_Session_impl_d::setParameter( - QDMI_Device_Session_Parameter param, const size_t size, - const void* value) const -> int { - if ((value != nullptr && size == 0) || - IS_INVALID_ARGUMENT(param, QDMI_DEVICE_SESSION_PARAMETER)) { - return QDMI_ERROR_INVALIDARGUMENT; - } - if (status_ != Status::ALLOCATED) { - return QDMI_ERROR_BADSTATE; - } - return QDMI_ERROR_NOTSUPPORTED; -} -auto MQT_NA_QDMI_Device_Session_impl_d::createDeviceJob( - // NOLINTNEXTLINE(readability-non-const-parameter) - MQT_NA_QDMI_Device_Job* job) -> int { - if (job == nullptr) { - return QDMI_ERROR_INVALIDARGUMENT; - } - if (status_ == Status::ALLOCATED) { - return QDMI_ERROR_BADSTATE; - } - auto uniqueJob = std::make_unique(this); - *job = jobs_.emplace(uniqueJob.get(), std::move(uniqueJob)).first->first; - return QDMI_SUCCESS; -} -auto MQT_NA_QDMI_Device_Session_impl_d::freeDeviceJob( - MQT_NA_QDMI_Device_Job job) -> void { - if (job != nullptr) { - jobs_.erase(job); - } -} -auto MQT_NA_QDMI_Device_Session_impl_d::queryDeviceProperty( - const QDMI_Device_Property prop, const size_t size, void* value, - size_t* sizeRet) const -> int { - if (status_ != Status::INITIALIZED) { - return QDMI_ERROR_BADSTATE; - } - return qdmi::na::Device::get().queryProperty(prop, size, value, sizeRet); -} -auto MQT_NA_QDMI_Device_Session_impl_d::querySiteProperty( +int MQT_NA_QDMI_Device_Session_impl_d::querySiteProperty( MQT_NA_QDMI_Site site, const QDMI_Site_Property prop, const size_t size, - void* value, size_t* sizeRet) const -> int { - if (site == nullptr) { + void* value, size_t* sizeRet) const { + if (site == nullptr || std::ranges::find(sites_, site) == sites_.end() || + !site->ownedBy(this)) { return QDMI_ERROR_INVALIDARGUMENT; } if (status_ != Status::INITIALIZED) { @@ -172,27 +323,35 @@ auto MQT_NA_QDMI_Device_Session_impl_d::querySiteProperty( } return site->queryProperty(prop, size, value, sizeRet); } -auto MQT_NA_QDMI_Device_Session_impl_d::queryOperationProperty( +int MQT_NA_QDMI_Device_Session_impl_d::queryOperationProperty( MQT_NA_QDMI_Operation operation, const size_t numSites, const MQT_NA_QDMI_Site* sites, const size_t numParams, const double* params, const QDMI_Operation_Property prop, const size_t size, void* value, - size_t* sizeRet) const -> int { - if (operation == nullptr) { + size_t* sizeRet) const { + if (operation == nullptr || + std::ranges::find(operations_, operation) == operations_.end() || + !operation->ownedBy(this)) { return QDMI_ERROR_INVALIDARGUMENT; } if (status_ != Status::INITIALIZED) { return QDMI_ERROR_BADSTATE; } + if (sites != nullptr) { + for (auto* const site : std::span{sites, numSites}) { + if (site == nullptr || std::ranges::find(sites_, site) == sites_.end() || + !site->ownedBy(this)) { + return QDMI_ERROR_INVALIDARGUMENT; + } + } + } return operation->queryProperty(numSites, sites, numParams, params, prop, size, value, sizeRet); } -auto MQT_NA_QDMI_Device_Job_impl_d::free() -> void { - session_->freeDeviceJob(this); -} +void MQT_NA_QDMI_Device_Job_impl_d::free() { session_->freeDeviceJob(this); } // NOLINTNEXTLINE(readability-convert-member-functions-to-static) -auto MQT_NA_QDMI_Device_Job_impl_d::setParameter( - const QDMI_Device_Job_Parameter param, const size_t size, const void* value) - -> int { +int MQT_NA_QDMI_Device_Job_impl_d::setParameter( + const QDMI_Device_Job_Parameter param, const size_t size, + const void* value) { if ((value != nullptr && size == 0) || IS_INVALID_ARGUMENT(param, QDMI_DEVICE_JOB_PARAMETER)) { return QDMI_ERROR_INVALIDARGUMENT; @@ -200,10 +359,10 @@ auto MQT_NA_QDMI_Device_Job_impl_d::setParameter( return QDMI_ERROR_NOTSUPPORTED; } // NOLINTNEXTLINE(readability-convert-member-functions-to-static) -auto MQT_NA_QDMI_Device_Job_impl_d::queryProperty( +int MQT_NA_QDMI_Device_Job_impl_d::queryProperty( // NOLINTNEXTLINE(readability-non-const-parameter) const QDMI_Device_Job_Property prop, const size_t size, void* value, - [[maybe_unused]] size_t* sizeRet) -> int { + [[maybe_unused]] size_t* sizeRet) { if ((value != nullptr && size == 0) || IS_INVALID_ARGUMENT(prop, QDMI_DEVICE_JOB_PROPERTY)) { return QDMI_ERROR_INVALIDARGUMENT; @@ -211,71 +370,46 @@ auto MQT_NA_QDMI_Device_Job_impl_d::queryProperty( return QDMI_ERROR_NOTSUPPORTED; } // NOLINTNEXTLINE(readability-convert-member-functions-to-static) -auto MQT_NA_QDMI_Device_Job_impl_d::submit() -> int { - return QDMI_ERROR_NOTSUPPORTED; -} +int MQT_NA_QDMI_Device_Job_impl_d::submit() { return QDMI_ERROR_NOTSUPPORTED; } // NOLINTNEXTLINE(readability-convert-member-functions-to-static) -auto MQT_NA_QDMI_Device_Job_impl_d::cancel() -> int { - return QDMI_ERROR_NOTSUPPORTED; -} +int MQT_NA_QDMI_Device_Job_impl_d::cancel() { return QDMI_ERROR_NOTSUPPORTED; } // NOLINTNEXTLINE(readability-non-const-parameter,readability-convert-member-functions-to-static) -auto MQT_NA_QDMI_Device_Job_impl_d::check(QDMI_Job_Status* status) -> int { +int MQT_NA_QDMI_Device_Job_impl_d::check(QDMI_Job_Status* status) { if (status == nullptr) { return QDMI_ERROR_INVALIDARGUMENT; } return QDMI_ERROR_NOTSUPPORTED; } // NOLINTNEXTLINE(readability-convert-member-functions-to-static) -auto MQT_NA_QDMI_Device_Job_impl_d::wait([[maybe_unused]] const size_t timeout) - -> int { +int MQT_NA_QDMI_Device_Job_impl_d::wait([[maybe_unused]] const size_t timeout) { return QDMI_ERROR_NOTSUPPORTED; } // NOLINTNEXTLINE(readability-convert-member-functions-to-static) -auto MQT_NA_QDMI_Device_Job_impl_d::getResults( +int MQT_NA_QDMI_Device_Job_impl_d::getResults( QDMI_Job_Result result, // NOLINTNEXTLINE(readability-non-const-parameter) - const size_t size, void* data, [[maybe_unused]] size_t* sizeRet) -> int { + const size_t size, void* data, [[maybe_unused]] size_t* sizeRet) { if ((data != nullptr && size == 0) || IS_INVALID_ARGUMENT(result, QDMI_JOB_RESULT)) { return QDMI_ERROR_INVALIDARGUMENT; } return QDMI_ERROR_NOTSUPPORTED; } -MQT_NA_QDMI_Site_impl_d::MQT_NA_QDMI_Site_impl_d(const uint64_t id, - const uint64_t module, - const uint64_t subModule, - const int64_t x, - const int64_t y) - : id_(id), moduleId_(module), subModuleId_(subModule), x_(x), y_(y) { - INITIALIZE_DECOHERENCETIMES(decoherenceTimes_); -} -MQT_NA_QDMI_Site_impl_d::MQT_NA_QDMI_Site_impl_d(const uint64_t id, - const int64_t x, - const int64_t y, - const uint64_t width, - const uint64_t height) - : id_(id), x_(x), y_(y), xExtent_(width), yExtent_(height), isZone(true) { - INITIALIZE_DECOHERENCETIMES(decoherenceTimes_); -} -auto MQT_NA_QDMI_Site_impl_d::makeUniqueSite(const uint64_t id, - const uint64_t moduleId, - const uint64_t subModuleId, - const int64_t x, const int64_t y) - -> std::unique_ptr { - const MQT_NA_QDMI_Site_impl_d site(id, moduleId, subModuleId, x, y); - return std::make_unique(site); -} -auto MQT_NA_QDMI_Site_impl_d::makeUniqueZone(const uint64_t id, const int64_t x, - const int64_t y, - const uint64_t width, - const uint64_t height) - -> std::unique_ptr { - const MQT_NA_QDMI_Site_impl_d site(id, x, y, width, height); - return std::make_unique(site); -} -auto MQT_NA_QDMI_Site_impl_d::queryProperty(const QDMI_Site_Property prop, - const size_t size, void* value, - size_t* sizeRet) const -> int { +MQT_NA_QDMI_Site_impl_d::MQT_NA_QDMI_Site_impl_d( + MQT_NA_QDMI_Device_Session_impl_d* owner, const uint64_t id, + const uint64_t module, const uint64_t subModule, const int64_t x, + const int64_t y, const uint64_t t1, const uint64_t t2) + : owner_(owner), id_(id), moduleId_(module), subModuleId_(subModule), x_(x), + y_(y), decoherenceTimes_{.t1_ = t1, .t2_ = t2} {} +MQT_NA_QDMI_Site_impl_d::MQT_NA_QDMI_Site_impl_d( + MQT_NA_QDMI_Device_Session_impl_d* owner, const uint64_t id, + const int64_t x, const int64_t y, const uint64_t width, + const uint64_t height, const uint64_t t1, const uint64_t t2) + : owner_(owner), id_(id), x_(x), y_(y), xExtent_(width), yExtent_(height), + decoherenceTimes_{.t1_ = t1, .t2_ = t2}, isZone(true) {} +int MQT_NA_QDMI_Site_impl_d::queryProperty(const QDMI_Site_Property prop, + const size_t size, void* value, + size_t* sizeRet) const { if ((value != nullptr && size == 0) || IS_INVALID_ARGUMENT(prop, QDMI_SITE_PROPERTY)) { return QDMI_ERROR_INVALIDARGUMENT; @@ -306,52 +440,58 @@ auto MQT_NA_QDMI_Site_impl_d::queryProperty(const QDMI_Site_Property prop, return QDMI_ERROR_NOTSUPPORTED; } MQT_NA_QDMI_Operation_impl_d::MQT_NA_QDMI_Operation_impl_d( - std::string name, const size_t numParameters, const size_t numQubits, - const uint64_t duration, const double fidelity, MQT_NA_QDMI_Site zone) - : name_(std::move(name)), numParameters_(numParameters), + MQT_NA_QDMI_Device_Session_impl_d* owner, std::string name, + const size_t numParameters, const size_t numQubits, const uint64_t duration, + const double fidelity, MQT_NA_QDMI_Site zone) + : owner_(owner), name_(std::move(name)), numParameters_(numParameters), numQubits_(numQubits), duration_(duration), fidelity_(fidelity), supportedSites_(std::vector{zone}), isZoned_(true) {} MQT_NA_QDMI_Operation_impl_d::MQT_NA_QDMI_Operation_impl_d( - std::string name, const size_t numParameters, const size_t numQubits, - const uint64_t duration, const double fidelity, - const uint64_t interactionRadius, uint64_t blockingRadius, - const double idlingFidelity, MQT_NA_QDMI_Site zone) - : name_(std::move(name)), numParameters_(numParameters), + MQT_NA_QDMI_Device_Session_impl_d* owner, std::string name, + const size_t numParameters, const size_t numQubits, const uint64_t duration, + const double fidelity, const uint64_t interactionRadius, + uint64_t blockingRadius, const double idlingFidelity, MQT_NA_QDMI_Site zone) + : owner_(owner), name_(std::move(name)), numParameters_(numParameters), numQubits_(numQubits), duration_(duration), fidelity_(fidelity), interactionRadius_(interactionRadius), blockingRadius_(blockingRadius), idlingFidelity_(idlingFidelity), supportedSites_(std::vector{zone}), isZoned_(true) {} MQT_NA_QDMI_Operation_impl_d::MQT_NA_QDMI_Operation_impl_d( - std::string name, const size_t numParameters, const uint64_t duration, - const double fidelity, const std::vector& sites) - : name_(std::move(name)), numParameters_(numParameters), numQubits_(1), - duration_(duration), fidelity_(fidelity), supportedSites_(sites) { + MQT_NA_QDMI_Device_Session_impl_d* owner, std::string name, + const size_t numParameters, const uint64_t duration, const double fidelity, + std::vector sites) + : owner_(owner), name_(std::move(name)), numParameters_(numParameters), + numQubits_(1), duration_(duration), fidelity_(fidelity), + supportedSites_(std::move(sites)) { sortSites(); } MQT_NA_QDMI_Operation_impl_d::MQT_NA_QDMI_Operation_impl_d( - std::string name, const size_t numParameters, const size_t numQubits, - const uint64_t duration, const double fidelity, - const uint64_t interactionRadius, uint64_t blockingRadius, - const std::vector>& sites) - : name_(std::move(name)), numParameters_(numParameters), + MQT_NA_QDMI_Device_Session_impl_d* owner, std::string name, + const size_t numParameters, const size_t numQubits, const uint64_t duration, + const double fidelity, const uint64_t interactionRadius, + uint64_t blockingRadius, + std::vector> sites) + : owner_(owner), name_(std::move(name)), numParameters_(numParameters), numQubits_(numQubits), duration_(duration), fidelity_(fidelity), interactionRadius_(interactionRadius), blockingRadius_(blockingRadius), - supportedSites_(sites) { + supportedSites_(std::move(sites)) { sortSites(); } MQT_NA_QDMI_Operation_impl_d::MQT_NA_QDMI_Operation_impl_d( - std::string name, size_t numParameters, uint64_t duration, double fidelity, + MQT_NA_QDMI_Device_Session_impl_d* owner, std::string name, + const size_t numParameters, const uint64_t duration, const double fidelity, MQT_NA_QDMI_Site zone) - : name_(std::move(name)), numParameters_(numParameters), + : owner_(owner), name_(std::move(name)), numParameters_(numParameters), duration_(duration), fidelity_(fidelity), supportedSites_(std::vector{zone}), isZoned_(true) {} MQT_NA_QDMI_Operation_impl_d::MQT_NA_QDMI_Operation_impl_d( - std::string name, size_t numParameters, MQT_NA_QDMI_Site zone, - uint64_t meanShuttlingSpeed) - : name_(std::move(name)), numParameters_(numParameters), + MQT_NA_QDMI_Device_Session_impl_d* owner, std::string name, + const size_t numParameters, MQT_NA_QDMI_Site zone, + const uint64_t meanShuttlingSpeed) + : owner_(owner), name_(std::move(name)), numParameters_(numParameters), meanShuttlingSpeed_(meanShuttlingSpeed), supportedSites_(std::vector{zone}), isZoned_(true) {} -auto MQT_NA_QDMI_Operation_impl_d::sortSites() -> void { +void MQT_NA_QDMI_Operation_impl_d::sortSites() { std::visit( [](auto& sites) { using T = std::decay_t; @@ -375,74 +515,11 @@ auto MQT_NA_QDMI_Operation_impl_d::sortSites() -> void { }, supportedSites_); } -auto MQT_NA_QDMI_Operation_impl_d::makeUniqueGlobalSingleQubit( - const std::string& name, const size_t numParameters, - const uint64_t duration, const double fidelity, MQT_NA_QDMI_Site zone) - -> std::unique_ptr { - MQT_NA_QDMI_Operation_impl_d op(name, numParameters, 1U, duration, fidelity, - zone); - return std::make_unique(std::move(op)); -} -auto MQT_NA_QDMI_Operation_impl_d::makeUniqueGlobalMultiQubit( - const std::string& name, const size_t numParameters, const size_t numQubits, - const uint64_t duration, const double fidelity, - const uint64_t interactionRadius, const uint64_t blockingRadius, - const double idlingFidelity, MQT_NA_QDMI_Site zone) - -> std::unique_ptr { - MQT_NA_QDMI_Operation_impl_d op(name, numParameters, numQubits, duration, - fidelity, interactionRadius, blockingRadius, - idlingFidelity, zone); - return std::make_unique(std::move(op)); -} -auto MQT_NA_QDMI_Operation_impl_d::makeUniqueLocalSingleQubit( - const std::string& name, const size_t numParameters, - const uint64_t duration, const double fidelity, - const std::vector& sites) - -> std::unique_ptr { - MQT_NA_QDMI_Operation_impl_d op(name, numParameters, duration, fidelity, - sites); - return std::make_unique(std::move(op)); -} -auto MQT_NA_QDMI_Operation_impl_d::makeUniqueLocalTwoQubit( - const std::string& name, const size_t numParameters, const size_t numQubits, - const uint64_t duration, const double fidelity, - const uint64_t interactionRadius, const uint64_t blockingRadius, - const std::vector>& sites) - -> std::unique_ptr { - MQT_NA_QDMI_Operation_impl_d op(name, numParameters, numQubits, duration, - fidelity, interactionRadius, blockingRadius, - sites); - return std::make_unique(std::move(op)); -} -auto MQT_NA_QDMI_Operation_impl_d::makeUniqueShuttlingLoad( - const std::string& name, const size_t numParameters, - const uint64_t duration, const double fidelity, MQT_NA_QDMI_Site zone) - -> std::unique_ptr { - MQT_NA_QDMI_Operation_impl_d op(name, numParameters, duration, fidelity, - zone); - return std::make_unique(std::move(op)); -} -auto MQT_NA_QDMI_Operation_impl_d::makeUniqueShuttlingMove( - const std::string& name, const size_t numParameters, MQT_NA_QDMI_Site zone, - const uint64_t meanShuttlingSpeed) - -> std::unique_ptr { - MQT_NA_QDMI_Operation_impl_d op(name, numParameters, zone, - meanShuttlingSpeed); - return std::make_unique(std::move(op)); -} -auto MQT_NA_QDMI_Operation_impl_d::makeUniqueShuttlingStore( - const std::string& name, const size_t numParameters, - const uint64_t duration, const double fidelity, MQT_NA_QDMI_Site zone) - -> std::unique_ptr { - MQT_NA_QDMI_Operation_impl_d op(name, numParameters, duration, fidelity, - zone); - return std::make_unique(std::move(op)); -} -auto MQT_NA_QDMI_Operation_impl_d::queryProperty( +int MQT_NA_QDMI_Operation_impl_d::queryProperty( const size_t numSites, const MQT_NA_QDMI_Site* sites, const size_t numParams, const double* params, const QDMI_Operation_Property prop, const size_t size, void* value, - size_t* sizeRet) const -> int { + size_t* sizeRet) const { if ((sites != nullptr && numSites == 0) || (params != nullptr && numParams == 0) || (value != nullptr && size == 0) || @@ -456,7 +533,7 @@ auto MQT_NA_QDMI_Operation_impl_d::queryProperty( if (numSites == 1) { // If the (single) site is not supported, return with an error const bool found = std::visit( - [sites](const auto& storedSites) -> bool { + [sites](const auto& storedSites) { using T = std::decay_t; if constexpr (std::is_same_v>) { return std::ranges::binary_search(storedSites, *sites, @@ -476,7 +553,7 @@ auto MQT_NA_QDMI_Operation_impl_d::queryProperty( // NOLINTEND(cppcoreguidelines-pro-bounds-pointer-arithmetic) // if the pair of sites is not supported, return with an error const bool found = std::visit( - [&needle](const auto& storedSites) -> bool { + [&needle](const auto& storedSites) { using T = std::decay_t; if constexpr (std::is_same_v< T, std::vector int { + [&](const auto& storedSites) { using T = std::decay_t; if constexpr (std::is_same_v>) { // Single-qubit: return flat array @@ -564,16 +641,18 @@ auto MQT_NA_QDMI_Operation_impl_d::queryProperty( return QDMI_ERROR_NOTSUPPORTED; } -int MQT_NA_QDMI_device_initialize() { - // ensure the singleton is initialized - std::ignore = qdmi::na::Device::get(); - return QDMI_SUCCESS; -} +int MQT_NA_QDMI_device_initialize() { return QDMI_SUCCESS; } int MQT_NA_QDMI_device_finalize() { return QDMI_SUCCESS; } int MQT_NA_QDMI_device_session_alloc(MQT_NA_QDMI_Device_Session* session) { - return qdmi::na::Device::get().sessionAlloc(session); + if (session == nullptr) { + return QDMI_ERROR_INVALIDARGUMENT; + } + // QDMI transfers ownership through its opaque C handle. + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + *session = new (std::nothrow) MQT_NA_QDMI_Device_Session_impl_d; + return *session == nullptr ? QDMI_ERROR_OUTOFMEM : QDMI_SUCCESS; } int MQT_NA_QDMI_device_session_init(MQT_NA_QDMI_Device_Session session) { @@ -584,7 +663,8 @@ int MQT_NA_QDMI_device_session_init(MQT_NA_QDMI_Device_Session session) { } void MQT_NA_QDMI_device_session_free(MQT_NA_QDMI_Device_Session session) { - qdmi::na::Device::get().sessionFree(session); + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + delete session; } int MQT_NA_QDMI_device_session_set_parameter( diff --git a/src/qdmi/devices/na/Generator.cpp b/src/qdmi/devices/na/Generator.cpp deleted file mode 100644 index d018aaac32..0000000000 --- a/src/qdmi/devices/na/Generator.cpp +++ /dev/null @@ -1,512 +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 Generator.cpp - * @brief The MQT QDMI device generator for neutral atom devices. - */ - -#include "qdmi/devices/na/Generator.hpp" - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace na { -namespace { -/** - * @brief Populates all array fields in the device object with default values. - * @param device is the device object to populate. - * @note This is a recursive auxiliary function used by @ref writeJSONSchema. - */ -auto populateArrayFields(Device& device) -> void { - device.traps.emplace_back().sublatticeOffsets.emplace_back(); - device.globalMultiQubitOperations.emplace_back(); - device.globalSingleQubitOperations.emplace_back(); - device.localMultiQubitOperations.emplace_back(); - device.localSingleQubitOperations.emplace_back(); - device.shuttlingUnits.emplace_back(); -} - -/** - * @brief Writes the name from the device object. - * @param device is the device object containing the name. - * @param os is the output stream to write the name to. - */ -auto writeName(const Device& device, std::ostream& os) -> void { - os << "#define INITIALIZE_NAME(var) var = \"" << device.name << "\"\n"; -} - -/** - * @brief Writes the qubits number from the device object. - * @param device is the device object containing the number of qubits. - * @param os is the output stream to write the qubits number to. - */ -auto writeQubitsNum(const Device& device, std::ostream& os) -> void { - os << "#define INITIALIZE_QUBITSNUM(var) var = " << device.numQubits - << "UL\n"; -} - -/** - * @brief Writes the length unit from the device object. - * @param device is the device object containing the length unit. - * @param os is the output stream to write the length unit to. - */ -auto writeLengthUnit(const Device& device, std::ostream& os) -> void { - os << "#define INITIALIZE_LENGTHUNIT(var) var = {\"" << device.lengthUnit.unit - << "\", " << device.lengthUnit.scaleFactor << "}\n"; -} - -/** - * @brief Writes the duration unit from the device object. - * @param device is the device object containing the duration unit. - * @param os is the output stream to write the duration unit to. - */ -auto writeDurationUnit(const Device& device, std::ostream& os) -> void { - os << "#define INITIALIZE_DURATIONUNIT(var) var = {\"" - << device.durationUnit.unit << "\", " << device.durationUnit.scaleFactor - << "}\n"; -} - -/** - * @brief Writes the minimum atom distance from the device object. - * @param device is the device object containing the minimum atom distance. - * @param os is the output stream to write the minimum atom distance to. - */ -auto writeMinAtomDistance(const Device& device, std::ostream& os) -> void { - os << "#define INITIALIZE_MINATOMDISTANCE(var) var = " - << device.minAtomDistance << "\n"; -} - -/** - * @brief Writes the sites from the device object. - * @param device is the device object containing the sites. - * @param os is the output stream to write the sites to. - */ -auto writeSites(const Device& device, std::ostream& os) -> void { - size_t count = 0; - os << "#define INITIALIZE_SITES(var) var.clear()"; - // first write all zone sites - for (const auto& operation : device.globalMultiQubitOperations) { - const auto& [origin, size] = operation.region; - const auto id = count++; - const auto x = origin.x; - const auto y = origin.y; - const auto width = size.width; - const auto height = size.height; - os << ";\\\n " - "MQT_NA_QDMI_Site globalOp" - << operation.name - << "ZoneSite = " - "var.emplace_back(MQT_NA_QDMI_Site_impl_d::makeUniqueZone(" - << id << "U, " << x << ", " << y << ", " << width << "U, " << height - << "U)).get()"; - } - for (const auto& operation : device.globalSingleQubitOperations) { - const auto& [origin, size] = operation.region; - const auto id = count++; - const auto x = origin.x; - const auto y = origin.y; - const auto width = size.width; - const auto height = size.height; - os << ";\\\n " - "MQT_NA_QDMI_Site globalOp" - << operation.name - << "ZoneSite = " - "var.emplace_back(MQT_NA_QDMI_Site_impl_d::makeUniqueZone(" - << id << "U, " << x << ", " << y << ", " << width << "U, " << height - << "U)).get()"; - } - for (const auto& shuttlingUnit : device.shuttlingUnits) { - const auto& [origin, size] = shuttlingUnit.region; - const auto id = count++; - const auto x = origin.x; - const auto y = origin.y; - const auto width = size.width; - const auto height = size.height; - os << ";\\\n " - "MQT_NA_QDMI_Site shuttlingUnit" - << shuttlingUnit.id - << "ZoneSite = " - "var.emplace_back(MQT_NA_QDMI_Site_impl_d::makeUniqueZone(" - << id << "U, " << x << ", " << y << ", " << width << "U, " << height - << "U)).get()"; - } - for (const auto& operation : device.localSingleQubitOperations) { - os << ";\\\n std::vector localOp" << operation.name - << "Sites"; - } - for (const auto& operation : device.localMultiQubitOperations) { - os << ";\\\n std::vector> " - "localOp" - << operation.name << "Sites"; - } - // then write all regular sites - std::vector> sites; - forEachRegularSites( - device.traps, - [&sites, &os, &device](const SiteInfo& site) { - sites.emplace_back(site.id, site.x, site.y); - os << ";\\\n " - "var.emplace_back(MQT_NA_QDMI_Site_impl_d::makeUniqueSite(" - << site.id << "U, " << site.moduleId << "U, " << site.subModuleId - << "U, " << site.x << ", " << site.y << "))"; - for (const auto& operation : device.localSingleQubitOperations) { - if (site.x >= operation.region.origin.x && - site.x <= operation.region.origin.x + - static_cast(operation.region.size.width) && - site.y >= operation.region.origin.y && - site.y <= - operation.region.origin.y + - static_cast(operation.region.size.height)) { - os << ";\\\n localOp" << operation.name - << "Sites.emplace_back(var.back().get())"; - } - } - // this generator (same as the device implementation) only supports - // two-qubit local operations - for (const auto& operation : device.localMultiQubitOperations) { - if (operation.region.origin.x <= site.x && - site.x <= operation.region.origin.x + - static_cast(operation.region.size.width) && - operation.region.origin.y <= site.y && - site.y <= - operation.region.origin.y + - static_cast(operation.region.size.height)) { - for (const auto& [i2, x2, y2] : - sites | std::views::take(sites.size() - 1)) { - if (operation.region.origin.x <= x2 && - x2 <= operation.region.origin.x + - static_cast(operation.region.size.width) && - operation.region.origin.y <= y2 && - y2 <= - operation.region.origin.y + - static_cast(operation.region.size.height) && - std::hypot(x2 - site.x, y2 - site.y) <= - static_cast(operation.interactionRadius)) { - os << ";\\\n localOp" << operation.name - << "Sites.emplace_back(var.at(" << i2 - << ").get(), var.back().get())"; - } - } - } - } - }, - count); - os << "\n"; -} - -/** - * @brief Writes the operations from the device object. - * @param device is the device object containing the operations configuration. - * @param os is the output stream to write the operations to. - */ -auto writeOperations(const Device& device, std::ostream& os) -> void { - os << "#define INITIALIZE_OPERATIONS(var) var.clear()"; - for (const auto& operation : device.globalSingleQubitOperations) { - os << ";\\\n" - " " - "var.emplace_back(MQT_NA_QDMI_Operation_impl_d::" - "makeUniqueGlobalSingleQubit(\"" - << operation.name << "\", " << operation.numParameters << ", " - << operation.duration << ", " << operation.fidelity << ", globalOp" - << operation.name << "ZoneSite))"; - } - for (const auto& operation : device.globalMultiQubitOperations) { - os << ";\\\n" - " " - "var.emplace_back(MQT_NA_QDMI_Operation_impl_d::" - "makeUniqueGlobalMultiQubit(\"" - << operation.name << "\", " << operation.numParameters << ", " - << operation.numQubits << ", " << operation.duration << ", " - << operation.fidelity << ", " << operation.interactionRadius << ", " - << operation.blockingRadius << ", " << operation.idlingFidelity - << ", globalOp" << operation.name << "ZoneSite))"; - } - for (const auto& operation : device.localSingleQubitOperations) { - os << ";\\\n" - " " - "var.emplace_back(MQT_NA_QDMI_Operation_impl_d::" - "makeUniqueLocalSingleQubit(\"" - << operation.name << "\", " << operation.numParameters << ", " - << operation.duration << ", " << operation.fidelity << ", localOp" - << operation.name << "Sites))"; - } - for (const auto& operation : device.localMultiQubitOperations) { - os << ";\\\n" - " " - "var.emplace_back(MQT_NA_QDMI_Operation_impl_d::" - "makeUniqueLocalTwoQubit(\"" - << operation.name << "\", " << operation.numParameters << ", " - << operation.numQubits << ", " << operation.duration << ", " - << operation.fidelity << "," << operation.interactionRadius << ", " - << operation.blockingRadius << ", localOp" << operation.name - << "Sites))"; - } - for (const auto& shuttlingUnit : device.shuttlingUnits) { - os << ";\\\n" - " " - "var.emplace_back(MQT_NA_QDMI_Operation_impl_d::" - "makeUniqueShuttlingLoad(\"load<" - << shuttlingUnit.id << ">\", " << shuttlingUnit.numParameters << ", " - << shuttlingUnit.loadDuration << ", " << shuttlingUnit.loadFidelity - << ", shuttlingUnit" << shuttlingUnit.id << "ZoneSite))"; - os << ";\\\n" - " " - "var.emplace_back(MQT_NA_QDMI_Operation_impl_d::" - "makeUniqueShuttlingMove(\"move<" - << shuttlingUnit.id << ">\", " << shuttlingUnit.numParameters - << ", shuttlingUnit" << shuttlingUnit.id << "ZoneSite, " - << shuttlingUnit.meanShuttlingSpeed << "))"; - os << ";\\\n" - " " - "var.emplace_back(MQT_NA_QDMI_Operation_impl_d::" - "makeUniqueShuttlingStore(\"store<" - << shuttlingUnit.id << ">\", " << shuttlingUnit.numParameters << ", " - << shuttlingUnit.storeDuration << ", " << shuttlingUnit.storeFidelity - << ", shuttlingUnit" << shuttlingUnit.id << "ZoneSite))"; - } - os << "\n"; -} - -/** - * @brief Writes the decoherence times from the device object. - * @param device is the device object containing the decoherence times. - * @param os is the output stream to write the sites to. - */ -auto writeDecoherenceTimes(const Device& device, std::ostream& os) -> void { - os << "#define INITIALIZE_DECOHERENCETIMES(var) var = {" - << device.decoherenceTimes.t1 << ", " << device.decoherenceTimes.t2 - << "}\n"; -} - -/** - * @brief Solves a 2D linear equation system. - * @details The equation has the following form: - * @code - * x1 * i + x2 * j = x0 - * y1 * i + y2 * j = y0 - * @endcode - * The free variables are i and j. - * @param x1 Coefficient for x in the first equation. - * @param x2 Coefficient for y in the first equation. - * @param y1 Coefficient for x in the second equation. - * @param y2 Coefficient for y in the second equation. - * @param x0 Right-hand side of the first equation. - * @param y0 Right-hand side of the second equation. - * @returns A pair containing the solution (x, y). - * @throws std::runtime_error if the system has no unique solution (determinant - * is near zero). - */ -template -[[nodiscard]] auto solve2DLinearEquation(const T x1, const T x2, const T y1, - const T y2, const T x0, const T y0) - -> std::pair { - // Calculate the determinant - const auto det = static_cast((x1 * y2) - (x2 * y1)); - if (constexpr auto epsilon = 1e-10; std::abs(det) < epsilon) { - throw std::runtime_error("The system of equations has no unique solution."); - } - // Calculate the solution - const auto detX = static_cast((x0 * y2) - (x2 * y0)); - const auto detY = static_cast((x1 * y0) - (x0 * y1)); - return {detX / det, detY / det}; -} - -/** - * @brief Increments the indices in lexicographic order. - * @details This function increments the first index that is less than its - * limit, resets all previous indices to their minimum values. - * @param indices The vector of indices to increment. - * @param minima The minimum values for each index (used when resetting). - * @param limits The limits for each index. - * @returns true if the increment was successful, false if all indices have - * reached their limits. - */ -[[nodiscard]] auto increment(std::vector& indices, - const std::vector& minima, - const std::vector& limits) -> bool { - size_t i = 0; - for (; i < indices.size() && indices[i] == limits[i]; ++i) { - } - if (i == indices.size()) { - // all indices are at their limits - return false; - } - for (size_t j = 0; j < i; ++j) { - indices[j] = minima[j]; // Reset all previous indices to their minima - } - ++indices[i]; // Increment the next index - return true; -} -} // namespace - -auto writeJSONSchema(std::ostream& os) -> void { - // Create a default device configuration - Device device; - - // Fill each array field with default values - populateArrayFields(device); - - // Convert the device configuration to a JSON object - // NOLINTNEXTLINE(misc-include-cleaner) - const nlohmann::json json = device; - - // Write to output stream - os << json; -} - -auto writeJSONSchema(const std::string& path) -> void { - // Write to file - std::ofstream ofs(path); - if (!ofs.good()) { - std::stringstream ss; - ss << "Failed to open file for writing: " << path; - throw std::runtime_error(ss.str()); - } - writeJSONSchema(ofs); - ofs.close(); - SPDLOG_INFO("JSON template written to {}", path); -} - -[[nodiscard]] auto readJSON(std::istream& is) -> Device { - // Read the device configuration from the input stream - nlohmann::json json; - try { - is >> json; - // NOLINTNEXTLINE(misc-include-cleaner) - } catch (const nlohmann::detail::parse_error& e) { - std::stringstream ss; - ss << "Failed to parse JSON string: " << e.what(); - throw std::runtime_error(ss.str()); - } - return json; -} - -[[nodiscard]] auto readJSON(const std::string& path) -> Device { - // Read the device configuration from a JSON file - std::ifstream ifs(path); - if (!ifs.good()) { - throw std::runtime_error("Failed to open JSON file: " + std::string(path)); - } - const auto& device = readJSON(ifs); - ifs.close(); - return device; -} - -auto writeHeader(const Device& device, std::ostream& os) -> void { - os << "#pragma once\n\n"; - writeName(device, os); - writeQubitsNum(device, os); - writeLengthUnit(device, os); - writeDurationUnit(device, os); - writeMinAtomDistance(device, os); - writeSites(device, os); - writeOperations(device, os); - writeDecoherenceTimes(device, os); -} - -auto writeHeader(const Device& device, const std::string& path) -> void { - std::ofstream ofs(path); - if (!ofs.good()) { - std::stringstream ss; - ss << "Failed to open header file for writing: " << path; - throw std::runtime_error(ss.str()); - } - writeHeader(device, ofs); - ofs.close(); - SPDLOG_INFO("Header file written to {}", path); -} -auto forEachRegularSites(const std::vector& lattices, - const std::function& f, - const size_t startId) -> void { - size_t count = startId; - size_t moduleCount = 0; - for (const auto& [latticeOrigin, latticeVector1, latticeVector2, - sublatticeOffsets, extent] : lattices) { - size_t subModuleCount = 0; - const auto& [origin, size] = extent; - const auto extentWidth = static_cast(size.width); - const auto extentHeight = static_cast(size.height); - - // approximate indices of the bottom left corner - const auto& [bottomLeftI, bottomLeftJ] = solve2DLinearEquation( - latticeVector1.x, latticeVector2.x, latticeVector1.y, latticeVector2.y, - origin.x - latticeOrigin.x, origin.y - latticeOrigin.y); - - // approximate indices of the bottom right corner - const auto& [bottomRightI, bottomRightJ] = solve2DLinearEquation( - latticeVector1.x, latticeVector2.x, latticeVector1.y, latticeVector2.y, - origin.x + extentWidth - latticeOrigin.x, origin.y - latticeOrigin.y); - - // approximate indices of the top left corner - const auto& [topLeftI, topLeftJ] = solve2DLinearEquation( - latticeVector1.x, latticeVector2.x, latticeVector1.y, latticeVector2.y, - origin.x - latticeOrigin.x, origin.y + extentHeight - latticeOrigin.y); - - // approximate indices of the top right corner - const auto& [topRightI, topRightJ] = solve2DLinearEquation( - latticeVector1.x, latticeVector2.x, latticeVector1.y, latticeVector2.y, - origin.x + extentWidth - latticeOrigin.x, - origin.y + extentHeight - latticeOrigin.y); - - const auto minI = static_cast( - std::floor(std::min({bottomLeftI, bottomRightI, topLeftI, topRightI}))); - const auto minJ = static_cast( - std::floor(std::min({bottomLeftJ, bottomRightJ, topLeftJ, topRightJ}))); - const auto maxI = static_cast( - std::floor(std::max({bottomLeftI, bottomRightI, topLeftI, topRightI}))); - const auto maxJ = static_cast( - std::floor(std::max({bottomLeftJ, bottomRightJ, topLeftJ, topRightJ}))); - - const std::vector minima{minI, minJ}; - const std::vector limits{maxI, maxJ}; - std::vector indices{minI, minJ}; - for (bool loop = true; loop; - loop = increment(indices, minima, limits), ++subModuleCount) { - // For every sublattice offset, add a site for repetition indices - for (const auto& [xOffset, yOffset] : sublatticeOffsets) { - const auto id = count++; - auto x = latticeOrigin.x + xOffset; - auto y = latticeOrigin.y + yOffset; - x += indices[0] * latticeVector1.x; - y += indices[0] * latticeVector1.y; - x += indices[1] * latticeVector2.x; - y += indices[1] * latticeVector2.y; - if (origin.x <= x && x <= origin.x + extentWidth && origin.y <= y && - y <= origin.y + extentHeight) { - // Only add the site if it is within the extent of the lattice - f(SiteInfo{.id = id, - .x = x, - .y = y, - .moduleId = moduleCount, - .subModuleId = subModuleCount}); - } - } - } - ++moduleCount; - } -} -} // namespace na diff --git a/test/na/fomac/CMakeLists.txt b/test/na/fomac/CMakeLists.txt index 05228857df..02de3c0959 100644 --- a/test/na/fomac/CMakeLists.txt +++ b/test/na/fomac/CMakeLists.txt @@ -12,7 +12,8 @@ if(TARGET MQT::CoreNAFoMaC) # Add the test executable package_add_test(${TARGET_NAME} MQT::CoreNAFoMaC test_fomac.cpp) # Set the device json path - target_compile_definitions(${TARGET_NAME} - PRIVATE NA_DEVICE_JSON="${PROJECT_SOURCE_DIR}/json/na/device.json") + target_compile_definitions( + ${TARGET_NAME} + PRIVATE NA_DEVICE_JSON="${PROJECT_SOURCE_DIR}/json/na/mqt-core-qdmi-na-device.json") mqt_copy_qdmi_runtime(${TARGET_NAME}) endif() diff --git a/test/python/na/test_na_qdmi.py b/test/python/na/test_na_qdmi.py index 65e8a60446..0c351b6e41 100644 --- a/test/python/na/test_na_qdmi.py +++ b/test/python/na/test_na_qdmi.py @@ -27,7 +27,7 @@ @pytest.fixture 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: + with pathlib.Path("json/na/mqt-core-qdmi-na-device.json").open(encoding="utf-8") as f: device_dict = load(f) return next(iter(devices())), device_dict diff --git a/test/qdmi/devices/na/CMakeLists.txt b/test/qdmi/devices/na/CMakeLists.txt index 69a3ee53e7..4e743e3e7a 100644 --- a/test/qdmi/devices/na/CMakeLists.txt +++ b/test/qdmi/devices/na/CMakeLists.txt @@ -6,35 +6,31 @@ # # Licensed under the MIT License -if(TARGET MQT::CoreQDMINaDeviceGen - AND TARGET MQT::CoreQDMINaDeviceGenerator - AND TARGET MQT::CoreQDMINaDevice) +if(TARGET MQT::CoreQDMINaDeviceConfig AND TARGET MQT::CoreQDMINaDevice) # Set QDMI prefix again for the tests set(QDMI_PREFIX "MQT_NA") generate_device_defs_executable(${QDMI_PREFIX} TARGET MQT::CoreQDMINaDevice) # Set test target name set(TARGET_NAME ${MQT_CORE_TARGET_NAME}-qdmi-na-device-test) + if(WIN32) + # Discover tests after the provider and its runtime assets have been copied. + set(CMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE PRE_TEST) + endif() # Add the test executable - package_add_test(${TARGET_NAME} MQT::CoreQDMINaDevice test_generator.cpp test_app.cpp - test_device.cpp) + package_add_test(${TARGET_NAME} MQT::CoreQDMINaDevice test_configuration.cpp test_device.cpp) # Add dependency to enforce implementation of all interface functions add_dependencies(${TARGET_NAME} qdmi_test_mqt_na_device_defs) # Link further libraries - target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQDMINaDeviceGen + target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQDMINaDeviceConfig nlohmann_json::nlohmann_json) # Set the executable path and version information target_compile_definitions( ${TARGET_NAME} - PRIVATE EXECUTABLE_PATH="$" - MQT_CORE_VERSION="${MQT_CORE_VERSION}" - NA_DEVICE_JSON="${PROJECT_SOURCE_DIR}/json/na/device.json") + PRIVATE MQT_CORE_VERSION="${MQT_CORE_VERSION}" + NA_DEVICE_JSON="${PROJECT_SOURCE_DIR}/json/na/mqt-core-qdmi-na-device.json") - # On Windows, we need to copy the DLL to the test executable directory + # On Windows, copy the provider and its runtime assets beside the test executable. if(WIN32) - add_custom_command( - TARGET ${TARGET_NAME} - PRE_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different $ - $) + mqt_copy_qdmi_runtime(${TARGET_NAME} MQT::CoreQDMINaDevice) endif() endif() diff --git a/test/qdmi/devices/na/test_app.cpp b/test/qdmi/devices/na/test_app.cpp deleted file mode 100644 index 58efc12eb2..0000000000 --- a/test/qdmi/devices/na/test_app.cpp +++ /dev/null @@ -1,372 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include - -#include -#include -#include -#include -#include - -#ifdef _WIN32 -#define PLATFORM_POPEN _popen -#define PLATFORM_PCLOSE _pclose -#else -// The following are included via but the direct include are platform- -// specific, so ignore the corresponding warning for platform-agnostic includes. -// NOLINTBEGIN(misc-include-cleaner) -#define PLATFORM_POPEN popen -#define PLATFORM_PCLOSE pclose -// NOLINTEND(misc-include-cleaner) -#endif // _WIN32 - -TEST(NaExecutableTest, Version) { - // Command to execute - // NOLINTNEXTLINE(misc-include-cleaner) - const std::string command = EXECUTABLE_PATH " --version"; - // Open a pipe to capture the output - FILE* pipe = PLATFORM_POPEN(command.c_str(), "r"); - ASSERT_NE(pipe, nullptr) << "Failed to open pipe"; - // Read the output - std::array buffer{}; - buffer.fill('\0'); - std::stringstream output; - while (fgets(buffer.data(), static_cast(buffer.size()), pipe) != - nullptr) { - output << buffer.data(); - } - // Close the pipe - const int returnCode = PLATFORM_PCLOSE(pipe); - // Print the captured output - std::cout << "Captured Output:\n" << output.str() << "\n"; - ASSERT_EQ(returnCode, 0) << "Executable failed with return code: " - << returnCode; - // Validate the output - EXPECT_EQ(output.str(), - // NOLINTNEXTLINE(misc-include-cleaner) - "MQT QDMI NA Device Generator (MQT Version " MQT_CORE_VERSION - ")\n"); -} - -TEST(NaExecutableTest, MissingSubcommand) { - // Command to execute - // NOLINTNEXTLINE(misc-include-cleaner) - const std::string command = EXECUTABLE_PATH; - // Open a pipe to capture the output - FILE* pipe = PLATFORM_POPEN(command.c_str(), "r"); - ASSERT_NE(pipe, nullptr) << "Failed to open pipe"; - // Read the output - std::array buffer{}; - buffer.fill('\0'); - std::stringstream output; - while (fgets(buffer.data(), static_cast(buffer.size()), pipe) != - nullptr) { - output << buffer.data(); - } - // Close the pipe - const int returnCode = PLATFORM_PCLOSE(pipe); - // Print the captured output - std::cout << "Captured Output:\n" << output.str() << "\n"; - EXPECT_NE(returnCode, 0); -} - -TEST(NaExecutableTest, UnknownSubcommand) { - // Command to execute - // NOLINTNEXTLINE(misc-include-cleaner) - const std::string command = EXECUTABLE_PATH " unknown"; - // Open a pipe to capture the output - FILE* pipe = PLATFORM_POPEN(command.c_str(), "r"); - ASSERT_NE(pipe, nullptr) << "Failed to open pipe"; - // Read the output - std::array buffer{}; - buffer.fill('\0'); - std::stringstream output; - while (fgets(buffer.data(), static_cast(buffer.size()), pipe) != - nullptr) { - output << buffer.data(); - } - // Close the pipe - const int returnCode = PLATFORM_PCLOSE(pipe); - // Print the captured output - std::cout << "Captured Output:\n" << output.str() << "\n"; - EXPECT_NE(returnCode, 0); -} - -TEST(NaExecutableTest, SchemaUnknownOption) { - // Command to execute - // NOLINTNEXTLINE(misc-include-cleaner) - const std::string command = EXECUTABLE_PATH " schema --unknown-option"; - // Open a pipe to capture the output - FILE* pipe = PLATFORM_POPEN(command.c_str(), "r"); - ASSERT_NE(pipe, nullptr) << "Failed to open pipe"; - // Read the output - std::array buffer{}; - buffer.fill('\0'); - std::stringstream output; - while (fgets(buffer.data(), static_cast(buffer.size()), pipe) != - nullptr) { - output << buffer.data(); - } - // Close the pipe - const int returnCode = PLATFORM_PCLOSE(pipe); - // Print the captured output - std::cout << "Captured Output:\n" << output.str() << "\n"; - EXPECT_NE(returnCode, 0); -} - -TEST(NaExecutableTest, SchemaMissingFile) { - // Command to execute - // NOLINTNEXTLINE(misc-include-cleaner) - const std::string command = EXECUTABLE_PATH " schema --output"; - // Open a pipe to capture the output - FILE* pipe = PLATFORM_POPEN(command.c_str(), "r"); - ASSERT_NE(pipe, nullptr) << "Failed to open pipe"; - // Read the output - std::array buffer{}; - buffer.fill('\0'); - std::stringstream output; - while (fgets(buffer.data(), static_cast(buffer.size()), pipe) != - nullptr) { - output << buffer.data(); - } - // Close the pipe - const int returnCode = PLATFORM_PCLOSE(pipe); - // Print the captured output - std::cout << "Captured Output:\n" << output.str() << "\n"; - EXPECT_NE(returnCode, 0); -} - -TEST(NaExecutableTest, ValidateInvalidJson) { - // Command to execute - // NOLINTNEXTLINE(misc-include-cleaner) - const std::string command = EXECUTABLE_PATH " validate"; - // Open a pipe to the executable - FILE* pipe = PLATFORM_POPEN(command.c_str(), "w"); - ASSERT_NE(pipe, nullptr) << "Failed to open pipe"; - // Write the schema to the executable's stdin - fwrite("{", sizeof(char), 2, pipe); - // Close the pipe - const int returnCode = PLATFORM_PCLOSE(pipe); - EXPECT_NE(returnCode, 0) << "Executable failed with return code: " - << returnCode; -} - -TEST(NaExecutableTest, GenerateMissingFile) { - // Command to execute - // NOLINTNEXTLINE(misc-include-cleaner) - const std::string command = EXECUTABLE_PATH " generate --output"; - // Open a pipe to capture the output - FILE* pipe = PLATFORM_POPEN(command.c_str(), "r"); - ASSERT_NE(pipe, nullptr) << "Failed to open pipe"; - // Read the output - std::array buffer{}; - buffer.fill('\0'); - std::stringstream output; - while (fgets(buffer.data(), static_cast(buffer.size()), pipe) != - nullptr) { - output << buffer.data(); - } - // Close the pipe - const int returnCode = PLATFORM_PCLOSE(pipe); - // Print the captured output - std::cout << "Captured Output:\n" << output.str() << "\n"; - EXPECT_NE(returnCode, 0); -} - -TEST(NaExecutableTest, Usage) { - // Command to execute - // NOLINTNEXTLINE(misc-include-cleaner) - const std::string command = EXECUTABLE_PATH " --help"; - // Open a pipe to capture the output - FILE* pipe = PLATFORM_POPEN(command.c_str(), "r"); - ASSERT_NE(pipe, nullptr) << "Failed to open pipe"; - // Read the output - std::array buffer{}; - buffer.fill('\0'); - std::stringstream output; - while (fgets(buffer.data(), static_cast(buffer.size()), pipe) != - nullptr) { - output << buffer.data(); - } - // Close the pipe - const int returnCode = PLATFORM_PCLOSE(pipe); - // Print the captured output - std::cout << "Captured Output:\n" << output.str() << "\n"; - ASSERT_EQ(returnCode, 0) << "Executable failed with return code: " - << returnCode; - EXPECT_FALSE(output.str().empty()); -} - -TEST(NaExecutableTest, SchemaUsage) { - // Command to execute - // NOLINTNEXTLINE(misc-include-cleaner) - const std::string command = EXECUTABLE_PATH " schema --help"; - // Open a pipe to capture the output - FILE* pipe = PLATFORM_POPEN(command.c_str(), "r"); - ASSERT_NE(pipe, nullptr) << "Failed to open pipe"; - // Read the output - std::array buffer{}; - buffer.fill('\0'); - std::stringstream output; - while (fgets(buffer.data(), static_cast(buffer.size()), pipe) != - nullptr) { - output << buffer.data(); - } - // Close the pipe - const int returnCode = PLATFORM_PCLOSE(pipe); - // Print the captured output - std::cout << "Captured Output:\n" << output.str() << "\n"; - ASSERT_EQ(returnCode, 0) << "Executable failed with return code: " - << returnCode; - EXPECT_TRUE(output.str().rfind("Generates a JSON schema", 0) == 0); -} - -TEST(NaExecutableTest, ValidateUsage) { - // Command to execute - // NOLINTNEXTLINE(misc-include-cleaner) - const std::string command = EXECUTABLE_PATH " validate --help"; - // Open a pipe to capture the output - FILE* pipe = PLATFORM_POPEN(command.c_str(), "r"); - ASSERT_NE(pipe, nullptr) << "Failed to open pipe"; - // Read the output - std::array buffer{}; - buffer.fill('\0'); - std::stringstream output; - while (fgets(buffer.data(), static_cast(buffer.size()), pipe) != - nullptr) { - output << buffer.data(); - } - // Close the pipe - const int returnCode = PLATFORM_PCLOSE(pipe); - // Print the captured output - std::cout << "Captured Output:\n" << output.str() << "\n"; - ASSERT_EQ(returnCode, 0) << "Executable failed with return code: " - << returnCode; - EXPECT_TRUE(output.str().rfind("Validates", 0) == 0); -} - -TEST(NaExecutableTest, GenerateUsage) { - // Command to execute - // NOLINTNEXTLINE(misc-include-cleaner) - const std::string command = EXECUTABLE_PATH " generate --help"; - // Open a pipe to capture the output - FILE* pipe = PLATFORM_POPEN(command.c_str(), "r"); - ASSERT_NE(pipe, nullptr) << "Failed to open pipe"; - // Read the output - std::array buffer{}; - buffer.fill('\0'); - std::stringstream output; - while (fgets(buffer.data(), static_cast(buffer.size()), pipe) != - nullptr) { - output << buffer.data(); - } - // Close the pipe - const int returnCode = PLATFORM_PCLOSE(pipe); - // Print the captured output - std::cout << "Captured Output:\n" << output.str() << "\n"; - ASSERT_EQ(returnCode, 0) << "Executable failed with return code: " - << returnCode; - EXPECT_TRUE(output.str().rfind("Generates a header file", 0) == 0); -} - -TEST(NaExecutableTest, RoundTrip) { - std::string schema; - // Capture the output of the schema command - { - // Command to execute - // NOLINTNEXTLINE(misc-include-cleaner) - const std::string command = EXECUTABLE_PATH " schema"; - // Open a pipe to capture the output - FILE* pipe = PLATFORM_POPEN(command.c_str(), "r"); - ASSERT_NE(pipe, nullptr) << "Failed to open pipe"; - // Read the output - std::array buffer{}; - buffer.fill('\0'); - std::stringstream output; - while (fgets(buffer.data(), static_cast(buffer.size()), pipe) != - nullptr) { - output << buffer.data(); - } - // Close the pipe - const int returnCode = PLATFORM_PCLOSE(pipe); - schema = output.str(); - // Print the captured output - std::cout << "Captured Output:\n" << schema << "\n"; - ASSERT_EQ(returnCode, 0) - << "Executable failed with return code: " << returnCode; - } - // Validate the output - { - // Command to execute - // NOLINTNEXTLINE(misc-include-cleaner) - const std::string command = EXECUTABLE_PATH " validate"; - // Open a pipe to the executable with write mode - FILE* pipe = PLATFORM_POPEN(command.c_str(), "w"); - ASSERT_NE(pipe, nullptr) << "Failed to open pipe"; - // Write the schema to the executable's stdin - fwrite(schema.c_str(), sizeof(char), schema.size(), pipe); - // Close the pipe - const int returnCode = PLATFORM_PCLOSE(pipe); - ASSERT_EQ(returnCode, 0) - << "Executable failed with return code: " << returnCode; - } -} - -TEST(NaExecutableTest, RoundTripFile) { - // Write schema to a file - { - // Command to execute - // NOLINTNEXTLINE(misc-include-cleaner) - const std::string command = EXECUTABLE_PATH " schema --output schema.json"; - // Open a pipe to capture the output - FILE* pipe = PLATFORM_POPEN(command.c_str(), "r"); - ASSERT_NE(pipe, nullptr) << "Failed to open pipe"; - // Read the output - std::array buffer{}; - buffer.fill('\0'); - std::stringstream output; - while (fgets(buffer.data(), static_cast(buffer.size()), pipe) != - nullptr) { - output << buffer.data(); - } - // Close the pipe - const int returnCode = PLATFORM_PCLOSE(pipe); - // Print the captured output - std::cout << "Captured Output:\n" << output.str() << "\n"; - ASSERT_EQ(returnCode, 0) - << "Executable failed with return code: " << returnCode; - } - // Validate the output - { - // Command to execute - // NOLINTNEXTLINE(misc-include-cleaner) - const std::string command = EXECUTABLE_PATH " validate schema.json"; - // Open a pipe to capture the output - FILE* pipe = PLATFORM_POPEN(command.c_str(), "r"); - ASSERT_NE(pipe, nullptr) << "Failed to open pipe"; - // Read the output - std::array buffer{}; - buffer.fill('\0'); - std::stringstream output; - while (fgets(buffer.data(), static_cast(buffer.size()), pipe) != - nullptr) { - output << buffer.data(); - } - // Close the pipe - const int returnCode = PLATFORM_PCLOSE(pipe); - // Print the captured output - std::cout << "Captured Output:\n" << output.str() << "\n"; - EXPECT_EQ(returnCode, 0) - << "Executable failed with return code: " << returnCode; - } - // Remove created file - std::remove("schema.json"); -} diff --git a/test/qdmi/devices/na/test_configuration.cpp b/test/qdmi/devices/na/test_configuration.cpp new file mode 100644 index 0000000000..bc6b28b939 --- /dev/null +++ b/test/qdmi/devices/na/test_configuration.cpp @@ -0,0 +1,181 @@ +/* + * 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/devices/na/Configuration.hpp" + +#include +#include // NOLINT(misc-include-cleaner) +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace na { +TEST(NaConfigurationTest, ParsesBundledDeviceStrictly) { + const auto device = readJSON(NA_DEVICE_JSON); + EXPECT_EQ(device.schemaVersion, 1); + EXPECT_EQ(device.name, "MQT NA Default QDMI Device"); + EXPECT_EQ(device.numQubits, 100); +} + +TEST(NaConfigurationTest, RejectsUnknownMissingAndInvalidValues) { + std::ifstream input(NA_DEVICE_JSON); + ASSERT_TRUE(input); + nlohmann::json json; + input >> json; + + auto unknown = json; + unknown["unknown"] = true; + EXPECT_THROW(static_cast(readJSON(unknown.dump(), "inline")), + std::invalid_argument); + + auto missing = json; + missing.erase("traps"); + EXPECT_THROW(static_cast(readJSON(missing.dump(), "inline")), + std::invalid_argument); + + auto invalidFidelity = json; + invalidFidelity["localSingleQubitOperations"][0]["fidelity"] = 2.; + EXPECT_THROW(static_cast(readJSON(invalidFidelity.dump(), "inline")), + std::invalid_argument); + + auto invalidUnit = json; + invalidUnit["durationUnit"]["unit"] = "ts"; + EXPECT_THROW(static_cast(readJSON(invalidUnit.dump(), "inline")), + std::invalid_argument); + + auto negativeExtent = json; + negativeExtent["traps"][0]["extent"]["size"]["width"] = -1; + EXPECT_THROW(static_cast(readJSON(negativeExtent.dump(), "inline")), + std::invalid_argument); + + auto unknownNested = json; + unknownNested["traps"][0]["extent"]["unexpected"] = true; + EXPECT_THROW(static_cast(readJSON(unknownNested.dump(), "inline")), + std::invalid_argument); + + auto missingNested = json; + missingNested["localSingleQubitOperations"][0].erase("duration"); + EXPECT_THROW(static_cast(readJSON(missingNested.dump(), "inline")), + std::invalid_argument); + + auto unsupportedSchema = json; + unsupportedSchema["schema-version"] = 2; + EXPECT_THROW(static_cast(readJSON(unsupportedSchema.dump(), "inline")), + std::invalid_argument); + + auto incorrectType = json; + incorrectType["numQubits"] = "many"; + EXPECT_THROW(static_cast(readJSON(incorrectType.dump(), "inline")), + std::invalid_argument); + + auto outOfRangeCoordinate = json; + outOfRangeCoordinate["traps"][0]["latticeOrigin"]["x"] = + static_cast(std::numeric_limits::max()) + 1; + EXPECT_THROW( + static_cast(readJSON(outOfRangeCoordinate.dump(), "inline")), + std::invalid_argument); + + auto degenerateLattice = json; + degenerateLattice["traps"][0]["latticeVector2"] = + degenerateLattice["traps"][0]["latticeVector1"]; + EXPECT_THROW(static_cast(readJSON(degenerateLattice.dump(), "inline")), + std::invalid_argument); + + auto duplicateCoordinates = json; + duplicateCoordinates["traps"].push_back(duplicateCoordinates["traps"][0]); + EXPECT_THROW( + static_cast(readJSON(duplicateCoordinates.dump(), "inline")), + std::invalid_argument); + + auto overflowingExtent = json; + overflowingExtent["traps"][0]["extent"]["size"]["width"] = + std::numeric_limits::max(); + EXPECT_THROW(static_cast(readJSON(overflowingExtent.dump(), "inline")), + std::invalid_argument); + + auto excessiveSiteCount = json; + excessiveSiteCount["traps"][0]["extent"]["size"]["width"] = + std::numeric_limits::max(); + EXPECT_THROW(static_cast(readJSON(excessiveSiteCount.dump(), "inline")), + std::invalid_argument); + + auto excessivePairWork = json; + excessivePairWork["traps"][0]["extent"]["size"]["width"] = 2499; + excessivePairWork["traps"][0]["extent"]["size"]["height"] = 1; + excessivePairWork["localMultiQubitOperations"][0]["region"] = + excessivePairWork["traps"][0]["extent"]; + EXPECT_THROW(static_cast(readJSON(excessivePairWork.dump(), "inline")), + std::invalid_argument); + + auto emptyOffsets = json; + emptyOffsets["traps"][0]["sublatticeOffsets"] = nlohmann::json::array(); + EXPECT_THROW(static_cast(readJSON(emptyOffsets.dump(), "inline")), + std::invalid_argument); +} + +TEST(NaConfigurationTest, PreservesCoordinatesNearSignedMaximum) { + std::ifstream input(NA_DEVICE_JSON); + ASSERT_TRUE(input); + nlohmann::json json; + input >> json; + + constexpr auto maximum = std::numeric_limits::max(); + constexpr auto origin = maximum - 2; + auto& trap = json["traps"][0]; + trap["latticeOrigin"] = {{"x", origin}, {"y", origin}}; + trap["latticeVector1"] = {{"x", 1}, {"y", 0}}; + trap["latticeVector2"] = {{"x", 0}, {"y", 1}}; + trap["sublatticeOffsets"] = {{{"x", 0}, {"y", 0}}}; + trap["extent"] = {{"origin", {{"x", origin}, {"y", origin}}}, + {"size", {{"width", 2}, {"height", 2}}}}; + json["numQubits"] = 9; + + const auto device = readJSON(json.dump(), "inline"); + std::vector> coordinates; + forEachRegularSites(device.traps, [&](const SiteInfo& site) { + coordinates.emplace_back(site.x, site.y); + }); + + EXPECT_EQ(coordinates.size(), 9); + EXPECT_NE(std::ranges::find(coordinates, std::pair{maximum, maximum}), + coordinates.end()); +} + +TEST(NaConfigurationTest, AcceptsLargeLatticeVectors) { + std::ifstream input(NA_DEVICE_JSON); + ASSERT_TRUE(input); + nlohmann::json json; + input >> json; + + auto& trap = json["traps"][0]; + trap["latticeOrigin"] = {{"x", 0}, {"y", 0}}; + trap["latticeVector1"] = {{"x", 4'000'000'000}, {"y", 0}}; + trap["latticeVector2"] = {{"x", 0}, {"y", 4'000'000'000}}; + trap["sublatticeOffsets"] = {{{"x", 0}, {"y", 0}}}; + trap["extent"] = {{"origin", {{"x", 0}, {"y", 0}}}, + {"size", {{"width", 1}, {"height", 1}}}}; + json["numQubits"] = 1; + + const auto device = readJSON(json.dump(), "inline"); + std::vector> coordinates; + forEachRegularSites(device.traps, [&](const SiteInfo& site) { + coordinates.emplace_back(site.x, site.y); + }); + + EXPECT_EQ(coordinates, std::vector({std::pair{0, 0}})); +} + +} // namespace na diff --git a/test/qdmi/devices/na/test_device.cpp b/test/qdmi/devices/na/test_device.cpp index 264df04de4..9f054660a7 100644 --- a/test/qdmi/devices/na/test_device.cpp +++ b/test/qdmi/devices/na/test_device.cpp @@ -9,17 +9,23 @@ */ #include "mqt_na_qdmi/device.h" -#include "qdmi/devices/na/Generator.hpp" +#include "qdmi/devices/na/Configuration.hpp" #include #include #include // NOLINT(misc-include-cleaner) +#include #include +#include #include #include +#include +#include #include #include +#include +#include #include #include #include @@ -29,9 +35,9 @@ 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::string stringConcat5(const std::string& a, const std::string& b, + const std::string& c, const std::string& d, + const std::string& e) { std::stringstream ss; ss << a << b << c << d << e; return ss.str(); @@ -51,13 +57,81 @@ namespace { /// Hash function for a pair struct PairHash { template - auto operator()(const std::pair& p) const noexcept -> std::size_t { + std::size_t operator()(const std::pair& p) const noexcept { // Use the hash of the first and second element of the pair return std::hash{}(p.first) ^ std::hash{}(p.second); } }; -[[nodiscard]] auto querySites(MQT_NA_QDMI_Device_Session session) - -> std::vector { + +class ScopedEnvironmentVariable { +public: + ScopedEnvironmentVariable(std::string name, const std::string& value) + : name_(std::move(name)) { + if (const auto* previous = std::getenv(name_.c_str()); + previous != nullptr) { + previous_ = previous; + } + set(value); + } + + ~ScopedEnvironmentVariable() { + if (previous_) { + static_cast(setWithoutChecking(*previous_)); + } else { +#ifdef _WIN32 + static_cast(_putenv_s(name_.c_str(), "")); +#else + // NOLINTNEXTLINE(misc-include-cleaner) + static_cast(unsetenv(name_.c_str())); +#endif + } + } + + ScopedEnvironmentVariable(const ScopedEnvironmentVariable&) = delete; + ScopedEnvironmentVariable& + operator=(const ScopedEnvironmentVariable&) = delete; + ScopedEnvironmentVariable(ScopedEnvironmentVariable&&) = delete; + ScopedEnvironmentVariable& operator=(ScopedEnvironmentVariable&&) = delete; + +private: + void set(const std::string& value) const { + if (!setWithoutChecking(value)) { + throw std::runtime_error("Failed to set environment variable " + name_); + } + } + + [[nodiscard]] bool setWithoutChecking(const std::string& value) const { +#ifdef _WIN32 + return _putenv_s(name_.c_str(), value.c_str()) == 0; +#else + // NOLINTNEXTLINE(misc-include-cleaner) + return setenv(name_.c_str(), value.c_str(), 1) == 0; +#endif + } + + std::string name_; + std::optional previous_; +}; + +[[nodiscard]] std::string queryName(MQT_NA_QDMI_Device_Session session) { + size_t size = 0; + if (MQT_NA_QDMI_device_session_query_device_property( + session, QDMI_DEVICE_PROPERTY_NAME, 0, nullptr, &size) != + QDMI_SUCCESS) { + throw std::runtime_error("Failed to query device name size"); + } + std::string name(size, '\0'); + if (MQT_NA_QDMI_device_session_query_device_property( + session, QDMI_DEVICE_PROPERTY_NAME, size, name.data(), nullptr) != + QDMI_SUCCESS) { + throw std::runtime_error("Failed to query device name"); + } + name.pop_back(); + return name; +} + +[[nodiscard]] std::vector +querySites(MQT_NA_QDMI_Device_Session session) { size_t size = 0; if (MQT_NA_QDMI_device_session_query_device_property( session, QDMI_DEVICE_PROPERTY_SITES, 0, nullptr, &size) != @@ -75,8 +149,8 @@ struct PairHash { } return sites; } -[[nodiscard]] auto queryOperations(MQT_NA_QDMI_Device_Session session) - -> std::vector { +[[nodiscard]] std::vector +queryOperations(MQT_NA_QDMI_Device_Session session) { size_t size = 0; if (MQT_NA_QDMI_device_session_query_device_property( session, QDMI_DEVICE_PROPERTY_OPERATIONS, 0, nullptr, &size) != @@ -145,6 +219,206 @@ class NaQDMIJobSpecificationTest : public NaQDMISpecificationTest { } // namespace +TEST(NaRuntimeConfiguration, + SessionsOwnIndependentModelsAndRejectForeignHandles) { + std::ifstream input(NA_DEVICE_JSON); + ASSERT_TRUE(input); + nlohmann::json customJson; + input >> customJson; + customJson["name"] = "Custom NA"; + customJson["numQubits"] = 5; + const auto customConfiguration = customJson.dump(); + + MQT_NA_QDMI_Device_Session custom = nullptr; + MQT_NA_QDMI_Device_Session bundled = nullptr; + ASSERT_EQ(MQT_NA_QDMI_device_session_alloc(&custom), QDMI_SUCCESS); + ASSERT_EQ(MQT_NA_QDMI_device_session_alloc(&bundled), QDMI_SUCCESS); + ASSERT_EQ(MQT_NA_QDMI_device_session_set_parameter( + custom, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM1, + customConfiguration.size() + 1, customConfiguration.c_str()), + QDMI_SUCCESS); + ASSERT_EQ(MQT_NA_QDMI_device_session_init(custom), QDMI_SUCCESS); + ASSERT_EQ(MQT_NA_QDMI_device_session_init(bundled), QDMI_SUCCESS); + + size_t customQubits = 0; + size_t bundledQubits = 0; + ASSERT_EQ(MQT_NA_QDMI_device_session_query_device_property( + custom, QDMI_DEVICE_PROPERTY_QUBITSNUM, sizeof(customQubits), + &customQubits, nullptr), + QDMI_SUCCESS); + ASSERT_EQ(MQT_NA_QDMI_device_session_query_device_property( + bundled, QDMI_DEVICE_PROPERTY_QUBITSNUM, sizeof(bundledQubits), + &bundledQubits, nullptr), + QDMI_SUCCESS); + EXPECT_EQ(customQubits, 5); + EXPECT_EQ(bundledQubits, 100); + + auto* const customSite = querySites(custom).front(); + auto* const customOperation = queryOperations(custom).front(); + EXPECT_EQ( + MQT_NA_QDMI_device_session_query_site_property( + bundled, customSite, QDMI_SITE_PROPERTY_INDEX, 0, nullptr, nullptr), + QDMI_ERROR_INVALIDARGUMENT); + EXPECT_EQ(MQT_NA_QDMI_device_session_query_operation_property( + bundled, customOperation, 0, nullptr, 0, nullptr, + QDMI_OPERATION_PROPERTY_NAME, 0, nullptr, nullptr), + QDMI_ERROR_INVALIDARGUMENT); + + MQT_NA_QDMI_device_session_free(bundled); + MQT_NA_QDMI_device_session_free(custom); +} + +TEST(NaRuntimeConfiguration, ValidatesRawParameterStringsAndRetry) { + MQT_NA_QDMI_Device_Session session = nullptr; + ASSERT_EQ(MQT_NA_QDMI_device_session_alloc(&session), QDMI_SUCCESS); + EXPECT_EQ(MQT_NA_QDMI_device_session_set_parameter( + session, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM2, 0, nullptr), + QDMI_SUCCESS); + EXPECT_EQ(MQT_NA_QDMI_device_session_set_parameter( + session, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM1, 0, nullptr), + QDMI_SUCCESS); + constexpr std::array embeddedNul{'x', '\0', 'y', '\0'}; + EXPECT_EQ(MQT_NA_QDMI_device_session_set_parameter( + session, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM2, + embeddedNul.size(), embeddedNul.data()), + QDMI_ERROR_INVALIDARGUMENT); + constexpr std::array missingTerminator{'x'}; + EXPECT_EQ(MQT_NA_QDMI_device_session_set_parameter( + session, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM1, + missingTerminator.size(), missingTerminator.data()), + QDMI_ERROR_INVALIDARGUMENT); + constexpr auto missing = std::to_array("does-not-exist.json"); + ASSERT_EQ(MQT_NA_QDMI_device_session_set_parameter( + session, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM2, missing.size(), + missing.data()), + QDMI_SUCCESS); + EXPECT_EQ(MQT_NA_QDMI_device_session_init(session), QDMI_ERROR_NOTFOUND); + constexpr auto malformed = std::to_array("{"); + ASSERT_EQ(MQT_NA_QDMI_device_session_set_parameter( + session, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM1, + malformed.size(), malformed.data()), + QDMI_SUCCESS); + EXPECT_EQ(MQT_NA_QDMI_device_session_init(session), + QDMI_ERROR_INVALIDARGUMENT); + ASSERT_EQ(MQT_NA_QDMI_device_session_set_parameter( + session, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM1, 1, ""), + QDMI_SUCCESS); + ASSERT_EQ(MQT_NA_QDMI_device_session_set_parameter( + session, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM2, + std::strlen(NA_DEVICE_JSON) + 1, NA_DEVICE_JSON), + QDMI_SUCCESS); + EXPECT_EQ(MQT_NA_QDMI_device_session_init(session), QDMI_SUCCESS); + EXPECT_EQ(MQT_NA_QDMI_device_session_set_parameter( + session, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM2, 1, ""), + QDMI_ERROR_BADSTATE); + MQT_NA_QDMI_device_session_free(session); +} + +TEST(NaRuntimeConfiguration, SelectsEnvironmentAndExplicitSources) { + std::ifstream input(NA_DEVICE_JSON); + ASSERT_TRUE(input); + nlohmann::json environmentJson; + input >> environmentJson; + environmentJson["name"] = "Environment NA"; + const auto inlineEnvironment = environmentJson.dump(); + + const ScopedEnvironmentVariable environmentInline( + "MQT_CORE_QDMI_NA_CONFIG_JSON", inlineEnvironment); + const ScopedEnvironmentVariable environmentFile( + "MQT_CORE_QDMI_NA_CONFIG_FILE", ""); + MQT_NA_QDMI_Device_Session environmentSession = nullptr; + ASSERT_EQ(MQT_NA_QDMI_device_session_alloc(&environmentSession), + QDMI_SUCCESS); + ASSERT_EQ(MQT_NA_QDMI_device_session_init(environmentSession), QDMI_SUCCESS); + EXPECT_EQ(queryName(environmentSession), "Environment NA"); + MQT_NA_QDMI_device_session_free(environmentSession); + + MQT_NA_QDMI_Device_Session explicitSession = nullptr; + ASSERT_EQ(MQT_NA_QDMI_device_session_alloc(&explicitSession), QDMI_SUCCESS); + ASSERT_EQ(MQT_NA_QDMI_device_session_set_parameter( + explicitSession, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM2, + std::strlen(NA_DEVICE_JSON) + 1, NA_DEVICE_JSON), + QDMI_SUCCESS); + ASSERT_EQ(MQT_NA_QDMI_device_session_init(explicitSession), QDMI_SUCCESS); + EXPECT_EQ(queryName(explicitSession), "MQT NA Default QDMI Device"); + MQT_NA_QDMI_device_session_free(explicitSession); +} + +TEST(NaRuntimeConfiguration, InlineWinsOverExplicitFile) { + std::ifstream input(NA_DEVICE_JSON); + ASSERT_TRUE(input); + nlohmann::json inlineJson; + input >> inlineJson; + inlineJson["name"] = "Inline NA"; + const auto configuration = inlineJson.dump(); + + MQT_NA_QDMI_Device_Session session = nullptr; + ASSERT_EQ(MQT_NA_QDMI_device_session_alloc(&session), QDMI_SUCCESS); + ASSERT_EQ(MQT_NA_QDMI_device_session_set_parameter( + session, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM2, + std::strlen(NA_DEVICE_JSON) + 1, NA_DEVICE_JSON), + QDMI_SUCCESS); + ASSERT_EQ(MQT_NA_QDMI_device_session_set_parameter( + session, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM1, + configuration.size() + 1, configuration.c_str()), + QDMI_SUCCESS); + ASSERT_EQ(MQT_NA_QDMI_device_session_init(session), QDMI_SUCCESS); + EXPECT_EQ(queryName(session), "Inline NA"); + MQT_NA_QDMI_device_session_free(session); +} + +TEST(NaRuntimeConfiguration, RejectsConflictingEnvironmentSources) { + const ScopedEnvironmentVariable environmentInline( + "MQT_CORE_QDMI_NA_CONFIG_JSON", "{}"); + const ScopedEnvironmentVariable environmentFile( + "MQT_CORE_QDMI_NA_CONFIG_FILE", NA_DEVICE_JSON); + MQT_NA_QDMI_Device_Session session = nullptr; + ASSERT_EQ(MQT_NA_QDMI_device_session_alloc(&session), QDMI_SUCCESS); + EXPECT_EQ(MQT_NA_QDMI_device_session_init(session), + QDMI_ERROR_INVALIDARGUMENT); + MQT_NA_QDMI_device_session_free(session); +} + +TEST(NaRuntimeConfiguration, InitializesIndependentSessionsConcurrently) { + std::ifstream input(NA_DEVICE_JSON); + ASSERT_TRUE(input); + nlohmann::json base; + input >> base; + + std::vector> sessions; + for (size_t i = 0; i < 4; ++i) { + auto configuration = base; + const auto name = "Concurrent NA " + std::to_string(i); + configuration["name"] = name; + // Exceptions are intentionally transported to the caller by the future. + sessions.emplace_back(std::async( + std::launch::async, + // NOLINTNEXTLINE(bugprone-exception-escape) + [configuration = configuration.dump(), name] { + MQT_NA_QDMI_Device_Session session = nullptr; + if (MQT_NA_QDMI_device_session_alloc(&session) != QDMI_SUCCESS) { + return std::string{"allocation failed"}; + } + const auto configured = + MQT_NA_QDMI_device_session_set_parameter( + session, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM1, + configuration.size() + 1, + configuration.c_str()) == QDMI_SUCCESS; + const auto initialized = + configured && + MQT_NA_QDMI_device_session_init(session) == QDMI_SUCCESS; + const auto result = initialized + ? queryName(session) + : std::string{"initialization failed"}; + MQT_NA_QDMI_device_session_free(session); + return result; + })); + } + for (size_t i = 0; i < sessions.size(); ++i) { + EXPECT_EQ(sessions[i].get(), "Concurrent NA " + std::to_string(i)); + } +} + TEST_F(NaQDMISpecificationTest, SessionAlloc) { EXPECT_EQ(MQT_NA_QDMI_device_session_alloc(nullptr), QDMI_ERROR_INVALIDARGUMENT); diff --git a/test/qdmi/devices/na/test_generator.cpp b/test/qdmi/devices/na/test_generator.cpp deleted file mode 100644 index 2447c605d1..0000000000 --- a/test/qdmi/devices/na/test_generator.cpp +++ /dev/null @@ -1,101 +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/devices/na/Generator.hpp" - -#include -#include // NOLINT(misc-include-cleaner) - -#include -#include -#include - -namespace na { -namespace { -// clang-tidy wants to include the forward header, but we have the full -// NOLINTNEXTLINE(misc-include-cleaner) -auto testPopulation(const nlohmann::json& json) -> void { - for (const auto& [key, value] : json.items()) { - if (value.is_array()) { - // Array field should have at least one default entry - EXPECT_GT(value.size(), 0) << "Array field '" << key - << "' should have at least one default entry"; - for (const auto& item : value) { - // Each entry in the array should not be null - EXPECT_FALSE(item.is_null()) - << "Array field '" << key << "' should not have null entries"; - testPopulation(item); - } - } else if (value.is_object()) { - testPopulation(value); - } - } -} -} // namespace - -TEST(NaGeneratorTest, WriteJSONSchema) { - std::ostringstream os; - EXPECT_NO_THROW(writeJSONSchema(os)); - // clang-tidy wants to include the forward header, but we have the full - // NOLINTNEXTLINE(misc-include-cleaner) - nlohmann::json json; - EXPECT_NO_THROW(json = nlohmann::json::parse(os.str())); - EXPECT_TRUE(json.is_object()); - EXPECT_GT(json.size(), 0); - testPopulation(json); -} - -TEST(NaGeneratorTest, DurationUnitNanosecond) { - std::istringstream is(R"({ - "durationUnit": { - "scaleFactor": 5, - "unit": "ns" - } -})"); - Device device; - ASSERT_NO_THROW(device = readJSON(is)); - EXPECT_EQ(device.durationUnit.scaleFactor, 5); - EXPECT_EQ(device.durationUnit.unit, "ns"); -} - -TEST(NaGeneratorTest, DurationUnitInvalid) { - std::istringstream is(R"({ - "durationUnit": { - "scaleFactor": 1, - "unit": "ts" - } -})"); - EXPECT_THROW(std::ignore = readJSON(is), std::runtime_error); -} - -TEST(NaGeneratorTest, LengthUnitNanometer) { - std::istringstream is(R"({ - "lengthUnit": { - "scaleFactor": 5, - "unit": "nm" - } -})"); - Device device; - ASSERT_NO_THROW(device = readJSON(is)); - EXPECT_EQ(device.lengthUnit.scaleFactor, 5); - EXPECT_EQ(device.lengthUnit.unit, "nm"); -} - -TEST(NaGeneratorTest, LengthUnitInvalid) { - std::istringstream is(R"({ - "lengthUnit": { - "scaleFactor": 1, - "unit": "tm" - } -})"); - EXPECT_THROW(std::ignore = readJSON(is), std::runtime_error); -} - -} // namespace na diff --git a/test/qdmi/driver/CMakeLists.txt b/test/qdmi/driver/CMakeLists.txt index e84243087b..513a18d6cb 100644 --- a/test/qdmi/driver/CMakeLists.txt +++ b/test/qdmi/driver/CMakeLists.txt @@ -47,6 +47,15 @@ if(TARGET MQT::CoreQDMIDriver) CONTENT "{\n \"schema-version\": 1,\n \"qdmi\": {\n \"devices\": [\n {\"id\": \"mqt.na.default\", \"library\": \"$\", \"prefix\": \"MQT_NA\"},\n {\"id\": \"mqt.sc.default\", \"library\": \"$\", \"prefix\": \"MQT_SC\"},\n {\"id\": \"mqt.ddsim.default\", \"library\": \"$\", \"prefix\": \"MQT_DDSIM\"},\n {\"id\": \"test.disabled\", \"enabled\": false},\n {\"id\": \"broken.example\", \"library\": \"missing-device-library\", \"prefix\": \"BROKEN\"}\n ]\n }\n}\n" ) + file(READ "${PROJECT_SOURCE_DIR}/json/na/mqt-core-qdmi-na-device.json" custom_na_json) + string(REPLACE "MQT NA Default QDMI Device" "Custom NA Driver Device" custom_na_json + "${custom_na_json}") + string(REPLACE "\"numQubits\": 100" "\"numQubits\": 4" custom_na_json "${custom_na_json}") + string(REPLACE "\"width\": 9" "\"width\": 1" custom_na_json "${custom_na_json}") + string(REPLACE "\"height\": 9" "\"height\": 1" custom_na_json "${custom_na_json}") + string(REPLACE "\"duration\": 100" "\"duration\": 77" custom_na_json "${custom_na_json}") + set(custom_na_file "${CMAKE_CURRENT_BINARY_DIR}/custom-na-device.json") + file(WRITE "${custom_na_file}" "${custom_na_json}") string(MAKE_C_IDENTIFIER "${TARGET_NAME}-mqt-core-qdmi-metadata-device" metadata_manifest_stem) set(metadata_manifest_file "$/${metadata_manifest_stem}.qdmi.json") @@ -56,6 +65,9 @@ if(TARGET MQT::CoreQDMIDriver) "MQT_CORE_QDMI_TEST_CONFIG_FILE=\"${config_file}\"" "MQT_CORE_QDMI_METADATA_MANIFEST=\"${metadata_manifest_file}\"" "MQT_CORE_QDMI_SESSION_DEVICE=\"$\"" + "MQT_CORE_QDMI_NA_LIBRARY=\"$\"" + "MQT_CORE_QDMI_CUSTOM_NA_FILE=\"${custom_na_file}\"" + "MQT_CORE_QDMI_DEFAULT_NA_FILE=\"${PROJECT_SOURCE_DIR}/json/na/mqt-core-qdmi-na-device.json\"" "TEST_DEVICE_LIBRARIES=std::array{ std::pair{\"$\", \"MQT_NA\"}, std::pair{\"$\", \"MQT_SC\"}, std::pair{\"$\", \"MQT_DDSIM\"} }" ) diff --git a/test/qdmi/driver/test_driver.cpp b/test/qdmi/driver/test_driver.cpp index d4dbd57545..fa9558a19b 100644 --- a/test/qdmi/driver/test_driver.cpp +++ b/test/qdmi/driver/test_driver.cpp @@ -922,7 +922,7 @@ TEST(DeviceRegistrationTest, ValidatesDuplicatesAndReplacement) { EXPECT_THROW(driver.registerDevice(original), std::invalid_argument); auto replacement = original; - replacement.session.custom1 = "replacement"; + replacement.session.custom3 = "replacement"; EXPECT_NO_THROW(driver.registerDevice(replacement, true)); auto* const opened = driver.open(original.id); ASSERT_NE(opened, nullptr); @@ -1094,6 +1094,55 @@ TEST(DeviceRegistrationTest, TypedConfigurationRejectsRawAdapterSlotConflict) { std::invalid_argument); } +TEST(DeviceRegistrationTest, + RuntimeConfigurationSeparatesModelsUsingOneNaProviderLibrary) { + auto& driver = qdmi::Driver::get(); + static_cast( + driver.registerDeviceIfAbsent({.id = "test.na.runtime-default", + .library = MQT_CORE_QDMI_NA_LIBRARY, + .prefix = "MQT_NA"})); + static_cast(driver.registerDeviceIfAbsent( + {.id = "test.na.runtime-custom", + .library = MQT_CORE_QDMI_NA_LIBRARY, + .prefix = "MQT_NA", + .session = {.deviceConfiguration = qdmi::FileDeviceConfiguration{ + .path = MQT_CORE_QDMI_CUSTOM_NA_FILE}}})); + + const auto defaultDevice = + fomac::Session::openDevice("test.na.runtime-default"); + const auto customDevice = + fomac::Session::openDevice("test.na.runtime-custom"); + EXPECT_EQ(defaultDevice.getName(), "MQT NA Default QDMI Device"); + EXPECT_EQ(defaultDevice.getQubitsNum(), 100); + EXPECT_EQ(customDevice.getName(), "Custom NA Driver Device"); + EXPECT_EQ(customDevice.getQubitsNum(), 4); + EXPECT_NE(defaultDevice.getSites().size(), customDevice.getSites().size()); + ASSERT_FALSE(defaultDevice.getOperations().empty()); + ASSERT_FALSE(customDevice.getOperations().empty()); + EXPECT_EQ(defaultDevice.getOperations().front().getDuration(), 100); + EXPECT_EQ(customDevice.getOperations().front().getDuration(), 77); + + qdmi::DeviceSessionConfig overrides; + overrides.deviceConfiguration = + qdmi::FileDeviceConfiguration{.path = MQT_CORE_QDMI_DEFAULT_NA_FILE}; + const auto overridden = + fomac::Session::openDevice("test.na.runtime-custom", overrides); + EXPECT_EQ(overridden.getName(), "MQT NA Default QDMI Device"); + EXPECT_EQ(overridden.getQubitsNum(), 100); + + static_cast(driver.registerDeviceIfAbsent( + {.id = "test.na.runtime-invalid", + .library = MQT_CORE_QDMI_NA_LIBRARY, + .prefix = "MQT_NA", + .session = {.deviceConfiguration = + qdmi::InlineDeviceConfiguration{.json = "{}"}}})); + EXPECT_THROW( + static_cast(fomac::Session::openDevice("test.na.runtime-invalid")), + std::runtime_error); + EXPECT_EQ(fomac::Session::openDevice("test.na.runtime-default").getName(), + "MQT NA Default QDMI Device"); +} + TEST(DeviceRegistrationTest, FreshOpenCreatesDistinctSessions) { registerSessionTestDevice(); const auto first = fomac::Session::openDevice("test.session-overrides"); @@ -1285,8 +1334,8 @@ TEST(DeviceSessionConfigTest, OpenWithBaseUrl) { TEST(DeviceSessionConfigTest, OpenWithCustomParameters) { qdmi::DeviceSessionConfig config; - config.custom1 = "RESONANCE_COCOS_V1"; - config.custom2 = "test_value"; + config.custom3 = "RESONANCE_COCOS_V1"; + config.custom4 = "test_value"; config.baseUrl = "http://localhost:9090"; for (const auto& [lib, prefix] : TEST_DEVICE_LIBRARIES) { @@ -1342,15 +1391,14 @@ TEST(DeviceSessionConfigTest, OpenWithUsernamePassword) { } } -TEST(DeviceSessionConfigTest, OpenWithAllParameters) { +TEST(DeviceSessionConfigTest, + OpenWithAuthenticationAndRemainingCustomParameters) { qdmi::DeviceSessionConfig config; config.baseUrl = "http://localhost:8080"; config.token = "test_token"; config.authUrl = "https://auth.example.com"; config.username = "user"; config.password = "pass"; - config.custom1 = "value1"; - config.custom2 = "value2"; config.custom3 = "value3"; config.custom4 = "value4"; config.custom5 = "value5"; @@ -1390,7 +1438,7 @@ TEST(DeviceSessionConfigTest, IdempotentLoadingWithDifferentConfigs) { { qdmi::DeviceSessionConfig config; config.baseUrl = "http://localhost:9090"; - config.custom1 = "API_V2"; + config.custom3 = "API_V2"; EXPECT_NO_THROW(static_cast(openTestDevice(lib, prefix, config));); } @@ -1406,16 +1454,13 @@ TEST(DeviceSessionConfigTest, IdempotentLoadingWithDifferentConfigs) { TEST(DynamicDeviceLibraryTest, ReusesLibraryWithFreshDeviceSessions) { const auto [library, prefix] = TEST_DEVICE_LIBRARIES.front(); - qdmi::DeviceSessionConfig firstSessionConfig; - firstSessionConfig.custom1 = "first-session"; - auto* const first = openTestDevice(library, prefix, firstSessionConfig); + auto* const first = + openTestDevice(library, prefix, {.custom3 = "first-session"}); const auto equivalentLibrary = std::filesystem::path(library).parent_path() / "." / std::filesystem::path(library).filename(); - qdmi::DeviceSessionConfig secondSessionConfig; - secondSessionConfig.custom1 = "second-session"; - auto* const second = - openTestDevice(equivalentLibrary.string(), prefix, secondSessionConfig); + auto* const second = openTestDevice(equivalentLibrary.string(), prefix, + {.custom3 = "second-session"}); ASSERT_NE(first, second); EXPECT_EQ(&first->getLibrary(), &second->getLibrary()); From 022a48e9be9566bff769b04be86d62f1919a2521 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Fri, 14 Aug 2026 18:05:31 +0200 Subject: [PATCH 08/35] =?UTF-8?q?=E2=9C=A8=20Backport=20runtime-configurab?= =?UTF-8?q?le=20superconducting=20devices?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace generated superconducting provider state with strict session-owned JSON configuration. Preserve job retrieval, custom-operation support, the v3 QDMI namespace, and independent per-session models. Backports #1980. Assisted-by: GPT-5.6 via Codex --- CHANGELOG.md | 3 + UPGRADING.md | 9 + docs/qdmi/index.md | 4 +- docs/qdmi/sc_device.md | 76 ++ .../qdmi/devices/sc/Configuration.hpp | 79 ++ include/mqt-core/qdmi/devices/sc/Device.hpp | 349 ++------ .../mqt-core/qdmi/devices/sc/Generator.hpp | 123 --- ...vice.json => mqt-core-qdmi-sc-device.json} | 48 +- src/qdmi/devices/sc/App.cpp | 512 ------------ src/qdmi/devices/sc/CMakeLists.txt | 81 +- src/qdmi/devices/sc/Configuration.cpp | 403 ++++++++++ src/qdmi/devices/sc/Device.cpp | 760 +++++++++--------- src/qdmi/devices/sc/Generator.cpp | 278 ------- test/python/qdmi/test_qdmi.py | 22 + test/qdmi/TestUtils.hpp | 76 ++ test/qdmi/devices/na/CMakeLists.txt | 1 + test/qdmi/devices/na/test_device.cpp | 53 +- test/qdmi/devices/sc/CMakeLists.txt | 27 +- test/qdmi/devices/sc/test_app.cpp | 372 --------- test/qdmi/devices/sc/test_configuration.cpp | 182 +++++ test/qdmi/devices/sc/test_device.cpp | 309 ++++++- test/qdmi/devices/sc/test_generator.cpp | 53 -- test/qdmi/driver/CMakeLists.txt | 1 + test/qdmi/driver/test_driver.cpp | 74 ++ test/qdmi/registry/CMakeLists.txt | 3 +- test/qdmi/registry/test_device_registry.cpp | 54 +- 26 files changed, 1773 insertions(+), 2179 deletions(-) create mode 100644 docs/qdmi/sc_device.md create mode 100644 include/mqt-core/qdmi/devices/sc/Configuration.hpp delete mode 100644 include/mqt-core/qdmi/devices/sc/Generator.hpp rename json/sc/{device.json => mqt-core-qdmi-sc-device.json} (76%) delete mode 100644 src/qdmi/devices/sc/App.cpp create mode 100644 src/qdmi/devices/sc/Configuration.cpp delete mode 100644 src/qdmi/devices/sc/Generator.cpp create mode 100644 test/qdmi/TestUtils.hpp delete mode 100644 test/qdmi/devices/sc/test_app.cpp create mode 100644 test/qdmi/devices/sc/test_configuration.cpp delete mode 100644 test/qdmi/devices/sc/test_generator.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index e62d5049bd..369eb6aa9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ releases may include breaking changes. ([**@burgholzer**]) - ✨ Expose compressed vector and matrix DD serialization through bytes-based Python APIs ([#1983]) ([**@burgholzer**]) +- ✨ Make superconducting QDMI devices runtime configurable with session-owned + topology, operations, and calibration data ([#1980]) ([**@burgholzer**]) - ✨ Expose registered QDMI device IDs without loading device libraries ([#1972]) ([**@burgholzer**]) - ✨ Add typed runtime configuration transport and relocatable assets for QDMI @@ -651,6 +653,7 @@ changelogs._ [#2005]: https://github.com/munich-quantum-toolkit/core/pull/2005 [#1984]: https://github.com/munich-quantum-toolkit/core/pull/1984 [#1983]: https://github.com/munich-quantum-toolkit/core/pull/1983 +[#1980]: https://github.com/munich-quantum-toolkit/core/pull/1980 [#1972]: https://github.com/munich-quantum-toolkit/core/pull/1972 [#1967]: https://github.com/munich-quantum-toolkit/core/pull/1967 [#1965]: https://github.com/munich-quantum-toolkit/core/pull/1965 diff --git a/UPGRADING.md b/UPGRADING.md index fcf9182111..873df25a79 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -6,6 +6,15 @@ of changes including minor and patch releases, please refer to the ## [Unreleased] +### Runtime-configurable SC QDMI device + +The built-in superconducting QDMI provider now parses its device description +when each session is initialized. The `mqt-core-qdmi-sc-device-gen` target, +SC-specific generator executable, `sc::writeHeader`, `sc::writeJSONSchema`, and +generated `DeviceMemberInitializers.hpp` file have been removed. Replace +generator API use with `sc::Device` and the `sc::readJSON` functions declared in +`qdmi/devices/sc/Configuration.hpp`. + ### Runtime-configurable neutral-atom QDMI device The built-in neutral-atom QDMI provider now parses its device description when diff --git a/docs/qdmi/index.md b/docs/qdmi/index.md index 3479f88080..43d59d477f 100644 --- a/docs/qdmi/index.md +++ b/docs/qdmi/index.md @@ -5,7 +5,8 @@ The 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 -[QDMI device for Neutral Atom Systems](na_device.md), and a +[QDMI device for Neutral Atom Systems](na_device.md), a +[QDMI device for Superconducting Systems](sc_device.md), and a [QDMI device for a Classical Quantum Circuit Simulator](ddsim_device). ```{toctree} @@ -13,6 +14,7 @@ components, such as a [QDMI driver](driver.md), a :caption: Table of Contents NA QDMI Device +SC QDMI Device DDSIM QDMI Device QDMI Driver QDMI device configuration diff --git a/docs/qdmi/sc_device.md b/docs/qdmi/sc_device.md new file mode 100644 index 0000000000..e872275259 --- /dev/null +++ b/docs/qdmi/sc_device.md @@ -0,0 +1,76 @@ +# Superconducting QDMI device + +The MQT Core superconducting (SC) provider builds a session-owned runtime model +from strict JSON. Separate sessions using the same provider library and prefix +can expose different names, capacities, topology, operations, and calibration. + +## Configuration sources + +The provider selects the first available source in this order: + +1. QDMI CUSTOM1 containing NUL-terminated inline JSON; +2. QDMI CUSTOM2 containing a NUL-terminated JSON file path; +3. `MQT_CORE_QDMI_SC_CONFIG_JSON`; +4. `MQT_CORE_QDMI_SC_CONFIG_FILE`; +5. `mqt-core-qdmi-sc-device.json` beside the provider shared library. + +When direct QDMI callers populate both explicit slots, inline JSON wins. Setting +both SC environment sources is an error. Configuration cannot be changed after +successful initialization. A failed initialization does not commit partial +state, so the same allocated session can be corrected and initialized again. + +## Schema and calibration + +Every description requires `"schema-version": 1`, a name, positive `numQubits`, +a duration unit, qubit properties, ordered couplings, and operations. Unknown +and missing fields are rejected. Each operation declares its name, parameter +count, arity, optional supported ordered site tuples, optional default duration +and fidelity, and optional tuple-specific overrides. + +```json +{ + "schema-version": 1, + "name": "Small SC device", + "numQubits": 2, + "durationUnit": {"unit": "ns", "scaleFactor": 1.0}, + "qubitProperties": { + "defaults": {"t1": 100000, "t2": 150000}, + "overrides": [{"qubit": 1, "t1": 95000}] + }, + "couplings": [[0, 1]], + "operations": [ + { + "name": "cz", + "numParameters": 0, + "numQubits": 2, + "duration": 200, + "fidelity": 0.99, + "siteOverrides": [ + {"sites": [0, 1], "duration": 180, "fidelity": 0.995} + ] + } + ] +} +``` + +`durationUnit.unit` is one of `s`, `ms`, `us`, and `ns`, and its `scaleFactor` +is positive and finite. Couplings contain distinct, valid, non-self tuples. +Their order is significant; list both orientations if an operation supports +both. Operation names are unique, durations are non-negative, and fidelities are +finite values in the inclusive range `[0, 1]`. + +The `sites` member may be omitted for one-qubit operations to support every +qubit, or for two-qubit operations to use the coupling map. Higher-arity +operations require explicit tuples. Every explicit two-qubit tuple must be an +exact ordered edge in the coupling map. Every site override must select one of +the operation's supported tuples and supply a duration, fidelity, or both. + +When operation duration or fidelity is queried, a matching tuple override wins +over the operation default. Missing data returns `QDMI_ERROR_NOTSUPPORTED`. +Qubit T1 and T2 use a per-qubit override first and the qubit default second. The +order of a configured site tuple is significant. Handles from another session +are rejected. + +The calibration values bundled in `json/sc/mqt-core-qdmi-sc-device.json` are +synthetic examples for testing and documentation; they are not measurements of +physical hardware. diff --git a/include/mqt-core/qdmi/devices/sc/Configuration.hpp b/include/mqt-core/qdmi/devices/sc/Configuration.hpp new file mode 100644 index 0000000000..65c908e395 --- /dev/null +++ b/include/mqt-core/qdmi/devices/sc/Configuration.hpp @@ -0,0 +1,79 @@ +/* + * 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 Configuration.hpp + * @brief Superconducting QDMI device configuration. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace sc { + +/// Strict schema-version-1 description of a superconducting device. +struct Device { + struct Unit { + std::string unit; + double scaleFactor = 1.; + }; + + struct QubitCalibration { + std::optional t1; + std::optional t2; + }; + + struct QubitOverride : QubitCalibration { + uint64_t qubit = 0; + }; + + struct QubitProperties { + QubitCalibration defaults; + std::vector overrides; + }; + + struct SiteOverride { + std::vector sites; + std::optional duration; + std::optional fidelity; + }; + + struct Operation { + std::string name; + uint64_t numParameters = 0; + uint64_t numQubits = 0; + std::optional>> sites; + std::optional duration; + std::optional fidelity; + std::vector siteOverrides; + }; + + uint64_t schemaVersion = 1; + std::string name; + uint64_t numQubits = 0; + Unit durationUnit; + QubitProperties qubitProperties; + std::vector> couplings; + std::vector operations; +}; + +/// Parse and validate a device description and report errors with @p source. +[[nodiscard]] Device readJSON(std::string_view json, std::string_view source); +[[nodiscard]] Device readJSON(std::istream& stream, + std::string_view source = "input"); +[[nodiscard]] Device readJSON(const std::string& path); + +} // namespace sc diff --git a/include/mqt-core/qdmi/devices/sc/Device.hpp b/include/mqt-core/qdmi/devices/sc/Device.hpp index 7a8f205fb3..cbd5117fa5 100644 --- a/include/mqt-core/qdmi/devices/sc/Device.hpp +++ b/include/mqt-core/qdmi/devices/sc/Device.hpp @@ -8,301 +8,106 @@ * Licensed under the MIT License */ -#pragma once - -/** @file +/** @file Device.hpp * @brief The MQT QDMI device implementation for superconducting devices. */ +#pragma once + #include "mqt_sc_qdmi/device.h" -#include "qdmi/common/Common.hpp" #include #include +#include #include +#include #include #include -#include -#include #include -namespace qdmi::sc { -class Device final : public Singleton { - friend class Singleton; - - /// @brief Provides access to the device name. - std::string name_; - - /// @brief The number of qubits in the device. - size_t qubitsNum_ = 0; - - /// @brief The list of sites. - std::vector> sites_; +struct MQT_SC_QDMI_Device_Session_impl_d; - /// @brief The list of couplings, i.e., qubit pairs - std::vector> couplingMap_; - - /// @brief The list of operations. - std::vector> operations_; - - /// @brief The list of device sessions. - std::unordered_map> - sessions_; - - /// @brief Private constructor to enforce the singleton pattern. - Device(); - -public: - /** - * @brief Allocates a new device session. - * @see MQT_SC_QDMI_device_session_alloc - */ - auto sessionAlloc(MQT_SC_QDMI_Device_Session* session) -> int; - - /** - * @brief Frees a device session. - * @see MQT_SC_QDMI_device_session_free - */ - auto sessionFree(MQT_SC_QDMI_Device_Session session) -> void; +struct MQT_SC_QDMI_Site_impl_d { + MQT_SC_QDMI_Device_Session_impl_d* owner = nullptr; + uint64_t id = 0; + std::optional t1; + std::optional t2; - /** - * @brief Query a device property. - * @see MQT_SC_QDMI_device_session_query_device_property - */ - auto queryProperty(QDMI_Device_Property prop, size_t size, void* value, - size_t* sizeRet) const -> int; + int queryProperty(QDMI_Site_Property property, size_t size, void* value, + size_t* sizeRet) const; }; -} // namespace qdmi::sc -/** - * @brief Implementation of the MQT_SC_QDMI_Device_Session structure. - */ -struct MQT_SC_QDMI_Device_Session_impl_d { -private: - /// The status of the session. - enum class Status : uint8_t { - ALLOCATED, ///< The session has been allocated but not initialized - INITIALIZED, ///< The session has been initialized and is ready for use +struct MQT_SC_QDMI_Operation_impl_d { + struct Calibration { + std::optional duration; + std::optional fidelity; }; - /// @brief The current status of the session. - Status status_ = Status::ALLOCATED; - /// @brief The device jobs associated with this session. - std::unordered_map> - jobs_; - -public: - /** - * @brief Initializes the device session. - * @see MQT_SC_QDMI_device_session_init - */ - auto init() -> int; - /** - * @brief Sets a parameter for the device session. - * @see MQT_SC_QDMI_device_session_set_parameter - */ - auto setParameter(QDMI_Device_Session_Parameter param, size_t size, - const void* value) const -> int; - - /** - * @brief Create a new device job. - * @see MQT_SC_QDMI_device_session_create_device_job - */ - auto createDeviceJob(MQT_SC_QDMI_Device_Job* job) -> int; - - /** - * @brief Frees the device job. - * @see MQT_SC_QDMI_device_job_free - */ - auto freeDeviceJob(MQT_SC_QDMI_Device_Job job) -> void; - - /** - * @brief Forwards a query of a device property to the device. - * @see MQT_SC_QDMI_device_session_query_device_property - */ - auto queryDeviceProperty(QDMI_Device_Property prop, size_t size, void* value, - size_t* sizeRet) const -> int; - - /** - * @brief Forwards a query of a site property to the site. - * @see MQT_SC_QDMI_device_session_query_site_property - */ - auto querySiteProperty(MQT_SC_QDMI_Site site, QDMI_Site_Property prop, - size_t size, void* value, size_t* sizeRet) const - -> int; - - /** - * @brief Forwards a query of an operation property to the operation. - * @see MQT_SC_QDMI_device_session_query_operation_property - */ - auto queryOperationProperty(MQT_SC_QDMI_Operation operation, size_t numSites, - const MQT_SC_QDMI_Site* sites, size_t numParams, - const double* params, - QDMI_Operation_Property prop, size_t size, - void* value, size_t* sizeRet) const -> int; + MQT_SC_QDMI_Device_Session_impl_d* owner = nullptr; + std::string name; + size_t numParameters = 0; + size_t numQubits = 0; + std::vector> supportedSites; + std::vector flattenedSites; + Calibration defaults; + std::vector, Calibration>> overrides; + + int queryProperty(size_t numSites, const MQT_SC_QDMI_Site* sites, + size_t numParams, const double* params, + QDMI_Operation_Property property, size_t size, void* value, + size_t* sizeRet) const; }; -/** - * @brief Implementation of the MQT_SC_QDMI_Device_Job structure. - */ struct MQT_SC_QDMI_Device_Job_impl_d { -private: - /// @brief The device session associated with the job. - MQT_SC_QDMI_Device_Session_impl_d* session_; - -public: - /** - * @brief Initializes a device job implementation bound to the given session. - * - * @param session Pointer to the owning MQT_SC_QDMI_Device_Session_impl_d. The - * session must remain valid for the job's lifetime. - */ explicit MQT_SC_QDMI_Device_Job_impl_d( MQT_SC_QDMI_Device_Session_impl_d* session) - : session_(session) {} - /** - * @brief Frees the device job. - * @note This function just forwards to the session's @ref freeDeviceJob - * function. This function is needed because the interface only provides the - * job handle to the @ref QDMI_job_free function and the job's session handle - * is private. - * @see QDMI_job_free - */ - auto free() -> void; - - /** - * @brief Sets a parameter for the job. - * @see MQT_SC_QDMI_device_job_set_parameter - */ - auto setParameter(QDMI_Device_Job_Parameter param, size_t size, - const void* value) -> int; - - /** - * @brief Queries a property of the job. - * @see MQT_SC_QDMI_device_job_query_property - */ - auto queryProperty(QDMI_Device_Job_Property prop, size_t size, void* value, - size_t* sizeRet) -> int; - - /** - * @brief Submits the job to the device. - * @see MQT_SC_QDMI_device_job_submit - */ - auto submit() -> int; - - /** - * @brief Cancels the job. - * @see MQT_SC_QDMI_device_job_cancel - */ - auto cancel() -> int; - - /** - * @brief Checks the status of the job. - * @see MQT_SC_QDMI_device_job_check - */ - auto check(QDMI_Job_Status* status) -> int; - - /** - * @brief Waits for the job to complete but at most for the specified timeout. - * @see MQT_SC_QDMI_device_job_wait - */ - auto wait(size_t timeout) -> int; - - /** - * @brief Gets the results of the job. - * @see MQT_SC_QDMI_device_job_get_results - */ - auto getResults(QDMI_Job_Result result, size_t size, void* data, - [[maybe_unused]] size_t* sizeRet) -> int; -}; - -/** - * @brief Implementation of the MQT_SC_QDMI_Site structure. - */ -struct MQT_SC_QDMI_Site_impl_d { - friend MQT_SC_QDMI_Operation_impl_d; - -private: - uint64_t id_ = 0; ///< Unique identifier of the site - - /** - * @brief Initializes a site implementation with the given unique identifier. - * - * @param id Unique identifier for the site. - */ - explicit MQT_SC_QDMI_Site_impl_d(uint64_t id) : id_(id) {} - -public: - /// @brief Factory function for regular sites. - [[nodiscard]] static auto makeUniqueSite(uint64_t id) - -> std::unique_ptr; - /** - * @brief Queries a property of the site. - * @see MQT_SC_QDMI_device_session_query_site_property - */ - auto queryProperty(QDMI_Site_Property prop, size_t size, void* value, - size_t* sizeRet) const -> int; + : session(session) {} + + MQT_SC_QDMI_Device_Session_impl_d* session; + void free(); + int setParameter(QDMI_Device_Job_Parameter parameter, size_t size, + const void* value); + int queryProperty(QDMI_Device_Job_Property property, size_t size, void* value, + size_t* sizeRet); + int submit(); + int cancel(); + int check(QDMI_Job_Status* status); + int wait(size_t timeout); + int getResults(QDMI_Job_Result result, size_t size, void* data, + size_t* sizeRet); }; -/** - * @brief Implementation of the MQT_SC_QDMI_Operation structure. - */ -struct MQT_SC_QDMI_Operation_impl_d { -private: - std::string name_; ///< Name of the operation - size_t numParameters_; ///< Number of parameters for the operation - /** - * @brief Number of qubits involved in the operation - */ - size_t numQubits_{}; - /** - * @brief Storage for individual sites and site pairs. - * @details Uses std::variant to preserve the tuple structure of the operation - * sites: - * - Single-qubit and zoned operations: vector - * - Local two-qubit operations: vector> - * This maintains type safety and QDMI specification compliance, which states - * that operation sites should be "a list of tuples" for local multi-qubit - * operations. - */ - using SitesStorage = - std::variant, - std::vector>>; - - /// The operation's supported sites - SitesStorage supportedSites_; - /// @brief Constructor for a single-qubit operation. - MQT_SC_QDMI_Operation_impl_d(std::string name, size_t numParameters, - const std::vector& sites); - /// @brief Constructor for a two-qubit operation. - MQT_SC_QDMI_Operation_impl_d( - std::string name, size_t numParameters, - const std::vector>& sites); - - /// @brief Sort the sites such that the occurrence of a given site can be - /// determined in O(log n) time. - auto sortSites() -> void; - -public: - /// @brief Factory function a single-qubit operation. - [[nodiscard]] static auto - makeUniqueSingleQubit(std::string name, size_t numParameters, - const std::vector& sites) - -> std::unique_ptr; - /// @brief Factory function a two-qubit operation. - [[nodiscard]] static auto makeUniqueTwoQubit( - std::string name, size_t numParameters, - const std::vector>& sites) - -> std::unique_ptr; - - /** - * @brief Queries a property of the operation. - * @see MQT_SC_QDMI_device_session_query_operation_property - */ - auto queryProperty(size_t numSites, const MQT_SC_QDMI_Site* sites, - size_t numParams, const double* params, - QDMI_Operation_Property prop, size_t size, void* value, - size_t* sizeRet) const -> int; +struct MQT_SC_QDMI_Device_Session_impl_d { + enum class Status : uint8_t { ALLOCATED, INITIALIZED }; + + Status status = Status::ALLOCATED; + std::optional inlineConfiguration; + std::optional fileConfiguration; + std::string name; + std::string durationUnit; + double durationScaleFactor = 1.; + size_t qubitsNum = 0; + std::vector> siteStorage; + std::vector sites; + std::vector> couplingMap; + std::vector> operationStorage; + std::vector operations; + std::unordered_map> + jobs; + + int init(); + int setParameter(QDMI_Device_Session_Parameter parameter, size_t size, + const void* value); + int createDeviceJob(MQT_SC_QDMI_Device_Job* job); + void freeDeviceJob(MQT_SC_QDMI_Device_Job job); + int queryDeviceProperty(QDMI_Device_Property property, size_t size, + void* value, size_t* sizeRet) const; + int querySiteProperty(MQT_SC_QDMI_Site site, QDMI_Site_Property property, + size_t size, void* value, size_t* sizeRet) const; + int queryOperationProperty(MQT_SC_QDMI_Operation operation, size_t numSites, + const MQT_SC_QDMI_Site* sites, size_t numParams, + const double* params, + QDMI_Operation_Property property, size_t size, + void* value, size_t* sizeRet) const; }; diff --git a/include/mqt-core/qdmi/devices/sc/Generator.hpp b/include/mqt-core/qdmi/devices/sc/Generator.hpp deleted file mode 100644 index 09631ae1d4..0000000000 --- a/include/mqt-core/qdmi/devices/sc/Generator.hpp +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#pragma once - -#include // NOLINT(misc-include-cleaner) - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace sc { -/** - * @brief Represents a superconducting device configuration. - * @details This struct defines the schema for the JSON representation of a - * superconducting device configuration. This struct, including all its - * sub-structs, implements functions to serialize and deserialize to and from - * JSON using the nlohmann::json library. - */ -struct Device { - /// @brief The name of the device. - std::string name; - /// @brief The number of qubits in the device. - uint64_t numQubits = 0; - /// @brief The list of couplings the device supports. - std::vector> couplings; - -private: - struct Operation { - /// @brief The name of the operation. - std::string name; - /// @brief The number of parameters the operation takes. - uint64_t numParameters = 0; - /// @brief The number of qubits the operation takes. - uint64_t numQubits = 0; - - // NOLINTNEXTLINE(misc-include-cleaner) - NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Operation, name, numParameters, - numQubits) - }; - -public: - /// @brief The list of operations the device supports. - std::vector operations; - - // NOLINTNEXTLINE(misc-include-cleaner) - NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Device, name, numQubits, - couplings, operations) -}; - -/** - * @brief Writes a JSON schema with default values for the device configuration - * to the specified output stream. - * @param os is the output stream to write the JSON schema to. - * @throws std::runtime_error if the JSON conversion fails. - */ -auto writeJSONSchema(std::ostream& os) -> void; - -/** - * @brief Writes a JSON schema with default values for the device configuration - * to the specified path. - * @param path The path to write the JSON schema to. - * @throws std::runtime_error if the JSON conversion fails or the file cannot be - * opened. - */ -auto writeJSONSchema(const std::string& path) -> void; - -/** - * @brief Parses the device configuration from an input stream. - * @param is is the input stream containing the JSON representation of the - * device configuration. - * @returns The parsed device configuration as a @ref sc::Device object. - * @throws std::runtime_error if the JSON cannot be parsed. - */ -[[nodiscard]] auto readJSON(std::istream& is) -> Device; - -/** - * @brief Parses the device configuration from a JSON file. - * @param path is the path to the JSON file containing the device configuration. - * @returns The parsed device configuration as a @ref sc::Device object. - * @throws std::runtime_error if the JSON file does not exist, or the JSON file - * cannot be parsed. - */ -[[nodiscard]] auto readJSON(const std::string& path) -> Device; - -/** - * @brief Writes a header file with the device configuration to the specified - * output stream. - * @param device is a parsed and in-memory representation of the device. - * @param os is the output stream to write the header file to. - * @throws std::runtime_error if the file cannot be opened or written to. - * @note This implementation only supports multi-qubit gates up to two - * qubits. - */ -auto writeHeader(const Device& device, std::ostream& os) -> void; - -/** - * @brief Writes a header file with the device configuration to the specified - * path. - * @param device is a parsed and in-memory representation of the device. - * @param path is the path to write the header file to. - * @throws std::runtime_error if the file cannot be opened or written to. - * @note This implementation only supports multi-qubit gates up to two - * qubits. - */ -auto writeHeader(const Device& device, const std::string& path) -> void; -} // namespace sc diff --git a/json/sc/device.json b/json/sc/mqt-core-qdmi-sc-device.json similarity index 76% rename from json/sc/device.json rename to json/sc/mqt-core-qdmi-sc-device.json index 2445006036..590e3fa203 100644 --- a/json/sc/device.json +++ b/json/sc/mqt-core-qdmi-sc-device.json @@ -1,6 +1,23 @@ { + "schema-version": 1, "name": "MQT SC Default QDMI Device", "numQubits": 100, + "durationUnit": { + "unit": "ns", + "scaleFactor": 1.0 + }, + "qubitProperties": { + "defaults": { + "t1": 100000, + "t2": 150000 + }, + "overrides": [ + { + "qubit": 7, + "t1": 95000 + } + ] + }, "couplings": [ [0, 1], [1, 2], @@ -184,8 +201,33 @@ [89, 99] ], "operations": [ - { "name": "r", "numParameters": 2, "numQubits": 1 }, - { "name": "cz", "numParameters": 0, "numQubits": 2 }, - { "name": "measure", "numParameters": 0, "numQubits": 1 } + { + "name": "r", + "numParameters": 2, + "numQubits": 1, + "duration": 20, + "fidelity": 0.999 + }, + { + "name": "cz", + "numParameters": 0, + "numQubits": 2, + "duration": 200, + "fidelity": 0.99, + "siteOverrides": [ + { + "sites": [0, 1], + "duration": 180, + "fidelity": 0.995 + } + ] + }, + { + "name": "measure", + "numParameters": 0, + "numQubits": 1, + "duration": 500, + "fidelity": 0.98 + } ] } diff --git a/src/qdmi/devices/sc/App.cpp b/src/qdmi/devices/sc/App.cpp deleted file mode 100644 index 0b74540fd2..0000000000 --- a/src/qdmi/devices/sc/App.cpp +++ /dev/null @@ -1,512 +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 App.cpp - * @brief Implementation of the generator tool for superconducting devices. - */ - -#include "qdmi/devices/sc/Generator.hpp" - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace { -/** - * @brief Writes usage information and available commands and options to stdout. - * - * @param programName Program executable name inserted into the Usage line. - */ -auto printUsage(const std::string& programName) -> void { - std::cout - << "Generator for turning superconducting computer JSON specifications " - "into " - "header files to be used as part of a superconducting QDMI device " - "implementation.\n" - "\n" - "Usage: " - << programName - << " [OPTIONS] [ARGS]\n" - "\n" - "Commands:\n" - " schema Generate a default JSON schema.\n" - " validate Validate a JSON specification.\n" - " generate Generate a header file from a JSON specification.\n" - "\n" - "Options:\n" - " -h, --help Show this help message and exit.\n" - " -v, --version Show version information and exit.\n"; -} - -/** - * Prints the usage information for the schema sub-command. - * @param programName is the name of the program executable. - */ -auto printSchemaUsage(const std::string& programName) -> void { - std::cout << "Generates a JSON schema with default values.\n" - "\n" - "Usage: " - << programName - << " schema [options]\n" - "\n" - "Options:\n" - " -h, --help Show this help message and exit.\n" - " -o, --output Specify the output file. If not\n" - " specified, prints to stdout.\n"; -} - -/** - * Prints the usage information for the validate sub-command. - * @param programName is the name of the program executable. - */ -auto printValidateUsage(const std::string& programName) -> void { - std::cout << "Validates a JSON specification against the schema.\n" - "\n" - "Usage: " - << programName - << " validate [options] []\n" - "\n" - "Arguments:\n" - " json_file the path to the JSON file to validate. If\n" - " not specified, the JSON is read from stdin.\n" - "\n" - "Options:\n" - " -h, --help Show this help message and exit.\n"; -} - -/** - * Prints the usage information for the generate sub-command. - * @param programName is the name of the program executable. - */ -auto printGenerateUsage(const std::string& programName) -> void { - std::cout << "Generates a header file from a JSON specification.\n" - "\n" - "Usage: " - << programName - << " generate [options] []\n" - "\n" - "Arguments:\n" - " json_file the path to the JSON file to generate the\n" - " header file from. If not specified, the\n" - " JSON is read from stdin.\n" - "\n" - "Options:\n" - " -h, --help Show this help message and exit.\n" - " -o, --output Specify the output file for the\n" - " generated header file. If no output\n" - " file is specified, the header file is\n" - " printed to stdout.\n"; -} - -/** - * @brief Writes the tool's version string to standard output. - * - * Prints the program name and the embedded MQT core version to stdout. - */ -auto printVersion() -> void { - // NOLINTNEXTLINE(misc-include-cleaner) - std::cout << "MQT QDMI SC Device Generator (MQT Version " MQT_CORE_VERSION - ")\n"; -} - -/// Enum to represent the different commands that can be executed. -enum class Command : uint8_t { - Schema, ///< Command to generate a JSON schema - Validate, ///< Command to validate a JSON specification - Generate ///< Command to generate a header file from a JSON specification -}; - -/// Struct to hold the parsed command line arguments. -struct Arguments { - std::string programName; ///< Name of the program executable - bool help = false; ///< Flag to indicate if help is requested - /// Flag to indicate if version information is requested - bool version = false; - std::optional command; ///< Command to execute -}; - -/// Struct to hold the parsed schema command line arguments. -struct SchemaArguments { - bool help = false; ///< Flag to indicate if help is requested - /// Optional output file for the schema - std::optional outputFile; -}; - -/// Struct to hold the parsed validate command line arguments. -struct ValidateArguments { - bool help = false; ///< Flag to indicate if help is requested - /// Optional JSON file to validate - std::optional jsonFile; -}; - -/// Struct to hold the parsed generate command line arguments. -struct GenerateArguments { - bool help = false; ///< Flag to indicate if help is requested - /// Optional output file for the generated header file - std::optional outputFile; - /// Optional JSON file to parse the device configuration - std::optional jsonFile; -}; - -/** - * @brief Parse top-level command-line options and locate the chosen - * sub-command. - * - * @param args Vector of command-line tokens (typically argv converted to - * std::string), where args[0] is the program name. - * @return std::pair The first element is the parsed - * top-level Arguments; the second element is the index in `args` of the - * first argument belonging to the chosen sub-command (i.e., one past the - * sub-command token). If no sub-command is present, the returned index - * will be `args.size() + 1`. - */ -auto parseArguments(const std::vector& args) - -> std::pair { - Arguments arguments; - arguments.programName = - args.empty() ? "mqt-core-sc-device-gen" : args.front(); - size_t i = 1; - while (i < args.size()) { - if (const std::string& arg = args.at(i); arg == "-h" || arg == "--help") { - arguments.help = true; - } else if (arg == "-v" || arg == "--version") { - arguments.version = true; - } else if (arg == "schema") { - arguments.command = Command::Schema; - break; // Stop top-level parsing; remaining args handled by schema parser - } else if (arg == "validate") { - arguments.command = Command::Validate; - // Stop top-level parsing; remaining args handled by validate parser - break; - } else if (arg == "generate") { - arguments.command = Command::Generate; - // Stop top-level parsing; remaining args handled by generate parser - break; - } else { - throw std::invalid_argument("Unknown argument: " + arg); - } - ++i; - } - return {arguments, i + 1}; -} - -/** - * @brief Parse arguments for the "schema" sub-command. - * - * Parses options for the schema command and produces a SchemaArguments value - * describing whether help was requested and which output file (if any) was set. - * - * @param args Vector of all command-line arguments. - * @param i Index of the first argument belonging to the schema sub-command. - * @return SchemaArguments Struct with `help` set if help was requested and - * `outputFile` containing the path provided with `-o|--output`, if any. - */ -auto parseSchemaArguments(const std::vector& args, size_t i) - -> SchemaArguments { - SchemaArguments schemaArgs; - while (i < args.size()) { - if (const std::string& arg = args.at(i); arg == "-h" || arg == "--help") { - schemaArgs.help = true; - } else if (arg == "-o" || arg == "--output") { - if (++i >= args.size()) { - throw std::invalid_argument("Missing value for output option."); - } - schemaArgs.outputFile = args.at(i); - } else { - throw std::invalid_argument("Unknown argument: " + arg); - } - ++i; - } - return schemaArgs; -} - -/** - * @brief Parses arguments for the "validate" subcommand. - * - * @param args Vector of command-line arguments. - * @param i Index of the first validate subcommand argument within @p args. - * @return ValidateArguments Parsed flags and optional JSON input file path: - * `help` is set if -h/--help was present, `jsonFile` contains the positional - * JSON file if provided. - * @throws std::invalid_argument if multiple JSON files are specified. - */ -auto parseValidateArguments(const std::vector& args, size_t i) - -> ValidateArguments { - ValidateArguments validateArgs; - while (i < args.size()) { - if (const std::string& arg = args.at(i); arg == "-h" || arg == "--help") { - validateArgs.help = true; - } else { - if (validateArgs.jsonFile.has_value()) { - throw std::invalid_argument("Multiple JSON files specified"); - } - validateArgs.jsonFile = arg; - } - ++i; - } - return validateArgs; -} - -/** - * Parse arguments for the "generate" subcommand. - * - * Recognizes the following arguments: - * - `-h`, `--help`: sets the help flag. - * - `-o `, `--output `: sets the output header file path. - * - `` (positional): sets the input JSON file; if omitted, input is - * read from stdin. - * - * @param args Vector of command-line arguments. - * @param i Index of the first argument belonging to the subcommand within - * `args`. - * @return GenerateArguments Structure with `help`, optional `outputFile`, and - * optional `jsonFile` populated. - * @throws std::invalid_argument If an `-o`/`--output` option is provided - * without a following value. - * @throws std::invalid_argument if multiple JSON files are specified. - */ -auto parseGenerateArguments(const std::vector& args, size_t i) - -> GenerateArguments { - GenerateArguments generateArgs; - while (i < args.size()) { - if (const std::string& arg = args.at(i); arg == "-h" || arg == "--help") { - generateArgs.help = true; - } else if (arg == "-o" || arg == "--output") { - if (++i >= args.size()) { - throw std::invalid_argument("Missing value for output option."); - } - generateArgs.outputFile = args.at(i); - } else { - if (generateArgs.jsonFile.has_value()) { - throw std::invalid_argument("Multiple JSON files specified"); - } - generateArgs.jsonFile = arg; - } - ++i; - } - return generateArgs; -} - -/** - * Executes the schema command, generating a JSON schema and writing it to the - * specified output file or stdout. - * @param progName is the name of the program executable. - * @param argVec is the vector of command line arguments. - * @param i is the index to the first sub-command argument within @p argVec - * @return 0 on success, 1 on error. - */ -auto executeSchemaCommand(const std::string& progName, - const std::vector& argVec, - const size_t i) -> int { - SchemaArguments schemaArgs; - // parse the rest of the command line arguments for the schema command - try { - schemaArgs = parseSchemaArguments(argVec, i); - } catch (const std::exception& e) { - SPDLOG_ERROR("Error parsing schema arguments: {}", e.what()); - printSchemaUsage(progName); - return 1; - } - // if the help flag is set, print the schema usage information and exit - if (schemaArgs.help) { - printSchemaUsage(progName); - return 0; - } - // generate the JSON schema and write it to the output file or stdout - try { - if (schemaArgs.outputFile.has_value()) { - sc::writeJSONSchema(schemaArgs.outputFile.value()); - } else { - sc::writeJSONSchema(std::cout); - } - } catch (const std::exception& e) { - SPDLOG_ERROR("Error generating JSON schema: {}", e.what()); - return 1; - } - return 0; -} - -/** - * @brief Run the "validate" subcommand to validate a JSON input. - * - * Parses validate-specific arguments, prints subcommand usage if the help - * flag is set, and validates JSON read from the provided file path or from - * standard input. - * - * @param progName Name of the program executable (used for usage output). - * @param argVec Full command-line argument vector. - * @param i Index of the first argument belonging to the validate subcommand. - * @return int `0` on successful validation or when help was printed, `1` on - * error. - */ -auto executeValidateCommand(const std::string& progName, - const std::vector& argVec, - const size_t i) -> int { - ValidateArguments validateArgs; - // parse the rest of the command line arguments for the validate command - try { - validateArgs = parseValidateArguments(argVec, i); - } catch (const std::exception& e) { - SPDLOG_ERROR("Error parsing validate arguments: {}", e.what()); - printValidateUsage(progName); - return 1; - } - - // if the help flag is set, print the validate usage information and exit - if (validateArgs.help) { - printValidateUsage(progName); - return 0; - } - // validate the JSON file or the JSON string from stdin - try { - if (validateArgs.jsonFile.has_value()) { - std::ignore = sc::readJSON(validateArgs.jsonFile.value()); - } else { - std::ignore = sc::readJSON(std::cin); - } - } catch (const std::exception& e) { - SPDLOG_ERROR("Error validating JSON: {}", e.what()); - return 1; - } - return 0; -} - -/** - * @brief Generates a C++ header from a device JSON specification (file or - * stdin). - * - * Parses generate-specific arguments from argVec starting at index i, reads a - * sc::Device from the specified JSON file or from stdin, and writes a header to - * the specified output file or to stdout. - * - * @param progName Program executable name (used for usage/help output). - * @param argVec Full command-line argument vector. - * @param i Index in argVec of the first generate sub-command argument. - * @return int 0 on success, 1 on error. - */ -auto executeGenerateCommand(const std::string& progName, - const std::vector& argVec, - const size_t i) -> int { - GenerateArguments generateArgs; - // parse the rest of the command line arguments for the generate command - try { - generateArgs = parseGenerateArguments(argVec, i); - } catch (const std::exception& e) { - SPDLOG_ERROR("Error parsing generate arguments: {}", e.what()); - printGenerateUsage(progName); - return 1; - } - // if the help flag is set, print the 'generate' usage information and exit - if (generateArgs.help) { - printGenerateUsage(progName); - return 0; - } - // generate the header file from the JSON specification - try { - sc::Device device; - // read the JSON file or the JSON string from stdin - if (generateArgs.jsonFile.has_value()) { - device = sc::readJSON(generateArgs.jsonFile.value()); - } else { - device = sc::readJSON(std::cin); - } - // write the header file to the output file or stdout - if (generateArgs.outputFile.has_value()) { - sc::writeHeader(device, generateArgs.outputFile.value()); - } else { - sc::writeHeader(device, std::cout); - } - } catch (const std::exception& e) { - SPDLOG_ERROR("Error generating header file: {}", e.what()); - return 1; - } - return 0; -} -} // namespace - -/** - * @brief Parses command-line arguments, dispatches the selected subcommand - * (schema, validate, generate), and performs the requested operation. - * - * The function handles global flags (help, version), prints usage/version - * information when requested, and forwards remaining arguments to the - * appropriate subcommand executor which performs IO and error handling. - * - * @param argc Number of command-line arguments. - * @param argv Array of command-line argument strings. - * @return int Exit code: `0` on success, `1` on error. - */ -int main(int argc, char* argv[]) { - std::vector argVec; - std::pair parsedArgs; - // `main` functions should not throw exceptions. Apparently, the - // initialization of a vector can throw exceptions, so we catch them here. - try { - argVec.reserve(static_cast(argc)); - for (const auto& arg : std::span(argv, static_cast(argc))) { - argVec.emplace_back(arg); - } - } catch (std::exception& e) { - SPDLOG_ERROR("Error parsing arguments into vector: {}", e.what()); - return 1; - } - // parse the command line arguments up to the first sub-command - try { - parsedArgs = parseArguments(argVec); - } catch (const std::exception& e) { - SPDLOG_ERROR("Error parsing arguments: {}", e.what()); - printUsage(argVec.empty() ? "mqt-core-sc-device-gen" : argVec.front()); - return 1; - } - // unpack the parsed arguments and the index of the first sub-command here - // because structured bindings only work with fresh variables - const auto& [args, i] = parsedArgs; - // print help or version information if requested - if (args.help) { - printUsage(args.programName); - return 0; - } - // if the version flag is set, print the version information and exit - if (args.version) { - printVersion(); - return 0; - } - // if no command is specified, print the usage information - if (!args.command.has_value()) { - printUsage(args.programName); - return 1; - } - switch (*args.command) { - case Command::Schema: - return executeSchemaCommand(args.programName, argVec, i); - case Command::Validate: - return executeValidateCommand(args.programName, argVec, i); - case Command::Generate: - return executeGenerateCommand(args.programName, argVec, i); - } - // LCOV_EXCL_START -#ifdef __GNUC__ // GCC, Clang, ICC - __builtin_unreachable(); -#elif defined(_MSC_VER) // MSVC - __assume(false); -#endif - // LCOV_EXCL_STOP -} diff --git a/src/qdmi/devices/sc/CMakeLists.txt b/src/qdmi/devices/sc/CMakeLists.txt index 22e78bfdb9..6876e67ab9 100644 --- a/src/qdmi/devices/sc/CMakeLists.txt +++ b/src/qdmi/devices/sc/CMakeLists.txt @@ -7,29 +7,27 @@ # Licensed under the MIT License # Set target name -set(TARGET_NAME ${MQT_CORE_TARGET_NAME}-qdmi-sc-device-gen) +set(TARGET_NAME ${MQT_CORE_TARGET_NAME}-qdmi-sc-device-config) # If the target is not already defined if(NOT TARGET ${TARGET_NAME}) - # Add library for device generation - add_mqt_core_library(${TARGET_NAME} ALIAS_NAME QDMIScDeviceGen) + # Add the runtime configuration parser library + add_mqt_core_library(${TARGET_NAME} ALIAS_NAME QDMIScDeviceConfig) # add sources to target - target_sources(${TARGET_NAME} PRIVATE Generator.cpp) + target_sources(${TARGET_NAME} PRIVATE Configuration.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/devices/sc/Generator.hpp) + ${MQT_CORE_INCLUDE_BUILD_DIR}/qdmi/devices/sc/Configuration.hpp) - # Link nlohmann_json and spdlog - target_link_libraries( - ${TARGET_NAME} - PUBLIC nlohmann_json::nlohmann_json - PRIVATE spdlog::spdlog) + target_link_libraries(${TARGET_NAME} PUBLIC nlohmann_json::nlohmann_json) + + list(APPEND MQT_CORE_TARGETS ${TARGET_NAME}) - # place the generated library in a predictable location where the executable can find it + # Place the library in the common output directories. set_target_properties( ${TARGET_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}" @@ -37,38 +35,6 @@ if(NOT TARGET ${TARGET_NAME}) RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR}") endif() -# Set target name -set(TARGET_NAME mqt-core-qdmi-sc-device-generator) - -# If the target is not already defined -if(NOT TARGET ${TARGET_NAME}) - # Add executable for device generation - add_executable(${TARGET_NAME}) - - # add sources to target - target_sources(${TARGET_NAME} PRIVATE App.cpp) - - # Link Generator library - target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQDMIScDeviceGen MQT::ProjectOptions - MQT::ProjectWarnings spdlog::spdlog) - - # set versioning information - set_target_properties(${TARGET_NAME} PROPERTIES VERSION ${PROJECT_VERSION} EXPORT_NAME - CoreScDeviceGen) - # place in the bin directory - set_target_properties(${TARGET_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY - "${CMAKE_BINARY_DIR}/bin") - - # set c++ standard - target_compile_features(${TARGET_NAME} PRIVATE cxx_std_20) - - # Make version available - target_compile_definitions(${TARGET_NAME} PRIVATE MQT_CORE_VERSION="${MQT_CORE_VERSION}") - - # Create an alias for the target - add_executable(MQT::CoreQDMIScDeviceGenerator ALIAS ${TARGET_NAME}) -endif() - # Set target name set(TARGET_NAME ${MQT_CORE_TARGET_NAME}-qdmi-sc-device) @@ -78,24 +44,8 @@ set(QDMI_PREFIX "MQT_SC") # If the target is not already defined if(NOT TARGET ${TARGET_NAME}) - # Set paths - set(JSON_FILE ${PROJECT_SOURCE_DIR}/json/sc/device.json) - set(DEVICE_HDR ${CMAKE_CURRENT_BINARY_DIR}/include/qdmi/sc/DeviceMemberInitializers.hpp) - - # Create include directory - file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/include/qdmi/sc) - - # Generate definitions for device - add_custom_command( - OUTPUT ${DEVICE_HDR} - COMMAND MQT::CoreQDMIScDeviceGenerator ARGS generate --output ${DEVICE_HDR} ${JSON_FILE} - DEPENDS ${JSON_FILE} MQT::CoreQDMIScDeviceGenerator - COMMENT "Generating C++ header from ${JSON_FILE}") - add_custom_target(generate_qdmi_sc_device_header DEPENDS ${DEVICE_HDR}) - # Add library add_mqt_core_library(${TARGET_NAME} FORCE_SHARED HIDDEN_VISIBILITY ALIAS_NAME QDMIScDevice) - add_dependencies(${TARGET_NAME} generate_qdmi_sc_device_header) # Generate prefixed QDMI headers generate_prefixed_qdmi_headers(${QDMI_PREFIX}) @@ -113,14 +63,21 @@ if(NOT TARGET ${TARGET_NAME}) ${MQT_CORE_INCLUDE_BUILD_DIR} ${CMAKE_CURRENT_BINARY_DIR}/include FILES - ${DEVICE_HDR} ${MQT_CORE_INCLUDE_BUILD_DIR}/qdmi/devices/sc/Device.hpp ${QDMI_HDRS}) # add link libraries - target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQDMICommon spdlog::spdlog) + target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQDMICommon MQT::CoreQDMIScDeviceConfig + spdlog::spdlog) - mqt_configure_qdmi_device(${TARGET_NAME} ID mqt.sc.default PREFIX ${QDMI_PREFIX}) + mqt_configure_qdmi_device( + ${TARGET_NAME} + ID + mqt.sc.default + PREFIX + ${QDMI_PREFIX} + RUNTIME_FILES + ${PROJECT_SOURCE_DIR}/json/sc/mqt-core-qdmi-sc-device.json) list(APPEND MQT_CORE_TARGETS ${TARGET_NAME}) endif() diff --git a/src/qdmi/devices/sc/Configuration.cpp b/src/qdmi/devices/sc/Configuration.cpp new file mode 100644 index 0000000000..9dc81f05bf --- /dev/null +++ b/src/qdmi/devices/sc/Configuration.cpp @@ -0,0 +1,403 @@ +/* + * 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 Configuration.cpp + * @brief Runtime configuration parsing for superconducting QDMI devices. + */ + +#include "qdmi/devices/sc/Configuration.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace sc { +namespace { +using Json = nlohmann::json; + +// Keep all schema diagnostics anchored to the selected configuration source. +[[noreturn]] void fail(const std::string_view source, + const std::string_view pointer, + const std::string_view message) { + throw std::invalid_argument(std::string(source) + ":" + std::string(pointer) + + " " + std::string(message)); +} + +void object(const Json& value, const std::string_view source, + const std::string_view pointer) { + if (!value.is_object()) { + fail(source, pointer, "must be an object"); + } +} + +void keys(const Json& value, + const std::initializer_list known, + const std::string_view source, const std::string_view pointer) { + const std::set allowed(known); + for (const auto& [key, unused] : value.items()) { + static_cast(unused); + if (!allowed.contains(key)) { + fail(source, pointer, "contains unknown key '" + key + "'"); + } + } +} + +template +[[nodiscard]] T required(const Json& value, const std::string& key, + const std::string_view source, + const std::string& pointer) { + const auto found = value.find(key); + if (found == value.end()) { + fail(source, pointer + "/" + key, "is required"); + } + if constexpr (std::is_same_v) { + if (!found->is_number_unsigned() && + (!found->is_number_integer() || found->get() < 0)) { + fail(source, pointer + "/" + key, "must be a non-negative integer"); + } + } + try { + return found->get(); + } catch (const Json::exception&) { + fail(source, pointer + "/" + key, "has an invalid type"); + } +} + +template +[[nodiscard]] std::optional +optional(const Json& value, const std::string& key, + const std::string_view source, const std::string& pointer) { + const auto found = value.find(key); + if (found == value.end()) { + return std::nullopt; + } + if constexpr (std::is_same_v) { + if (!found->is_number_unsigned() && + (!found->is_number_integer() || found->get() < 0)) { + fail(source, pointer + "/" + key, "must be a non-negative integer"); + } + } + try { + return found->get(); + } catch (const Json::exception&) { + fail(source, pointer + "/" + key, "has an invalid type"); + } +} + +[[nodiscard]] std::vector indices(const Json& value, + const std::string_view source, + const std::string& pointer) { + if (!value.is_array()) { + fail(source, pointer, "must be an array of unsigned integers"); + } + std::vector result; + result.reserve(value.size()); + for (size_t i = 0; i < value.size(); ++i) { + const auto& item = value[i]; + if (!item.is_number_unsigned() && + (!item.is_number_integer() || item.get() < 0)) { + fail(source, pointer + "/" + std::to_string(i), + "must be a non-negative integer"); + } + result.emplace_back(item.get()); + } + return result; +} + +[[nodiscard]] Device::QubitCalibration +calibration(const Json& value, const std::string_view source, + const std::string& pointer) { + object(value, source, pointer); + keys(value, {"t1", "t2"}, source, pointer); + auto result = Device::QubitCalibration{ + .t1 = optional(value, "t1", source, pointer), + .t2 = optional(value, "t2", source, pointer)}; + if ((result.t1 && *result.t1 == 0) || (result.t2 && *result.t2 == 0)) { + fail(source, pointer, "t1 and t2 must be positive when present"); + } + return result; +} + +void validateFidelity(const std::optional& fidelity, + const std::string_view source, + const std::string& pointer) { + if (fidelity && + (!std::isfinite(*fidelity) || *fidelity < 0. || *fidelity > 1.)) { + fail(source, pointer, "must be finite and in [0, 1]"); + } +} + +[[nodiscard]] Device parse(const Json& root, const std::string_view source) { + object(root, source, "$"); + keys(root, + {"schema-version", "name", "numQubits", "durationUnit", + "qubitProperties", "couplings", "operations"}, + source, "$"); + for (const auto* const key : + {"schema-version", "name", "numQubits", "durationUnit", + "qubitProperties", "couplings", "operations"}) { + if (!root.contains(key)) { + fail(source, "$/" + std::string(key), "is required"); + } + } + Device result; + result.schemaVersion = + required(root, "schema-version", source, "$"); + if (result.schemaVersion != 1) { + fail(source, "$/schema-version", "must be 1"); + } + result.name = required(root, "name", source, "$"); + if (result.name.empty()) { + fail(source, "$/name", "must not be empty"); + } + result.numQubits = required(root, "numQubits", source, "$"); + if (result.numQubits == 0 || + result.numQubits > std::vector{}.max_size()) { + fail(source, "$/numQubits", "must be positive and representable"); + } + + const auto& unit = root.at("durationUnit"); + object(unit, source, "$/durationUnit"); + keys(unit, {"unit", "scaleFactor"}, source, "$/durationUnit"); + result.durationUnit.unit = + required(unit, "unit", source, "$/durationUnit"); + result.durationUnit.scaleFactor = + required(unit, "scaleFactor", source, "$/durationUnit"); + constexpr std::array supportedUnits{"s", "ms", "us", "ns"}; + if (std::ranges::find(supportedUnits, result.durationUnit.unit) == + supportedUnits.end() || + !std::isfinite(result.durationUnit.scaleFactor) || + result.durationUnit.scaleFactor <= 0.) { + fail(source, "$/durationUnit", + "must use s, ms, us, or ns and a positive finite scaleFactor"); + } + + { + const auto& properties = root.at("qubitProperties"); + object(properties, source, "$/qubitProperties"); + keys(properties, {"defaults", "overrides"}, source, "$/qubitProperties"); + for (const auto* const key : {"defaults", "overrides"}) { + if (!properties.contains(key)) { + fail(source, "$/qubitProperties/" + std::string(key), "is required"); + } + } + result.qubitProperties.defaults = calibration( + properties.at("defaults"), source, "$/qubitProperties/defaults"); + { + const auto& overrides = properties.at("overrides"); + if (!overrides.is_array()) { + fail(source, "$/qubitProperties/overrides", "must be an array"); + } + std::set overridden; + for (size_t i = 0; i < overrides.size(); ++i) { + const auto pointer = "$/qubitProperties/overrides/" + std::to_string(i); + const auto& value = overrides[i]; + object(value, source, pointer); + keys(value, {"qubit", "t1", "t2"}, source, pointer); + Device::QubitOverride entry; + entry.qubit = required(value, "qubit", source, pointer); + entry.t1 = optional(value, "t1", source, pointer); + entry.t2 = optional(value, "t2", source, pointer); + if (entry.qubit >= result.numQubits || (entry.t1 && *entry.t1 == 0) || + (entry.t2 && *entry.t2 == 0) || (!entry.t1 && !entry.t2) || + !overridden.emplace(entry.qubit).second) { + fail(source, pointer, + "must select one unique valid qubit and override t1 or t2"); + } + result.qubitProperties.overrides.emplace_back(entry); + } + } + } + + const auto& couplings = root.at("couplings"); + if (!couplings.is_array()) { + fail(source, "$/couplings", "must be an array"); + } + std::set> uniqueCouplings; + for (size_t i = 0; i < couplings.size(); ++i) { + const auto pointer = "$/couplings/" + std::to_string(i); + if (!couplings[i].is_array() || couplings[i].size() != 2) { + fail(source, pointer, "must contain exactly two qubit indices"); + } + std::pair coupling; + const auto parsed = indices(couplings[i], source, pointer); + coupling = {parsed[0], parsed[1]}; + if (coupling.first >= result.numQubits || + coupling.second >= result.numQubits || + coupling.first == coupling.second || + !uniqueCouplings.emplace(coupling).second) { + fail(source, pointer, "must be a unique, non-self tuple of valid qubits"); + } + result.couplings.emplace_back(coupling); + } + + const auto& operations = root.at("operations"); + if (!operations.is_array()) { + fail(source, "$/operations", "must be an array"); + } + std::set names; + for (size_t i = 0; i < operations.size(); ++i) { + const auto pointer = "$/operations/" + std::to_string(i); + const auto& value = operations[i]; + object(value, source, pointer); + keys(value, + {"name", "numParameters", "numQubits", "sites", "duration", "fidelity", + "siteOverrides"}, + source, pointer); + Device::Operation operation; + operation.name = required(value, "name", source, pointer); + operation.numParameters = + required(value, "numParameters", source, pointer); + operation.numQubits = + required(value, "numQubits", source, pointer); + operation.duration = optional(value, "duration", source, pointer); + operation.fidelity = optional(value, "fidelity", source, pointer); + validateFidelity(operation.fidelity, source, pointer + "/fidelity"); + if (operation.name.empty() || operation.numQubits == 0 || + operation.numQubits > result.numQubits || + operation.numParameters > std::numeric_limits::max() || + !names.emplace(operation.name).second) { + fail(source, pointer, + "must have a unique non-empty name and representable counts"); + } + if (const auto sites = value.find("sites"); sites != value.end()) { + if (!sites->is_array()) { + fail(source, pointer + "/sites", "must be an array"); + } + operation.sites.emplace(); + std::set> uniqueSites; + for (size_t j = 0; j < sites->size(); ++j) { + auto tuple = indices((*sites)[j], source, + pointer + "/sites/" + std::to_string(j)); + const auto supportedByTopology = + operation.numQubits != 2 || + (tuple.size() == 2 && + uniqueCouplings.contains(std::pair{tuple[0], tuple[1]})); + if (const std::set tupleSites(tuple.begin(), tuple.end()); + tuple.size() != operation.numQubits || + tupleSites.size() != tuple.size() || + std::ranges::any_of( + tuple, + [&](const auto qubit) { return qubit >= result.numQubits; }) || + !supportedByTopology || !uniqueSites.emplace(tuple).second) { + fail(source, pointer + "/sites/" + std::to_string(j), + "must be a unique tuple matching the operation arity and " + "device connectivity"); + } + operation.sites->emplace_back(std::move(tuple)); + } + } + if (!operation.sites && operation.numQubits > 2) { + fail(source, pointer + "/sites", + "is required for operations with arity greater than two"); + } + if (const auto overrides = value.find("siteOverrides"); + overrides != value.end()) { + if (!overrides->is_array()) { + fail(source, pointer + "/siteOverrides", "must be an array"); + } + std::set> overriddenSites; + for (size_t j = 0; j < overrides->size(); ++j) { + const auto overridePointer = + pointer + "/siteOverrides/" + std::to_string(j); + const auto& overrideJson = (*overrides)[j]; + object(overrideJson, source, overridePointer); + keys(overrideJson, {"sites", "duration", "fidelity"}, source, + overridePointer); + Device::SiteOverride override; + const auto siteValues = overrideJson.find("sites"); + if (siteValues == overrideJson.end()) { + fail(source, overridePointer + "/sites", "is required"); + } + override.sites = + indices(*siteValues, source, overridePointer + "/sites"); + override.duration = optional(overrideJson, "duration", source, + overridePointer); + override.fidelity = + optional(overrideJson, "fidelity", source, overridePointer); + validateFidelity(override.fidelity, source, + overridePointer + "/fidelity"); + const std::set tupleSites(override.sites.begin(), + override.sites.end()); + auto supported = false; + if (override.sites.size() == operation.numQubits) { + if (operation.sites) { + supported = std::ranges::find(*operation.sites, override.sites) != + operation.sites->end(); + } else if (operation.numQubits == 1) { + supported = true; + } else if (operation.numQubits == 2) { + supported = std::ranges::find( + result.couplings, + std::pair{override.sites[0], override.sites[1]}) != + result.couplings.end(); + } + } + if (override.sites.size() != operation.numQubits || + tupleSites.size() != override.sites.size() || + std::ranges::any_of( + override.sites, + [&](const auto qubit) { return qubit >= result.numQubits; }) || + !supported || !overriddenSites.emplace(override.sites).second || + (!override.duration && !override.fidelity)) { + fail(source, overridePointer, + "must be one unique supported tuple and override a valid value"); + } + operation.siteOverrides.emplace_back(std::move(override)); + } + } + result.operations.emplace_back(std::move(operation)); + } + return result; +} +} // namespace + +Device readJSON(const std::string_view json, const std::string_view source) { + try { + return parse(Json::parse(json), source); + } catch (const Json::exception& error) { + throw std::invalid_argument(std::string(source) + + ": invalid JSON: " + error.what()); + } +} + +Device readJSON(std::istream& stream, const std::string_view source) { + const std::string json{std::istreambuf_iterator(stream), + std::istreambuf_iterator()}; + return readJSON(json, source); +} + +Device readJSON(const std::string& path) { + std::ifstream input(path); + if (!input) { + throw std::runtime_error("Failed to open JSON file: " + path); + } + return readJSON(input, path); +} + +} // namespace sc diff --git a/src/qdmi/devices/sc/Device.cpp b/src/qdmi/devices/sc/Device.cpp index 5f236e69d9..873d8bf170 100644 --- a/src/qdmi/devices/sc/Device.cpp +++ b/src/qdmi/devices/sc/Device.cpp @@ -14,86 +14,240 @@ #include "qdmi/devices/sc/Device.hpp" +#include "mqt_sc_qdmi/constants.h" #include "mqt_sc_qdmi/device.h" +#include "mqt_sc_qdmi/types.h" #include "qdmi/common/Common.hpp" -#include "qdmi/sc/DeviceMemberInitializers.hpp" +#include "qdmi/common/DeviceConfiguration.hpp" +#include "qdmi/devices/sc/Configuration.hpp" + +#include #include -#include #include #include #include -#include +#include #include +#include +#include #include -#include +#include +#include #include -#include #include -namespace qdmi::sc { +namespace { +[[nodiscard]] bool +contains(const std::vector>& haystack, + const std::vector& needle) { + return std::ranges::find(haystack, needle) != haystack.end(); +} -Device::Device() { - // NOLINTBEGIN(cppcoreguidelines-prefer-member-initializer) - INITIALIZE_NAME(name_); - INITIALIZE_QUBITSNUM(qubitsNum_); - // NOLINTEND(cppcoreguidelines-prefer-member-initializer) - // NOLINTNEXTLINE(misc-const-correctness) - INITIALIZE_SITES(sites_); - INITIALIZE_COUPLINGMAP(couplingMap_); - INITIALIZE_OPERATIONS(operations_); -} -auto Device::sessionAlloc(MQT_SC_QDMI_Device_Session* session) -> int { - if (session == nullptr) { - return QDMI_ERROR_INVALIDARGUMENT; +[[nodiscard]] std::vector +materializeTuple(const std::vector& indices, + const std::vector& sites) { + std::vector tuple; + tuple.reserve(indices.size()); + for (const auto index : indices) { + if (index >= sites.size()) { + throw std::invalid_argument("operation site index is out of range"); + } + tuple.emplace_back(sites[index]); } - auto uniqueSession = std::make_unique(); - const auto& it = - sessions_.emplace(uniqueSession.get(), std::move(uniqueSession)).first; - // get the key, i.e., the raw pointer to the session from the map iterator - *session = it->first; - return QDMI_SUCCESS; + return tuple; } -auto Device::sessionFree(MQT_SC_QDMI_Device_Session session) -> void { - if (session != nullptr) { - if (const auto& it = sessions_.find(session); it != sessions_.end()) { - sessions_.erase(it); +} // namespace + +int MQT_SC_QDMI_Device_Session_impl_d::init() { + if (status != Status::ALLOCATED) { + return QDMI_ERROR_BADSTATE; + } + int loadStatus = QDMI_SUCCESS; + const auto loaded = qdmi::detail::loadDeviceConfiguration( + inlineConfiguration, fileConfiguration, "MQT_CORE_QDMI_SC_CONFIG_JSON", + "MQT_CORE_QDMI_SC_CONFIG_FILE", "mqt-core-qdmi-sc-device.json", + reinterpret_cast(&MQT_SC_QDMI_device_initialize), + loadStatus); + if (!loaded) { + return loadStatus; + } + try { + const auto configuration = sc::readJSON(loaded->json, loaded->source); + + std::vector> newSiteStorage; + std::vector newSites; + newSiteStorage.reserve(configuration.numQubits); + newSites.reserve(configuration.numQubits); + for (uint64_t id = 0; id < configuration.numQubits; ++id) { + auto site = std::make_unique(); + site->owner = this; + site->id = id; + site->t1 = configuration.qubitProperties.defaults.t1; + site->t2 = configuration.qubitProperties.defaults.t2; + newSites.emplace_back(site.get()); + newSiteStorage.emplace_back(std::move(site)); + } + for (const auto& override : configuration.qubitProperties.overrides) { + auto& site = newSiteStorage.at(override.qubit); + if (override.t1) { + site->t1 = override.t1; + } + if (override.t2) { + site->t2 = override.t2; + } + } + + std::vector> newCouplingMap; + newCouplingMap.reserve(configuration.couplings.size()); + for (const auto& [first, second] : configuration.couplings) { + newCouplingMap.emplace_back(newSites.at(first), newSites.at(second)); + } + + std::vector> + newOperationStorage; + std::vector newOperations; + newOperationStorage.reserve(configuration.operations.size()); + newOperations.reserve(configuration.operations.size()); + for (const auto& operationConfiguration : configuration.operations) { + auto operation = std::make_unique(); + operation->owner = this; + operation->name = operationConfiguration.name; + operation->numParameters = operationConfiguration.numParameters; + operation->numQubits = operationConfiguration.numQubits; + operation->defaults = {.duration = operationConfiguration.duration, + .fidelity = operationConfiguration.fidelity}; + if (operationConfiguration.sites) { + for (const auto& tuple : *operationConfiguration.sites) { + operation->supportedSites.emplace_back( + materializeTuple(tuple, newSites)); + } + } else if (operation->numQubits == 1) { + for (auto* const site : newSites) { + operation->supportedSites.push_back({site}); + } + } else if (operation->numQubits == 2) { + for (const auto& [first, second] : newCouplingMap) { + operation->supportedSites.push_back({first, second}); + } + } + for (const auto& tuple : operation->supportedSites) { + operation->flattenedSites.insert(operation->flattenedSites.end(), + tuple.begin(), tuple.end()); + } + for (const auto& override : operationConfiguration.siteOverrides) { + auto tuple = materializeTuple(override.sites, newSites); + if (!contains(operation->supportedSites, tuple)) { + throw std::invalid_argument( + "operation site override is not a supported tuple"); + } + operation->overrides.emplace_back( + std::move(tuple), + MQT_SC_QDMI_Operation_impl_d::Calibration{ + .duration = override.duration, .fidelity = override.fidelity}); + } + newOperations.emplace_back(operation.get()); + newOperationStorage.emplace_back(std::move(operation)); } + + name = configuration.name; + durationUnit = configuration.durationUnit.unit; + durationScaleFactor = configuration.durationUnit.scaleFactor; + qubitsNum = static_cast(configuration.numQubits); + siteStorage = std::move(newSiteStorage); + sites = std::move(newSites); + couplingMap = std::move(newCouplingMap); + operationStorage = std::move(newOperationStorage); + operations = std::move(newOperations); + status = Status::INITIALIZED; + return QDMI_SUCCESS; + } catch (const std::bad_alloc&) { + SPDLOG_ERROR("Out of memory while initializing SC device from {}", + loaded->source); + return QDMI_ERROR_OUTOFMEM; + } catch (const std::invalid_argument& error) { + SPDLOG_ERROR("Invalid SC device configuration from {}: {}", loaded->source, + error.what()); + return QDMI_ERROR_INVALIDARGUMENT; + } catch (const std::exception& error) { + SPDLOG_ERROR("Failed to initialize SC device from {}: {}", loaded->source, + error.what()); + return QDMI_ERROR_FATAL; + } +} + +int MQT_SC_QDMI_Device_Session_impl_d::setParameter( + const QDMI_Device_Session_Parameter parameter, const size_t size, + const void* value) { + if (parameter == QDMI_DEVICE_SESSION_PARAMETER_MAX || + IS_INVALID_ARGUMENT(parameter, QDMI_DEVICE_SESSION_PARAMETER)) { + return QDMI_ERROR_INVALIDARGUMENT; + } + if (status != Status::ALLOCATED) { + return QDMI_ERROR_BADSTATE; } + return qdmi::detail::setDeviceConfigurationParameter( + parameter, size, value, inlineConfiguration, fileConfiguration); } -auto Device::queryProperty(const QDMI_Device_Property prop, const size_t size, - void* value, size_t* sizeRet) const -> int { + +int MQT_SC_QDMI_Device_Session_impl_d::createDeviceJob( + MQT_SC_QDMI_Device_Job* job) { + if (job == nullptr) { + return QDMI_ERROR_INVALIDARGUMENT; + } + if (status != Status::INITIALIZED) { + return QDMI_ERROR_BADSTATE; + } + auto value = std::make_unique(this); + *job = value.get(); + jobs.emplace(*job, std::move(value)); + return QDMI_SUCCESS; +} + +void MQT_SC_QDMI_Device_Session_impl_d::freeDeviceJob( + MQT_SC_QDMI_Device_Job job) { + jobs.erase(job); +} + +int MQT_SC_QDMI_Device_Session_impl_d::queryDeviceProperty( + const QDMI_Device_Property property, const size_t size, void* value, + size_t* sizeRet) const { + if (status != Status::INITIALIZED) { + return QDMI_ERROR_BADSTATE; + } if ((value != nullptr && size == 0) || - IS_INVALID_ARGUMENT(prop, QDMI_DEVICE_PROPERTY)) { + IS_INVALID_ARGUMENT(property, QDMI_DEVICE_PROPERTY)) { return QDMI_ERROR_INVALIDARGUMENT; } - ADD_STRING_PROPERTY(QDMI_DEVICE_PROPERTY_NAME, name_.c_str(), prop, size, + ADD_STRING_PROPERTY(QDMI_DEVICE_PROPERTY_NAME, name.c_str(), property, size, value, sizeRet) - // NOLINTNEXTLINE(misc-include-cleaner) - ADD_STRING_PROPERTY(QDMI_DEVICE_PROPERTY_VERSION, MQT_CORE_VERSION, prop, - size, value, sizeRet) - // NOLINTNEXTLINE(misc-include-cleaner) - ADD_STRING_PROPERTY(QDMI_DEVICE_PROPERTY_LIBRARYVERSION, QDMI_VERSION, prop, + ADD_STRING_PROPERTY(QDMI_DEVICE_PROPERTY_VERSION, MQT_CORE_VERSION, property, size, value, sizeRet) + ADD_STRING_PROPERTY(QDMI_DEVICE_PROPERTY_LIBRARYVERSION, QDMI_VERSION, + property, size, value, sizeRet) ADD_SINGLE_VALUE_PROPERTY(QDMI_DEVICE_PROPERTY_STATUS, QDMI_Device_Status, - QDMI_DEVICE_STATUS_IDLE, prop, size, value, sizeRet) - ADD_SINGLE_VALUE_PROPERTY(QDMI_DEVICE_PROPERTY_QUBITSNUM, size_t, qubitsNum_, - prop, size, value, sizeRet) - // This device never needs calibration + QDMI_DEVICE_STATUS_IDLE, property, size, value, + sizeRet) + ADD_SINGLE_VALUE_PROPERTY(QDMI_DEVICE_PROPERTY_QUBITSNUM, size_t, qubitsNum, + property, size, value, sizeRet) ADD_SINGLE_VALUE_PROPERTY(QDMI_DEVICE_PROPERTY_NEEDSCALIBRATION, size_t, 0, - prop, size, value, sizeRet) - // This device does not support pulse-level control + property, size, value, sizeRet) ADD_SINGLE_VALUE_PROPERTY( QDMI_DEVICE_PROPERTY_PULSESUPPORT, QDMI_Device_Pulse_Support_Level, - QDMI_DEVICE_PULSE_SUPPORT_LEVEL_NONE, prop, size, value, sizeRet) - ADD_LIST_PROPERTY(QDMI_DEVICE_PROPERTY_SITES, MQT_SC_QDMI_Site, sites_, prop, - size, value, sizeRet) + QDMI_DEVICE_PULSE_SUPPORT_LEVEL_NONE, property, size, value, sizeRet) + ADD_STRING_PROPERTY(QDMI_DEVICE_PROPERTY_DURATIONUNIT, durationUnit.c_str(), + property, size, value, sizeRet) + ADD_SINGLE_VALUE_PROPERTY(QDMI_DEVICE_PROPERTY_DURATIONSCALEFACTOR, double, + durationScaleFactor, property, size, value, sizeRet) + ADD_LIST_PROPERTY(QDMI_DEVICE_PROPERTY_SITES, MQT_SC_QDMI_Site, sites, + property, size, value, sizeRet) #define SITE_PAIR std::pair - ADD_LIST_PROPERTY(QDMI_DEVICE_PROPERTY_COUPLINGMAP, SITE_PAIR, couplingMap_, - prop, size, value, sizeRet) + ADD_LIST_PROPERTY(QDMI_DEVICE_PROPERTY_COUPLINGMAP, SITE_PAIR, couplingMap, + property, size, value, sizeRet) +#undef SITE_PAIR ADD_LIST_PROPERTY(QDMI_DEVICE_PROPERTY_OPERATIONS, MQT_SC_QDMI_Operation, - operations_, prop, size, value, sizeRet) - if (prop == QDMI_DEVICE_PROPERTY_SUPPORTEDPROGRAMFORMATS) { + operations, property, size, value, sizeRet) + if (property == QDMI_DEVICE_PROPERTY_SUPPORTEDPROGRAMFORMATS) { if (value != nullptr && size > 0) { return QDMI_ERROR_INVALIDARGUMENT; } @@ -104,335 +258,208 @@ auto Device::queryProperty(const QDMI_Device_Property prop, const size_t size, } return QDMI_ERROR_NOTSUPPORTED; } -} // namespace qdmi::sc -auto MQT_SC_QDMI_Device_Session_impl_d::init() -> int { - if (status_ != Status::ALLOCATED) { +int MQT_SC_QDMI_Device_Session_impl_d::querySiteProperty( + MQT_SC_QDMI_Site site, const QDMI_Site_Property property, const size_t size, + void* value, size_t* sizeRet) const { + if (status != Status::INITIALIZED) { return QDMI_ERROR_BADSTATE; } - status_ = Status::INITIALIZED; - return QDMI_SUCCESS; -} -auto MQT_SC_QDMI_Device_Session_impl_d::setParameter( - QDMI_Device_Session_Parameter param, const size_t size, - const void* value) const -> int { - if ((value != nullptr && size == 0) || - IS_INVALID_ARGUMENT(param, QDMI_DEVICE_SESSION_PARAMETER)) { + if (site == nullptr || site->owner != this) { return QDMI_ERROR_INVALIDARGUMENT; } - if (status_ != Status::ALLOCATED) { + return site->queryProperty(property, size, value, sizeRet); +} + +int MQT_SC_QDMI_Device_Session_impl_d::queryOperationProperty( + MQT_SC_QDMI_Operation operation, const size_t numSites, + const MQT_SC_QDMI_Site* suppliedSites, const size_t numParams, + const double* params, const QDMI_Operation_Property property, + const size_t size, void* value, size_t* sizeRet) const { + if (status != Status::INITIALIZED) { return QDMI_ERROR_BADSTATE; } - return QDMI_ERROR_NOTSUPPORTED; -} -auto MQT_SC_QDMI_Device_Session_impl_d::createDeviceJob( - // NOLINTNEXTLINE(readability-non-const-parameter) - MQT_SC_QDMI_Device_Job* job) -> int { - if (job == nullptr) { + if (operation == nullptr || operation->owner != this) { return QDMI_ERROR_INVALIDARGUMENT; } - if (status_ != Status::INITIALIZED) { - return QDMI_ERROR_BADSTATE; + if (suppliedSites != nullptr) { + for (auto* const site : std::span{suppliedSites, numSites}) { + if (site == nullptr || site->owner != this) { + return QDMI_ERROR_INVALIDARGUMENT; + } + } } - auto uniqueJob = std::make_unique(this); - *job = jobs_.emplace(uniqueJob.get(), std::move(uniqueJob)).first->first; - return QDMI_SUCCESS; + return operation->queryProperty(numSites, suppliedSites, numParams, params, + property, size, value, sizeRet); } -auto MQT_SC_QDMI_Device_Session_impl_d::freeDeviceJob( - MQT_SC_QDMI_Device_Job job) -> void { - if (job != nullptr) { - jobs_.erase(job); + +int MQT_SC_QDMI_Site_impl_d::queryProperty(const QDMI_Site_Property property, + const size_t size, void* value, + size_t* sizeRet) const { + if ((value != nullptr && size == 0) || + IS_INVALID_ARGUMENT(property, QDMI_SITE_PROPERTY)) { + return QDMI_ERROR_INVALIDARGUMENT; } -} -auto MQT_SC_QDMI_Device_Session_impl_d::queryDeviceProperty( - const QDMI_Device_Property prop, const size_t size, void* value, - size_t* sizeRet) const -> int { - if (status_ != Status::INITIALIZED) { - return QDMI_ERROR_BADSTATE; + ADD_SINGLE_VALUE_PROPERTY(QDMI_SITE_PROPERTY_INDEX, uint64_t, id, property, + size, value, sizeRet) + if (property == QDMI_SITE_PROPERTY_T1) { + if (!t1) { + return QDMI_ERROR_NOTSUPPORTED; + } + ADD_SINGLE_VALUE_PROPERTY(QDMI_SITE_PROPERTY_T1, uint64_t, *t1, property, + size, value, sizeRet) + } + if (property == QDMI_SITE_PROPERTY_T2) { + if (!t2) { + return QDMI_ERROR_NOTSUPPORTED; + } + ADD_SINGLE_VALUE_PROPERTY(QDMI_SITE_PROPERTY_T2, uint64_t, *t2, property, + size, value, sizeRet) } - return qdmi::sc::Device::get().queryProperty(prop, size, value, sizeRet); + return QDMI_ERROR_NOTSUPPORTED; } -auto MQT_SC_QDMI_Device_Session_impl_d::querySiteProperty( - MQT_SC_QDMI_Site site, const QDMI_Site_Property prop, const size_t size, - void* value, size_t* sizeRet) const -> int { - if (site == nullptr) { + +int MQT_SC_QDMI_Operation_impl_d::queryProperty( + const size_t numSites, const MQT_SC_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 { + if ((sites == nullptr) != (numSites == 0) || + (params == nullptr) != (numParams == 0) || + (value != nullptr && size == 0) || + IS_INVALID_ARGUMENT(property, QDMI_OPERATION_PROPERTY)) { return QDMI_ERROR_INVALIDARGUMENT; } - if (status_ != Status::INITIALIZED) { - return QDMI_ERROR_BADSTATE; + std::vector tuple; + if (sites != nullptr) { + if (numSites != numQubits) { + return QDMI_ERROR_INVALIDARGUMENT; + } + const std::span suppliedSites{sites, numSites}; + tuple.assign(suppliedSites.begin(), suppliedSites.end()); + if (!contains(supportedSites, tuple)) { + return QDMI_ERROR_NOTSUPPORTED; + } } - return site->queryProperty(prop, size, value, sizeRet); -} -auto MQT_SC_QDMI_Device_Session_impl_d::queryOperationProperty( - MQT_SC_QDMI_Operation operation, const size_t numSites, - const MQT_SC_QDMI_Site* sites, const size_t numParams, const double* params, - const QDMI_Operation_Property prop, const size_t size, void* value, - size_t* sizeRet) const -> int { - if (operation == nullptr) { + if (params != nullptr && numParams != numParameters) { return QDMI_ERROR_INVALIDARGUMENT; } - if (status_ != Status::INITIALIZED) { - return QDMI_ERROR_BADSTATE; + ADD_STRING_PROPERTY(QDMI_OPERATION_PROPERTY_NAME, name.c_str(), property, + size, value, sizeRet) + ADD_SINGLE_VALUE_PROPERTY(QDMI_OPERATION_PROPERTY_PARAMETERSNUM, size_t, + numParameters, property, size, value, sizeRet) + ADD_SINGLE_VALUE_PROPERTY(QDMI_OPERATION_PROPERTY_QUBITSNUM, size_t, + numQubits, property, size, value, sizeRet) + ADD_LIST_PROPERTY(QDMI_OPERATION_PROPERTY_SITES, MQT_SC_QDMI_Site, + flattenedSites, property, size, value, sizeRet) + const auto calibration = [&]() -> Calibration { + if (!tuple.empty()) { + if (const auto found = std::ranges::find( + overrides, tuple, + &std::pair, Calibration>::first); + found != overrides.end()) { + return {.duration = found->second.duration ? found->second.duration + : defaults.duration, + .fidelity = found->second.fidelity ? found->second.fidelity + : defaults.fidelity}; + } + } + return defaults; + }(); + if (property == QDMI_OPERATION_PROPERTY_DURATION) { + if (!calibration.duration) { + return QDMI_ERROR_NOTSUPPORTED; + } + ADD_SINGLE_VALUE_PROPERTY(QDMI_OPERATION_PROPERTY_DURATION, uint64_t, + *calibration.duration, property, size, value, + sizeRet) } - return operation->queryProperty(numSites, sites, numParams, params, prop, - size, value, sizeRet); -} -auto MQT_SC_QDMI_Device_Job_impl_d::free() -> void { - session_->freeDeviceJob(this); + if (property == QDMI_OPERATION_PROPERTY_FIDELITY) { + if (!calibration.fidelity) { + return QDMI_ERROR_NOTSUPPORTED; + } + ADD_SINGLE_VALUE_PROPERTY(QDMI_OPERATION_PROPERTY_FIDELITY, double, + *calibration.fidelity, property, size, value, + sizeRet) + } + return QDMI_ERROR_NOTSUPPORTED; } + +void MQT_SC_QDMI_Device_Job_impl_d::free() { session->freeDeviceJob(this); } // NOLINTNEXTLINE(readability-convert-member-functions-to-static) -auto MQT_SC_QDMI_Device_Job_impl_d::setParameter( - const QDMI_Device_Job_Parameter param, const size_t size, const void* value) - -> int { +int MQT_SC_QDMI_Device_Job_impl_d::setParameter( + const QDMI_Device_Job_Parameter parameter, const size_t size, + const void* value) { if ((value != nullptr && size == 0) || - IS_INVALID_ARGUMENT(param, QDMI_DEVICE_JOB_PARAMETER)) { + IS_INVALID_ARGUMENT(parameter, QDMI_DEVICE_JOB_PARAMETER)) { return QDMI_ERROR_INVALIDARGUMENT; } return QDMI_ERROR_NOTSUPPORTED; } // NOLINTNEXTLINE(readability-convert-member-functions-to-static) -auto MQT_SC_QDMI_Device_Job_impl_d::queryProperty( - // NOLINTNEXTLINE(readability-non-const-parameter) - const QDMI_Device_Job_Property prop, const size_t size, void* value, - [[maybe_unused]] size_t* sizeRet) -> int { +int MQT_SC_QDMI_Device_Job_impl_d::queryProperty( + const QDMI_Device_Job_Property property, const size_t size, void* value, + size_t* /*sizeRet*/) { if ((value != nullptr && size == 0) || - IS_INVALID_ARGUMENT(prop, QDMI_DEVICE_JOB_PROPERTY)) { + IS_INVALID_ARGUMENT(property, QDMI_DEVICE_JOB_PROPERTY)) { return QDMI_ERROR_INVALIDARGUMENT; } return QDMI_ERROR_NOTSUPPORTED; } // NOLINTNEXTLINE(readability-convert-member-functions-to-static) -auto MQT_SC_QDMI_Device_Job_impl_d::submit() -> int { - return QDMI_ERROR_NOTSUPPORTED; -} +int MQT_SC_QDMI_Device_Job_impl_d::submit() { return QDMI_ERROR_NOTSUPPORTED; } // NOLINTNEXTLINE(readability-convert-member-functions-to-static) -auto MQT_SC_QDMI_Device_Job_impl_d::cancel() -> int { - return QDMI_ERROR_NOTSUPPORTED; -} -// NOLINTNEXTLINE(readability-convert-member-functions-to-static,readability-non-const-parameter) -auto MQT_SC_QDMI_Device_Job_impl_d::check(QDMI_Job_Status* status) -> int { - if (status == nullptr) { - return QDMI_ERROR_INVALIDARGUMENT; - } - return QDMI_ERROR_NOTSUPPORTED; +int MQT_SC_QDMI_Device_Job_impl_d::cancel() { return QDMI_ERROR_NOTSUPPORTED; } +// NOLINTNEXTLINE(readability-non-const-parameter,readability-convert-member-functions-to-static) +int MQT_SC_QDMI_Device_Job_impl_d::check(QDMI_Job_Status* status) { + return status == nullptr ? QDMI_ERROR_INVALIDARGUMENT + : QDMI_ERROR_NOTSUPPORTED; } // NOLINTNEXTLINE(readability-convert-member-functions-to-static) -auto MQT_SC_QDMI_Device_Job_impl_d::wait([[maybe_unused]] const size_t timeout) - -> int { +int MQT_SC_QDMI_Device_Job_impl_d::wait(size_t /*timeout*/) { return QDMI_ERROR_NOTSUPPORTED; } // NOLINTNEXTLINE(readability-convert-member-functions-to-static) -auto MQT_SC_QDMI_Device_Job_impl_d::getResults( - QDMI_Job_Result result, - // NOLINTNEXTLINE(readability-non-const-parameter) - const size_t size, void* data, [[maybe_unused]] size_t* sizeRet) -> int { +int MQT_SC_QDMI_Device_Job_impl_d::getResults(const QDMI_Job_Result result, + const size_t size, void* data, + size_t* /*sizeRet*/) { if ((data != nullptr && size == 0) || IS_INVALID_ARGUMENT(result, QDMI_JOB_RESULT)) { return QDMI_ERROR_INVALIDARGUMENT; } return QDMI_ERROR_NOTSUPPORTED; } -auto MQT_SC_QDMI_Site_impl_d::makeUniqueSite(const uint64_t id) - -> std::unique_ptr { - const MQT_SC_QDMI_Site_impl_d site(id); - return std::make_unique(site); -} -auto MQT_SC_QDMI_Site_impl_d::queryProperty(const QDMI_Site_Property prop, - const size_t size, void* value, - size_t* sizeRet) const -> int { - if ((value != nullptr && size == 0) || - IS_INVALID_ARGUMENT(prop, QDMI_SITE_PROPERTY)) { - return QDMI_ERROR_INVALIDARGUMENT; - } - ADD_SINGLE_VALUE_PROPERTY(QDMI_SITE_PROPERTY_INDEX, uint64_t, id_, prop, size, - value, sizeRet) - return QDMI_ERROR_NOTSUPPORTED; -} -auto MQT_SC_QDMI_Operation_impl_d::sortSites() -> void { - std::visit( - [](auto& sites) { - using T = std::decay_t; - if constexpr (std::is_same_v>) { - // Single-qubit: sort flat list by pointer address - std::ranges::sort(sites, std::less{}); - } else if constexpr (std::is_same_v< - T, std::vector>>) { - // Two-qubit: normalize each pair (first < second) - // Use std::less for proper total order (pointer comparison with - // operator> invokes undefined behavior) - std::ranges::for_each(sites, [](auto& p) { - if (std::less{}(p.second, p.first)) { - std::swap(p.first, p.second); - } - }); - std::ranges::sort(sites); - } - // more cases go here if needed in the future - }, - supportedSites_); -} -MQT_SC_QDMI_Operation_impl_d::MQT_SC_QDMI_Operation_impl_d( - std::string name, const size_t numParameters, - const std::vector& sites) - : name_(std::move(name)), numParameters_(numParameters), numQubits_(1), - supportedSites_(sites) { - sortSites(); -} -MQT_SC_QDMI_Operation_impl_d::MQT_SC_QDMI_Operation_impl_d( - std::string name, const size_t numParameters, - const std::vector>& sites) - : name_(std::move(name)), numParameters_(numParameters), numQubits_(2), - supportedSites_(sites) { - sortSites(); -} -auto MQT_SC_QDMI_Operation_impl_d::makeUniqueSingleQubit( - std::string name, const size_t numParameters, - const std::vector& sites) - -> std::unique_ptr { - const MQT_SC_QDMI_Operation_impl_d op(std::move(name), numParameters, sites); - return std::make_unique(op); -} -auto MQT_SC_QDMI_Operation_impl_d::makeUniqueTwoQubit( - std::string name, const size_t numParameters, - const std::vector>& sites) - -> std::unique_ptr { - const MQT_SC_QDMI_Operation_impl_d op(std::move(name), numParameters, sites); - return std::make_unique(op); -} -auto MQT_SC_QDMI_Operation_impl_d::queryProperty( - const size_t numSites, const MQT_SC_QDMI_Site* sites, - const size_t numParams, const double* params, - const QDMI_Operation_Property prop, const size_t size, void* value, - size_t* sizeRet) const -> int { - if ((sites != nullptr && numSites == 0) || - (params != nullptr && numParams == 0) || - (value != nullptr && size == 0) || - IS_INVALID_ARGUMENT(prop, QDMI_OPERATION_PROPERTY)) { - return QDMI_ERROR_INVALIDARGUMENT; - } - if (sites != nullptr) { - // If numQubits_ == 1 or isZoned_ == true - if (numSites == 1) { - // If the (single) site is not supported, return with an error - const bool found = std::visit( - [sites](const S& storedSites) -> bool { - using T = std::decay_t; - if constexpr (std::is_same_v>) { - return std::ranges::binary_search(storedSites, *sites, - std::less{}); - } - return false; // Wrong variant type - }, - supportedSites_); - if (!found) { - return QDMI_ERROR_NOTSUPPORTED; - } - } else if (numSites == 2) { - // NOLINTBEGIN(cppcoreguidelines-pro-bounds-pointer-arithmetic) - const std::pair needle = std::less{}(sites[0], sites[1]) - ? std::make_pair(sites[0], sites[1]) - : std::make_pair(sites[1], sites[0]); - // NOLINTEND(cppcoreguidelines-pro-bounds-pointer-arithmetic) - // if the pair of sites is not supported, return with an error - const bool found = std::visit( - [&needle](const S& storedSites) -> bool { - using T = std::decay_t; - if constexpr (std::is_same_v< - T, std::vector>>) { - return std::ranges::binary_search(storedSites, needle); - } - return false; // Wrong variant type - }, - supportedSites_); - if (!found) { - return QDMI_ERROR_NOTSUPPORTED; - } - } // this device does not support operations with more than two qubits - } - ADD_STRING_PROPERTY(QDMI_OPERATION_PROPERTY_NAME, name_.c_str(), prop, size, - value, sizeRet) - ADD_SINGLE_VALUE_PROPERTY(QDMI_OPERATION_PROPERTY_PARAMETERSNUM, size_t, - numParameters_, prop, size, value, sizeRet) - ADD_SINGLE_VALUE_PROPERTY(QDMI_OPERATION_PROPERTY_QUBITSNUM, size_t, - numQubits_, prop, size, value, sizeRet) - if (prop == QDMI_OPERATION_PROPERTY_SITES) { - return std::visit( - [&](const S& storedSites) -> int { - using T = std::decay_t; - if constexpr (std::is_same_v>) { - // Single-qubit: return flat array - ADD_LIST_PROPERTY(QDMI_OPERATION_PROPERTY_SITES, MQT_SC_QDMI_Site, - storedSites, prop, size, value, sizeRet) - } else if constexpr (std::is_same_v< - T, - std::vector>>) { - // Ensure std::pair has standard layout and expected size - static_assert(std::is_standard_layout_v< - std::pair>); - static_assert( - sizeof(std::pair) == - 2 * sizeof(MQT_SC_QDMI_Site)); - // Two-qubit: reinterpret as flat array of sites using std::span - // std::pair has standard layout, so the memory layout of - // vector> is equivalent to Site[2*N] - const auto flatView = std::span( - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) - reinterpret_cast(storedSites.data()), - storedSites.size() * 2); - ADD_LIST_PROPERTY(QDMI_OPERATION_PROPERTY_SITES, MQT_SC_QDMI_Site, - flatView, prop, size, value, sizeRet) - } - // more cases go here if needed in the future - return QDMI_ERROR_NOTSUPPORTED; - }, - supportedSites_); - } - return QDMI_ERROR_NOTSUPPORTED; -} - -int MQT_SC_QDMI_device_initialize() { - // ensure the singleton is initialized - std::ignore = qdmi::sc::Device::get(); - return QDMI_SUCCESS; -} +int MQT_SC_QDMI_device_initialize() { return QDMI_SUCCESS; } int MQT_SC_QDMI_device_finalize() { return QDMI_SUCCESS; } - int MQT_SC_QDMI_device_session_alloc(MQT_SC_QDMI_Device_Session* session) { - return qdmi::sc::Device::get().sessionAlloc(session); -} - -int MQT_SC_QDMI_device_session_init(MQT_SC_QDMI_Device_Session session) { if (session == nullptr) { return QDMI_ERROR_INVALIDARGUMENT; } - return session->init(); + // QDMI transfers ownership through its opaque C handle. + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + *session = new (std::nothrow) MQT_SC_QDMI_Device_Session_impl_d; + return *session == nullptr ? QDMI_ERROR_OUTOFMEM : QDMI_SUCCESS; +} +int MQT_SC_QDMI_device_session_init(MQT_SC_QDMI_Device_Session session) { + return session == nullptr ? QDMI_ERROR_INVALIDARGUMENT : session->init(); } - void MQT_SC_QDMI_device_session_free(MQT_SC_QDMI_Device_Session session) { - qdmi::sc::Device::get().sessionFree(session); + // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) + delete session; } - int MQT_SC_QDMI_device_session_set_parameter( - MQT_SC_QDMI_Device_Session session, QDMI_Device_Session_Parameter param, - const size_t size, const void* value) { - if (session == nullptr) { - return QDMI_ERROR_INVALIDARGUMENT; - } - return session->setParameter(param, size, value); + MQT_SC_QDMI_Device_Session session, + const QDMI_Device_Session_Parameter parameter, const size_t size, + const void* value) { + return session == nullptr ? QDMI_ERROR_INVALIDARGUMENT + : session->setParameter(parameter, size, value); } - int MQT_SC_QDMI_device_session_create_device_job( MQT_SC_QDMI_Device_Session session, MQT_SC_QDMI_Device_Job* job) { - if (session == nullptr) { - return QDMI_ERROR_INVALIDARGUMENT; - } - return session->createDeviceJob(job); + return session == nullptr ? QDMI_ERROR_INVALIDARGUMENT + : session->createDeviceJob(job); } - int MQT_SC_QDMI_device_session_retrieve_device_job_by_id( [[maybe_unused]] MQT_SC_QDMI_Device_Session session, [[maybe_unused]] const char* jobId, @@ -445,94 +472,63 @@ void MQT_SC_QDMI_device_job_free(MQT_SC_QDMI_Device_Job job) { job->free(); } } - -int MQT_SC_QDMI_device_job_set_parameter(MQT_SC_QDMI_Device_Job job, - const QDMI_Device_Job_Parameter param, - const size_t size, const void* value) { - if (job == nullptr) { - return QDMI_ERROR_INVALIDARGUMENT; - } - return job->setParameter(param, size, value); +int MQT_SC_QDMI_device_job_set_parameter( + MQT_SC_QDMI_Device_Job job, const QDMI_Device_Job_Parameter parameter, + const size_t size, const void* value) { + return job == nullptr ? QDMI_ERROR_INVALIDARGUMENT + : job->setParameter(parameter, size, value); } - -int MQT_SC_QDMI_device_job_query_property(MQT_SC_QDMI_Device_Job job, - const QDMI_Device_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 MQT_SC_QDMI_device_job_query_property( + MQT_SC_QDMI_Device_Job job, const QDMI_Device_Job_Property property, + const size_t size, void* value, size_t* sizeRet) { + return job == nullptr ? QDMI_ERROR_INVALIDARGUMENT + : job->queryProperty(property, size, value, sizeRet); } - int MQT_SC_QDMI_device_job_submit(MQT_SC_QDMI_Device_Job job) { - if (job == nullptr) { - return QDMI_ERROR_INVALIDARGUMENT; - } - return job->submit(); + return job == nullptr ? QDMI_ERROR_INVALIDARGUMENT : job->submit(); } - int MQT_SC_QDMI_device_job_cancel(MQT_SC_QDMI_Device_Job job) { - if (job == nullptr) { - return QDMI_ERROR_INVALIDARGUMENT; - } - return job->cancel(); + return job == nullptr ? QDMI_ERROR_INVALIDARGUMENT : job->cancel(); } - int MQT_SC_QDMI_device_job_check(MQT_SC_QDMI_Device_Job job, QDMI_Job_Status* status) { - if (job == nullptr) { - return QDMI_ERROR_INVALIDARGUMENT; - } - return job->check(status); + return job == nullptr ? QDMI_ERROR_INVALIDARGUMENT : job->check(status); } - int MQT_SC_QDMI_device_job_wait(MQT_SC_QDMI_Device_Job job, const size_t timeout) { - if (job == nullptr) { - return QDMI_ERROR_INVALIDARGUMENT; - } - return job->wait(timeout); + return job == nullptr ? QDMI_ERROR_INVALIDARGUMENT : job->wait(timeout); } - int MQT_SC_QDMI_device_job_get_results(MQT_SC_QDMI_Device_Job job, - QDMI_Job_Result result, + const 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); + return job == nullptr ? QDMI_ERROR_INVALIDARGUMENT + : job->getResults(result, size, data, sizeRet); } - int MQT_SC_QDMI_device_session_query_device_property( - MQT_SC_QDMI_Device_Session session, const QDMI_Device_Property prop, + MQT_SC_QDMI_Device_Session session, const QDMI_Device_Property property, const size_t size, void* value, size_t* sizeRet) { - if (session == nullptr) { - return QDMI_ERROR_INVALIDARGUMENT; - } - return session->queryDeviceProperty(prop, size, value, sizeRet); + return session == nullptr + ? QDMI_ERROR_INVALIDARGUMENT + : session->queryDeviceProperty(property, size, value, sizeRet); } - int MQT_SC_QDMI_device_session_query_site_property( MQT_SC_QDMI_Device_Session session, MQT_SC_QDMI_Site site, - const QDMI_Site_Property prop, const size_t size, void* value, + const QDMI_Site_Property property, const size_t size, void* value, size_t* sizeRet) { - if (session == nullptr) { - return QDMI_ERROR_INVALIDARGUMENT; - } - return session->querySiteProperty(site, prop, size, value, sizeRet); + return session == nullptr + ? QDMI_ERROR_INVALIDARGUMENT + : session->querySiteProperty(site, property, size, value, sizeRet); } - int MQT_SC_QDMI_device_session_query_operation_property( MQT_SC_QDMI_Device_Session session, MQT_SC_QDMI_Operation operation, const size_t numSites, const MQT_SC_QDMI_Site* sites, const size_t numParams, const double* params, - const QDMI_Operation_Property prop, const size_t size, void* value, + const QDMI_Operation_Property property, const size_t size, void* value, size_t* sizeRet) { - if (session == nullptr) { - return QDMI_ERROR_INVALIDARGUMENT; - } - return session->queryOperationProperty(operation, numSites, sites, numParams, - params, prop, size, value, sizeRet); + return session == nullptr + ? QDMI_ERROR_INVALIDARGUMENT + : session->queryOperationProperty(operation, numSites, sites, + numParams, params, property, + size, value, sizeRet); } diff --git a/src/qdmi/devices/sc/Generator.cpp b/src/qdmi/devices/sc/Generator.cpp deleted file mode 100644 index 165b093dfb..0000000000 --- a/src/qdmi/devices/sc/Generator.cpp +++ /dev/null @@ -1,278 +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 Generator.cpp - * @brief The MQT QDMI device generator for superconducting devices. - */ - -#include "qdmi/devices/sc/Generator.hpp" - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace sc { -namespace { -/** - * @brief Ensure array fields of a Device contain default entries. - * - * Ensures arrays that must not be empty have default elements; in particular, - * appends a default (empty) coupling to device.couplings. - * - * @param device Device instance whose array fields will be populated. - */ -auto populateArrayFields(Device& device) -> void { - device.couplings.emplace_back(); - device.operations.emplace_back(); -} - -/** - * @brief Writes a C preprocessor macro that initializes a variable with the - * device's name. - * - * The macro emitted is `#define INITIALIZE_NAME(var) var = ""`. - */ -auto writeName(const Device& device, std::ostream& os) -> void { - os << "#define INITIALIZE_NAME(var) var = \"" << device.name << "\"\n"; -} - -/** - * @brief Emits a C macro that initializes the device's qubit count. - * - * @param device Device whose `numQubits` value will be embedded in the macro. - * @param os Output stream to which the macro definition is written. - */ -auto writeQubitsNum(const Device& device, std::ostream& os) -> void { - os << "#define INITIALIZE_QUBITSNUM(var) var = " << device.numQubits - << "ULL\n"; -} - -/** - * @brief Generates a C preprocessor macro that initializes the device's qubit - * sites and coupling map. - * - * Writes a macro `INITIALIZE_SITES(var)` which clears `var`, appends - * `numQubits` unique sites (by index), and constructs a `_couplings` vector - * reserved to the number of couplings and populated with pairs of site pointers - * corresponding to `device.couplings`. - * - * @param device Device containing `numQubits` and `couplings`. - * @param os Output stream to which the macro definition is written. - */ -auto writeSites(const Device& device, std::ostream& os) -> void { - os << "#define INITIALIZE_SITES(var) var.clear()"; - for (uint64_t id = 0; id < device.numQubits; ++id) { - os << ";\\\n " - "var.emplace_back(MQT_SC_QDMI_Site_impl_d::makeUniqueSite(" - << id << "ULL))"; - } - os << ";\\\n std::vector _singleQubitSites"; - os << ";\\\n _singleQubitSites.reserve(var.size())"; - os << ";\\\n std::ranges::transform(var, " - "std::back_inserter(_singleQubitSites), [](const " - "std::unique_ptr& site) { return site.get(); " - "})"; - os << ";\\\n std::vector> " - "_couplings"; - os << ";\\\n _couplings.reserve(" << device.couplings.size() << ")"; - for (const auto& [i1, i2] : device.couplings) { - os << ";\\\n " - "_couplings.emplace_back(var.at(" - << i1 << ").get(), var.at(" << i2 << ").get())"; - } - os << "\n"; -} - -/** - * @brief Writes the operations from the device object. - * @param device is the device object containing the operations. - * @param os is the output stream to write the operations to. - */ -auto writeOperations(const Device& device, std::ostream& os) -> void { - os << "#define INITIALIZE_OPERATIONS(var) var.clear()"; - for (const auto& operation : device.operations) { - if (operation.numQubits == 1) { - os << ";\\\n" - " " - "var.emplace_back(MQT_SC_QDMI_Operation_impl_d::" - "makeUniqueSingleQubit(\"" - << operation.name << "\", " << operation.numParameters - << ", _singleQubitSites))"; - } else if (operation.numQubits == 2) { - os << ";\\\n" - " " - "var.emplace_back(MQT_SC_QDMI_Operation_impl_d::" - "makeUniqueTwoQubit(\"" - << operation.name << "\", " << operation.numParameters - << ", _couplings))"; - } else { - std::ostringstream ss; - ss << "Got operation with " << operation.numQubits << " qubits but only " - << "single- and two-qubit operations are supported."; - throw std::runtime_error(ss.str()); - } - } - os << "\n"; -} - -/** - * @brief Emits a macro to initialize the device coupling map. - * - * Writes the C preprocessor macro `INITIALIZE_COUPLINGMAP(var)` which assigns - * `var = _couplings`. - * - * @note This macro depends on the `_couplings` variable created by - * the INITIALIZE_SITES macro from writeSites(). The macro - * INITIALIZE_SITES must be invoked before INITIALIZE_COUPLINGMAP. - * - * @param os Output stream to write the macro definition to. - */ -auto writeCouplingMap(const Device& /* unused */, std::ostream& os) -> void { - os << "#define INITIALIZE_COUPLINGMAP(var) var = _couplings\n"; -} -} // namespace - -auto writeJSONSchema(std::ostream& os) -> void { - // Create a default device configuration - Device device; - - // Fill each array field with default values - populateArrayFields(device); - - // Convert the device configuration to a JSON object - // NOLINTNEXTLINE(misc-include-cleaner) - const nlohmann::json json = device; - - // Write to the output stream - os << json; -} - -/** - * @brief Write a default device JSON schema to the specified file path. - * - * Opens the file at `path` for writing and writes a JSON template representing - * a default Device configuration. The function closes the file on completion. - * - * @param path Filesystem path where the JSON template will be written. - * @throws std::runtime_error If the file at `path` cannot be opened for - * writing. - */ -auto writeJSONSchema(const std::string& path) -> void { - // Write to a file - std::ofstream ofs(path); - if (!ofs.good()) { - std::stringstream ss; - ss << "Failed to open file for writing: " << path; - throw std::runtime_error(ss.str()); - } - writeJSONSchema(ofs); - ofs.close(); - SPDLOG_INFO("JSON template written to {}", path); -} - -/** - * @brief Parses a Device configuration from an input stream containing JSON. - * - * Reads JSON from the provided input stream and converts it into a Device. - * - * @param is Input stream that supplies the JSON representation of the Device. - * @return Device constructed from the parsed JSON. - * @throws std::runtime_error If JSON parsing fails; the exception message - * contains parser error details. - */ -[[nodiscard]] auto readJSON(std::istream& is) -> Device { - // Read the device configuration from the input stream - nlohmann::json json; - try { - is >> json; - // NOLINTNEXTLINE(misc-include-cleaner) - } catch (const nlohmann::detail::parse_error& e) { - std::stringstream ss; - ss << "Failed to parse JSON string: " << e.what(); - throw std::runtime_error(ss.str()); - } - return json; -} - -/** - * @brief Read a Device configuration from a JSON file. - * - * @param path Filesystem path to the JSON file containing the device - * configuration. - * @return Device parsed from the JSON file. - * @throws std::runtime_error If the file cannot be opened or if parsing the - * JSON fails. - */ -[[nodiscard]] auto readJSON(const std::string& path) -> Device { - // Read the device configuration from a JSON file - std::ifstream ifs(path); - if (!ifs.good()) { - throw std::runtime_error("Failed to open JSON file: " + std::string(path)); - } - auto device = readJSON(ifs); - ifs.close(); - return device; -} - -/** - * @brief Writes a C++ header snippet that initializes the provided Device as C - * macros. - * - * Writes macros defining the device name, qubit count, site initializers, and - * coupling map to the given output stream; the header begins with a pragma once - * guard. - * - * @param device Device to serialize into header macros. - * @param os Output stream to write the header content to. - */ -auto writeHeader(const Device& device, std::ostream& os) -> void { - os << "#pragma once\n\n" - << "#include \n" - << "#include \n" - << "#include \n" - << "#include \n" - << "#include \n\n"; - writeName(device, os); - writeQubitsNum(device, os); - writeSites(device, os); - writeCouplingMap(device, os); - writeOperations(device, os); -} - -/** - * @brief Write a C++ header file that defines macros to initialize the given - * Device. - * - * @param device Device to serialize into initialization macros (name, qubit - * sites, coupling map). - * @param path Filesystem path where the header will be created/overwritten. - * @throws std::runtime_error if the file at `path` cannot be opened for - * writing. - */ -auto writeHeader(const Device& device, const std::string& path) -> void { - std::ofstream ofs(path); - if (!ofs.good()) { - std::stringstream ss; - ss << "Failed to open header file for writing: " << path; - throw std::runtime_error(ss.str()); - } - writeHeader(device, ofs); - ofs.close(); - SPDLOG_INFO("Header file written to {}", path); -} -} // namespace sc diff --git a/test/python/qdmi/test_qdmi.py b/test/python/qdmi/test_qdmi.py index a77101b847..13cc6c9f1f 100644 --- a/test/python/qdmi/test_qdmi.py +++ b/test/python/qdmi/test_qdmi.py @@ -10,6 +10,7 @@ from __future__ import annotations +import json import tempfile from pathlib import Path from typing import Any, cast @@ -1074,6 +1075,27 @@ def test_device_configuration_arguments_are_mutually_exclusive() -> None: ) +def test_sc_open_device_accepts_runtime_configuration(tmp_path: Path) -> None: + """The built-in SC provider should materialize a per-open file model.""" + configuration = json.loads(Path("json/sc/mqt-core-qdmi-sc-device.json").read_text(encoding="utf-8")) + configuration["name"] = "Python custom SC device" + configuration["numQubits"] = 5 + configuration["couplings"] = [[0, 1], [1, 2], [2, 3], [3, 4]] + configuration["qubitProperties"]["overrides"] = [] + for operation in configuration["operations"]: + operation.pop("sites", None) + operation["siteOverrides"] = [] + configuration_file = tmp_path / "sc-device.json" + configuration_file.write_text(json.dumps(configuration), encoding="utf-8") + + device = open_device( + "mqt.sc.default", + device_config_file=configuration_file, + ) + assert device.name() == "Python custom SC device" + assert device.qubits_num() == 5 + + def test_site_keeps_fresh_session_alive() -> None: """A site should remain usable after its device wrapper is destroyed.""" site = open_device("mqt.na.default").sites()[0] diff --git a/test/qdmi/TestUtils.hpp b/test/qdmi/TestUtils.hpp new file mode 100644 index 0000000000..2cbb85548f --- /dev/null +++ b/test/qdmi/TestUtils.hpp @@ -0,0 +1,76 @@ +/* + * 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 TestUtils.hpp + * @brief Shared test utilities for QDMI components. + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace mqt::test { + +/// Temporarily sets an environment variable and restores its previous value. +class ScopedEnvironmentVariable { +public: + ScopedEnvironmentVariable(std::string name, const std::string& value) + : name_(std::move(name)) { + if (const auto* previous = std::getenv(name_.c_str()); + previous != nullptr) { + previous_ = previous; + } + set(value); + } + + ~ScopedEnvironmentVariable() { + if (previous_) { + static_cast(setWithoutChecking(*previous_)); + } else { +#ifdef _WIN32 + static_cast(_putenv_s(name_.c_str(), "")); +#else + // NOLINTNEXTLINE(misc-include-cleaner) + static_cast(unsetenv(name_.c_str())); +#endif + } + } + + ScopedEnvironmentVariable(const ScopedEnvironmentVariable&) = delete; + ScopedEnvironmentVariable& + operator=(const ScopedEnvironmentVariable&) = delete; + ScopedEnvironmentVariable(ScopedEnvironmentVariable&&) = delete; + ScopedEnvironmentVariable& operator=(ScopedEnvironmentVariable&&) = delete; + +private: + void set(const std::string& value) const { + if (!setWithoutChecking(value)) { + throw std::runtime_error("Failed to set environment variable " + name_); + } + } + + [[nodiscard]] bool setWithoutChecking(const std::string& value) const { +#ifdef _WIN32 + return _putenv_s(name_.c_str(), value.c_str()) == 0; +#else + // NOLINTNEXTLINE(misc-include-cleaner) + return setenv(name_.c_str(), value.c_str(), 1) == 0; +#endif + } + + std::string name_; + std::optional previous_; +}; + +} // namespace mqt::test diff --git a/test/qdmi/devices/na/CMakeLists.txt b/test/qdmi/devices/na/CMakeLists.txt index 4e743e3e7a..926d0afcf8 100644 --- a/test/qdmi/devices/na/CMakeLists.txt +++ b/test/qdmi/devices/na/CMakeLists.txt @@ -18,6 +18,7 @@ if(TARGET MQT::CoreQDMINaDeviceConfig AND TARGET MQT::CoreQDMINaDevice) endif() # Add the test executable package_add_test(${TARGET_NAME} MQT::CoreQDMINaDevice test_configuration.cpp test_device.cpp) + target_include_directories(${TARGET_NAME} PRIVATE ${PROJECT_SOURCE_DIR}/test) # Add dependency to enforce implementation of all interface functions add_dependencies(${TARGET_NAME} qdmi_test_mqt_na_device_defs) # Link further libraries diff --git a/test/qdmi/devices/na/test_device.cpp b/test/qdmi/devices/na/test_device.cpp index 9f054660a7..8a0075d341 100644 --- a/test/qdmi/devices/na/test_device.cpp +++ b/test/qdmi/devices/na/test_device.cpp @@ -9,6 +9,7 @@ */ #include "mqt_na_qdmi/device.h" +#include "qdmi/TestUtils.hpp" #include "qdmi/devices/na/Configuration.hpp" #include @@ -20,12 +21,10 @@ #include #include #include -#include #include #include #include #include -#include #include #include #include @@ -63,55 +62,7 @@ struct PairHash { } }; -class ScopedEnvironmentVariable { -public: - ScopedEnvironmentVariable(std::string name, const std::string& value) - : name_(std::move(name)) { - if (const auto* previous = std::getenv(name_.c_str()); - previous != nullptr) { - previous_ = previous; - } - set(value); - } - - ~ScopedEnvironmentVariable() { - if (previous_) { - static_cast(setWithoutChecking(*previous_)); - } else { -#ifdef _WIN32 - static_cast(_putenv_s(name_.c_str(), "")); -#else - // NOLINTNEXTLINE(misc-include-cleaner) - static_cast(unsetenv(name_.c_str())); -#endif - } - } - - ScopedEnvironmentVariable(const ScopedEnvironmentVariable&) = delete; - ScopedEnvironmentVariable& - operator=(const ScopedEnvironmentVariable&) = delete; - ScopedEnvironmentVariable(ScopedEnvironmentVariable&&) = delete; - ScopedEnvironmentVariable& operator=(ScopedEnvironmentVariable&&) = delete; - -private: - void set(const std::string& value) const { - if (!setWithoutChecking(value)) { - throw std::runtime_error("Failed to set environment variable " + name_); - } - } - - [[nodiscard]] bool setWithoutChecking(const std::string& value) const { -#ifdef _WIN32 - return _putenv_s(name_.c_str(), value.c_str()) == 0; -#else - // NOLINTNEXTLINE(misc-include-cleaner) - return setenv(name_.c_str(), value.c_str(), 1) == 0; -#endif - } - - std::string name_; - std::optional previous_; -}; +using mqt::test::ScopedEnvironmentVariable; [[nodiscard]] std::string queryName(MQT_NA_QDMI_Device_Session session) { size_t size = 0; diff --git a/test/qdmi/devices/sc/CMakeLists.txt b/test/qdmi/devices/sc/CMakeLists.txt index eaf8216240..11aea2e84c 100644 --- a/test/qdmi/devices/sc/CMakeLists.txt +++ b/test/qdmi/devices/sc/CMakeLists.txt @@ -6,35 +6,32 @@ # # Licensed under the MIT License -if(TARGET MQT::CoreQDMIScDeviceGen - AND TARGET MQT::CoreQDMIScDeviceGenerator - AND TARGET MQT::CoreQDMIScDevice) +if(TARGET MQT::CoreQDMIScDeviceConfig AND TARGET MQT::CoreQDMIScDevice) # Set QDMI prefix again for the tests set(QDMI_PREFIX "MQT_SC") generate_device_defs_executable(${QDMI_PREFIX} TARGET MQT::CoreQDMIScDevice) # Set test target name set(TARGET_NAME ${MQT_CORE_TARGET_NAME}-qdmi-sc-device-test) + if(WIN32) + # Discover tests after the provider and its runtime assets have been copied. + set(CMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE PRE_TEST) + endif() # Add the test executable - package_add_test(${TARGET_NAME} MQT::CoreQDMIScDevice test_generator.cpp test_app.cpp - test_device.cpp) + package_add_test(${TARGET_NAME} MQT::CoreQDMIScDevice test_configuration.cpp test_device.cpp) + target_include_directories(${TARGET_NAME} PRIVATE ${PROJECT_SOURCE_DIR}/test) # Add dependency to enforce implementation of all interface functions add_dependencies(${TARGET_NAME} qdmi_test_mqt_sc_device_defs) # Link further libraries - target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQDMIScDeviceGen + target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQDMIScDeviceConfig nlohmann_json::nlohmann_json) # Set the executable path and version information target_compile_definitions( ${TARGET_NAME} - PRIVATE EXECUTABLE_PATH="$" - MQT_CORE_VERSION="${MQT_CORE_VERSION}" - SC_DEVICE_JSON="${PROJECT_SOURCE_DIR}/json/sc/device.json") + PRIVATE MQT_CORE_VERSION="${MQT_CORE_VERSION}" + SC_DEVICE_JSON="${PROJECT_SOURCE_DIR}/json/sc/mqt-core-qdmi-sc-device.json") - # On Windows, we need to copy the DLL to the test executable directory + # On Windows, copy the provider and its runtime assets beside the test executable. if(WIN32) - add_custom_command( - TARGET ${TARGET_NAME} - PRE_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different $ - $) + mqt_copy_qdmi_runtime(${TARGET_NAME} MQT::CoreQDMIScDevice) endif() endif() diff --git a/test/qdmi/devices/sc/test_app.cpp b/test/qdmi/devices/sc/test_app.cpp deleted file mode 100644 index 8a9224191b..0000000000 --- a/test/qdmi/devices/sc/test_app.cpp +++ /dev/null @@ -1,372 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include - -#include -#include -#include -#include -#include - -#ifdef _WIN32 -#define PLATFORM_POPEN _popen -#define PLATFORM_PCLOSE _pclose -#else -// The following are included via but the direct include are platform- -// specific, so ignore the corresponding warning for platform-agnostic includes. -// NOLINTBEGIN(misc-include-cleaner) -#define PLATFORM_POPEN popen -#define PLATFORM_PCLOSE pclose -// NOLINTEND(misc-include-cleaner) -#endif // _WIN32 - -TEST(ScExecutableTest, Version) { - // Command to execute - // NOLINTNEXTLINE(misc-include-cleaner) - const std::string command = EXECUTABLE_PATH " --version"; - // Open a pipe to capture the output - FILE* pipe = PLATFORM_POPEN(command.c_str(), "r"); - ASSERT_NE(pipe, nullptr) << "Failed to open pipe"; - // Read the output - std::array buffer{}; - buffer.fill('\0'); - std::stringstream output; - while (fgets(buffer.data(), static_cast(buffer.size()), pipe) != - nullptr) { - output << buffer.data(); - } - // Close the pipe - const int returnCode = PLATFORM_PCLOSE(pipe); - // Print the captured output - std::cout << "Captured Output:\n" << output.str() << "\n"; - ASSERT_EQ(returnCode, 0) << "Executable failed with return code: " - << returnCode; - // Validate the output - EXPECT_EQ(output.str(), - // NOLINTNEXTLINE(misc-include-cleaner) - "MQT QDMI SC Device Generator (MQT Version " MQT_CORE_VERSION - ")\n"); -} - -TEST(ScExecutableTest, MissingSubcommand) { - // Command to execute - // NOLINTNEXTLINE(misc-include-cleaner) - const std::string command = EXECUTABLE_PATH; - // Open a pipe to capture the output - FILE* pipe = PLATFORM_POPEN(command.c_str(), "r"); - ASSERT_NE(pipe, nullptr) << "Failed to open pipe"; - // Read the output - std::array buffer{}; - buffer.fill('\0'); - std::stringstream output; - while (fgets(buffer.data(), static_cast(buffer.size()), pipe) != - nullptr) { - output << buffer.data(); - } - // Close the pipe - const int returnCode = PLATFORM_PCLOSE(pipe); - // Print the captured output - std::cout << "Captured Output:\n" << output.str() << "\n"; - EXPECT_NE(returnCode, 0); -} - -TEST(ScExecutableTest, UnknownSubcommand) { - // Command to execute - // NOLINTNEXTLINE(misc-include-cleaner) - const std::string command = EXECUTABLE_PATH " unknown"; - // Open a pipe to capture the output - FILE* pipe = PLATFORM_POPEN(command.c_str(), "r"); - ASSERT_NE(pipe, nullptr) << "Failed to open pipe"; - // Read the output - std::array buffer{}; - buffer.fill('\0'); - std::stringstream output; - while (fgets(buffer.data(), static_cast(buffer.size()), pipe) != - nullptr) { - output << buffer.data(); - } - // Close the pipe - const int returnCode = PLATFORM_PCLOSE(pipe); - // Print the captured output - std::cout << "Captured Output:\n" << output.str() << "\n"; - EXPECT_NE(returnCode, 0); -} - -TEST(ScExecutableTest, SchemaUnknownOption) { - // Command to execute - // NOLINTNEXTLINE(misc-include-cleaner) - const std::string command = EXECUTABLE_PATH " schema --unknown-option"; - // Open a pipe to capture the output - FILE* pipe = PLATFORM_POPEN(command.c_str(), "r"); - ASSERT_NE(pipe, nullptr) << "Failed to open pipe"; - // Read the output - std::array buffer{}; - buffer.fill('\0'); - std::stringstream output; - while (fgets(buffer.data(), static_cast(buffer.size()), pipe) != - nullptr) { - output << buffer.data(); - } - // Close the pipe - const int returnCode = PLATFORM_PCLOSE(pipe); - // Print the captured output - std::cout << "Captured Output:\n" << output.str() << "\n"; - EXPECT_NE(returnCode, 0); -} - -TEST(ScExecutableTest, SchemaMissingFile) { - // Command to execute - // NOLINTNEXTLINE(misc-include-cleaner) - const std::string command = EXECUTABLE_PATH " schema --output"; - // Open a pipe to capture the output - FILE* pipe = PLATFORM_POPEN(command.c_str(), "r"); - ASSERT_NE(pipe, nullptr) << "Failed to open pipe"; - // Read the output - std::array buffer{}; - buffer.fill('\0'); - std::stringstream output; - while (fgets(buffer.data(), static_cast(buffer.size()), pipe) != - nullptr) { - output << buffer.data(); - } - // Close the pipe - const int returnCode = PLATFORM_PCLOSE(pipe); - // Print the captured output - std::cout << "Captured Output:\n" << output.str() << "\n"; - EXPECT_NE(returnCode, 0); -} - -TEST(ScExecutableTest, ValidateInvalidJson) { - // Command to execute - // NOLINTNEXTLINE(misc-include-cleaner) - const std::string command = EXECUTABLE_PATH " validate"; - // Open a pipe to the executable - FILE* pipe = PLATFORM_POPEN(command.c_str(), "w"); - ASSERT_NE(pipe, nullptr) << "Failed to open pipe"; - // Write the schema to the executable's stdin - fwrite("{", sizeof(char), 2, pipe); - // Close the pipe - const int returnCode = PLATFORM_PCLOSE(pipe); - EXPECT_NE(returnCode, 0) << "Executable failed with return code: " - << returnCode; -} - -TEST(ScExecutableTest, GenerateMissingFile) { - // Command to execute - // NOLINTNEXTLINE(misc-include-cleaner) - const std::string command = EXECUTABLE_PATH " generate --output"; - // Open a pipe to capture the output - FILE* pipe = PLATFORM_POPEN(command.c_str(), "r"); - ASSERT_NE(pipe, nullptr) << "Failed to open pipe"; - // Read the output - std::array buffer{}; - buffer.fill('\0'); - std::stringstream output; - while (fgets(buffer.data(), static_cast(buffer.size()), pipe) != - nullptr) { - output << buffer.data(); - } - // Close the pipe - const int returnCode = PLATFORM_PCLOSE(pipe); - // Print the captured output - std::cout << "Captured Output:\n" << output.str() << "\n"; - EXPECT_NE(returnCode, 0); -} - -TEST(ScExecutableTest, Usage) { - // Command to execute - // NOLINTNEXTLINE(misc-include-cleaner) - const std::string command = EXECUTABLE_PATH " --help"; - // Open a pipe to capture the output - FILE* pipe = PLATFORM_POPEN(command.c_str(), "r"); - ASSERT_NE(pipe, nullptr) << "Failed to open pipe"; - // Read the output - std::array buffer{}; - buffer.fill('\0'); - std::stringstream output; - while (fgets(buffer.data(), static_cast(buffer.size()), pipe) != - nullptr) { - output << buffer.data(); - } - // Close the pipe - const int returnCode = PLATFORM_PCLOSE(pipe); - // Print the captured output - std::cout << "Captured Output:\n" << output.str() << "\n"; - ASSERT_EQ(returnCode, 0) << "Executable failed with return code: " - << returnCode; - EXPECT_FALSE(output.str().empty()); -} - -TEST(ScExecutableTest, SchemaUsage) { - // Command to execute - // NOLINTNEXTLINE(misc-include-cleaner) - const std::string command = EXECUTABLE_PATH " schema --help"; - // Open a pipe to capture the output - FILE* pipe = PLATFORM_POPEN(command.c_str(), "r"); - ASSERT_NE(pipe, nullptr) << "Failed to open pipe"; - // Read the output - std::array buffer{}; - buffer.fill('\0'); - std::stringstream output; - while (fgets(buffer.data(), static_cast(buffer.size()), pipe) != - nullptr) { - output << buffer.data(); - } - // Close the pipe - const int returnCode = PLATFORM_PCLOSE(pipe); - // Print the captured output - std::cout << "Captured Output:\n" << output.str() << "\n"; - ASSERT_EQ(returnCode, 0) << "Executable failed with return code: " - << returnCode; - EXPECT_TRUE(output.str().rfind("Generates a JSON schema", 0) == 0); -} - -TEST(ScExecutableTest, ValidateUsage) { - // Command to execute - // NOLINTNEXTLINE(misc-include-cleaner) - const std::string command = EXECUTABLE_PATH " validate --help"; - // Open a pipe to capture the output - FILE* pipe = PLATFORM_POPEN(command.c_str(), "r"); - ASSERT_NE(pipe, nullptr) << "Failed to open pipe"; - // Read the output - std::array buffer{}; - buffer.fill('\0'); - std::stringstream output; - while (fgets(buffer.data(), static_cast(buffer.size()), pipe) != - nullptr) { - output << buffer.data(); - } - // Close the pipe - const int returnCode = PLATFORM_PCLOSE(pipe); - // Print the captured output - std::cout << "Captured Output:\n" << output.str() << "\n"; - ASSERT_EQ(returnCode, 0) << "Executable failed with return code: " - << returnCode; - EXPECT_TRUE(output.str().rfind("Validates", 0) == 0); -} - -TEST(ScExecutableTest, GenerateUsage) { - // Command to execute - // NOLINTNEXTLINE(misc-include-cleaner) - const std::string command = EXECUTABLE_PATH " generate --help"; - // Open a pipe to capture the output - FILE* pipe = PLATFORM_POPEN(command.c_str(), "r"); - ASSERT_NE(pipe, nullptr) << "Failed to open pipe"; - // Read the output - std::array buffer{}; - buffer.fill('\0'); - std::stringstream output; - while (fgets(buffer.data(), static_cast(buffer.size()), pipe) != - nullptr) { - output << buffer.data(); - } - // Close the pipe - const int returnCode = PLATFORM_PCLOSE(pipe); - // Print the captured output - std::cout << "Captured Output:\n" << output.str() << "\n"; - ASSERT_EQ(returnCode, 0) << "Executable failed with return code: " - << returnCode; - EXPECT_TRUE(output.str().rfind("Generates a header file", 0) == 0); -} - -TEST(ScExecutableTest, RoundTrip) { - std::string schema; - // Capture the output of the schema command - { - // Command to execute - // NOLINTNEXTLINE(misc-include-cleaner) - const std::string command = EXECUTABLE_PATH " schema"; - // Open a pipe to capture the output - FILE* pipe = PLATFORM_POPEN(command.c_str(), "r"); - ASSERT_NE(pipe, nullptr) << "Failed to open pipe"; - // Read the output - std::array buffer{}; - buffer.fill('\0'); - std::stringstream output; - while (fgets(buffer.data(), static_cast(buffer.size()), pipe) != - nullptr) { - output << buffer.data(); - } - // Close the pipe - const int returnCode = PLATFORM_PCLOSE(pipe); - schema = output.str(); - // Print the captured output - std::cout << "Captured Output:\n" << schema << "\n"; - ASSERT_EQ(returnCode, 0) - << "Executable failed with return code: " << returnCode; - } - // Validate the output - { - // Command to execute - // NOLINTNEXTLINE(misc-include-cleaner) - const std::string command = EXECUTABLE_PATH " validate"; - // Open a pipe to the executable with write mode - FILE* pipe = PLATFORM_POPEN(command.c_str(), "w"); - ASSERT_NE(pipe, nullptr) << "Failed to open pipe"; - // Write the schema to the executable's stdin - fwrite(schema.c_str(), sizeof(char), schema.size(), pipe); - // Close the pipe - const int returnCode = PLATFORM_PCLOSE(pipe); - ASSERT_EQ(returnCode, 0) - << "Executable failed with return code: " << returnCode; - } -} - -TEST(ScExecutableTest, RoundTripFile) { - // Write schema to a file - { - // Command to execute - // NOLINTNEXTLINE(misc-include-cleaner) - const std::string command = EXECUTABLE_PATH " schema --output schema.json"; - // Open a pipe to capture the output - FILE* pipe = PLATFORM_POPEN(command.c_str(), "r"); - ASSERT_NE(pipe, nullptr) << "Failed to open pipe"; - // Read the output - std::array buffer{}; - buffer.fill('\0'); - std::stringstream output; - while (fgets(buffer.data(), static_cast(buffer.size()), pipe) != - nullptr) { - output << buffer.data(); - } - // Close the pipe - const int returnCode = PLATFORM_PCLOSE(pipe); - // Print the captured output - std::cout << "Captured Output:\n" << output.str() << "\n"; - ASSERT_EQ(returnCode, 0) - << "Executable failed with return code: " << returnCode; - } - // Validate the output - { - // Command to execute - // NOLINTNEXTLINE(misc-include-cleaner) - const std::string command = EXECUTABLE_PATH " validate schema.json"; - // Open a pipe to capture the output - FILE* pipe = PLATFORM_POPEN(command.c_str(), "r"); - ASSERT_NE(pipe, nullptr) << "Failed to open pipe"; - // Read the output - std::array buffer{}; - buffer.fill('\0'); - std::stringstream output; - while (fgets(buffer.data(), static_cast(buffer.size()), pipe) != - nullptr) { - output << buffer.data(); - } - // Close the pipe - const int returnCode = PLATFORM_PCLOSE(pipe); - // Print the captured output - std::cout << "Captured Output:\n" << output.str() << "\n"; - EXPECT_EQ(returnCode, 0) - << "Executable failed with return code: " << returnCode; - } - // Remove created file - std::remove("schema.json"); -} diff --git a/test/qdmi/devices/sc/test_configuration.cpp b/test/qdmi/devices/sc/test_configuration.cpp new file mode 100644 index 0000000000..8dd6ffbbd5 --- /dev/null +++ b/test/qdmi/devices/sc/test_configuration.cpp @@ -0,0 +1,182 @@ +/* + * 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/devices/sc/Configuration.hpp" + +#include +#include +#include // NOLINT(misc-include-cleaner) +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace sc { +namespace { +using Json = nlohmann::json; + +[[nodiscard]] Json bundledJson() { + std::ifstream input(SC_DEVICE_JSON); + if (!input) { + throw std::runtime_error("failed to open bundled SC configuration"); + } + return Json::parse(input); +} + +void expectInvalid(const std::function& mutate, + const std::string& diagnostic) { + auto value = bundledJson(); + mutate(value); + try { + static_cast(readJSON(value.dump(), "test-source")); + FAIL() << "Expected invalid SC configuration"; + } catch (const std::invalid_argument& error) { + EXPECT_THAT(error.what(), testing::HasSubstr("test-source:$")); + EXPECT_THAT(error.what(), testing::HasSubstr(diagnostic)); + } +} +} // namespace + +TEST(ScConfigurationTest, ParsesBundledDeviceStrictly) { + const auto device = readJSON(SC_DEVICE_JSON); + EXPECT_EQ(device.schemaVersion, 1); + EXPECT_EQ(device.name, "MQT SC Default QDMI Device"); + EXPECT_EQ(device.numQubits, 100); + EXPECT_EQ(device.operations.size(), 3); + EXPECT_EQ(device.operations[0].name, "r"); + EXPECT_EQ(device.operations[1].name, "cz"); + EXPECT_EQ(device.operations[2].name, "measure"); + ASSERT_EQ(device.couplings.size(), 180); + EXPECT_EQ(device.couplings.front(), (std::pair{0, 1})); + EXPECT_EQ(device.couplings.back(), (std::pair{89, 99})); +} + +TEST(ScConfigurationTest, RejectsTopLevelSchemaErrors) { + expectInvalid([](auto& root) { root["unknown"] = true; }, "unknown"); + expectInvalid([](auto& root) { root.erase("qubitProperties"); }, + "qubitProperties"); + expectInvalid([](auto& root) { root["schema-version"] = 2; }, + "schema-version"); + expectInvalid([](auto& root) { root["name"] = ""; }, "name"); + expectInvalid([](auto& root) { root["numQubits"] = 0; }, "numQubits"); + expectInvalid([](auto& root) { root["numQubits"] = "100"; }, "numQubits"); + expectInvalid( + [](auto& root) { + root["numQubits"] = std::numeric_limits::max(); + }, + "numQubits"); +} + +TEST(ScConfigurationTest, RejectsInvalidUnitsAndCalibration) { + expectInvalid([](auto& root) { root["durationUnit"]["unit"] = "fortnight"; }, + "durationUnit"); + expectInvalid([](auto& root) { root["durationUnit"]["scaleFactor"] = 0.; }, + "durationUnit"); + expectInvalid( + [](auto& root) { + root["qubitProperties"]["overrides"].push_back( + {{"qubit", 100}, {"t1", 1}}); + }, + "qubitProperties/overrides"); + expectInvalid( + [](auto& root) { + root["qubitProperties"]["overrides"].push_back( + {{"qubit", 8}, {"t1", 0}}); + }, + "qubitProperties/overrides"); +} + +TEST(ScConfigurationTest, RejectsInvalidOrderedTopology) { + expectInvalid( + [](auto& root) { root["couplings"].push_back(root["couplings"][0]); }, + "couplings"); + expectInvalid([](auto& root) { root["couplings"][0] = {0, 0}; }, "couplings"); + expectInvalid([](auto& root) { root["couplings"][0] = {0, 100}; }, + "couplings"); + + auto root = bundledJson(); + root["couplings"] = {{1, 0}, {0, 1}}; + root["operations"][1]["siteOverrides"] = Json::array(); + const auto parsed = readJSON(root.dump(), "ordered"); + EXPECT_EQ(parsed.couplings[0], (std::pair{1, 0})); + EXPECT_EQ(parsed.couplings[1], (std::pair{0, 1})); +} + +TEST(ScConfigurationTest, ValidatesOperationSupportAndCalibration) { + expectInvalid( + [](auto& root) { + root["operations"].push_back(root["operations"].front()); + }, + "operations"); + expectInvalid( + [](auto& root) { + root["operations"][0]["numQubits"] = 3; + root["operations"][0].erase("sites"); + }, + "sites"); + expectInvalid( + [](auto& root) { + root["operations"][1]["sites"] = {{0, 2}}; + root["operations"][1]["siteOverrides"] = Json::array(); + }, + "device connectivity"); + expectInvalid([](auto& root) { root["operations"][0]["fidelity"] = 1.1; }, + "fidelity"); + expectInvalid( + [](auto& root) { + root["operations"][1]["siteOverrides"][0]["sites"] = {1, 0}; + }, + "siteOverrides"); + expectInvalid( + [](auto& root) { + root["operations"][1]["siteOverrides"][0].erase("duration"); + root["operations"][1]["siteOverrides"][0].erase("fidelity"); + }, + "siteOverrides"); +} + +TEST(ScConfigurationTest, AcceptsZeroDurationAndRejectsOverflowingJsonNumber) { + auto root = bundledJson(); + root["operations"][0]["duration"] = 0; + root["operations"][1]["siteOverrides"][0]["duration"] = 0; + EXPECT_NO_THROW(static_cast(readJSON(root.dump(), "zero-duration"))); + EXPECT_THROW( + static_cast(readJSON( + R"({"schema-version":1e400,"name":"x","numQubits":1,"durationUnit":{"unit":"ns","scaleFactor":1},"qubitProperties":{"defaults":{},"overrides":[]},"couplings":[],"operations":[]})", + "overflow")), + std::invalid_argument); +} + +TEST(ScConfigurationTest, AcceptsExplicitHigherAritySites) { + auto root = bundledJson(); + root["numQubits"] = 3; + root["couplings"] = Json::array(); + root["qubitProperties"]["overrides"] = Json::array(); + root["operations"] = Json::array( + {{{"name", "three"}, + {"numParameters", 1}, + {"numQubits", 3}, + {"sites", {{2, 0, 1}}}, + {"duration", 0}, + {"siteOverrides", {{{"sites", {2, 0, 1}}, {"fidelity", 0.75}}}}}}); + const auto parsed = readJSON(root.dump(), "higher-arity"); + ASSERT_EQ(parsed.operations.size(), 1); + ASSERT_TRUE(parsed.operations[0].sites.has_value()); + EXPECT_EQ(parsed.operations[0].sites->front(), + (std::vector{2, 0, 1})); +} + +} // namespace sc diff --git a/test/qdmi/devices/sc/test_device.cpp b/test/qdmi/devices/sc/test_device.cpp index 66479c9a4a..f05646419a 100644 --- a/test/qdmi/devices/sc/test_device.cpp +++ b/test/qdmi/devices/sc/test_device.cpp @@ -9,20 +9,27 @@ */ #include "mqt_sc_qdmi/device.h" +#include "qdmi/TestUtils.hpp" #include #include #include // NOLINT(misc-include-cleaner) +#include +#include #include +#include +#include +#include #include #include +#include #include namespace { -[[nodiscard]] auto querySites(MQT_SC_QDMI_Device_Session session) - -> std::vector { +[[nodiscard]] std::vector +querySites(MQT_SC_QDMI_Device_Session session) { size_t size = 0; if (MQT_SC_QDMI_device_session_query_device_property( session, QDMI_DEVICE_PROPERTY_SITES, 0, nullptr, &size) != @@ -90,6 +97,304 @@ class ScQDMIJobSpecificationTest : public ScQDMISpecificationTest { } // namespace +namespace { +constexpr auto CUSTOM_SC = R"({ + "schema-version": 1, + "name": "Custom SC", + "numQubits": 2, + "durationUnit": {"unit": "ns", "scaleFactor": 0.5}, + "qubitProperties": { + "defaults": {"t1": 100, "t2": 200}, + "overrides": [{"qubit": 1, "t1": 90}] + }, + "couplings": [[0, 1], [1, 0]], + "operations": [{ + "name": "cz", + "numParameters": 0, + "numQubits": 2, + "duration": 20, + "fidelity": 0.9, + "siteOverrides": [{"sites": [0, 1], "duration": 10, "fidelity": 0.95}] + }] +})"; + +using mqt::test::ScopedEnvironmentVariable; + +[[nodiscard]] MQT_SC_QDMI_Device_Session +initializedSession(const std::string_view configuration = CUSTOM_SC) { + MQT_SC_QDMI_Device_Session session = nullptr; + if (MQT_SC_QDMI_device_session_alloc(&session) != QDMI_SUCCESS) { + throw std::runtime_error("Failed to allocate custom SC session"); + } + if (MQT_SC_QDMI_device_session_set_parameter( + session, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM1, + configuration.size() + 1, configuration.data()) != QDMI_SUCCESS || + MQT_SC_QDMI_device_session_init(session) != QDMI_SUCCESS) { + MQT_SC_QDMI_device_session_free(session); + throw std::runtime_error("Failed to initialize custom SC session"); + } + return session; +} + +[[nodiscard]] std::string queryName(MQT_SC_QDMI_Device_Session session) { + size_t size = 0; + if (MQT_SC_QDMI_device_session_query_device_property( + session, QDMI_DEVICE_PROPERTY_NAME, 0, nullptr, &size) != + QDMI_SUCCESS) { + throw std::runtime_error("Failed to query device name size"); + } + std::string name(size, '\0'); + if (MQT_SC_QDMI_device_session_query_device_property( + session, QDMI_DEVICE_PROPERTY_NAME, size, name.data(), nullptr) != + QDMI_SUCCESS) { + throw std::runtime_error("Failed to query device name"); + } + name.resize(size - 1); + return name; +} + +[[nodiscard]] std::vector +queryOperations(MQT_SC_QDMI_Device_Session session) { + size_t size = 0; + if (MQT_SC_QDMI_device_session_query_device_property( + session, QDMI_DEVICE_PROPERTY_OPERATIONS, 0, nullptr, &size) != + QDMI_SUCCESS) { + throw std::runtime_error("Failed to query operations"); + } + std::vector operations(size / + sizeof(MQT_SC_QDMI_Operation)); + if (MQT_SC_QDMI_device_session_query_device_property( + session, QDMI_DEVICE_PROPERTY_OPERATIONS, size, + static_cast(operations.data()), nullptr) != QDMI_SUCCESS) { + throw std::runtime_error("Failed to retrieve operations"); + } + return operations; +} +} // namespace + +TEST(ScRuntimeConfiguration, ValidatesRawParameterStringsAndRetry) { + MQT_SC_QDMI_Device_Session session = nullptr; + ASSERT_EQ(MQT_SC_QDMI_device_session_alloc(&session), QDMI_SUCCESS); + EXPECT_EQ(MQT_SC_QDMI_device_session_set_parameter( + session, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM1, 0, nullptr), + QDMI_SUCCESS); + constexpr std::array missingNul{'{', '}'}; + EXPECT_EQ(MQT_SC_QDMI_device_session_set_parameter( + session, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM1, + missingNul.size(), missingNul.data()), + QDMI_ERROR_INVALIDARGUMENT); + constexpr std::array embeddedNul{'{', '\0', '}', '\0'}; + EXPECT_EQ(MQT_SC_QDMI_device_session_set_parameter( + session, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM1, + embeddedNul.size(), embeddedNul.data()), + QDMI_ERROR_INVALIDARGUMENT); + constexpr auto file = std::to_array(SC_DEVICE_JSON); + ASSERT_EQ(MQT_SC_QDMI_device_session_set_parameter( + session, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM1, + std::strlen(CUSTOM_SC) + 1, CUSTOM_SC), + QDMI_SUCCESS); + ASSERT_EQ(MQT_SC_QDMI_device_session_set_parameter( + session, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM2, file.size(), + file.data()), + QDMI_SUCCESS); + ASSERT_EQ(MQT_SC_QDMI_device_session_init(session), QDMI_SUCCESS); + EXPECT_EQ(queryName(session), "Custom SC"); + MQT_SC_QDMI_device_session_free(session); + + ASSERT_EQ(MQT_SC_QDMI_device_session_alloc(&session), QDMI_SUCCESS); + constexpr auto malformed = std::to_array("{"); + ASSERT_EQ(MQT_SC_QDMI_device_session_set_parameter( + session, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM1, + malformed.size(), malformed.data()), + QDMI_SUCCESS); + EXPECT_EQ(MQT_SC_QDMI_device_session_init(session), + QDMI_ERROR_INVALIDARGUMENT); + ASSERT_EQ(MQT_SC_QDMI_device_session_set_parameter( + session, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM1, + std::strlen(CUSTOM_SC) + 1, CUSTOM_SC), + QDMI_SUCCESS); + EXPECT_EQ(MQT_SC_QDMI_device_session_init(session), QDMI_SUCCESS); + EXPECT_EQ(MQT_SC_QDMI_device_session_set_parameter( + session, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM1, 1, ""), + QDMI_ERROR_BADSTATE); + MQT_SC_QDMI_device_session_free(session); +} + +TEST(ScRuntimeConfiguration, RejectsOperationOutsideCouplingMap) { + auto configuration = nlohmann::json::parse(CUSTOM_SC); + configuration["couplings"] = {{0, 1}}; + configuration["operations"][0]["sites"] = {{1, 0}}; + configuration["operations"][0]["siteOverrides"] = nlohmann::json::array(); + const auto serialized = configuration.dump(); + + MQT_SC_QDMI_Device_Session session = nullptr; + ASSERT_EQ(MQT_SC_QDMI_device_session_alloc(&session), QDMI_SUCCESS); + ASSERT_EQ(MQT_SC_QDMI_device_session_set_parameter( + session, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM1, + serialized.size() + 1, serialized.c_str()), + QDMI_SUCCESS); + EXPECT_EQ(MQT_SC_QDMI_device_session_init(session), + QDMI_ERROR_INVALIDARGUMENT); + MQT_SC_QDMI_device_session_free(session); +} + +TEST(ScRuntimeConfiguration, SessionsOwnIndependentModelsAndCalibration) { + auto* custom = initializedSession(); + MQT_SC_QDMI_Device_Session bundled = nullptr; + ASSERT_EQ(MQT_SC_QDMI_device_session_alloc(&bundled), QDMI_SUCCESS); + ASSERT_EQ(MQT_SC_QDMI_device_session_init(bundled), QDMI_SUCCESS); + + size_t customQubits = 0; + size_t bundledQubits = 0; + ASSERT_EQ(MQT_SC_QDMI_device_session_query_device_property( + custom, QDMI_DEVICE_PROPERTY_QUBITSNUM, sizeof(customQubits), + &customQubits, nullptr), + QDMI_SUCCESS); + ASSERT_EQ(MQT_SC_QDMI_device_session_query_device_property( + bundled, QDMI_DEVICE_PROPERTY_QUBITSNUM, sizeof(bundledQubits), + &bundledQubits, nullptr), + QDMI_SUCCESS); + EXPECT_EQ(customQubits, 2); + EXPECT_EQ(bundledQubits, 100); + + const auto sites = querySites(custom); + uint64_t t1 = 0; + ASSERT_EQ( + MQT_SC_QDMI_device_session_query_site_property( + custom, sites[1], QDMI_SITE_PROPERTY_T1, sizeof(t1), &t1, nullptr), + QDMI_SUCCESS); + EXPECT_EQ(t1, 90); + uint64_t t2 = 0; + ASSERT_EQ( + MQT_SC_QDMI_device_session_query_site_property( + custom, sites[1], QDMI_SITE_PROPERTY_T2, sizeof(t2), &t2, nullptr), + QDMI_SUCCESS); + EXPECT_EQ(t2, 200); + + auto* const operation = queryOperations(custom).front(); + uint64_t duration = 0; + double fidelity = 0.; + ASSERT_EQ(MQT_SC_QDMI_device_session_query_operation_property( + custom, operation, sites.size(), sites.data(), 0, nullptr, + QDMI_OPERATION_PROPERTY_DURATION, sizeof(duration), &duration, + nullptr), + QDMI_SUCCESS); + EXPECT_EQ(duration, 10); + ASSERT_EQ(MQT_SC_QDMI_device_session_query_operation_property( + custom, operation, sites.size(), sites.data(), 0, nullptr, + QDMI_OPERATION_PROPERTY_FIDELITY, sizeof(fidelity), &fidelity, + nullptr), + QDMI_SUCCESS); + EXPECT_DOUBLE_EQ(fidelity, 0.95); + + const std::array reverseSites{sites[1], sites[0]}; + ASSERT_EQ(MQT_SC_QDMI_device_session_query_operation_property( + custom, operation, reverseSites.size(), reverseSites.data(), 0, + nullptr, QDMI_OPERATION_PROPERTY_DURATION, sizeof(duration), + &duration, nullptr), + QDMI_SUCCESS); + EXPECT_EQ(duration, 20); + ASSERT_EQ(MQT_SC_QDMI_device_session_query_operation_property( + custom, operation, reverseSites.size(), reverseSites.data(), 0, + nullptr, QDMI_OPERATION_PROPERTY_FIDELITY, sizeof(fidelity), + &fidelity, nullptr), + QDMI_SUCCESS); + EXPECT_DOUBLE_EQ(fidelity, 0.9); + + auto* const bundledSite = querySites(bundled).front(); + EXPECT_EQ( + MQT_SC_QDMI_device_session_query_site_property( + custom, bundledSite, QDMI_SITE_PROPERTY_INDEX, 0, nullptr, nullptr), + QDMI_ERROR_INVALIDARGUMENT); + EXPECT_EQ(MQT_SC_QDMI_device_session_query_operation_property( + bundled, operation, 0, nullptr, 0, nullptr, + QDMI_OPERATION_PROPERTY_NAME, 0, nullptr, nullptr), + QDMI_ERROR_INVALIDARGUMENT); + + MQT_SC_QDMI_device_session_free(bundled); + MQT_SC_QDMI_device_session_free(custom); +} + +TEST(ScRuntimeConfiguration, SelectsEnvironmentAndExplicitFileSources) { + const ScopedEnvironmentVariable environmentJson( + "MQT_CORE_QDMI_SC_CONFIG_JSON", CUSTOM_SC); + + MQT_SC_QDMI_Device_Session environmentSession = nullptr; + ASSERT_EQ(MQT_SC_QDMI_device_session_alloc(&environmentSession), + QDMI_SUCCESS); + ASSERT_EQ(MQT_SC_QDMI_device_session_init(environmentSession), QDMI_SUCCESS); + EXPECT_EQ(queryName(environmentSession), "Custom SC"); + MQT_SC_QDMI_device_session_free(environmentSession); + + MQT_SC_QDMI_Device_Session explicitFileSession = nullptr; + ASSERT_EQ(MQT_SC_QDMI_device_session_alloc(&explicitFileSession), + QDMI_SUCCESS); + constexpr auto file = std::to_array(SC_DEVICE_JSON); + ASSERT_EQ(MQT_SC_QDMI_device_session_set_parameter( + explicitFileSession, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM2, + file.size(), file.data()), + QDMI_SUCCESS); + ASSERT_EQ(MQT_SC_QDMI_device_session_init(explicitFileSession), QDMI_SUCCESS); + EXPECT_EQ(queryName(explicitFileSession), "MQT SC Default QDMI Device"); + MQT_SC_QDMI_device_session_free(explicitFileSession); + + const ScopedEnvironmentVariable environmentFile( + "MQT_CORE_QDMI_SC_CONFIG_FILE", SC_DEVICE_JSON); + MQT_SC_QDMI_Device_Session conflictingEnvironmentSession = nullptr; + ASSERT_EQ(MQT_SC_QDMI_device_session_alloc(&conflictingEnvironmentSession), + QDMI_SUCCESS); + EXPECT_EQ(MQT_SC_QDMI_device_session_init(conflictingEnvironmentSession), + QDMI_ERROR_INVALIDARGUMENT); + MQT_SC_QDMI_device_session_free(conflictingEnvironmentSession); +} + +TEST(ScRuntimeConfiguration, MapsMissingExplicitFileToNotFound) { + MQT_SC_QDMI_Device_Session session = nullptr; + ASSERT_EQ(MQT_SC_QDMI_device_session_alloc(&session), QDMI_SUCCESS); + constexpr auto missing = + std::to_array("missing-sc-device-configuration.json"); + ASSERT_EQ(MQT_SC_QDMI_device_session_set_parameter( + session, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM2, missing.size(), + missing.data()), + QDMI_SUCCESS); + EXPECT_EQ(MQT_SC_QDMI_device_session_init(session), QDMI_ERROR_NOTFOUND); + MQT_SC_QDMI_device_session_free(session); +} + +TEST(ScRuntimeConfiguration, MissingCalibrationReturnsNotSupported) { + auto configuration = nlohmann::json::parse(CUSTOM_SC); + configuration["qubitProperties"]["defaults"] = nlohmann::json::object(); + configuration["qubitProperties"]["overrides"] = nlohmann::json::array(); + configuration["operations"][0].erase("duration"); + configuration["operations"][0].erase("fidelity"); + configuration["operations"][0]["siteOverrides"] = nlohmann::json::array(); + const auto serialized = configuration.dump(); + auto* session = initializedSession(serialized); + const auto sites = querySites(session); + auto* const operation = queryOperations(session).front(); + EXPECT_EQ(MQT_SC_QDMI_device_session_query_site_property( + session, sites[0], QDMI_SITE_PROPERTY_T1, 0, nullptr, nullptr), + QDMI_ERROR_NOTSUPPORTED); + EXPECT_EQ(MQT_SC_QDMI_device_session_query_operation_property( + session, operation, 2, sites.data(), 0, nullptr, + QDMI_OPERATION_PROPERTY_DURATION, 0, nullptr, nullptr), + QDMI_ERROR_NOTSUPPORTED); + MQT_SC_QDMI_device_session_free(session); +} + +TEST(ScRuntimeConfiguration, InitializesIndependentSessionsConcurrently) { + auto initializeAndQuery = [] { + auto* session = initializedSession(); + const auto name = queryName(session); + MQT_SC_QDMI_device_session_free(session); + return name; + }; + auto first = std::async(std::launch::async, initializeAndQuery); + auto second = std::async(std::launch::async, initializeAndQuery); + EXPECT_EQ(first.get(), "Custom SC"); + EXPECT_EQ(second.get(), "Custom SC"); +} + TEST_F(ScQDMISpecificationTest, SessionAlloc) { EXPECT_EQ(MQT_SC_QDMI_device_session_alloc(nullptr), QDMI_ERROR_INVALIDARGUMENT); diff --git a/test/qdmi/devices/sc/test_generator.cpp b/test/qdmi/devices/sc/test_generator.cpp deleted file mode 100644 index df27b2ef79..0000000000 --- a/test/qdmi/devices/sc/test_generator.cpp +++ /dev/null @@ -1,53 +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/devices/sc/Generator.hpp" - -#include -#include // NOLINT(misc-include-cleaner) - -#include - -namespace sc { -namespace { -// clang-tidy wants to include the forward header, but we have the full -// NOLINTNEXTLINE(misc-include-cleaner) -auto testPopulation(const nlohmann::json& json) -> void { - for (const auto& [key, value] : json.items()) { - if (value.is_array()) { - // Array field should have at least one default entry - EXPECT_GT(value.size(), 0) << "Array field '" << key - << "' should have at least one default entry"; - for (const auto& item : value) { - // Each entry in the array should not be null - EXPECT_FALSE(item.is_null()) - << "Array field '" << key << "' should not have null entries"; - testPopulation(item); - } - } else if (value.is_object()) { - testPopulation(value); - } - } -} -} // namespace - -TEST(ScGeneratorTest, WriteJSONSchema) { - std::ostringstream os; - EXPECT_NO_THROW(writeJSONSchema(os)); - // clang-tidy wants to include the forward header, but we have the full - // NOLINTNEXTLINE(misc-include-cleaner) - nlohmann::json json; - EXPECT_NO_THROW(json = nlohmann::json::parse(os.str())); - EXPECT_TRUE(json.is_object()); - EXPECT_GT(json.size(), 0); - testPopulation(json); -} - -} // namespace sc diff --git a/test/qdmi/driver/CMakeLists.txt b/test/qdmi/driver/CMakeLists.txt index 513a18d6cb..625febfce7 100644 --- a/test/qdmi/driver/CMakeLists.txt +++ b/test/qdmi/driver/CMakeLists.txt @@ -68,6 +68,7 @@ if(TARGET MQT::CoreQDMIDriver) "MQT_CORE_QDMI_NA_LIBRARY=\"$\"" "MQT_CORE_QDMI_CUSTOM_NA_FILE=\"${custom_na_file}\"" "MQT_CORE_QDMI_DEFAULT_NA_FILE=\"${PROJECT_SOURCE_DIR}/json/na/mqt-core-qdmi-na-device.json\"" + "MQT_CORE_QDMI_SC_LIBRARY=\"$\"" "TEST_DEVICE_LIBRARIES=std::array{ std::pair{\"$\", \"MQT_NA\"}, std::pair{\"$\", \"MQT_SC\"}, std::pair{\"$\", \"MQT_DDSIM\"} }" ) diff --git a/test/qdmi/driver/test_driver.cpp b/test/qdmi/driver/test_driver.cpp index fa9558a19b..f369b1ff5a 100644 --- a/test/qdmi/driver/test_driver.cpp +++ b/test/qdmi/driver/test_driver.cpp @@ -1218,6 +1218,80 @@ TEST(DeviceRegistrationTest, CustomOperationRetainsOwningDeviceSession) { EXPECT_EQ(operation.getQubitsNum(), 1); } +TEST(DeviceRegistrationTest, + ScRuntimeConfigurationSeparatesModelsUsingOneProviderLibrary) { + auto& driver = qdmi::Driver::get(); + static_cast(driver.registerDeviceIfAbsent( + {.id = "test.sc.runtime-one", + .library = MQT_CORE_QDMI_SC_LIBRARY, + .prefix = "MQT_SC", + .session = {.deviceConfiguration = + qdmi::InlineDeviceConfiguration{.json = R"({ + "schema-version":1, + "name":"SC runtime one", + "numQubits":1, + "durationUnit":{"unit":"ns","scaleFactor":1}, + "qubitProperties":{"defaults":{"t1":10,"t2":20},"overrides":[]}, + "couplings":[], + "operations":[{ + "name":"r", + "numParameters":2, + "numQubits":1, + "duration":7, + "fidelity":0.8 + }] + })"}}})); + static_cast(driver.registerDeviceIfAbsent( + {.id = "test.sc.runtime-two", + .library = MQT_CORE_QDMI_SC_LIBRARY, + .prefix = "MQT_SC", + .session = {.deviceConfiguration = + qdmi::InlineDeviceConfiguration{.json = R"({ + "schema-version":1, + "name":"SC runtime two", + "numQubits":2, + "durationUnit":{"unit":"us","scaleFactor":0.5}, + "qubitProperties":{"defaults":{},"overrides":[]}, + "couplings":[[1,0]], + "operations":[] + })"}}})); + + const auto first = fomac::Session::openDevice("test.sc.runtime-one"); + const auto second = fomac::Session::openDevice("test.sc.runtime-two"); + EXPECT_EQ(first.getName(), "SC runtime one"); + EXPECT_EQ(first.getQubitsNum(), 1); + const auto firstSites = first.getSites(); + ASSERT_EQ(firstSites.size(), 1); + EXPECT_EQ(firstSites[0].getT1(), 10); + EXPECT_EQ(firstSites[0].getT2(), 20); + const auto firstOperations = first.getOperations(); + ASSERT_EQ(firstOperations.size(), 1); + EXPECT_EQ(firstOperations[0].getName(), "r"); + EXPECT_EQ(firstOperations[0].getDuration({firstSites[0]}), 7); + EXPECT_EQ(firstOperations[0].getFidelity({firstSites[0]}), 0.8); + EXPECT_EQ(second.getName(), "SC runtime two"); + EXPECT_EQ(second.getQubitsNum(), 2); + const auto secondCouplingMap = second.getCouplingMap(); + ASSERT_TRUE(secondCouplingMap.has_value()); + ASSERT_EQ(secondCouplingMap->size(), 1); + + qdmi::DeviceSessionConfig configurationOverride; + configurationOverride.deviceConfiguration = + qdmi::InlineDeviceConfiguration{.json = R"({ + "schema-version":1, + "name":"SC per-open override", + "numQubits":3, + "durationUnit":{"unit":"ms","scaleFactor":2}, + "qubitProperties":{"defaults":{},"overrides":[]}, + "couplings":[[0,2]], + "operations":[] + })"}; + const auto overridden = + fomac::Session::openDevice("test.sc.runtime-one", configurationOverride); + EXPECT_EQ(overridden.getName(), "SC per-open override"); + EXPECT_EQ(overridden.getQubitsNum(), 3); +} + TEST(DeviceRegistrationTest, FreshJobRetainsItsDeviceSession) { registerSessionTestDevice(); std::optional job; diff --git a/test/qdmi/registry/CMakeLists.txt b/test/qdmi/registry/CMakeLists.txt index 918784ccd0..5e0f1115ca 100644 --- a/test/qdmi/registry/CMakeLists.txt +++ b/test/qdmi/registry/CMakeLists.txt @@ -10,7 +10,8 @@ set(TARGET_NAME mqt-core-qdmi-registry-test) if(TARGET MQT::CoreQDMIDriver) package_add_test(${TARGET_NAME} MQT::CoreQDMIDriver test_device_registry.cpp) - target_include_directories(${TARGET_NAME} PRIVATE ${PROJECT_SOURCE_DIR}/src/qdmi/driver) + target_include_directories(${TARGET_NAME} PRIVATE ${PROJECT_SOURCE_DIR}/src/qdmi/driver + ${PROJECT_SOURCE_DIR}/test) target_compile_definitions( ${TARGET_NAME} PRIVATE diff --git a/test/qdmi/registry/test_device_registry.cpp b/test/qdmi/registry/test_device_registry.cpp index e529ccde80..8be48e49ae 100644 --- a/test/qdmi/registry/test_device_registry.cpp +++ b/test/qdmi/registry/test_device_registry.cpp @@ -9,20 +9,18 @@ */ #include "DeviceRegistry.hpp" +#include "qdmi/TestUtils.hpp" #include "qdmi/driver/Driver.hpp" #include #include -#include #include #include -#include #include #include #include #include -#include #include #include @@ -56,55 +54,7 @@ class TemporaryDirectory { std::filesystem::path path_; }; -class ScopedEnvironmentVariable { -public: - ScopedEnvironmentVariable(std::string name, const std::string& value) - : name_(std::move(name)) { - if (const auto* previous = std::getenv(name_.c_str()); - previous != nullptr) { - previous_ = previous; - } - set(value); - } - - ~ScopedEnvironmentVariable() { - if (previous_) { - static_cast(setWithoutChecking(*previous_)); - } else { -#ifdef _WIN32 - static_cast(_putenv_s(name_.c_str(), "")); -#else - // NOLINTNEXTLINE(misc-include-cleaner) - static_cast(unsetenv(name_.c_str())); -#endif - } - } - - ScopedEnvironmentVariable(const ScopedEnvironmentVariable&) = delete; - ScopedEnvironmentVariable& - operator=(const ScopedEnvironmentVariable&) = delete; - ScopedEnvironmentVariable(ScopedEnvironmentVariable&&) = delete; - ScopedEnvironmentVariable& operator=(ScopedEnvironmentVariable&&) = delete; - -private: - void set(const std::string& value) const { - if (!setWithoutChecking(value)) { - throw std::runtime_error("Failed to set environment variable " + name_); - } - } - - [[nodiscard]] bool setWithoutChecking(const std::string& value) const { -#ifdef _WIN32 - return _putenv_s(name_.c_str(), value.c_str()) == 0; -#else - // NOLINTNEXTLINE(misc-include-cleaner) - return setenv(name_.c_str(), value.c_str(), 1) == 0; -#endif - } - - std::string name_; - std::optional previous_; -}; +using mqt::test::ScopedEnvironmentVariable; class ScopedCurrentPath { public: From ac44c09e59bc7a2824ed7aa6e569cd0cc6c2b842 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Fri, 14 Aug 2026 18:06:05 +0200 Subject: [PATCH 09/35] =?UTF-8?q?=E2=9C=A8=20Backport=20reusable=20IQM=20d?= =?UTF-8?q?evice=20models?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bundle strict runtime descriptions for IQM Garnet and Emerald with stable QDMI registry IDs. Reuse the superconducting provider library without loading it during registry discovery. Backports #1992. Assisted-by: GPT-5.6 via Codex --- CHANGELOG.md | 3 + cmake/AddMQTQDMIDevice.cmake | 67 +- docs/qdmi/sc_device.md | 31 +- .../qdmi/devices/sc/Configuration.hpp | 1 + include/mqt-core/qdmi/devices/sc/Device.hpp | 1 + json/sc/iqm-emerald.json | 1208 +++++++++++++++++ json/sc/iqm-garnet.json | 469 +++++++ src/qdmi/devices/sc/CMakeLists.txt | 7 +- src/qdmi/devices/sc/Configuration.cpp | 12 +- src/qdmi/devices/sc/Device.cpp | 10 + test/qdmi/devices/sc/CMakeLists.txt | 4 +- test/qdmi/devices/sc/test_configuration.cpp | 88 ++ test/qdmi/devices/sc/test_device.cpp | 29 +- test/qdmi/driver/CMakeLists.txt | 24 + .../driver/runtime_file_device/CMakeLists.txt | 15 +- test/qdmi/driver/test_driver.cpp | 10 +- test/qdmi/registry/test_device_registry.cpp | 19 +- 17 files changed, 1967 insertions(+), 31 deletions(-) create mode 100644 json/sc/iqm-emerald.json create mode 100644 json/sc/iqm-garnet.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 369eb6aa9f..b755583f9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,8 @@ releases may include breaking changes. position ([#2008], [#2010]) ([**@burgholzer**]) - ✨ Add PennyLane support for gate-based QDMI devices ([#2005]) ([**@burgholzer**]) +- ✨ Bundle reusable IQM Garnet and Emerald superconducting device models with + stable QDMI registry IDs ([#1992]) ([**@burgholzer**]) - ✨ Expose compressed vector and matrix DD serialization through bytes-based Python APIs ([#1983]) ([**@burgholzer**]) - ✨ Make superconducting QDMI devices runtime configurable with session-owned @@ -651,6 +653,7 @@ changelogs._ [#2010]: https://github.com/munich-quantum-toolkit/core/pull/2010 [#2008]: https://github.com/munich-quantum-toolkit/core/pull/2008 [#2005]: https://github.com/munich-quantum-toolkit/core/pull/2005 +[#1992]: https://github.com/munich-quantum-toolkit/core/pull/1992 [#1984]: https://github.com/munich-quantum-toolkit/core/pull/1984 [#1983]: https://github.com/munich-quantum-toolkit/core/pull/1983 [#1980]: https://github.com/munich-quantum-toolkit/core/pull/1980 diff --git a/cmake/AddMQTQDMIDevice.cmake b/cmake/AddMQTQDMIDevice.cmake index 392e059036..951975090f 100644 --- a/cmake/AddMQTQDMIDevice.cmake +++ b/cmake/AddMQTQDMIDevice.cmake @@ -22,7 +22,7 @@ endfunction() # Configure and register a relocatable built-in QDMI device. The generated fragment is emitted # beside the runtime library in both build and install trees. function(mqt_configure_qdmi_device target) - cmake_parse_arguments(ARG "" "ID;PREFIX" "RUNTIME_FILES" ${ARGN}) + cmake_parse_arguments(ARG "" "ID;PREFIX" "RUNTIME_FILES;CONFIGURATIONS" ${ARGN}) if(NOT TARGET ${target}) message(FATAL_ERROR "Unknown QDMI device target: ${target}") endif() @@ -38,12 +38,65 @@ function(mqt_configure_qdmi_device target) _mqt_qdmi_json_escape(device_id "${ARG_ID}") _mqt_qdmi_json_escape(device_prefix "${ARG_PREFIX}") + set(runtime_file_names) + foreach(runtime_file IN LISTS ARG_RUNTIME_FILES) + get_filename_component(runtime_file_name "${runtime_file}" NAME) + list(APPEND runtime_file_names "${runtime_file_name}") + add_custom_command( + TARGET ${target} + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different "${runtime_file}" + "$/${runtime_file_name}") + endforeach() + + set(device_entries + " {\n \"id\": \"${device_id}\",\n \"library\": \"$\",\n \"prefix\": \"${device_prefix}\",\n \"enabled\": true\n }" + ) + set(configuration_ids "${ARG_ID}") + foreach(configuration IN LISTS ARG_CONFIGURATIONS) + string(REPLACE "|" ";" configuration_parts "${configuration}") + list(LENGTH configuration_parts configuration_length) + if(NOT configuration_length EQUAL 2) + message( + FATAL_ERROR + "QDMI configuration '${configuration}' must use the form '|'" + ) + endif() + list(GET configuration_parts 0 configuration_id) + list(GET configuration_parts 1 configuration_file) + if(configuration_id STREQUAL "" OR configuration_file STREQUAL "") + message( + FATAL_ERROR + "QDMI configuration '${configuration}' must use a non-empty device ID and runtime file name" + ) + endif() + list(FIND configuration_ids "${configuration_id}" configuration_id_index) + if(NOT configuration_id_index EQUAL -1) + message(FATAL_ERROR "Duplicate QDMI device ID '${configuration_id}'") + endif() + list(APPEND configuration_ids "${configuration_id}") + list(FIND runtime_file_names "${configuration_file}" runtime_file_index) + if(runtime_file_index EQUAL -1) + message( + FATAL_ERROR + "QDMI configuration '${configuration_id}' refers to unknown runtime file '${configuration_file}'" + ) + endif() + _mqt_qdmi_json_escape(configuration_id "${configuration_id}") + _mqt_qdmi_json_escape(configuration_file "${configuration_file}") + string( + APPEND + device_entries + ",\n {\n \"id\": \"${configuration_id}\",\n \"library\": \"$\",\n \"prefix\": \"${device_prefix}\",\n \"enabled\": true,\n \"session\": {\n \"device-config\": {\n \"file\": \"${configuration_file}\"\n }\n }\n }" + ) + endforeach() + 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\": \"${device_id}\",\n \"library\": \"$\",\n \"prefix\": \"${device_prefix}\",\n \"enabled\": true\n }\n ]\n }\n}\n" + "{\n \"schema-version\": 1,\n \"qdmi\": {\n \"devices\": [\n${device_entries}\n ]\n }\n}\n" ) add_custom_command( @@ -51,16 +104,6 @@ function(mqt_configure_qdmi_device target) POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different "${fragment}" "$/${target}.qdmi.json") - set(runtime_file_names) - foreach(runtime_file IN LISTS ARG_RUNTIME_FILES) - get_filename_component(runtime_file_name "${runtime_file}" NAME) - list(APPEND runtime_file_names "${runtime_file_name}") - add_custom_command( - TARGET ${target} - POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different "${runtime_file}" - "$/${runtime_file_name}") - endforeach() set_target_properties( ${target} PROPERTIES QDMI_DEVICE_ID "${ARG_ID}" diff --git a/docs/qdmi/sc_device.md b/docs/qdmi/sc_device.md index e872275259..833657056a 100644 --- a/docs/qdmi/sc_device.md +++ b/docs/qdmi/sc_device.md @@ -35,7 +35,7 @@ and fidelity, and optional tuple-specific overrides. "durationUnit": {"unit": "ns", "scaleFactor": 1.0}, "qubitProperties": { "defaults": {"t1": 100000, "t2": 150000}, - "overrides": [{"qubit": 1, "t1": 95000}] + "overrides": [{"qubit": 1, "name": "QB2", "t1": 95000}] }, "couplings": [[0, 1]], "operations": [ @@ -66,11 +66,32 @@ exact ordered edge in the coupling map. Every site override must select one of the operation's supported tuples and supply a duration, fidelity, or both. When operation duration or fidelity is queried, a matching tuple override wins -over the operation default. Missing data returns `QDMI_ERROR_NOTSUPPORTED`. -Qubit T1 and T2 use a per-qubit override first and the qubit default second. The -order of a configured site tuple is significant. Handles from another session -are rejected. +over the operation default. Missing data returns `QDMI_ERROR_NOTSUPPORTED`. Each +qubit override may also provide a non-empty site name. Qubit T1 and T2 use a +per-qubit override first and the qubit default second. The order of a configured +site tuple is significant. Handles from another session are rejected. The calibration values bundled in `json/sc/mqt-core-qdmi-sc-device.json` are synthetic examples for testing and documentation; they are not measurements of physical hardware. + +## Bundled IQM models + +The SC provider also installs `json/sc/iqm-garnet.json` and +`json/sc/iqm-emerald.json`. They model the 20-qubit, 30-edge Garnet and +54-qubit, 90-edge Emerald architectures and expose the portable IQM operation +set `r`, `cz`, and `measure`. Their stable registry IDs are `mqt.sc.iqm.garnet` +and `mqt.sc.iqm.emerald`. + +The snapshots were derived from authenticated IQM architecture and calibration +data retrieved on 2 August 2026 through +[QDMI-on-IQM](https://github.com/iqm-finland/QDMI-on-IQM). Site names and +topology came from each static architecture, operation loci from its default +dynamic architecture, and available T1, T2, and per-locus fidelities from its +default calibration-set quality metrics. Coherence times were rounded to the +nearest nanosecond and stored as integral values using unit `us` and scale +factor `0.001`. They are historical compiler-facing snapshots, not claims about +current hardware calibration. + +The IQM interface did not report operation durations, so the files intentionally +omit them. diff --git a/include/mqt-core/qdmi/devices/sc/Configuration.hpp b/include/mqt-core/qdmi/devices/sc/Configuration.hpp index 65c908e395..ae43bf9aa7 100644 --- a/include/mqt-core/qdmi/devices/sc/Configuration.hpp +++ b/include/mqt-core/qdmi/devices/sc/Configuration.hpp @@ -38,6 +38,7 @@ struct Device { struct QubitOverride : QubitCalibration { uint64_t qubit = 0; + std::optional name; }; struct QubitProperties { diff --git a/include/mqt-core/qdmi/devices/sc/Device.hpp b/include/mqt-core/qdmi/devices/sc/Device.hpp index cbd5117fa5..18d2ebc79d 100644 --- a/include/mqt-core/qdmi/devices/sc/Device.hpp +++ b/include/mqt-core/qdmi/devices/sc/Device.hpp @@ -30,6 +30,7 @@ struct MQT_SC_QDMI_Device_Session_impl_d; struct MQT_SC_QDMI_Site_impl_d { MQT_SC_QDMI_Device_Session_impl_d* owner = nullptr; uint64_t id = 0; + std::optional name; std::optional t1; std::optional t2; diff --git a/json/sc/iqm-emerald.json b/json/sc/iqm-emerald.json new file mode 100644 index 0000000000..2c1946d0ba --- /dev/null +++ b/json/sc/iqm-emerald.json @@ -0,0 +1,1208 @@ +{ + "schema-version": 1, + "name": "IQM Emerald", + "numQubits": 54, + "durationUnit": { + "unit": "us", + "scaleFactor": 0.001 + }, + "qubitProperties": { + "defaults": {}, + "overrides": [ + { + "qubit": 0, + "name": "QB1", + "t1": 46392, + "t2": 18969 + }, + { + "qubit": 1, + "name": "QB2", + "t1": 33007, + "t2": 16460 + }, + { + "qubit": 2, + "name": "QB3", + "t1": 38653, + "t2": 38725 + }, + { + "qubit": 3, + "name": "QB4", + "t1": 28816, + "t2": 11451 + }, + { + "qubit": 4, + "name": "QB5", + "t1": 47439, + "t2": 14665 + }, + { + "qubit": 5, + "name": "QB6", + "t1": 30969, + "t2": 11164 + }, + { + "qubit": 6, + "name": "QB7", + "t1": 58892, + "t2": 19023 + }, + { + "qubit": 7, + "name": "QB8", + "t1": 59127, + "t2": 49134 + }, + { + "qubit": 8, + "name": "QB9", + "t1": 44851, + "t2": 13052 + }, + { + "qubit": 9, + "name": "QB10", + "t1": 34595, + "t2": 23058 + }, + { + "qubit": 10, + "name": "QB11", + "t1": 49730, + "t2": 16047 + }, + { + "qubit": 11, + "name": "QB12", + "t1": 57990, + "t2": 18641 + }, + { + "qubit": 12, + "name": "QB13", + "t1": 37552, + "t2": 13574 + }, + { + "qubit": 13, + "name": "QB14", + "t1": 60939, + "t2": 15828 + }, + { + "qubit": 14, + "name": "QB15", + "t1": 53111, + "t2": 24942 + }, + { + "qubit": 15, + "name": "QB16", + "t1": 12839, + "t2": 5184 + }, + { + "qubit": 16, + "name": "QB17", + "t1": 47534, + "t2": 18994 + }, + { + "qubit": 17, + "name": "QB18", + "t1": 56827, + "t2": 16352 + }, + { + "qubit": 18, + "name": "QB19", + "t1": 66474, + "t2": 26253 + }, + { + "qubit": 19, + "name": "QB20", + "t1": 47973, + "t2": 13827 + }, + { + "qubit": 20, + "name": "QB21", + "t1": 72091, + "t2": 15125 + }, + { + "qubit": 21, + "name": "QB22", + "t1": 39443, + "t2": 14638 + }, + { + "qubit": 22, + "name": "QB23", + "t1": 55778, + "t2": 27195 + }, + { + "qubit": 23, + "name": "QB24", + "t1": 55772, + "t2": 22297 + }, + { + "qubit": 24, + "name": "QB25", + "t2": 6886 + }, + { + "qubit": 25, + "name": "QB26", + "t1": 26525, + "t2": 7568 + }, + { + "qubit": 26, + "name": "QB27", + "t1": 63437, + "t2": 11303 + }, + { + "qubit": 27, + "name": "QB28", + "t1": 62264, + "t2": 8956 + }, + { + "qubit": 28, + "name": "QB29", + "t1": 61288, + "t2": 15118 + }, + { + "qubit": 29, + "name": "QB30", + "t1": 57094, + "t2": 23073 + }, + { + "qubit": 30, + "name": "QB31", + "t1": 48601, + "t2": 14956 + }, + { + "qubit": 31, + "name": "QB32", + "t1": 72002, + "t2": 22335 + }, + { + "qubit": 32, + "name": "QB33", + "t1": 47763, + "t2": 8608 + }, + { + "qubit": 33, + "name": "QB34", + "t1": 63844, + "t2": 8466 + }, + { + "qubit": 34, + "name": "QB35", + "t1": 68596, + "t2": 15522 + }, + { + "qubit": 35, + "name": "QB36", + "t1": 63416, + "t2": 8020 + }, + { + "qubit": 36, + "name": "QB37", + "t1": 62283, + "t2": 17850 + }, + { + "qubit": 37, + "name": "QB38", + "t1": 58796, + "t2": 10178 + }, + { + "qubit": 38, + "name": "QB39", + "t1": 43527, + "t2": 29849 + }, + { + "qubit": 39, + "name": "QB40", + "t1": 66368, + "t2": 17218 + }, + { + "qubit": 40, + "name": "QB41", + "t1": 62758, + "t2": 2844 + }, + { + "qubit": 41, + "name": "QB42", + "t1": 81317, + "t2": 17700 + }, + { + "qubit": 42, + "name": "QB43", + "t1": 56373, + "t2": 10539 + }, + { + "qubit": 43, + "name": "QB44", + "t1": 54740, + "t2": 33183 + }, + { + "qubit": 44, + "name": "QB45", + "t1": 26947, + "t2": 11212 + }, + { + "qubit": 45, + "name": "QB46", + "t1": 70432, + "t2": 42362 + }, + { + "qubit": 46, + "name": "QB47", + "t1": 55818, + "t2": 11454 + }, + { + "qubit": 47, + "name": "QB48", + "t1": 68798, + "t2": 5847 + }, + { + "qubit": 48, + "name": "QB49", + "t1": 72418, + "t2": 18075 + }, + { + "qubit": 49, + "name": "QB50", + "t1": 64957, + "t2": 29958 + }, + { + "qubit": 50, + "name": "QB51", + "t1": 84468, + "t2": 29219 + }, + { + "qubit": 51, + "name": "QB52", + "t1": 75117, + "t2": 21134 + }, + { + "qubit": 52, + "name": "QB53", + "t1": 81721, + "t2": 18444 + }, + { + "qubit": 53, + "name": "QB54", + "t1": 38356, + "t2": 11444 + } + ] + }, + "couplings": [ + [0, 1], + [0, 4], + [1, 5], + [2, 3], + [2, 8], + [3, 4], + [3, 9], + [4, 5], + [4, 10], + [5, 6], + [5, 11], + [6, 12], + [7, 8], + [7, 15], + [8, 9], + [8, 16], + [9, 10], + [9, 17], + [10, 11], + [10, 18], + [11, 12], + [11, 19], + [12, 13], + [12, 20], + [13, 21], + [14, 15], + [14, 22], + [15, 16], + [15, 23], + [16, 17], + [16, 24], + [17, 18], + [17, 25], + [18, 19], + [18, 26], + [19, 20], + [19, 27], + [20, 21], + [20, 28], + [21, 29], + [22, 23], + [23, 24], + [23, 31], + [24, 25], + [24, 32], + [25, 26], + [25, 33], + [26, 27], + [26, 34], + [27, 28], + [27, 35], + [28, 29], + [28, 36], + [29, 30], + [29, 37], + [30, 38], + [31, 32], + [31, 39], + [32, 33], + [32, 40], + [33, 34], + [33, 41], + [34, 35], + [34, 42], + [35, 36], + [35, 43], + [36, 37], + [36, 44], + [37, 38], + [37, 45], + [39, 40], + [40, 41], + [40, 46], + [41, 42], + [41, 47], + [42, 43], + [42, 48], + [43, 44], + [43, 49], + [44, 45], + [44, 50], + [46, 47], + [47, 48], + [47, 51], + [48, 49], + [48, 52], + [49, 50], + [49, 53], + [51, 52], + [52, 53] + ], + "operations": [ + { + "name": "r", + "numQubits": 1, + "numParameters": 2, + "siteOverrides": [ + { + "sites": [0], + "fidelity": 0.9996837259582892 + }, + { + "sites": [1], + "fidelity": 0.9997052599579568 + }, + { + "sites": [2], + "fidelity": 0.999658146168424 + }, + { + "sites": [3], + "fidelity": 0.9996382180977418 + }, + { + "sites": [4], + "fidelity": 0.9997163301730092 + }, + { + "sites": [5], + "fidelity": 0.9993081871591356 + }, + { + "sites": [6], + "fidelity": 0.9994468373023766 + }, + { + "sites": [7], + "fidelity": 0.9997545944945864 + }, + { + "sites": [8], + "fidelity": 0.9996344151472129 + }, + { + "sites": [9], + "fidelity": 0.9994028358077222 + }, + { + "sites": [10], + "fidelity": 0.9997369849275862 + }, + { + "sites": [11], + "fidelity": 0.9997607298339284 + }, + { + "sites": [12], + "fidelity": 0.9995638022714364 + }, + { + "sites": [13], + "fidelity": 0.99973282233036 + }, + { + "sites": [14], + "fidelity": 0.9997443095078864 + }, + { + "sites": [15], + "fidelity": 0.998390759467938 + }, + { + "sites": [16], + "fidelity": 0.9996245569346616 + }, + { + "sites": [17], + "fidelity": 0.9997296669685454 + }, + { + "sites": [18], + "fidelity": 0.9992727053295768 + }, + { + "sites": [19], + "fidelity": 0.9997355273183044 + }, + { + "sites": [20], + "fidelity": 0.9997940915645592 + }, + { + "sites": [21], + "fidelity": 0.999713295982722 + }, + { + "sites": [22], + "fidelity": 0.9997576517990368 + }, + { + "sites": [23], + "fidelity": 0.9997351448227784 + }, + { + "sites": [24], + "fidelity": 0.9220039436040088 + }, + { + "sites": [25], + "fidelity": 0.999106430711988 + }, + { + "sites": [26], + "fidelity": 0.9997356212159578 + }, + { + "sites": [27], + "fidelity": 0.999747431092373 + }, + { + "sites": [28], + "fidelity": 0.9997570401314052 + }, + { + "sites": [29], + "fidelity": 0.9996119559220664 + }, + { + "sites": [30], + "fidelity": 0.9996406964898988 + }, + { + "sites": [31], + "fidelity": 0.9997828898342456 + }, + { + "sites": [32], + "fidelity": 0.9995917591656592 + }, + { + "sites": [33], + "fidelity": 0.999762795756679 + }, + { + "sites": [34], + "fidelity": 0.9992465640814856 + }, + { + "sites": [35], + "fidelity": 0.9997454406169174 + }, + { + "sites": [36], + "fidelity": 0.9976289815379576 + }, + { + "sites": [37], + "fidelity": 0.9996972469343436 + }, + { + "sites": [38], + "fidelity": 0.9998049596655614 + }, + { + "sites": [39], + "fidelity": 0.9998079523647964 + }, + { + "sites": [40], + "fidelity": 0.9996627803644492 + }, + { + "sites": [41], + "fidelity": 0.9992507642966924 + }, + { + "sites": [42], + "fidelity": 0.9997836605333504 + }, + { + "sites": [43], + "fidelity": 0.9993100166735815 + }, + { + "sites": [44], + "fidelity": 0.9996204579320964 + }, + { + "sites": [45], + "fidelity": 0.9996397465373394 + }, + { + "sites": [46], + "fidelity": 0.999620748967918 + }, + { + "sites": [47], + "fidelity": 0.9997172828117402 + }, + { + "sites": [48], + "fidelity": 0.9989578543033786 + }, + { + "sites": [49], + "fidelity": 0.9997024385275626 + }, + { + "sites": [50], + "fidelity": 0.9995567388383506 + }, + { + "sites": [51], + "fidelity": 0.9997402987838522 + }, + { + "sites": [52], + "fidelity": 0.9997563213467264 + }, + { + "sites": [53], + "fidelity": 0.999578650755719 + } + ] + }, + { + "name": "cz", + "numQubits": 2, + "numParameters": 0, + "siteOverrides": [ + { + "sites": [0, 1], + "fidelity": 0.9969413345034056 + }, + { + "sites": [0, 4], + "fidelity": 0.9971538791849028 + }, + { + "sites": [1, 5], + "fidelity": 0.9896068069584034 + }, + { + "sites": [2, 3], + "fidelity": 0.9936850385964238 + }, + { + "sites": [2, 8], + "fidelity": 0.9963780209952412 + }, + { + "sites": [3, 4], + "fidelity": 0.9939031989226854 + }, + { + "sites": [3, 9], + "fidelity": 0.9970910907254354 + }, + { + "sites": [4, 5], + "fidelity": 0.996991775771158 + }, + { + "sites": [4, 10], + "fidelity": 0.9974597649803834 + }, + { + "sites": [5, 6], + "fidelity": 0.9966308505035064 + }, + { + "sites": [5, 11], + "fidelity": 0.9941798555018222 + }, + { + "sites": [6, 12], + "fidelity": 0.9956867925342664 + }, + { + "sites": [7, 8], + "fidelity": 0.9976130955357776 + }, + { + "sites": [7, 15], + "fidelity": 0.9927742975291184 + }, + { + "sites": [8, 9], + "fidelity": 0.9936731656682896 + }, + { + "sites": [8, 16], + "fidelity": 0.9970517105492036 + }, + { + "sites": [9, 10], + "fidelity": 0.9975956000466732 + }, + { + "sites": [9, 17], + "fidelity": 0.9944571851924452 + }, + { + "sites": [10, 11], + "fidelity": 0.9983917808888104 + }, + { + "sites": [10, 18], + "fidelity": 0.9950968161349752 + }, + { + "sites": [11, 12], + "fidelity": 0.96312952210938 + }, + { + "sites": [11, 19], + "fidelity": 0.9893629174089708 + }, + { + "sites": [12, 13], + "fidelity": 0.9940207675618336 + }, + { + "sites": [12, 20], + "fidelity": 0.995107544118774 + }, + { + "sites": [13, 21], + "fidelity": 0.9944177501984252 + }, + { + "sites": [14, 15], + "fidelity": 0.996034253387076 + }, + { + "sites": [15, 16], + "fidelity": 0.9950864680626792 + }, + { + "sites": [15, 23], + "fidelity": 0.9965054309654308 + }, + { + "sites": [16, 17], + "fidelity": 0.9957804025185316 + }, + { + "sites": [17, 18], + "fidelity": 0.9949181486814976 + }, + { + "sites": [17, 25], + "fidelity": 0.992047426076022 + }, + { + "sites": [18, 19], + "fidelity": 0.9920871546478374 + }, + { + "sites": [18, 26], + "fidelity": 0.9938891759644176 + }, + { + "sites": [19, 20], + "fidelity": 0.995357196393145 + }, + { + "sites": [19, 27], + "fidelity": 0.9976887326012306 + }, + { + "sites": [20, 21], + "fidelity": 0.9950802073855424 + }, + { + "sites": [20, 28], + "fidelity": 0.9978913041787768 + }, + { + "sites": [21, 29], + "fidelity": 0.9976827959004356 + }, + { + "sites": [22, 23], + "fidelity": 0.995369943200364 + }, + { + "sites": [25, 26], + "fidelity": 0.9955806042140616 + }, + { + "sites": [25, 33], + "fidelity": 0.9958010088606108 + }, + { + "sites": [26, 27], + "fidelity": 0.9962345356717062 + }, + { + "sites": [26, 34], + "fidelity": 0.9981417551356184 + }, + { + "sites": [27, 28], + "fidelity": 0.9967739645334112 + }, + { + "sites": [27, 35], + "fidelity": 0.9980532728665198 + }, + { + "sites": [28, 29], + "fidelity": 0.9977659471572182 + }, + { + "sites": [28, 36], + "fidelity": 0.9982152000974854 + }, + { + "sites": [29, 30], + "fidelity": 0.9920766762220788 + }, + { + "sites": [29, 37], + "fidelity": 0.9918246356284744 + }, + { + "sites": [30, 38], + "fidelity": 0.994813078599712 + }, + { + "sites": [31, 32], + "fidelity": 0.9967666143627752 + }, + { + "sites": [31, 39], + "fidelity": 0.998131814461782 + }, + { + "sites": [32, 33], + "fidelity": 0.9974524240802696 + }, + { + "sites": [32, 40], + "fidelity": 0.9958471942184204 + }, + { + "sites": [33, 34], + "fidelity": 0.9974062683079248 + }, + { + "sites": [33, 41], + "fidelity": 0.9976248923184052 + }, + { + "sites": [34, 35], + "fidelity": 0.9959294668088978 + }, + { + "sites": [34, 42], + "fidelity": 0.9812245811750008 + }, + { + "sites": [35, 36], + "fidelity": 0.9980348808276452 + }, + { + "sites": [35, 43], + "fidelity": 0.9950018157241084 + }, + { + "sites": [36, 37], + "fidelity": 0.993699267633727 + }, + { + "sites": [36, 44], + "fidelity": 0.9959674850407992 + }, + { + "sites": [37, 38], + "fidelity": 0.9973622226740442 + }, + { + "sites": [37, 45], + "fidelity": 0.9976887076822556 + }, + { + "sites": [39, 40], + "fidelity": 0.9949481790019008 + }, + { + "sites": [40, 41], + "fidelity": 0.992872635595606 + }, + { + "sites": [40, 46], + "fidelity": 0.9974552435400168 + }, + { + "sites": [41, 42], + "fidelity": 0.9824111076495718 + }, + { + "sites": [41, 47], + "fidelity": 0.9965086263649008 + }, + { + "sites": [42, 43], + "fidelity": 0.996330798077716 + }, + { + "sites": [42, 48], + "fidelity": 0.9956021949824008 + }, + { + "sites": [43, 44], + "fidelity": 0.997855106272212 + }, + { + "sites": [43, 49], + "fidelity": 0.9971746806292596 + }, + { + "sites": [44, 45], + "fidelity": 0.9978944398345154 + }, + { + "sites": [44, 50], + "fidelity": 0.9982026716773316 + }, + { + "sites": [46, 47], + "fidelity": 0.9974461582526928 + }, + { + "sites": [47, 48], + "fidelity": 0.998320780696598 + }, + { + "sites": [47, 51], + "fidelity": 0.9902910606789695 + }, + { + "sites": [49, 50], + "fidelity": 0.9975735768660834 + }, + { + "sites": [51, 52], + "fidelity": 0.998132940868902 + }, + { + "sites": [52, 53], + "fidelity": 0.9979934112597808 + } + ] + }, + { + "name": "measure", + "numQubits": 1, + "numParameters": 0, + "siteOverrides": [ + { + "sites": [0], + "fidelity": 0.9918 + }, + { + "sites": [1], + "fidelity": 0.99165 + }, + { + "sites": [2], + "fidelity": 0.98575 + }, + { + "sites": [3], + "fidelity": 0.9845 + }, + { + "sites": [4], + "fidelity": 0.98875 + }, + { + "sites": [5], + "fidelity": 0.9506 + }, + { + "sites": [6], + "fidelity": 0.98965 + }, + { + "sites": [7], + "fidelity": 0.98915 + }, + { + "sites": [8], + "fidelity": 0.9928 + }, + { + "sites": [9], + "fidelity": 0.98985 + }, + { + "sites": [10], + "fidelity": 0.99345 + }, + { + "sites": [11], + "fidelity": 0.9931 + }, + { + "sites": [12], + "fidelity": 0.99355 + }, + { + "sites": [13], + "fidelity": 0.9885 + }, + { + "sites": [14], + "fidelity": 0.9889 + }, + { + "sites": [15], + "fidelity": 0.98155 + }, + { + "sites": [16], + "fidelity": 0.99385 + }, + { + "sites": [17], + "fidelity": 0.9886 + }, + { + "sites": [18], + "fidelity": 0.9903 + }, + { + "sites": [19], + "fidelity": 0.99475 + }, + { + "sites": [20], + "fidelity": 0.9911 + }, + { + "sites": [21], + "fidelity": 0.9895 + }, + { + "sites": [22], + "fidelity": 0.97955 + }, + { + "sites": [23], + "fidelity": 0.9885 + }, + { + "sites": [24], + "fidelity": 0.9842 + }, + { + "sites": [25], + "fidelity": 0.9655 + }, + { + "sites": [26], + "fidelity": 0.98735 + }, + { + "sites": [27], + "fidelity": 0.98175 + }, + { + "sites": [28], + "fidelity": 0.99005 + }, + { + "sites": [29], + "fidelity": 0.99185 + }, + { + "sites": [30], + "fidelity": 0.9878 + }, + { + "sites": [31], + "fidelity": 0.98825 + }, + { + "sites": [32], + "fidelity": 0.987 + }, + { + "sites": [33], + "fidelity": 0.98445 + }, + { + "sites": [34], + "fidelity": 0.9771 + }, + { + "sites": [35], + "fidelity": 0.98335 + }, + { + "sites": [36], + "fidelity": 0.98375 + }, + { + "sites": [37], + "fidelity": 0.9881 + }, + { + "sites": [38], + "fidelity": 0.99525 + }, + { + "sites": [39], + "fidelity": 0.98875 + }, + { + "sites": [40], + "fidelity": 0.98535 + }, + { + "sites": [41], + "fidelity": 0.9789 + }, + { + "sites": [42], + "fidelity": 0.98905 + }, + { + "sites": [43], + "fidelity": 0.98765 + }, + { + "sites": [44], + "fidelity": 0.9873 + }, + { + "sites": [45], + "fidelity": 0.98355 + }, + { + "sites": [46], + "fidelity": 0.98895 + }, + { + "sites": [47], + "fidelity": 0.98605 + }, + { + "sites": [48], + "fidelity": 0.96875 + }, + { + "sites": [49], + "fidelity": 0.9918 + }, + { + "sites": [50], + "fidelity": 0.9828 + }, + { + "sites": [51], + "fidelity": 0.9898 + }, + { + "sites": [52], + "fidelity": 0.9856 + }, + { + "sites": [53], + "fidelity": 0.98915 + } + ] + } + ] +} diff --git a/json/sc/iqm-garnet.json b/json/sc/iqm-garnet.json new file mode 100644 index 0000000000..8138b2c8c9 --- /dev/null +++ b/json/sc/iqm-garnet.json @@ -0,0 +1,469 @@ +{ + "schema-version": 1, + "name": "IQM Garnet", + "numQubits": 20, + "durationUnit": { + "unit": "us", + "scaleFactor": 0.001 + }, + "qubitProperties": { + "defaults": {}, + "overrides": [ + { + "qubit": 0, + "name": "QB1", + "t1": 26626, + "t2": 8376 + }, + { + "qubit": 1, + "name": "QB2", + "t1": 31464, + "t2": 7486 + }, + { + "qubit": 2, + "name": "QB3", + "t1": 19788, + "t2": 5840 + }, + { + "qubit": 3, + "name": "QB4", + "t1": 20631, + "t2": 8074 + }, + { + "qubit": 4, + "name": "QB5", + "t1": 39421, + "t2": 8333 + }, + { + "qubit": 5, + "name": "QB6", + "t1": 40760, + "t2": 5595 + }, + { + "qubit": 6, + "name": "QB7", + "t1": 40486, + "t2": 9095 + }, + { + "qubit": 7, + "name": "QB8", + "t1": 22847, + "t2": 7863 + }, + { + "qubit": 8, + "name": "QB9", + "t1": 39431, + "t2": 10938 + }, + { + "qubit": 9, + "name": "QB10", + "t1": 22077, + "t2": 5460 + }, + { + "qubit": 10, + "name": "QB11", + "t1": 40756, + "t2": 11069 + }, + { + "qubit": 11, + "name": "QB12", + "t1": 39781, + "t2": 14250 + }, + { + "qubit": 12, + "name": "QB13", + "t1": 38259, + "t2": 5252 + }, + { + "qubit": 13, + "name": "QB14", + "t1": 33608, + "t2": 6569 + }, + { + "qubit": 14, + "name": "QB15", + "t1": 39013, + "t2": 5953 + }, + { + "qubit": 15, + "name": "QB16", + "t1": 41590, + "t2": 6138 + }, + { + "qubit": 16, + "name": "QB17", + "t1": 41016, + "t2": 13593 + }, + { + "qubit": 17, + "name": "QB18", + "t1": 44688, + "t2": 5943 + }, + { + "qubit": 18, + "name": "QB19", + "t1": 46327, + "t2": 8782 + }, + { + "qubit": 19, + "name": "QB20", + "t1": 36131, + "t2": 22737 + } + ] + }, + "couplings": [ + [0, 1], + [0, 3], + [1, 4], + [2, 3], + [2, 7], + [3, 4], + [3, 8], + [4, 5], + [4, 9], + [5, 6], + [5, 10], + [6, 11], + [7, 8], + [7, 12], + [8, 9], + [8, 13], + [9, 10], + [9, 14], + [10, 11], + [10, 15], + [11, 16], + [12, 13], + [13, 14], + [13, 17], + [14, 15], + [14, 18], + [15, 16], + [15, 19], + [17, 18], + [18, 19] + ], + "operations": [ + { + "name": "r", + "numQubits": 1, + "numParameters": 2, + "siteOverrides": [ + { + "sites": [0], + "fidelity": 0.9989764855541426 + }, + { + "sites": [1], + "fidelity": 0.9994137341974704 + }, + { + "sites": [2], + "fidelity": 0.9988162826876488 + }, + { + "sites": [3], + "fidelity": 0.999275463615636 + }, + { + "sites": [4], + "fidelity": 0.9993874406823252 + }, + { + "sites": [5], + "fidelity": 0.9991834975710048 + }, + { + "sites": [6], + "fidelity": 0.9973521795708336 + }, + { + "sites": [7], + "fidelity": 0.9993508422282292 + }, + { + "sites": [8], + "fidelity": 0.99863732026566 + }, + { + "sites": [9], + "fidelity": 0.9986913070662058 + }, + { + "sites": [10], + "fidelity": 0.9994306432829452 + }, + { + "sites": [11], + "fidelity": 0.9962689684191476 + }, + { + "sites": [12], + "fidelity": 0.9972606059400604 + }, + { + "sites": [13], + "fidelity": 0.9992887068659668 + }, + { + "sites": [14], + "fidelity": 0.9991463242195284 + }, + { + "sites": [15], + "fidelity": 0.9966745165466736 + }, + { + "sites": [16], + "fidelity": 0.9993841985863008 + }, + { + "sites": [17], + "fidelity": 0.9991722904379012 + }, + { + "sites": [18], + "fidelity": 0.9996405112039688 + }, + { + "sites": [19], + "fidelity": 0.999593725030246 + } + ] + }, + { + "name": "cz", + "numQubits": 2, + "numParameters": 0, + "siteOverrides": [ + { + "sites": [0, 1], + "fidelity": 0.9943350602403423 + }, + { + "sites": [0, 3], + "fidelity": 0.9928715569164268 + }, + { + "sites": [1, 4], + "fidelity": 0.9909991817411294 + }, + { + "sites": [2, 3], + "fidelity": 0.9949180529897732 + }, + { + "sites": [2, 7], + "fidelity": 0.9912364331308542 + }, + { + "sites": [3, 4], + "fidelity": 0.9954890495965448 + }, + { + "sites": [3, 8], + "fidelity": 0.9928434575218484 + }, + { + "sites": [4, 5], + "fidelity": 0.991448076779792 + }, + { + "sites": [4, 9], + "fidelity": 0.989613618448402 + }, + { + "sites": [5, 6], + "fidelity": 0.9900308941074192 + }, + { + "sites": [5, 10], + "fidelity": 0.9913980205867798 + }, + { + "sites": [6, 11], + "fidelity": 0.9981157273861604 + }, + { + "sites": [7, 8], + "fidelity": 0.9896080668164158 + }, + { + "sites": [7, 12], + "fidelity": 0.993811402272364 + }, + { + "sites": [8, 9], + "fidelity": 0.9874011987628732 + }, + { + "sites": [8, 13], + "fidelity": 0.9944623008406192 + }, + { + "sites": [9, 10], + "fidelity": 0.9910179943052172 + }, + { + "sites": [9, 14], + "fidelity": 0.9881207404127784 + }, + { + "sites": [10, 11], + "fidelity": 0.9987871076255316 + }, + { + "sites": [10, 15], + "fidelity": 0.9949952689115732 + }, + { + "sites": [11, 16], + "fidelity": 0.9988891343238776 + }, + { + "sites": [12, 13], + "fidelity": 0.9934219679946316 + }, + { + "sites": [13, 14], + "fidelity": 0.9928397790919078 + }, + { + "sites": [13, 17], + "fidelity": 0.9949536678366192 + }, + { + "sites": [14, 15], + "fidelity": 0.99401170644631 + }, + { + "sites": [14, 18], + "fidelity": 0.9934856904331918 + }, + { + "sites": [15, 16], + "fidelity": 0.9958018845677588 + }, + { + "sites": [15, 19], + "fidelity": 0.99470226480463 + }, + { + "sites": [17, 18], + "fidelity": 0.9967902526632172 + }, + { + "sites": [18, 19], + "fidelity": 0.9977112949748272 + } + ] + }, + { + "name": "measure", + "numQubits": 1, + "numParameters": 0, + "siteOverrides": [ + { + "sites": [0], + "fidelity": 0.9817 + }, + { + "sites": [1], + "fidelity": 0.97535 + }, + { + "sites": [2], + "fidelity": 0.97275 + }, + { + "sites": [3], + "fidelity": 0.9881 + }, + { + "sites": [4], + "fidelity": 0.9774 + }, + { + "sites": [5], + "fidelity": 0.9759 + }, + { + "sites": [6], + "fidelity": 0.9761 + }, + { + "sites": [7], + "fidelity": 0.9757 + }, + { + "sites": [8], + "fidelity": 0.9724 + }, + { + "sites": [9], + "fidelity": 0.9807 + }, + { + "sites": [10], + "fidelity": 0.9824 + }, + { + "sites": [11], + "fidelity": 0.98425 + }, + { + "sites": [12], + "fidelity": 0.9728 + }, + { + "sites": [13], + "fidelity": 0.97175 + }, + { + "sites": [14], + "fidelity": 0.97115 + }, + { + "sites": [15], + "fidelity": 0.97975 + }, + { + "sites": [16], + "fidelity": 0.9814 + }, + { + "sites": [17], + "fidelity": 0.9847 + }, + { + "sites": [18], + "fidelity": 0.9702 + }, + { + "sites": [19], + "fidelity": 0.98535 + } + ] + } + ] +} diff --git a/src/qdmi/devices/sc/CMakeLists.txt b/src/qdmi/devices/sc/CMakeLists.txt index 6876e67ab9..b37cf5f20b 100644 --- a/src/qdmi/devices/sc/CMakeLists.txt +++ b/src/qdmi/devices/sc/CMakeLists.txt @@ -77,7 +77,12 @@ if(NOT TARGET ${TARGET_NAME}) PREFIX ${QDMI_PREFIX} RUNTIME_FILES - ${PROJECT_SOURCE_DIR}/json/sc/mqt-core-qdmi-sc-device.json) + ${PROJECT_SOURCE_DIR}/json/sc/mqt-core-qdmi-sc-device.json + ${PROJECT_SOURCE_DIR}/json/sc/iqm-garnet.json + ${PROJECT_SOURCE_DIR}/json/sc/iqm-emerald.json + CONFIGURATIONS + "mqt.sc.iqm.garnet|iqm-garnet.json" + "mqt.sc.iqm.emerald|iqm-emerald.json") list(APPEND MQT_CORE_TARGETS ${TARGET_NAME}) endif() diff --git a/src/qdmi/devices/sc/Configuration.cpp b/src/qdmi/devices/sc/Configuration.cpp index 9dc81f05bf..f5d18f03f2 100644 --- a/src/qdmi/devices/sc/Configuration.cpp +++ b/src/qdmi/devices/sc/Configuration.cpp @@ -218,16 +218,20 @@ void validateFidelity(const std::optional& fidelity, const auto pointer = "$/qubitProperties/overrides/" + std::to_string(i); const auto& value = overrides[i]; object(value, source, pointer); - keys(value, {"qubit", "t1", "t2"}, source, pointer); + keys(value, {"qubit", "name", "t1", "t2"}, source, pointer); Device::QubitOverride entry; entry.qubit = required(value, "qubit", source, pointer); + entry.name = optional(value, "name", source, pointer); entry.t1 = optional(value, "t1", source, pointer); entry.t2 = optional(value, "t2", source, pointer); - if (entry.qubit >= result.numQubits || (entry.t1 && *entry.t1 == 0) || - (entry.t2 && *entry.t2 == 0) || (!entry.t1 && !entry.t2) || + if (entry.qubit >= result.numQubits || + (entry.name && entry.name->empty()) || + (entry.t1 && *entry.t1 == 0) || (entry.t2 && *entry.t2 == 0) || + (!entry.name && !entry.t1 && !entry.t2) || !overridden.emplace(entry.qubit).second) { fail(source, pointer, - "must select one unique valid qubit and override t1 or t2"); + "must select one unique valid qubit and override name, t1, or " + "t2 with valid values"); } result.qubitProperties.overrides.emplace_back(entry); } diff --git a/src/qdmi/devices/sc/Device.cpp b/src/qdmi/devices/sc/Device.cpp index 873d8bf170..7783e8fdce 100644 --- a/src/qdmi/devices/sc/Device.cpp +++ b/src/qdmi/devices/sc/Device.cpp @@ -90,6 +90,9 @@ int MQT_SC_QDMI_Device_Session_impl_d::init() { } for (const auto& override : configuration.qubitProperties.overrides) { auto& site = newSiteStorage.at(override.qubit); + if (override.name) { + site->name = override.name; + } if (override.t1) { site->t1 = override.t1; } @@ -302,6 +305,13 @@ int MQT_SC_QDMI_Site_impl_d::queryProperty(const QDMI_Site_Property property, } ADD_SINGLE_VALUE_PROPERTY(QDMI_SITE_PROPERTY_INDEX, uint64_t, id, property, size, value, sizeRet) + if (property == QDMI_SITE_PROPERTY_NAME) { + if (!name) { + return QDMI_ERROR_NOTSUPPORTED; + } + ADD_STRING_PROPERTY(QDMI_SITE_PROPERTY_NAME, name->c_str(), property, size, + value, sizeRet) + } if (property == QDMI_SITE_PROPERTY_T1) { if (!t1) { return QDMI_ERROR_NOTSUPPORTED; diff --git a/test/qdmi/devices/sc/CMakeLists.txt b/test/qdmi/devices/sc/CMakeLists.txt index 11aea2e84c..351ccdc96e 100644 --- a/test/qdmi/devices/sc/CMakeLists.txt +++ b/test/qdmi/devices/sc/CMakeLists.txt @@ -28,7 +28,9 @@ if(TARGET MQT::CoreQDMIScDeviceConfig AND TARGET MQT::CoreQDMIScDevice) target_compile_definitions( ${TARGET_NAME} PRIVATE MQT_CORE_VERSION="${MQT_CORE_VERSION}" - SC_DEVICE_JSON="${PROJECT_SOURCE_DIR}/json/sc/mqt-core-qdmi-sc-device.json") + SC_DEVICE_JSON="${PROJECT_SOURCE_DIR}/json/sc/mqt-core-qdmi-sc-device.json" + IQM_GARNET_JSON="${PROJECT_SOURCE_DIR}/json/sc/iqm-garnet.json" + IQM_EMERALD_JSON="${PROJECT_SOURCE_DIR}/json/sc/iqm-emerald.json") # On Windows, copy the provider and its runtime assets beside the test executable. if(WIN32) diff --git a/test/qdmi/devices/sc/test_configuration.cpp b/test/qdmi/devices/sc/test_configuration.cpp index 8dd6ffbbd5..7089657c5d 100644 --- a/test/qdmi/devices/sc/test_configuration.cpp +++ b/test/qdmi/devices/sc/test_configuration.cpp @@ -15,6 +15,8 @@ #include // NOLINT(misc-include-cleaner) #include +#include +#include #include #include #include @@ -64,6 +66,68 @@ TEST(ScConfigurationTest, ParsesBundledDeviceStrictly) { EXPECT_EQ(device.couplings.back(), (std::pair{89, 99})); } +TEST(ScConfigurationTest, ParsesIqmDeviceModels) { + const auto assertModel = [](const Device& device, const std::string& name, + const size_t qubits, const size_t couplings, + const size_t czLoci) { + EXPECT_EQ(device.name, name); + EXPECT_EQ(device.numQubits, qubits); + EXPECT_EQ(device.couplings.size(), couplings); + EXPECT_EQ(device.durationUnit.unit, "us"); + EXPECT_DOUBLE_EQ(device.durationUnit.scaleFactor, 0.001); + ASSERT_EQ(device.qubitProperties.overrides.size(), qubits); + for (size_t i = 0; i < qubits; ++i) { + const auto& qubit = device.qubitProperties.overrides[i]; + EXPECT_EQ(qubit.qubit, i); + ASSERT_TRUE(qubit.name); + EXPECT_EQ(*qubit.name, "QB" + std::to_string(i + 1)); + } + + ASSERT_EQ(device.operations.size(), 3); + EXPECT_EQ(device.operations[0].name, "r"); + EXPECT_EQ(device.operations[0].numParameters, 2); + EXPECT_EQ(device.operations[0].numQubits, 1); + EXPECT_EQ(device.operations[0].siteOverrides.size(), qubits); + EXPECT_EQ(device.operations[1].name, "cz"); + EXPECT_EQ(device.operations[1].numParameters, 0); + EXPECT_EQ(device.operations[1].numQubits, 2); + EXPECT_EQ(device.operations[1].siteOverrides.size(), czLoci); + EXPECT_EQ(device.operations[2].name, "measure"); + EXPECT_EQ(device.operations[2].numParameters, 0); + EXPECT_EQ(device.operations[2].numQubits, 1); + EXPECT_EQ(device.operations[2].siteOverrides.size(), qubits); + for (const auto& operation : device.operations) { + EXPECT_FALSE(operation.duration); + for (const auto& locus : operation.siteOverrides) { + EXPECT_FALSE(locus.duration); + EXPECT_TRUE(locus.fidelity); + } + } + }; + + const auto garnet = readJSON(IQM_GARNET_JSON); + assertModel(garnet, "IQM Garnet", 20, 30, 30); + EXPECT_EQ(std::ranges::count_if( + garnet.qubitProperties.overrides, + [](const auto& qubit) { return qubit.t1.has_value(); }), + 20); + EXPECT_EQ(std::ranges::count_if( + garnet.qubitProperties.overrides, + [](const auto& qubit) { return qubit.t2.has_value(); }), + 20); + + const auto emerald = readJSON(IQM_EMERALD_JSON); + assertModel(emerald, "IQM Emerald", 54, 90, 81); + EXPECT_EQ(std::ranges::count_if( + emerald.qubitProperties.overrides, + [](const auto& qubit) { return qubit.t1.has_value(); }), + 53); + EXPECT_EQ(std::ranges::count_if( + emerald.qubitProperties.overrides, + [](const auto& qubit) { return qubit.t2.has_value(); }), + 54); +} + TEST(ScConfigurationTest, RejectsTopLevelSchemaErrors) { expectInvalid([](auto& root) { root["unknown"] = true; }, "unknown"); expectInvalid([](auto& root) { root.erase("qubitProperties"); }, @@ -97,6 +161,30 @@ TEST(ScConfigurationTest, RejectsInvalidUnitsAndCalibration) { {{"qubit", 8}, {"t1", 0}}); }, "qubitProperties/overrides"); + expectInvalid( + [](auto& root) { + root["qubitProperties"]["overrides"].push_back( + {{"qubit", 8}, {"name", ""}}); + }, + "qubitProperties/overrides"); + expectInvalid( + [](auto& root) { + root["qubitProperties"]["overrides"].push_back({{"qubit", 8}}); + }, + "qubitProperties/overrides"); +} + +TEST(ScConfigurationTest, AcceptsOptionalSiteNames) { + auto root = bundledJson(); + root["qubitProperties"]["overrides"].push_back( + {{"qubit", 8}, {"name", "QB9"}}); + const auto device = readJSON(root.dump(), "site-name"); + const auto& qubitOverride = device.qubitProperties.overrides.back(); + EXPECT_EQ(qubitOverride.qubit, 8); + ASSERT_TRUE(qubitOverride.name); + EXPECT_EQ(*qubitOverride.name, "QB9"); + EXPECT_FALSE(qubitOverride.t1); + EXPECT_FALSE(qubitOverride.t2); } TEST(ScConfigurationTest, RejectsInvalidOrderedTopology) { diff --git a/test/qdmi/devices/sc/test_device.cpp b/test/qdmi/devices/sc/test_device.cpp index f05646419a..42ab394a29 100644 --- a/test/qdmi/devices/sc/test_device.cpp +++ b/test/qdmi/devices/sc/test_device.cpp @@ -105,7 +105,10 @@ constexpr auto CUSTOM_SC = R"({ "durationUnit": {"unit": "ns", "scaleFactor": 0.5}, "qubitProperties": { "defaults": {"t1": 100, "t2": 200}, - "overrides": [{"qubit": 1, "t1": 90}] + "overrides": [ + {"qubit": 0, "name": "QB1"}, + {"qubit": 1, "name": "QB2", "t1": 90} + ] }, "couplings": [[0, 1], [1, 0]], "operations": [{ @@ -170,6 +173,24 @@ queryOperations(MQT_SC_QDMI_Device_Session session) { } return operations; } + +[[nodiscard]] std::string querySiteName(MQT_SC_QDMI_Device_Session session, + MQT_SC_QDMI_Site site) { + size_t size = 0; + if (MQT_SC_QDMI_device_session_query_site_property( + session, site, QDMI_SITE_PROPERTY_NAME, 0, nullptr, &size) != + QDMI_SUCCESS) { + throw std::runtime_error("Failed to query site name size"); + } + std::string name(size, '\0'); + if (MQT_SC_QDMI_device_session_query_site_property( + session, site, QDMI_SITE_PROPERTY_NAME, size, name.data(), nullptr) != + QDMI_SUCCESS) { + throw std::runtime_error("Failed to query site name"); + } + name.resize(size - 1); + return name; +} } // namespace TEST(ScRuntimeConfiguration, ValidatesRawParameterStringsAndRetry) { @@ -258,6 +279,8 @@ TEST(ScRuntimeConfiguration, SessionsOwnIndependentModelsAndCalibration) { EXPECT_EQ(bundledQubits, 100); const auto sites = querySites(custom); + EXPECT_EQ(querySiteName(custom, sites[0]), "QB1"); + EXPECT_EQ(querySiteName(custom, sites[1]), "QB2"); uint64_t t1 = 0; ASSERT_EQ( MQT_SC_QDMI_device_session_query_site_property( @@ -302,6 +325,10 @@ TEST(ScRuntimeConfiguration, SessionsOwnIndependentModelsAndCalibration) { EXPECT_DOUBLE_EQ(fidelity, 0.9); auto* const bundledSite = querySites(bundled).front(); + EXPECT_EQ( + MQT_SC_QDMI_device_session_query_site_property( + bundled, bundledSite, QDMI_SITE_PROPERTY_NAME, 0, nullptr, nullptr), + QDMI_ERROR_NOTSUPPORTED); EXPECT_EQ( MQT_SC_QDMI_device_session_query_site_property( custom, bundledSite, QDMI_SITE_PROPERTY_INDEX, 0, nullptr, nullptr), diff --git a/test/qdmi/driver/CMakeLists.txt b/test/qdmi/driver/CMakeLists.txt index 625febfce7..dd2cf91693 100644 --- a/test/qdmi/driver/CMakeLists.txt +++ b/test/qdmi/driver/CMakeLists.txt @@ -140,4 +140,28 @@ if(TARGET MQT::CoreQDMIDriver) mqt-core-qdmi-runtime-file-install) set_tests_properties(mqt-core-qdmi-runtime-file-install-verify PROPERTIES FIXTURES_REQUIRED mqt-core-qdmi-runtime-file-install) + + function(add_qdmi_configuration_rejection_test name invalid_configuration) + set(build_dir "${CMAKE_CURRENT_BINARY_DIR}/runtime-file-device-invalid-${name}") + set(configure_command + ${CMAKE_COMMAND} -S "${CMAKE_CURRENT_SOURCE_DIR}/runtime_file_device" -B "${build_dir}" -G + "${CMAKE_GENERATOR}" + "-DMQT_CORE_QDMI_HELPER=${PROJECT_SOURCE_DIR}/cmake/AddMQTQDMIDevice.cmake" + "-DMQT_CORE_QDMI_TEST_SOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR}" + "-DMQT_CORE_QDMI_INVALID_CONFIGURATION=${invalid_configuration}" + "-DCMAKE_BUILD_TYPE=$") + if(CMAKE_GENERATOR_PLATFORM) + list(APPEND configure_command -A "${CMAKE_GENERATOR_PLATFORM}") + endif() + if(CMAKE_GENERATOR_TOOLSET) + list(APPEND configure_command -T "${CMAKE_GENERATOR_TOOLSET}") + endif() + add_test(NAME "mqt-core-qdmi-configuration-rejects-${name}" COMMAND ${configure_command}) + set_tests_properties("mqt-core-qdmi-configuration-rejects-${name}" PROPERTIES WILL_FAIL TRUE) + endfunction() + + add_qdmi_configuration_rejection_test(empty-id empty-id) + add_qdmi_configuration_rejection_test(empty-file empty-file) + add_qdmi_configuration_rejection_test(duplicate-base-id duplicate-base-id) + add_qdmi_configuration_rejection_test(duplicate-configured-id duplicate-configured-id) endif() diff --git a/test/qdmi/driver/runtime_file_device/CMakeLists.txt b/test/qdmi/driver/runtime_file_device/CMakeLists.txt index 40e6c3b574..334ae1ccfb 100644 --- a/test/qdmi/driver/runtime_file_device/CMakeLists.txt +++ b/test/qdmi/driver/runtime_file_device/CMakeLists.txt @@ -18,6 +18,17 @@ include("${MQT_CORE_QDMI_HELPER}") add_library(mqt-core-qdmi-runtime-file-device SHARED "${MQT_CORE_QDMI_TEST_SOURCE_DIR}/metadata_device.cpp") +set(configurations "test.runtime-file.variant|metadata-runtime.json") +if(MQT_CORE_QDMI_INVALID_CONFIGURATION STREQUAL "empty-id") + set(configurations "|metadata-runtime.json") +elseif(MQT_CORE_QDMI_INVALID_CONFIGURATION STREQUAL "empty-file") + set(configurations "test.runtime-file.variant|") +elseif(MQT_CORE_QDMI_INVALID_CONFIGURATION STREQUAL "duplicate-base-id") + set(configurations "test.runtime-file|metadata-runtime.json") +elseif(MQT_CORE_QDMI_INVALID_CONFIGURATION STREQUAL "duplicate-configured-id") + set(configurations "test.runtime-file.variant|metadata-runtime.json" + "test.runtime-file.variant|metadata-runtime.json") +endif() mqt_configure_qdmi_device( mqt-core-qdmi-runtime-file-device ID @@ -25,4 +36,6 @@ mqt_configure_qdmi_device( PREFIX TEST_RUNTIME RUNTIME_FILES - "${MQT_CORE_QDMI_TEST_SOURCE_DIR}/metadata-runtime.json") + "${MQT_CORE_QDMI_TEST_SOURCE_DIR}/metadata-runtime.json" + CONFIGURATIONS + ${configurations}) diff --git a/test/qdmi/driver/test_driver.cpp b/test/qdmi/driver/test_driver.cpp index f369b1ff5a..f8becc0fdb 100644 --- a/test/qdmi/driver/test_driver.cpp +++ b/test/qdmi/driver/test_driver.cpp @@ -892,8 +892,8 @@ TEST(ConfiguredDriverTest, ExposesWorkingDefinitionsAndIsolatesFailures) { ASSERT_EQ(QDMI_session_query_session_property( session, QDMI_SESSION_PROPERTY_DEVICES, 0, nullptr, &size), QDMI_SUCCESS); - ASSERT_EQ(size, 3 * sizeof(QDMI_Device)); - std::array devices{}; + ASSERT_EQ(size, 5 * sizeof(QDMI_Device)); + std::array devices{}; ASSERT_EQ(QDMI_session_query_session_property( session, QDMI_SESSION_PROPERTY_DEVICES, size, static_cast(devices.data()), nullptr), @@ -902,9 +902,9 @@ TEST(ConfiguredDriverTest, ExposesWorkingDefinitionsAndIsolatesFailures) { std::vector names; std::ranges::transform(devices, std::back_inserter(names), queryName); EXPECT_THAT(names, - testing::UnorderedElementsAre("MQT NA Default QDMI Device", - "MQT Core DDSIM QDMI Device", - "MQT SC Default QDMI Device")); + testing::UnorderedElementsAre( + "IQM Emerald", "IQM Garnet", "MQT NA Default QDMI Device", + "MQT Core DDSIM QDMI Device", "MQT SC Default QDMI Device")); QDMI_session_free(session); } diff --git a/test/qdmi/registry/test_device_registry.cpp b/test/qdmi/registry/test_device_registry.cpp index 8be48e49ae..7f85f65d62 100644 --- a/test/qdmi/registry/test_device_registry.cpp +++ b/test/qdmi/registry/test_device_registry.cpp @@ -349,13 +349,30 @@ TEST(DeviceRegistry, DiscoversGeneratedBuildTreeManifests) { const ScopedEnvironmentVariable configJson("MQT_CORE_QDMI_CONFIG_JSON", ""); const qdmi::detail::DeviceRegistry registry; - ASSERT_EQ(registry.definitions().size(), 3); + ASSERT_EQ(registry.definitions().size(), 5); EXPECT_EQ(registry.definitions().at(0).id, "mqt.ddsim.default"); EXPECT_EQ(registry.definitions().at(1).id, "mqt.na.default"); EXPECT_EQ(registry.definitions().at(2).id, "mqt.sc.default"); + EXPECT_EQ(registry.definitions().at(3).id, "mqt.sc.iqm.emerald"); + EXPECT_EQ(registry.definitions().at(4).id, "mqt.sc.iqm.garnet"); for (const auto& definition : registry.definitions()) { EXPECT_TRUE(std::filesystem::is_regular_file(definition.library)); } + + const auto assertPackagedModel = [&](const std::string_view id, + const std::string_view filename) { + const auto* definition = findDefinition(registry, id); + ASSERT_NE(definition, nullptr); + ASSERT_TRUE(definition->session.deviceConfiguration); + const auto* file = std::get_if( + &*definition->session.deviceConfiguration); + ASSERT_NE(file, nullptr); + EXPECT_EQ(file->path.filename(), filename); + EXPECT_EQ(file->path.parent_path(), definition->library.parent_path()); + EXPECT_TRUE(std::filesystem::is_regular_file(file->path)); + }; + assertPackagedModel("mqt.sc.iqm.garnet", "iqm-garnet.json"); + assertPackagedModel("mqt.sc.iqm.emerald", "iqm-emerald.json"); } #endif From f6a493e587b5dcdb28985c2e9aeab6b7b30948aa Mon Sep 17 00:00:00 2001 From: Daniel Haag <121057143+denialhaag@users.noreply.github.com> Date: Fri, 14 Aug 2026 18:07:33 +0200 Subject: [PATCH 10/35] =?UTF-8?q?=F0=9F=90=8D=20Backport=20CPython=203.15?= =?UTF-8?q?=20wheel=20preparation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Advertise CPython 3.15, build its wheels without running dependency-limited tests, include the complete JSON device catalog in wheel tests, and update the reusable workflow revision while preserving the v3 MLIR presets. Backports #2011. Assisted-by: GPT-5.6 via Codex --- .github/workflows/cd.yml | 4 ++-- .github/workflows/ci.yml | 30 +++++++++++++++--------------- .github/workflows/upstream.yml | 4 ++-- CHANGELOG.md | 4 +++- pyproject.toml | 4 +++- 5 files changed, 25 insertions(+), 21 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index d03cbbbd39..db677da779 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -12,7 +12,7 @@ permissions: jobs: build-sdist: name: 🐍 Packaging - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-packaging-sdist.yml@973217bac3ad300d9982fffdd3b37a9b5b3a51ea # v2.2.1 + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-packaging-sdist.yml@579203ef22082fa5d5483412f442e50d0c5ff4a7 # v2.2.2 # Builds wheels on all supported platforms using cibuildwheel. # The wheels are uploaded as GitHub artifacts `dev-cibw-*` or `cibw-*`, depending on whether the workflow is triggered from a PR or a release. @@ -30,7 +30,7 @@ jobs: windows-2025, windows-11-arm, ] - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-packaging-wheel-cibuildwheel.yml@973217bac3ad300d9982fffdd3b37a9b5b3a51ea # v2.2.1 + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-packaging-wheel-cibuildwheel.yml@579203ef22082fa5d5483412f442e50d0c5ff4a7 # v2.2.2 with: runs-on: ${{ matrix.runs-on }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 085ac33f84..240dd0779c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ concurrency: jobs: change-detection: name: 🔍 Change - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-change-detection.yml@973217bac3ad300d9982fffdd3b37a9b5b3a51ea # v2.2.1 + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-change-detection.yml@579203ef22082fa5d5483412f442e50d0c5ff4a7 # v2.2.2 cpp-tests-ubuntu: name: 🇨‌ Test 🐧 needs: change-detection @@ -29,7 +29,7 @@ jobs: - runs-on: ubuntu-24.04 compiler: gcc preset: debug-mlir - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-tests-ubuntu.yml@973217bac3ad300d9982fffdd3b37a9b5b3a51ea # v2.2.1 + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-tests-ubuntu.yml@579203ef22082fa5d5483412f442e50d0c5ff4a7 # v2.2.2 with: runs-on: ${{ matrix.runs-on }} compiler: ${{ matrix.compiler }} @@ -51,7 +51,7 @@ jobs: - runs-on: macos-26 compiler: clang preset: debug-mlir - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-tests-macos.yml@973217bac3ad300d9982fffdd3b37a9b5b3a51ea # v2.2.1 + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-tests-macos.yml@579203ef22082fa5d5483412f442e50d0c5ff4a7 # v2.2.2 with: runs-on: ${{ matrix.runs-on }} compiler: ${{ matrix.compiler }} @@ -73,7 +73,7 @@ jobs: - runs-on: windows-2025 compiler: msvc preset: debug-windows-mlir - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-tests-windows.yml@973217bac3ad300d9982fffdd3b37a9b5b3a51ea # v2.2.1 + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-tests-windows.yml@579203ef22082fa5d5483412f442e50d0c5ff4a7 # v2.2.2 with: runs-on: ${{ matrix.runs-on }} compiler: ${{ matrix.compiler }} @@ -93,7 +93,7 @@ jobs: runs-on: [ubuntu-24.04, ubuntu-24.04-arm] compiler: [gcc, clang, clang-20, clang-21] preset: [release-mlir, debug-mlir] - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-tests-ubuntu.yml@973217bac3ad300d9982fffdd3b37a9b5b3a51ea # v2.2.1 + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-tests-ubuntu.yml@579203ef22082fa5d5483412f442e50d0c5ff4a7 # v2.2.2 with: runs-on: ${{ matrix.runs-on }} compiler: ${{ matrix.compiler }} @@ -112,7 +112,7 @@ jobs: runs-on: [macos-15, macos-15-intel, macos-26, macos-26-intel] compiler: [clang, clang-20, clang-21, gcc-14, gcc-15] preset: [release-mlir, debug-mlir] - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-tests-macos.yml@973217bac3ad300d9982fffdd3b37a9b5b3a51ea # v2.2.1 + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-tests-macos.yml@579203ef22082fa5d5483412f442e50d0c5ff4a7 # v2.2.2 with: runs-on: ${{ matrix.runs-on }} compiler: ${{ matrix.compiler }} @@ -131,7 +131,7 @@ jobs: runs-on: [windows-2022, windows-2025, windows-11-arm] compiler: [msvc, clang] preset: [release-windows-mlir, debug-windows-mlir] - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-tests-windows.yml@973217bac3ad300d9982fffdd3b37a9b5b3a51ea # v2.2.1 + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-tests-windows.yml@579203ef22082fa5d5483412f442e50d0c5ff4a7 # v2.2.2 with: runs-on: ${{ matrix.runs-on }} compiler: ${{ matrix.compiler }} @@ -144,7 +144,7 @@ jobs: name: 🇨‌ Coverage needs: change-detection if: fromJSON(needs.change-detection.outputs.run-cpp-tests) - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-coverage.yml@973217bac3ad300d9982fffdd3b37a9b5b3a51ea # v2.2.1 + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-coverage.yml@579203ef22082fa5d5483412f442e50d0c5ff4a7 # v2.2.2 with: setup-mlir: true llvm-version: 21.1.8 @@ -156,7 +156,7 @@ jobs: name: 🇨‌ Lint needs: change-detection if: fromJSON(needs.change-detection.outputs.run-cpp-linter) - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-linter.yml@973217bac3ad300d9982fffdd3b37a9b5b3a51ea # v2.2.1 + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-linter.yml@579203ef22082fa5d5483412f442e50d0c5ff4a7 # v2.2.2 with: clang-version: 22 build-project: true @@ -185,7 +185,7 @@ jobs: macos-26-intel, windows-2025, ] - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-tests.yml@973217bac3ad300d9982fffdd3b37a9b5b3a51ea # v2.2.1 + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-tests.yml@579203ef22082fa5d5483412f442e50d0c5ff4a7 # v2.2.2 with: runs-on: ${{ matrix.runs-on }} @@ -193,7 +193,7 @@ jobs: name: 🐍 Coverage needs: [change-detection, python-tests] if: fromJSON(needs.change-detection.outputs.run-python-tests) - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-coverage.yml@973217bac3ad300d9982fffdd3b37a9b5b3a51ea # v2.2.1 + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-coverage.yml@579203ef22082fa5d5483412f442e50d0c5ff4a7 # v2.2.2 permissions: contents: read id-token: write @@ -207,7 +207,7 @@ jobs: fail-fast: false matrix: runs-on: [macos-15, macos-15-intel, windows-2022] - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-tests.yml@973217bac3ad300d9982fffdd3b37a9b5b3a51ea # v2.2.1 + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-tests.yml@579203ef22082fa5d5483412f442e50d0c5ff4a7 # v2.2.2 with: runs-on: ${{ matrix.runs-on }} @@ -215,7 +215,7 @@ jobs: name: 🐍 Lint needs: change-detection if: fromJSON(needs.change-detection.outputs.run-python-linter) - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-linter.yml@973217bac3ad300d9982fffdd3b37a9b5b3a51ea # v2.2.1 + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-linter.yml@579203ef22082fa5d5483412f442e50d0c5ff4a7 # v2.2.2 with: check-stubs: true enable-ty: true @@ -225,7 +225,7 @@ jobs: name: 🚀 CD needs: change-detection if: fromJSON(needs.change-detection.outputs.run-cd) - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-packaging-sdist.yml@973217bac3ad300d9982fffdd3b37a9b5b3a51ea # v2.2.1 + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-packaging-sdist.yml@579203ef22082fa5d5483412f442e50d0c5ff4a7 # v2.2.2 build-wheel: name: 🚀 CD @@ -243,7 +243,7 @@ jobs: windows-2025, windows-11-arm, ] - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-packaging-wheel-cibuildwheel.yml@973217bac3ad300d9982fffdd3b37a9b5b3a51ea # v2.2.1 + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-packaging-wheel-cibuildwheel.yml@579203ef22082fa5d5483412f442e50d0c5ff4a7 # v2.2.2 with: runs-on: ${{ matrix.runs-on }} diff --git a/.github/workflows/upstream.yml b/.github/workflows/upstream.yml index 4d5d72c390..02b1e1e8b2 100644 --- a/.github/workflows/upstream.yml +++ b/.github/workflows/upstream.yml @@ -19,7 +19,7 @@ jobs: fail-fast: false matrix: runs-on: [ubuntu-24.04, macos-15, windows-2025] - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-qiskit-upstream-tests.yml@973217bac3ad300d9982fffdd3b37a9b5b3a51ea # v2.2.1 + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-qiskit-upstream-tests.yml@579203ef22082fa5d5483412f442e50d0c5ff4a7 # v2.2.2 with: runs-on: ${{ matrix.runs-on }} setup-z3: true @@ -28,7 +28,7 @@ jobs: name: Create issue on failure needs: qiskit-upstream-tests if: ${{ always() }} - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-qiskit-upstream-issue.yml@973217bac3ad300d9982fffdd3b37a9b5b3a51ea # v2.2.1 + uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-qiskit-upstream-issue.yml@579203ef22082fa5d5483412f442e50d0c5ff4a7 # v2.2.2 with: tests-result: ${{ needs.qiskit-upstream-tests.result }} permissions: diff --git a/CHANGELOG.md b/CHANGELOG.md index b755583f9e..b13eb473c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ releases may include breaking changes. - ✨ Support retrieving existing jobs by ID through the QDMI client API and C++ and Python FoMaC APIs, and expose optional device queue length and job queue position ([#2008], [#2010]) ([**@burgholzer**]) +- 🐍 Start building CPython 3.15 wheels ([#2011]) ([**@denialhaag**]) - ✨ Add PennyLane support for gate-based QDMI devices ([#2005]) ([**@burgholzer**]) - ✨ Bundle reusable IQM Garnet and Emerald superconducting device models with @@ -464,7 +465,7 @@ _If you are upgrading: please see [`UPGRADING.md`](UPGRADING.md#320)._ ### Added -- 🐍 Build Python 3.14 wheels ([#1076]) ([**@denialhaag**]) +- 🐍 Start building CPython 3.14 wheels ([#1076]) ([**@denialhaag**]) - ✨ Add MQT-internal MLIR dialect conversions ([#1001]) ([**@li-mingbao**]) ### Changed @@ -652,6 +653,7 @@ changelogs._ [#2025]: https://github.com/munich-quantum-toolkit/core/pull/2025 [#2010]: https://github.com/munich-quantum-toolkit/core/pull/2010 [#2008]: https://github.com/munich-quantum-toolkit/core/pull/2008 +[#2011]: https://github.com/munich-quantum-toolkit/core/pull/2011 [#2005]: https://github.com/munich-quantum-toolkit/core/pull/2005 [#1992]: https://github.com/munich-quantum-toolkit/core/pull/1992 [#1984]: https://github.com/munich-quantum-toolkit/core/pull/1984 diff --git a/pyproject.toml b/pyproject.toml index 29328b10fa..fb30fa7e80 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ classifiers = [ "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.15", "Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)", "Typing :: Typed", ] @@ -287,12 +288,13 @@ build = "cp3*" skip = ["*-musllinux_*"] archs = "auto64" test-groups = ["test"] -test-sources = ["test/python", "test/circuits", "json/na"] +test-sources = ["test/python", "test/circuits", "json"] test-command = "pytest test/python" build-frontend = "uv" test-skip = [ "cp3??t-*", # no freethreading qiskit wheels "cp*-win_arm64", # no numpy, qiskit, ... wheels + "cp315*", # no 3.15 wheels yet ] [tool.cibuildwheel.linux] From 370e00f1c0ab578fb75dd3c475631123b71e978a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 18:08:00 +0200 Subject: [PATCH 11/35] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Backport=20the=20rum?= =?UTF-8?q?dl=20pre-commit=20update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update the Markdown linter and formatter hook to v0.2.49. Backports #2027. Assisted-by: GPT-5.6 via Codex --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bf4f87d766..55a4dae83d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -114,7 +114,7 @@ repos: ## Format Markdown files with rumdl - repo: https://github.com/rvben/rumdl-pre-commit - rev: v0.2.37 + rev: v0.2.49 hooks: - id: rumdl args: [--fix] From 25d9e835842aab0d63f68f2cf3b247efbf4c147f Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Fri, 14 Aug 2026 18:09:03 +0200 Subject: [PATCH 12/35] =?UTF-8?q?=F0=9F=94=A7=20Backport=20standard=20dyna?= =?UTF-8?q?mic=20package=20metadata?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adopt scikit-build-core's standard dynamic-metadata table for setuptools-scm version discovery. Backports #2065. Assisted-by: GPT-5.6 via Codex --- pyproject.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index fb30fa7e80..992c550cb5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -115,7 +115,6 @@ install.components = [ "qdmi_Development", ] -metadata.version.provider = "scikit_build_core.metadata.setuptools_scm" sdist.include = ["python/mqt/core/_version.py"] sdist.exclude = [ "**/.github", @@ -129,6 +128,9 @@ sdist.exclude = [ "**/tests", ] +[[tool.dynamic-metadata]] +provider = "scikit_build_core.metadata.setuptools_scm" + [tool.check-sdist] sdist-only = ["python/mqt/core/_version.py"] git-only = [ From 2da291fe5db5e0bb6d171fbe2f0db6e01066d4ef Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 18:09:35 +0200 Subject: [PATCH 13/35] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Backport=20nanobind?= =?UTF-8?q?=202.14?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update the build, stub-generation, lock, and clang-tidy environments to nanobind 2.14.0. Backports #2073. Assisted-by: GPT-5.6 via Codex --- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 2 ++ noxfile.py | 1 - pyproject.toml | 4 ++-- uv.lock | 10 +++++----- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 240dd0779c..345dd06ea5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -162,7 +162,7 @@ jobs: build-project: true files-changed-only: true setup-python: true - install-pkgs: "nanobind==2.13.0" + install-pkgs: "nanobind==2.14.0" cpp-linter-extra-args: "-std=c++20" # The vendored toml++ header is checked upstream and is not maintained # according to MQT Core's clang-tidy configuration. diff --git a/CHANGELOG.md b/CHANGELOG.md index b13eb473c2..284e15e7db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ releases may include breaking changes. namespaces, expose device registration and opening through `mqt.core.qdmi.driver`, retain v3 FoMaC compatibility aliases, and let the Qiskit adapter open stable device IDs directly ([#2074]) ([**@burgholzer**]) +- ⬆️ Update `nanobind` to version 2.14.0 ([#2073]) ([**@denialhaag**]) - 🚀 Reduce ZX diagram growth for multi-controlled X gates with an exact ancilla-free quadratic decomposition ([#1984]) ([**@burgholzer**]) @@ -649,6 +650,7 @@ changelogs._ [#2074]: https://github.com/munich-quantum-toolkit/core/pull/2074 [#2043]: https://github.com/munich-quantum-toolkit/core/pull/2043 +[#2073]: https://github.com/munich-quantum-toolkit/core/pull/2073 [#2042]: https://github.com/munich-quantum-toolkit/core/pull/2042 [#2025]: https://github.com/munich-quantum-toolkit/core/pull/2025 [#2010]: https://github.com/munich-quantum-toolkit/core/pull/2010 diff --git a/noxfile.py b/noxfile.py index 8e8e6e5731..2013a9c336 100755 --- a/noxfile.py +++ b/noxfile.py @@ -211,7 +211,6 @@ def stubs(session: nox.Session) -> None: session.run( "uv", "sync", - "--no-editable", "--no-dev", "--group", "build", diff --git a/pyproject.toml b/pyproject.toml index 992c550cb5..c7e767b352 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ [build-system] requires = [ - "nanobind~=2.13.0", + "nanobind~=2.14.0", "scikit-build-core~=1.0.3", "setuptools-scm>=9.2.2", ] @@ -336,7 +336,7 @@ exclude = [ [dependency-groups] build = [ - "nanobind~=2.13.0", + "nanobind~=2.14.0", "scikit-build-core~=1.0.3", "setuptools-scm>=9.2.2", ] diff --git a/uv.lock b/uv.lock index d89b775729..45c4e17c5b 100644 --- a/uv.lock +++ b/uv.lock @@ -1825,13 +1825,13 @@ provides-extras = ["pennylane", "qiskit"] [package.metadata.requires-dev] build = [ - { name = "nanobind", specifier = "~=2.13.0" }, + { name = "nanobind", specifier = "~=2.14.0" }, { name = "scikit-build-core", specifier = "~=1.0.3" }, { name = "setuptools-scm", specifier = ">=9.2.2" }, ] dev = [ { name = "lit", specifier = ">=18.1.8" }, - { name = "nanobind", specifier = "~=2.13.0" }, + { name = "nanobind", specifier = "~=2.14.0" }, { name = "nox", specifier = ">=2025.11.12" }, { name = "numpy", marker = "python_full_version >= '3.13'", specifier = ">=2.1" }, { name = "numpy", marker = "python_full_version >= '3.14'", specifier = ">=2.3.2" }, @@ -1955,11 +1955,11 @@ wheels = [ [[package]] name = "nanobind" -version = "2.13.0" +version = "2.14.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/07/e5/a6fc2f024eaef17c68b94b5f8f56098d31859a7840a11a3dd45da2d3908a/nanobind-2.13.0.tar.gz", hash = "sha256:c7b04d6a6a4cd57985571e605539399b51331ae455d7fce576a5e2fcb89b1dcf", size = 1052101, upload-time = "2026-06-18T06:03:49.491Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/23/a6fdf94de7bf760da93a115593cc1756a7b3d408c12681df9e4c63bf11e3/nanobind-2.14.0.tar.gz", hash = "sha256:0cf1477d8b1697abc1454028b6469e7113d9aa6ba3ced8b102cc569affbacb13", size = 1053642, upload-time = "2026-08-07T06:07:51.605Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/8d/cec69a0a804711fd89f47fad92cd2a438a1ceaafd9dabaf3b8b403b5cbba/nanobind-2.13.0-py3-none-any.whl", hash = "sha256:9268d7201eaf5519ae61b7a83175f2af77088cb4b99dd3ce5b00550f697da7b7", size = 269387, upload-time = "2026-06-18T06:03:47.508Z" }, + { url = "https://files.pythonhosted.org/packages/94/17/fba47a85a7b0bfb9ab5a5100e8a26b361b881089c7d8cad9a95f895f1546/nanobind-2.14.0-py3-none-any.whl", hash = "sha256:a57cd4fe613db78410ca13ba007a3ffaf3125b831d8be46ec7f4351e8cf6e513", size = 269917, upload-time = "2026-08-07T06:07:50.045Z" }, ] [[package]] From 2e4b0c78e0b07d5a6a748d9a7185c79013fa3799 Mon Sep 17 00:00:00 2001 From: Daniel Haag <121057143+denialhaag@users.noreply.github.com> Date: Fri, 14 Aug 2026 18:10:57 +0200 Subject: [PATCH 14/35] =?UTF-8?q?=E2=9A=A1=20Backport=20faster=20QDMI=20st?= =?UTF-8?q?ub=20generation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Build only the extension modules required by recursive stub generation using a small non-optimized unity build and reuse the worktree-local environment. Keep the v3 documentation session unchanged. Backports #2075. Assisted-by: GPT-5.6 via Codex --- noxfile.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/noxfile.py b/noxfile.py index 2013a9c336..f81a333b2a 100755 --- a/noxfile.py +++ b/noxfile.py @@ -31,7 +31,6 @@ nox.needs_version = ">=2025.10.16" nox.options.default_venv_backend = "uv" - PYTHON_ALL_VERSIONS = ["3.10", "3.11", "3.12", "3.13", "3.14"] if os.environ.get("CI", None): @@ -207,13 +206,29 @@ def docs(session: nox.Session) -> None: @nox.session(reuse_venv=True, venv_backend="uv") def stubs(session: nox.Session) -> None: """Generate type stubs for Python bindings using nanobind.""" - env = {"UV_PROJECT_ENVIRONMENT": session.virtualenv.location} + env = { + "UV_PROJECT_ENVIRONMENT": session.virtualenv.location, + # Stub generation only imports the extension modules, so this build + # favors compilation speed over optimized code. + "SKBUILD_CMAKE_BUILD_TYPE": "MinSizeRel", + "SKBUILD_CMAKE_ARGS": ( + "-DBUILD_MQT_CORE_QDMI_NA_DEVICE=OFF;" + "-DBUILD_MQT_CORE_QDMI_SC_DEVICE=OFF;" + "-DCMAKE_UNITY_BUILD=ON;" + "-DCMAKE_VERIFY_INTERFACE_HEADER_SETS=OFF;" + "-DENABLE_IPO=OFF" + ), + } + + session.run("uv", "sync", "--inexact", "--only-group", "build", env=env) session.run( "uv", "sync", "--no-dev", "--group", "build", + "--no-build-isolation-package", + "mqt-core", # build the project without isolation env=env, ) From 954e0ec321a6a848da72667c11ee8f12a13b579c Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Fri, 14 Aug 2026 18:14:49 +0200 Subject: [PATCH 15/35] =?UTF-8?q?=E2=9A=A1=20Backport=20build=20and=20CI?= =?UTF-8?q?=20time=20reductions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enable unity builds for ordinary v3 builds, keep lint non-unity with interface-header verification, and reduce the routine C++ matrix to one Debug and one Release job per operating system while retaining LLVM/MLIR 21. Backports #2083. Assisted-by: GPT-5.6 via Codex --- .github/workflows/ci.yml | 109 ++++------------------------ CMakePresets.json | 7 +- cmake/StandardProjectSettings.cmake | 4 +- noxfile.py | 8 +- pyproject.toml | 1 + test/dd/test_approximations.cpp | 11 +-- test/dd/test_state_generation.cpp | 8 +- 7 files changed, 33 insertions(+), 115 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 345dd06ea5..fa8c94fe05 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,17 +18,17 @@ jobs: cpp-tests-ubuntu: name: 🇨‌ Test 🐧 needs: change-detection - if: fromJSON(needs.change-detection.outputs.run-cpp-tests) && !contains(github.event.pull_request.labels.*.name, 'extensive-cpp-ci') + if: fromJSON(needs.change-detection.outputs.run-cpp-tests) strategy: fail-fast: false matrix: - runs-on: [ubuntu-24.04, ubuntu-24.04-arm] - compiler: [gcc] - preset: [release-mlir] include: - runs-on: ubuntu-24.04 compiler: gcc preset: debug-mlir + - runs-on: ubuntu-24.04-arm + compiler: gcc + preset: release-mlir uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-tests-ubuntu.yml@579203ef22082fa5d5483412f442e50d0c5ff4a7 # v2.2.2 with: runs-on: ${{ matrix.runs-on }} @@ -40,17 +40,17 @@ jobs: cpp-tests-macos: name: 🇨‌ Test 🍎 needs: change-detection - if: fromJSON(needs.change-detection.outputs.run-cpp-tests) && !contains(github.event.pull_request.labels.*.name, 'extensive-cpp-ci') + if: fromJSON(needs.change-detection.outputs.run-cpp-tests) strategy: fail-fast: false matrix: - runs-on: [macos-26, macos-26-intel] - compiler: [clang] - preset: [release-mlir] include: - runs-on: macos-26 compiler: clang preset: debug-mlir + - runs-on: macos-26-intel + compiler: clang + preset: release-mlir uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-tests-macos.yml@579203ef22082fa5d5483412f442e50d0c5ff4a7 # v2.2.2 with: runs-on: ${{ matrix.runs-on }} @@ -62,75 +62,17 @@ jobs: cpp-tests-windows: name: 🇨‌ Test 🏁 needs: change-detection - if: fromJSON(needs.change-detection.outputs.run-cpp-tests) && !contains(github.event.pull_request.labels.*.name, 'extensive-cpp-ci') + if: fromJSON(needs.change-detection.outputs.run-cpp-tests) strategy: fail-fast: false matrix: - runs-on: [windows-2025, windows-11-arm] - compiler: [msvc] - preset: [release-windows-mlir] include: - runs-on: windows-2025 compiler: msvc preset: debug-windows-mlir - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-tests-windows.yml@579203ef22082fa5d5483412f442e50d0c5ff4a7 # v2.2.2 - with: - runs-on: ${{ matrix.runs-on }} - compiler: ${{ matrix.compiler }} - setup-mlir: true - llvm-version: 21.1.8 - mlir-debug: ${{ matrix.preset == 'debug-windows-mlir' }} - preset-name: ${{ matrix.preset }} - - # run extensive C++ tests on PRs labeled with the `extensive-cpp-ci` label - cpp-tests-extensive-ubuntu: - name: 🇨‌ Test (Extensive) 🐧 - needs: change-detection - if: fromJSON(needs.change-detection.outputs.run-cpp-tests) && github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'extensive-cpp-ci') - strategy: - fail-fast: false - matrix: - runs-on: [ubuntu-24.04, ubuntu-24.04-arm] - compiler: [gcc, clang, clang-20, clang-21] - preset: [release-mlir, debug-mlir] - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-tests-ubuntu.yml@579203ef22082fa5d5483412f442e50d0c5ff4a7 # v2.2.2 - with: - runs-on: ${{ matrix.runs-on }} - compiler: ${{ matrix.compiler }} - setup-mlir: true - llvm-version: 21.1.8 - preset-name: ${{ matrix.preset }} - - # run extensive C++ tests on PRs labeled with the `extensive-cpp-ci` label - cpp-tests-extensive-macos: - name: 🇨‌ Test (Extensive) 🍎 - needs: change-detection - if: fromJSON(needs.change-detection.outputs.run-cpp-tests) && github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'extensive-cpp-ci') - strategy: - fail-fast: false - matrix: - runs-on: [macos-15, macos-15-intel, macos-26, macos-26-intel] - compiler: [clang, clang-20, clang-21, gcc-14, gcc-15] - preset: [release-mlir, debug-mlir] - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-tests-macos.yml@579203ef22082fa5d5483412f442e50d0c5ff4a7 # v2.2.2 - with: - runs-on: ${{ matrix.runs-on }} - compiler: ${{ matrix.compiler }} - setup-mlir: true - llvm-version: 21.1.8 - preset-name: ${{ matrix.preset }} - - # run extensive C++ tests on PRs labeled with the `extensive-cpp-ci` label - cpp-tests-extensive-windows: - name: 🇨‌ Test (Extensive) 🏁 - needs: change-detection - if: fromJSON(needs.change-detection.outputs.run-cpp-tests) && github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'extensive-cpp-ci') - strategy: - fail-fast: false - matrix: - runs-on: [windows-2022, windows-2025, windows-11-arm] - compiler: [msvc, clang] - preset: [release-windows-mlir, debug-windows-mlir] + - runs-on: windows-11-arm + compiler: msvc + preset: release-windows-mlir uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-tests-windows.yml@579203ef22082fa5d5483412f442e50d0c5ff4a7 # v2.2.2 with: runs-on: ${{ matrix.runs-on }} @@ -198,19 +140,6 @@ jobs: contents: read id-token: write - # run extensive Python tests on PRs labeled with the `extensive-python-ci` label - python-tests-extensive: - name: 🐍 Test (Extensive) - needs: change-detection - if: fromJSON(needs.change-detection.outputs.run-python-tests) && github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'extensive-python-ci') - strategy: - fail-fast: false - matrix: - runs-on: [macos-15, macos-15-intel, windows-2022] - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-python-tests.yml@579203ef22082fa5d5483412f442e50d0c5ff4a7 # v2.2.2 - with: - runs-on: ${{ matrix.runs-on }} - python-linter: name: 🐍 Lint needs: change-detection @@ -256,13 +185,9 @@ jobs: - cpp-tests-ubuntu - cpp-tests-macos - cpp-tests-windows - - cpp-tests-extensive-ubuntu - - cpp-tests-extensive-macos - - cpp-tests-extensive-windows - cpp-coverage - cpp-linter - python-tests - - python-tests-extensive - python-linter - build-sdist - build-wheel @@ -273,13 +198,9 @@ jobs: with: allowed-skips: >- ${{ - (!fromJSON(needs.change-detection.outputs.run-cpp-tests) || contains(github.event.pull_request.labels.*.name, 'extensive-cpp-ci')) + !fromJSON(needs.change-detection.outputs.run-cpp-tests) && 'cpp-tests-ubuntu,cpp-tests-macos,cpp-tests-windows,' || '' }} - ${{ - (!fromJSON(needs.change-detection.outputs.run-cpp-tests) || !contains(github.event.pull_request.labels.*.name, 'extensive-cpp-ci') || github.event_name != 'pull_request') - && 'cpp-tests-extensive-ubuntu,cpp-tests-extensive-macos,cpp-tests-extensive-windows,' || '' - }} ${{ !fromJSON(needs.change-detection.outputs.run-cpp-tests) && 'cpp-coverage,' || '' @@ -292,10 +213,6 @@ jobs: !fromJSON(needs.change-detection.outputs.run-python-tests) && 'python-tests,python-coverage,' || '' }} - ${{ - (!fromJSON(needs.change-detection.outputs.run-python-tests) || !contains(github.event.pull_request.labels.*.name, 'extensive-python-ci') || github.event_name != 'pull_request') - && 'python-tests-extensive,' || '' - }} ${{ !fromJSON(needs.change-detection.outputs.run-python-linter) && 'python-linter,' || '' diff --git a/CMakePresets.json b/CMakePresets.json index 973af4c626..78f905b145 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -5,6 +5,9 @@ "name": "base", "hidden": true, "binaryDir": "${sourceDir}/build/${presetName}", + "cacheVariables": { + "CMAKE_UNITY_BUILD": "ON" + }, "environment": { "CCACHE_DIR": "${sourceDir}/.cache/ccache", "CCACHE_MAXSIZE": "4GiB", @@ -108,7 +111,9 @@ "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug", "BUILD_MQT_CORE_BENCHMARKS": "ON", - "BUILD_MQT_CORE_BINDINGS": "ON" + "BUILD_MQT_CORE_BINDINGS": "ON", + "CMAKE_UNITY_BUILD": "OFF", + "CMAKE_VERIFY_INTERFACE_HEADER_SETS": "ON" } }, { diff --git a/cmake/StandardProjectSettings.cmake b/cmake/StandardProjectSettings.cmake index 0eae102bef..b16d598a76 100644 --- a/cmake/StandardProjectSettings.cmake +++ b/cmake/StandardProjectSettings.cmake @@ -30,8 +30,8 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS CACHE BOOL "Export compile commands" FORCE) set(CMAKE_VERIFY_INTERFACE_HEADER_SETS - ON - CACHE BOOL "Verify interface header sets" FORCE) + OFF + CACHE BOOL "Verify interface header sets") if(CMAKE_CXX_COMPILER_ID MATCHES ".*Clang") add_compile_options(-fcolor-diagnostics) diff --git a/noxfile.py b/noxfile.py index f81a333b2a..53c19c7bdf 100755 --- a/noxfile.py +++ b/noxfile.py @@ -211,13 +211,7 @@ def stubs(session: nox.Session) -> None: # Stub generation only imports the extension modules, so this build # favors compilation speed over optimized code. "SKBUILD_CMAKE_BUILD_TYPE": "MinSizeRel", - "SKBUILD_CMAKE_ARGS": ( - "-DBUILD_MQT_CORE_QDMI_NA_DEVICE=OFF;" - "-DBUILD_MQT_CORE_QDMI_SC_DEVICE=OFF;" - "-DCMAKE_UNITY_BUILD=ON;" - "-DCMAKE_VERIFY_INTERFACE_HEADER_SETS=OFF;" - "-DENABLE_IPO=OFF" - ), + "SKBUILD_CMAKE_ARGS": ("-DBUILD_MQT_CORE_QDMI_NA_DEVICE=OFF;-DBUILD_MQT_CORE_QDMI_SC_DEVICE=OFF"), } session.run("uv", "sync", "--inexact", "--only-group", "build", env=env) diff --git a/pyproject.toml b/pyproject.toml index c7e767b352..1d9a69a561 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -143,6 +143,7 @@ git-only = [ BUILD_MQT_CORE_BINDINGS = "ON" BUILD_MQT_CORE_TESTS = "OFF" BUILD_MQT_CORE_SHARED_LIBS = "ON" +CMAKE_UNITY_BUILD = "ON" [[tool.scikit-build.overrides]] if.python-version = ">=3.13" diff --git a/test/dd/test_approximations.cpp b/test/dd/test_approximations.cpp index 5d37d126ad..cce46ee697 100644 --- a/test/dd/test_approximations.cpp +++ b/test/dd/test_approximations.cpp @@ -30,7 +30,8 @@ namespace { /** * @brief Compare the elements of @p a and @p b with precision @p delta. */ -void vecNear(const CVec& a, const CVec& b, const double delta = 1e-6) { +void expectApproximationVectorNear(const CVec& a, const CVec& b, + const double delta = 1e-6) { for (std::size_t i = 0; i < b.size(); ++i) { EXPECT_NEAR(a[i].real(), b[i].real(), delta); EXPECT_NEAR(a[i].imag(), b[i].imag(), delta); @@ -175,7 +176,7 @@ TEST(ApproximationTest, TwoQubitRemoveNode) { const CVec expected{{0.755929}, {0.654654}, {0}, {0}}; - vecNear(state.getVector(), expected); + expectApproximationVectorNear(state.getVector(), expected); EXPECT_EQ(state.size(), 3); EXPECT_NEAR(meta.fidelity, 0.875, 1e-3); @@ -231,7 +232,7 @@ TEST(ApproximationTest, TwoQubitCorrectlyRebuilt) { auto ref = simulate(qcRef, makeZeroState(nq, *dd), *dd); const CVec expected{{0}, {0}, {0}, {0, 1}}; - vecNear(state.getVector(), expected); + expectApproximationVectorNear(state.getVector(), expected); state.printVector(); EXPECT_EQ(state.size(), 3); EXPECT_NEAR(meta.fidelity, 0.75, 1e-3); @@ -279,7 +280,7 @@ TEST(ApproximationTest, ThreeQubitRemoveNodeWithChildren) { const auto meta = approximate(state, fidelity, *dd); const CVec expected{{0, 1}, {0}, {0}, {0}, {0}, {0}, {0}, {0}}; - vecNear(state.getVector(), expected); + expectApproximationVectorNear(state.getVector(), expected); EXPECT_EQ(state.size(), 4); EXPECT_NEAR(meta.fidelity, 0.75, 1e-3); } @@ -326,7 +327,7 @@ TEST(ApproximationTest, ThreeQubitRemoveUnconnected) { auto meta = approximate(state, fidelity, *dd); const CVec expected{{0}, {-1}, {0}, {0}, {0}, {0}, {0}, {0}}; - vecNear(state.getVector(), expected); + expectApproximationVectorNear(state.getVector(), expected); EXPECT_EQ(state.size(), 4); EXPECT_NEAR(meta.fidelity, 0.361, 1e-3); } diff --git a/test/dd/test_state_generation.cpp b/test/dd/test_state_generation.cpp index a4145f1959..7efcde25ef 100644 --- a/test/dd/test_state_generation.cpp +++ b/test/dd/test_state_generation.cpp @@ -31,7 +31,7 @@ namespace { /** * @brief Compare the elements of @p a and @p b with precision @p delta. */ -void vecNear(CVec a, CVec b, double delta = 1e-6) { +void expectStateVectorNear(CVec a, CVec b, double delta = 1e-6) { for (std::size_t i = 0; i < b.size(); ++i) { EXPECT_NEAR(a[i].real(), b[i].real(), delta); EXPECT_NEAR(a[i].imag(), b[i].imag(), delta); @@ -118,7 +118,7 @@ TEST(StateGenerationTest, MakeBasisDifficult) { auto dd = std::make_unique(nq); auto basis = makeBasisState(nq, state, *dd); - vecNear(basis.getVector(), vec); + expectStateVectorNear(basis.getVector(), vec); dd->decRef(basis); dd->garbageCollect(true); @@ -142,7 +142,7 @@ TEST(StateGenerationTest, MakeGHZ) { auto dd = std::make_unique(nq); auto ghz = makeGHZState(nq, *dd); - vecNear(ghz.getVector(), vec); + expectStateVectorNear(ghz.getVector(), vec); dd->decRef(ghz); dd->garbageCollect(true); @@ -183,7 +183,7 @@ TEST(StateGenerationTest, MakeW) { auto dd = std::make_unique(nq); auto w = makeWState(nq, *dd); - vecNear(w.getVector(), vec); + expectStateVectorNear(w.getVector(), vec); dd->decRef(w); dd->garbageCollect(true); From 628e09a044b8184ba42be9c7307149a284bbb63f Mon Sep 17 00:00:00 2001 From: simon1hofmann <119581649+simon1hofmann@users.noreply.github.com> Date: Fri, 14 Aug 2026 18:15:25 +0200 Subject: [PATCH 16/35] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Backport=20simplifie?= =?UTF-8?q?d=20optional-dependency=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inline the Qiskit and PennyLane import checks while preserving the public availability flags, and remove the now-unused private compatibility helper and its tests. Backports #2108. Signed-off-by: simon1hofmann <119581649+simon1hofmann@users.noreply.github.com> Assisted-by: GPT-5.6 via Codex --- CHANGELOG.md | 3 + python/mqt/core/_compat/optional.py | 171 ----------------- python/mqt/core/plugins/pennylane/__init__.py | 21 ++- python/mqt/core/plugins/qiskit/__init__.py | 20 +- test/python/test_optional_dependency.py | 177 ------------------ 5 files changed, 27 insertions(+), 365 deletions(-) delete mode 100644 python/mqt/core/_compat/optional.py delete mode 100644 test/python/test_optional_dependency.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 284e15e7db..611305374c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,8 @@ releases may include breaking changes. ### Changed +- ♻️ Simplify Python optional-dependency checks while preserving the Qiskit and + PennyLane availability flags ([#2108]) ([**@simon1hofmann**]) - 💥 Move Python QDMI entities and the neutral-atom specialization to QDMI namespaces, expose device registration and opening through `mqt.core.qdmi.driver`, retain v3 FoMaC compatibility aliases, and let the @@ -648,6 +650,7 @@ changelogs._ +[#2108]: https://github.com/munich-quantum-toolkit/core/pull/2108 [#2074]: https://github.com/munich-quantum-toolkit/core/pull/2074 [#2043]: https://github.com/munich-quantum-toolkit/core/pull/2043 [#2073]: https://github.com/munich-quantum-toolkit/core/pull/2073 diff --git a/python/mqt/core/_compat/optional.py b/python/mqt/core/_compat/optional.py deleted file mode 100644 index 0f717291b3..0000000000 --- a/python/mqt/core/_compat/optional.py +++ /dev/null @@ -1,171 +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 - -"""Optional dependency management. - -Inspired by: -- Qiskit's LazyDependencyManager: https://github.com/Qiskit/qiskit/blob/main/qiskit/utils/lazy_tester.py -""" - -from __future__ import annotations - -import contextlib -import importlib -import warnings -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from collections.abc import Generator - from types import ModuleType - -__all__ = ["OptionalDependencyTester"] - - -class OptionalDependencyTester: - """Manager for optional dependencies with lazy testing and caching. - - This class provides a reusable pattern for managing optional dependencies - across MQT projects. It supports: - - Lazy evaluation and caching of import checks - - Boolean context for conditional logic - - Required imports with custom error messages - - Test-friendly context manager to temporarily disable availability - - Examples: - Basic usage with boolean context: - - >>> HAS_QISKIT = OptionalDependencyTester("qiskit") - >>> if HAS_QISKIT: - ... import qiskit - ... # Use qiskit - - Requiring with custom message: - - >>> HAS_QISKIT.require_now("use Qiskit backends") # Raises if not available - - Getting the imported module: - - >>> qiskit = HAS_QISKIT.require_module("use Qiskit functionality") - - Testing with temporary disable: - - >>> with HAS_QISKIT.disable_locally(): - ... assert not HAS_QISKIT - """ - - def __init__( - self, - module: str, - *, - install_msg: str | None = None, - warn_on_fail: bool = False, - ) -> None: - """Initialize an optional dependency tester. - - Args: - module: Name of the module to test (e.g., "qiskit", "matplotlib"). - install_msg: Optional installation instructions for error messages. - If not provided, defaults to "Install with 'pip install {module}'". - warn_on_fail: If True, emit a warning when import fails. Default is False - to avoid noise for truly optional dependencies. - """ - self._module = module - self._bool: bool | None = None - self._install_msg = install_msg or f"Install with 'pip install {module}'" - self._warn_on_fail = warn_on_fail - - def _is_available(self) -> bool: - """Test module availability by attempting import. - - Returns: - True if the module can be imported, False otherwise. - """ - try: - importlib.import_module(self._module) - except ImportError as exc: - if self._warn_on_fail: # pragma: no cover - warnings.warn( - f"Optional module '{self._module}' failed to import: {exc!r}", - category=ImportWarning, - stacklevel=3, - ) - return False - else: - return True - - def __bool__(self) -> bool: - """Check if the dependency is available (cached). - - Returns: - True if the dependency is available, False otherwise. - """ - if self._bool is None: - self._bool = self._is_available() - return self._bool - - def require_now(self, feature: str = "use this feature") -> None: - """Require the dependency is available, raising if not. - - Args: - feature: Description of what requires this dependency (for error message). - - Raises: - ImportError: If the dependency is not available. - """ - if self: - return - msg = f"The '{self._module}' library is required to {feature}. {self._install_msg}" - raise ImportError(msg) - - def require_module(self, feature: str = "use this feature") -> ModuleType: - """Require the dependency and return the imported module. - - Args: - feature: Description of what requires this dependency (for error message). - - Returns: - The imported module. - """ - self.require_now(feature) - return importlib.import_module(self._module) - - @contextlib.contextmanager - def disable_locally(self) -> Generator[None, None, None]: - """Context manager to temporarily treat dependency as unavailable. - - This is useful for testing code paths that handle missing dependencies. - - Yields: - None - - Examples: - >>> HAS_QISKIT = OptionalDependencyTester("qiskit") - >>> with HAS_QISKIT.disable_locally(): - ... assert not HAS_QISKIT # Temporarily False - >>> assert HAS_QISKIT # Back to actual state - """ - previous = self._bool - self._bool = False - try: - yield - finally: - self._bool = previous - - @property - def module_name(self) -> str: - """The name of the module being tested.""" - return self._module - - def __repr__(self) -> str: - """Return string representation. - - Returns: - String representation showing module and availability status. - """ - available = "available" if self else "not available" - return f"" diff --git a/python/mqt/core/plugins/pennylane/__init__.py b/python/mqt/core/plugins/pennylane/__init__.py index 211439077a..b3ae44613c 100644 --- a/python/mqt/core/plugins/pennylane/__init__.py +++ b/python/mqt/core/plugins/pennylane/__init__.py @@ -8,23 +8,26 @@ """PennyLane interface for gate-based QDMI devices.""" +# ruff: file-ignore[non-empty-init-module] + from __future__ import annotations import sys +from importlib import import_module from typing import TYPE_CHECKING -from ..._compat.optional import OptionalDependencyTester - -HAS_PENNYLANE = OptionalDependencyTester( # ruff:ignore[non-empty-init-module] Optional plugin - "pennylane", - install_msg="Install with 'uv pip install mqt-core[pennylane]'", -) +try: + import_module("pennylane") +except ModuleNotFoundError as error: + if error.name != "pennylane": + raise + HAS_PENNYLANE = False +else: + HAS_PENNYLANE = True __all__ = ["HAS_PENNYLANE"] -if TYPE_CHECKING or ( # ruff:ignore[non-empty-init-module] Optional plugin - sys.version_info >= (3, 11) and HAS_PENNYLANE -): +if TYPE_CHECKING or (sys.version_info >= (3, 11) and HAS_PENNYLANE): from .converter import ConvertedProgram, convert_program from .device import DDSIMDevice, QDMIDevice from .exceptions import ( diff --git a/python/mqt/core/plugins/qiskit/__init__.py b/python/mqt/core/plugins/qiskit/__init__.py index 0753f87c89..22ab6d7a76 100644 --- a/python/mqt/core/plugins/qiskit/__init__.py +++ b/python/mqt/core/plugins/qiskit/__init__.py @@ -8,23 +8,27 @@ """MQT Qiskit Plugin.""" +# ruff: file-ignore[non-empty-init-module] + from __future__ import annotations +from importlib import import_module from typing import TYPE_CHECKING -from ..._compat.optional import OptionalDependencyTester - -# Optional dependency tester for Qiskit -HAS_QISKIT = OptionalDependencyTester( # ruff:ignore[non-empty-init-module] Used for handling optional plugin - "qiskit", - install_msg="Install with 'pip install mqt-core[qiskit]'", -) +try: + import_module("qiskit") +except ModuleNotFoundError as error: + if error.name != "qiskit": + raise + HAS_QISKIT = False +else: + HAS_QISKIT = True __all__ = [ "HAS_QISKIT", ] -if TYPE_CHECKING or HAS_QISKIT: # ruff:ignore[non-empty-init-module] Used for handling optional plugin +if TYPE_CHECKING or HAS_QISKIT: from .backend import QDMIBackend from .converters import qiskit_to_iqm_json from .estimator import QDMIEstimator diff --git a/test/python/test_optional_dependency.py b/test/python/test_optional_dependency.py deleted file mode 100644 index f3e1726538..0000000000 --- a/test/python/test_optional_dependency.py +++ /dev/null @@ -1,177 +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 - -"""Tests for OptionalDependencyTester.""" - -from __future__ import annotations - -import sys -import warnings - -import pytest - -from mqt.core._compat.optional import OptionalDependencyTester # ruff:ignore[import-private-name] - - -def test_available_module() -> None: - """Test with a module that should be available (sys).""" - tester = OptionalDependencyTester("sys") - assert tester - assert bool(tester) - - -def test_unavailable_module() -> None: - """Test with a module that definitely doesn't exist.""" - tester = OptionalDependencyTester("this_module_does_not_exist_xyz123") - assert not tester - assert not bool(tester) - - -def test_caching() -> None: - """Test that availability check is cached.""" - tester = OptionalDependencyTester("sys") - - # First check - assert tester - - # Cache should be set now - assert tester._bool # ruff:ignore[private-member-access] - - # Second check should use cache - assert tester - - -def test_require_now_available() -> None: - """Test require_now with an available module.""" - tester = OptionalDependencyTester("sys") - # Should not raise - tester.require_now("test this") - - -def test_require_now_unavailable() -> None: - """Test require_now with an unavailable module.""" - tester = OptionalDependencyTester("this_module_does_not_exist_xyz123") - - with pytest.raises(ImportError, match="required to test this feature"): - tester.require_now("test this feature") - - -def test_require_module_available() -> None: - """Test require_module returns the imported module.""" - tester = OptionalDependencyTester("sys") - module = tester.require_module("test this") - - assert module is sys - - -def test_require_module_unavailable() -> None: - """Test require_module raises for unavailable module.""" - tester = OptionalDependencyTester("this_module_does_not_exist_xyz123") - - with pytest.raises(ImportError, match="required to use feature"): - tester.require_module("use feature") - - -def test_custom_install_message() -> None: - """Test custom installation message appears in errors.""" - tester = OptionalDependencyTester( - "nonexistent_module", - install_msg="Install with 'pip install special-package'", - ) - - with pytest.raises(ImportError, match="pip install special-package"): - tester.require_now("test") - - -def test_disable_locally() -> None: - """Test disable_locally context manager.""" - tester = OptionalDependencyTester("sys") - - # Initially available - assert tester - - # Temporarily disabled - with tester.disable_locally(): - assert not tester - - # Back to available after context - assert tester - - -def test_disable_locally_preserves_state() -> None: - """Test disable_locally preserves original state including None.""" - tester = OptionalDependencyTester("sys") - # Don't check yet, so _bool is None - assert tester._bool is None # ruff:ignore[private-member-access] - - with tester.disable_locally(): - assert not tester - - # Should be None again (not True) - assert tester._bool is None # ruff:ignore[private-member-access] - - -def test_disable_locally_with_unavailable() -> None: - """Test disable_locally works with already unavailable modules.""" - tester = OptionalDependencyTester("this_module_does_not_exist_xyz123") - - # Check it's unavailable - assert not tester - - # Disable locally (should stay False) - with tester.disable_locally(): - assert not tester - - # Still unavailable after context - assert not tester - - -def test_module_name_property() -> None: - """Test module_name property.""" - tester = OptionalDependencyTester("test_module") - assert tester.module_name == "test_module" - - -def test_repr() -> None: - """Test string representation.""" - tester_available = OptionalDependencyTester("sys") - tester_unavailable = OptionalDependencyTester("nonexistent") - - repr_available = repr(tester_available) - repr_unavailable = repr(tester_unavailable) - - assert "sys" in repr_available - assert "available" in repr_available - - assert "nonexistent" in repr_unavailable - assert "not available" in repr_unavailable - - -def test_warn_on_fail() -> None: - """Test that warn_on_fail emits warnings when enabled.""" - tester = OptionalDependencyTester( - "this_module_does_not_exist_xyz123", - warn_on_fail=True, - ) - - with pytest.warns(ImportWarning, match="failed to import"): - _ = bool(tester) - - -def test_no_warn_by_default() -> None: - """Test that warnings are not emitted by default.""" - tester = OptionalDependencyTester("this_module_does_not_exist_xyz123") - - # Should not emit warnings - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - _ = bool(tester) - - # Filter for ImportWarnings - import_warnings = [warning for warning in w if issubclass(warning.category, ImportWarning)] - assert len(import_warnings) == 0 From 80aea82a7b7a9717c8f02084fd48b07882cd563d Mon Sep 17 00:00:00 2001 From: Daniel Haag <121057143+denialhaag@users.noreply.github.com> Date: Fri, 14 Aug 2026 18:16:44 +0200 Subject: [PATCH 17/35] =?UTF-8?q?=F0=9F=93=9D=20Backport=20Sphinx-LLM=20do?= =?UTF-8?q?cumentation=20indexes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enable the Sphinx-LLM text builder and require the first release that generates the llms.txt sitemap. Refresh the v3 dependency lock for the feature. Backports the Sphinx-LLM changes from #1989 and #2046. Assisted-by: GPT-5.6 via Codex --- CHANGELOG.md | 4 ++++ docs/conf.py | 1 + pyproject.toml | 1 + uv.lock | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 40 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 611305374c..0c0ca518f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ releases may include breaking changes. ### Added +- 📝 Generate `llms.txt` documentation indexes with Sphinx-LLM ([#1989], + [#2046]) ([**@denialhaag**], [**@burgholzer**]) - 🧪 Test static Slurm license admission and QDMI execution with DDSIM and SC, and document the cluster setup ([#2043]) ([**@burgholzer**]) - ✨ Add C++ and Python adapters that open the QDMI device named by one local @@ -651,6 +653,7 @@ changelogs._ [#2108]: https://github.com/munich-quantum-toolkit/core/pull/2108 +[#2046]: https://github.com/munich-quantum-toolkit/core/pull/2046 [#2074]: https://github.com/munich-quantum-toolkit/core/pull/2074 [#2043]: https://github.com/munich-quantum-toolkit/core/pull/2043 [#2073]: https://github.com/munich-quantum-toolkit/core/pull/2073 @@ -659,6 +662,7 @@ changelogs._ [#2010]: https://github.com/munich-quantum-toolkit/core/pull/2010 [#2008]: https://github.com/munich-quantum-toolkit/core/pull/2008 [#2011]: https://github.com/munich-quantum-toolkit/core/pull/2011 +[#1989]: https://github.com/munich-quantum-toolkit/core/pull/1989 [#2005]: https://github.com/munich-quantum-toolkit/core/pull/2005 [#1992]: https://github.com/munich-quantum-toolkit/core/pull/1992 [#1984]: https://github.com/munich-quantum-toolkit/core/pull/1984 diff --git a/docs/conf.py b/docs/conf.py index 5577e1293d..729049552d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -62,6 +62,7 @@ "myst_nb", "sphinx_copybutton", "sphinx_design", + "sphinx_llm.txt", "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.napoleon", diff --git a/pyproject.toml b/pyproject.toml index 1d9a69a561..0b578d3ae5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -348,6 +348,7 @@ docs = [ "sphinx-autoapi>=3.6.1", "sphinx-copybutton>=0.5.2", "sphinx-design>=0.6.1", + "sphinx-llm>=0.4.1", "sphinxcontrib-bibtex>=2.6.5", "sphinxcontrib-svg2pdfconverter>=1.3.0", "sphinxext-opengraph>=0.13.0", diff --git a/uv.lock b/uv.lock index 45c4e17c5b..14a8969986 100644 --- a/uv.lock +++ b/uv.lock @@ -1801,6 +1801,7 @@ docs = [ { name = "sphinx-copybutton" }, { name = "sphinx-design", version = "0.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx-design", version = "0.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx-llm" }, { name = "sphinxcontrib-bibtex" }, { name = "sphinxcontrib-svg2pdfconverter" }, { name = "sphinxext-opengraph" }, @@ -1860,6 +1861,7 @@ docs = [ { name = "sphinx-autoapi", specifier = ">=3.6.1" }, { name = "sphinx-copybutton", specifier = ">=0.5.2" }, { name = "sphinx-design", specifier = ">=0.6.1" }, + { name = "sphinx-llm", specifier = ">=0.4.1" }, { name = "sphinxcontrib-bibtex", specifier = ">=2.6.5" }, { name = "sphinxcontrib-svg2pdfconverter", specifier = ">=1.3.0" }, { name = "sphinxext-opengraph", specifier = ">=0.13.0" }, @@ -3858,6 +3860,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/30/cf/45dd359f6ca0c3762ce0490f681da242f0530c49c81050c035c016bfdd3a/sphinx_design-0.7.0-py3-none-any.whl", hash = "sha256:f82bf179951d58f55dca78ab3706aeafa496b741a91b1911d371441127d64282", size = 2220350, upload-time = "2026-01-19T13:12:51.077Z" }, ] +[[package]] +name = "sphinx-llm" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "sphinx-markdown-builder" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/58/6b1802820a17e7a128b45046d735f934af00f4290704b647b126213bdeb4/sphinx_llm-1.0.0.tar.gz", hash = "sha256:1cc8284343a0100caa3110010da5933cdad8da37d2b9dda0e2a7d503b8da6b1c", size = 381976, upload-time = "2026-08-14T07:48:29.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/5b/2a338974998bc0fe563635625abdb16a000ee06a63101b361c834cd56ccb/sphinx_llm-1.0.0-py3-none-any.whl", hash = "sha256:8d45dbe5f9caffb4d78589c35d15fc8399dd41fc5b445ee4292ff83969754e75", size = 60282, upload-time = "2026-08-14T07:48:28.161Z" }, +] + +[[package]] +name = "sphinx-markdown-builder" +version = "0.6.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "tabulate" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a0/58/0b7b9a7d071140b3705885d51932e8b62f520388c2772e4952189971727b/sphinx_markdown_builder-0.6.10.tar.gz", hash = "sha256:cd5acf88d52ea0146a712fd557404f10326dff3428a78ba928e59b1727fd4a86", size = 22688, upload-time = "2026-03-11T10:56:57.639Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/8f/9fecf3d081d5cd49eff83a17b9fef50ed741e6223ab3bb906de4ab0068f9/sphinx_markdown_builder-0.6.10-py3-none-any.whl", hash = "sha256:16d86738b9ac69fcbc86e373c31c6402c30af1fa8d98d0f62cc5f38bfe5fc26e", size = 16700, upload-time = "2026-03-11T10:56:56.135Z" }, +] + [[package]] name = "sphinxcontrib-applehelp" version = "2.0.0" From f21b9b1195659b7dfeb1adb624d4421db46d237a Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Fri, 14 Aug 2026 18:17:38 +0200 Subject: [PATCH 18/35] =?UTF-8?q?=F0=9F=A7=AA=20Backport=20stronger=20ZX?= =?UTF-8?q?=20equivalence=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verify multi-controlled X, Z, and RZ constructions against independent exact-unitary references before checking their ZX representations. Backports #2029. Assisted-by: GPT-5.6 via Codex --- test/zx/test_zx_functionality.cpp | 39 ++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/test/zx/test_zx_functionality.cpp b/test/zx/test_zx_functionality.cpp index 3beeb9915f..d2ebdfcbfb 100644 --- a/test/zx/test_zx_functionality.cpp +++ b/test/zx/test_zx_functionality.cpp @@ -62,6 +62,18 @@ void checkEquivalence(const qc::QuantumComputation& qc1, } } +void checkExactUnitaryEquivalence(const qc::QuantumComputation& qc1, + const qc::QuantumComputation& qc2) { + ASSERT_EQ(qc1.getNqubits(), qc2.getNqubits()); + + dd::Package package(qc1.getNqubits()); + const auto actual = dd::buildFunctionality(qc1, package); + const auto expected = dd::buildFunctionality(qc2, package); + EXPECT_EQ(actual, expected); + package.decRef(actual); + package.decRef(expected); +} + void addDirtyAncillaMcx(qc::QuantumComputation& circuit, const std::vector& controls, const qc::Qubit target, @@ -321,9 +333,9 @@ TEST_F(ZXFunctionalityTest, MCX) { qc = qc::QuantumComputation(4); qc.mcx({1, 2, 3}, 0); - auto qcPrime = qc::QuantumComputation(4); - qcPrime.mcx({3, 2, 1}, 0); + const auto qcPrime = makeReferenceMcx(3); + checkExactUnitaryEquivalence(qc, qcPrime); checkEquivalence(qc, qcPrime, {0, 1, 2, 3}); } @@ -365,17 +377,11 @@ TEST_F(ZXFunctionalityTest, LargeMCX) { std::vector qubits(numQubits); std::iota(qubits.begin(), qubits.end(), 0U); + checkExactUnitaryEquivalence(qc, reference); checkEquivalence(qc, reference, qubits); const auto diag = FunctionalityConstruction::buildFunctionality(&qc); EXPECT_LT(diag.getNVertices(), 22U * numControls * numControls); - - dd::Package package(numQubits); - const auto actual = dd::buildFunctionality(qc, package); - const auto expected = dd::buildFunctionality(reference, package); - EXPECT_EQ(actual.p, expected.p); - package.decRef(actual); - package.decRef(expected); } } @@ -385,8 +391,9 @@ TEST_F(ZXFunctionalityTest, MCZ) { qc.mcz({1, 2, 3}, 0); auto qcPrime = qc::QuantumComputation(4); - qcPrime.mcz({1, 2, 3}, 0); + addReferenceMcphase(qcPrime, PI, {1, 2, 3}, 0); + checkExactUnitaryEquivalence(qc, qcPrime); checkEquivalence(qc, qcPrime, {0, 1, 2, 3}); } @@ -427,13 +434,17 @@ TEST_F(ZXFunctionalityTest, MCZ2) { TEST_F(ZXFunctionalityTest, MCRZ) { + const auto phase = PI / 4; qc = qc::QuantumComputation(4); - qc.mcrz(PI / 4, {1, 2, 3}, 0); + qc.mcrz(phase, {1, 2, 3}, 0); auto qcPrime = qc::QuantumComputation(4); - qcPrime.h(0); - qcPrime.mcrx(PI / 4, {1, 2, 3}, 0); - qcPrime.h(0); + qcPrime.crz(phase / 2, 3, 0); + qcPrime.mcx({1, 2}, 0); + qcPrime.crz(-phase / 2, 3, 0); + qcPrime.mcx({1, 2}, 0); + + checkExactUnitaryEquivalence(qc, qcPrime); checkEquivalence(qc, qcPrime, {0, 1, 2, 3}); } From fc53de2810e0a4180a81200f0df1c0a40e9712b6 Mon Sep 17 00:00:00 2001 From: Daniel Haag <121057143+denialhaag@users.noreply.github.com> Date: Fri, 14 Aug 2026 18:17:48 +0200 Subject: [PATCH 19/35] =?UTF-8?q?=F0=9F=94=A5=20Backport=20removal=20of=20?= =?UTF-8?q?dead=20typing=20compatibility=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the unused private typing compatibility module and its Ruff configuration. No v3 production code imports the module. Backports #2059. Assisted-by: GPT-5.6 via Codex --- pyproject.toml | 4 ---- python/mqt/core/_compat/typing.py | 29 ----------------------------- 2 files changed, 33 deletions(-) delete mode 100644 python/mqt/core/_compat/typing.py diff --git a/pyproject.toml b/pyproject.toml index 0b578d3ae5..632238d70b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -233,10 +233,6 @@ ignore = [ "TID252" # Prefer absolute imports over relative imports from parent modules ] future-annotations = true -typing-modules = ["mqt.core._compat.typing"] - -[tool.ruff.lint.flake8-tidy-imports.banned-api] -"typing.assert_never".msg = "Use mqt.core._compat.typing.assert_never instead." [tool.ruff.lint.isort] known-first-party = ["mqt.core"] diff --git a/python/mqt/core/_compat/typing.py b/python/mqt/core/_compat/typing.py deleted file mode 100644 index 9c621a5170..0000000000 --- a/python/mqt/core/_compat/typing.py +++ /dev/null @@ -1,29 +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 - -from __future__ import annotations - -import sys -from typing import TYPE_CHECKING - -if sys.version_info >= (3, 11): - from typing import assert_never -elif TYPE_CHECKING: - from typing_extensions import assert_never -else: - - def assert_never(_: object) -> None: - msg = "Expected code to be unreachable" - raise AssertionError(msg) - - -__all__ = ["assert_never"] - - -def __dir__() -> list[str]: - return __all__ From 905c6717c6b2c19f46643d7fb97b7bd04cd66471 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 18:18:35 +0200 Subject: [PATCH 20/35] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Backport=20CI=20and?= =?UTF-8?q?=20hook=20patch=20updates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update the release publishing actions, release drafter, uv lock hook, project-schema validator, Ruff, and ty to the compatible patch revisions already used on main. Backports the compatible updates from #2019, #2020, #2021, and #2023. The newer rumdl update from #2027 is already present in this series. Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Assisted-by: GPT-5.6 via Codex --- .github/workflows/cd.yml | 4 ++-- .github/workflows/release-drafter.yml | 2 +- .pre-commit-config.yaml | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index db677da779..849612af1a 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -50,7 +50,7 @@ jobs: path: dist merge-multiple: true - name: Generate artifact attestation for sdist and wheel(s) - uses: actions/attest@f7c74d28b9d84cb8768d0b8ca14a4bac6ef463e6 # v4.2.0 + uses: actions/attest@508db95dd578ae2727ebd6217d5ba78e4fbda05d # v4.2.1 with: subject-path: dist/* - - uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # v1.14.1 + - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 558ffba089..b704a56bf2 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -15,4 +15,4 @@ jobs: contents: write runs-on: ubuntu-slim steps: - - uses: release-drafter/release-drafter@eada3c96a64734dd381cfbda23511034e328ddb0 # v7.6.0 + - uses: release-drafter/release-drafter@34d80673e067bdc0c24568d3af899c216adcfaa9 # v7.7.0 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 55a4dae83d..67ae077616 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -47,14 +47,14 @@ repos: ## Check pyproject.toml file - repo: https://github.com/henryiii/validate-pyproject-schema-store - rev: 2026.07.17 + rev: 2026.08.01 hooks: - id: validate-pyproject priority: 0 ## Ensure uv.lock is up to date - repo: https://github.com/astral-sh/uv-pre-commit - rev: 0.11.29 + rev: 0.12.1 hooks: - id: uv-lock priority: 0 @@ -148,7 +148,7 @@ repos: ## Format and lint Python files with ruff - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.22 + rev: v0.16.1 hooks: - id: ruff-format types_or: [python, pyi, jupyter, markdown] @@ -159,7 +159,7 @@ repos: ## Check Python types with ty - repo: https://github.com/astral-sh/ty-pre-commit - rev: v0.0.61 + rev: v0.0.65 hooks: - id: ty args: [--only-dev] From 224f94ee21a0f41ac3499401adb70e5afba8350b Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Fri, 14 Aug 2026 18:19:43 +0200 Subject: [PATCH 21/35] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Backport=20native=20?= =?UTF-8?q?unity-build=20compatibility=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keep the v3 DD bindings and neutral-atom operation sources compatible with the unity builds enabled by the CI optimization. Main-only LLVM/MLIR documentation-build changes are intentionally not included. Backports the applicable native fixes from #2047. Assisted-by: GPT-5.6 via Codex --- bindings/dd/register_matrix_dds.cpp | 2 +- bindings/dd/register_vector_dds.cpp | 2 +- src/na/operations/GlobalOp.cpp | 8 ++++---- src/na/operations/LocalOp.cpp | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/bindings/dd/register_matrix_dds.cpp b/bindings/dd/register_matrix_dds.cpp index ad3b626ffc..86c5ade19a 100644 --- a/bindings/dd/register_matrix_dds.cpp +++ b/bindings/dd/register_matrix_dds.cpp @@ -37,7 +37,7 @@ using Matrix = nb::ndarray, nb::ndim<2>>; // NOLINTNEXTLINE(misc-use-internal-linkage) Matrix getMatrix(const dd::mEdge& m, const size_t numQubits, - const dd::fp threshold = 0.) { + const dd::fp threshold) { if (numQubits > 20U) { throw nb::value_error("num_qubits exceeds practical limit of 20"); } diff --git a/bindings/dd/register_vector_dds.cpp b/bindings/dd/register_vector_dds.cpp index 5cc7502b65..e0818c2377 100644 --- a/bindings/dd/register_vector_dds.cpp +++ b/bindings/dd/register_vector_dds.cpp @@ -37,7 +37,7 @@ using namespace nb::literals; using Vector = nb::ndarray, nb::ndim<1>>; // NOLINTNEXTLINE(misc-use-internal-linkage) -Vector getVector(const dd::vEdge& v, const dd::fp threshold = 0.) { +Vector getVector(const dd::vEdge& v, const dd::fp threshold) { auto vec = v.getVector(threshold); auto dataPtr = std::make_unique[]>(vec.size()); std::ranges::copy(vec, dataPtr.get()); diff --git a/src/na/operations/GlobalOp.cpp b/src/na/operations/GlobalOp.cpp index 9374d6d072..b6bad2f828 100644 --- a/src/na/operations/GlobalOp.cpp +++ b/src/na/operations/GlobalOp.cpp @@ -20,8 +20,8 @@ namespace na { namespace { -auto printParams(const std::vector& params, std::ostringstream& os) - -> void { +auto printGlobalParams(const std::vector& params, + std::ostringstream& os) -> void { if (!params.empty()) { for (const auto& p : params) { os << p << " "; @@ -35,14 +35,14 @@ auto GlobalOp::toString() const -> std::string { ss << std::setprecision(5) << std::fixed; ss << "@+ " << name_ << " "; if (zones_.size() == 1) { - printParams(params_, ss); + printGlobalParams(params_, ss); ss << *zones_.front(); return ss.str(); } ss << "[\n"; for (const auto& atom : zones_) { ss << " "; - printParams(params_, ss); + printGlobalParams(params_, ss); ss << *atom << "\n"; } ss << "]"; diff --git a/src/na/operations/LocalOp.cpp b/src/na/operations/LocalOp.cpp index 509d0f93fd..2c5c57d346 100644 --- a/src/na/operations/LocalOp.cpp +++ b/src/na/operations/LocalOp.cpp @@ -21,7 +21,7 @@ namespace na { namespace { -auto printParams(const std::vector& params, std::ostringstream& os) +auto printLocalParams(const std::vector& params, std::ostringstream& os) -> void { if (!params.empty()) { for (const auto& p : params) { @@ -36,14 +36,14 @@ auto LocalOp::toString() const -> std::string { ss << std::setprecision(5) << std::fixed; ss << "@+ " << name_ << " "; if (atoms_.size() == 1) { - printParams(params_, ss); + printLocalParams(params_, ss); ss << *atoms_.front(); return ss.str(); } ss << "[\n"; for (const auto& atom : atoms_) { ss << " "; - printParams(params_, ss); + printLocalParams(params_, ss); ss << *atom << "\n"; } ss << "]"; From d38c2417d35a5358eb0b598f2c4250e65d5a5d47 Mon Sep 17 00:00:00 2001 From: Daniel Haag <121057143+denialhaag@users.noreply.github.com> Date: Fri, 14 Aug 2026 18:20:41 +0200 Subject: [PATCH 22/35] =?UTF-8?q?=F0=9F=93=9D=20Backport=20compatible=20ma?= =?UTF-8?q?intenance=20cleanups?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Correct the Markdown hook indentation, simplify two documentation links, and keep the QDMI installation snippet independent of a particular virtual-environment prompt. Backports the remaining compatible maintenance changes from #1989. Assisted-by: GPT-5.6 via Codex --- .pre-commit-config.yaml | 2 +- docs/index.md | 6 ++---- docs/qdmi/qdmi_backend.md | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 67ae077616..52d2a1f424 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -112,7 +112,7 @@ repos: types_or: [yaml, html, css, scss, javascript, json, json5] priority: 5 - ## Format Markdown files with rumdl + ## Format Markdown files with rumdl - repo: https://github.com/rvben/rumdl-pre-commit rev: v0.2.49 hooks: diff --git a/docs/index.md b/docs/index.md index 821424e36d..8991ff494d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -115,14 +115,12 @@ for the quantum computing community. To support this endeavor, please consider: -- Starring and sharing our repositories: - [https://github.com/munich-quantum-toolkit](https://github.com/munich-quantum-toolkit) +- Starring and sharing our repositories: - Contributing code, documentation, tests, or examples via issues and pull requests - Citing the MQT in your publications (see {doc}`References `) - Using the MQT in research and teaching, and sharing feedback and use cases -- Sponsoring us on GitHub: - [https://github.com/sponsors/munich-quantum-toolkit](https://github.com/sponsors/munich-quantum-toolkit) +- Sponsoring us on GitHub:

diff --git a/docs/qdmi/qdmi_backend.md b/docs/qdmi/qdmi_backend.md index e44bf20020..b260cc6ea3 100644 --- a/docs/qdmi/qdmi_backend.md +++ b/docs/qdmi/qdmi_backend.md @@ -33,7 +33,7 @@ uv pip install "mqt-core[qiskit]" :sync: pip ```console -(.venv) $ python -m pip install "mqt-core[qiskit]" +python -m pip install "mqt-core[qiskit]" ``` ::: From e7898a52d0b287b0ce4feda50a7cf2265af37e01 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Fri, 14 Aug 2026 18:24:51 +0200 Subject: [PATCH 23/35] =?UTF-8?q?=F0=9F=94=A7=20Backport=20the=20optional-?= =?UTF-8?q?component=20wheel=20target?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Build Python packages through one aggregate target whose dependencies follow the enabled QDMI devices. This preserves the existing v3 wheel contents and lets stub and documentation builds disable unused providers without requesting absent targets. Backports the applicable packaging target from #2074 and completes the #2075 optimization on v3. Assisted-by: GPT-5.6 via Codex --- CMakeLists.txt | 26 ++++++++++++++++++++++++++ pyproject.toml | 19 ++----------------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1af6427aed..00ff6b189b 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -170,6 +170,32 @@ if(BUILD_MQT_CORE_MLIR) endif() endif() +if(BUILD_MQT_CORE_BINDINGS) + set(MQT_CORE_WHEEL_TARGETS + mqt-core-ir + mqt-core-algorithms + mqt-core-circuit-optimizer + mqt-core-dd + mqt-core-zx + mqt-core-ds + mqt-core-na + mqt-core-ir-bindings + mqt-core-dd-bindings + mqt-core-qdmi-bindings + mqt-core-na-bindings) + if(BUILD_MQT_CORE_QDMI_DDSIM_DEVICE) + list(APPEND MQT_CORE_WHEEL_TARGETS mqt-core-qdmi-ddsim-device) + endif() + if(BUILD_MQT_CORE_QDMI_NA_DEVICE) + list(APPEND MQT_CORE_WHEEL_TARGETS mqt-core-qdmi-na-device) + endif() + if(BUILD_MQT_CORE_QDMI_SC_DEVICE) + list(APPEND MQT_CORE_WHEEL_TARGETS mqt-core-qdmi-sc-device) + endif() + add_custom_target(mqt-core-wheel) + add_dependencies(mqt-core-wheel ${MQT_CORE_WHEEL_TARGETS}) +endif() + if(PROJECT_IS_TOP_LEVEL) if(NOT TARGET mqt-core-uninstall) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/cmake/cmake_uninstall.cmake.in diff --git a/pyproject.toml b/pyproject.toml index 632238d70b..13638e4e78 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,23 +90,8 @@ ninja.version = ">=1.10" # Setuptools-style build caching in a local directory build-dir = "build/python/{build_type}" -# All the targets to build -build.targets = [ - "mqt-core-ir", - "mqt-core-algorithms", - "mqt-core-circuit-optimizer", - "mqt-core-dd", - "mqt-core-zx", - "mqt-core-ds", - "mqt-core-na", - "mqt-core-ir-bindings", - "mqt-core-dd-bindings", - "mqt-core-qdmi-bindings", - "mqt-core-na-bindings", - "mqt-core-qdmi-ddsim-device", - "mqt-core-qdmi-na-device", - "mqt-core-qdmi-sc-device", -] +# Build the targets that match the enabled optional components. +build.targets = ["mqt-core-wheel"] install.components = [ "mqt-core_Runtime", From 428d523117ed8ae22cabe2b85925f85f6b656676 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Fri, 14 Aug 2026 18:43:37 +0200 Subject: [PATCH 24/35] =?UTF-8?q?=F0=9F=93=9D=20Finalize=20the=20combined?= =?UTF-8?q?=20v3=20backport?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Record the complete compatibility sweep and exact validation results. Adapt the Slurm workflow and runtime-configuration documentation to the v3 branch and documentation toolchain, and use current QDMI terminology throughout. Assisted-by: GPT-5.6 via Codex --- .agent/plans/backport-qdmi-slurm-v3.md | 260 +++++++++++++++---------- .github/workflows/slurm.yml | 2 +- CHANGELOG.md | 6 +- UPGRADING.md | 6 +- docs/index.md | 3 +- docs/qdmi/configuration.md | 24 +-- docs/qdmi/na_device.md | 6 +- 7 files changed, 183 insertions(+), 124 deletions(-) diff --git a/.agent/plans/backport-qdmi-slurm-v3.md b/.agent/plans/backport-qdmi-slurm-v3.md index 53adb4e89a..f3853d9ff4 100644 --- a/.agent/plans/backport-qdmi-slurm-v3.md +++ b/.agent/plans/backport-qdmi-slurm-v3.md @@ -1,4 +1,4 @@ -# Backport the remaining QDMI and Slurm functionality to v3.x +# Backport compatible QDMI, device, and maintenance changes to v3.x This ExecPlan is a living document. The sections `Progress`, `Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must @@ -9,76 +9,122 @@ repository root. ## Purpose / Big Picture -MQT Core 3 already exposes the QDMI Python namespace. This backport completes -the compatible QDMI feature set that merged into `main` after the previous -combined v3 backport. Users can inspect device queues, retrieve provider jobs, -query provider-specific operation lists, and select a registered device from a -Slurm license environment. Administrators also get a two-node Slurm fixture and -a concise tutorial that demonstrate cluster-wide admission without presenting -license metadata as authorization. +MQT Core 3 already exposes the QDMI Python namespace through the first namespace +backport. This combined backport completes the compatible QDMI work that has +since merged into `main`, restores the previously omitted runtime device +configuration, and carries compatible test, documentation, packaging, +dependency, and CI maintenance in one pull request. + +Users can inspect device queues, retrieve provider jobs, query custom operation +lists, configure the bundled neutral-atom and superconducting devices at +runtime, select reusable IQM models, and select a registered device from a Slurm +license environment. Administrators also get a two-node Slurm fixture and a +concise tutorial that demonstrate cluster-wide admission without presenting +license metadata as authorization. The documentation build emits `llms.txt` and +`llms-full.txt` through Sphinx-LLM. ## Progress -- [x] (2026-08-14 18:20Z) Compared every first-parent `main` change merged - after pull request #2024 with the current `v3.x` architecture. -- [x] (2026-08-14 18:25Z) Selected pull requests #2010, #2008, #2042, #2025, - and #2043 as one coherent, v3-compatible functionality range. -- [ ] Port queue telemetry and job retrieval while preserving the v3 QDMI - driver and Python namespace. -- [ ] Port generic custom-operation list queries. -- [ ] Port the Slurm license selector and its system fixture and tutorial. -- [ ] Regenerate stubs, validate the aggregate range, and complete an - adversarial review. +- [x] (2026-08-14) Compared each first-parent `main` change merged after pull + request #2024 with the current `v3.x` architecture. +- [x] (2026-08-14) Ported queue telemetry, job retrieval, custom operation + lists, the Slurm selector, and the Slurm fixture. +- [x] (2026-08-14) Ported runtime-configurable neutral-atom and superconducting + devices and the reusable IQM Garnet and Emerald models. +- [x] (2026-08-14) Ported compatible Python, packaging, dependency, CI, test, + documentation, and cleanup changes, including Sphinx-LLM. +- [x] (2026-08-14) Added the optional-component wheel target needed by the + faster stub build and regenerated the recursive stubs without a tracked + result. +- [x] (2026-08-14) Built and tested the complete non-MLIR native and Python + surfaces and built the documentation with Sphinx-LLM. +- [x] (2026-08-14) Completed the final lint pass and adversarial aggregate + review. Corrected the v3 workflow target, documentation roles, QDMI + terminology, optional-component wheel target, and unity-build collisions. - [ ] Publish one combined pull request against `v3.x` and verify its exact - revision, metadata, signatures, and replacement CI. + revision, metadata, signatures, assignment, labels, and replacement CI. ## Surprises & Discoveries -- Observation: The first namespace backport, pull request #2113, already - contains the compatibility surface required by the later Slurm layer. - Evidence: the current `v3.x` tip exports `mqt.core.qdmi`, - `mqt.core.qdmi.driver`, and the deprecated v3 aliases. -- Observation: The QDMI features form a dependency chain even though they - merged as separate pull requests on `main`. Queue telemetry updates the QDMI - revision and low-level driver contract; job retrieval and custom operation - lists build on that contract; the Slurm adapter builds on the namespace. +- Observation: Pull request #2113 already contains the compatibility surface + required by the later QDMI and Slurm layers. Evidence: `v3.x` exports + `mqt.core.qdmi`, `mqt.core.qdmi.driver`, and the deprecated v3 aliases. +- Observation: The QDMI features form a dependency chain even though they merged + as separate pull requests on `main`. Queue telemetry updates the QDMI revision + and low-level driver contract; retrieval and custom operation lists build on + that contract; the Slurm adapter builds on the namespace. +- Observation: The v3 Python package still listed each native target directly. + Disabling the NA and SC providers for stub generation therefore requested + targets that CMake intentionally did not create. Evidence: the first stub + build failed on `mqt-core-qdmi-na-device`. The conditional `mqt-core-wheel` + target from the namespace work fixes this without changing wheel contents. +- Observation: Enabling unity builds exposed the same DD-binding and + neutral-atom anonymous-namespace collisions fixed on `main`. The applicable + source fixes from pull request #2047 are required even though its newer MLIR + documentation-build path is not present on v3. +- Observation: Sphinx-LLM entered `main` as part of pull request #1989, while + pull request #2046 only added the minimum version. Both pieces are required + for a functional backport. ## Decision Log -- Decision: Use one pull request with separate signed commits for each upstream - functionality. Rationale: this saves stable-branch CI capacity while keeping - provenance, review boundaries, and possible future reverts clear. +- Decision: Use one pull request with separate signed commits for coherent + upstream functions. Rationale: this saves stable-branch CI capacity while + preserving provenance, review boundaries, and possible future reverts. Date/Author: 2026-08-14 / Codex. -- Decision: Exclude compiler and QIR changes, breaking removals, dependency - churn, and CI-only optimizations merged after #2024. Rationale: those changes - either depend on the LLVM/MLIR 22 architecture, conflict with the v3 - compatibility promise, or do not justify widening a functional backport. +- Decision: Include non-breaking maintenance when it applies to v3. This covers + CPython 3.15 preparation, standard dynamic package metadata, nanobind 2.14, + faster stubs, Sphinx-LLM, unity builds, the reduced CI matrix, hook and action + updates, stronger ZX tests, optional-dependency cleanup, and dead private + typing-code removal. Date/Author: 2026-08-14 / Codex. +- Decision: Exclude the compiler-target, QCO/QTensor, newer QIR-runtime, and + OpenQASM semantic series. Their source paths do not exist on v3 or depend on + the LLVM/MLIR 22 architecture. Also exclude the explicitly breaking ZX and + CMake-helper removals. Date/Author: 2026-08-14 / Codex. +- Decision: Treat lock-only maintenance as superseded by the final lock + generated from this aggregate dependency set. Rationale: replaying an older + lock snapshot would replace newer, intentionally resolved metadata. Date/Author: 2026-08-14 / Codex. - Decision: Keep one local Slurm license with an optional count of one as the selector grammar. Rationale: this is the reviewed `main` contract; Slurm licenses are admission controls and mutable environment metadata, not access - credentials or allocation attestation. - Date/Author: 2026-08-14 / Codex. + credentials or allocation attestation. Date/Author: 2026-08-14 / Codex. ## Outcomes & Retrospective -Implementation and final validation are pending. +The aggregate non-MLIR release build succeeds with LLVM 21.1.8 `llc`. All 1,526 +CTest cases pass; two provider-specification job-ID cases are intentionally +skipped. The complete Python suite passes 531 tests with five documented skips. +Recursive stub generation completes without a tracked stub change. + +The Sphinx build succeeds with the existing v3 warning baseline and produces +non-empty `llms.txt` and `llms-full.txt` outputs. The privileged Slurm fixture +remains a GitHub Actions validation because it requires Docker, systemd, cgroup +v2, and two Slurm compute containers. + +Final lint and the adversarial review pass. Publication and exact-head CI are +pending. ## Context and Orientation QDMI is the low-level device interface. Device providers implement its device side. `src/qdmi/driver` loads registered provider libraries and implements the QDMI client interface. `src/fomac` and `include/mqt-core/fomac` provide owning -C++ wrappers, while `bindings/qdmi` exposes the Python namespace. The current -v3 Python API uses `mqt.core.qdmi` for device objects and -`mqt.core.qdmi.driver` for registration and opening. +C++ wrappers, while `bindings/qdmi` exposes the Python namespace. The current v3 +Python API uses `mqt.core.qdmi` for device objects and `mqt.core.qdmi.driver` +for registration and opening. Queue telemetry consists of an optional device queue length and an optional job queue position. Job retrieval opens an existing provider job by its stable job identifier. A custom operation list is a provider-defined QDMI property whose -payload is an array of operation handles; FoMaC converts those handles to the +payload is an array of operation handles. FoMaC converts those handles to the same owning `Operation` objects used by the standard operation property. +The NA and SC providers load strict JSON models into session-owned state. Their +configuration can come from persistent device definitions, explicit session +parameters, environment-selected files, or bundled defaults. The IQM Garnet and +Emerald files are reusable SC models registered under stable IDs. + The Slurm adapter reads `SLURM_JOB_LICENSES`, accepts one registered local device ID with an omitted count or count one, opens a fresh device session, and accepts device states `IDLE` and `BUSY`. The environment variable is mutable. @@ -88,114 +134,126 @@ the authorization boundary. ## Plan of Work -First adapt the implementation and tests from #2010 to the v3 driver and the -new QDMI Python namespace. Update the bundled QDMI revision to the version that -defines the queue and retrieval client interfaces. Preserve optional-provider -loading so old provider libraries remain usable and report unsupported -retrieval rather than failing to load. - -Next add the high-level retrieval and custom-operation APIs from #2008 and -#2042. Keep returned devices, jobs, sites, and operations owning their provider -sessions. Return `None` or `std::nullopt` for unsupported optional properties, -reject malformed byte counts, and preserve provider error codes. - -Then adapt #2025 and #2043 directly to the v3 namespace established by #2113. -Keep the mechanism-specific `fomac::slurm::openDeviceFromLicense()` C++ API and -`mqt.core.qdmi.slurm.open_device_from_license()` Python API. Add the modern -Slurm 25.11 fixture, wheel-based container installation, DDSIM and SC license -scenarios, and the trust-boundary tutorial. - -Keep one signed commit per upstream pull request. Resolve the combined -`CHANGELOG.md` additions semantically and do not carry obsolete intermediate -plans or superseded API names from development history. +Adapt the implementation and tests from pull request #2010 to the v3 driver and +the QDMI Python namespace. Update the bundled QDMI revision to the version that +defines queue and retrieval interfaces. Preserve optional-provider loading so +old provider libraries remain usable and report unsupported retrieval rather +than failing to load. + +Add the high-level retrieval and custom-operation APIs from pull requests #2008 +and #2042. Keep returned devices, jobs, sites, and operations owning their +provider sessions. Return `None` or `std::nullopt` for unsupported optional +properties, reject malformed byte counts, and preserve provider errors. + +Adapt pull requests #2025 and #2043 to the v3 namespace. Keep the +mechanism-specific `fomac::slurm::openDeviceFromLicense()` C++ API and +`mqt.core.qdmi.slurm.open_device_from_license()` Python API. Add the Slurm 25.11 +fixture, wheel-based container installation, DDSIM and SC license scenarios, and +the trust-boundary tutorial. + +Port the runtime configuration from pull requests #1974, #1980, and #1992. Then +apply each compatible maintenance change independently, retain v3's optional +LLVM/MLIR 21 contract, and omit main-only architecture work. Resolve the +combined changelog semantically and do not carry obsolete intermediate plans or +superseded APIs. ## Concrete Steps -Run all commands from the repository root through the worktree-local wrapper -when they create caches or build artifacts. +Run cache-producing commands through the worktree-local wrapper. -Apply the five upstream changes in dependency order and inspect each staged -range before committing. Regenerate recursive stubs after all binding changes: +Regenerate recursive stubs after all binding changes: ./.agent/run.sh uvx nox -s stubs -Configure and build the native release tree, then run the focused driver, -FoMaC, DDSIM, and Slurm-selector tests: +Configure, build, and test the non-MLIR release tree with LLVM 21.1.8 `llc` on +`PATH`: ./.agent/run.sh cmake --preset release ./.agent/run.sh cmake --build --preset release ./.agent/run.sh ctest --preset release -Install the Python package and run the QDMI tests: +Install the Python package and run the Python suite: ./.agent/run.sh uv sync --inexact --only-group build --only-group test ./.agent/run.sh uv sync --inexact --no-dev --no-build-isolation-package mqt-core - ./.agent/run.sh uv run --no-sync pytest test/python/qdmi -q + ./.agent/run.sh uv run --no-sync pytest test/python -q -Run strict documentation and repository validation: +Run documentation and repository validation: ./.agent/run.sh uvx nox --non-interactive -s docs ./.agent/run.sh uvx nox -s lint git diff --check origin/v3.x...HEAD -The privileged Slurm workflow is executed by GitHub Actions because it requires -Docker, systemd, cgroup v2, and two Slurm compute containers. +The privileged Slurm workflow runs in GitHub Actions. ## Validation and Acceptance -The driver tests must prove absent and present queue values, a fresh status -query for each job queue-position request, supported and unsupported retrieval, -correct job ownership, custom operation handles, malformed custom payloads, and -operation-property queries. Python tests must prove equivalent `None`, error, -and lifetime behavior through `mqt.core.qdmi`. +The driver tests prove absent and present queue values, a fresh status query for +each job queue-position request, supported and unsupported retrieval, correct +job ownership, custom operation handles, malformed custom payloads, and +operation-property queries. Python tests prove equivalent `None`, error, and +lifetime behavior through `mqt.core.qdmi`. + +Runtime-configuration tests prove strict model parsing, source precedence, +independent and concurrent sessions, supported topology and calibration data, +and stable model registration. Packaging must build with all device targets and +with the NA and SC providers disabled for stub generation. -The selector tests must accept only `` and -`:1`. They must reject a missing value, whitespace, -unknown IDs, remote licenses, malformed or non-unit counts, and AND or OR -expressions. It must open a fresh device in `IDLE` or `BUSY` state and reject -all other states with the ID and status in the diagnostic. +The selector tests accept only `` and +`:1`. They reject a missing value, whitespace, unknown +IDs, remote licenses, malformed or non-unit counts, and AND or OR expressions. +The adapter opens a fresh device in `IDLE` or `BUSY` state and rejects all other +states with the ID and status in the diagnostic. The privileged fixture must run two held DDSIM Bell jobs on separate nodes, leave a third pending for the DDSIM license while CPUs remain available, run an SC job independently, release one DDSIM job, admit the pending job, and validate -256 samples containing only `00` and `11`. It must also prove that the selector -rejects compound and non-unit expressions. The container must import the built -wheel without `PYTHONPATH`, and the packaged `slurmd` unit must report -`Delegate=yes`. +256 samples containing only `00` and `11`. It also proves that the selector +rejects compound and non-unit expressions. The container imports the built wheel +without `PYTHONPATH`, and the packaged `slurmd` unit reports `Delegate=yes`. + +The documentation build must load `sphinx_llm.txt` and create non-empty +`llms.txt` and `llms-full.txt` files. ## Idempotence and Recovery Builds, stub generation, tests, and documentation commands are repeatable. A -failed source adaptation is repaired in the current logical commit rather than +failed source adaptation is repaired in its logical commit rather than by discarding unrelated work. If `v3.x` advances before publication, rebase the complete sequence, resolve changelog conflicts semantically, regenerate stubs, -and rerun the affected tests. Never modify another task worktree. +and rerun affected tests. Never modify another task worktree. ## Artifacts and Notes -The selected upstream commits are the squash commits for #2010, #2008, #2042, -#2025, and #2043 on `main`. Pull request #2113 is the authoritative v3 prefix. -Final exact heads, test counts, and any environment-limited checks will be added -here after validation. +The combined series backports functionality from pull requests `#1974`, `#1980`, +`#1989`, `#1992`, `#2008`, `#2010`, `#2011`, `#2019`, `#2020`, `#2021`, `#2023`, +`#2025`, `#2027`, `#2029`, `#2042`, `#2043`, `#2046`, `#2047`, `#2059`, `#2065`, +`#2073`, `#2074`, `#2075`, `#2083`, and `#2108`. Pull request `#2113` is the +authoritative v3 prefix. + +The lock-only pull request #2033 is represented by the final `uv lock` result. +Compiler and QIR changes are excluded because the corresponding main source tree +is not present on v3. Pull requests #2082 and #2106 are explicitly breaking and +remain main-only. -## Interfaces and Dependencies +### Interfaces and Dependencies -The final public additions are: +The public additions are: -- optional C++ and Python device queue-length queries and job queue-position - queries; +- optional C++ and Python device queue-length and job queue-position queries; - `fomac::Device::retrieveJobById(std::string_view)` and `mqt.core.qdmi.Device.retrieve_job_by_id(str)`; - `fomac::Device::queryCustomOperations(fomac::CustomProperty)` and `mqt.core.qdmi.Device.query_custom_operations(CustomProperty)`; - `fomac::slurm::openDeviceFromLicense()` and - `mqt.core.qdmi.slurm.open_device_from_license()`. + `mqt.core.qdmi.slurm.open_device_from_license()`; +- runtime-configurable NA and SC providers and stable IQM model registrations. -The bundled QDMI revision must expose the matching queue and retrieval enums and +The bundled QDMI revision exposes the matching queue and retrieval enums and functions. The Slurm fixture requires Slurm 25.11 or newer, Ubuntu 26.04, systemd, Munge, cgroup v2, and Docker Compose. Runtime device authorization is provider-specific and remains outside the Slurm selector. -Revision note: created this plan after the first namespace backport merged and -after classifying every `main` change merged since the previous combined v3 -backport. +Revision note: expanded the original QDMI and Slurm plan after the user asked +for one comprehensive non-breaking backport, runtime device configuration, +compatible CI and dependency updates, and Sphinx-LLM. diff --git a/.github/workflows/slurm.yml b/.github/workflows/slurm.yml index 04c282574d..3256dae539 100644 --- a/.github/workflows/slurm.yml +++ b/.github/workflows/slurm.yml @@ -3,7 +3,7 @@ name: Slurm integration on: push: branches: - - main + - v3.x paths: - ".github/workflows/slurm.yml" - "bindings/qdmi/**" diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c0ca518f2..ff94543f96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -623,9 +623,9 @@ _If you are upgrading: please see [`UPGRADING.md`](UPGRADING.md#300)._ ## [2.7.0] - 2024-10-08 -_📚 Refer to the [GitHub Release -Notes](https://github.com/munich-quantum-toolkit/core/releases) for previous -changelogs._ +_📚 Refer to the +[GitHub Release Notes](https://github.com/munich-quantum-toolkit/core/releases) +for previous changelogs._ diff --git a/UPGRADING.md b/UPGRADING.md index 873df25a79..c720db1ada 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -25,8 +25,8 @@ with the `na::Device` configuration type and the `na::readJSON` functions in `qdmi/devices/na/Configuration.hpp`. At runtime, use the registry `session.device-config` field or Python -`device_config` and `device_config_file` arguments. Direct QDMI v1 clients pass -inline JSON through CUSTOM1 or a file path through CUSTOM2. +`device_config` and `device_config_file` arguments. Direct low-level QDMI +clients pass inline JSON through CUSTOM1 or a file path through CUSTOM2. ### Bundled QDMI devices in embedded builds @@ -124,7 +124,7 @@ an unknown or disabled ID fails. `fomac::Session::openDevice` creates a fresh owned session on every call. `qdmi::Driver::open(id)` retains its cached-device behavior for client callers. -See the [QDMI device configuration guide](docs/qdmi/configuration.md) for the +See the {doc}`QDMI device configuration guide ` for the versioned JSON and TOML formats, configuration precedence, and relocatable device manifests. diff --git a/docs/index.md b/docs/index.md index 8991ff494d..c85495084e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -115,7 +115,8 @@ for the quantum computing community. To support this endeavor, please consider: -- Starring and sharing our repositories: +- Starring and sharing our repositories: + - Contributing code, documentation, tests, or examples via issues and pull requests - Citing the MQT in your publications (see {doc}`References `) diff --git a/docs/qdmi/configuration.md b/docs/qdmi/configuration.md index 0c15ba1e2d..961fc44080 100644 --- a/docs/qdmi/configuration.md +++ b/docs/qdmi/configuration.md @@ -63,10 +63,10 @@ or: The inline value must be a JSON object. A relative file path is resolved against the registry file that declares it. The complete source is one merge field: changing from `inline` to `file` at a higher-precedence layer replaces the -inherited inline JSON. The Driver adapts inline JSON to QDMI v1 CUSTOM1 and a -file path to CUSTOM2 when opening the native session. Consequently, -`device-config` cannot be combined with raw `custom1` or `custom2`; CUSTOM3 -through CUSTOM5 remain available to providers. +inherited inline JSON. The Driver adapts inline JSON to the low-level QDMI +CUSTOM1 parameter and a file path to CUSTOM2 when opening the native session. +Consequently, `device-config` cannot be combined with raw `custom1` or +`custom2`; CUSTOM3 through CUSTOM5 remain available to providers. Relative library and authentication-file paths are resolved against the file that declared them. For `MQT_CORE_QDMI_CONFIG_JSON`, they resolve against the @@ -159,13 +159,13 @@ registrations without loading native device libraries or exposing their paths, prefixes, or session configuration. The equivalent C++ registration operation is -{cpp-api:func}`qdmi::Driver::registerDevice`. Duplicate IDs are rejected unless +{cpp:func}`qdmi::Driver::registerDevice`. Duplicate IDs are rejected unless `replace` is true, and an opened definition cannot be replaced. -{cpp-api:func}`qdmi::Driver::registeredDeviceIds` provides the same load-free -enumeration, and {cpp-api:func}`qdmi::Driver::open` returns the cached device. -{cpp-api:func}`fomac::Session::openDevice` returns a fresh device session and -does not add it to the QDMI client catalog. Runtime registrations and explicit -opens are not added to that catalog. +{cpp:func}`qdmi::Driver::registeredDeviceIds` provides the same load-free +enumeration, and {cpp:func}`qdmi::Driver::open` returns the cached device. +{cpp:func}`fomac::Session::openDevice` returns a fresh device session and does +not add it to the QDMI client catalog. Runtime registrations and explicit opens +are not added to that catalog. Multiple definitions may refer to the same library and prefix. MQT Core reuses the initialized library while creating a fresh QDMI device session, with its own @@ -225,8 +225,8 @@ definition by stable ID. A fully static executable has no portable shared-module location. Place the fragments beside the executable, point `MQT_CORE_QDMI_CONFIG_FILE` at a complete -configuration, or use {cpp-api:func}`qdmi::Driver::registerDevice` and -{cpp-api:func}`qdmi::Driver::open`. No install prefix is compiled into the +configuration, or use {cpp:func}`qdmi::Driver::registerDevice` and +{cpp:func}`qdmi::Driver::open`. No install prefix is compiled into the manifests. An installed MQT Core CMake package provides a helper that colocates selected diff --git a/docs/qdmi/na_device.md b/docs/qdmi/na_device.md index ad48234dcc..7eec73010f 100644 --- a/docs/qdmi/na_device.md +++ b/docs/qdmi/na_device.md @@ -23,14 +23,14 @@ state, so the same allocated session can be corrected and initialized again. Driver users should use the typed `device-config` registry field or the `device_config` and `device_config_file` Python arguments described in -{doc}`configuration`. Direct QDMI v1 clients use CUSTOM1 and CUSTOM2. +{doc}`configuration`. Direct low-level QDMI clients use CUSTOM1 and CUSTOM2. ## Schema and validation Every description is a strict JSON object with `"schema-version": 1`. The bundled example is `json/na/mqt-core-qdmi-na-device.json`, and the C++ value -model is {cpp-api:class}`na::Device`. Required top-level and nested fields -cannot be omitted, unknown fields are rejected, and validation covers: +model is {cpp:class}`na::Device`. Required top-level and nested fields cannot be +omitted, unknown fields are rejected, and validation covers: - non-empty names and positive device capacity; - finite positive unit scales and supported units; From 480087be07c62da5fafd8457acf19eb7f2401ef2 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Fri, 14 Aug 2026 19:16:22 +0200 Subject: [PATCH 25/35] =?UTF-8?q?=F0=9F=94=87=20Resolve=20v3=20backport=20?= =?UTF-8?q?lint=20diagnostics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use the neutral-atom lattice scalar type for the determinant threshold and construct complete device session configurations in the backported tests. This avoids double-promotion and partial-aggregate diagnostics without changing runtime behavior. Assisted-by: GPT-5.6 via Codex --- src/qdmi/devices/na/Configuration.cpp | 3 +- test/fomac/test_slurm.cpp | 8 +++- test/qdmi/driver/test_driver.cpp | 58 +++++++++++++++++---------- 3 files changed, 46 insertions(+), 23 deletions(-) diff --git a/src/qdmi/devices/na/Configuration.cpp b/src/qdmi/devices/na/Configuration.cpp index 4d256b2382..6d0255e4d7 100644 --- a/src/qdmi/devices/na/Configuration.cpp +++ b/src/qdmi/devices/na/Configuration.cpp @@ -481,7 +481,8 @@ solve2DLinearEquation(const int64_t x1, const int64_t x2, const int64_t y1, }; const auto det = (asLatticeFloat(x1) * asLatticeFloat(y2)) - (asLatticeFloat(x2) * asLatticeFloat(y1)); - if (constexpr auto epsilon = 1e-10; std::abs(det) < epsilon) { + if (constexpr auto epsilon = static_cast(1e-10); + std::abs(det) < epsilon) { throw std::runtime_error("The system of equations has no unique solution."); } const auto detX = (asLatticeFloat(x0) * asLatticeFloat(y2)) - diff --git a/test/fomac/test_slurm.cpp b/test/fomac/test_slurm.cpp index 28847dc6be..b65151b12c 100644 --- a/test/fomac/test_slurm.cpp +++ b/test/fomac/test_slurm.cpp @@ -70,13 +70,19 @@ class ScopedSlurmLicenses { std::optional originalValue; }; +qdmi::DeviceSessionConfig makeStatusSession(const std::string& status) { + qdmi::DeviceSessionConfig session; + session.custom4 = status; + return session; +} + void registerStatusDevice(const std::string& id, const std::string& configuredStatus) { static_cast(qdmi::Driver::get().registerDeviceIfAbsent( {.id = id, .library = MQT_CORE_FOMAC_SLURM_TEST_DEVICE, .prefix = "TEST_SESSION", - .session = {.custom4 = configuredStatus}})); + .session = makeStatusSession(configuredStatus)})); } } // namespace diff --git a/test/qdmi/driver/test_driver.cpp b/test/qdmi/driver/test_driver.cpp index f8becc0fdb..be62d7f5ad 100644 --- a/test/qdmi/driver/test_driver.cpp +++ b/test/qdmi/driver/test_driver.cpp @@ -843,6 +843,19 @@ constexpr std::array DEVICES{"MQT NA Default QDMI Device", "MQT SC Default QDMI Device"}; namespace { +qdmi::DeviceSessionConfig +makeDeviceConfigurationSession(qdmi::DeviceConfigurationSource source) { + qdmi::DeviceSessionConfig session; + session.deviceConfiguration = std::move(source); + return session; +} + +qdmi::DeviceSessionConfig makeCustom3Session(std::string value) { + qdmi::DeviceSessionConfig session; + session.custom3 = std::move(value); + return session; +} + void registerSessionTestDevice() { qdmi::DeviceSessionConfig sessionConfig; sessionConfig.baseUrl = "registered-base"; @@ -1100,13 +1113,14 @@ TEST(DeviceRegistrationTest, static_cast( driver.registerDeviceIfAbsent({.id = "test.na.runtime-default", .library = MQT_CORE_QDMI_NA_LIBRARY, - .prefix = "MQT_NA"})); + .prefix = "MQT_NA", + .session = {}})); static_cast(driver.registerDeviceIfAbsent( {.id = "test.na.runtime-custom", .library = MQT_CORE_QDMI_NA_LIBRARY, .prefix = "MQT_NA", - .session = {.deviceConfiguration = qdmi::FileDeviceConfiguration{ - .path = MQT_CORE_QDMI_CUSTOM_NA_FILE}}})); + .session = makeDeviceConfigurationSession(qdmi::FileDeviceConfiguration{ + .path = MQT_CORE_QDMI_CUSTOM_NA_FILE})})); const auto defaultDevice = fomac::Session::openDevice("test.na.runtime-default"); @@ -1134,8 +1148,8 @@ TEST(DeviceRegistrationTest, {.id = "test.na.runtime-invalid", .library = MQT_CORE_QDMI_NA_LIBRARY, .prefix = "MQT_NA", - .session = {.deviceConfiguration = - qdmi::InlineDeviceConfiguration{.json = "{}"}}})); + .session = makeDeviceConfigurationSession( + qdmi::InlineDeviceConfiguration{.json = "{}"})})); EXPECT_THROW( static_cast(fomac::Session::openDevice("test.na.runtime-invalid")), std::runtime_error); @@ -1221,12 +1235,8 @@ TEST(DeviceRegistrationTest, CustomOperationRetainsOwningDeviceSession) { TEST(DeviceRegistrationTest, ScRuntimeConfigurationSeparatesModelsUsingOneProviderLibrary) { auto& driver = qdmi::Driver::get(); - static_cast(driver.registerDeviceIfAbsent( - {.id = "test.sc.runtime-one", - .library = MQT_CORE_QDMI_SC_LIBRARY, - .prefix = "MQT_SC", - .session = {.deviceConfiguration = - qdmi::InlineDeviceConfiguration{.json = R"({ + const auto firstConfig = makeDeviceConfigurationSession( + qdmi::InlineDeviceConfiguration{.json = R"({ "schema-version":1, "name":"SC runtime one", "numQubits":1, @@ -1240,13 +1250,14 @@ TEST(DeviceRegistrationTest, "duration":7, "fidelity":0.8 }] - })"}}})); - static_cast(driver.registerDeviceIfAbsent( - {.id = "test.sc.runtime-two", - .library = MQT_CORE_QDMI_SC_LIBRARY, - .prefix = "MQT_SC", - .session = {.deviceConfiguration = - qdmi::InlineDeviceConfiguration{.json = R"({ + })"}); + static_cast( + driver.registerDeviceIfAbsent({.id = "test.sc.runtime-one", + .library = MQT_CORE_QDMI_SC_LIBRARY, + .prefix = "MQT_SC", + .session = firstConfig})); + const auto secondConfig = makeDeviceConfigurationSession( + qdmi::InlineDeviceConfiguration{.json = R"({ "schema-version":1, "name":"SC runtime two", "numQubits":2, @@ -1254,7 +1265,12 @@ TEST(DeviceRegistrationTest, "qubitProperties":{"defaults":{},"overrides":[]}, "couplings":[[1,0]], "operations":[] - })"}}})); + })"}); + static_cast( + driver.registerDeviceIfAbsent({.id = "test.sc.runtime-two", + .library = MQT_CORE_QDMI_SC_LIBRARY, + .prefix = "MQT_SC", + .session = secondConfig})); const auto first = fomac::Session::openDevice("test.sc.runtime-one"); const auto second = fomac::Session::openDevice("test.sc.runtime-two"); @@ -1529,12 +1545,12 @@ TEST(DeviceSessionConfigTest, IdempotentLoadingWithDifferentConfigs) { TEST(DynamicDeviceLibraryTest, ReusesLibraryWithFreshDeviceSessions) { const auto [library, prefix] = TEST_DEVICE_LIBRARIES.front(); auto* const first = - openTestDevice(library, prefix, {.custom3 = "first-session"}); + openTestDevice(library, prefix, makeCustom3Session("first-session")); const auto equivalentLibrary = std::filesystem::path(library).parent_path() / "." / std::filesystem::path(library).filename(); auto* const second = openTestDevice(equivalentLibrary.string(), prefix, - {.custom3 = "second-session"}); + makeCustom3Session("second-session")); ASSERT_NE(first, second); EXPECT_EQ(&first->getLibrary(), &second->getLibrary()); From c32fd4d50aedcab1ffbdcf8426d2ae23533d2a44 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Fri, 14 Aug 2026 19:49:31 +0200 Subject: [PATCH 26/35] =?UTF-8?q?=E2=9C=85=20Cover=20optional=20plugin=20i?= =?UTF-8?q?mport=20failures?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exercise the compatibility paths that disable a plugin only when its top-level dependency is absent and propagate missing nested dependencies. Apply the same contract tests to the Qiskit and PennyLane plugins. Assisted-by: GPT-5.6 via Codex --- test/python/plugins/test_optional_plugins.py | 69 ++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 test/python/plugins/test_optional_plugins.py diff --git a/test/python/plugins/test_optional_plugins.py b/test/python/plugins/test_optional_plugins.py new file mode 100644 index 0000000000..672499c07a --- /dev/null +++ b/test/python/plugins/test_optional_plugins.py @@ -0,0 +1,69 @@ +# 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 + +"""Test optional plugin dependency detection.""" + +from __future__ import annotations + +import importlib +from typing import TYPE_CHECKING + +import pytest + +if TYPE_CHECKING: + from types import ModuleType + + +@pytest.mark.parametrize( + ("dependency", "availability_flag"), [("pennylane", "HAS_PENNYLANE"), ("qiskit", "HAS_QISKIT")] +) +def test_missing_optional_dependency( + monkeypatch: pytest.MonkeyPatch, + dependency: str, + availability_flag: str, +) -> None: + """Treat a missing top-level dependency as an unavailable plugin.""" + plugin = importlib.import_module(f"mqt.core.plugins.{dependency}") + original_import_module = importlib.import_module + + def import_without_dependency(name: str, package: str | None = None) -> ModuleType: + if name == dependency: + raise ModuleNotFoundError(name=dependency) + return original_import_module(name, package) + + try: + with monkeypatch.context() as context: + context.setattr(importlib, "import_module", import_without_dependency) + reloaded_plugin = importlib.reload(plugin) + assert getattr(reloaded_plugin, availability_flag) is False + finally: + importlib.reload(plugin) + + +@pytest.mark.parametrize("dependency", ["pennylane", "qiskit"]) +def test_nested_import_failure_is_not_hidden( + monkeypatch: pytest.MonkeyPatch, + dependency: str, +) -> None: + """Propagate a missing dependency imported by an installed plugin.""" + plugin = importlib.import_module(f"mqt.core.plugins.{dependency}") + original_import_module = importlib.import_module + + def import_with_nested_failure(name: str, package: str | None = None) -> ModuleType: + if name == dependency: + raise ModuleNotFoundError(name="nested_dependency") + return original_import_module(name, package) + + try: + with monkeypatch.context() as context: + context.setattr(importlib, "import_module", import_with_nested_failure) + with pytest.raises(ModuleNotFoundError) as exc_info: + importlib.reload(plugin) + assert exc_info.value.name == "nested_dependency" + finally: + importlib.reload(plugin) From 3ef5307880f7d1efce7bbee37fa791afb5666b73 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Fri, 14 Aug 2026 20:26:22 +0200 Subject: [PATCH 27/35] =?UTF-8?q?=F0=9F=A7=B0=20Backport=20typed=20QDMI=20?= =?UTF-8?q?adapter=20configuration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add public session and job parameter TypedDicts and typed stable-ID keyword overrides while preserving the v3 session_parameters mapping. Keep PennyLane session and job mappings separate and retain runtime validation. Backports the compatible configuration typing from #2084. Assisted-by: GPT-5.6 via Codex --- docs/conf.py | 1 + docs/qdmi/qdmi_backend.md | 15 +++- pyproject.toml | 4 ++ python/mqt/core/_compat/typing.py | 25 +++++++ python/mqt/core/plugins/pennylane/device.py | 10 +-- python/mqt/core/plugins/qiskit/backend.py | 23 ++++-- python/mqt/core/typing.py | 46 ++++++++++++ .../plugins/qdmi_pennylane/test_device.py | 6 +- test/python/plugins/qiskit/test_backend.py | 71 ++++++++++++++++++- uv.lock | 4 ++ 10 files changed, 192 insertions(+), 13 deletions(-) create mode 100644 python/mqt/core/_compat/typing.py create mode 100644 python/mqt/core/typing.py diff --git a/docs/conf.py b/docs/conf.py index 729049552d..aad31a09dd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -167,6 +167,7 @@ def format_url(self, _e: Entry) -> HRef: # ruff:ignore[no-self-use] # they are not documented objects. nitpick_ignore_regex = [ ("py:class", r"Ellipsis"), + ("py:class", r"mqt\.core\._compat\.typing\.Unpack"), ("py:class", r"ParametersType"), ("py:class", r"pennylane\.tape\.QuantumScriptOrBatch"), ("py:class", r"pennylane\.transforms\.core\.CompilePipeline"), diff --git a/docs/qdmi/qdmi_backend.md b/docs/qdmi/qdmi_backend.md index b260cc6ea3..cda5a673f2 100644 --- a/docs/qdmi/qdmi_backend.md +++ b/docs/qdmi/qdmi_backend.md @@ -93,8 +93,19 @@ print(f"Backend: {backend.name}") print(f"Qubits: {backend.target.num_qubits}") ``` -The optional `session_parameters` mapping applies explicit overrides to this -fresh device session. Persistent configuration remains the default. +Optional session keywords apply explicit overrides to this fresh device session. +Their names and value types are described by +{py:class}`mqt.core.typing.QDMISessionParameters`; persistent configuration +remains the default. The `session_parameters` mapping remains available in MQT +Core 3: + +```python +backend = QDMIBackend.from_device_id( + "provider.device", + token="access-token", + custom1="provider-specific-value", +) +``` ### Filtering Backends diff --git a/pyproject.toml b/pyproject.toml index 13638e4e78..22274bc8e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,9 @@ classifiers = [ ] requires-python = ">=3.10" dynamic = ["version"] +dependencies = [ + "typing-extensions>=4.12; python_version < '3.11'", +] [project.optional-dependencies] pennylane = [ @@ -218,6 +221,7 @@ ignore = [ "TID252" # Prefer absolute imports over relative imports from parent modules ] future-annotations = true +typing-modules = ["mqt.core._compat.typing"] [tool.ruff.lint.isort] known-first-party = ["mqt.core"] diff --git a/python/mqt/core/_compat/typing.py b/python/mqt/core/_compat/typing.py new file mode 100644 index 0000000000..b29e52cb1c --- /dev/null +++ b/python/mqt/core/_compat/typing.py @@ -0,0 +1,25 @@ +# 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 + +"""Compatibility imports for typing features.""" + +import sys + +if sys.version_info >= (3, 11): + from typing import Unpack as _Unpack +else: + from typing_extensions import Unpack as _Unpack + +# Give introspection tools an unconditional module-level export. +Unpack = _Unpack + +__all__ = ["Unpack"] + + +def __dir__() -> list[str]: + return __all__ diff --git a/python/mqt/core/plugins/pennylane/device.py b/python/mqt/core/plugins/pennylane/device.py index 3ba2da43ec..03f01d02e5 100644 --- a/python/mqt/core/plugins/pennylane/device.py +++ b/python/mqt/core/plugins/pennylane/device.py @@ -55,6 +55,8 @@ from pennylane.tape import QuantumScript, QuantumScriptOrBatch from pennylane.typing import Result, ResultBatch + from mqt.core.typing import QDMIJobParameters, QDMISessionParameters + __all__ = ["DDSIMDevice", "QDMIDevice"] _SESSION_PARAMETERS = frozenset({ @@ -122,8 +124,8 @@ def __init__( wires: int | Sequence[Hashable] | None = None, shots: int | Sequence[int | tuple[int, int]] | Shots | None = 1024, *, - session_parameters: Mapping[str, Any] | None = None, - job_parameters: Mapping[str, str | bool | float | None] | None = None, + session_parameters: QDMISessionParameters | None = None, + job_parameters: QDMIJobParameters | None = None, ) -> None: """Initialize and open a fresh QDMI device session. @@ -378,8 +380,8 @@ def __init__( wires: int | Sequence[Hashable] | None = None, shots: int | Sequence[int | tuple[int, int]] | Shots | None = 1024, *, - session_parameters: Mapping[str, object] | None = None, - job_parameters: Mapping[str, str | bool | float | None] | None = None, + session_parameters: QDMISessionParameters | None = None, + job_parameters: QDMIJobParameters | None = None, ) -> None: """Open the built-in DDSIM device by its stable QDMI ID.""" super().__init__( diff --git a/python/mqt/core/plugins/qiskit/backend.py b/python/mqt/core/plugins/qiskit/backend.py index 62fdb52f6a..0311007838 100644 --- a/python/mqt/core/plugins/qiskit/backend.py +++ b/python/mqt/core/plugins/qiskit/backend.py @@ -50,6 +50,8 @@ from qiskit.circuit import Instruction, Parameter from qiskit.circuit.parameterexpression import ParameterValueType + from ..._compat.typing import Unpack + from ...typing import QDMISessionParameters from .provider import QDMIProvider # Type alias for parameter values @@ -195,21 +197,32 @@ def from_device_id( *, provider: QDMIProvider | None = None, session_parameters: Mapping[str, Any] | None = None, + **session_overrides: Unpack[QDMISessionParameters], ) -> QDMIBackend: """Open a registered QDMI device and adapt it for Qiskit. Args: device_id: Stable ID from the QDMI device registry. provider: Provider to associate with the backend. - session_parameters: Optional overrides for this device session. + session_parameters: Optional v3-compatible mapping of overrides for + this device session. + session_overrides: Typed overrides for this device session. Returns: A Qiskit backend for a fresh QDMI device session. + + Raises: + TypeError: If one override is present in both supported v3 call + styles. """ - return cls( - device=open_device(device_id, **dict(session_parameters or {})), - provider=provider, - ) + overrides = dict(session_parameters or {}) + duplicates = overrides.keys() & session_overrides.keys() + if duplicates: + names = ", ".join(sorted(duplicates)) + msg = f"QDMI session parameter(s) specified twice: {names}" + raise TypeError(msg) + overrides.update(session_overrides) + return cls(device=open_device(device_id, **overrides), provider=provider) @property def target(self) -> Target: diff --git a/python/mqt/core/typing.py b/python/mqt/core/typing.py new file mode 100644 index 0000000000..549efa8fa4 --- /dev/null +++ b/python/mqt/core/typing.py @@ -0,0 +1,46 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Shared public typing helpers for MQT Core.""" + +import os # ruff: ignore[typing-only-standard-library-import] +from typing import TypedDict + +__all__ = ["QDMIJobParameters", "QDMISessionParameters"] + + +def __dir__() -> list[str]: + return __all__ + + +class QDMISessionParameters(TypedDict, total=False): + """Keyword arguments accepted when opening a QDMI device session.""" + + base_url: str | None + token: str | None + auth_file: str | os.PathLike[str] | None + auth_url: str | None + username: str | None + password: str | None + device_config: str | None + device_config_file: str | os.PathLike[str] | None + custom1: str | None + custom2: str | None + custom3: str | None + custom4: str | None + custom5: str | None + + +class QDMIJobParameters(TypedDict, total=False): + """Custom keyword arguments accepted when submitting a QDMI job.""" + + custom1: str | bool | float | None + custom2: str | bool | float | None + custom3: str | bool | float | None + custom4: str | bool | float | None + custom5: str | bool | float | None diff --git a/test/python/plugins/qdmi_pennylane/test_device.py b/test/python/plugins/qdmi_pennylane/test_device.py index 8d709959b5..03c37b66d0 100644 --- a/test/python/plugins/qdmi_pennylane/test_device.py +++ b/test/python/plugins/qdmi_pennylane/test_device.py @@ -201,7 +201,11 @@ def test_validates_configuration_and_width(monkeypatch: pytest.MonkeyPatch) -> N _patch_device(monkeypatch, qdmi) with pytest.raises(PennyLaneConfigurationError, match="unknown"): - QDMIDevice("fake.qdmi", wires=2, session_parameters={"unknown": "value"}) + QDMIDevice( + "fake.qdmi", + wires=2, + session_parameters={"unknown": "value"}, # ty: ignore[invalid-argument-type, invalid-key] + ) with pytest.raises(PennyLaneConfigurationError, match="3 wires"): QDMIDevice("fake.qdmi", wires=3) diff --git a/test/python/plugins/qiskit/test_backend.py b/test/python/plugins/qiskit/test_backend.py index 1f947d51db..4ddd213cc7 100644 --- a/test/python/plugins/qiskit/test_backend.py +++ b/test/python/plugins/qiskit/test_backend.py @@ -10,7 +10,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Protocol +import os +from pathlib import Path +from typing import TYPE_CHECKING, Protocol, get_type_hints import numpy as np import pytest @@ -26,6 +28,7 @@ ) from mqt.core.plugins.qiskit.exceptions import UnsupportedDeviceError from mqt.core.qdmi.driver import open_device +from mqt.core.typing import QDMISessionParameters if TYPE_CHECKING: from mqt.core.qdmi import Device as QDMIDevice @@ -67,12 +70,78 @@ def fake_open_device(device_id: str, **session_parameters: object) -> QDMIDevice monkeypatch.setattr("mqt.core.plugins.qiskit.backend.open_device", fake_open_device) + auth_file = Path("auth.json") + config_file = Path("device.json") + session_parameters: QDMISessionParameters = { + "base_url": "https://device.example", + "token": "token", + "auth_file": auth_file, + "auth_url": "https://auth.example", + "username": "user", + "password": "password", + "device_config": "{}", + "device_config_file": config_file, + "custom1": "one", + "custom2": "two", + "custom3": "three", + "custom4": "four", + "custom5": "five", + } + backend = QDMIBackend.from_device_id("test.device", **session_parameters) + + assert observed == ("test.device", session_parameters) + assert backend.target.num_qubits > 0 + + +def test_backend_from_device_id_preserves_v3_session_mapping(monkeypatch: pytest.MonkeyPatch) -> None: + """Keep the v3 session-parameter mapping while adding typed keywords.""" + observed: tuple[str, dict[str, object]] | None = None + + def fake_open_device(device_id: str, **session_parameters: object) -> QDMIDevice: + nonlocal observed + observed = device_id, session_parameters + return open_device("mqt.ddsim.default") + + monkeypatch.setattr("mqt.core.plugins.qiskit.backend.open_device", fake_open_device) + backend = QDMIBackend.from_device_id("test.device", session_parameters={"custom1": "value"}) assert observed == ("test.device", {"custom1": "value"}) assert backend.target.num_qubits > 0 +def test_backend_from_device_id_rejects_duplicate_session_parameters() -> None: + """Reject one override supplied through both supported v3 call styles.""" + with pytest.raises(TypeError, match="specified twice: custom1"): + QDMIBackend.from_device_id( + "mqt.ddsim.default", + session_parameters={"custom1": "mapping"}, + custom1="keyword", + ) + + +def test_backend_from_device_id_rejects_unknown_session_parameter() -> None: + """Reject unknown stable-ID factory keywords at runtime.""" + with pytest.raises(TypeError, match="unknown"): + QDMIBackend.from_device_id("mqt.ddsim.default", unknown="value") + + +def test_qdmi_session_parameter_annotations_are_runtime_resolvable() -> None: + """Expose the public TypedDict to runtime annotation consumers.""" + annotations = get_type_hints(QDMISessionParameters) + assert annotations["auth_file"] == str | os.PathLike[str] | None + + +def test_backend_from_device_id_rejects_conflicting_device_configuration() -> None: + """Retain native validation for mutually exclusive device configuration sources.""" + with pytest.raises(ValueError, match="mutually exclusive"): + QDMIBackend.from_device_id( + "mqt.ddsim.default", + device_config="{}", + device_config_file=Path("device.json"), + ) + + def test_backend_instantiation(ddsim_backend: QDMIBackend) -> None: """Backend exposes target qubit count.""" assert ddsim_backend.target.num_qubits > 0 diff --git a/uv.lock b/uv.lock index 14a8969986..9667b2551b 100644 --- a/uv.lock +++ b/uv.lock @@ -1754,6 +1754,9 @@ wheels = [ [[package]] name = "mqt-core" source = { editable = "." } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] [package.optional-dependencies] pennylane = [ @@ -1821,6 +1824,7 @@ test = [ requires-dist = [ { name = "pennylane", marker = "python_full_version >= '3.11' and extra == 'pennylane'", specifier = ">=0.45.1,<0.46" }, { name = "qiskit", extras = ["qasm3-import"], marker = "extra == 'qiskit'", specifier = ">=1.1.0" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'", specifier = ">=4.12" }, ] provides-extras = ["pennylane", "qiskit"] From 5d44ef101bcc2738f42738364a11fb6aa0034ce7 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Fri, 14 Aug 2026 20:27:06 +0200 Subject: [PATCH 28/35] =?UTF-8?q?=F0=9F=AA=AA=20Backport=20stable=20QDMI?= =?UTF-8?q?=20IDs=20in=20Qiskit=20backends?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Record the registry ID when a backend opens a registered device. Allow already-open and specialized devices to supply an ID explicitly while keeping direct construction and the v3 session-parameter mapping valid. Backports the compatible stable-ID behavior from #2084. Assisted-by: GPT-5.6 via Codex --- python/mqt/core/plugins/qiskit/backend.py | 21 +++++++++++++++++++-- test/python/plugins/qiskit/test_backend.py | 13 +++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/python/mqt/core/plugins/qiskit/backend.py b/python/mqt/core/plugins/qiskit/backend.py index 0311007838..c4949f0ff9 100644 --- a/python/mqt/core/plugins/qiskit/backend.py +++ b/python/mqt/core/plugins/qiskit/backend.py @@ -170,12 +170,19 @@ def is_convertible(device: QDMIDevice) -> 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: QDMIDevice, provider: QDMIProvider | None = None) -> None: + def __init__( + self, + device: QDMIDevice, + provider: QDMIProvider | None = None, + *, + device_id: str | None = None, + ) -> None: """Initialize the backend with a QDMI device wrapper. Args: device: QDMI device wrapper. provider: Provider instance that created this backend. + device_id: Stable registry ID for the opened device, if known. Raises: UnsupportedDeviceError: If the device cannot be represented in Qiskit's Target model. @@ -186,6 +193,7 @@ def __init__(self, device: QDMIDevice, provider: QDMIProvider | None = None) -> super().__init__(name=device.name(), provider=provider, backend_version=device.version()) self._device = device + self._device_id = device_id # Build Target from device self._target = self._build_target() @@ -222,7 +230,16 @@ def from_device_id( msg = f"QDMI session parameter(s) specified twice: {names}" raise TypeError(msg) overrides.update(session_overrides) - return cls(device=open_device(device_id, **overrides), provider=provider) + return cls( + device=open_device(device_id, **overrides), + provider=provider, + device_id=device_id, + ) + + @property + def device_id(self) -> str | None: + """Stable QDMI device ID, if known.""" + return self._device_id @property def target(self) -> Target: diff --git a/test/python/plugins/qiskit/test_backend.py b/test/python/plugins/qiskit/test_backend.py index 4ddd213cc7..5e7cd4d7c4 100644 --- a/test/python/plugins/qiskit/test_backend.py +++ b/test/python/plugins/qiskit/test_backend.py @@ -145,6 +145,19 @@ def test_backend_from_device_id_rejects_conflicting_device_configuration() -> No def test_backend_instantiation(ddsim_backend: QDMIBackend) -> None: """Backend exposes target qubit count.""" assert ddsim_backend.target.num_qubits > 0 + assert ddsim_backend.device_id == "mqt.ddsim.default" + + +def test_direct_backend_has_no_stable_device_id() -> None: + """Direct construction remains valid when no registry ID is available.""" + backend = QDMIBackend(open_device("mqt.ddsim.default")) + assert backend.device_id is None + + +def test_direct_backend_accepts_explicit_stable_device_id() -> None: + """Specialized construction can retain the ID of an already-open device.""" + backend = QDMIBackend(open_device("mqt.ddsim.default"), device_id="allocated.device") + assert backend.device_id == "allocated.device" def _single_qubit_circuit() -> QuantumCircuit: From 75729c0d9c059c839fa00cd1a605312196e2003e Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Fri, 14 Aug 2026 20:27:36 +0200 Subject: [PATCH 29/35] =?UTF-8?q?=F0=9F=94=8E=20Backport=20lazy=20QDMI=20b?= =?UTF-8?q?ackend=20discovery?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Read the live QDMI registry for each discovery call and open fresh backends only when requested. Add exact stable-ID lookup and sanitized warnings that skip unavailable devices without exposing configuration. Backports the provider discovery behavior from #2084. Assisted-by: GPT-5.6 via Codex --- python/mqt/core/plugins/qiskit/provider.py | 80 ++++++++---- test/python/plugins/qiskit/test_provider.py | 130 +++++++++++++++++++- 2 files changed, 186 insertions(+), 24 deletions(-) diff --git a/python/mqt/core/plugins/qiskit/provider.py b/python/mqt/core/plugins/qiskit/provider.py index e874bdfba9..050b5c1e62 100644 --- a/python/mqt/core/plugins/qiskit/provider.py +++ b/python/mqt/core/plugins/qiskit/provider.py @@ -14,8 +14,18 @@ from __future__ import annotations +import warnings +from typing import TYPE_CHECKING + from ...qdmi.driver import registered_device_ids from .backend import QDMIBackend +from .exceptions import UnsupportedDeviceError + +if TYPE_CHECKING: + from collections.abc import Iterator + + from ..._compat.typing import Unpack + from ...typing import QDMISessionParameters __all__ = ["QDMIProvider"] @@ -27,9 +37,8 @@ def __dir__() -> list[str]: class QDMIProvider: """Provider for registered QDMI devices. - This provider discovers and manages registered QDMI devices. It provides a - Qiskit-idiomatic interface for device - discovery and backend instantiation. + This provider discovers registered QDMI devices lazily and adapts + Qiskit-compatible devices as backends. Examples: List all available backends: @@ -47,15 +56,10 @@ class QDMIProvider: configuration or provider-specific environment. """ - def __init__(self) -> None: - """Initialize a provider for the registered QDMI devices.""" - self._backends: list[QDMIBackend] = [] - for device_id in registered_device_ids(): - try: - backend = QDMIBackend.from_device_id(device_id, provider=self) - except (IndexError, RuntimeError, ValueError): - continue - self._backends.append(backend) + @staticmethod + def device_ids() -> list[str]: + """Return the current registered QDMI device IDs without opening them.""" + return registered_device_ids() def backends(self, name: str | None = None) -> list[QDMIBackend]: """Return all available backends, optionally filtered by name substring. @@ -64,7 +68,7 @@ def backends(self, name: str | None = None) -> list[QDMIBackend]: name: If provided, return only backends whose name contains this substring. Returns: - List of QiskitBackend instances. Empty list if name specified but not found. + Compatible QDMI backends. The list is empty when no name matches. Examples: Get all backends: @@ -74,14 +78,46 @@ def backends(self, name: str | None = None) -> list[QDMIBackend]: Filter backends by name substring: - >>> na_backends = provider.backends(name="NA") # matches "MQT NA Default QDMI Device" - >>> qdmi_backends = provider.backends(name="QDMI") # matches all devices with "QDMI" in name + >>> ddsim_backends = provider.backends(name="DDSIM") + >>> qdmi_backends = provider.backends(name="QDMI") + """ + return list(self._iter_backends(name)) + + def _iter_backends(self, name: str | None = None) -> Iterator[QDMIBackend]: + """Open available registered backends one at a time. + + Args: + name: If provided, yield only backends whose name contains this substring. + + Yields: + Each backend whose registered device can be opened. """ - # Filter by name substring if specified - if name is not None: - return [b for b in self._backends if b.name is not None and name in b.name] + for device_id in self.device_ids(): + try: + backend = self.get_backend_by_device_id(device_id) + except UnsupportedDeviceError: + continue + except (IndexError, RuntimeError, ValueError): + warnings.warn( + f"Could not open registered QDMI device '{device_id}'.", + RuntimeWarning, + stacklevel=3, + ) + continue + if name is None or (backend.name is not None and name in backend.name): + yield backend + + def get_backend_by_device_id( + self, + device_id: str, + **session_parameters: Unpack[QDMISessionParameters], + ) -> QDMIBackend: + """Open one fresh backend for an exact stable QDMI device ID. - return self._backends + Returns: + A backend for a fresh device session. + """ + return QDMIBackend.from_device_id(device_id, provider=self, **session_parameters) def get_backend(self, name: str) -> QDMIBackend: """Get a single backend by name. @@ -90,7 +126,7 @@ def get_backend(self, name: str) -> QDMIBackend: name: Name of the backend to retrieve. Returns: - QiskitBackend instance. + The matching QDMI backend. Raises: ValueError: If no matching backend found. @@ -101,7 +137,7 @@ def get_backend(self, name: str) -> QDMIBackend: >>> provider = QDMIProvider() >>> backend = provider.get_backend("MQT Core DDSIM QDMI Device") """ - for backend in self._backends: + for backend in self._iter_backends(name): if backend.name == name: return backend @@ -110,4 +146,4 @@ def get_backend(self, name: str) -> QDMIBackend: def __repr__(self) -> str: """Return string representation of the provider.""" - return f"" + return f"" diff --git a/test/python/plugins/qiskit/test_provider.py b/test/python/plugins/qiskit/test_provider.py index aaae6a573a..1de03677a8 100644 --- a/test/python/plugins/qiskit/test_provider.py +++ b/test/python/plugins/qiskit/test_provider.py @@ -10,9 +10,13 @@ from __future__ import annotations +import warnings + import pytest -from mqt.core.plugins.qiskit import QDMIProvider +from mqt.core.plugins.qiskit import QDMIBackend, QDMIProvider +from mqt.core.plugins.qiskit.exceptions import UnsupportedDeviceError +from mqt.core.qdmi.driver import open_device def test_provider_backends_filter_by_name() -> None: @@ -65,6 +69,34 @@ def test_provider_get_backend_by_name() -> None: assert backend.provider is provider +def test_provider_get_backend_stops_after_exact_match(monkeypatch: pytest.MonkeyPatch) -> None: + """Exact-name lookup does not open devices after the matching backend.""" + expected = QDMIBackend(open_device("mqt.ddsim.default"), device_id="matching.device") + opened_ids: list[str] = [] + monkeypatch.setattr( + "mqt.core.plugins.qiskit.provider.registered_device_ids", + lambda: ["unavailable.device", "matching.device", "later.device"], + ) + + def lookup(device_id: str, **_session_parameters: object) -> QDMIBackend: + opened_ids.append(device_id) + if device_id == "unavailable.device": + msg = "credential=do-not-disclose" + raise RuntimeError(msg) + if device_id == "later.device": + pytest.fail("exact-name lookup continued after finding its backend") + return expected + + provider = QDMIProvider() + monkeypatch.setattr(provider, "get_backend_by_device_id", lookup) + + expected_name = expected.name + assert expected_name is not None + with pytest.warns(RuntimeWarning, match="unavailable.device"): + assert provider.get_backend(expected_name) is expected + assert opened_ids == ["unavailable.device", "matching.device"] + + def test_provider_get_backend_nonexistent() -> None: """Provider raises ValueError for non-existent backend.""" provider = QDMIProvider() @@ -86,7 +118,7 @@ def test_provider_repr() -> None: provider = QDMIProvider() repr_str = repr(provider) assert "QDMIProvider" in repr_str - assert "backends=" in repr_str + assert "devices=" in repr_str def test_backend_has_provider_reference() -> None: @@ -104,3 +136,97 @@ def test_provider_default_constructor() -> None: assert len(backends) > 0 backend = provider.get_backend("MQT Core DDSIM QDMI Device") assert backend.name == "MQT Core DDSIM QDMI Device" + + +def test_provider_construction_opens_no_devices(monkeypatch: pytest.MonkeyPatch) -> None: + """Constructing a provider does not initialize any registered device.""" + monkeypatch.setattr( + "mqt.core.plugins.qiskit.provider.QDMIBackend.from_device_id", + lambda *_args, **_kwargs: pytest.fail("provider construction opened a device"), + ) + QDMIProvider() + + +def test_provider_reads_registry_on_each_discovery_call(monkeypatch: pytest.MonkeyPatch) -> None: + """A provider reflects registrations made after its construction.""" + device_ids = ["first.device"] + monkeypatch.setattr("mqt.core.plugins.qiskit.provider.registered_device_ids", lambda: list(device_ids)) + provider = QDMIProvider() + + assert provider.device_ids() == ["first.device"] + device_ids.append("second.device") + assert provider.device_ids() == ["first.device", "second.device"] + + +def test_provider_exact_id_lookup_forwards_session_parameters(monkeypatch: pytest.MonkeyPatch) -> None: + """Exact-ID lookup opens only that device with typed overrides.""" + observed: tuple[str, QDMIProvider, dict[str, object]] | None = None + expected = QDMIBackend(open_device("mqt.ddsim.default"), device_id="test.device") + + def fake_from_device_id( + device_id: str, + *, + provider: QDMIProvider, + **session_parameters: object, + ) -> QDMIBackend: + nonlocal observed + observed = device_id, provider, session_parameters + return expected + + monkeypatch.setattr("mqt.core.plugins.qiskit.provider.QDMIBackend.from_device_id", fake_from_device_id) + provider = QDMIProvider() + token = str(123) + + backend = provider.get_backend_by_device_id("test.device", token=token, custom1="queue") + + assert backend is expected + assert observed == ("test.device", provider, {"token": token, "custom1": "queue"}) + + +def test_provider_warns_with_only_id_and_skips_unavailable_device(monkeypatch: pytest.MonkeyPatch) -> None: + """Enumeration reports an unavailable ID without leaking failure details.""" + available = QDMIBackend(open_device("mqt.ddsim.default"), device_id="available.device") + monkeypatch.setattr( + "mqt.core.plugins.qiskit.provider.registered_device_ids", + lambda: ["available.device", "unavailable.device"], + ) + + def lookup(device_id: str, **_session_parameters: object) -> QDMIBackend: + if device_id == "unavailable.device": + msg = "credential=do-not-disclose" + raise RuntimeError(msg) + return available + + provider = QDMIProvider() + monkeypatch.setattr(provider, "get_backend_by_device_id", lookup) + + with pytest.warns(RuntimeWarning) as warnings: + assert provider.backends() == [available] + + message = str(warnings[0].message) + assert warnings[0].filename == __file__ + assert "unavailable.device" in message + assert "credential" not in message + assert "do-not-disclose" not in message + + +def test_provider_silently_skips_incompatible_device(monkeypatch: pytest.MonkeyPatch) -> None: + """Enumeration silently omits devices that Qiskit cannot represent.""" + available = QDMIBackend(open_device("mqt.ddsim.default"), device_id="available.device") + monkeypatch.setattr( + "mqt.core.plugins.qiskit.provider.registered_device_ids", + lambda: ["incompatible.device", "available.device"], + ) + + def lookup(device_id: str, **_session_parameters: object) -> QDMIBackend: + if device_id == "incompatible.device": + msg = "device cannot be represented by a Qiskit target" + raise UnsupportedDeviceError(msg) + return available + + provider = QDMIProvider() + monkeypatch.setattr(provider, "get_backend_by_device_id", lookup) + + with warnings.catch_warnings(): + warnings.simplefilter("error") + assert provider.backends() == [available] From b30826d31e17cb2c50fc5eaa4b169eef6d41be2c Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Fri, 14 Aug 2026 20:28:47 +0200 Subject: [PATCH 30/35] =?UTF-8?q?=F0=9F=92=A5=20Backport=20explicit=20QDMI?= =?UTF-8?q?=20primitive=20defaults?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add sampler and estimator factories to each QDMI backend and match #2084 by replacing the generic primitive options mapping with explicit shot and precision defaults. Assisted-by: GPT-5.6 via Codex --- CHANGELOG.md | 3 +++ UPGRADING.md | 15 +++++++++++ docs/qdmi/qdmi_backend.md | 15 ++++++----- python/mqt/core/plugins/qiskit/backend.py | 27 ++++++++++++++++++++ python/mqt/core/plugins/qiskit/estimator.py | 10 ++++---- python/mqt/core/plugins/qiskit/sampler.py | 5 +--- test/python/plugins/qiskit/test_estimator.py | 21 +++++++++++---- test/python/plugins/qiskit/test_sampler.py | 16 ++++++++++-- 8 files changed, 90 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff94543f96..ed0cc4b82b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,8 @@ releases may include breaking changes. ### Changed +- 💥 Replace the MQT-specific QDMI primitive `options` mappings with explicit + shot and precision defaults ([#2084]) ([**@burgholzer**]) - ♻️ Simplify Python optional-dependency checks while preserving the Qiskit and PennyLane availability flags ([#2108]) ([**@simon1hofmann**]) - 💥 Move Python QDMI entities and the neutral-atom specialization to QDMI @@ -652,6 +654,7 @@ for previous changelogs._ +[#2084]: https://github.com/munich-quantum-toolkit/core/pull/2084 [#2108]: https://github.com/munich-quantum-toolkit/core/pull/2108 [#2046]: https://github.com/munich-quantum-toolkit/core/pull/2046 [#2074]: https://github.com/munich-quantum-toolkit/core/pull/2074 diff --git a/UPGRADING.md b/UPGRADING.md index c720db1ada..ddf58a081f 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -6,6 +6,21 @@ of changes including minor and patch releases, please refer to the ## [Unreleased] +### QDMI Qiskit primitive options + +`QDMISampler` and `QDMIEstimator` no longer accept the MQT-specific `options` +mapping. Pass shot and precision defaults directly, preferably through the +backend factories: + +```python +sampler = backend.sampler(default_shots=2048) +estimator = backend.estimator(default_precision=0.01, default_shots=2048) +``` + +Replace `QDMIEstimator(..., options={"default_shots": shots})` with +`QDMIEstimator(..., default_shots=shots)`. The sampler ignored its former +`options` mapping, so remove that argument without replacement. + ### Runtime-configurable SC QDMI device The built-in superconducting QDMI provider now parses its device description diff --git a/docs/qdmi/qdmi_backend.md b/docs/qdmi/qdmi_backend.md index cda5a673f2..99ed860a89 100644 --- a/docs/qdmi/qdmi_backend.md +++ b/docs/qdmi/qdmi_backend.md @@ -312,11 +312,10 @@ The {py:class}`~mqt.core.plugins.qiskit.QDMISampler` implements the measurement counts (bitstrings). ```{code-cell} ipython3 -from mqt.core.plugins.qiskit import QDMISampler from qiskit import QuantumCircuit -# Initialize sampler with the backend -sampler = QDMISampler(backend) +# Construct a sampler from the backend +sampler = backend.sampler(default_shots=1024) # Create a circuit qc = QuantumCircuit(2) @@ -342,13 +341,12 @@ The {py:class}`~mqt.core.plugins.qiskit.QDMIEstimator` implements the observables. ```{code-cell} ipython3 -from mqt.core.plugins.qiskit import QDMIEstimator from qiskit import QuantumCircuit from qiskit.quantum_info import SparsePauliOp import numpy as np -# Initialize estimator -estimator = QDMIEstimator(backend) +# Construct an estimator from the backend +estimator = backend.estimator(default_precision=0.0, default_shots=1024) # Create a circuit and observable qc = QuantumCircuit(2) @@ -370,6 +368,11 @@ print(f"Expectation value: {ev}") print(f"Standard deviation: {std}") ``` +Direct construction of `QDMISampler(backend, ...)` and +`QDMIEstimator(backend, ...)` remains available for applications that prefer it. +The direct primitive constructors use the same explicit defaults as the backend +factories. + You can also use parameterized circuits with the estimator: ```{code-cell} ipython3 diff --git a/python/mqt/core/plugins/qiskit/backend.py b/python/mqt/core/plugins/qiskit/backend.py index c4949f0ff9..f8c5335702 100644 --- a/python/mqt/core/plugins/qiskit/backend.py +++ b/python/mqt/core/plugins/qiskit/backend.py @@ -33,6 +33,7 @@ from ...qdmi import ProgramFormat from ...qdmi.driver import open_device from .converters import qiskit_to_iqm_json +from .estimator import QDMIEstimator from .exceptions import ( CircuitValidationError, JobSubmissionError, @@ -43,6 +44,7 @@ ) from .gates import MoveGate from .job import QDMIJob +from .sampler import QDMISampler if TYPE_CHECKING: from collections.abc import Iterable, Mapping, MutableSet, Sequence @@ -241,6 +243,31 @@ def device_id(self) -> str | None: """Stable QDMI device ID, if known.""" return self._device_id + def sampler(self, *, default_shots: int = 1024) -> QDMISampler: + """Construct a QDMI sampler for this backend. + + Returns: + A sampler that executes on this backend. + """ + return QDMISampler(self, default_shots=default_shots) + + def estimator( + self, + *, + default_precision: float = 0.0, + default_shots: int = 1024, + ) -> QDMIEstimator: + """Construct a QDMI estimator for this backend. + + Returns: + An estimator that executes on this backend. + """ + return QDMIEstimator( + self, + default_precision=default_precision, + default_shots=default_shots, + ) + @property def target(self) -> Target: """The Target describing the capabilities of the backend.""" diff --git a/python/mqt/core/plugins/qiskit/estimator.py b/python/mqt/core/plugins/qiskit/estimator.py index db3d8c0779..ad34bb0547 100644 --- a/python/mqt/core/plugins/qiskit/estimator.py +++ b/python/mqt/core/plugins/qiskit/estimator.py @@ -10,7 +10,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING import numpy as np from qiskit.circuit import QuantumCircuit @@ -49,18 +49,18 @@ def __init__( backend: QDMIBackend, *, default_precision: float = 0.0, - options: dict[str, Any] | None = None, + default_shots: int = 1024, ) -> None: """Initialize the QDMI Estimator. Args: backend: The QDMI backend to execute circuits on. default_precision: The default precision for expectation-value estimates. - options: Default options for the estimator. + default_shots: The number of shots used when no positive precision is set. """ self._backend = backend self._default_precision = default_precision - self._options = options or {} + self._default_shots = default_shots @property def backend(self) -> QDMIBackend: @@ -118,7 +118,7 @@ def _run_pub(self, pub: EstimatorPub) -> PubResult: precision = pub.precision # Calculate shots based on precision (if provided), otherwise use default - shots = self._options.get("default_shots", 1024) + shots = self._default_shots if precision is not None and precision > 0: shots = int(np.ceil(1.0 / precision**2)) diff --git a/python/mqt/core/plugins/qiskit/sampler.py b/python/mqt/core/plugins/qiskit/sampler.py index b2bf1642f4..4b93349f62 100644 --- a/python/mqt/core/plugins/qiskit/sampler.py +++ b/python/mqt/core/plugins/qiskit/sampler.py @@ -10,7 +10,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING import numpy as np from qiskit.primitives.base import BaseSamplerV2 @@ -50,18 +50,15 @@ def __init__( backend: QDMIBackend, *, default_shots: int = 1024, - options: dict[str, Any] | None = None, ) -> None: """Initialize the QDMI Sampler. Args: backend: The QDMI backend to execute circuits on. default_shots: The default number of shots to use if not specified in run. - options: Default options for the sampler. """ self._backend = backend self._default_shots = default_shots - self._options = options or {} @property def backend(self) -> QDMIBackend: diff --git a/test/python/plugins/qiskit/test_estimator.py b/test/python/plugins/qiskit/test_estimator.py index 064abcbc25..c8adc27035 100644 --- a/test/python/plugins/qiskit/test_estimator.py +++ b/test/python/plugins/qiskit/test_estimator.py @@ -88,10 +88,9 @@ def test_estimator_precision_handling(estimator: QDMIEstimator) -> None: assert result[0].metadata["shots"] == other_expected_shots -def test_estimator_options(estimator: QDMIEstimator) -> None: - """Test estimator options handling.""" - # Test default_shots option - estimator2 = QDMIEstimator(estimator.backend, options={"default_shots": 500}) +def test_estimator_defaults(estimator: QDMIEstimator) -> None: + """Test explicit estimator shot and precision defaults.""" + estimator2 = QDMIEstimator(estimator.backend, default_shots=500) qc = QuantumCircuit(1) op = SparsePauliOp("Z") @@ -99,7 +98,7 @@ def test_estimator_options(estimator: QDMIEstimator) -> None: result = job.result() assert result[0].metadata["shots"] == 500 - # Test default_precision option + # Test the default precision. estimator_prec = QDMIEstimator(estimator.backend, default_precision=0.1) job = estimator_prec.run([(qc, op)]) result = job.result() @@ -107,6 +106,18 @@ def test_estimator_options(estimator: QDMIEstimator) -> None: assert result[0].metadata["shots"] == 100 +def test_backend_constructs_estimator() -> None: + """A backend constructs an estimator that retains its identity and defaults.""" + backend = QDMIBackend.from_device_id("mqt.ddsim.default") + estimator = backend.estimator(default_shots=73) + qc = QuantumCircuit(1) + op = SparsePauliOp("Z") + + assert isinstance(estimator, QDMIEstimator) + assert estimator.backend is backend + assert estimator.run([(qc, op)]).result()[0].metadata["shots"] == 73 + + def test_estimator_observable_bases(estimator: QDMIEstimator) -> None: """Test estimating observables in different bases.""" qc = QuantumCircuit(2) diff --git a/test/python/plugins/qiskit/test_sampler.py b/test/python/plugins/qiskit/test_sampler.py index df58164944..a99389fd17 100644 --- a/test/python/plugins/qiskit/test_sampler.py +++ b/test/python/plugins/qiskit/test_sampler.py @@ -112,8 +112,8 @@ def test_sampler_run_multiple_cregs(sampler: QDMISampler) -> None: assert c1_bits.num_bits == 1 -def test_sampler_options(sampler: QDMISampler) -> None: - """Test sampler options mechanism.""" +def test_sampler_shot_defaults(sampler: QDMISampler) -> None: + """Test sampler shot defaults.""" # 1. Use default shots from init sampler2 = QDMISampler(sampler.backend, default_shots=500) qc = QuantumCircuit(1) @@ -129,6 +129,18 @@ def test_sampler_options(sampler: QDMISampler) -> None: assert result[0].metadata["shots"] == 200 +def test_backend_constructs_sampler() -> None: + """A backend constructs a sampler that retains its identity and defaults.""" + backend = QDMIBackend.from_device_id("mqt.ddsim.default") + sampler = backend.sampler(default_shots=37) + qc = QuantumCircuit(1) + qc.measure_all() + + assert isinstance(sampler, QDMISampler) + assert sampler.backend is backend + assert sampler.run([(qc,)]).result()[0].metadata["shots"] == 37 + + def test_sampler_no_circuits(sampler: QDMISampler) -> None: """Test run with empty pub list.""" job = sampler.run([]) From b31187cccccb34a634f7f9374a6a888793ed757b Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Fri, 14 Aug 2026 20:30:35 +0200 Subject: [PATCH 31/35] =?UTF-8?q?=F0=9F=93=9D=20Record=20the=20combined=20?= =?UTF-8?q?v3=20backport=20boundary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document the selected QDMI, Slurm, device, Qiskit, maintenance, build, and documentation changes. Record the explicit primitive-default migration and the main-only compiler, QCO, QIR, OpenQASM, and ZX boundaries. Assisted-by: GPT-5.6 via Codex --- .agent/plans/backport-qdmi-slurm-v3.md | 80 +++++++++++++++++++++----- CHANGELOG.md | 4 ++ 2 files changed, 71 insertions(+), 13 deletions(-) diff --git a/.agent/plans/backport-qdmi-slurm-v3.md b/.agent/plans/backport-qdmi-slurm-v3.md index f3853d9ff4..a14baba38a 100644 --- a/.agent/plans/backport-qdmi-slurm-v3.md +++ b/.agent/plans/backport-qdmi-slurm-v3.md @@ -21,7 +21,9 @@ runtime, select reusable IQM models, and select a registered device from a Slurm license environment. Administrators also get a two-node Slurm fixture and a concise tutorial that demonstrate cluster-wide admission without presenting license metadata as authorization. The documentation build emits `llms.txt` and -`llms-full.txt` through Sphinx-LLM. +`llms-full.txt` through Sphinx-LLM. Qiskit users can construct typed backends by +stable device ID, inspect the retained ID, discover devices lazily, and create +samplers and estimators from a backend. ## Progress @@ -36,6 +38,17 @@ license metadata as authorization. The documentation build emits `llms.txt` and - [x] (2026-08-14) Added the optional-component wheel target needed by the faster stub build and regenerated the recursive stubs without a tracked result. +- [x] (2026-08-14) Ported the applicable QDMI adapter changes from pull request + #2084, including its explicit primitive defaults and removal of the + generic `options` mapping. Retained only the compatible v3 + `session_parameters` mapping. +- [x] (2026-08-14) Ported the removal of dead pybind11 CMake support from pull + request #2106 and the removal of `[tool.qdmi]` configuration from pull + request #2116. +- [x] (2026-08-14) Rescanned current `main` and added the compatible + documentation-build improvements from #1988 and repository guidance from + #2072. Rejected QIR, QCO, compiler-target, and OpenQASM changes whose + source layers do not exist on v3. - [x] (2026-08-14) Built and tested the complete non-MLIR native and Python surfaces and built the documentation with Sphinx-LLM. - [x] (2026-08-14) Completed the final lint pass and adversarial aggregate @@ -65,6 +78,19 @@ license metadata as authorization. The documentation build emits `llms.txt` and - Observation: Sphinx-LLM entered `main` as part of pull request #1989, while pull request #2046 only added the minimum version. Both pieces are required for a functional backport. +- Observation: Pull request #2084 combines adapter conveniences with deliberate + API simplification. MQT Core 3 has no Python MLIR compiler-target binding, so + that factory cannot be ported. The Qiskit primitive factories and direct + constructors can use the same explicit defaults as `main`, and the generic + `options` mapping can be removed with a focused upgrade note. +- Observation: Pull requests #2106 and #2116 are source-breaking but suitable + for the planned minor release. The former removes only dead pybind11 build + helpers. The latter removes the deprecated `[tool.qdmi]` path while retaining + persistent `qdmi.json` configuration. +- Observation: The QIR result-order fix (#1979), QIR-Runner test layer (#2044), + QCO drive-by layout work (#2060), and later compiler/OpenQASM changes depend + on source and runtime layers that are absent from v3. Partial ports would not + preserve their upstream contracts. ## Decision Log @@ -79,8 +105,19 @@ license metadata as authorization. The documentation build emits `llms.txt` and typing-code removal. Date/Author: 2026-08-14 / Codex. - Decision: Exclude the compiler-target, QCO/QTensor, newer QIR-runtime, and OpenQASM semantic series. Their source paths do not exist on v3 or depend on - the LLVM/MLIR 22 architecture. Also exclude the explicitly breaking ZX and - CMake-helper removals. Date/Author: 2026-08-14 / Codex. + the LLVM/MLIR 22 architecture. Also exclude the explicitly breaking ZX + removal. Date/Author: 2026-08-14 / Codex. +- Decision: Backport the Qiskit parts of #2084 that have a v3 implementation. + Keep the compatible `session_parameters` mapping, remove the generic primitive + `options` mapping in favor of explicit defaults, and omit + `CompilerTarget.from_device_id` because v3 does not expose the Python MLIR + compiler-target binding. Date/Author: 2026-08-14 / Codex. +- Decision: Include #2106 and #2116 in this minor release. Their removals are + intentional cleanup with clear migration paths, not architectural v4-only + changes. Date/Author: 2026-08-14 / Codex. +- Decision: Include #1988 and #2072 after the final rescan. Adapt #1988 to the + optional v3 MLIR build instead of copying the main-only documentation CMake + flag. Date/Author: 2026-08-14 / Codex. - Decision: Treat lock-only maintenance as superseded by the final lock generated from this aggregate dependency set. Rationale: replaying an older lock snapshot would replace newer, intentionally resolved metadata. @@ -92,10 +129,12 @@ license metadata as authorization. The documentation build emits `llms.txt` and ## Outcomes & Retrospective -The aggregate non-MLIR release build succeeds with LLVM 21.1.8 `llc`. All 1,526 +The aggregate non-MLIR release build succeeds with LLVM 21.1.8 `llc`. All 1,742 CTest cases pass; two provider-specification job-ID cases are intentionally -skipped. The complete Python suite passes 531 tests with five documented skips. -Recursive stub generation completes without a tracked stub change. +skipped. The updated complete Python suite passes 550 tests with five documented +skips. The focused Python 3.10 QDMI and Qiskit suite passes 420 tests with two +documented MLIR skips. Recursive stub generation completes without a tracked +stub change. The Sphinx build succeeds with the existing v3 warning baseline and produces non-empty `llms.txt` and `llms-full.txt` outputs. The privileged Slurm fixture @@ -157,6 +196,19 @@ LLVM/MLIR 21 contract, and omit main-only architecture work. Resolve the combined changelog semantically and do not carry obsolete intermediate plans or superseded APIs. +Port the applicable Qiskit adapter work from pull request #2084. Add public +typed session and job parameter dictionaries, direct typed session keywords, +stable backend IDs, lazy provider discovery, exact-ID lookup, and backend +primitive factories. Keep the compatible v3 session mapping. Match `main` by +using explicit primitive defaults and removing the generic primitive `options` +mapping. Omit the unavailable Python MLIR compiler-target factory. + +Port #2106 and #2116 as separate provenance-preserving commits. Remove the +unused pybind11 CMake helper and use one nanobind binding helper. Remove +`[tool.qdmi]` parsing and the vendored TOML parser while retaining `qdmi.json`. +Apply the compatible documentation-build changes from #1988 and agent guidance +from #2072. Record the main-only exclusions from the final rescan. + ## Concrete Steps Run cache-producing commands through the worktree-local wrapper. @@ -226,15 +278,15 @@ and rerun affected tests. Never modify another task worktree. ## Artifacts and Notes The combined series backports functionality from pull requests `#1974`, `#1980`, -`#1989`, `#1992`, `#2008`, `#2010`, `#2011`, `#2019`, `#2020`, `#2021`, `#2023`, -`#2025`, `#2027`, `#2029`, `#2042`, `#2043`, `#2046`, `#2047`, `#2059`, `#2065`, -`#2073`, `#2074`, `#2075`, `#2083`, and `#2108`. Pull request `#2113` is the -authoritative v3 prefix. +`#1988`, `#1989`, `#1992`, `#2008`, `#2010`, `#2011`, `#2019`, `#2020`, `#2021`, +`#2023`, `#2025`, `#2027`, `#2029`, `#2042`, `#2043`, `#2046`, `#2047`, `#2059`, +`#2065`, `#2072`, `#2073`, `#2074`, `#2075`, `#2083`, `#2106`, `#2108`, and +`#2116`. Pull request `#2113` is the authoritative v3 prefix. The series also +backports the compatible Qiskit subset of pull request `#2084`. The lock-only pull request #2033 is represented by the final `uv lock` result. -Compiler and QIR changes are excluded because the corresponding main source tree -is not present on v3. Pull requests #2082 and #2106 are explicitly breaking and -remain main-only. +Compiler, QCO, and QIR changes are excluded where the corresponding main source +or runtime layer is not present on v3. Pull request #2082 remains v4-only. ### Interfaces and Dependencies @@ -248,6 +300,8 @@ The public additions are: - `fomac::slurm::openDeviceFromLicense()` and `mqt.core.qdmi.slurm.open_device_from_license()`; - runtime-configurable NA and SC providers and stable IQM model registrations. +- typed QDMI adapter parameters, stable Qiskit backend IDs, lazy provider + discovery, and Qiskit sampler and estimator factories. The bundled QDMI revision exposes the matching queue and retrieval enums and functions. The Slurm fixture requires Slurm 25.11 or newer, Ubuntu 26.04, diff --git a/CHANGELOG.md b/CHANGELOG.md index ed0cc4b82b..19fd34edc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ releases may include breaking changes. ### Added +- 🚸 Add typed stable-ID construction for Qiskit backends, lazy provider + discovery, and sampler and estimator factories with explicit shot and + precision defaults ([#2084]) ([**@burgholzer**]) - 📝 Generate `llms.txt` documentation indexes with Sphinx-LLM ([#1989], [#2046]) ([**@denialhaag**], [**@burgholzer**]) - 🧪 Test static Slurm license admission and QDMI execution with DDSIM and SC, @@ -656,6 +659,7 @@ for previous changelogs._ [#2084]: https://github.com/munich-quantum-toolkit/core/pull/2084 [#2108]: https://github.com/munich-quantum-toolkit/core/pull/2108 +[#2084]: https://github.com/munich-quantum-toolkit/core/pull/2084 [#2046]: https://github.com/munich-quantum-toolkit/core/pull/2046 [#2074]: https://github.com/munich-quantum-toolkit/core/pull/2074 [#2043]: https://github.com/munich-quantum-toolkit/core/pull/2043 From 805b6f1b04a08eece48a1f283d6f0a8ee7cbeaee Mon Sep 17 00:00:00 2001 From: Daniel Haag <121057143+denialhaag@users.noreply.github.com> Date: Fri, 14 Aug 2026 22:37:14 +0200 Subject: [PATCH 32/35] =?UTF-8?q?=F0=9F=92=A5=20Backport=20removal=20of=20?= =?UTF-8?q?dead=20pybind11=20CMake=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the unused pybind11 binding helper and make add_mqt_python_binding the sole nanobind helper. Update all v3 binding targets and document the downstream CMake migration. Backports #2106. Assisted-by: GPT-5.6 via Codex --- CHANGELOG.md | 5 +++- UPGRADING.md | 22 +++++++++++++++ bindings/dd/CMakeLists.txt | 2 +- bindings/ir/CMakeLists.txt | 2 +- bindings/na/CMakeLists.txt | 2 +- bindings/qdmi/CMakeLists.txt | 2 +- cmake/AddMQTPythonBinding.cmake | 48 --------------------------------- 7 files changed, 30 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19fd34edc8..f470ee54a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,9 @@ releases may include breaking changes. ### Changed +- 💥 Remove the unused `pybind11` CMake helper and rename + `add_mqt_python_binding_nanobind` to `add_mqt_python_binding` ([#2106]) + ([**@denialhaag**]) - 💥 Replace the MQT-specific QDMI primitive `options` mappings with explicit shot and precision defaults ([#2084]) ([**@burgholzer**]) - ♻️ Simplify Python optional-dependency checks while preserving the Qiskit and @@ -657,7 +660,7 @@ for previous changelogs._ -[#2084]: https://github.com/munich-quantum-toolkit/core/pull/2084 +[#2106]: https://github.com/munich-quantum-toolkit/core/pull/2106 [#2108]: https://github.com/munich-quantum-toolkit/core/pull/2108 [#2084]: https://github.com/munich-quantum-toolkit/core/pull/2084 [#2046]: https://github.com/munich-quantum-toolkit/core/pull/2046 diff --git a/UPGRADING.md b/UPGRADING.md index ddf58a081f..078d8f607c 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -6,6 +6,28 @@ of changes including minor and patch releases, please refer to the ## [Unreleased] +### Python binding CMake helper + +The `add_mqt_python_binding_nanobind` function is now called +`add_mqt_python_binding`. Rename the calls in downstream `CMakeLists.txt` files: + +```cmake +add_mqt_python_binding( + MYPACKAGE + py_mypackage + ${SOURCES} + MODULE_NAME + _core + INSTALL_DIR + . + LINK_LIBS + MQT::Core) +``` + +The former `add_mqt_python_binding` function built modules with `pybind11`. All +MQT projects have switched from `pybind11` to `nanobind`, so no build used that +function. MQT Core no longer provides it. + ### QDMI Qiskit primitive options `QDMISampler` and `QDMIEstimator` no longer accept the MQT-specific `options` diff --git a/bindings/dd/CMakeLists.txt b/bindings/dd/CMakeLists.txt index 005d184f5b..c0a07b634b 100644 --- a/bindings/dd/CMakeLists.txt +++ b/bindings/dd/CMakeLists.txt @@ -11,7 +11,7 @@ if(NOT TARGET ${MQT_CORE_TARGET_NAME}-dd-bindings) file(GLOB_RECURSE DD_SOURCES **.cpp) # declare the Python module - add_mqt_python_binding_nanobind( + add_mqt_python_binding( CORE ${MQT_CORE_TARGET_NAME}-dd-bindings ${DD_SOURCES} diff --git a/bindings/ir/CMakeLists.txt b/bindings/ir/CMakeLists.txt index 8a8459be05..695639ef2b 100644 --- a/bindings/ir/CMakeLists.txt +++ b/bindings/ir/CMakeLists.txt @@ -11,7 +11,7 @@ if(NOT TARGET ${MQT_CORE_TARGET_NAME}-ir-bindings) file(GLOB_RECURSE IR_SOURCES **.cpp) # declare the Python module - add_mqt_python_binding_nanobind( + add_mqt_python_binding( CORE ${MQT_CORE_TARGET_NAME}-ir-bindings ${IR_SOURCES} diff --git a/bindings/na/CMakeLists.txt b/bindings/na/CMakeLists.txt index 688e7a7ab9..5f56d6d2fd 100644 --- a/bindings/na/CMakeLists.txt +++ b/bindings/na/CMakeLists.txt @@ -13,7 +13,7 @@ if(NOT TARGET ${TARGET_NAME}) file(GLOB_RECURSE SOURCES *.cpp) # declare the Python module - add_mqt_python_binding_nanobind( + add_mqt_python_binding( CORE ${TARGET_NAME} ${SOURCES} diff --git a/bindings/qdmi/CMakeLists.txt b/bindings/qdmi/CMakeLists.txt index 426c4d4093..8bbcc9ed4f 100644 --- a/bindings/qdmi/CMakeLists.txt +++ b/bindings/qdmi/CMakeLists.txt @@ -13,7 +13,7 @@ if(NOT TARGET ${TARGET_NAME}) file(GLOB_RECURSE SOURCES **.cpp) # declare the Python module - add_mqt_python_binding_nanobind( + add_mqt_python_binding( CORE ${TARGET_NAME} ${SOURCES} diff --git a/cmake/AddMQTPythonBinding.cmake b/cmake/AddMQTPythonBinding.cmake index 8acd6a152c..a272ae03ee 100644 --- a/cmake/AddMQTPythonBinding.cmake +++ b/cmake/AddMQTPythonBinding.cmake @@ -7,54 +7,6 @@ # Licensed under the MIT License function(add_mqt_python_binding package_name target_name) - # parse the arguments - cmake_parse_arguments(ARG "" "MODULE_NAME;INSTALL_DIR" "LINK_LIBS" ${ARGN}) - set(SOURCES ${ARG_UNPARSED_ARGUMENTS}) - - # declare the Python module - pybind11_add_module( - # name of the extension - ${target_name} - # prefer thin LTO if available - THIN_LTO - # optimize the bindings for size - OPT_SIZE - # source code goes here - ${SOURCES}) - - # set default "." for INSTALL_DIR - if(NOT ARG_INSTALL_DIR) - set(ARG_INSTALL_DIR ".") - endif() - - if(ARG_MODULE_NAME) - # the library name must be the same as the module name - set_target_properties(${target_name} PROPERTIES OUTPUT_NAME ${ARG_MODULE_NAME}) - target_compile_definitions(${target_name} - PRIVATE MQT_${package_name}_MODULE_NAME=${ARG_MODULE_NAME}) - else() - # use the target name as the module name - target_compile_definitions(${target_name} - PRIVATE MQT_${package_name}_MODULE_NAME=${target_name}) - endif() - - # add project libraries to the link libraries - list(APPEND ARG_LINK_LIBS MQT::ProjectOptions MQT::ProjectWarnings) - - # Set c++ standard - target_compile_features(${target_name} PRIVATE cxx_std_20) - - # link the required libraries - target_link_libraries(${target_name} PRIVATE ${ARG_LINK_LIBS}) - - # install directive for scikit-build-core - install( - TARGETS ${target_name} - DESTINATION ${ARG_INSTALL_DIR} - COMPONENT ${MQT_${package_name}_TARGET_NAME}_Python) -endfunction() - -function(add_mqt_python_binding_nanobind package_name target_name) cmake_parse_arguments(ARG "" "MODULE_NAME;INSTALL_DIR" "LINK_LIBS" ${ARGN}) set(SOURCES ${ARG_UNPARSED_ARGUMENTS}) From 93a5b518cff339643f8d24952f379a20cdfed76c Mon Sep 17 00:00:00 2001 From: Daniel Haag <121057143+denialhaag@users.noreply.github.com> Date: Fri, 14 Aug 2026 22:38:19 +0200 Subject: [PATCH 33/35] =?UTF-8?q?=F0=9F=92=A5=20Backport=20removal=20of=20?= =?UTF-8?q?pyproject=20QDMI=20configuration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stop discovering QDMI device definitions from [tool.qdmi], remove the vendored toml++ dependency, and retain qdmi.json as the project-level configuration format. Backports #2116 with v3-compatible tests and documentation. Assisted-by: GPT-5.6 via Codex --- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 6 + UPGRADING.md | 37 + docs/qdmi/configuration.md | 14 +- src/qdmi/driver/CMakeLists.txt | 2 - src/qdmi/driver/DeviceRegistry.cpp | 34 +- test/qdmi/registry/test_device_registry.cpp | 47 +- vendor/tomlplusplus/README.md | 10 - vendor/tomlplusplus/toml.hpp | 17899 ------------------ 9 files changed, 58 insertions(+), 17993 deletions(-) delete mode 100644 vendor/tomlplusplus/README.md delete mode 100644 vendor/tomlplusplus/toml.hpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa8c94fe05..e1c3e13c95 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -106,7 +106,7 @@ jobs: setup-python: true install-pkgs: "nanobind==2.14.0" cpp-linter-extra-args: "-std=c++20" - # The vendored toml++ header is checked upstream and is not maintained + # Vendored third-party headers are checked upstream and are not maintained # according to MQT Core's clang-tidy configuration. cpp-linter-ignore-extra: "vendor/**" setup-mlir: true diff --git a/CHANGELOG.md b/CHANGELOG.md index f470ee54a2..96eb2ba089 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,11 @@ releases may include breaking changes. - 🚀 Reduce ZX diagram growth for multi-controlled X gates with an exact ancilla-free quadratic decomposition ([#1984]) ([**@burgholzer**]) +### Removed + +- 💥 Remove QDMI device configuration through `[tool.qdmi]` in `pyproject.toml` + and the vendored toml++ header ([#2116]) ([**@denialhaag**]) + ## [3.8.0] - 2026-07-30 _If you are upgrading: please see [`UPGRADING.md`](UPGRADING.md#380)._ @@ -660,6 +665,7 @@ for previous changelogs._ +[#2116]: https://github.com/munich-quantum-toolkit/core/pull/2116 [#2106]: https://github.com/munich-quantum-toolkit/core/pull/2106 [#2108]: https://github.com/munich-quantum-toolkit/core/pull/2108 [#2084]: https://github.com/munich-quantum-toolkit/core/pull/2084 diff --git a/UPGRADING.md b/UPGRADING.md index 078d8f607c..52fabb57c7 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -6,6 +6,43 @@ of changes including minor and patch releases, please refer to the ## [Unreleased] +### Removal of QDMI configuration through `pyproject.toml` + +MQT Core no longer reads QDMI device definitions from a `[tool.qdmi]` table in +`pyproject.toml`. Project discovery now looks only for `qdmi.json`. Move an +existing table into a `qdmi.json` file beside the `pyproject.toml`. For example, +replace this `pyproject.toml` table: + +```toml +[tool.qdmi] +devices = [ + { id = "example.device", library = "libexample-device.so", prefix = "EXAMPLE" }, +] +``` + +with this `qdmi.json`: + +```json +{ + "schema-version": 1, + "qdmi": { + "devices": [ + { + "id": "example.device", + "library": "libexample-device.so", + "prefix": "EXAMPLE" + } + ] + } +} +``` + +The JSON document adds the `"schema-version": 1` key and nests the device array +under `qdmi`. Every other key keeps its name and meaning. Relative paths still +resolve against the file that declares them. `MQT_CORE_QDMI_CONFIG_FILE`, +`MQT_CORE_QDMI_CONFIG_JSON`, the system and user files, and the packaged +`*.qdmi.json` fragments do not change. + ### Python binding CMake helper The `add_mqt_python_binding_nanobind` function is now called diff --git a/docs/qdmi/configuration.md b/docs/qdmi/configuration.md index 961fc44080..772f6770e9 100644 --- a/docs/qdmi/configuration.md +++ b/docs/qdmi/configuration.md @@ -1,9 +1,9 @@ # QDMI device configuration -MQT Core discovers QDMI device definitions from versioned JSON or TOML -configuration. Discovery only parses definitions. When the QDMI driver -initializes a client session, it opens the configured native libraries. The -stable-ID API opens only the requested device. +MQT Core discovers QDMI device definitions from versioned JSON configuration. +Discovery only parses definitions. When the QDMI driver initializes a client +session, it opens the configured native libraries. The stable-ID API opens only +the requested device. :::{warning} QDMI configuration is a native-code loading trust boundary. Use configuration @@ -84,8 +84,7 @@ Definitions are merged field by field by ID, from lowest to highest precedence: 1. generated `*.qdmi.json` fragments packaged beside the MQT Core Driver; 2. the system `qdmi.json`; 3. the user or XDG `qdmi.json`; -4. the nearest project `qdmi.json`, or `[tool.qdmi]` in `pyproject.toml` when no - dedicated file exists in that directory; +4. the nearest project `qdmi.json`; 5. `MQT_CORE_QDMI_CONFIG_JSON`. On Unix, file configuration uses `/etc/mqt-core/qdmi.json` and then @@ -95,8 +94,7 @@ On Unix, file configuration uses `/etc/mqt-core/qdmi.json` and then An entry containing only its ID and `"enabled": false` masks an inherited definition. Since definitions are merged field by field, a later definition with -the same ID must explicitly set `"enabled": true` to enable it again. Within one -directory, `qdmi.json` takes precedence over `pyproject.toml`. The final +the same ID must explicitly set `"enabled": true` to enable it again. The final disabled ID remains reserved, so fallback registration cannot silently re-enable a device that an administrator disabled. diff --git a/src/qdmi/driver/CMakeLists.txt b/src/qdmi/driver/CMakeLists.txt index a83cddd967..3b3cdbe1c4 100644 --- a/src/qdmi/driver/CMakeLists.txt +++ b/src/qdmi/driver/CMakeLists.txt @@ -25,8 +25,6 @@ if(NOT TARGET ${TARGET_NAME}) PUBLIC qdmi::qdmi MQT::CoreQDMICommon PRIVATE qdmi::qdmi_project_warnings nlohmann_json::nlohmann_json spdlog::spdlog ${CMAKE_DL_LIBS}) - target_include_directories(${TARGET_NAME} SYSTEM - PRIVATE ${PROJECT_SOURCE_DIR}/vendor/tomlplusplus) mqt_get_qdmi_device_targets(QDMI_DEVICE_TARGETS) diff --git a/src/qdmi/driver/DeviceRegistry.cpp b/src/qdmi/driver/DeviceRegistry.cpp index 4211bd5cdb..8d23c2e8cf 100644 --- a/src/qdmi/driver/DeviceRegistry.cpp +++ b/src/qdmi/driver/DeviceRegistry.cpp @@ -13,7 +13,6 @@ #include "qdmi/driver/Driver.hpp" #include // NOLINT(misc-include-cleaner) -#include #include #include @@ -23,7 +22,6 @@ #include #include #include -#include #include #include #include @@ -267,23 +265,6 @@ parseDevicePatch(const Json& value, const std::filesystem::path& source, } } -[[nodiscard]] auto readPyproject(const std::filesystem::path& path) - -> std::optional { - try { - const auto table = toml::parse_file(path.string()); - const auto* qdmiTable = table["tool"]["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) { @@ -407,12 +388,6 @@ void appendFragments(std::vector& files, if (std::filesystem::is_regular_file(dedicated)) { return dedicated; } - 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; @@ -521,14 +496,7 @@ DeviceRegistry::DeviceRegistry() { }; for (const auto& file : discoverFiles()) { - 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())); - } + mergePatches(parseConfiguration(readJson(file), file, file.parent_path())); } const auto inlineBase = std::filesystem::current_path(); if (auto inlineJson = environment("MQT_CORE_QDMI_CONFIG_JSON")) { diff --git a/test/qdmi/registry/test_device_registry.cpp b/test/qdmi/registry/test_device_registry.cpp index 7f85f65d62..83b5958df4 100644 --- a/test/qdmi/registry/test_device_registry.cpp +++ b/test/qdmi/registry/test_device_registry.cpp @@ -376,40 +376,12 @@ TEST(DeviceRegistry, DiscoversGeneratedBuildTreeManifests) { } #endif -TEST(DeviceRegistry, ReadsProjectConfigurationFromPyprojectToml) { +TEST(DeviceRegistry, ReadsProjectConfigurationFromNearestQdmiJson) { const TemporaryDirectory directory; - static_cast(directory.write("pyproject.toml", R"( - [tool.qdmi] - devices = [{ id = "toml", library = "device.so", prefix = "TOML" }] - )")); - const ScopedCurrentPath currentPath(directory.path()); - const ScopedEnvironmentVariable configFile("MQT_CORE_QDMI_CONFIG_FILE", ""); - const ScopedEnvironmentVariable configJson("MQT_CORE_QDMI_CONFIG_JSON", ""); -#ifdef _WIN32 - const ScopedEnvironmentVariable userConfig("APPDATA", - directory.path().string()); -#else - const ScopedEnvironmentVariable userConfig("XDG_CONFIG_HOME", - directory.path().string()); -#endif - - const qdmi::detail::DeviceRegistry registry; - const auto* definition = findDefinition(registry, "toml"); - ASSERT_NE(definition, nullptr); - EXPECT_EQ(std::filesystem::weakly_canonical(definition->library), - std::filesystem::weakly_canonical(directory.path()) / "device.so"); -} - -TEST(DeviceRegistry, DedicatedProjectFileWinsOverPyproject) { - const TemporaryDirectory directory; - static_cast(directory.write("pyproject.toml", R"( - [tool.qdmi] - devices = [{ id = "toml", library = "toml.so", prefix = "TOML" }] - )")); static_cast(directory.write("qdmi.json", R"({ "schema-version": 1, "qdmi": {"devices": [ - {"id": "json", "library": "json.so", "prefix": "JSON"} + {"id": "json", "library": "device.so", "prefix": "JSON"} ]} })")); const ScopedCurrentPath currentPath(directory.path()); @@ -417,8 +389,10 @@ TEST(DeviceRegistry, DedicatedProjectFileWinsOverPyproject) { const ScopedEnvironmentVariable configJson("MQT_CORE_QDMI_CONFIG_JSON", ""); const qdmi::detail::DeviceRegistry registry; - EXPECT_NE(findDefinition(registry, "json"), nullptr); - EXPECT_EQ(findDefinition(registry, "toml"), nullptr); + const auto* definition = findDefinition(registry, "json"); + ASSERT_NE(definition, nullptr); + EXPECT_EQ(std::filesystem::weakly_canonical(definition->library), + std::filesystem::weakly_canonical(directory.path()) / "device.so"); } TEST(DeviceRegistry, MergesProjectConfigurationOverUserConfiguration) { @@ -477,7 +451,7 @@ TEST(DeviceRegistry, ReportsInvalidDocumentsAndDefinitionTypes) { } } -TEST(DeviceRegistry, ReportsInvalidExplicitJsonAndToml) { +TEST(DeviceRegistry, ReportsInvalidExplicitJson) { const TemporaryDirectory directory; { const ScopedEnvironmentVariable configFile( @@ -493,13 +467,6 @@ TEST(DeviceRegistry, ReportsInvalidExplicitJsonAndToml) { EXPECT_THROW(static_cast(qdmi::detail::DeviceRegistry()), std::invalid_argument); } - { - static_cast(directory.write("pyproject.toml", "[tool.qdmi\n")); - const ScopedCurrentPath currentPath(directory.path()); - const ScopedEnvironmentVariable configFile("MQT_CORE_QDMI_CONFIG_FILE", ""); - EXPECT_THROW(static_cast(qdmi::detail::DeviceRegistry()), - std::invalid_argument); - } } } // namespace diff --git a/vendor/tomlplusplus/README.md b/vendor/tomlplusplus/README.md deleted file mode 100644 index 6e5384047b..0000000000 --- a/vendor/tomlplusplus/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# 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 deleted file mode 100644 index caf87c4c21..0000000000 --- a/vendor/tomlplusplus/toml.hpp +++ /dev/null @@ -1,17899 +0,0 @@ -//---------------------------------------------------------------------------------------------------------------------- -// -// 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