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..67e356d0d1 --- /dev/null +++ b/.agent/plans/1687-int-final-qdmi-bridge.md @@ -0,0 +1,520 @@ +# 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 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 + +- [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`. +- [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. +- [x] (2026-08-04) Committed the reviewed ordered-site fix as `2d562a2fa` and + completed a fresh independent exact-head verification with no material + findings. +- [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. +- [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. +- [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. + +### 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 + 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 + 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. +- 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 + +- 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: 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 + 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. 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 + 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. +- 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. 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 + 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 + +The implementation is complete and locally validated. It adds one detached +adapter rather than another target model, one Python target type, three CLI +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 +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. +The replacement branch and PR description are published, all historical review +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 + +`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, 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 +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 +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 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 +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 + +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 the focused adapter CTest selection: + + .agent/run.sh ctest --test-dir build/release \ + --output-on-failure -R 'CompilerFoMaCAdapter' + +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. 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 +`compile_program`. + +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 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: + + 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 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 +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/.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/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/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/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..bfb5cc6447 --- /dev/null +++ b/docs/mlir/target_compilation.md @@ -0,0 +1,104 @@ +# 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 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 +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/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/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..3471390f9b --- /dev/null +++ b/mlir/lib/Compiler/FoMaCAdapter.cpp @@ -0,0 +1,301 @@ +/* + * 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 +#include +#include +#include + +namespace mlir { + +static void requireAdapterInput(const bool condition, + const llvm::Twine& message) { + if (!condition) { + throw std::invalid_argument(message.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 + ")"); +} + +[[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) { + return first < second ? CompilerTarget::Coupling{first, second} + : CompilerTarget::Coupling{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; + return llvm::checkedMulUnsigned(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, + const std::vector& deviceSites, + const std::optional>& couplings, + const llvm::StringRef deviceName) { + if (!flattenedSites) { + 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(); + 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()); + 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()); + 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; + } + + 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()); + 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()); + coversTarget = expected && supportedCouplings.size() == *expected; + } else { + llvm::DenseSet expectedCouplings; + expectedCouplings.reserve(couplings->size()); + for (const auto& [first, second] : *couplings) { + expectedCouplings.insert(canonicalCoupling(first, second)); + } + coversTarget = + supportedCouplings.size() == expectedCouplings.size() && + std::ranges::all_of(expectedCouplings, [&](const auto& coupling) { + return supportedCouplings.contains(coupling); + }); + } + 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"); + + 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) { + 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) { + requireCircuitDevice(!operation.isZoned(), 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(); + 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()); + 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..ce43bb8087 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,10 +22,12 @@ #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 #include +#include #include #include #include @@ -53,6 +58,7 @@ #include #include +#include #include #include #include @@ -84,6 +90,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 +213,36 @@ 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. + */ +[[nodiscard]] 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 + return reportQDMIErrorIf( + status != 0, + Twine("Failed to configure the QDMI registry from '") + path + "'."); +} + /** * @brief Load and parse a `.qasm` file */ @@ -335,6 +386,24 @@ static int runCompiler(int argc, char** argv) { llvm::cl::ParseCommandLineOptions(argc, argv, "MQT Compiler Collection Driver\n"); + 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) { + for (const auto& id : qdmi::Driver::get().registeredDeviceIds()) { + llvm::outs() << id << "\n"; + } + return 0; + } + const auto parsedInputFormat = parseInputFormat(inputFormat, inputFilename); if (!parsedInputFormat) { llvm::errs() << "Could not determine the input format for '" @@ -347,6 +416,30 @@ static int runCompiler(int argc, char** argv) { return 1; } + std::optional compilerTarget; + if (!qdmiDevice.empty()) { + 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); + compilerTarget.emplace(compilerTargetFromDevice(device)); + } + // Set up MLIR context with all required dialects DialectRegistry registry; registry @@ -422,6 +515,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..f2708c1c67 100644 --- a/mlir/unittests/Compiler/CMakeLists.txt +++ b/mlir/unittests/Compiler/CMakeLists.txt @@ -6,11 +6,13 @@ # # Licensed under the MIT License -add_executable(mqt-core-mlir-unittests-compiler test_compiler_pipeline.cpp test_compiler_target.cpp) +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 PRIVATE GTest::gtest_main + MQTCompilerFoMaCAdapter MQTCompilerTarget MQTCompilerPipeline MLIRQCTranslation @@ -25,6 +27,16 @@ target_link_libraries( MLIRQuantumComputationPrograms MQT::CoreIR) +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 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 new file mode 100644 index 0000000000..3092e030db --- /dev/null +++ b/mlir/unittests/Compiler/Inputs/heterogeneous-sc.json @@ -0,0 +1,35 @@ +{ + "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/test_compiler_fomac_adapter.cpp b/mlir/unittests/Compiler/test_compiler_fomac_adapter.cpp new file mode 100644 index 0000000000..140549b5a2 --- /dev/null +++ b/mlir/unittests/Compiler/test_compiler_fomac_adapter.cpp @@ -0,0 +1,157 @@ +/* + * 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 +#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, 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 { + 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/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. 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/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 072c65187a..1d5e84f20f 100644 --- a/test/python/test_mlir.py +++ b/test/python/test_mlir.py @@ -10,13 +10,16 @@ from __future__ import annotations +import re from pathlib import Path 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 +290,101 @@ def test_compile_program_exposes_raw_and_optimized_qco() -> None: assert raw.ir != optimized.ir +@pytest.fixture(scope="module") +def garnet_target() -> CompilerTarget: + """Snapshot the bundled IQM Garnet device. + + Returns: + The detached compiler target. + """ + return CompilerTarget.from_device(open_device("mqt.sc.iqm.garnet")) + + +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=garnet_target, + ) + + assert isinstance(result, QCOProgram) + 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 + + qco = compile_program(QASM_STRING, output=OutputFormat.QCO) + assert isinstance(qco, QCOProgram) + + qco.compile_for_target(target) + + 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(garnet_target: CompilerTarget) -> None: + """Retain IQM topology and calibration independently of the live device.""" + target = garnet_target + + 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) 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;