From c5f9d560ee52d1f09316e6d449c716b0fee78d76 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Tue, 4 Aug 2026 13:51:11 +0200 Subject: [PATCH 1/6] =?UTF-8?q?=E2=9C=A8=20Integrate=20QDMI=20devices=20wi?= =?UTF-8?q?th=20the=20MLIR=20compiler?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a detached FoMaC adapter, Python target bindings, and the minimal mqt-cc device workflow on top of the compiler-owned target pipeline. Preserve calibration metadata, reject unsupported site models, and keep conformance as the final target check. Assisted-by: GPT-5.6 via Codex --- .agent/plans/1687-int-final-qdmi-bridge.md | 433 ++++++++++++++++++ CHANGELOG.md | 3 + bindings/mlir/CMakeLists.txt | 1 + bindings/mlir/register_mlir.cpp | 264 ++++++++++- bindings/patterns.txt | 47 ++ docs/mlir/index.md | 5 +- docs/mlir/python_compiler_collection.md | 3 + docs/mlir/target_compilation.md | 102 +++++ mlir/include/mlir/Compiler/FoMaCAdapter.h | 32 ++ .../include/mlir/Compiler/TargetCompilation.h | 4 +- mlir/lib/Compiler/CMakeLists.txt | 20 +- mlir/lib/Compiler/FoMaCAdapter.cpp | 287 ++++++++++++ mlir/lib/Compiler/TargetCompilation.cpp | 5 +- mlir/tools/mqt-cc/CMakeLists.txt | 11 +- mlir/tools/mqt-cc/mqt-cc.cpp | 89 ++++ mlir/unittests/Compiler/CMakeLists.txt | 56 ++- mlir/unittests/Compiler/Inputs/bell.qasm | 9 + .../Compiler/Inputs/heterogeneous-sc.json | 37 ++ .../Compiler/Inputs/mqt-cc-explicit.qdmi.json | 11 + .../Compiler/test_compiler_fomac_adapter.cpp | 122 +++++ python/mqt/core/mlir.pyi | 263 ++++++++++- test/python/test_mlir.py | 89 ++++ 22 files changed, 1878 insertions(+), 15 deletions(-) create mode 100644 .agent/plans/1687-int-final-qdmi-bridge.md create mode 100644 docs/mlir/target_compilation.md create mode 100644 mlir/include/mlir/Compiler/FoMaCAdapter.h create mode 100644 mlir/lib/Compiler/FoMaCAdapter.cpp create mode 100644 mlir/unittests/Compiler/Inputs/bell.qasm create mode 100644 mlir/unittests/Compiler/Inputs/heterogeneous-sc.json create mode 100644 mlir/unittests/Compiler/Inputs/mqt-cc-explicit.qdmi.json create mode 100644 mlir/unittests/Compiler/test_compiler_fomac_adapter.cpp diff --git a/.agent/plans/1687-int-final-qdmi-bridge.md b/.agent/plans/1687-int-final-qdmi-bridge.md new file mode 100644 index 0000000000..287658cf1f --- /dev/null +++ b/.agent/plans/1687-int-final-qdmi-bridge.md @@ -0,0 +1,433 @@ +# Integrate QDMI devices with the MLIR compiler + +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 + +The compiler-target foundation, target-backed mapper, target-independent +two-qubit gate fusion, target-native synthesis, conformance verifier, and +canonical target pipeline are now merged. This final slice connects those +compiler-owned abstractions to QDMI without duplicating them. After the change, +a C++ or Python user can snapshot a circuit-model `fomac::Device` into an +immutable `mlir::CompilerTarget` and compile for it after the device and session +have been destroyed. The `mqt-cc` executable can list configured QDMI devices, +select one by stable identifier, and run the same target pipeline. + +The bridge retains device names, site names, topology, T1/T2 values, operation +capabilities, fidelities, and available durations. It rejects neutral-atom zone +models with a direct diagnostic because this target pipeline currently models +circuit sites only. CoreFoMaC remains MLIR-free, `MQTCompilerTarget` remains +FoMaC/QDMI/CoreIR-free, and no second target DTO or dynamic driver boundary is +introduced. + +The observable proof combines focused adapter tests, Python target construction +and compilation tests, and a minimal `mqt-cc` device workflow. The existing pull +request is then rewritten from current `main` as this thin integration rather +than retaining its historical merge-heavy implementation. + +### Progress + +- [x] (2026-08-04) Verified that PR #1999 merged and that fresh `origin/main` is + `47a25e76087f1c44cf2c622c2b628c1b57e2f7a6`. +- [x] (2026-08-04) Created an isolated worktree from that exact revision and + read the repository policies, AI disclosure rules, contributor guidance, + and remediation protocol. +- [x] (2026-08-04) Refreshed PR #1687 at exact historical head + `dd9619bd27a34ced8ed68a4ee4533cb85771f144`, inventoried all 35 review + threads, and confirmed that only issue #1082 should close. +- [x] (2026-08-04) Inventoried the merged compiler target/pipeline, live FoMaC + query surface, Python binding and generated-stub boundary, QDMI provider + discovery, CLI, bundled IQM assets, CMake runtime-copy helper, tests, and + documentation. +- [x] (2026-08-04) Implemented the narrow FoMaC-to-compiler-target adapter and + focused lifetime, calibration, all-to-all, and zone-rejection tests. +- [x] (2026-08-04) Exposed the immutable compiler target and target compilation + through the MLIR Python binding, regenerated the authoritative stub, and + added focused Python behavior tests. +- [x] (2026-08-04) Added the three QDMI CLI options and minimal subprocess tests + under the established compiler test root while reusing provider discovery, + runtime assets, and the canonical target pipeline. +- [x] (2026-08-04) Added concise workflow documentation and a separate #1687 + changelog entry credited to Matthias Reumann and Lukas Burgholzer. +- [x] (2026-08-04) Built the adapter, compiler tests, bindings, and CLI; passed + all 221 compiler tests, 29 focused Python tests, eight adapter/CLI CTests, + a provider-disabled compiler build, authoritative stub generation, strict + warning-free documentation, changed-source clang-tidy, repository lint, + and `git diff --check`. +- [ ] Commit the validated implementation, complete an independent exact-head + review, and address every material finding. +- [ ] Rewrite the existing PR branch with an exact force-with-lease, replace the + obsolete PR description, verify the replacement head, and monitor CI. + +### Surprises & Discoveries + +- Observation: the live compiler already owns every semantic and pipeline + abstraction needed by this slice. The historical PR's `fomac::Target`, + targeting pass, mapper augmentation, native-gate menu, and duplicated pipeline + are obsolete and must not be ported. +- Observation: `CompilerTarget::Operation` deliberately models homogeneous + target-wide support, while QDMI can report a restricted site list. The adapter + must therefore verify that a one-qubit operation covers every site and that a + two-qubit operation covers every topology edge or all-to-all pair before + treating it as target-wide. Ordered site tuples then carry calibration + overrides only. +- Observation: the bundled IQM Garnet and Emerald models, stable registry IDs, + runtime assets, site names, T1/T2 values, and fidelities are already present + on `main`. This slice consumes those models rather than adding fixtures or + provenance text. +- Observation: `mqt_copy_qdmi_runtime` already copies built-in provider + libraries, registry manifests, and assets beside an executable. The CLI does + not need another plugin loader or packaging mechanism. +- Observation: the old PR description and most unresolved threads refer to + deleted predecessor abstractions. The final branch should satisfy the + remaining behavior through the merged prerequisite PRs and this adapter, then + describe only the actual user workflow. +- Observation: `mqt-cc` is not currently shipped as part of the Python wheel. + This slice keeps that packaging boundary unchanged; Python target compilation + is provided directly by the extension and packaged QDMI providers. +- Observation: MQT Core does not currently export or install the MLIR compiler + libraries and generated headers as a consumable SDK. Exporting the adapter + would require exporting the compiler pipeline, dialect libraries, generated + headers, and their MLIR dependency closure. This slice therefore documents the + C++ and `mqt-cc` workflows as source-build interfaces and leaves a coherent + MLIR SDK/package boundary to a dedicated follow-up. +- Observation: the previously merged target pipeline ran the generic QCO cleanup + after target-native synthesis and conformance. Its canonicalization patterns + can rewrite `qco.r` operations with special angles back to `qco.rx` or + `qco.ry`, making a formerly conforming Garnet result non-native. A real Garnet + compilation exposed this issue; unit tests that stopped at conformance did + not. +- Observation: a clean build with all three built-in QDMI providers disabled + exposed that provider-backed test sources and runtime copying must be + conditional. The compiler test target now builds without provider libraries, + while normal CI retains full live-device coverage. + +### Decision Log + +- Decision: add one public adapter function, + `mlir::compilerTargetFromDevice(const fomac::Device&)`, in a small library + that links `MQTCompilerTarget` and `MQT::CoreFoMaC`. Rationale: dependency + direction stays acyclic and compiler semantics remain owned by MLIR while + callers opt into the live-device bridge. Date/Author: 2026-08-04, GPT-5.6 via + Codex. +- Decision: snapshot all QDMI data eagerly and return a detached + `CompilerTarget`. Rationale: compiler execution must not depend on a live QDMI + session or provider handle, and the target already has shared immutable + storage for cheap copies. Date/Author: 2026-08-04, GPT-5.6 via Codex. +- Decision: reject any device site that is a zone and any zoned operation. + Rationale: circuit-model topology and neutral-atom zones have different + semantics; silently flattening zones into qubits would create an invalid + target. Date/Author: 2026-08-04, GPT-5.6 via Codex. +- Decision: reject explicit QDMI operation site lists that are not homogeneous + over the compiler target. Rationale: the compiler target intentionally + represents one target-wide gate set; silently widening a restricted QDMI + operation would allow synthesis to emit an unsupported gate. Date/Author: + 2026-08-04, GPT-5.6 via Codex. +- Decision: expose `CompilerTarget.from_device(device)` from `mqt.core.mlir`, + not `Device.target()` from `mqt.core.fomac`. Rationale: CoreFoMaC and its + binding remain independent of MLIR, target ownership is visible in the + compiler namespace, and compilation APIs accept one explicit target type. + Date/Author: 2026-08-04, GPT-5.6 via Codex. +- Decision: add `QCOProgram.compile_for_target` and an optional `target` to the + sole `compile_program` function. Rationale: Python mirrors the two canonical + C++ entry points and does not accept devices, coupling maps, native-gate + strings, or compatibility shims. Date/Author: 2026-08-04, GPT-5.6 via Codex. +- Decision: add only `--qdmi-list-devices`, `--qdmi-device`, and `--qdmi-config` + to `mqt-cc`. Configure the registry before the first driver access, snapshot + the selected device, and invoke the existing canonical target pipeline. + Rationale: this is the irreducible user surface and preserves current provider + discovery rather than introducing another dynamic boundary. Date/Author: + 2026-08-04, GPT-5.6 via Codex. +- Decision: reject target compilation when the requested output or custom pass + sequence cannot preserve or safely compose the target assignment, using the + validation already centralized in `runDefaultPipeline`. Rationale: options + must not be silently ignored and the CLI must not replicate pipeline rules. + Date/Author: 2026-08-04, GPT-5.6 via Codex. +- Decision: keep four CLI subprocess tests under + `mlir/unittests/Compiler/Inputs`, reuse the release-bundled IQM assets, and + make `mqt-cc` a test-only dependency when the SC provider is enabled. + Rationale: this respects the established test layout and proves only option + parsing, registry selection, and one compilation workflow without promoting + the optional LLVM tool into every default build. Date/Author: 2026-08-04, + GPT-5.6 via Codex. +- Decision: perform generic QCO cleanup before target-native synthesis, retain + only CSE and dead-value cleanup afterward, and run conformance last. + Rationale: no target-independent canonicalizer may reintroduce a gate outside + the native operation set after it has been synthesized and verified. + Date/Author: 2026-08-04, GPT-5.6 via Codex. +- Decision: do not add an upgrade-guide entry, compatibility alias, wheel + shipment for `mqt-cc`, IQM data attribution, or a second changelog reference + in prerequisite entries. Rationale: the compiler collection is unreleased and + the user requested a compact final integration entry credited only to Matthias + Reumann and Lukas Burgholzer. Date/Author: 2026-08-04, GPT-5.6 via Codex. +- Decision: do not add a partial install/export path for only the adapter. + Rationale: the repository has no installed MLIR SDK boundary, and exporting a + single facade while omitting the pipeline, dialects, and generated headers + would be unusable. The documented C++ and CLI workflows are explicitly + source-build workflows; packaged Python target compilation remains covered. + Date/Author: 2026-08-04, GPT-5.6 via Codex. + +### Outcomes & Retrospective + +The implementation is complete and locally validated. It adds one detached +adapter rather than another target model, one Python target type, three CLI +options, four irreducible subprocess checks, and no compatibility surface. The +real integration test found and fixed a pass-ordering bug in the merged target +pipeline: generic canonicalization now runs before native synthesis, while +conformance remains the final semantic check. + +The source-build C++ and CLI workflows and packaged Python workflow are proven. +A distributable MLIR C++ SDK remains a separate packaging concern because the +current repository does not export the compiler dialects, generated headers, or +pipeline dependency closure. Independent exact-head review and publication are +still pending. + +### Context and Orientation + +`mlir/include/mlir/Compiler/Target.h` and `mlir/lib/Compiler/Target.cpp` define +`mlir::CompilerTarget`. It can be constructed from a site count or detailed +sites, optional undirected topology, optional homogeneous operation +capabilities, and an optional duration unit. The detailed `Site`, `SiteTuple`, +and `Operation` values retain names, coherence times, ordered calibration sites, +durations, and fidelities. An absent topology means all-to-all; an absent +operation set means every operation is native. + +`mlir/include/mlir/Compiler/TargetCompilation.h` and +`mlir/lib/Compiler/TargetCompilation.cpp` define the canonical compilation +sequence. `QCOProgram::compileForTarget` and the optional target accepted by +`runDefaultPipeline` both delegate to it. The bridge must call these entry +points rather than compose passes itself. + +`include/fomac/FoMaC.hpp` and `src/fomac/FoMaC.cpp` define the live QDMI +wrapper. `fomac::Session` owns device discovery and returns `fomac::Device` +handles. Device, site, and operation queries provide the data needed for a +detached compiler target. The adapter is the only new library that links FoMaC +to `MQTCompilerTarget`. + +`bindings/mlir/register_mlir.cpp` implements the `mqt.core.mlir` nanobind +extension. `python/mqt/core/mlir.pyi` is generated by the repository `stubs` +session and must not be edited by hand. The binding already owns the typed +program and `compile_program` surface. + +`mlir/tools/mqt-cc/mqt-cc.cpp` implements the standalone compiler driver. +`src/qdmi/driver` owns provider discovery and the stable device registry. +`mqt_copy_qdmi_runtime` is the existing CMake helper for colocating the built-in +providers and assets with an executable. + +The Garnet and Emerald configurations are installed from `json/sc/` and are +registered as `mqt.sc.iqm.garnet` and `mqt.sc.iqm.emerald`. The neutral-atom +default model is useful only to prove the adapter's explicit zone diagnostic. + +This task may add the adapter header, source, library, focused tests, minimal +CLI tests under the compiler test root, concise compiler/QDMI workflow +documentation, Python bindings and generated stub updates, the separate +changelog entry, and this ExecPlan. It must not reimplement the target or +pipeline, modify CoreFoMaC to depend on MLIR, add a legacy CoreIR dependency to +the adapter or CLI, or revive historical targeting abstractions. + +### Plan of Work + +First add the adapter header and source under the compiler subtree. Query the +device name, sites, duration unit, topology, and operations once. Convert site +indices after checking that they fit the nonnegative i64 target domain. Reject +zones, preserve optional site metadata, convert the optional undirected coupling +map, and snapshot homogeneous operation capabilities plus ordered site-tuple +calibration. Preserve reported duration units, default an omitted scale factor +to one, and reject a scale factor without a unit. Let `CompilerTarget` perform +cross-object validation and canonicalization. + +Add focused C++ tests for a detached bundled IQM target, counts and calibration, +missing topology as all-to-all on a circuit device, rejection of restricted +operation support, and the neutral-atom zone diagnostic. Build the adapter as a +distinct library with only public dependencies on FoMaC and `MQTCompilerTarget`; +copy the QDMI runtime only to tests or executables that need live provider +discovery. + +Next bind the compiler target value types and immutable properties in +`mqt.core.mlir`. Provide direct site-count and detailed-site constructors, +`CompilerTarget.from_device`, operation support queries, +`QCOProgram.compile_for_target`, and `compile_program(..., target=None)`. +Regenerate the stub and add tests that construct a target directly, snapshot +Garnet, destroy the session/device, inspect names and calibration, and compile a +small program through the canonical pipeline. + +Then extend `mqt-cc` with the three QDMI options. Apply an explicit registry +configuration before initializing the driver, list stable identifiers without +opening devices, open only a selected device, snapshot it through the adapter, +and pass it to `runDefaultPipeline`. Keep option validation compact and rely on +the compiler API for target/output and target/custom-pipeline diagnostics. Link +the tool to the adapter and copy the existing runtime beside it. Add a tiny +input under `mlir/unittests/Compiler/Inputs` and only the irreducible list, +unknown-ID, explicit-config, and Garnet compilation checks. + +Finally document direct C++, Python, and CLI workflows without design history. +Refer qubit-reuse users to `mqt-qubit-reuse`, link to existing QDMI registry and +IQM model documentation instead of duplicating it, and state only that +unavailable durations are absent. Add the separate changelog entry for #1687 +with the requested two authors and no upgrade note. + +### Milestones + +The first milestone produces the detached bridge. Add +`mlir/include/mlir/Compiler/FoMaCAdapter.h` and +`mlir/lib/Compiler/FoMaCAdapter.cpp`, then build the compiler unit-test target. +At the end, an IQM device can be destroyed immediately after conversion while +the returned target still exposes its name, topology, coherence values, gate +set, and fidelity data. A restricted SC operation and a neutral-atom zone model +both fail with precise diagnostics. + +The second milestone exposes the same owned value through Python and the +command-line driver. Regenerate `python/mqt/core/mlir.pyi`, run +`test/python/test_mlir.py`, and execute the four `mqt-cc` CTests. At the end, +Python can construct or snapshot a target and compile for it, while a +source-build `mqt-cc` can list devices, apply an explicit registry +configuration, reject an unknown ID, and compile the Bell program for Garnet. + +The third milestone proves cohesion and publication readiness. Generate the MLIR +reference documentation, run strict Sphinx documentation, changed-source +clang-tidy, complete relevant C++ suites, provider-disabled configuration, stub +generation, and repository lint. An independent exact-head `mqt-pr-review` must +find no material correctness, bloat, packaging, or documentation issue before +the historical PR branch is replaced. + +### Concrete Steps + +Run all commands from the repository root of the isolated task worktree. + +Configure a task-local release build with the repository wrapper: + + MLIR_DIR=/lib/cmake/mlir \ + .agent/run.sh cmake --preset release + +Build the adapter, compiler tests, MLIR Python extension, and CLI: + + .agent/run.sh cmake --build build/release --target \ + MQTCompilerFoMaCAdapter mqt-core-mlir-unittests-compiler \ + mqt-core-mlir-bindings mqt-cc -j 8 + +Run focused C++ and CLI CTest selections: + + .agent/run.sh ctest --test-dir build/release \ + --output-on-failure -R 'CompilerFoMaCAdapter|mqt-cc' + +Run focused Python tests, regenerate the authoritative stub, and build strict +documentation: + + .agent/run.sh uv run pytest test/python/test_mlir.py -q + .agent/run.sh uvx nox -s stubs + .agent/run.sh uvx nox -s docs + +Run changed-source clang-tidy using `build/release/compile_commands.json`, then +end with the repository-required lint and diff checks: + + git diff --check + .agent/run.sh uvx nox -s lint + +Use changed-source clang-tidy and the relevant complete compiler/QDMI suites in +proportion to the final touch set. Record exact test counts and any environment +boundaries in this plan. + +### Validation and Acceptance + +The existing IQM model tests retain the Garnet and Emerald size, topology, gate +set, and calibration coverage from #1992. The adapter tests must prove: + +1. Garnet snapshots as 20 sites and 30 undirected edges with `r`, `cz`, and + `measure`. +2. Reported site names, T1/T2, and fidelities survive while unavailable + operation durations remain absent. +3. The target remains valid after the originating device and session are + destroyed. +4. A circuit-model device without topology becomes all-to-all. +5. Site-dependent operation support fails rather than being widened. +6. Neutral-atom zone models fail with a precise circuit-model diagnostic. + +The Python tests must prove direct construction, immutable metadata access, +`from_device`, detached lifetime, `compile_for_target`, and optional-target +`compile_program`. + +The CLI tests must prove listing devices, unknown identifiers, explicit registry +configuration, and one successful Garnet compilation. They need not duplicate +the adapter, mapping, synthesis, conformance, or full compiler suites. + +The final revision must build all touched targets, pass the focused and relevant +complete tests, regenerate stubs without an uncommitted delta, pass strict +documentation, changed-source clang-tidy, repository lint, `git diff --check`, +and an independent exact-head `mqt-pr-review`. A provider-disabled build must +configure and build the compiler test target without expecting unavailable +runtime libraries. The packaged Python extension and QDMI provider assets must +be exercised by the Python test session. The source-build-only C++ adapter and +CLI must be labeled as such; a full installed MLIR SDK consumer is deliberately +outside this thin bridge because the repository does not yet expose that package +boundary. C++ patch coverage must be at least 90 percent in CI. + +Before publication, refresh `origin/main`, the remote #1687 head, review +threads, and PR metadata. Replace the historical branch only with: + + git push origin \ + --force-with-lease=refs/heads/feat/arch-option-and-qdmi: \ + HEAD:refs/heads/feat/arch-option-and-qdmi + +The rewritten PR description begins with the required AI disclosure, describes +the three user workflows and dependency boundary, lists validation, and says +`Closes #1082`. It must not close #1079 or #1133. + +### Idempotence and Recovery + +All source edits are ordinary patches in the isolated worktree. Re-running +configuration, builds, tests, stub generation, and lint is safe. The adapter is +deterministic because it snapshots immutable query results into value types. + +If a build or test reveals a provider-discovery path issue, inspect the +executable-local registry and assets produced by `mqt_copy_qdmi_runtime` before +changing code. Do not add a second search path or dynamic loader to mask a +configuration error. + +If the remote PR head changes before publication, stop instead of force-pushing. +Fetch and compare the new revision, incorporate authorized changes deliberately, +then use a newly verified exact lease. Never use an unqualified force push. + +### Artifacts and Notes + +The historical PR head `dd9619bd27a34ced8ed68a4ee4533cb85771f144` is evidence +only. No historical commit or implementation file should be cherry-picked. The +useful prerequisite behavior is already merged through PRs #1992, #1993, #1997, + +## 1998, and #1999 + +The unresolved historical review threads map either to those merged +prerequisites or to this slice's adapter, bindings, CLI, and workflow +documentation. Thread resolution is considered only after the replacement head +contains and verifies the requested behavior. + +### Interfaces and Dependencies + +The adapter exposes: + + namespace mlir { + CompilerTarget compilerTargetFromDevice(const fomac::Device& device); + } + +`MQTCompilerFoMaCAdapter` publicly links `MQTCompilerTarget` and +`MQT::CoreFoMaC`. `MQTCompilerTarget` and CoreFoMaC do not gain new +dependencies. + +Python exposes the same owned target concept: + + target = CompilerTarget.from_device(device) + program.compile_for_target(target) + compile_program(source, target=target) + +The CLI exposes only: + + mqt-cc --qdmi-list-devices + mqt-cc --qdmi-device=mqt.sc.iqm.garnet input.qasm + mqt-cc --qdmi-config=registry.json --qdmi-device= input.qasm + +The target is snapshotted before compilation and no compilation pass retains a +FoMaC or QDMI handle. diff --git a/CHANGELOG.md b/CHANGELOG.md index 54df46a214..f56e1fd430 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ releases may include breaking changes. ### Added +- ✨ Integrate QDMI devices as MLIR compiler targets across C++, Python, and + `mqt-cc` ([#1687]) ([**@MatthiasReumann**], [**@burgholzer**]) - ✨ Add immutable MLIR compiler targets and a canonical target compilation pipeline for decomposition, optimization, mapping, native synthesis, and conformance ([#1993], [#1999]) ([**@simon1hofmann**], [**@burgholzer**]) @@ -819,6 +821,7 @@ for previous changelogs._ [#1702]: https://github.com/munich-quantum-toolkit/core/pull/1702 [#1700]: https://github.com/munich-quantum-toolkit/core/pull/1700 [#1694]: https://github.com/munich-quantum-toolkit/core/pull/1694 +[#1687]: https://github.com/munich-quantum-toolkit/core/pull/1687 [#1676]: https://github.com/munich-quantum-toolkit/core/pull/1676 [#1675]: https://github.com/munich-quantum-toolkit/core/pull/1675 [#1674]: https://github.com/munich-quantum-toolkit/core/pull/1674 diff --git a/bindings/mlir/CMakeLists.txt b/bindings/mlir/CMakeLists.txt index ceabe0f544..68e089c228 100644 --- a/bindings/mlir/CMakeLists.txt +++ b/bindings/mlir/CMakeLists.txt @@ -22,6 +22,7 @@ if(NOT TARGET ${TARGET_NAME}) INSTALL_DIR . LINK_LIBS + MQTCompilerFoMaCAdapter MQTCompilerPipeline MQT::CoreIR) diff --git a/bindings/mlir/register_mlir.cpp b/bindings/mlir/register_mlir.cpp index 7e5e54c681..9eae2d6de3 100644 --- a/bindings/mlir/register_mlir.cpp +++ b/bindings/mlir/register_mlir.cpp @@ -8,17 +8,24 @@ * Licensed under the MIT License */ +#include "fomac/FoMaC.hpp" // NOLINT(misc-include-cleaner) #include "ir/QuantumComputation.hpp" +#include "mlir/Compiler/FoMaCAdapter.h" #include "mlir/Compiler/Programs.h" +#include "mlir/Compiler/Target.h" #include #include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) #include // NOLINT(misc-include-cleaner) #include // NOLINT(misc-include-cleaner) #include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) #include #include +#include #include #include #include @@ -27,6 +34,7 @@ #include #include #include +#include namespace mqt { @@ -227,10 +235,11 @@ programFromPath(const std::filesystem::path& path) { */ [[nodiscard]] mlir::CompilerProgram compileProgram(const nb::object& program, const mlir::ProgramFormat output, - const bool inplace, const std::string& qcoPipeline, - const bool enableTiming, const bool enableStatistics) { + const bool inplace, const mlir::CompilerTarget* const target, + const std::string& qcoPipeline, const bool enableTiming, + const bool enableStatistics) { return takeResult(mlir::runDefaultPipeline(programFromInput(program, inplace), - output, nullptr, qcoPipeline, + output, target, qcoPipeline, enableTiming, enableStatistics)); } @@ -241,6 +250,7 @@ NB_MODULE(MQT_CORE_MODULE_NAME, m) { nb::module_::import_("typing"); nb::module_::import_("mqt.core.ir"); + nb::module_::import_("mqt.core.fomac"); nb::enum_(m, "QIRProfile", "QIR target profiles.") .value("BASE", mlir::QIRProfile::Base, "The QIR Base Profile.") @@ -262,6 +272,240 @@ NB_MODULE(MQT_CORE_MODULE_NAME, m) { .value("QIR_ADAPTIVE", mlir::ProgramFormat::QIRAdaptive, "QIR for the Adaptive Profile."); + auto compilerTarget = nb::class_( + m, "CompilerTarget", R"pb(Immutable MLIR compiler target. + +An absent topology means all-to-all connectivity. An absent operation set +means every operation is native.)pb"); + + auto durationUnit = nb::class_( + compilerTarget, "DurationUnit", "Unit for raw target timing metadata."); + durationUnit.def(nb::init(), "unit"_a, "scale_factor"_a) + .def_prop_ro( + "unit", + [](const mlir::CompilerTarget::DurationUnit& value) { + return value.unit().str(); + }, + "The reported duration unit.") + .def_prop_ro("scale_factor", + &mlir::CompilerTarget::DurationUnit::scaleFactor, + "The multiplier applied to raw timing values."); + + auto targetSite = nb::class_( + compilerTarget, "Site", "A hardware site and its optional metadata."); + targetSite + .def(nb::init, + std::optional, std::optional>(), + "site_id"_a, "name"_a = nb::none(), "t1"_a = nb::none(), + "t2"_a = nb::none()) + .def_prop_ro("id", &mlir::CompilerTarget::Site::id, + "The target-defined nonnegative site identifier.") + .def_prop_ro( + "name", + [](const mlir::CompilerTarget::Site& site) { + const auto name = site.name(); + return name ? std::optional(name->str()) + : std::nullopt; + }, + "The reported site name, if available.") + .def_prop_ro("t1", &mlir::CompilerTarget::Site::t1, + "The raw T1 coherence time, if available.") + .def_prop_ro("t2", &mlir::CompilerTarget::Site::t2, + "The raw T2 coherence time, if available."); + + auto siteTuple = nb::class_( + compilerTarget, "SiteTuple", + "Calibration data for an ordered tuple of target sites."); + siteTuple + .def(nb::init, + std::optional, std::optional>(), + "sites"_a, "duration"_a = nb::none(), "fidelity"_a = nb::none()) + .def_prop_ro( + "sites", + [](const mlir::CompilerTarget::SiteTuple& tuple) { + return std::vector( + tuple.sites().begin(), tuple.sites().end()); + }, + "The ordered target site identifiers.") + .def_prop_ro("duration", &mlir::CompilerTarget::SiteTuple::duration, + "The raw operation duration, if available.") + .def_prop_ro("fidelity", &mlir::CompilerTarget::SiteTuple::fidelity, + "The operation fidelity, if available."); + + auto targetOperation = nb::class_( + compilerTarget, "Operation", + "A homogeneous target-wide operation capability and its calibration."); + targetOperation + .def(nb::new_( + [](std::string name, const size_t numQubits, + const size_t numParameters, + std::optional> + siteTuples, + const std::optional duration, + const std::optional fidelity) { + return mlir::CompilerTarget::Operation( + std::move(name), numQubits, numParameters, + std::move(siteTuples) + .value_or( + std::vector{}), + duration, fidelity); + }), + "name"_a, "num_qubits"_a, "num_parameters"_a, + "site_tuples"_a = nb::none(), "duration"_a = nb::none(), + "fidelity"_a = nb::none()) + .def_prop_ro( + "name", + [](const mlir::CompilerTarget::Operation& operation) { + return operation.name().str(); + }, + "The exact reported operation name.") + .def_prop_ro( + "canonical_name", + [](const mlir::CompilerTarget::Operation& operation) { + return operation.canonicalName().str(); + }, + "The normalized compiler operation name.") + .def_prop_ro("num_qubits", &mlir::CompilerTarget::Operation::numQubits, + "The fixed operation arity.") + .def_prop_ro("num_parameters", + &mlir::CompilerTarget::Operation::numParameters, + "The number of real-valued parameters.") + .def_prop_ro( + "site_tuples", + [](const mlir::CompilerTarget::Operation& operation) { + return std::vector( + operation.siteTuples().begin(), operation.siteTuples().end()); + }, + "Ordered site-specific calibration data.") + .def_prop_ro("duration", &mlir::CompilerTarget::Operation::duration, + "The raw default duration, if available.") + .def_prop_ro("fidelity", &mlir::CompilerTarget::Operation::fidelity, + "The default fidelity, if available."); + + nb::enum_( + compilerTarget, "GateKind", "Recognized native gate capability.") + .value("U", mlir::CompilerTarget::GateKind::U) + .value("X", mlir::CompilerTarget::GateKind::X) + .value("SX", mlir::CompilerTarget::GateKind::SX) + .value("RZ", mlir::CompilerTarget::GateKind::RZ) + .value("RX", mlir::CompilerTarget::GateKind::RX) + .value("RY", mlir::CompilerTarget::GateKind::RY) + .value("R", mlir::CompilerTarget::GateKind::R) + .value("RXX", mlir::CompilerTarget::GateKind::RXX) + .value("RYY", mlir::CompilerTarget::GateKind::RYY) + .value("RZX", mlir::CompilerTarget::GateKind::RZX) + .value("RZZ", mlir::CompilerTarget::GateKind::RZZ) + .value("ISWAP", mlir::CompilerTarget::GateKind::ISWAP) + .value("CZ", mlir::CompilerTarget::GateKind::CZ) + .value("CX", mlir::CompilerTarget::GateKind::CX) + .value("ECR", mlir::CompilerTarget::GateKind::ECR); + + nb::enum_( + compilerTarget, "SingleQubitBasis", + "Recognized target-wide single-qubit synthesis basis.") + .value("U", mlir::CompilerTarget::SingleQubitBasis::U) + .value("ZSXX", mlir::CompilerTarget::SingleQubitBasis::ZSXX) + .value("R", mlir::CompilerTarget::SingleQubitBasis::R) + .value("XZX", mlir::CompilerTarget::SingleQubitBasis::XZX) + .value("XYX", mlir::CompilerTarget::SingleQubitBasis::XYX) + .value("ZYZ", mlir::CompilerTarget::SingleQubitBasis::ZYZ) + .value("ZXZ", mlir::CompilerTarget::SingleQubitBasis::ZXZ); + + auto synthesisBasis = nb::class_( + compilerTarget, "SynthesisBasis", + "One synthesis basis usable across the complete target."); + synthesisBasis + .def_ro("single_qubit", + &mlir::CompilerTarget::SynthesisBasis::singleQubit, + "The single-qubit synthesis basis.") + .def_ro("entangler", &mlir::CompilerTarget::SynthesisBasis::entangler, + "The two-qubit entangler."); + + compilerTarget + .def(nb::init>, + std::optional>, + std::optional>(), + "num_qubits"_a, nb::kw_only(), "couplings"_a = nb::none(), + "operations"_a = nb::none(), "duration_unit"_a = nb::none()) + .def(nb::init>, + std::optional>, + std::optional>(), + "name"_a, "num_qubits"_a, nb::kw_only(), "couplings"_a = nb::none(), + "operations"_a = nb::none(), "duration_unit"_a = nb::none()) + .def(nb::init, + std::optional>, + std::optional>, + std::optional>(), + "sites"_a, nb::kw_only(), "couplings"_a = nb::none(), + "operations"_a = nb::none(), "duration_unit"_a = nb::none()) + .def(nb::init, + std::optional>, + std::optional>, + std::optional>(), + "name"_a, "sites"_a, nb::kw_only(), "couplings"_a = nb::none(), + "operations"_a = nb::none(), "duration_unit"_a = nb::none()) + .def_static("from_device", &mlir::compilerTargetFromDevice, "device"_a, + "Snapshot a circuit-model QDMI device.") + .def_prop_ro( + "name", + [](const mlir::CompilerTarget& target) { + const auto name = target.name(); + return name ? std::optional(name->str()) + : std::nullopt; + }, + "The target name, if available.") + .def_prop_ro("duration_unit", &mlir::CompilerTarget::durationUnit, + "The target timing unit, if available.") + .def_prop_ro("num_qubits", &mlir::CompilerTarget::numQubits, + "The number of target sites.") + .def_prop_ro( + "sites", + [](const mlir::CompilerTarget& target) { + return std::vector( + target.sites().begin(), target.sites().end()); + }, + "Detailed sites in compiler-vertex order.") + .def_prop_ro("has_explicit_topology", + &mlir::CompilerTarget::hasExplicitTopology, + "Whether the target defines a coupling topology.") + .def_prop_ro( + "couplings", + [](const mlir::CompilerTarget& target) { + return std::vector( + target.couplings().begin(), target.couplings().end()); + }, + "Canonical undirected couplings in target site IDs.") + .def_prop_ro("has_explicit_operations", + &mlir::CompilerTarget::hasExplicitOperations, + "Whether the target defines an operation set.") + .def_prop_ro( + "operations", + [](const mlir::CompilerTarget& target) { + return std::vector( + target.operations().begin(), target.operations().end()); + }, + "Operation capabilities in reported order.") + .def_prop_ro( + "supported_gates", + [](const mlir::CompilerTarget& target) { + return std::vector( + target.supportedGates().begin(), target.supportedGates().end()); + }, + "Recognized native gates supported by the target.") + .def_prop_ro("synthesis_basis", &mlir::CompilerTarget::synthesisBasis, + "A complete target-wide synthesis basis, if available.") + .def( + "supports_operation", + [](const mlir::CompilerTarget& target, const std::string_view name, + const size_t numQubits, + const std::optional numParameters) { + return target.supportsOperation(name, numQubits, numParameters); + }, + "name"_a, "num_qubits"_a, "num_parameters"_a = nb::none(), + "Whether the target supports an operation capability."); + auto program = nb::class_( m, "Program", R"pb(Base class for a typed MLIR compiler program. @@ -412,6 +656,11 @@ operations.)pb"); "Decompose controlled X/Z/SWAP gates, qco.rccx, and constant-angle " "phase gates that act on at least min_qubits qubits (min_qubits " "must be at least 3; default 3 means wider than two-qubit).") + .def("compile_for_target", + &BooleanMemberAdapter<&mlir::QCOProgram::compileForTarget>::call, + "target"_a, nb::kw_only(), "enable_timing"_a = false, + "enable_statistics"_a = false, + "Compile this QCO program for the target in place.") .def( "to_qc", [](mlir::QCOProgram& value, const bool copy) { @@ -506,8 +755,8 @@ LLVM bitcode.)pb"); m.def("compile_program", &compileProgram, "program"_a, nb::kw_only(), "output"_a = mlir::ProgramFormat::QC, "inplace"_a = false, - "qco_pipeline"_a = "mqt-qco-default", "enable_timing"_a = false, - "enable_statistics"_a = false, + "target"_a = nb::none(), "qco_pipeline"_a = "mqt-qco-default", + "enable_timing"_a = false, "enable_statistics"_a = false, R"pb( Run the coordinated default MQT compiler pipeline. @@ -521,7 +770,10 @@ Use the typed programs directly to construct a custom pipeline stage by stage. program: Source text, a file path, a circuit, or a typed compiler program. output: The requested output stage of the compiler pipeline. inplace: Whether a typed input program may be consumed. - qco_pipeline: The QCO optimization pipeline to run. + target: An optional compiler target for decomposition, mapping, and native + synthesis. A target requires optimized QCO, QC, or QIR output. + qco_pipeline: The QCO optimization pipeline to run. A custom pipeline + cannot be combined with a target. enable_timing: Whether to collect pass timing information. enable_statistics: Whether to collect pass statistics. diff --git a/bindings/patterns.txt b/bindings/patterns.txt index 5ee2e8895e..2353c2f318 100644 --- a/bindings/patterns.txt +++ b/bindings/patterns.txt @@ -86,6 +86,48 @@ mqt\.core\.fomac\.Device\.Operation\.query_custom_property$: ) -> str | bool | int | float | bytes | None: \doc +mqt\.core\.mlir\.CompilerTarget\.__init__$: + \from collections.abc import Sequence + \from typing import overload + @overload + def __init__( + self, + num_qubits: int, + *, + couplings: Sequence[tuple[int, int]] | None = None, + operations: Sequence[CompilerTarget.Operation] | None = None, + duration_unit: CompilerTarget.DurationUnit | None = None, + ) -> None: ... + @overload + def __init__( + self, + name: str, + num_qubits: int, + *, + couplings: Sequence[tuple[int, int]] | None = None, + operations: Sequence[CompilerTarget.Operation] | None = None, + duration_unit: CompilerTarget.DurationUnit | None = None, + ) -> None: ... + @overload + def __init__( + self, + sites: Sequence[CompilerTarget.Site], + *, + couplings: Sequence[tuple[int, int]] | None = None, + operations: Sequence[CompilerTarget.Operation] | None = None, + duration_unit: CompilerTarget.DurationUnit | None = None, + ) -> None: ... + @overload + def __init__( + self, + name: str, + sites: Sequence[CompilerTarget.Site], + *, + couplings: Sequence[tuple[int, int]] | None = None, + operations: Sequence[CompilerTarget.Operation] | None = None, + duration_unit: CompilerTarget.DurationUnit | None = None, + ) -> None: ... + mqt.core.mlir.compile_program: \from typing import overload, Literal @overload @@ -100,6 +142,7 @@ mqt.core.mlir.compile_program: *, output: Literal[OutputFormat.QC, OutputFormat.QC_IMPORT] = ..., inplace: bool = False, + target: CompilerTarget | None = None, qco_pipeline: str = "mqt-qco-default", enable_timing: bool = False, enable_statistics: bool = False, @@ -116,6 +159,7 @@ mqt.core.mlir.compile_program: *, output: Literal[OutputFormat.QCO, OutputFormat.QCO_OPTIMIZED], inplace: bool = False, + target: CompilerTarget | None = None, qco_pipeline: str = "mqt-qco-default", enable_timing: bool = False, enable_statistics: bool = False, @@ -132,6 +176,7 @@ mqt.core.mlir.compile_program: *, output: Literal[OutputFormat.JEFF], inplace: bool = False, + target: CompilerTarget | None = None, qco_pipeline: str = "mqt-qco-default", enable_timing: bool = False, enable_statistics: bool = False, @@ -148,6 +193,7 @@ mqt.core.mlir.compile_program: *, output: Literal[OutputFormat.QIR_BASE, OutputFormat.QIR_ADAPTIVE], inplace: bool = False, + target: CompilerTarget | None = None, qco_pipeline: str = "mqt-qco-default", enable_timing: bool = False, enable_statistics: bool = False, @@ -164,6 +210,7 @@ mqt.core.mlir.compile_program: *, output: OutputFormat, inplace: bool = False, + target: CompilerTarget | None = None, qco_pipeline: str = "mqt-qco-default", enable_timing: bool = False, enable_statistics: bool = False, diff --git a/docs/mlir/index.md b/docs/mlir/index.md index 2a376d333c..bffbd86ed1 100644 --- a/docs/mlir/index.md +++ b/docs/mlir/index.md @@ -5,7 +5,9 @@ quantum-classical compilation framework built on the Multi-Level Intermediate Representation (MLIR). For an overview, see {cite:p}`MQTCompilerCollection2026`. The {doc}`Python compiler guide ` describes how to -compile and inspect quantum programs from Python. The remaining pages are the +compile and inspect quantum programs from Python. The +{doc}`target-compilation guide ` shows how to compile for +QDMI devices from Python, C++, and `mqt-cc`. The remaining pages are the technical reference for the underlying MLIR infrastructure. We define multiple dialects, each with its dedicated purpose: @@ -29,6 +31,7 @@ interoperability, we provide {doc}`conversions ` between dialects. :maxdepth: 2 python_compiler_collection +target_compilation QC QCO QTensor diff --git a/docs/mlir/python_compiler_collection.md b/docs/mlir/python_compiler_collection.md index 9dc67f60ac..e1c576b6fc 100644 --- a/docs/mlir/python_compiler_collection.md +++ b/docs/mlir/python_compiler_collection.md @@ -21,6 +21,9 @@ Install {doc}`MQT Core <../installation>` and import the compiler interface: from mqt.core.mlir import OutputFormat, QCProgram, QIRProfile, compile_program ``` +To compile for a configured QDMI device, see +{doc}`target compilation `. + ## Compile an OpenQASM program The following OpenQASM program prepares a Bell state and records the outcome of diff --git a/docs/mlir/target_compilation.md b/docs/mlir/target_compilation.md new file mode 100644 index 0000000000..c153c766dd --- /dev/null +++ b/docs/mlir/target_compilation.md @@ -0,0 +1,102 @@ +# Compile for a QDMI device + +An MLIR {code}`mlir::CompilerTarget` is an immutable snapshot of a circuit-model +device. It contains the device sites, topology, native operations, and available +calibration data. Compilation decomposes supported multi-qubit operations, +optimizes and maps the program, synthesizes native gates, and verifies that the +result conforms to the target. + +The snapshot is independent of its originating QDMI session. It can therefore be +stored, copied cheaply, and reused for multiple compilations. + +## Python + +Open a configured QDMI device and snapshot it as a compiler target: + +```python +from mqt.core.fomac import open_device +from mqt.core.mlir import CompilerTarget, OutputFormat, compile_program + +target = CompilerTarget.from_device(open_device("mqt.sc.iqm.garnet")) +compiled = compile_program( + "bell.qasm", + target=target, + output=OutputFormat.QCO_OPTIMIZED, +) +``` + +Target compilation accepts optimized QCO, QC, or QIR output and uses the +canonical QCO pipeline; it cannot be combined with a custom `qco_pipeline`. + +The target can also be constructed directly. Omitting `couplings` selects +all-to-all connectivity; omitting `operations` means that every operation is +native: + +```python +target = CompilerTarget(3, couplings=[(0, 1), (1, 2)]) +``` + +Use {py:meth}`~mqt.core.mlir.QCOProgram.compile_for_target` to apply target +compilation to an existing QCO program. For pass-level benchmarking, the C++ API +exposes separate factories for pre-routing optimization, mapping, native +synthesis, and conformance verification. + +## Command line from a source build + +List the stable IDs of configured QDMI devices: + +```console +mqt-cc --qdmi-list-devices +``` + +Select a device when compiling: + +```console +mqt-cc --qdmi-device=mqt.sc.iqm.garnet \ + --emit=qco-optimized input.qasm +``` + +An explicit registry file can be selected before device discovery: + +```console +mqt-cc --qdmi-config=/path/to/qdmi.json \ + --qdmi-device=example.device input.qasm +``` + +Target compilation produces optimized QCO, QC, or QIR. It cannot be combined +with a custom `--passes` pipeline because the canonical target pipeline owns the +required pass ordering. + +## C++ source-tree API + +The source build provides a narrow FoMaC bridge between a live QDMI device and +the compiler-owned target: + +```cpp +#include "fomac/FoMaC.hpp" +#include "mlir/Compiler/FoMaCAdapter.h" +#include "mlir/Compiler/Programs.h" + +auto device = fomac::Session::openDevice("mqt.sc.iqm.garnet"); +auto target = mlir::compilerTargetFromDevice(device); + +auto qc = mlir::QCProgram::fromQASMFile("input.qasm"); +if (!qc) { + return 1; +} +auto qco = std::move(*qc).intoQCO(); +if (!qco || !qco->compileForTarget(target)) { + return 1; +} +``` + +The adapter accepts circuit-model devices. Neutral-atom zone models require a +different compilation model and are rejected with a diagnostic. + +The bundled Garnet and Emerald snapshots contain available T1, T2, and fidelity +data. Operation durations are absent because they were unavailable. See +{doc}`../qdmi/sc_device` for their stable IDs and {doc}`../qdmi/configuration` +for registry configuration. + +If the program should use fewer physical qubits, run the {code}`mqt-qubit-reuse` +pipeline before target compilation. diff --git a/mlir/include/mlir/Compiler/FoMaCAdapter.h b/mlir/include/mlir/Compiler/FoMaCAdapter.h new file mode 100644 index 0000000000..32f1189b4e --- /dev/null +++ b/mlir/include/mlir/Compiler/FoMaCAdapter.h @@ -0,0 +1,32 @@ +/* + * 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 "mlir/Compiler/Target.h" + +namespace fomac { +class Device; +} // namespace fomac + +namespace mlir { + +/** + * @brief Snapshot a circuit-model QDMI device as an MLIR compiler target. + * + * @details The returned target owns all queried metadata and remains valid + * after the originating device and session have been destroyed. Neutral-atom + * zone models and site-dependent operation support are not supported by the + * circuit-model compiler pipeline. + */ +[[nodiscard]] CompilerTarget +compilerTargetFromDevice(const fomac::Device& device); + +} // namespace mlir diff --git a/mlir/include/mlir/Compiler/TargetCompilation.h b/mlir/include/mlir/Compiler/TargetCompilation.h index 79c614677b..4a03c81fb2 100644 --- a/mlir/include/mlir/Compiler/TargetCompilation.h +++ b/mlir/include/mlir/Compiler/TargetCompilation.h @@ -20,8 +20,8 @@ class OpPassManager; * * @details Decomposes supported multi-controlled gates, performs * target-independent optimization, maps to the target topology, synthesizes - * native operations, verifies target conformance, and cleans up the resulting - * QCO program. + * native operations, performs a final local cleanup, and verifies target + * conformance. */ void populateTargetCompilationPipeline(OpPassManager& pm, const CompilerTarget& target); diff --git a/mlir/lib/Compiler/CMakeLists.txt b/mlir/lib/Compiler/CMakeLists.txt index 834a0ec5c6..ae2f29de70 100644 --- a/mlir/lib/Compiler/CMakeLists.txt +++ b/mlir/lib/Compiler/CMakeLists.txt @@ -23,6 +23,24 @@ mqt_mlir_target_use_project_options(MQTCompilerTarget) target_sources(MQTCompilerTarget PUBLIC FILE_SET HEADERS BASE_DIRS ${MQT_MLIR_SOURCE_INCLUDE_DIR} FILES ${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Compiler/Target.h) +# Build the optional FoMaC-to-compiler-target adapter +add_mlir_library( + MQTCompilerFoMaCAdapter + PARTIAL_SOURCES_INTENDED + FoMaCAdapter.cpp + ADDITIONAL_HEADER_DIRS + ${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Compiler + LINK_LIBS + PUBLIC + MQTCompilerTarget + MQT::CoreFoMaC) + +mqt_mlir_target_use_project_options(MQTCompilerFoMaCAdapter) + +target_sources( + MQTCompilerFoMaCAdapter PUBLIC FILE_SET HEADERS BASE_DIRS ${MQT_MLIR_SOURCE_INCLUDE_DIR} FILES + ${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Compiler/FoMaCAdapter.h) + # Build the compiler pipeline library add_mlir_library( MQTCompilerPipeline @@ -58,7 +76,7 @@ mqt_mlir_target_use_project_options(MQTCompilerPipeline) # collect header files file(GLOB_RECURSE COMPILER_HEADERS_SOURCE "${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Compiler/*.h") -list(FILTER COMPILER_HEADERS_SOURCE EXCLUDE REGEX "/Target\\.h$") +list(FILTER COMPILER_HEADERS_SOURCE EXCLUDE REGEX "/(FoMaCAdapter|Target)\\.h$") target_sources(MQTCompilerPipeline PUBLIC FILE_SET HEADERS BASE_DIRS ${MQT_MLIR_SOURCE_INCLUDE_DIR} FILES ${COMPILER_HEADERS_SOURCE}) diff --git a/mlir/lib/Compiler/FoMaCAdapter.cpp b/mlir/lib/Compiler/FoMaCAdapter.cpp new file mode 100644 index 0000000000..d7b04688fb --- /dev/null +++ b/mlir/lib/Compiler/FoMaCAdapter.cpp @@ -0,0 +1,287 @@ +/* + * 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 "mlir/Compiler/FoMaCAdapter.h" + +#include "fomac/FoMaC.hpp" +#include "mlir/Compiler/Target.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mlir { + +[[nodiscard]] static CompilerTarget::SiteId checkedSiteId(const size_t index) { + if (index > static_cast( + std::numeric_limits::max())) { + throw std::invalid_argument( + "QDMI site index exceeds the nonnegative i64 compiler-target domain"); + } + return static_cast(index); +} + +[[noreturn]] static void +rejectNonCircuitDevice(const llvm::StringRef deviceName, + const llvm::StringRef detail) { + throw std::invalid_argument( + (std::string("QDMI device '") + deviceName.str() + + "' cannot be used as an MLIR compiler target: only circuit-model " + "devices with one qubit per non-zone site are supported (" + + detail.str() + ")")); +} + +[[noreturn]] static void +rejectNonHomogeneousOperation(const llvm::StringRef deviceName, + const llvm::StringRef operationName, + const llvm::StringRef detail) { + throw std::invalid_argument( + (std::string("QDMI device '") + deviceName.str() + "' operation '" + + operationName.str() + + "' cannot be represented by the MLIR compiler target: operation " + "support must be homogeneous across the device (" + + detail.str() + ")")); +} + +[[nodiscard]] static CompilerTarget::Coupling +canonicalCoupling(const CompilerTarget::SiteId first, + const CompilerTarget::SiteId second) { + if (first < second) { + return {first, second}; + } + return {second, first}; +} + +[[nodiscard]] static std::optional +allToAllCouplingCount(const size_t numSites) { + if (numSites < 2) { + return 0; + } + const auto first = numSites % 2 == 0 ? numSites / 2 : numSites; + const auto second = numSites % 2 == 0 ? numSites - 1 : (numSites - 1) / 2; + if (first > std::numeric_limits::max() / second) { + return std::nullopt; + } + return first * second; +} + +static void validateHomogeneousSupport( + const fomac::Operation& operation, const size_t arity, + const std::optional>& flattenedSites, + const std::vector& deviceSites, + const std::optional>& couplings, + const llvm::StringRef deviceName) { + if (!flattenedSites) { + return; + } + const auto operationName = operation.getName(); + if (flattenedSites->size() % arity != 0) { + rejectNonHomogeneousOperation( + deviceName, operationName, + "the reported site list is not divisible by the fixed arity"); + } + if (arity > 2) { + rejectNonHomogeneousOperation( + deviceName, operationName, + "explicit site lists are supported only for one- and two-qubit " + "operations"); + } + + llvm::DenseSet knownSites; + knownSites.reserve(deviceSites.size()); + for (const auto& site : deviceSites) { + knownSites.insert(site.id()); + } + + if (arity == 1) { + llvm::DenseSet supportedSites; + supportedSites.reserve(flattenedSites->size()); + for (const auto& site : *flattenedSites) { + const auto siteId = checkedSiteId(site.getIndex()); + if (!knownSites.contains(siteId) || + !supportedSites.insert(siteId).second) { + rejectNonHomogeneousOperation( + deviceName, operationName, + "the reported one-qubit sites must be unique device sites"); + } + } + if (supportedSites.size() != knownSites.size()) { + rejectNonHomogeneousOperation( + deviceName, operationName, + "the operation is not available on every device site"); + } + return; + } + + llvm::DenseSet reportedTuples; + llvm::DenseSet supportedCouplings; + reportedTuples.reserve(flattenedSites->size() / arity); + supportedCouplings.reserve(flattenedSites->size() / arity); + for (size_t offset = 0; offset < flattenedSites->size(); offset += arity) { + const auto first = checkedSiteId((*flattenedSites)[offset].getIndex()); + const auto second = checkedSiteId((*flattenedSites)[offset + 1].getIndex()); + if (first == second || !knownSites.contains(first) || + !knownSites.contains(second) || + !reportedTuples.insert({first, second}).second) { + rejectNonHomogeneousOperation( + deviceName, operationName, + "the reported two-qubit sites must be unique pairs of device sites"); + } + supportedCouplings.insert(canonicalCoupling(first, second)); + } + + if (!couplings) { + const auto expected = allToAllCouplingCount(knownSites.size()); + if (!expected || supportedCouplings.size() != *expected) { + rejectNonHomogeneousOperation( + deviceName, operationName, + "the operation is not available on every all-to-all site pair"); + } + return; + } + + llvm::DenseSet expectedCouplings; + expectedCouplings.reserve(couplings->size()); + for (const auto& [first, second] : *couplings) { + expectedCouplings.insert(canonicalCoupling(first, second)); + } + if (supportedCouplings.size() != expectedCouplings.size() || + !std::ranges::all_of(expectedCouplings, [&](const auto& coupling) { + return supportedCouplings.contains(coupling); + })) { + rejectNonHomogeneousOperation( + deviceName, operationName, + "the operation is not available on every topology edge"); + } +} + +[[nodiscard]] static std::optional +snapshotDurationUnit(const fomac::Device& device) { + auto unit = device.getDurationUnit(); + const auto scaleFactor = device.getDurationScaleFactor(); + if (!unit) { + if (scaleFactor) { + throw std::invalid_argument( + "QDMI device reports a duration scale factor without a duration " + "unit"); + } + return std::nullopt; + } + return CompilerTarget::DurationUnit(std::move(*unit), + scaleFactor.value_or(1.)); +} + +[[nodiscard]] static std::vector snapshotSiteTuples( + const fomac::Operation& operation, const size_t arity, + const std::optional>& flattenedSites, + const std::optional defaultDuration, + const std::optional defaultFidelity) { + if (!flattenedSites) { + return {}; + } + + std::vector siteTuples; + siteTuples.reserve(flattenedSites->size() / arity); + for (size_t offset = 0; offset < flattenedSites->size(); offset += arity) { + std::vector sites; + std::vector siteIds; + sites.reserve(arity); + siteIds.reserve(arity); + for (size_t index = 0; index < arity; ++index) { + const auto& site = (*flattenedSites)[offset + index]; + sites.emplace_back(site); + siteIds.emplace_back(checkedSiteId(site.getIndex())); + } + + const auto duration = operation.getDuration(sites); + const auto fidelity = operation.getFidelity(sites); + if (duration != defaultDuration || fidelity != defaultFidelity) { + siteTuples.emplace_back(std::move(siteIds), duration, fidelity); + } + } + return siteTuples; +} + +[[nodiscard]] static std::vector snapshotOperations( + const std::vector& operations, + const std::vector& deviceSites, + const std::optional>& couplings, + const llvm::StringRef deviceName) { + std::vector targetOperations; + targetOperations.reserve(operations.size()); + for (const auto& operation : operations) { + if (operation.isZoned()) { + rejectNonCircuitDevice(deviceName, + "the device exposes a zoned operation"); + } + const auto arity = operation.getQubitsNum(); + if (!arity || *arity == 0) { + continue; + } + const auto flattenedSites = operation.getSites(); + validateHomogeneousSupport(operation, *arity, flattenedSites, deviceSites, + couplings, deviceName); + const auto duration = operation.getDuration(); + const auto fidelity = operation.getFidelity(); + targetOperations.emplace_back( + operation.getName(), *arity, operation.getParametersNum(), + snapshotSiteTuples(operation, *arity, flattenedSites, duration, + fidelity), + duration, fidelity); + } + return targetOperations; +} + +CompilerTarget compilerTargetFromDevice(const fomac::Device& device) { + auto deviceName = device.getName(); + const auto deviceSites = device.getSites(); + if (std::ranges::any_of(deviceSites, + [](const auto& site) { return site.isZone(); })) { + rejectNonCircuitDevice(deviceName, "the device exposes zone sites"); + } + if (device.getQubitsNum() != deviceSites.size()) { + rejectNonCircuitDevice( + deviceName, "the qubit count does not match the regular-site count"); + } + + std::vector sites; + sites.reserve(deviceSites.size()); + for (const auto& site : deviceSites) { + sites.emplace_back(checkedSiteId(site.getIndex()), site.getName(), + site.getT1(), site.getT2()); + } + + std::optional> couplings; + if (const auto deviceCouplings = device.getCouplingMap()) { + couplings.emplace(); + couplings->reserve(deviceCouplings->size()); + for (const auto& [source, target] : *deviceCouplings) { + couplings->emplace_back(checkedSiteId(source.getIndex()), + checkedSiteId(target.getIndex())); + } + } + + auto operations = + snapshotOperations(device.getOperations(), sites, couplings, deviceName); + auto durationUnit = snapshotDurationUnit(device); + return {std::move(deviceName), std::move(sites), std::move(couplings), + std::move(operations), std::move(durationUnit)}; +} + +} // namespace mlir diff --git a/mlir/lib/Compiler/TargetCompilation.cpp b/mlir/lib/Compiler/TargetCompilation.cpp index e68b5bf69e..898e0a7d93 100644 --- a/mlir/lib/Compiler/TargetCompilation.cpp +++ b/mlir/lib/Compiler/TargetCompilation.cpp @@ -16,6 +16,7 @@ #include "mlir/Support/Passes.h" #include +#include namespace mlir { @@ -25,9 +26,11 @@ void populateTargetCompilationPipeline(OpPassManager& pm, populateDefaultQCOOptimizationPipeline(pm); pm.addPass(qco::createFuseTwoQubitGates()); pm.addPass(qco::createMappingPass(target, qco::MappingPassOptions{})); + populateQCOCleanupPipeline(pm); pm.addPass(qco::createTargetNativeSynthesis(target)); + pm.addPass(createCSEPass()); + pm.addPass(createRemoveDeadValuesPass()); pm.addPass(qco::createVerifyTargetConformance(target)); - populateQCOCleanupPipeline(pm); } } // namespace mlir diff --git a/mlir/tools/mqt-cc/CMakeLists.txt b/mlir/tools/mqt-cc/CMakeLists.txt index bb241b0273..30fa53d892 100644 --- a/mlir/tools/mqt-cc/CMakeLists.txt +++ b/mlir/tools/mqt-cc/CMakeLists.txt @@ -7,11 +7,12 @@ # Licensed under the MIT License # Build the compiler driver executable -add_mlir_tool(mqt-cc mqt-cc.cpp DEPENDS MQTCompilerPipeline SUPPORT_PLUGINS) +add_mlir_tool(mqt-cc mqt-cc.cpp DEPENDS MQTCompilerPipeline MQTCompilerFoMaCAdapter SUPPORT_PLUGINS) llvm_map_components_to_libnames(llvm_native_libs bitwriter) target_link_libraries( mqt-cc - PRIVATE MQTCompilerPipeline + PRIVATE MQTCompilerFoMaCAdapter + MQTCompilerPipeline MLIRParser MLIRSupport MLIRQCTranslation @@ -25,6 +26,12 @@ target_link_libraries( ${llvm_native_libs}) mqt_mlir_target_use_project_options(mqt-cc) + +mqt_get_qdmi_device_targets(qdmi_device_targets) +if(qdmi_device_targets) + mqt_copy_qdmi_runtime(mqt-cc ${qdmi_device_targets}) +endif() + llvm_update_compile_flags(mqt-cc) mlir_check_all_link_libraries(mqt-cc) export_executable_symbols_for_plugins(mqt-cc) diff --git a/mlir/tools/mqt-cc/mqt-cc.cpp b/mlir/tools/mqt-cc/mqt-cc.cpp index 5e115a1bc2..78d643a483 100644 --- a/mlir/tools/mqt-cc/mqt-cc.cpp +++ b/mlir/tools/mqt-cc/mqt-cc.cpp @@ -8,6 +8,9 @@ * Licensed under the MIT License */ +#include "fomac/FoMaC.hpp" +#include "mlir/Compiler/FoMaCAdapter.h" +#include "mlir/Compiler/TargetCompilation.h" #include "mlir/Conversion/JeffToQCO/JeffToQCO.h" #include "mlir/Conversion/QCOToJeff/QCOToJeff.h" #include "mlir/Conversion/QCOToQC/QCOToQC.h" @@ -19,6 +22,7 @@ #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" #include "mlir/Support/Passes.h" +#include "qdmi/driver/Driver.hpp" #include #include @@ -53,6 +57,7 @@ #include #include +#include #include #include #include @@ -84,6 +89,21 @@ static llvm::cl::opt outputFormat( "qir-adaptive, or jeff"), llvm::cl::value_desc("format"), llvm::cl::init("mlir")); +static llvm::cl::opt + qdmiListDevices("qdmi-list-devices", + llvm::cl::desc("List configured QDMI device IDs and exit"), + llvm::cl::init(false)); + +static llvm::cl::opt qdmiDevice( + "qdmi-device", + llvm::cl::desc("Compile for the QDMI device with this stable ID"), + llvm::cl::value_desc("id"), llvm::cl::init("")); + +static llvm::cl::opt qdmiConfig( + "qdmi-config", + llvm::cl::desc("Use an explicit QDMI registry configuration file"), + llvm::cl::value_desc("registry.json"), llvm::cl::init("")); + namespace { enum class InputFormat : std::uint8_t { MLIR, QASM, Jeff }; enum class InputDialect : std::uint8_t { QC, QCO }; @@ -192,6 +212,27 @@ static llvm::cl::opt decomposeMultiControlledMinQubits( "narrower gates undecomposed."), llvm::cl::init(3)); +/** + * @brief Configure the QDMI registry before initializing its singleton. + */ +static LogicalResult configureQDMIRegistry(const StringRef path) { +#ifdef _WIN32 + const auto status = + _putenv_s("MQT_CORE_QDMI_CONFIG_FILE", path.str().c_str()); +#else + // NOLINTBEGIN(misc-include-cleaner) + const auto status = + setenv("MQT_CORE_QDMI_CONFIG_FILE", path.str().c_str(), 1); + // NOLINTEND(misc-include-cleaner) +#endif + if (status == 0) { + return success(); + } + llvm::errs() << "Failed to configure the QDMI registry from '" << path + << "'.\n"; + return failure(); +} + /** * @brief Load and parse a `.qasm` file */ @@ -335,6 +376,26 @@ static int runCompiler(int argc, char** argv) { llvm::cl::ParseCommandLineOptions(argc, argv, "MQT Compiler Collection Driver\n"); + if (!qdmiConfig.empty() && configureQDMIRegistry(qdmiConfig).failed()) { + return 1; + } + if (qdmiListDevices) { + if (!qdmiDevice.empty()) { + llvm::errs() + << "--qdmi-list-devices cannot be combined with --qdmi-device.\n"; + return 1; + } + for (const auto& id : qdmi::Driver::get().registeredDeviceIds()) { + llvm::outs() << id << "\n"; + } + return 0; + } + if (!qdmiConfig.empty() && qdmiDevice.empty()) { + llvm::errs() + << "--qdmi-config requires --qdmi-device or --qdmi-list-devices.\n"; + return 1; + } + const auto parsedInputFormat = parseInputFormat(inputFormat, inputFilename); if (!parsedInputFormat) { llvm::errs() << "Could not determine the input format for '" @@ -347,6 +408,30 @@ static int runCompiler(int argc, char** argv) { return 1; } + std::optional compilerTarget; + if (!qdmiDevice.empty()) { + if (*parsedOutputFormat == OutputFormat::QCImport || + *parsedOutputFormat == OutputFormat::QCO || + *parsedOutputFormat == OutputFormat::Jeff) { + llvm::errs() + << "--qdmi-device requires qco-optimized, qc/mlir, qir-base, or " + "qir-adaptive output.\n"; + return 1; + } + if (passPipeline.hasAnyOccurrences()) { + llvm::errs() << "--qdmi-device cannot be combined with --passes.\n"; + return 1; + } + if (enableDecomposeMultiControlled) { + llvm::errs() << "--qdmi-device cannot be combined with " + "--decompose-multi-controlled; target compilation " + "already performs the required decomposition.\n"; + return 1; + } + const auto device = fomac::Session::openDevice(qdmiDevice); + compilerTarget.emplace(compilerTargetFromDevice(device)); + } + // Set up MLIR context with all required dialects DialectRegistry registry; registry @@ -422,6 +507,10 @@ static int runCompiler(int argc, char** argv) { if (*parsedOutputFormat != OutputFormat::QCImport && *parsedOutputFormat != OutputFormat::QCO) { if (failed(runPasses([&](OpPassManager& pm) { + if (compilerTarget) { + populateTargetCompilationPipeline(pm, *compilerTarget); + return success(); + } populateQCOCleanupPipeline(pm); if (passPipeline.hasAnyOccurrences()) { if (failed(passPipeline.addToPipeline(pm, [](const Twine& message) { diff --git a/mlir/unittests/Compiler/CMakeLists.txt b/mlir/unittests/Compiler/CMakeLists.txt index 0fdafac389..d319ce031a 100644 --- a/mlir/unittests/Compiler/CMakeLists.txt +++ b/mlir/unittests/Compiler/CMakeLists.txt @@ -6,11 +6,21 @@ # # Licensed under the MIT License -add_executable(mqt-core-mlir-unittests-compiler test_compiler_pipeline.cpp test_compiler_target.cpp) +set(compiler_test_sources test_compiler_pipeline.cpp test_compiler_target.cpp) +set(has_compiler_qdmi_provider_tests OFF) +if(TARGET MQT::CoreQDMIScDevice + AND TARGET MQT::CoreQDMI_DDSIM_Device + AND TARGET MQT::CoreQDMINaDevice) + list(APPEND compiler_test_sources test_compiler_fomac_adapter.cpp) + set(has_compiler_qdmi_provider_tests ON) +endif() + +add_executable(mqt-core-mlir-unittests-compiler ${compiler_test_sources}) target_link_libraries( mqt-core-mlir-unittests-compiler PRIVATE GTest::gtest_main + MQTCompilerFoMaCAdapter MQTCompilerTarget MQTCompilerPipeline MLIRQCTranslation @@ -25,7 +35,51 @@ target_link_libraries( MLIRQuantumComputationPrograms MQT::CoreIR) +if(has_compiler_qdmi_provider_tests) + mqt_copy_qdmi_runtime(mqt-core-mlir-unittests-compiler MQT::CoreQDMIScDevice + MQT::CoreQDMI_DDSIM_Device MQT::CoreQDMINaDevice) + target_compile_definitions( + mqt-core-mlir-unittests-compiler + PRIVATE + MQT_CORE_MLIR_HETEROGENEOUS_SC_CONFIG="${CMAKE_CURRENT_SOURCE_DIR}/Inputs/heterogeneous-sc.json" + ) +endif() + mqt_mlir_configure_unittest_target(mqt-core-mlir-unittests-compiler) gtest_discover_tests(mqt-core-mlir-unittests-compiler PROPERTIES LABELS mqt-mlir-unittests DISCOVERY_TIMEOUT 60) + +if(TARGET MQT::CoreQDMIScDevice) + # `mqt-cc` is normally an optional LLVM tool. Build it only as a dependency of the test target + # when its irreducible subprocess tests are enabled. + add_dependencies(mqt-core-mlir-unittests-compiler mqt-cc) + + set(mqt_cc_test_input ${CMAKE_CURRENT_SOURCE_DIR}/Inputs/bell.qasm) + add_test(NAME mqt-cc-qdmi-list COMMAND $ --qdmi-list-devices) + set_tests_properties(mqt-cc-qdmi-list PROPERTIES LABELS mqt-mlir-unittests + PASS_REGULAR_EXPRESSION "mqt.sc.iqm.garnet") + + add_test( + NAME mqt-cc-qdmi-explicit-config + COMMAND + $ + "--qdmi-config=${CMAKE_CURRENT_SOURCE_DIR}/Inputs/mqt-cc-explicit.qdmi.json" + --qdmi-list-devices) + set_tests_properties( + mqt-cc-qdmi-explicit-config + PROPERTIES LABELS mqt-mlir-unittests PASS_REGULAR_EXPRESSION "mqt.sc.iqm.emerald" + FAIL_REGULAR_EXPRESSION "mqt.sc.iqm.garnet") + + add_test(NAME mqt-cc-qdmi-unknown COMMAND $ --qdmi-device=unknown + ${mqt_cc_test_input}) + set_tests_properties( + mqt-cc-qdmi-unknown PROPERTIES LABELS mqt-mlir-unittests PASS_REGULAR_EXPRESSION + "mqt-cc failed: Unknown QDMI device ID 'unknown'") + + add_test(NAME mqt-cc-qdmi-garnet COMMAND $ --qdmi-device=mqt.sc.iqm.garnet + --emit=qco-optimized ${mqt_cc_test_input}) + set_tests_properties( + mqt-cc-qdmi-garnet PROPERTIES LABELS mqt-mlir-unittests PASS_REGULAR_EXPRESSION "qco.r" + FAIL_REGULAR_EXPRESSION "qco.rx|qco.ry") +endif() diff --git a/mlir/unittests/Compiler/Inputs/bell.qasm b/mlir/unittests/Compiler/Inputs/bell.qasm new file mode 100644 index 0000000000..74a35eaecf --- /dev/null +++ b/mlir/unittests/Compiler/Inputs/bell.qasm @@ -0,0 +1,9 @@ +OPENQASM 3.0; +include "stdgates.inc"; + +qubit[2] q; +bit[2] result; + +h q[0]; +cx q[0], q[1]; +result = measure q; diff --git a/mlir/unittests/Compiler/Inputs/heterogeneous-sc.json b/mlir/unittests/Compiler/Inputs/heterogeneous-sc.json new file mode 100644 index 0000000000..4fca754396 --- /dev/null +++ b/mlir/unittests/Compiler/Inputs/heterogeneous-sc.json @@ -0,0 +1,37 @@ +{ + "schema-version": 1, + "name": "Heterogeneous SC Test Device", + "numQubits": 3, + "durationUnit": { + "unit": "ns", + "scaleFactor": 1.0 + }, + "qubitProperties": { + "defaults": {}, + "overrides": [] + }, + "couplings": [ + [0, 1], + [1, 2] + ], + "operations": [ + { + "name": "r", + "numQubits": 1, + "numParameters": 2 + }, + { + "name": "cz", + "numQubits": 2, + "numParameters": 0, + "sites": [ + [0, 1] + ] + }, + { + "name": "measure", + "numQubits": 1, + "numParameters": 0 + } + ] +} diff --git a/mlir/unittests/Compiler/Inputs/mqt-cc-explicit.qdmi.json b/mlir/unittests/Compiler/Inputs/mqt-cc-explicit.qdmi.json new file mode 100644 index 0000000000..c5f5f7c450 --- /dev/null +++ b/mlir/unittests/Compiler/Inputs/mqt-cc-explicit.qdmi.json @@ -0,0 +1,11 @@ +{ + "schema-version": 1, + "qdmi": { + "devices": [ + { + "id": "mqt.sc.iqm.garnet", + "enabled": false + } + ] + } +} diff --git a/mlir/unittests/Compiler/test_compiler_fomac_adapter.cpp b/mlir/unittests/Compiler/test_compiler_fomac_adapter.cpp new file mode 100644 index 0000000000..b3f212a4b8 --- /dev/null +++ b/mlir/unittests/Compiler/test_compiler_fomac_adapter.cpp @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "fomac/FoMaC.hpp" +#include "mlir/Compiler/FoMaCAdapter.h" +#include "mlir/Compiler/Target.h" +#include "qdmi/driver/Driver.hpp" + +#include +#include +#include + +#include +#include + +using mlir::CompilerTarget; + +[[nodiscard]] static const CompilerTarget::Operation& +findOperation(const CompilerTarget& target, const llvm::StringRef name) { + const auto* const found = + llvm::find_if(target.operations(), + [&](const auto& op) { return op.canonicalName() == name; }); + if (found == target.operations().end()) { + throw std::out_of_range("Target operation not found"); + } + return *found; +} + +TEST(CompilerFoMaCAdapterTest, SnapshotsIQMCalibrationAndLifetime) { + const auto target = [] { + const auto device = fomac::Session::openDevice("mqt.sc.iqm.garnet"); + return mlir::compilerTargetFromDevice(device); + }(); + + ASSERT_TRUE(target.name()); + EXPECT_EQ(*target.name(), "IQM Garnet"); + EXPECT_EQ(target.numQubits(), 20); + EXPECT_TRUE(target.hasExplicitTopology()); + EXPECT_EQ(target.couplings().size(), 30); + + ASSERT_TRUE(target.durationUnit()); + EXPECT_EQ(target.durationUnit()->unit(), "us"); + EXPECT_DOUBLE_EQ(target.durationUnit()->scaleFactor(), 0.001); + + ASSERT_EQ(target.sites().size(), 20); + ASSERT_TRUE(target.sites().front().name()); + EXPECT_EQ(*target.sites().front().name(), "QB1"); + EXPECT_EQ(target.sites().front().t1(), 26626); + EXPECT_EQ(target.sites().front().t2(), 8376); + + ASSERT_EQ(target.operations().size(), 3); + const auto& r = findOperation(target, "r"); + const auto& cz = findOperation(target, "cz"); + const auto& measure = findOperation(target, "measure"); + EXPECT_EQ(r.siteTuples().size(), 20); + EXPECT_EQ(cz.siteTuples().size(), 30); + EXPECT_EQ(measure.siteTuples().size(), 20); + for (const auto& operation : target.operations()) { + EXPECT_FALSE(operation.duration()); + for (const auto& tuple : operation.siteTuples()) { + EXPECT_FALSE(tuple.duration()); + EXPECT_TRUE(tuple.fidelity()); + } + } + + EXPECT_TRUE(target.supportsOperation("r", 1, 2)); + EXPECT_TRUE(target.supportsOperation("cz", 2, 0)); + EXPECT_TRUE(target.supportsOperation("measure", 1, 0)); + EXPECT_FALSE(target.supportsOperation("rx", 1, 1)); + ASSERT_TRUE(target.synthesisBasis()); + EXPECT_EQ(target.synthesisBasis()->singleQubit, + CompilerTarget::SingleQubitBasis::R); + EXPECT_EQ(target.synthesisBasis()->entangler, CompilerTarget::GateKind::CZ); +} + +TEST(CompilerFoMaCAdapterTest, PreservesMissingTopologyAsAllToAll) { + const auto device = fomac::Session::openDevice("mqt.ddsim.default"); + const auto target = mlir::compilerTargetFromDevice(device); + + EXPECT_EQ(target.numQubits(), 65535); + EXPECT_FALSE(target.hasExplicitTopology()); + EXPECT_TRUE(target.areAdjacent(0, target.numQubits() - 1)); + EXPECT_TRUE(target.supportsOperation("h", 1, 0)); + EXPECT_TRUE(target.supportsOperation("cx", 2, 0)); + EXPECT_TRUE(target.supportsOperation("measure", 1, 0)); +} + +TEST(CompilerFoMaCAdapterTest, RejectsNonhomogeneousOperationSupport) { + qdmi::DeviceSessionConfig overrides; + overrides.deviceConfiguration = + qdmi::FileDeviceConfiguration{MQT_CORE_MLIR_HETEROGENEOUS_SC_CONFIG}; + const auto device = fomac::Session::openDevice("mqt.sc.default", overrides); + try { + const auto target = mlir::compilerTargetFromDevice(device); + FAIL() << "Expected a homogeneous-operation diagnostic, got " + << target.operations().size() << " target operations"; + } catch (const std::invalid_argument& error) { + EXPECT_NE(std::string(error.what()).find("homogeneous"), std::string::npos); + EXPECT_NE(std::string(error.what()).find("every topology edge"), + std::string::npos); + } +} + +TEST(CompilerFoMaCAdapterTest, RejectsNeutralAtomZoneModels) { + const auto device = fomac::Session::openDevice("mqt.na.default"); + try { + const auto target = mlir::compilerTargetFromDevice(device); + FAIL() << "Expected a neutral-atom diagnostic, got " << target.numQubits() + << " target sites"; + } catch (const std::invalid_argument& error) { + EXPECT_NE(std::string(error.what()).find("only circuit-model devices"), + std::string::npos); + EXPECT_NE(std::string(error.what()).find("zone"), std::string::npos); + } +} diff --git a/python/mqt/core/mlir.pyi b/python/mqt/core/mlir.pyi index 20e205608d..685afa26ef 100644 --- a/python/mqt/core/mlir.pyi +++ b/python/mqt/core/mlir.pyi @@ -10,10 +10,12 @@ import enum import os +from collections.abc import Sequence from typing import Literal, overload import qiskit +import mqt.core.fomac import mqt.core.ir class QIRProfile(enum.Enum): @@ -49,6 +51,252 @@ class OutputFormat(enum.Enum): QIR_ADAPTIVE = 6 """QIR for the Adaptive Profile.""" +class CompilerTarget: + """Immutable MLIR compiler target. + + An absent topology means all-to-all connectivity. An absent operation set + means every operation is native. + """ + + @overload + def __init__( + self, + num_qubits: int, + *, + couplings: Sequence[tuple[int, int]] | None = None, + operations: Sequence[CompilerTarget.Operation] | None = None, + duration_unit: CompilerTarget.DurationUnit | None = None, + ) -> None: ... + @overload + def __init__( + self, + name: str, + num_qubits: int, + *, + couplings: Sequence[tuple[int, int]] | None = None, + operations: Sequence[CompilerTarget.Operation] | None = None, + duration_unit: CompilerTarget.DurationUnit | None = None, + ) -> None: ... + @overload + def __init__( + self, + sites: Sequence[CompilerTarget.Site], + *, + couplings: Sequence[tuple[int, int]] | None = None, + operations: Sequence[CompilerTarget.Operation] | None = None, + duration_unit: CompilerTarget.DurationUnit | None = None, + ) -> None: ... + @overload + def __init__( + self, + name: str, + sites: Sequence[CompilerTarget.Site], + *, + couplings: Sequence[tuple[int, int]] | None = None, + operations: Sequence[CompilerTarget.Operation] | None = None, + duration_unit: CompilerTarget.DurationUnit | None = None, + ) -> None: ... + + class DurationUnit: + """Unit for raw target timing metadata.""" + + def __init__(self, unit: str, scale_factor: float) -> None: ... + @property + def unit(self) -> str: + """The reported duration unit.""" + + @property + def scale_factor(self) -> float: + """The multiplier applied to raw timing values.""" + + class Site: + """A hardware site and its optional metadata.""" + + def __init__( + self, site_id: int, name: str | None = None, t1: int | None = None, t2: int | None = None + ) -> None: ... + @property + def id(self) -> int: + """The target-defined nonnegative site identifier.""" + + @property + def name(self) -> str | None: + """The reported site name, if available.""" + + @property + def t1(self) -> int | None: + """The raw T1 coherence time, if available.""" + + @property + def t2(self) -> int | None: + """The raw T2 coherence time, if available.""" + + class SiteTuple: + """Calibration data for an ordered tuple of target sites.""" + + def __init__( + self, sites: Sequence[int], duration: int | None = None, fidelity: float | None = None + ) -> None: ... + @property + def sites(self) -> list[int]: + """The ordered target site identifiers.""" + + @property + def duration(self) -> int | None: + """The raw operation duration, if available.""" + + @property + def fidelity(self) -> float | None: + """The operation fidelity, if available.""" + + class Operation: + """A homogeneous target-wide operation capability and its calibration.""" + + def __init__( + self, + name: str, + num_qubits: int, + num_parameters: int, + site_tuples: Sequence[CompilerTarget.SiteTuple] | None = None, + duration: int | None = None, + fidelity: float | None = None, + ) -> None: ... + @property + def name(self) -> str: + """The exact reported operation name.""" + + @property + def canonical_name(self) -> str: + """The normalized compiler operation name.""" + + @property + def num_qubits(self) -> int: + """The fixed operation arity.""" + + @property + def num_parameters(self) -> int: + """The number of real-valued parameters.""" + + @property + def site_tuples(self) -> list[CompilerTarget.SiteTuple]: + """Ordered site-specific calibration data.""" + + @property + def duration(self) -> int | None: + """The raw default duration, if available.""" + + @property + def fidelity(self) -> float | None: + """The default fidelity, if available.""" + + class GateKind(enum.Enum): + """Recognized native gate capability.""" + + U = 0 + + X = 1 + + SX = 2 + + RZ = 3 + + RX = 4 + + RY = 5 + + R = 6 + + RXX = 7 + + RYY = 8 + + RZX = 9 + + RZZ = 10 + + ISWAP = 11 + + CZ = 12 + + CX = 13 + + ECR = 14 + + class SingleQubitBasis(enum.Enum): + """Recognized target-wide single-qubit synthesis basis.""" + + U = 0 + + ZSXX = 1 + + R = 2 + + XZX = 3 + + XYX = 4 + + ZYZ = 5 + + ZXZ = 6 + + class SynthesisBasis: + """One synthesis basis usable across the complete target.""" + + @property + def single_qubit(self) -> CompilerTarget.SingleQubitBasis: + """The single-qubit synthesis basis.""" + + @property + def entangler(self) -> CompilerTarget.GateKind: + """The two-qubit entangler.""" + + @staticmethod + def from_device(device: mqt.core.fomac.Device) -> CompilerTarget: + """Snapshot a circuit-model QDMI device.""" + + @property + def name(self) -> str | None: + """The target name, if available.""" + + @property + def duration_unit(self) -> CompilerTarget.DurationUnit | None: + """The target timing unit, if available.""" + + @property + def num_qubits(self) -> int: + """The number of target sites.""" + + @property + def sites(self) -> list[CompilerTarget.Site]: + """Detailed sites in compiler-vertex order.""" + + @property + def has_explicit_topology(self) -> bool: + """Whether the target defines a coupling topology.""" + + @property + def couplings(self) -> list[tuple[int, int]]: + """Canonical undirected couplings in target site IDs.""" + + @property + def has_explicit_operations(self) -> bool: + """Whether the target defines an operation set.""" + + @property + def operations(self) -> list[CompilerTarget.Operation]: + """Operation capabilities in reported order.""" + + @property + def supported_gates(self) -> list[CompilerTarget.GateKind]: + """Recognized native gates supported by the target.""" + + @property + def synthesis_basis(self) -> CompilerTarget.SynthesisBasis | None: + """A complete target-wide synthesis basis, if available.""" + + def supports_operation(self, name: str, num_qubits: int, num_parameters: int | None = None) -> bool: + """Whether the target supports an operation capability.""" + class Program: """Base class for a typed MLIR compiler program. @@ -164,6 +412,11 @@ class QCOProgram(Program): def decompose_multi_controlled(self, *, min_qubits: int = 3) -> None: """Decompose controlled X/Z/SWAP gates, qco.rccx, and constant-angle phase gates that act on at least min_qubits qubits (min_qubits must be at least 3; default 3 means wider than two-qubit).""" + def compile_for_target( + self, target: CompilerTarget, *, enable_timing: bool = False, enable_statistics: bool = False + ) -> None: + """Compile this QCO program for the target in place.""" + def to_qc(self, *, copy: bool = False) -> QCProgram: """Convert this program to QC. @@ -248,6 +501,7 @@ def compile_program( *, output: Literal[OutputFormat.QC, OutputFormat.QC_IMPORT] = ..., inplace: bool = False, + target: CompilerTarget | None = None, qco_pipeline: str = "mqt-qco-default", enable_timing: bool = False, enable_statistics: bool = False, @@ -264,6 +518,7 @@ def compile_program( *, output: Literal[OutputFormat.QCO, OutputFormat.QCO_OPTIMIZED], inplace: bool = False, + target: CompilerTarget | None = None, qco_pipeline: str = "mqt-qco-default", enable_timing: bool = False, enable_statistics: bool = False, @@ -280,6 +535,7 @@ def compile_program( *, output: Literal[OutputFormat.JEFF], inplace: bool = False, + target: CompilerTarget | None = None, qco_pipeline: str = "mqt-qco-default", enable_timing: bool = False, enable_statistics: bool = False, @@ -296,6 +552,7 @@ def compile_program( *, output: Literal[OutputFormat.QIR_BASE, OutputFormat.QIR_ADAPTIVE], inplace: bool = False, + target: CompilerTarget | None = None, qco_pipeline: str = "mqt-qco-default", enable_timing: bool = False, enable_statistics: bool = False, @@ -312,6 +569,7 @@ def compile_program( *, output: OutputFormat, inplace: bool = False, + target: CompilerTarget | None = None, qco_pipeline: str = "mqt-qco-default", enable_timing: bool = False, enable_statistics: bool = False, @@ -328,7 +586,10 @@ def compile_program( program: Source text, a file path, a circuit, or a typed compiler program. output: The requested output stage of the compiler pipeline. inplace: Whether a typed input program may be consumed. - qco_pipeline: The QCO optimization pipeline to run. + target: An optional compiler target for decomposition, mapping, and native + synthesis. A target requires optimized QCO, QC, or QIR output. + qco_pipeline: The QCO optimization pipeline to run. A custom pipeline + cannot be combined with a target. enable_timing: Whether to collect pass timing information. enable_statistics: Whether to collect pass statistics. diff --git a/test/python/test_mlir.py b/test/python/test_mlir.py index 072c65187a..7e301786d0 100644 --- a/test/python/test_mlir.py +++ b/test/python/test_mlir.py @@ -15,8 +15,10 @@ import pytest from qiskit import QuantumCircuit +from mqt.core.fomac import open_device from mqt.core.ir import QuantumComputation from mqt.core.mlir import ( + CompilerTarget, JeffProgram, OutputFormat, QCOProgram, @@ -287,6 +289,93 @@ def test_compile_program_exposes_raw_and_optimized_qco() -> None: assert raw.ir != optimized.ir +def _iqm_like_target() -> CompilerTarget: + """Construct a two-site target with the IQM gate set. + + Returns: + A target with sparse site IDs, one coupling, and the IQM native gates. + """ + return CompilerTarget( + "IQM-like target", + [ + CompilerTarget.Site(10, name="QB1"), + CompilerTarget.Site(20, name="QB2"), + ], + couplings=[(10, 20)], + operations=[ + CompilerTarget.Operation("r", 1, 2), + CompilerTarget.Operation("cz", 2, 0), + CompilerTarget.Operation("measure", 1, 0), + ], + ) + + +def _assert_iqm_native(program: QCOProgram) -> None: + """Check target assignment and the IQM native gate set.""" + assert "qco.static 10" in program.ir + assert "qco.static 20" in program.ir + assert "qco.r(" in program.ir + assert "qco.measure" in program.ir + assert "qco.rx" not in program.ir + assert "qco.ry" not in program.ir + + +def test_compile_program_for_target() -> None: + """Compile through the canonical target pipeline.""" + result = compile_program( + QASM_STRING, + output=OutputFormat.QCO_OPTIMIZED, + target=_iqm_like_target(), + ) + + assert isinstance(result, QCOProgram) + _assert_iqm_native(result) + + +def test_qco_program_compiles_for_target() -> None: + """Expose target compilation on typed QCO programs.""" + qco = compile_program(QASM_STRING, output=OutputFormat.QCO) + assert isinstance(qco, QCOProgram) + + qco.compile_for_target(_iqm_like_target()) + + _assert_iqm_native(qco) + + +def test_compiler_target_snapshots_qdmi_device() -> None: + """Retain IQM topology and calibration independently of the live device.""" + target = CompilerTarget.from_device(open_device("mqt.sc.iqm.garnet")) + + assert target.name == "IQM Garnet" + assert target.num_qubits == 20 + assert len(target.couplings) == 30 + assert target.sites[0].name == "QB1" + assert target.sites[0].t1 == 26626 + assert target.sites[0].t2 == 8376 + assert target.duration_unit is not None + assert target.duration_unit.unit == "us" + assert target.duration_unit.scale_factor == pytest.approx(0.001) + assert target.supports_operation("r", 1, 2) + assert target.supports_operation("cz", 2, 0) + assert target.supports_operation("measure", 1, 0) + assert not target.supports_operation("rx", 1, 1) + assert target.synthesis_basis is not None + assert target.synthesis_basis.single_qubit == CompilerTarget.SingleQubitBasis.R + assert target.synthesis_basis.entangler == CompilerTarget.GateKind.CZ + assert [operation.name for operation in target.operations] == ["r", "cz", "measure"] + assert [len(operation.site_tuples) for operation in target.operations] == [20, 30, 20] + assert all( + site_tuple.fidelity is not None for operation in target.operations for site_tuple in operation.site_tuples + ) + assert all(site_tuple.duration is None for operation in target.operations for site_tuple in operation.site_tuples) + + +def test_compiler_target_rejects_qdmi_zone_model() -> None: + """Reject neutral-atom zones at the circuit-target boundary.""" + with pytest.raises(ValueError, match="only circuit-model devices"): + CompilerTarget.from_device(open_device("mqt.na.default")) + + def test_qco_program_runs_textual_pipeline() -> None: """Run registered QCO passes through MLIR textual pipeline syntax.""" qco = compile_program(QASM_STRING, output=OutputFormat.QCO) From c90aa5d64c7aeb944681f506b9a006695b286cce Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Tue, 4 Aug 2026 14:27:03 +0200 Subject: [PATCH 2/6] =?UTF-8?q?=F0=9F=90=9B=20Preserve=20ordered=20QDMI=20?= =?UTF-8?q?operation=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assisted-by: GPT-5.6 via Codex --- .agent/plans/1687-int-final-qdmi-bridge.md | 44 ++++++++++++----- docs/mlir/target_compilation.md | 4 +- mlir/lib/Compiler/FoMaCAdapter.cpp | 46 +++++++++++++---- mlir/unittests/Compiler/CMakeLists.txt | 2 + .../Inputs/directional-one-way-sc.json | 32 ++++++++++++ .../Inputs/directional-two-way-sc.json | 49 +++++++++++++++++++ .../Compiler/Inputs/heterogeneous-sc.json | 4 +- .../Compiler/test_compiler_fomac_adapter.cpp | 35 +++++++++++++ 8 files changed, 191 insertions(+), 25 deletions(-) create mode 100644 mlir/unittests/Compiler/Inputs/directional-one-way-sc.json create mode 100644 mlir/unittests/Compiler/Inputs/directional-two-way-sc.json diff --git a/.agent/plans/1687-int-final-qdmi-bridge.md b/.agent/plans/1687-int-final-qdmi-bridge.md index 287658cf1f..ede39866eb 100644 --- a/.agent/plans/1687-int-final-qdmi-bridge.md +++ b/.agent/plans/1687-int-final-qdmi-bridge.md @@ -59,8 +59,14 @@ than retaining its historical merge-heavy implementation. a provider-disabled compiler build, authoritative stub generation, strict warning-free documentation, changed-source clang-tidy, repository lint, and `git diff --check`. -- [ ] Commit the validated implementation, complete an independent exact-head - review, and address every material finding. +- [x] (2026-08-04) Committed the initial implementation and completed two + independent exact-head reviews. Both identified the same ordered-site + widening bug; the adapter now rejects one-way directional operations, + preserves two-way ordered calibration, and retains IQM's symmetric CZ + convention. All 223 compiler tests, ten focused adapter/CLI CTests, + changed-source clang-tidy, repository lint, and `git diff --check` pass. +- [ ] Commit the reviewed ordered-site fix and complete a fresh independent + exact-head verification with no material findings. - [ ] Rewrite the existing PR branch with an exact force-with-lease, replace the obsolete PR description, verify the replacement head, and monitor CI. @@ -106,6 +112,13 @@ than retaining its historical merge-heavy implementation. exposed that provider-backed test sources and runtime copying must be conditional. The compiler test target now builds without provider libraries, while normal CI retains full live-device coverage. +- Observation: QDMI operation site tuples are ordered, while the compiler + deliberately models an undirected topology and homogeneous bidirectional gate + support. Canonicalizing a one-way two-qubit site list would silently widen the + device contract. The adapter must require both orientations for directional + and unknown operations while allowing proven operand-symmetric gates such as + CZ to report each edge once. Missing two-qubit site information is likewise + insufficient when a device reports an explicit topology. ### Decision Log @@ -128,6 +141,11 @@ than retaining its historical merge-heavy implementation. represents one target-wide gate set; silently widening a restricted QDMI operation would allow synthesis to emit an unsupported gate. Date/Author: 2026-08-04, GPT-5.6 via Codex. +- Decision: accept one reported orientation only for a conservative set of + operand-swap-invariant gates and require both ordered tuples for every site + pair otherwise. Rationale: this preserves IQM's symmetric CZ data without + misrepresenting directional or unknown QDMI operations as bidirectional. + Date/Author: 2026-08-04, GPT-5.6 via Codex. - Decision: expose `CompilerTarget.from_device(device)` from `mqt.core.mlir`, not `Device.target()` from `mqt.core.fomac`. Rationale: CoreFoMaC and its binding remain independent of MLIR, target ownership is visible in the @@ -181,11 +199,14 @@ real integration test found and fixed a pass-ordering bug in the merged target pipeline: generic canonicalization now runs before native synthesis, while conformance remains the final semantic check. -The source-build C++ and CLI workflows and packaged Python workflow are proven. -A distributable MLIR C++ SDK remains a separate packaging concern because the -current repository does not export the compiler dialects, generated headers, or -pipeline dependency closure. Independent exact-head review and publication are -still pending. +Independent review additionally found and corrected one ordered-QDMI-site +widening bug at the adapter boundary. Directional and unknown operations now +prove both orientations on every supported pair; operand-symmetric gates retain +their compact one-tuple-per-edge representation. The source-build C++ and CLI +workflows and packaged Python workflow are proven. A distributable MLIR C++ SDK +remains a separate packaging concern because the current repository does not +export the compiler dialects, generated headers, or pipeline dependency closure. +Fresh exact-head verification and publication are still pending. ### Context and Orientation @@ -345,7 +366,9 @@ set, and calibration coverage from #1992. The adapter tests must prove: destroyed. 4. A circuit-model device without topology becomes all-to-all. 5. Site-dependent operation support fails rather than being widened. -6. Neutral-atom zone models fail with a precise circuit-model diagnostic. +6. One-way directional operation support fails, while both ordered orientations + and their distinct calibration survive conversion. +7. Neutral-atom zone models fail with a precise circuit-model diagnostic. The Python tests must prove direct construction, immutable metadata access, `from_device`, detached lifetime, `compile_for_target`, and optional-target @@ -396,9 +419,8 @@ then use a newly verified exact lease. Never use an unqualified force push. The historical PR head `dd9619bd27a34ced8ed68a4ee4533cb85771f144` is evidence only. No historical commit or implementation file should be cherry-picked. The -useful prerequisite behavior is already merged through PRs #1992, #1993, #1997, - -## 1998, and #1999 +useful prerequisite behavior is already merged through PRs `#1992`, `#1993`, +`#1997`, `#1998`, and `#1999`. The unresolved historical review threads map either to those merged prerequisites or to this slice's adapter, bindings, CLI, and workflow diff --git a/docs/mlir/target_compilation.md b/docs/mlir/target_compilation.md index c153c766dd..bfb5cc6447 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -90,7 +90,9 @@ if (!qco || !qco->compileForTarget(target)) { } ``` -The adapter accepts circuit-model devices. Neutral-atom zone models require a +The adapter accepts circuit-model devices whose operations are available +throughout the topology in both operand orientations. Operand-symmetric gates, +such as CZ, may report each edge once. Neutral-atom zone models require a different compilation model and are rejected with a diagnostic. The bundled Garnet and Emerald snapshots contain available T1, T2, and fidelity diff --git a/mlir/lib/Compiler/FoMaCAdapter.cpp b/mlir/lib/Compiler/FoMaCAdapter.cpp index d7b04688fb..0d68b2276d 100644 --- a/mlir/lib/Compiler/FoMaCAdapter.cpp +++ b/mlir/lib/Compiler/FoMaCAdapter.cpp @@ -15,6 +15,7 @@ #include #include +#include #include #include @@ -81,6 +82,15 @@ allToAllCouplingCount(const size_t numSites) { return first * second; } +[[nodiscard]] static bool +isSwapInvariantOperation(const llvm::StringRef operationName) { + const auto canonicalName = operationName.trim().lower(); + return llvm::StringSwitch(canonicalName) + .Cases("cz", "swap", "iswap", true) + .Cases("rxx", "ryy", "rzz", true) + .Default(false); +} + static void validateHomogeneousSupport( const fomac::Operation& operation, const size_t arity, const std::optional>& flattenedSites, @@ -88,6 +98,12 @@ static void validateHomogeneousSupport( const std::optional>& couplings, const llvm::StringRef deviceName) { if (!flattenedSites) { + if (arity == 2 && couplings) { + rejectNonHomogeneousOperation( + deviceName, operation.getName(), + "the device reports an explicit topology but no ordered two-qubit " + "site support"); + } return; } const auto operationName = operation.getName(); @@ -153,21 +169,31 @@ static void validateHomogeneousSupport( deviceName, operationName, "the operation is not available on every all-to-all site pair"); } - return; + } else { + llvm::DenseSet expectedCouplings; + expectedCouplings.reserve(couplings->size()); + for (const auto& [first, second] : *couplings) { + expectedCouplings.insert(canonicalCoupling(first, second)); + } + if (supportedCouplings.size() != expectedCouplings.size() || + !std::ranges::all_of(expectedCouplings, [&](const auto& coupling) { + return supportedCouplings.contains(coupling); + })) { + rejectNonHomogeneousOperation( + deviceName, operationName, + "the operation is not available on every topology edge"); + } } - llvm::DenseSet expectedCouplings; - expectedCouplings.reserve(couplings->size()); - for (const auto& [first, second] : *couplings) { - expectedCouplings.insert(canonicalCoupling(first, second)); - } - if (supportedCouplings.size() != expectedCouplings.size() || - !std::ranges::all_of(expectedCouplings, [&](const auto& coupling) { - return supportedCouplings.contains(coupling); + if (!isSwapInvariantOperation(operationName) && + !std::ranges::all_of(supportedCouplings, [&](const auto& coupling) { + return reportedTuples.contains(coupling) && + reportedTuples.contains( + CompilerTarget::Coupling{coupling.second, coupling.first}); })) { rejectNonHomogeneousOperation( deviceName, operationName, - "the operation is not available on every topology edge"); + "both orientations must be available on every supported site pair"); } } diff --git a/mlir/unittests/Compiler/CMakeLists.txt b/mlir/unittests/Compiler/CMakeLists.txt index d319ce031a..605aca91ab 100644 --- a/mlir/unittests/Compiler/CMakeLists.txt +++ b/mlir/unittests/Compiler/CMakeLists.txt @@ -42,6 +42,8 @@ if(has_compiler_qdmi_provider_tests) mqt-core-mlir-unittests-compiler PRIVATE MQT_CORE_MLIR_HETEROGENEOUS_SC_CONFIG="${CMAKE_CURRENT_SOURCE_DIR}/Inputs/heterogeneous-sc.json" + MQT_CORE_MLIR_DIRECTIONAL_ONE_WAY_SC_CONFIG="${CMAKE_CURRENT_SOURCE_DIR}/Inputs/directional-one-way-sc.json" + MQT_CORE_MLIR_DIRECTIONAL_TWO_WAY_SC_CONFIG="${CMAKE_CURRENT_SOURCE_DIR}/Inputs/directional-two-way-sc.json" ) endif() diff --git a/mlir/unittests/Compiler/Inputs/directional-one-way-sc.json b/mlir/unittests/Compiler/Inputs/directional-one-way-sc.json new file mode 100644 index 0000000000..37ed4d903a --- /dev/null +++ b/mlir/unittests/Compiler/Inputs/directional-one-way-sc.json @@ -0,0 +1,32 @@ +{ + "schema-version": 1, + "name": "One-way SC Test Device", + "numQubits": 2, + "durationUnit": { + "unit": "ns", + "scaleFactor": 1.0 + }, + "qubitProperties": { + "defaults": {}, + "overrides": [] + }, + "couplings": [[0, 1]], + "operations": [ + { + "name": "r", + "numQubits": 1, + "numParameters": 2 + }, + { + "name": "cx", + "numQubits": 2, + "numParameters": 0, + "sites": [[0, 1]] + }, + { + "name": "measure", + "numQubits": 1, + "numParameters": 0 + } + ] +} diff --git a/mlir/unittests/Compiler/Inputs/directional-two-way-sc.json b/mlir/unittests/Compiler/Inputs/directional-two-way-sc.json new file mode 100644 index 0000000000..73680a5971 --- /dev/null +++ b/mlir/unittests/Compiler/Inputs/directional-two-way-sc.json @@ -0,0 +1,49 @@ +{ + "schema-version": 1, + "name": "Two-way SC Test Device", + "numQubits": 2, + "durationUnit": { + "unit": "ns", + "scaleFactor": 1.0 + }, + "qubitProperties": { + "defaults": {}, + "overrides": [] + }, + "couplings": [ + [0, 1], + [1, 0] + ], + "operations": [ + { + "name": "r", + "numQubits": 1, + "numParameters": 2 + }, + { + "name": "cx", + "numQubits": 2, + "numParameters": 0, + "sites": [ + [0, 1], + [1, 0] + ], + "fidelity": 0.9, + "siteOverrides": [ + { + "sites": [0, 1], + "fidelity": 0.91 + }, + { + "sites": [1, 0], + "fidelity": 0.92 + } + ] + }, + { + "name": "measure", + "numQubits": 1, + "numParameters": 0 + } + ] +} diff --git a/mlir/unittests/Compiler/Inputs/heterogeneous-sc.json b/mlir/unittests/Compiler/Inputs/heterogeneous-sc.json index 4fca754396..3092e030db 100644 --- a/mlir/unittests/Compiler/Inputs/heterogeneous-sc.json +++ b/mlir/unittests/Compiler/Inputs/heterogeneous-sc.json @@ -24,9 +24,7 @@ "name": "cz", "numQubits": 2, "numParameters": 0, - "sites": [ - [0, 1] - ] + "sites": [[0, 1]] }, { "name": "measure", diff --git a/mlir/unittests/Compiler/test_compiler_fomac_adapter.cpp b/mlir/unittests/Compiler/test_compiler_fomac_adapter.cpp index b3f212a4b8..140549b5a2 100644 --- a/mlir/unittests/Compiler/test_compiler_fomac_adapter.cpp +++ b/mlir/unittests/Compiler/test_compiler_fomac_adapter.cpp @@ -14,6 +14,7 @@ #include "qdmi/driver/Driver.hpp" #include +#include #include #include @@ -108,6 +109,40 @@ TEST(CompilerFoMaCAdapterTest, RejectsNonhomogeneousOperationSupport) { } } +TEST(CompilerFoMaCAdapterTest, RejectsDirectionalOperationWithoutReverseSites) { + qdmi::DeviceSessionConfig overrides; + overrides.deviceConfiguration = qdmi::FileDeviceConfiguration{ + MQT_CORE_MLIR_DIRECTIONAL_ONE_WAY_SC_CONFIG}; + const auto device = fomac::Session::openDevice("mqt.sc.default", overrides); + try { + const auto target = mlir::compilerTargetFromDevice(device); + FAIL() << "Expected a bidirectional-operation diagnostic, got " + << target.operations().size() << " target operations"; + } catch (const std::invalid_argument& error) { + EXPECT_NE(std::string(error.what()).find("both orientations"), + std::string::npos); + } +} + +TEST(CompilerFoMaCAdapterTest, + PreservesDirectionalCalibrationWhenBothOrientationsExist) { + qdmi::DeviceSessionConfig overrides; + overrides.deviceConfiguration = qdmi::FileDeviceConfiguration{ + MQT_CORE_MLIR_DIRECTIONAL_TWO_WAY_SC_CONFIG}; + const auto device = fomac::Session::openDevice("mqt.sc.default", overrides); + const auto target = mlir::compilerTargetFromDevice(device); + + ASSERT_EQ(target.couplings().size(), 1); + const auto& cx = findOperation(target, "cx"); + ASSERT_EQ(cx.siteTuples().size(), 2); + EXPECT_EQ(cx.siteTuples()[0].sites(), + (llvm::ArrayRef{0, 1})); + EXPECT_DOUBLE_EQ(*cx.siteTuples()[0].fidelity(), 0.91); + EXPECT_EQ(cx.siteTuples()[1].sites(), + (llvm::ArrayRef{1, 0})); + EXPECT_DOUBLE_EQ(*cx.siteTuples()[1].fidelity(), 0.92); +} + TEST(CompilerFoMaCAdapterTest, RejectsNeutralAtomZoneModels) { const auto device = fomac::Session::openDevice("mqt.na.default"); try { From b6bfc9f531621f643fb8f091272042129e49b700 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Tue, 4 Aug 2026 14:37:02 +0200 Subject: [PATCH 3/6] =?UTF-8?q?=F0=9F=93=9D=20Complete=20the=20integration?= =?UTF-8?q?=20ExecPlan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assisted-by: GPT-5.6 via Codex --- .agent/plans/1687-int-final-qdmi-bridge.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.agent/plans/1687-int-final-qdmi-bridge.md b/.agent/plans/1687-int-final-qdmi-bridge.md index ede39866eb..6da20ec8fb 100644 --- a/.agent/plans/1687-int-final-qdmi-bridge.md +++ b/.agent/plans/1687-int-final-qdmi-bridge.md @@ -65,8 +65,9 @@ than retaining its historical merge-heavy implementation. preserves two-way ordered calibration, and retains IQM's symmetric CZ convention. All 223 compiler tests, ten focused adapter/CLI CTests, changed-source clang-tidy, repository lint, and `git diff --check` pass. -- [ ] Commit the reviewed ordered-site fix and complete a fresh independent - exact-head verification with no material findings. +- [x] (2026-08-04) Committed the reviewed ordered-site fix as `2d562a2fa` and + completed a fresh independent exact-head verification with no material + findings. - [ ] Rewrite the existing PR branch with an exact force-with-lease, replace the obsolete PR description, verify the replacement head, and monitor CI. @@ -206,7 +207,7 @@ their compact one-tuple-per-edge representation. The source-build C++ and CLI workflows and packaged Python workflow are proven. A distributable MLIR C++ SDK remains a separate packaging concern because the current repository does not export the compiler dialects, generated headers, or pipeline dependency closure. -Fresh exact-head verification and publication are still pending. +Publication and replacement-head CI are still pending. ### Context and Orientation From 11cc7b4973987a1968877b7e477fec1f8757473f Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Tue, 4 Aug 2026 15:48:38 +0200 Subject: [PATCH 4/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20QDMI=20vali?= =?UTF-8?q?dation=20paths?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assisted-by: GPT-5.6 via Codex --- .agent/plans/1687-int-final-qdmi-bridge.md | 38 +++- mlir/lib/Compiler/FoMaCAdapter.cpp | 200 ++++++++++----------- mlir/tools/mqt-cc/mqt-cc.cpp | 76 ++++---- 3 files changed, 168 insertions(+), 146 deletions(-) diff --git a/.agent/plans/1687-int-final-qdmi-bridge.md b/.agent/plans/1687-int-final-qdmi-bridge.md index 6da20ec8fb..72a7222e5a 100644 --- a/.agent/plans/1687-int-final-qdmi-bridge.md +++ b/.agent/plans/1687-int-final-qdmi-bridge.md @@ -68,8 +68,20 @@ than retaining its historical merge-heavy implementation. - [x] (2026-08-04) Committed the reviewed ordered-site fix as `2d562a2fa` and completed a fresh independent exact-head verification with no material findings. -- [ ] Rewrite the existing PR branch with an exact force-with-lease, replace the - obsolete PR description, verify the replacement head, and monitor CI. +- [x] (2026-08-04) Rewrote the existing PR branch with an exact + force-with-lease, replaced the obsolete PR description, verified the + signed replacement head, and resolved all 35 historical review threads. +- [x] (2026-08-04) Monitored exact head `11ec5816d` through the complete Actions + matrix: every C++ and Python build, test, lint, documentation, aggregate, + and total-coverage check passed. The sole failure was the C++ patch + coverage gate at 78.4 percent. +- [x] (2026-08-04) Centralized adapter preconditions and CLI diagnostics so the + existing behavioral tests cover the shared failure contracts. Independent + review caught and corrected diagnostic-precedence drift before + publication; the original fail-fast order and four irreducible CLI tests + remain. +- [ ] Publish the signed coverage follow-up and monitor replacement CI to a + terminal result. ### Surprises & Discoveries @@ -120,6 +132,11 @@ than retaining its historical merge-heavy implementation. and unknown operations while allowing proven operand-symmetric gates such as CZ to report each edge once. Missing two-qubit site information is likewise insufficient when a device reports an explicit topology. +- Observation: exact-head C++ patch coverage reported 42 uncovered lines, all in + the adapter and CLI. Most were duplicated defensive throw-and-print bodies, + not missing user workflows. Central shared precondition and diagnostic paths + let the existing invalid-device and successful CLI tests cover the same + contracts without adding subprocess fixtures solely for line coverage. ### Decision Log @@ -190,6 +207,12 @@ than retaining its historical merge-heavy implementation. would be unusable. The documented C++ and CLI workflows are explicitly source-build workflows; packaged Python target compilation remains covered. Date/Author: 2026-08-04, GPT-5.6 via Codex. +- Decision: centralize adapter preconditions and CLI diagnostics while retaining + the four irreducible subprocess checks. Rationale: normal devices execute the + positive contracts, existing negative adapter tests exercise one shared + exception path, and the CLI does not gain redundant tests for every + combination of equivalent option errors. Date/Author: 2026-08-04, GPT-5.6 via + Codex. ### Outcomes & Retrospective @@ -207,7 +230,10 @@ their compact one-tuple-per-edge representation. The source-build C++ and CLI workflows and packaged Python workflow are proven. A distributable MLIR C++ SDK remains a separate packaging concern because the current repository does not export the compiler dialects, generated headers, or pipeline dependency closure. -Publication and replacement-head CI are still pending. +The replacement branch and PR description are published, all historical review +threads are resolved, and the full exact-head Actions matrix is green. A compact +follow-up is in progress for the remaining C++ patch-coverage gate; fresh +replacement-head CI and human re-review remain pending. ### Context and Orientation @@ -419,9 +445,9 @@ then use a newly verified exact lease. Never use an unqualified force push. ### Artifacts and Notes The historical PR head `dd9619bd27a34ced8ed68a4ee4533cb85771f144` is evidence -only. No historical commit or implementation file should be cherry-picked. The -useful prerequisite behavior is already merged through PRs `#1992`, `#1993`, -`#1997`, `#1998`, and `#1999`. +only. No historical commit or implementation file was cherry-picked. The useful +prerequisite behavior is merged through PRs `#1992`, `#1993`, `#1997`, `#1998`, +and `#1999`; the rewritten integration was published at `11ec5816d`. The unresolved historical review threads map either to those merged prerequisites or to this slice's adapter, bindings, CLI, and workflow diff --git a/mlir/lib/Compiler/FoMaCAdapter.cpp b/mlir/lib/Compiler/FoMaCAdapter.cpp index 0d68b2276d..3471390f9b 100644 --- a/mlir/lib/Compiler/FoMaCAdapter.cpp +++ b/mlir/lib/Compiler/FoMaCAdapter.cpp @@ -16,6 +16,8 @@ #include #include #include +#include +#include #include #include @@ -29,44 +31,50 @@ namespace mlir { -[[nodiscard]] static CompilerTarget::SiteId checkedSiteId(const size_t index) { - if (index > static_cast( - std::numeric_limits::max())) { - throw std::invalid_argument( - "QDMI site index exceeds the nonnegative i64 compiler-target domain"); +static void requireAdapterInput(const bool condition, + const llvm::Twine& message) { + if (!condition) { + throw std::invalid_argument(message.str()); } - return static_cast(index); } -[[noreturn]] static void -rejectNonCircuitDevice(const llvm::StringRef deviceName, - const llvm::StringRef detail) { - throw std::invalid_argument( - (std::string("QDMI device '") + deviceName.str() + - "' cannot be used as an MLIR compiler target: only circuit-model " - "devices with one qubit per non-zone site are supported (" + - detail.str() + ")")); +static void requireCircuitDevice(const bool condition, + const llvm::StringRef deviceName, + const llvm::StringRef detail) { + requireAdapterInput( + condition, llvm::Twine("QDMI device '") + deviceName + + "' cannot be used as an MLIR compiler target: only " + "circuit-model devices with one qubit per non-zone site " + "are supported (" + + detail + ")"); +} + +static void requireHomogeneousOperation(const bool condition, + const llvm::StringRef deviceName, + const llvm::StringRef operationName, + const llvm::StringRef detail) { + requireAdapterInput( + condition, llvm::Twine("QDMI device '") + deviceName + "' operation '" + + operationName + + "' cannot be represented by the MLIR compiler target: " + "operation support must be homogeneous across the device " + "(" + + detail + ")"); } -[[noreturn]] static void -rejectNonHomogeneousOperation(const llvm::StringRef deviceName, - const llvm::StringRef operationName, - const llvm::StringRef detail) { - throw std::invalid_argument( - (std::string("QDMI device '") + deviceName.str() + "' operation '" + - operationName.str() + - "' cannot be represented by the MLIR compiler target: operation " - "support must be homogeneous across the device (" + - detail.str() + ")")); +[[nodiscard]] static CompilerTarget::SiteId checkedSiteId(const size_t index) { + requireAdapterInput( + index <= static_cast( + std::numeric_limits::max()), + "QDMI site index exceeds the nonnegative i64 compiler-target domain"); + return static_cast(index); } [[nodiscard]] static CompilerTarget::Coupling canonicalCoupling(const CompilerTarget::SiteId first, const CompilerTarget::SiteId second) { - if (first < second) { - return {first, second}; - } - return {second, first}; + return first < second ? CompilerTarget::Coupling{first, second} + : CompilerTarget::Coupling{second, first}; } [[nodiscard]] static std::optional @@ -76,10 +84,7 @@ allToAllCouplingCount(const size_t numSites) { } const auto first = numSites % 2 == 0 ? numSites / 2 : numSites; const auto second = numSites % 2 == 0 ? numSites - 1 : (numSites - 1) / 2; - if (first > std::numeric_limits::max() / second) { - return std::nullopt; - } - return first * second; + return llvm::checkedMulUnsigned(first, second); } [[nodiscard]] static bool @@ -98,26 +103,20 @@ static void validateHomogeneousSupport( const std::optional>& couplings, const llvm::StringRef deviceName) { if (!flattenedSites) { - if (arity == 2 && couplings) { - rejectNonHomogeneousOperation( - deviceName, operation.getName(), - "the device reports an explicit topology but no ordered two-qubit " - "site support"); - } + requireHomogeneousOperation( + arity != 2 || !couplings, deviceName, operation.getName(), + "the device reports an explicit topology but no ordered two-qubit " + "site support"); return; } const auto operationName = operation.getName(); - if (flattenedSites->size() % arity != 0) { - rejectNonHomogeneousOperation( - deviceName, operationName, - "the reported site list is not divisible by the fixed arity"); - } - if (arity > 2) { - rejectNonHomogeneousOperation( - deviceName, operationName, - "explicit site lists are supported only for one- and two-qubit " - "operations"); - } + requireHomogeneousOperation( + flattenedSites->size() % arity == 0, deviceName, operationName, + "the reported site list is not divisible by the fixed arity"); + requireHomogeneousOperation( + arity <= 2, deviceName, operationName, + "explicit site lists are supported only for one- and two-qubit " + "operations"); llvm::DenseSet knownSites; knownSites.reserve(deviceSites.size()); @@ -130,18 +129,14 @@ static void validateHomogeneousSupport( supportedSites.reserve(flattenedSites->size()); for (const auto& site : *flattenedSites) { const auto siteId = checkedSiteId(site.getIndex()); - if (!knownSites.contains(siteId) || - !supportedSites.insert(siteId).second) { - rejectNonHomogeneousOperation( - deviceName, operationName, - "the reported one-qubit sites must be unique device sites"); - } - } - if (supportedSites.size() != knownSites.size()) { - rejectNonHomogeneousOperation( - deviceName, operationName, - "the operation is not available on every device site"); + const auto inserted = supportedSites.insert(siteId).second; + requireHomogeneousOperation( + knownSites.contains(siteId) && inserted, deviceName, operationName, + "the reported one-qubit sites must be unique device sites"); } + requireHomogeneousOperation( + supportedSites.size() == knownSites.size(), deviceName, operationName, + "the operation is not available on every device site"); return; } @@ -152,61 +147,58 @@ static void validateHomogeneousSupport( for (size_t offset = 0; offset < flattenedSites->size(); offset += arity) { const auto first = checkedSiteId((*flattenedSites)[offset].getIndex()); const auto second = checkedSiteId((*flattenedSites)[offset + 1].getIndex()); - if (first == second || !knownSites.contains(first) || - !knownSites.contains(second) || - !reportedTuples.insert({first, second}).second) { - rejectNonHomogeneousOperation( - deviceName, operationName, - "the reported two-qubit sites must be unique pairs of device sites"); - } + const auto inserted = reportedTuples.insert({first, second}).second; + const auto validTuple = first != second && knownSites.contains(first) && + knownSites.contains(second) && inserted; + requireHomogeneousOperation( + validTuple, deviceName, operationName, + "the reported two-qubit sites must be unique pairs of device sites"); supportedCouplings.insert(canonicalCoupling(first, second)); } + auto coversTarget = false; if (!couplings) { const auto expected = allToAllCouplingCount(knownSites.size()); - if (!expected || supportedCouplings.size() != *expected) { - rejectNonHomogeneousOperation( - deviceName, operationName, - "the operation is not available on every all-to-all site pair"); - } + coversTarget = expected && supportedCouplings.size() == *expected; } else { llvm::DenseSet expectedCouplings; expectedCouplings.reserve(couplings->size()); for (const auto& [first, second] : *couplings) { expectedCouplings.insert(canonicalCoupling(first, second)); } - if (supportedCouplings.size() != expectedCouplings.size() || - !std::ranges::all_of(expectedCouplings, [&](const auto& coupling) { + coversTarget = + supportedCouplings.size() == expectedCouplings.size() && + std::ranges::all_of(expectedCouplings, [&](const auto& coupling) { return supportedCouplings.contains(coupling); - })) { - rejectNonHomogeneousOperation( - deviceName, operationName, - "the operation is not available on every topology edge"); - } + }); } + requireHomogeneousOperation( + coversTarget, deviceName, operationName, + couplings ? "the operation is not available on every topology edge" + : "the operation is not available on every all-to-all site " + "pair"); - if (!isSwapInvariantOperation(operationName) && - !std::ranges::all_of(supportedCouplings, [&](const auto& coupling) { - return reportedTuples.contains(coupling) && - reportedTuples.contains( - CompilerTarget::Coupling{coupling.second, coupling.first}); - })) { - rejectNonHomogeneousOperation( - deviceName, operationName, - "both orientations must be available on every supported site pair"); - } + requireHomogeneousOperation( + isSwapInvariantOperation(operationName) || + std::ranges::all_of( + supportedCouplings, + [&](const auto& coupling) { + return reportedTuples.contains(coupling) && + reportedTuples.contains(CompilerTarget::Coupling{ + coupling.second, coupling.first}); + }), + deviceName, operationName, + "both orientations must be available on every supported site pair"); } [[nodiscard]] static std::optional snapshotDurationUnit(const fomac::Device& device) { auto unit = device.getDurationUnit(); const auto scaleFactor = device.getDurationScaleFactor(); + requireAdapterInput( + unit || !scaleFactor, + "QDMI device reports a duration scale factor without a duration unit"); if (!unit) { - if (scaleFactor) { - throw std::invalid_argument( - "QDMI device reports a duration scale factor without a duration " - "unit"); - } return std::nullopt; } return CompilerTarget::DurationUnit(std::move(*unit), @@ -252,10 +244,8 @@ snapshotDurationUnit(const fomac::Device& device) { std::vector targetOperations; targetOperations.reserve(operations.size()); for (const auto& operation : operations) { - if (operation.isZoned()) { - rejectNonCircuitDevice(deviceName, - "the device exposes a zoned operation"); - } + requireCircuitDevice(!operation.isZoned(), deviceName, + "the device exposes a zoned operation"); const auto arity = operation.getQubitsNum(); if (!arity || *arity == 0) { continue; @@ -277,14 +267,12 @@ snapshotDurationUnit(const fomac::Device& device) { CompilerTarget compilerTargetFromDevice(const fomac::Device& device) { auto deviceName = device.getName(); const auto deviceSites = device.getSites(); - if (std::ranges::any_of(deviceSites, - [](const auto& site) { return site.isZone(); })) { - rejectNonCircuitDevice(deviceName, "the device exposes zone sites"); - } - if (device.getQubitsNum() != deviceSites.size()) { - rejectNonCircuitDevice( - deviceName, "the qubit count does not match the regular-site count"); - } + requireCircuitDevice( + std::ranges::none_of(deviceSites, + [](const auto& site) { return site.isZone(); }), + deviceName, "the device exposes zone sites"); + requireCircuitDevice(device.getQubitsNum() == deviceSites.size(), deviceName, + "the qubit count does not match the regular-site count"); std::vector sites; sites.reserve(deviceSites.size()); diff --git a/mlir/tools/mqt-cc/mqt-cc.cpp b/mlir/tools/mqt-cc/mqt-cc.cpp index 78d643a483..ce43bb8087 100644 --- a/mlir/tools/mqt-cc/mqt-cc.cpp +++ b/mlir/tools/mqt-cc/mqt-cc.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -212,10 +213,22 @@ static llvm::cl::opt decomposeMultiControlledMinQubits( "narrower gates undecomposed."), llvm::cl::init(3)); +/** + * @brief Report a violated QDMI command-line constraint. + */ +[[nodiscard]] static LogicalResult reportQDMIErrorIf(const bool condition, + const Twine& message) { + if (!condition) { + return success(); + } + llvm::errs() << message << "\n"; + return failure(); +} + /** * @brief Configure the QDMI registry before initializing its singleton. */ -static LogicalResult configureQDMIRegistry(const StringRef path) { +[[nodiscard]] static LogicalResult configureQDMIRegistry(const StringRef path) { #ifdef _WIN32 const auto status = _putenv_s("MQT_CORE_QDMI_CONFIG_FILE", path.str().c_str()); @@ -225,12 +238,9 @@ static LogicalResult configureQDMIRegistry(const StringRef path) { setenv("MQT_CORE_QDMI_CONFIG_FILE", path.str().c_str(), 1); // NOLINTEND(misc-include-cleaner) #endif - if (status == 0) { - return success(); - } - llvm::errs() << "Failed to configure the QDMI registry from '" << path - << "'.\n"; - return failure(); + return reportQDMIErrorIf( + status != 0, + Twine("Failed to configure the QDMI registry from '") + path + "'."); } /** @@ -376,25 +386,23 @@ static int runCompiler(int argc, char** argv) { llvm::cl::ParseCommandLineOptions(argc, argv, "MQT Compiler Collection Driver\n"); - if (!qdmiConfig.empty() && configureQDMIRegistry(qdmiConfig).failed()) { + if ((!qdmiConfig.empty() && configureQDMIRegistry(qdmiConfig).failed()) || + reportQDMIErrorIf( + qdmiListDevices && !qdmiDevice.empty(), + "--qdmi-list-devices cannot be combined with --qdmi-device.") + .failed() || + reportQDMIErrorIf( + !qdmiConfig.empty() && !qdmiListDevices && qdmiDevice.empty(), + "--qdmi-config requires --qdmi-device or --qdmi-list-devices.") + .failed()) { return 1; } if (qdmiListDevices) { - if (!qdmiDevice.empty()) { - llvm::errs() - << "--qdmi-list-devices cannot be combined with --qdmi-device.\n"; - return 1; - } for (const auto& id : qdmi::Driver::get().registeredDeviceIds()) { llvm::outs() << id << "\n"; } return 0; } - if (!qdmiConfig.empty() && qdmiDevice.empty()) { - llvm::errs() - << "--qdmi-config requires --qdmi-device or --qdmi-list-devices.\n"; - return 1; - } const auto parsedInputFormat = parseInputFormat(inputFormat, inputFilename); if (!parsedInputFormat) { @@ -410,22 +418,22 @@ static int runCompiler(int argc, char** argv) { std::optional compilerTarget; if (!qdmiDevice.empty()) { - if (*parsedOutputFormat == OutputFormat::QCImport || - *parsedOutputFormat == OutputFormat::QCO || - *parsedOutputFormat == OutputFormat::Jeff) { - llvm::errs() - << "--qdmi-device requires qco-optimized, qc/mlir, qir-base, or " - "qir-adaptive output.\n"; - return 1; - } - if (passPipeline.hasAnyOccurrences()) { - llvm::errs() << "--qdmi-device cannot be combined with --passes.\n"; - return 1; - } - if (enableDecomposeMultiControlled) { - llvm::errs() << "--qdmi-device cannot be combined with " - "--decompose-multi-controlled; target compilation " - "already performs the required decomposition.\n"; + if (reportQDMIErrorIf( + *parsedOutputFormat == OutputFormat::QCImport || + *parsedOutputFormat == OutputFormat::QCO || + *parsedOutputFormat == OutputFormat::Jeff, + "--qdmi-device requires qco-optimized, qc/mlir, qir-base, or " + "qir-adaptive output.") + .failed() || + reportQDMIErrorIf(passPipeline.hasAnyOccurrences(), + "--qdmi-device cannot be combined with --passes.") + .failed() || + reportQDMIErrorIf( + enableDecomposeMultiControlled, + "--qdmi-device cannot be combined with " + "--decompose-multi-controlled; target compilation already " + "performs the required decomposition.") + .failed()) { return 1; } const auto device = fomac::Session::openDevice(qdmiDevice); From ea98c0b78d4f519d02d3b896ccad72a0057698c3 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Tue, 4 Aug 2026 17:14:42 +0200 Subject: [PATCH 5/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Streamline=20QDMI=20in?= =?UTF-8?q?tegration=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Require the bundled providers for full test builds so provider-backed tests can be registered unconditionally. Exercise the real Garnet target in Python while keeping a small direct sparse target for isolated API coverage. Assisted-by: GPT-5.6 via Codex --- .agent/plans/1687-int-final-qdmi-bridge.md | 71 ++++++++++++----- CMakeLists.txt | 6 ++ docs/qdmi/driver.md | 5 +- mlir/unittests/Compiler/CMakeLists.txt | 88 +++++++++------------ test/CMakeLists.txt | 4 +- test/na/CMakeLists.txt | 4 +- test/python/test_mlir.py | 83 ++++++++++--------- test/qdmi/CMakeLists.txt | 6 +- test/qdmi/registry/CMakeLists.txt | 11 +-- test/qdmi/registry/test_device_registry.cpp | 2 - 10 files changed, 149 insertions(+), 131 deletions(-) diff --git a/.agent/plans/1687-int-final-qdmi-bridge.md b/.agent/plans/1687-int-final-qdmi-bridge.md index 72a7222e5a..47b26988fd 100644 --- a/.agent/plans/1687-int-final-qdmi-bridge.md +++ b/.agent/plans/1687-int-final-qdmi-bridge.md @@ -80,8 +80,26 @@ than retaining its historical merge-heavy implementation. review caught and corrected diagnostic-precedence drift before publication; the original fail-fast order and four irreducible CLI tests remain. -- [ ] Publish the signed coverage follow-up and monitor replacement CI to a - terminal result. +- [x] (2026-08-04) Published the signed coverage follow-up as `5d2ad837a`; + Codecov reports 93.1 percent C++ patch coverage, and every completed + replacement check passed before the next review revision superseded it. +- [x] (2026-08-04) Simplified the test-build boundary so the complete test suite + requires all bundled QDMI providers while provider-selective non-test + builds remain supported. Removed the resulting conditional test + registration and replaced the synthetic IQM-like Python fixture with one + real Garnet integration test plus one direct sparse-target API test. +- [x] (2026-08-04) Rebuilt the complete release target and passed all 223 + compiler tests, ten adapter/CLI CTests, 29 Python MLIR tests, 15 registry + tests, 114 driver tests, 263 circuit FoMaC tests, two neutral-atom FoMaC + tests, strict documentation, repository lint, and `git diff --check`. Also + verified that a provider-free non-test build still configures and builds + the adapter and `mqt-cc`, while a test build with providers disabled fails + at configuration with the intended diagnostic. +- [x] (2026-08-04) Completed an independent exact-diff review with no actionable + findings, then regenerated the release build from the current CMake + sources and repeated the complete affected suites. +- [ ] Publish the streamlined test revision and monitor its replacement exact + head. ### Surprises & Discoveries @@ -122,9 +140,10 @@ than retaining its historical merge-heavy implementation. compilation exposed this issue; unit tests that stopped at conformance did not. - Observation: a clean build with all three built-in QDMI providers disabled - exposed that provider-backed test sources and runtime copying must be - conditional. The compiler test target now builds without provider libraries, - while normal CI retains full live-device coverage. + initially motivated conditional provider-backed tests. Follow-up review + established a simpler boundary: the complete MQT Core test suite requires all + bundled providers, while embedded and other non-test builds may still select + or omit providers independently. - Observation: QDMI operation site tuples are ordered, while the compiler deliberately models an undirected topology and homogeneous bidirectional gate support. Canonicalizing a one-way two-qubit site list would silently widen the @@ -213,6 +232,17 @@ than retaining its historical merge-heavy implementation. exception path, and the CLI does not gain redundant tests for every combination of equivalent option errors. Date/Author: 2026-08-04, GPT-5.6 via Codex. +- Decision: retain the three provider build options for embedded and other + non-test consumers, but require every bundled provider when + `BUILD_MQT_CORE_TESTS` is enabled. Rationale: the full suite can register its + provider integration tests unconditionally without removing the useful + provider-free production boundary. Date/Author: 2026-08-04, GPT-5.6 via Codex. +- Decision: use the bundled Garnet target only for the Python QDMI integration + path and retain one small direct sparse target for constructor and typed + compilation coverage. Rationale: the two tests now exercise distinct public + contracts without maintaining a misleading partial IQM model or asserting a + particular placement chosen by the mapper. Date/Author: 2026-08-04, GPT-5.6 + via Codex. ### Outcomes & Retrospective @@ -231,9 +261,12 @@ workflows and packaged Python workflow are proven. A distributable MLIR C++ SDK remains a separate packaging concern because the current repository does not export the compiler dialects, generated headers, or pipeline dependency closure. The replacement branch and PR description are published, all historical review -threads are resolved, and the full exact-head Actions matrix is green. A compact -follow-up is in progress for the remaining C++ patch-coverage gate; fresh -replacement-head CI and human re-review remain pending. +threads are resolved, and the published C++ patch coverage is 93.1 percent. The +latest local revision also makes the full test suite's provider requirement +explicit, removes the resulting conditional registration, and separates real +Garnet integration coverage from direct target-construction coverage. A fresh +independent review found no actionable issue. Replacement-head CI and human +re-review remain pending. ### Context and Orientation @@ -338,10 +371,10 @@ configuration, reject an unknown ID, and compile the Bell program for Garnet. The third milestone proves cohesion and publication readiness. Generate the MLIR reference documentation, run strict Sphinx documentation, changed-source -clang-tidy, complete relevant C++ suites, provider-disabled configuration, stub -generation, and repository lint. An independent exact-head `mqt-pr-review` must -find no material correctness, bloat, packaging, or documentation issue before -the historical PR branch is replaced. +clang-tidy, complete relevant C++ suites, a provider-disabled non-test compiler +build, stub generation, and repository lint. An independent exact-head +`mqt-pr-review` must find no material correctness, bloat, packaging, or +documentation issue before the historical PR branch is replaced. ### Concrete Steps @@ -408,13 +441,13 @@ the adapter, mapping, synthesis, conformance, or full compiler suites. The final revision must build all touched targets, pass the focused and relevant complete tests, regenerate stubs without an uncommitted delta, pass strict documentation, changed-source clang-tidy, repository lint, `git diff --check`, -and an independent exact-head `mqt-pr-review`. A provider-disabled build must -configure and build the compiler test target without expecting unavailable -runtime libraries. The packaged Python extension and QDMI provider assets must -be exercised by the Python test session. The source-build-only C++ adapter and -CLI must be labeled as such; a full installed MLIR SDK consumer is deliberately -outside this thin bridge because the repository does not yet expose that package -boundary. C++ patch coverage must be at least 90 percent in CI. +and an independent exact-head `mqt-pr-review`. A provider-disabled non-test +build must configure and build the compiler adapter and CLI without expecting +unavailable runtime libraries. The packaged Python extension and QDMI provider +assets must be exercised by the Python test session. The source-build-only C++ +adapter and CLI must be labeled as such; a full installed MLIR SDK consumer is +deliberately outside this thin bridge because the repository does not yet expose +that package boundary. C++ patch coverage must be at least 90 percent in CI. Before publication, refresh `origin/main`, the remote #1687 head, review threads, and PR metadata. Replace the historical branch only with: diff --git a/CMakeLists.txt b/CMakeLists.txt index 597afd33a7..893683272d 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -70,6 +70,12 @@ option(BUILD_MQT_CORE_QDMI_SC_DEVICE "Build the MQT Core superconducting QDMI de ${PROJECT_IS_TOP_LEVEL}) option(BUILD_MQT_CORE_DOCUMENTATION "Generate documentation artifacts as part of the CMake build" OFF) +if(BUILD_MQT_CORE_TESTS + AND (NOT BUILD_MQT_CORE_QDMI_DDSIM_DEVICE + OR NOT BUILD_MQT_CORE_QDMI_NA_DEVICE + OR NOT BUILD_MQT_CORE_QDMI_SC_DEVICE)) + message(FATAL_ERROR "BUILD_MQT_CORE_TESTS requires all bundled QDMI devices") +endif() if(APPLE AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU") message( FATAL_ERROR diff --git a/docs/qdmi/driver.md b/docs/qdmi/driver.md index 8c6129c9a8..4fa2eed1a6 100644 --- a/docs/qdmi/driver.md +++ b/docs/qdmi/driver.md @@ -42,8 +42,9 @@ integration tests. The QDMI driver and FoMaC libraries are available independently. Device-free builds can register external device libraries through -[QDMI device configuration](configuration.md). When MQT Core's C++ tests are -enabled, device-specific tests are omitted with their corresponding device. +[QDMI device configuration](configuration.md). Building MQT Core's C++ tests +requires all three bundled devices so that the complete device integration is +tested. ## Python Bindings diff --git a/mlir/unittests/Compiler/CMakeLists.txt b/mlir/unittests/Compiler/CMakeLists.txt index 605aca91ab..aea08a9dcc 100644 --- a/mlir/unittests/Compiler/CMakeLists.txt +++ b/mlir/unittests/Compiler/CMakeLists.txt @@ -6,16 +6,8 @@ # # Licensed under the MIT License -set(compiler_test_sources test_compiler_pipeline.cpp test_compiler_target.cpp) -set(has_compiler_qdmi_provider_tests OFF) -if(TARGET MQT::CoreQDMIScDevice - AND TARGET MQT::CoreQDMI_DDSIM_Device - AND TARGET MQT::CoreQDMINaDevice) - list(APPEND compiler_test_sources test_compiler_fomac_adapter.cpp) - set(has_compiler_qdmi_provider_tests ON) -endif() - -add_executable(mqt-core-mlir-unittests-compiler ${compiler_test_sources}) +add_executable(mqt-core-mlir-unittests-compiler test_compiler_fomac_adapter.cpp + test_compiler_pipeline.cpp test_compiler_target.cpp) target_link_libraries( mqt-core-mlir-unittests-compiler @@ -35,53 +27,49 @@ target_link_libraries( MLIRQuantumComputationPrograms MQT::CoreIR) -if(has_compiler_qdmi_provider_tests) - mqt_copy_qdmi_runtime(mqt-core-mlir-unittests-compiler MQT::CoreQDMIScDevice - MQT::CoreQDMI_DDSIM_Device MQT::CoreQDMINaDevice) - target_compile_definitions( - mqt-core-mlir-unittests-compiler - PRIVATE - MQT_CORE_MLIR_HETEROGENEOUS_SC_CONFIG="${CMAKE_CURRENT_SOURCE_DIR}/Inputs/heterogeneous-sc.json" - MQT_CORE_MLIR_DIRECTIONAL_ONE_WAY_SC_CONFIG="${CMAKE_CURRENT_SOURCE_DIR}/Inputs/directional-one-way-sc.json" - MQT_CORE_MLIR_DIRECTIONAL_TWO_WAY_SC_CONFIG="${CMAKE_CURRENT_SOURCE_DIR}/Inputs/directional-two-way-sc.json" - ) -endif() +mqt_copy_qdmi_runtime(mqt-core-mlir-unittests-compiler MQT::CoreQDMIScDevice + MQT::CoreQDMI_DDSIM_Device MQT::CoreQDMINaDevice) +target_compile_definitions( + mqt-core-mlir-unittests-compiler + PRIVATE + MQT_CORE_MLIR_HETEROGENEOUS_SC_CONFIG="${CMAKE_CURRENT_SOURCE_DIR}/Inputs/heterogeneous-sc.json" + MQT_CORE_MLIR_DIRECTIONAL_ONE_WAY_SC_CONFIG="${CMAKE_CURRENT_SOURCE_DIR}/Inputs/directional-one-way-sc.json" + MQT_CORE_MLIR_DIRECTIONAL_TWO_WAY_SC_CONFIG="${CMAKE_CURRENT_SOURCE_DIR}/Inputs/directional-two-way-sc.json" +) mqt_mlir_configure_unittest_target(mqt-core-mlir-unittests-compiler) gtest_discover_tests(mqt-core-mlir-unittests-compiler PROPERTIES LABELS mqt-mlir-unittests DISCOVERY_TIMEOUT 60) -if(TARGET MQT::CoreQDMIScDevice) - # `mqt-cc` is normally an optional LLVM tool. Build it only as a dependency of the test target - # when its irreducible subprocess tests are enabled. - add_dependencies(mqt-core-mlir-unittests-compiler mqt-cc) +# `mqt-cc` is normally an optional LLVM tool. Build it only as a dependency of the test target when +# its irreducible subprocess tests are enabled. +add_dependencies(mqt-core-mlir-unittests-compiler mqt-cc) - set(mqt_cc_test_input ${CMAKE_CURRENT_SOURCE_DIR}/Inputs/bell.qasm) - add_test(NAME mqt-cc-qdmi-list COMMAND $ --qdmi-list-devices) - set_tests_properties(mqt-cc-qdmi-list PROPERTIES LABELS mqt-mlir-unittests - PASS_REGULAR_EXPRESSION "mqt.sc.iqm.garnet") +set(mqt_cc_test_input ${CMAKE_CURRENT_SOURCE_DIR}/Inputs/bell.qasm) +add_test(NAME mqt-cc-qdmi-list COMMAND $ --qdmi-list-devices) +set_tests_properties(mqt-cc-qdmi-list PROPERTIES LABELS mqt-mlir-unittests PASS_REGULAR_EXPRESSION + "mqt.sc.iqm.garnet") - add_test( - NAME mqt-cc-qdmi-explicit-config - COMMAND - $ - "--qdmi-config=${CMAKE_CURRENT_SOURCE_DIR}/Inputs/mqt-cc-explicit.qdmi.json" - --qdmi-list-devices) - set_tests_properties( - mqt-cc-qdmi-explicit-config - PROPERTIES LABELS mqt-mlir-unittests PASS_REGULAR_EXPRESSION "mqt.sc.iqm.emerald" - FAIL_REGULAR_EXPRESSION "mqt.sc.iqm.garnet") +add_test( + NAME mqt-cc-qdmi-explicit-config + COMMAND + $ + "--qdmi-config=${CMAKE_CURRENT_SOURCE_DIR}/Inputs/mqt-cc-explicit.qdmi.json" + --qdmi-list-devices) +set_tests_properties( + mqt-cc-qdmi-explicit-config + PROPERTIES LABELS mqt-mlir-unittests PASS_REGULAR_EXPRESSION "mqt.sc.iqm.emerald" + FAIL_REGULAR_EXPRESSION "mqt.sc.iqm.garnet") - add_test(NAME mqt-cc-qdmi-unknown COMMAND $ --qdmi-device=unknown - ${mqt_cc_test_input}) - set_tests_properties( - mqt-cc-qdmi-unknown PROPERTIES LABELS mqt-mlir-unittests PASS_REGULAR_EXPRESSION - "mqt-cc failed: Unknown QDMI device ID 'unknown'") +add_test(NAME mqt-cc-qdmi-unknown COMMAND $ --qdmi-device=unknown + ${mqt_cc_test_input}) +set_tests_properties( + mqt-cc-qdmi-unknown PROPERTIES LABELS mqt-mlir-unittests PASS_REGULAR_EXPRESSION + "mqt-cc failed: Unknown QDMI device ID 'unknown'") - add_test(NAME mqt-cc-qdmi-garnet COMMAND $ --qdmi-device=mqt.sc.iqm.garnet - --emit=qco-optimized ${mqt_cc_test_input}) - set_tests_properties( - mqt-cc-qdmi-garnet PROPERTIES LABELS mqt-mlir-unittests PASS_REGULAR_EXPRESSION "qco.r" - FAIL_REGULAR_EXPRESSION "qco.rx|qco.ry") -endif() +add_test(NAME mqt-cc-qdmi-garnet COMMAND $ --qdmi-device=mqt.sc.iqm.garnet + --emit=qco-optimized ${mqt_cc_test_input}) +set_tests_properties( + mqt-cc-qdmi-garnet PROPERTIES LABELS mqt-mlir-unittests PASS_REGULAR_EXPRESSION "qco.r" + FAIL_REGULAR_EXPRESSION "qco.rx|qco.ry") diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a51f3a6127..6203a14e01 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -15,9 +15,7 @@ add_subdirectory(na) add_subdirectory(zx) add_subdirectory(qir) add_subdirectory(qdmi) -if(BUILD_MQT_CORE_QDMI_NA_DEVICE) - add_subdirectory(fomac) -endif() +add_subdirectory(fomac) # copy test circuits to build directory file(COPY ${PROJECT_SOURCE_DIR}/test/circuits DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/test/na/CMakeLists.txt b/test/na/CMakeLists.txt index 017ad42d0d..fa743e93b1 100644 --- a/test/na/CMakeLists.txt +++ b/test/na/CMakeLists.txt @@ -12,6 +12,4 @@ if(TARGET MQT::CoreNA) target_link_libraries(mqt-core-na-test PRIVATE MQT::CoreQASM) endif() -if(BUILD_MQT_CORE_QDMI_NA_DEVICE) - add_subdirectory(fomac) -endif() +add_subdirectory(fomac) diff --git a/test/python/test_mlir.py b/test/python/test_mlir.py index 7e301786d0..1d5e84f20f 100644 --- a/test/python/test_mlir.py +++ b/test/python/test_mlir.py @@ -10,6 +10,7 @@ from __future__ import annotations +import re from pathlib import Path import pytest @@ -289,62 +290,70 @@ def test_compile_program_exposes_raw_and_optimized_qco() -> None: assert raw.ir != optimized.ir -def _iqm_like_target() -> CompilerTarget: - """Construct a two-site target with the IQM gate set. +@pytest.fixture(scope="module") +def garnet_target() -> CompilerTarget: + """Snapshot the bundled IQM Garnet device. Returns: - A target with sparse site IDs, one coupling, and the IQM native gates. + The detached compiler target. """ - return CompilerTarget( - "IQM-like target", - [ - CompilerTarget.Site(10, name="QB1"), - CompilerTarget.Site(20, name="QB2"), - ], - couplings=[(10, 20)], - operations=[ - CompilerTarget.Operation("r", 1, 2), - CompilerTarget.Operation("cz", 2, 0), - CompilerTarget.Operation("measure", 1, 0), - ], - ) - - -def _assert_iqm_native(program: QCOProgram) -> None: - """Check target assignment and the IQM native gate set.""" - assert "qco.static 10" in program.ir - assert "qco.static 20" in program.ir - assert "qco.r(" in program.ir - assert "qco.measure" in program.ir - assert "qco.rx" not in program.ir - assert "qco.ry" not in program.ir + return CompilerTarget.from_device(open_device("mqt.sc.iqm.garnet")) -def test_compile_program_for_target() -> None: - """Compile through the canonical target pipeline.""" +def test_compile_program_for_qdmi_target(garnet_target: CompilerTarget) -> None: + """Compile through the canonical target pipeline for a QDMI device.""" result = compile_program( QASM_STRING, output=OutputFormat.QCO_OPTIMIZED, - target=_iqm_like_target(), + target=garnet_target, ) assert isinstance(result, QCOProgram) - _assert_iqm_native(result) - + static_sites = {int(site) for site in re.findall(r"qco\.static (\d+)", result.ir)} + assert len(static_sites) == 2 + assert static_sites <= {site.id for site in garnet_target.sites} + assert "qco.r(" in result.ir + assert "qco.ctrl" in result.ir + assert "qco.z " in result.ir + assert result.ir.count("qco.measure") == 2 + assert "qco.rx" not in result.ir + assert "qco.ry" not in result.ir + + +def test_qco_program_compiles_for_direct_sparse_target() -> None: + """Expose direct target construction and typed QCO compilation.""" + target = CompilerTarget( + "sparse target", + [CompilerTarget.Site(10), CompilerTarget.Site(20)], + couplings=[(10, 20)], + operations=[ + CompilerTarget.Operation("u", 1, 3), + CompilerTarget.Operation("cz", 2, 0), + CompilerTarget.Operation("measure", 1, 0), + ], + ) + assert target.name == "sparse target" + assert [site.id for site in target.sites] == [10, 20] + assert target.couplings == [(10, 20)] + assert target.synthesis_basis is not None + assert target.synthesis_basis.single_qubit == CompilerTarget.SingleQubitBasis.U + assert target.synthesis_basis.entangler == CompilerTarget.GateKind.CZ -def test_qco_program_compiles_for_target() -> None: - """Expose target compilation on typed QCO programs.""" qco = compile_program(QASM_STRING, output=OutputFormat.QCO) assert isinstance(qco, QCOProgram) - qco.compile_for_target(_iqm_like_target()) + qco.compile_for_target(target) - _assert_iqm_native(qco) + assert {int(site) for site in re.findall(r"qco\.static (\d+)", qco.ir)} == {10, 20} + assert "qco.u(" in qco.ir + assert "qco.ctrl" in qco.ir + assert "qco.z " in qco.ir + assert qco.ir.count("qco.measure") == 2 -def test_compiler_target_snapshots_qdmi_device() -> None: +def test_compiler_target_snapshots_qdmi_device(garnet_target: CompilerTarget) -> None: """Retain IQM topology and calibration independently of the live device.""" - target = CompilerTarget.from_device(open_device("mqt.sc.iqm.garnet")) + target = garnet_target assert target.name == "IQM Garnet" assert target.num_qubits == 20 diff --git a/test/qdmi/CMakeLists.txt b/test/qdmi/CMakeLists.txt index e7408fe449..a1af9424f3 100644 --- a/test/qdmi/CMakeLists.txt +++ b/test/qdmi/CMakeLists.txt @@ -7,9 +7,5 @@ # Licensed under the MIT License add_subdirectory(devices) -if(BUILD_MQT_CORE_QDMI_DDSIM_DEVICE - AND BUILD_MQT_CORE_QDMI_NA_DEVICE - AND BUILD_MQT_CORE_QDMI_SC_DEVICE) - add_subdirectory(driver) -endif() +add_subdirectory(driver) add_subdirectory(registry) diff --git a/test/qdmi/registry/CMakeLists.txt b/test/qdmi/registry/CMakeLists.txt index 5e0f1115ca..0860295347 100644 --- a/test/qdmi/registry/CMakeLists.txt +++ b/test/qdmi/registry/CMakeLists.txt @@ -12,14 +12,5 @@ if(TARGET MQT::CoreQDMIDriver) package_add_test(${TARGET_NAME} MQT::CoreQDMIDriver test_device_registry.cpp) target_include_directories(${TARGET_NAME} PRIVATE ${PROJECT_SOURCE_DIR}/src/qdmi/driver ${PROJECT_SOURCE_DIR}/test) - target_compile_definitions( - ${TARGET_NAME} - PRIVATE - MQT_CORE_QDMI_HAS_ALL_BUILTIN_DEVICES=$,$,$> - ) - if(BUILD_MQT_CORE_QDMI_DDSIM_DEVICE - OR BUILD_MQT_CORE_QDMI_NA_DEVICE - OR BUILD_MQT_CORE_QDMI_SC_DEVICE) - mqt_copy_qdmi_runtime(${TARGET_NAME}) - endif() + mqt_copy_qdmi_runtime(${TARGET_NAME}) endif() diff --git a/test/qdmi/registry/test_device_registry.cpp b/test/qdmi/registry/test_device_registry.cpp index 593066d399..3842d3d5e2 100644 --- a/test/qdmi/registry/test_device_registry.cpp +++ b/test/qdmi/registry/test_device_registry.cpp @@ -342,7 +342,6 @@ TEST(DeviceRegistry, ResolvesRelativeConfigurationPathsBeforeCwdChanges) { "auth.json"); } -#if MQT_CORE_QDMI_HAS_ALL_BUILTIN_DEVICES TEST(DeviceRegistry, DiscoversGeneratedBuildTreeManifests) { const TemporaryDirectory directory; const auto configFile = emptyConfig(directory); @@ -374,7 +373,6 @@ TEST(DeviceRegistry, DiscoversGeneratedBuildTreeManifests) { assertPackagedModel("mqt.sc.iqm.garnet", "iqm-garnet.json"); assertPackagedModel("mqt.sc.iqm.emerald", "iqm-emerald.json"); } -#endif TEST(DeviceRegistry, ReadsProjectConfigurationFromPyprojectToml) { const TemporaryDirectory directory; From dce3302ed2d8d63cee04e34eeac2b7fb9bd62459 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Tue, 4 Aug 2026 21:31:45 +0200 Subject: [PATCH 6/6] =?UTF-8?q?=E2=9A=A1=20Streamline=20compiler=20and=20d?= =?UTF-8?q?ocumentation=20builds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assisted-by: GPT-5.6 via Codex --- .agent/plans/1687-int-final-qdmi-bridge.md | 61 ++++++++++--------- .readthedocs.yaml | 9 +-- mlir/unittests/Compiler/CMakeLists.txt | 32 ---------- mlir/unittests/Compiler/Inputs/bell.qasm | 9 --- .../Compiler/Inputs/mqt-cc-explicit.qdmi.json | 11 ---- noxfile.py | 5 +- 6 files changed, 40 insertions(+), 87 deletions(-) delete mode 100644 mlir/unittests/Compiler/Inputs/bell.qasm delete mode 100644 mlir/unittests/Compiler/Inputs/mqt-cc-explicit.qdmi.json diff --git a/.agent/plans/1687-int-final-qdmi-bridge.md b/.agent/plans/1687-int-final-qdmi-bridge.md index 47b26988fd..67e356d0d1 100644 --- a/.agent/plans/1687-int-final-qdmi-bridge.md +++ b/.agent/plans/1687-int-final-qdmi-bridge.md @@ -25,10 +25,10 @@ circuit sites only. CoreFoMaC remains MLIR-free, `MQTCompilerTarget` remains FoMaC/QDMI/CoreIR-free, and no second target DTO or dynamic driver boundary is introduced. -The observable proof combines focused adapter tests, Python target construction -and compilation tests, and a minimal `mqt-cc` device workflow. The existing pull -request is then rewritten from current `main` as this thin integration rather -than retaining its historical merge-heavy implementation. +The observable proof combines focused adapter tests and Python target +construction and compilation tests. The existing pull request is then rewritten +from current `main` as this thin integration rather than retaining its +historical merge-heavy implementation. ### Progress @@ -98,6 +98,13 @@ than retaining its historical merge-heavy implementation. - [x] (2026-08-04) Completed an independent exact-diff review with no actionable findings, then regenerated the release build from the current CMake sources and repeated the complete affected suites. +- [x] (2026-08-04) Removed the four `mqt-cc` subprocess tests and their + fixtures, leaving the optional tool outside the compiler unit-test + dependency graph. Restored Release documentation builds, reused the + documentation environment, and generated `htmlzip` from the existing HTML + output instead of compiling and rendering the docs twice. Retained a fresh + Sphinx environment because the custom C++ API domain caches each + regenerated Doxygen inventory. - [ ] Publish the streamlined test revision and monitor its replacement exact head. @@ -209,7 +216,9 @@ than retaining its historical merge-heavy implementation. Rationale: this respects the established test layout and proves only option parsing, registry selection, and one compilation workflow without promoting the optional LLVM tool into every default build. Date/Author: 2026-08-04, - GPT-5.6 via Codex. + GPT-5.6 via Codex. Superseded on 2026-08-04: the subprocess coverage and its + build dependency added more CMake complexity than useful independent + assurance, so the tests and fixtures were removed at maintainer request. - Decision: perform generic QCO cleanup before target-native synthesis, retain only CSE and dead-value cleanup afterward, and run conformance last. Rationale: no target-independent canonicalizer may reintroduce a gate outside @@ -231,7 +240,8 @@ than retaining its historical merge-heavy implementation. positive contracts, existing negative adapter tests exercise one shared exception path, and the CLI does not gain redundant tests for every combination of equivalent option errors. Date/Author: 2026-08-04, GPT-5.6 via - Codex. + Codex. Superseded on 2026-08-04 by the later decision to remove all `mqt-cc` + subprocess checks and their unit-test build dependency. - Decision: retain the three provider build options for embedded and other non-test consumers, but require every bundled provider when `BUILD_MQT_CORE_TESTS` is enabled. Rationale: the full suite can register its @@ -248,10 +258,10 @@ than retaining its historical merge-heavy implementation. The implementation is complete and locally validated. It adds one detached adapter rather than another target model, one Python target type, three CLI -options, four irreducible subprocess checks, and no compatibility surface. The -real integration test found and fixed a pass-ordering bug in the merged target -pipeline: generic canonicalization now runs before native synthesis, while -conformance remains the final semantic check. +options, and no compatibility surface. The real integration test found and fixed +a pass-ordering bug in the merged target pipeline: generic canonicalization now +runs before native synthesis, while conformance remains the final semantic +check. Independent review additionally found and corrected one ordered-QDMI-site widening bug at the adapter boundary. Directional and unknown operations now @@ -304,12 +314,12 @@ The Garnet and Emerald configurations are installed from `json/sc/` and are registered as `mqt.sc.iqm.garnet` and `mqt.sc.iqm.emerald`. The neutral-atom default model is useful only to prove the adapter's explicit zone diagnostic. -This task may add the adapter header, source, library, focused tests, minimal -CLI tests under the compiler test root, concise compiler/QDMI workflow -documentation, Python bindings and generated stub updates, the separate -changelog entry, and this ExecPlan. It must not reimplement the target or -pipeline, modify CoreFoMaC to depend on MLIR, add a legacy CoreIR dependency to -the adapter or CLI, or revive historical targeting abstractions. +This task may add the adapter header, source, library, focused tests, concise +compiler/QDMI workflow documentation, Python bindings and generated stub +updates, the separate changelog entry, and this ExecPlan. It must not +reimplement the target or pipeline, modify CoreFoMaC to depend on MLIR, add a +legacy CoreIR dependency to the adapter or CLI, or revive historical targeting +abstractions. ### Plan of Work @@ -343,8 +353,8 @@ opening devices, open only a selected device, snapshot it through the adapter, and pass it to `runDefaultPipeline`. Keep option validation compact and rely on the compiler API for target/output and target/custom-pipeline diagnostics. Link the tool to the adapter and copy the existing runtime beside it. Add a tiny -input under `mlir/unittests/Compiler/Inputs` and only the irreducible list, -unknown-ID, explicit-config, and Garnet compilation checks. +source-build check by building `mqt-cc`; do not promote the optional tool into +the compiler unit-test target or add subprocess tests. Finally document direct C++, Python, and CLI workflows without design history. Refer qubit-reuse users to `mqt-qubit-reuse`, link to existing QDMI registry and @@ -364,10 +374,9 @@ both fail with precise diagnostics. The second milestone exposes the same owned value through Python and the command-line driver. Regenerate `python/mqt/core/mlir.pyi`, run -`test/python/test_mlir.py`, and execute the four `mqt-cc` CTests. At the end, -Python can construct or snapshot a target and compile for it, while a -source-build `mqt-cc` can list devices, apply an explicit registry -configuration, reject an unknown ID, and compile the Bell program for Garnet. +`test/python/test_mlir.py`, and build `mqt-cc`. At the end, Python can construct +or snapshot a target and compile for it, while the command-line integration +remains available without coupling the optional tool to the unit-test build. The third milestone proves cohesion and publication readiness. Generate the MLIR reference documentation, run strict Sphinx documentation, changed-source @@ -391,10 +400,10 @@ Build the adapter, compiler tests, MLIR Python extension, and CLI: MQTCompilerFoMaCAdapter mqt-core-mlir-unittests-compiler \ mqt-core-mlir-bindings mqt-cc -j 8 -Run focused C++ and CLI CTest selections: +Run the focused adapter CTest selection: .agent/run.sh ctest --test-dir build/release \ - --output-on-failure -R 'CompilerFoMaCAdapter|mqt-cc' + --output-on-failure -R 'CompilerFoMaCAdapter' Run focused Python tests, regenerate the authoritative stub, and build strict documentation: @@ -434,10 +443,6 @@ The Python tests must prove direct construction, immutable metadata access, `from_device`, detached lifetime, `compile_for_target`, and optional-target `compile_program`. -The CLI tests must prove listing devices, unknown identifiers, explicit registry -configuration, and one successful Garnet compilation. They need not duplicate -the adapter, mapping, synthesis, conformance, or full compiler suites. - The final revision must build all touched targets, pass the focused and relevant complete tests, regenerate stubs without an uncommitted delta, pass strict documentation, changed-source clang-tidy, repository lint, `git diff --check`, diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 0a94109b5c..6df4fa5f72 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -33,12 +33,13 @@ build: - asdf install uv latest - asdf global uv latest # Install MLIR - - curl -LsSf https://github.com/munich-quantum-software/setup-mlir/releases/latest/download/setup-mlir.sh | bash -s -- -v 22.1.7 -p $HOME/mlir + - curl -LsSf https://github.com/munich-quantum-software/setup-mlir/releases/download/v1.4.1/setup-mlir.sh | bash -s -- -v 22.1.7 -p $HOME/mlir build: html: - MLIR_DIR=$HOME/mlir/lib/cmake/mlir uvx nox --non-interactive -s docs - mkdir -p $READTHEDOCS_OUTPUT/html && cp -R docs/_build/html/. $READTHEDOCS_OUTPUT/html htmlzip: - - MLIR_DIR=$HOME/mlir/lib/cmake/mlir uvx nox --non-interactive -s docs -- -b dirhtml - - mkdir -p $READTHEDOCS_OUTPUT/htmlzip - - zip -r $READTHEDOCS_OUTPUT/htmlzip/html.zip docs/_build/dirhtml/* + - | + mkdir -p $READTHEDOCS_OUTPUT/htmlzip + archive=$(realpath $READTHEDOCS_OUTPUT/htmlzip/html.zip) + (cd docs/_build/html && zip -r $archive .) diff --git a/mlir/unittests/Compiler/CMakeLists.txt b/mlir/unittests/Compiler/CMakeLists.txt index aea08a9dcc..f2708c1c67 100644 --- a/mlir/unittests/Compiler/CMakeLists.txt +++ b/mlir/unittests/Compiler/CMakeLists.txt @@ -41,35 +41,3 @@ mqt_mlir_configure_unittest_target(mqt-core-mlir-unittests-compiler) gtest_discover_tests(mqt-core-mlir-unittests-compiler PROPERTIES LABELS mqt-mlir-unittests DISCOVERY_TIMEOUT 60) - -# `mqt-cc` is normally an optional LLVM tool. Build it only as a dependency of the test target when -# its irreducible subprocess tests are enabled. -add_dependencies(mqt-core-mlir-unittests-compiler mqt-cc) - -set(mqt_cc_test_input ${CMAKE_CURRENT_SOURCE_DIR}/Inputs/bell.qasm) -add_test(NAME mqt-cc-qdmi-list COMMAND $ --qdmi-list-devices) -set_tests_properties(mqt-cc-qdmi-list PROPERTIES LABELS mqt-mlir-unittests PASS_REGULAR_EXPRESSION - "mqt.sc.iqm.garnet") - -add_test( - NAME mqt-cc-qdmi-explicit-config - COMMAND - $ - "--qdmi-config=${CMAKE_CURRENT_SOURCE_DIR}/Inputs/mqt-cc-explicit.qdmi.json" - --qdmi-list-devices) -set_tests_properties( - mqt-cc-qdmi-explicit-config - PROPERTIES LABELS mqt-mlir-unittests PASS_REGULAR_EXPRESSION "mqt.sc.iqm.emerald" - FAIL_REGULAR_EXPRESSION "mqt.sc.iqm.garnet") - -add_test(NAME mqt-cc-qdmi-unknown COMMAND $ --qdmi-device=unknown - ${mqt_cc_test_input}) -set_tests_properties( - mqt-cc-qdmi-unknown PROPERTIES LABELS mqt-mlir-unittests PASS_REGULAR_EXPRESSION - "mqt-cc failed: Unknown QDMI device ID 'unknown'") - -add_test(NAME mqt-cc-qdmi-garnet COMMAND $ --qdmi-device=mqt.sc.iqm.garnet - --emit=qco-optimized ${mqt_cc_test_input}) -set_tests_properties( - mqt-cc-qdmi-garnet PROPERTIES LABELS mqt-mlir-unittests PASS_REGULAR_EXPRESSION "qco.r" - FAIL_REGULAR_EXPRESSION "qco.rx|qco.ry") diff --git a/mlir/unittests/Compiler/Inputs/bell.qasm b/mlir/unittests/Compiler/Inputs/bell.qasm deleted file mode 100644 index 74a35eaecf..0000000000 --- a/mlir/unittests/Compiler/Inputs/bell.qasm +++ /dev/null @@ -1,9 +0,0 @@ -OPENQASM 3.0; -include "stdgates.inc"; - -qubit[2] q; -bit[2] result; - -h q[0]; -cx q[0], q[1]; -result = measure q; diff --git a/mlir/unittests/Compiler/Inputs/mqt-cc-explicit.qdmi.json b/mlir/unittests/Compiler/Inputs/mqt-cc-explicit.qdmi.json deleted file mode 100644 index c5f5f7c450..0000000000 --- a/mlir/unittests/Compiler/Inputs/mqt-cc-explicit.qdmi.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "schema-version": 1, - "qdmi": { - "devices": [ - { - "id": "mqt.sc.iqm.garnet", - "enabled": false - } - ] - } -} diff --git a/noxfile.py b/noxfile.py index 971477ed82..48b962fbf4 100755 --- a/noxfile.py +++ b/noxfile.py @@ -139,7 +139,7 @@ def qiskit(session: nox.Session) -> None: session.run("uv", "pip", "show", "qiskit", env=env) -@nox.session(python="3.14", reuse_venv=False) +@nox.session(python="3.14", reuse_venv=True) def docs(session: nox.Session) -> None: """Build the docs. Use "--non-interactive" to avoid serving. Pass "-b linkcheck" to check links.""" parser = argparse.ArgumentParser() @@ -152,8 +152,7 @@ def docs(session: nox.Session) -> None: env = { "UV_PROJECT_ENVIRONMENT": session.virtualenv.location, - # Favor fast compilation for this short-lived documentation build. - "SKBUILD_CMAKE_BUILD_TYPE": "Debug", + "SKBUILD_CMAKE_BUILD_TYPE": "Release", # Let scikit-build-core generate the MLIR reference pages while it # builds the extension used to execute the documentation examples. # Header-set verification and IPO remain enabled by default elsewhere.