diff --git a/.agent/plans/backport-qdmi-slurm-v3.md b/.agent/plans/backport-qdmi-slurm-v3.md new file mode 100644 index 0000000000..a14baba38a --- /dev/null +++ b/.agent/plans/backport-qdmi-slurm-v3.md @@ -0,0 +1,313 @@ +# 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 +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 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. 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 + +- [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) 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 + 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, assignment, labels, and replacement CI. + +## Surprises & Discoveries + +- 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. +- 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 + +- 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: 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 + 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. + 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 + +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 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 +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. + +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 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. +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 + +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. + +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. + +Regenerate recursive stubs after all binding changes: + + ./.agent/run.sh uvx nox -s stubs + +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 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 -q + +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 runs in GitHub Actions. + +## Validation and Acceptance + +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 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 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 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 affected tests. Never modify another task worktree. + +## Artifacts and Notes + +The combined series backports functionality from pull requests `#1974`, `#1980`, +`#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, 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 + +The public additions are: + +- 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()`; +- 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, +systemd, Munge, cgroup v2, and Docker Compose. Runtime device authorization is +provider-specific and remains outside the Slurm selector. + +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/cd.yml b/.github/workflows/cd.yml index d03cbbbd39..849612af1a 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 }} @@ -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/ci.yml b/.github/workflows/ci.yml index 085ac33f84..e1c3e13c95 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,22 +14,22 @@ 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 - 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 - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-tests-ubuntu.yml@973217bac3ad300d9982fffdd3b37a9b5b3a51ea # v2.2.1 + - 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 }} compiler: ${{ matrix.compiler }} @@ -40,18 +40,18 @@ 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 - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-tests-macos.yml@973217bac3ad300d9982fffdd3b37a9b5b3a51ea # v2.2.1 + - 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 }} compiler: ${{ matrix.compiler }} @@ -62,76 +62,18 @@ 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@973217bac3ad300d9982fffdd3b37a9b5b3a51ea # v2.2.1 - 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@973217bac3ad300d9982fffdd3b37a9b5b3a51ea # v2.2.1 - 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@973217bac3ad300d9982fffdd3b37a9b5b3a51ea # v2.2.1 - 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] - uses: munich-quantum-toolkit/workflows/.github/workflows/reusable-cpp-tests-windows.yml@973217bac3ad300d9982fffdd3b37a9b5b3a51ea # v2.2.1 + - 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 }} compiler: ${{ matrix.compiler }} @@ -144,7 +86,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,15 +98,15 @@ 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 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 + # 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 @@ -185,7 +127,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,29 +135,16 @@ 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 - # 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@973217bac3ad300d9982fffdd3b37a9b5b3a51ea # v2.2.1 - with: - runs-on: ${{ matrix.runs-on }} - python-linter: 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 +154,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 +172,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 }} @@ -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/.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/.github/workflows/slurm.yml b/.github/workflows/slurm.yml new file mode 100644 index 0000000000..3256dae539 --- /dev/null +++ b/.github/workflows/slurm.yml @@ -0,0 +1,69 @@ +name: Slurm integration + +on: + push: + branches: + - v3.x + 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/.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/.pre-commit-config.yaml b/.pre-commit-config.yaml index bf4f87d766..52d2a1f424 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 @@ -112,9 +112,9 @@ 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.37 + rev: v0.2.49 hooks: - id: rumdl args: [--fix] @@ -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] diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 87fac4ca7e..5a856399bf 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -12,7 +12,6 @@ build: python: "3.14" apt_packages: - graphviz - - inkscape - zstd jobs: post_checkout: diff --git a/AGENTS.md b/AGENTS.md index e79cb0e64d..444f6aa1ee 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,6 +28,41 @@ MQT Core. The project-wide policy for AI-assisted contributions is discard or overwrite changes that are outside the task. - Follow the patterns in neighboring files and prefer the smallest change that fully solves the problem. +- Write code comments, documentation, tests, changelog entries, and public text + for the final design. Do not preserve prompts, review chronology, former + names, or abandoned approaches unless they remain necessary user-facing + context. +- Apply + [Orwell's six rules for writing](https://www.orwellfoundation.com/the-orwell-foundation/orwell/essays-and-other-works/politics-and-the-english-language/) + to every category of prose, including reasoning, descriptions, commit + messages, documentation, docstrings, comments, test text, diagnostics, and + handoffs: + + 1. Do not use a familiar metaphor, simile, or other figure of speech. + 2. Use a short word when it has the same meaning as a long word. + 3. Remove every word that does not add meaning. + 4. Use active voice when possible. + 5. Use everyday English instead of a foreign phrase, scientific word, or + jargon term when this does not reduce precision. + 6. Break a rule before it makes the text unclear, incorrect, or needlessly + difficult to read. + + Also apply the relevant principles of + [ASD-STE100 Simplified Technical English](https://www.asd-ste100.org/): use + short, direct sentences; give each sentence one main idea; use one term for + one meaning; and use explicit nouns instead of vague pronouns. Treat these as + mandatory style rules, not as a claim of formal ASD-STE100 compliance. +- Base terminology and phrasing on repository usage and established precedents + in the quantum computing, LLVM/MLIR and compiler, high-performance computing, + and general computer science communities. Use the established term that most + precisely matches the concept. If communities use different terms, explain the + mapping once. Do not invent synonyms for variety. +- Add tests that protect intended behavior or reproduce a concrete regression. + Do not test provisional implementation choices that are not part of the + supported contract. +- Remove obsolete scaffolding and diagnostic suppressions before handoff. Keep a + workaround or suppression only when it is still necessary, scope it as + narrowly as possible, and document the technical reason. - Add or update automated tests for every behavioral code change. During development, run the narrowest relevant test first, then the required lint checks before handoff. diff --git a/CHANGELOG.md b/CHANGELOG.md index 105ebe9a77..96eb2ba089 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,10 +12,29 @@ 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, + 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 + 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**]) +- ๐Ÿ 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 + 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 + 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 @@ -23,13 +42,26 @@ 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 + 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 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**]) +### 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)._ @@ -451,7 +483,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 @@ -604,9 +636,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._ @@ -633,10 +665,25 @@ 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 +[#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 +[#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 +[#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 [#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/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/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/UPGRADING.md b/UPGRADING.md index e3c2f7e351..52fabb57c7 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -6,6 +6,112 @@ 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 +`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` +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 +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 +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 low-level QDMI +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`. @@ -92,7 +198,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/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/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/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/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/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/bindings/qdmi/qdmi.cpp b/bindings/qdmi/qdmi.cpp index 0aac6d9a95..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", @@ -309,6 +314,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 +413,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."); @@ -425,6 +438,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, @@ -484,6 +505,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/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/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}) 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/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/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/docs/conf.py b/docs/conf.py index 5577e1293d..baef0fad7b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -62,12 +62,12 @@ "myst_nb", "sphinx_copybutton", "sphinx_design", + "sphinx_llm.txt", "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.napoleon", "sphinx.ext.viewcode", "sphinxcontrib.bibtex", - "sphinxcontrib.inkscapeconverter", "sphinxext.opengraph", ] @@ -166,6 +166,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/index.md b/docs/index.md index 821424e36d..c85495084e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -116,13 +116,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) + - 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/configuration.md b/docs/qdmi/configuration.md index 1bfc3a948e..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 @@ -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 @@ -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. @@ -159,18 +157,60 @@ 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 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 @@ -183,8 +223,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/index.md b/docs/qdmi/index.md index db2228ee36..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,9 +14,11 @@ 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 +Slurm integration QDMI-Qiskit Backend PennyLane interface for QDMI devices ``` diff --git a/docs/qdmi/na_device.md b/docs/qdmi/na_device.md index dd6e3fcb3f..7eec73010f 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 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: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/docs/qdmi/qdmi_backend.md b/docs/qdmi/qdmi_backend.md index e44bf20020..99ed860a89 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]" ``` ::: @@ -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 @@ -301,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) @@ -331,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) @@ -359,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/docs/qdmi/sc_device.md b/docs/qdmi/sc_device.md new file mode 100644 index 0000000000..833657056a --- /dev/null +++ b/docs/qdmi/sc_device.md @@ -0,0 +1,97 @@ +# 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, "name": "QB2", "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`. 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/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/include/mqt-core/fomac/FoMaC.hpp b/include/mqt-core/fomac/FoMaC.hpp index 67e88d84a1..db22e05f13 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) { @@ -104,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) { @@ -466,6 +500,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; @@ -514,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 @@ -547,6 +595,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: @@ -564,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 { @@ -685,6 +747,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/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/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/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/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/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 // 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/include/mqt-core/qdmi/devices/sc/Configuration.hpp b/include/mqt-core/qdmi/devices/sc/Configuration.hpp new file mode 100644 index 0000000000..ae43bf9aa7 --- /dev/null +++ b/include/mqt-core/qdmi/devices/sc/Configuration.hpp @@ -0,0 +1,80 @@ +/* + * 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; + std::optional name; + }; + + 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..18d2ebc79d 100644 --- a/include/mqt-core/qdmi/devices/sc/Device.hpp +++ b/include/mqt-core/qdmi/devices/sc/Device.hpp @@ -8,301 +8,107 @@ * 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_; - - /// @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_Device_Session_impl_d; - /** - * @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; +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; + + 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; + : 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_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; -}; - -/** - * @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/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/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/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/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/noxfile.py b/noxfile.py index 8e8e6e5731..f5125e82e0 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): @@ -150,7 +149,13 @@ def docs(session: nox.Session) -> None: if serve: session.install("sphinx-autobuild") - env = {"UV_PROJECT_ENVIRONMENT": session.virtualenv.location} + env = { + "UV_PROJECT_ENVIRONMENT": session.virtualenv.location, + # Favor fast compilation for this short-lived documentation build. + "SKBUILD_CMAKE_BUILD_TYPE": "Debug", + # Header-set verification and IPO remain enabled by default elsewhere. + "SKBUILD_CMAKE_ARGS": "-DCMAKE_VERIFY_INTERFACE_HEADER_SETS=OFF;-DENABLE_IPO=OFF", + } # install build and docs dependencies on top of the existing environment session.run( "uv", @@ -207,14 +212,23 @@ 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"), + } + + session.run("uv", "sync", "--inexact", "--only-group", "build", env=env) session.run( "uv", "sync", - "--no-editable", "--no-dev", "--group", "build", + "--no-build-isolation-package", + "mqt-core", # build the project without isolation env=env, ) diff --git a/pyproject.toml b/pyproject.toml index 47c217c595..96ec09bf66 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", ] @@ -41,11 +41,15 @@ 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", ] requires-python = ">=3.10" dynamic = ["version"] +dependencies = [ + "typing-extensions>=4.12; python_version < '3.11'", +] [project.optional-dependencies] pennylane = [ @@ -89,23 +93,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", @@ -114,7 +103,6 @@ install.components = [ "qdmi_Development", ] -metadata.version.provider = "scikit_build_core.metadata.setuptools_scm" sdist.include = ["python/mqt/core/_version.py"] sdist.exclude = [ "**/.github", @@ -128,6 +116,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 = [ @@ -140,6 +131,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" @@ -231,14 +223,12 @@ ignore = [ 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"] [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"] @@ -286,12 +276,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] @@ -331,7 +322,7 @@ exclude = [ [dependency-groups] build = [ - "nanobind~=2.13.0", + "nanobind~=2.14.0", "scikit-build-core~=1.0.3", "setuptools-scm>=9.2.2", ] @@ -342,10 +333,10 @@ 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", - "qiskit[qasm3-import,visualization]>=1.1.0", + "qiskit[visualization]>=1.1.0", "openqasm-pygments>=0.2.0", "breathe>=4.36.0", "pennylane>=0.45.1,<0.46; python_version >= '3.11'", 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/_compat/typing.py b/python/mqt/core/_compat/typing.py index 9c621a5170..b29e52cb1c 100644 --- a/python/mqt/core/_compat/typing.py +++ b/python/mqt/core/_compat/typing.py @@ -6,23 +6,19 @@ # # Licensed under the MIT License -from __future__ import annotations +"""Compatibility imports for typing features.""" 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 + from typing import Unpack as _Unpack else: + from typing_extensions import Unpack as _Unpack - def assert_never(_: object) -> None: - msg = "Expected code to be unreachable" - raise AssertionError(msg) +# Give introspection tools an unconditional module-level export. +Unpack = _Unpack - -__all__ = ["assert_never"] +__all__ = ["Unpack"] def __dir__() -> list[str]: 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/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/__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/python/mqt/core/plugins/qiskit/backend.py b/python/mqt/core/plugins/qiskit/backend.py index 62fdb52f6a..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 @@ -50,6 +52,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 @@ -168,12 +172,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. @@ -184,6 +195,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() @@ -195,20 +207,65 @@ 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. """ + 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, **dict(session_parameters or {})), + 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 + + 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 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/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/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/python/mqt/core/qdmi/__init__.pyi b/python/mqt/core/qdmi/__init__.pyi index 28a96d0c5a..a54e56bea8 100644 --- a/python/mqt/core/qdmi/__init__.pyi +++ b/python/mqt/core/qdmi/__init__.pyi @@ -13,6 +13,7 @@ from collections.abc import Sequence from typing import overload from . import driver as driver +from . import slurm as slurm class Job: """A job represents a submitted quantum program execution.""" @@ -113,6 +114,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 +235,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.""" @@ -251,6 +259,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 @@ -302,6 +317,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/python/mqt/core/qdmi/slurm.pyi b/python/mqt/core/qdmi/slurm.pyi new file mode 100644 index 0000000000..98ce6d6907 --- /dev/null +++ b/python/mqt/core/qdmi/slurm.pyi @@ -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 + +"""Open a QDMI device named by the Slurm license environment.""" + +import mqt.core.qdmi + +def open_device_from_license() -> 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/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/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/FoMaC.cpp b/src/fomac/FoMaC.cpp index fb3e7156a6..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>> @@ -275,6 +296,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); @@ -407,6 +432,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( @@ -524,6 +558,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/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/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/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 << "]"; 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/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/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..6d0255e4d7 --- /dev/null +++ b/src/qdmi/devices/na/Configuration.cpp @@ -0,0 +1,669 @@ +/* + * 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 = 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)) - + (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 5bf591e1b7..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; + } + 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; } - 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; + } + 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( @@ -604,7 +684,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/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/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..b37cf5f20b 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,26 @@ 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 + ${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 new file mode 100644 index 0000000000..f5d18f03f2 --- /dev/null +++ b/src/qdmi/devices/sc/Configuration.cpp @@ -0,0 +1,407 @@ +/* + * 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", "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.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 name, t1, or " + "t2 with valid values"); + } + 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 d8f37eda95..7783e8fdce 100644 --- a/src/qdmi/devices/sc/Device.cpp +++ b/src/qdmi/devices/sc/Device.cpp @@ -14,86 +14,243 @@ #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.name) { + site->name = override.name; + } + 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); +} + +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; } -auto Device::queryProperty(const QDMI_Device_Property prop, const size_t size, - void* value, size_t* sizeRet) const -> int { + +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,424 +261,284 @@ 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_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; + } + 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); } - -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_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; } -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; +void MQT_SC_QDMI_device_job_free(MQT_SC_QDMI_Device_Job job) { + if (job != nullptr) { + job->free(); } - return job->queryProperty(prop, size, value, sizeRet); } - +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 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/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/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/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); 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_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/fomac/test_slurm.cpp b/test/fomac/test_slurm.cpp new file mode 100644 index 0000000000..b65151b12c --- /dev/null +++ b/test/fomac/test_slurm.cpp @@ -0,0 +1,164 @@ +/* + * 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; +}; + +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 = makeStatusSession(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/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/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..5e7cd4d7c4 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,15 +70,94 @@ 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 + 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: 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_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] 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([]) 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) diff --git a/test/python/qdmi/test_qdmi.py b/test/python/qdmi/test_qdmi.py index c2b0b0c71e..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 @@ -173,6 +174,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() @@ -235,6 +244,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 @@ -592,6 +606,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. @@ -623,6 +645,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) @@ -1042,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/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/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 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/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/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/devices/na/CMakeLists.txt b/test/qdmi/devices/na/CMakeLists.txt index 69a3ee53e7..926d0afcf8 100644 --- a/test/qdmi/devices/na/CMakeLists.txt +++ b/test/qdmi/devices/na/CMakeLists.txt @@ -6,35 +6,32 @@ # # 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) + 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 - 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..8a0075d341 100644 --- a/test/qdmi/devices/na/test_device.cpp +++ b/test/qdmi/devices/na/test_device.cpp @@ -9,17 +9,22 @@ */ #include "mqt_na_qdmi/device.h" -#include "qdmi/devices/na/Generator.hpp" +#include "qdmi/TestUtils.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 @@ -29,9 +34,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 +56,33 @@ 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 { + +using mqt::test::ScopedEnvironmentVariable; + +[[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 +100,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 +170,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/devices/sc/CMakeLists.txt b/test/qdmi/devices/sc/CMakeLists.txt index eaf8216240..351ccdc96e 100644 --- a/test/qdmi/devices/sc/CMakeLists.txt +++ b/test/qdmi/devices/sc/CMakeLists.txt @@ -6,35 +6,34 @@ # # 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" + IQM_GARNET_JSON="${PROJECT_SOURCE_DIR}/json/sc/iqm-garnet.json" + IQM_EMERALD_JSON="${PROJECT_SOURCE_DIR}/json/sc/iqm-emerald.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..7089657c5d --- /dev/null +++ b/test/qdmi/devices/sc/test_configuration.cpp @@ -0,0 +1,270 @@ +/* + * 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 +#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, 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"); }, + "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"); + 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) { + 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..42ab394a29 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,331 @@ 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": 0, "name": "QB1"}, + {"qubit": 1, "name": "QB2", "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; +} + +[[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) { + 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); + 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( + 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( + 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), + 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 e84243087b..dd2cf91693 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,10 @@ 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\"" + "MQT_CORE_QDMI_SC_LIBRARY=\"$\"" "TEST_DEVICE_LIBRARIES=std::array{ std::pair{\"$\", \"MQT_NA\"}, std::pair{\"$\", \"MQT_SC\"}, std::pair{\"$\", \"MQT_DDSIM\"} }" ) @@ -127,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/session_device.cpp b/test/qdmi/driver/session_device.cpp index 5f91e81268..17e7b32d28 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; @@ -27,6 +34,7 @@ struct QDMI_Device_Session_impl_d { struct QDMI_Device_Job_impl_d { QDMI_Device_Session session = nullptr; + bool retrieved = false; }; namespace { @@ -45,11 +53,55 @@ 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; } +[[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; @@ -65,6 +117,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. @@ -165,6 +233,38 @@ 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_STATUS) { + return queryValue( + deviceStatus(parameter(session, QDMI_DEVICE_SESSION_PARAMETER_CUSTOM4)), + size, value, sizeRet); + } if (prop != QDMI_DEVICE_PROPERTY_NAME) { return QDMI_ERROR_NOTSUPPORTED; } @@ -190,10 +290,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; } @@ -205,23 +321,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 +368,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..be62d7f5ad 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."; @@ -824,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"; @@ -873,8 +905,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), @@ -883,9 +915,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); } @@ -903,7 +935,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); @@ -1075,6 +1107,56 @@ 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", + .session = {}})); + static_cast(driver.registerDeviceIfAbsent( + {.id = "test.na.runtime-custom", + .library = MQT_CORE_QDMI_NA_LIBRARY, + .prefix = "MQT_NA", + .session = makeDeviceConfigurationSession(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 = makeDeviceConfigurationSession( + 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"); @@ -1082,6 +1164,150 @@ 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, + ScRuntimeConfigurationSeparatesModelsUsingOneProviderLibrary) { + auto& driver = qdmi::Driver::get(); + const auto firstConfig = makeDeviceConfigurationSession( + 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-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, + "durationUnit":{"unit":"us","scaleFactor":0.5}, + "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"); + 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; @@ -1099,6 +1325,35 @@ 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); + + const auto retrievedJob = device.retrieveJobById("session-job"); + EXPECT_EQ(retrievedJob.getId(), "session-job"); +} + TEST(DeviceRegistrationTest, FreshChildDeviceRetainsItsRootSession) { registerSessionTestDevice(); std::optional child; @@ -1169,8 +1424,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) { @@ -1226,15 +1481,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"; @@ -1274,7 +1528,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));); } @@ -1290,16 +1544,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, makeCustom3Session("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, + makeCustom3Session("second-session")); ASSERT_NE(first, second); EXPECT_EQ(&first->getLibrary(), &second->getLibrary()); 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..83b5958df4 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: @@ -399,50 +349,39 @@ 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)); } -} -#endif -TEST(DeviceRegistry, ReadsProjectConfigurationFromPyprojectToml) { - 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"); + 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 -TEST(DeviceRegistry, DedicatedProjectFileWinsOverPyproject) { +TEST(DeviceRegistry, ReadsProjectConfigurationFromNearestQdmiJson) { 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()); @@ -450,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) { @@ -510,7 +451,7 @@ TEST(DeviceRegistry, ReportsInvalidDocumentsAndDefinitionTypes) { } } -TEST(DeviceRegistry, ReportsInvalidExplicitJsonAndToml) { +TEST(DeviceRegistry, ReportsInvalidExplicitJson) { const TemporaryDirectory directory; { const ScopedEnvironmentVariable configFile( @@ -526,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/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 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}); } diff --git a/uv.lock b/uv.lock index d89b775729..57fc21c3d2 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 = [ @@ -1792,7 +1795,7 @@ docs = [ { name = "myst-nb" }, { name = "openqasm-pygments" }, { name = "pennylane", marker = "python_full_version >= '3.11'" }, - { name = "qiskit", extra = ["qasm3-import", "visualization"] }, + { name = "qiskit", extra = ["visualization"] }, { name = "setuptools-scm" }, { 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.*'" }, @@ -1801,8 +1804,8 @@ 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" }, ] test = [ @@ -1820,18 +1823,19 @@ 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"] [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" }, @@ -1853,15 +1857,15 @@ docs = [ { name = "myst-nb", specifier = ">=1.3.0" }, { name = "openqasm-pygments", specifier = ">=0.2.0" }, { name = "pennylane", marker = "python_full_version >= '3.11'", specifier = ">=0.45.1,<0.46" }, - { name = "qiskit", extras = ["qasm3-import", "visualization"], specifier = ">=1.1.0" }, + { name = "qiskit", extras = ["visualization"], specifier = ">=1.1.0" }, { name = "setuptools-scm", specifier = ">=9.2.2" }, { name = "sphinx", specifier = ">=8.1.3" }, { name = "sphinx", marker = "python_full_version >= '3.11'", specifier = ">=8.2.3" }, { 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" }, ] test = [ @@ -1955,11 +1959,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]] @@ -3858,6 +3862,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" @@ -3930,20 +3966,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, ] -[[package]] -name = "sphinxcontrib-svg2pdfconverter" -version = "2.1.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'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b9/05/11b12853b6a0398bd19d5464b68cb97dde21c354265ff2c1929f92ae03a9/sphinxcontrib_svg2pdfconverter-2.1.0.tar.gz", hash = "sha256:9756e82d5f3bf11629ffcbafb1f8a1092d3bb4789e33494032cdce9a9c8459d3", size = 6805, upload-time = "2026-03-05T18:23:44.74Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/7d/9b6b1ef6fe13bac6455d002ca4a626fac325cf94b4183f31cafe9b1ed17c/sphinxcontrib_svg2pdfconverter-2.1.0-py3-none-any.whl", hash = "sha256:805635ac274583e1606b0ec7fb21f16b69ca8ff223dd7e237d91127015628b0c", size = 9259, upload-time = "2026-03-05T18:23:43.662Z" }, -] - [[package]] name = "sphinxext-opengraph" version = "0.13.0" 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