From 78ae24ab6360494391306684658e9c9bbdb51b3a Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Mon, 3 Aug 2026 16:02:50 +0200 Subject: [PATCH 1/6] =?UTF-8?q?=E2=9C=A8=20Split=20target=20optimization?= =?UTF-8?q?=20from=20native=20synthesis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the duplicate native-gate menu with a minimal CompilerTarget basis adapter and independent pre-routing optimization, post-routing synthesis, and final conformance passes. Keep exact operation and ordered-locus support authoritative, including native SWAPs, reverse-only symmetric entanglers, and modifier shells. Co-authored-by: Simon Hofmann Assisted-by: GPT-5.6 via Codex --- .agent/plans/1687-syn-01-target-synthesis.md | 417 ++++++++++ CHANGELOG.md | 6 +- bindings/mlir/register_mlir.cpp | 6 - mlir/include/mlir/Compiler/Programs.h | 3 - .../Transforms/Decomposition/NativeGateset.h | 93 --- .../Transforms/Decomposition/SynthesisBasis.h | 43 + .../QCO/Transforms/Decomposition/Weyl.h | 13 +- .../mlir/Dialect/QCO/Transforms/Passes.h | 23 + .../mlir/Dialect/QCO/Transforms/Passes.td | 74 -- mlir/lib/Compiler/Programs.cpp | 15 - .../Decomposition/NativeGateset.cpp | 274 ------- .../Decomposition/SynthesisBasis.cpp | 125 +++ .../QCO/Transforms/Decomposition/Weyl.cpp | 91 ++- .../FuseTwoQubitUnitaryRuns.cpp | 475 ----------- .../NativeSynthesis/TargetSynthesis.cpp | 765 ++++++++++++++++++ mlir/lib/Support/Passes.cpp | 1 - mlir/tools/mqt-cc/mqt-cc.cpp | 26 - .../Compiler/test_compiler_pipeline.cpp | 105 --- .../Decomposition/test_weyl_decomposition.cpp | 411 ++-------- .../Transforms/NativeSynthesis/CMakeLists.txt | 8 +- .../test_fuse_two_qubit_unitary_runs.cpp | 758 ----------------- .../NativeSynthesis/test_target_synthesis.cpp | 565 +++++++++++++ python/mqt/core/mlir.pyi | 3 - test/python/test_mlir.py | 14 - 24 files changed, 2063 insertions(+), 2251 deletions(-) create mode 100644 .agent/plans/1687-syn-01-target-synthesis.md delete mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeGateset.h create mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/SynthesisBasis.h delete mode 100644 mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeGateset.cpp create mode 100644 mlir/lib/Dialect/QCO/Transforms/Decomposition/SynthesisBasis.cpp delete mode 100644 mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp create mode 100644 mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp delete mode 100644 mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp create mode 100644 mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp diff --git a/.agent/plans/1687-syn-01-target-synthesis.md b/.agent/plans/1687-syn-01-target-synthesis.md new file mode 100644 index 0000000000..40e7eb128c --- /dev/null +++ b/.agent/plans/1687-syn-01-target-synthesis.md @@ -0,0 +1,417 @@ +# Split target-independent optimization from target-native synthesis + +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 + +Two-qubit optimization and hardware lowering must answer different questions. +Before routing, the compiler should rewrite a constant unitary window only when +doing so strictly reduces its two-qubit gate count, without choosing a hardware +basis. After routing, the compiler should lower operations that the real +`mlir::CompilerTarget` does not support at their mapped provider sites and +remove routing SWAPs on targets that do not declare SWAP native. A final, +independently runnable pass must reject any operation that is not legal for its +actual type, arity, parameter count, provider site IDs, and ordered locus. + +After this change, C++ pipeline code can construct these three stages +independently. Focused tests demonstrate a profitable CX cancellation, preserve +isolated and runtime-parameterized gates before routing, lower SWAP to a +target-selected U/CX basis after routing, and reject direction, operation, +arity, parameter, and site mismatches. + +## Progress + +- [x] (2026-08-03 13:09Z) Verified the assigned isolated worktree, clean status, + initial CT-01 development base `b6eb95521cb76224c137496f113f38b9ce295854`, + and repository policy; read `AGENTS.md`, `docs/ai_usage.md`, + `.agent/PLANS.md`, and the MQT PR remediation instructions. +- [x] (2026-08-03 13:42Z) Inspected `CompilerTarget`, the native-menu parser, + Euler and Weyl decomposition, two-qubit run scanning, mapping output, + compiler and Python APIs, focused tests, the changelog, and Simon + Hofmann's useful #1969 commits. +- [x] (2026-08-03 15:35Z) Removed the duplicate gate enum/parser and obsolete + textual menu, CLI, C++ `QCOProgram`, Python binding/stub, and + menu-specific tests. Added only a minimal decomposition adapter from + `CompilerTarget::SynthesisBasis`. +- [x] (2026-08-03 16:18Z) Implemented independently constructible pre-routing + optimization, target-native synthesis, and target-conformance passes. +- [x] (2026-08-03 17:02Z) Replaced the broad menu suite with sixteen focused + stage-contract tests and built and ran the target-synthesis, + decomposition, and compiler unit-test binaries successfully. +- [x] (2026-08-03 18:33Z) Updated the existing Unreleased changelog entry and + completed source formatting, changed-file checks, repository lint, and + initial diff checks. +- [x] (2026-08-03 19:01Z) Resolved all three blockers from independent read-only + review: reverse-only symmetric-entangler loci, native SWAP authority, and + native `qco.pow` body handling. The reviewer approved the remediated code + and the focused suite passes all sixteen tests. +- [x] (2026-08-03 19:20Z) Rebuilt and reran all three affected suites, reran + changed-file hooks and full repository lint, completed focused + `clang-tidy` checks and the final diff audit, and prepared one signed + atomic commit for local handoff without publication. +- [x] (2026-08-03 19:30Z) Restacked the sole SYN commit without content + conflicts onto the final CT-01 squash + `f775395a25fddba0a3b54996416d1311bc6ebe71`. The old and restacked commits + have the same stable patch ID. +- [x] (2026-08-03 19:45Z) Repeated the requested builds, 16/199/215 tests, + interface and stub checks, focused `clang-tidy`, hooks, lint, + stale-surface search, and diff audits on the final CT-01 base, then + amended this evidence into the single signed commit. + +## Milestones + +### Milestone 1: Make `CompilerTarget` the only capability model + +The completed first milestone removes +`mlir/Dialect/QCO/Transforms/Decomposition/NativeGateset.h` and its source. Gate +identity, provider aliases, arity, parameter counts, global capability checks, +and basis selection now remain in `mlir/include/mlir/Compiler/Target.h` and +`mlir/lib/Compiler/Target.cpp`. The new decomposition-only +`NativeSynthesisBasis` adapts the target-selected single-qubit basis to +`EulerBasis` and caches a Weyl decomposer for the target-selected entangler. It +contains no parser, menu, aliases, target classification, or independent basis +selection. + +The generated `fuse-two-qubit-unitary-runs` pass and all of its string-menu +surfaces are removed rather than retained as a synthetic target. The later PIPE +slice owns high-level target pipeline composition, so this slice exposes only +the three typed C++ pass factories. + +### Milestone 2: Separate optimization, lowering, and verification + +The completed pre-routing stage scans maximal constant windows on one pair of +linear QCO wires. It evaluates a canonical U/CX Weyl decomposition and rewrites +only when the synthesized entangler count is strictly smaller than the window's +original two-qubit operation count. It does not accept a target and does not +rewrite isolated or runtime-parameterized gates. + +The completed post-routing stage first asks the supplied `CompilerTarget` +whether each one- or two-qubit unitary is supported at its ordered provider +locus. Supported operations, including supported runtime-parameterized gates, +remain untouched. Unsupported constant operations and all ordinary `qco.swap` +operations that the target does not support are lowered through the target's +globally usable synthesis basis. A target-native SWAP remains untouched. For a +symmetric basis entangler available only at the reverse ordered locus, emission +reverses the entangler operands while preserving logical wire identity. The pass +asks for the basis only after finding an actual lowering need, so an incomplete +target can still accept an already-conforming program. An unsupported runtime +gate receives an operation-local matrix diagnostic. + +The completed conformance stage traces each qubit operand back to `qco.static`, +including values passing through QCO `if`/`index_switch` and SCF `for`/`while` +regions. It then calls `CompilerTarget::supports` on the real operation and +ordered provider site IDs. It checks unitary, measurement, and reset operations +and reports the actual operation spelling, arity, parameter count, and locus. + +### Milestone 3: Prove the contracts and hand off one atomic change + +The focused target-synthesis test binary now proves the three stage boundaries, +including unitary equivalence for profitable optimization, SWAP lowering, and a +reverse-only symmetric entangler. It also proves that native SWAP and `qco.pow` +shells remain untouched. The decomposition and compiler suites prove that typed +bases still cover every supported entangler and that removing the old high-level +menu API does not break the remaining compiler pipeline. Changelog curation, +formatting and lint, and independent review are complete. After CT-01 was +squash-merged, the same patch restacked without content conflicts onto the final +target implementation. Post-restack validation and the final signed amend +complete this milestone. + +This slice must not add `compileForTarget`, alter default pipeline composition, +or remove the coupling-only `placeAndRoute` overload; those are PIPE +responsibilities. It must not push, open or modify a pull request, or mutate +GitHub without separate authorization. + +## Surprises & Discoveries + +- Observation: The old two-qubit pass performed four jobs: single-qubit + target-basis lowering, two-qubit optimization, isolated two-qubit lowering, + and residual menu checking. Evidence: its `hasNonNativeGate` condition could + rewrite a pre-routing window without reducing the entangler count. +- Observation: CT-01 already recognizes all fifteen gates understood by the + deleted menu, including provider aliases and the same entangler preference. + The old `NativeGateset` duplicated the enum, parser switch, basis resolution, + and operation classifier. +- Observation: Mapping emits `qco.static` with hardware identifiers, while + `CompilerTarget` permits sparse provider IDs. Evidence: the conformance tests + use sites 10 and 20 and distinguish loci `[10, 20]` and `[20, 10]`. +- Observation: Applying an MLIR greedy rewrite driver can reorder an unrelated + constant even when the quantum pattern does not match. The preservation test + therefore asserts that the isolated SWAP remains and no entangler is + introduced, rather than requiring byte-identical generic IR. +- Observation: Initial configuration required network access to fetch pinned + repository dependencies. Once fetched into the worktree-local build tree, + focused compilation and tests were repeatable without source workarounds. +- Observation: The first post-restack stub check selected the Python-packaged + CMake launcher and an unpinned MLIR, then failed during configuration. + Pointing the check at the real CMake binary and local LLVM/MLIR 22.1.3 + installation built the bindings, regenerated every stub, and left the tracked + stubs unchanged. +- Observation: Pull request #1969 established the useful progressive ordering of + decomposition, optional routing, and late native synthesis, with a test that + routed SWAPs disappear. Its `targetNative` Python duck typing and coupling CLI + are intentionally excluded. +- Observation: `CompilerTarget` deliberately accepts an operand-symmetric + entangler as globally usable when either ordered orientation is available, + while exact operation support remains order-sensitive. Target-native emission + must therefore choose the supported orientation per mapped locus. +- Observation: `qco.pow`, like `qco.ctrl` and `qco.inv`, is a target-visible + unitary shell with a region body. Synthesis and conformance must classify the + shell and skip its implementation body. +- Observation: Independent review also identified non-blocking follow-up + opportunities: failure-atomic preflight, sink-only source validation, + dedicated `index_switch`/`while` regressions, all single-qubit adapter bases, + and more legacy scanner regressions. These are classified as future hardening + rather than expanding this slice after its requested contracts pass. + +## Decision Log + +- Decision: Remove the native-menu pass and all high-level menu APIs in this + slice instead of retaining a compatibility adapter. Rationale: the series + intentionally moves callers to a typed `CompilerTarget`; a synthetic menu + target would preserve the parallel configuration model this change removes. + Date/Author: 2026-08-03, GPT-5.6 via Codex. +- Decision: Keep `NativeSynthesisBasis` as a minimal decomposition-only adapter + directly constructed from `CompilerTarget::SynthesisBasis`. Rationale: Euler + emission and cached Weyl decomposition need transform-specific values, but + target capability semantics and basis selection must remain centralized. + Date/Author: 2026-08-03, GPT-5.6 via Codex. +- Decision: Measure pre-routing profitability by a strict reduction in two-qubit + basis uses and materialize a canonical U/CX sequence only after the comparison + succeeds. Rationale: two-qubit operations drive routing cost, and selecting a + hardware basis before routing would conflate optimization with target + legality. Date/Author: 2026-08-03, GPT-5.6 via Codex. +- Decision: Let `CompilerTarget::supports` decide whether an ordinary `qco.swap` + requires post-routing lowering. Rationale: the target is the sole capability + authority; routing SWAPs still lower on ordinary targets that do not report + SWAP, while a target-native SWAP must remain legal even when the target has no + global synthesis basis. Date/Author: 2026-08-03, GPT-5.6 via Codex. +- Decision: For an operand-symmetric target basis supported only at the reverse + ordered locus, emit each entangler with reversed operands and map its results + back to the original logical wires. Rationale: global target basis selection + accepts either orientation for symmetric gates, but final conformance checks + the exact provider order. Date/Author: 2026-08-03, GPT-5.6 via Codex. +- Decision: Do not require `CompilerTarget::synthesisBasis()` at pass + construction or pass entry. Rationale: absent operations mean all operations + are native, and an incomplete explicit target can still describe a conforming + program. Missing-basis failure matters only after an unsupported operation + actually needs lowering. Date/Author: 2026-08-03, GPT-5.6 via Codex. +- Decision: Keep optimization, target-native synthesis, and conformance as + separate manual factories rather than textual passes. Rationale: + `CompilerTarget` is an immutable typed C++ value that cannot be faithfully + represented by generic pass options, and separate factories make each stage + independently testable and benchmarkable. Date/Author: 2026-08-03, GPT-5.6 via + Codex. +- Decision: Preserve Simon Hofmann's authorship in the final commit. Rationale: + the implementation materially carries forward the progressive post-routing + behavior and SWAP-removal coverage from his #1969 work while excluding the + unapproved Python and CLI designs. Date/Author: 2026-08-03, GPT-5.6 via Codex. + +## Outcomes & Retrospective + +The implementation now has one capability authority and three separately +observable transform stages. On the final CT-01 base, sixteen focused tests pass +along with all 199 decomposition and 215 compiler tests. The relevant CMake +interface-header targets, CLI build and surface checks, and full Python stub +regeneration pass; regeneration leaves the tracked stubs unchanged. Independent +review approved the remediated code with no remaining code blockers. +Changed-file hooks, full repository lint, stale-surface search, and +`git diff --check` pass. Focused `clang-tidy` is clean for the new synthesis +source, basis adapter, and both modified test sources; the Weyl source reports +only three pre-existing warnings on unchanged header lines. The result is +contained in one signed local commit directly on the final CT-01 squash and is +not pushed. + +The main design lesson is that target support and resynthesis profitability must +not share a configuration surface. The canonical U/CX optimizer is useful +without hardware knowledge, while post-routing lowering and conformance require +the exact target and provider-site order. + +## Context and Orientation + +`mlir/include/mlir/Compiler/Target.h` defines the immutable target. Its +`supports(Operation*, locus)` query recognizes QCO operation semantics and +checks provider site IDs, ordered loci, arity, and parameter count. Its +`synthesisBasis()` query returns one globally usable single-qubit basis and +entangler only when both exist. + +`mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp` contains +the constant-window scanner, three pass implementations, provider-site tracing, +and diagnostics. Public factory declarations live in +`mlir/include/mlir/Dialect/QCO/Transforms/Passes.h`. + +`mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/SynthesisBasis.h` and +its source adapt the typed target basis for the existing Euler and Weyl +emitters. `mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp` emits the +selected entangler and single-qubit factors. + +QCO qubits use linear static single assignment: each operation consumes a qubit +value and returns its successor. A mapped hardware qubit starts at `qco.static`; +structured control flow passes the value through block arguments and results. +Conformance must trace that value chain rather than interpret SSA positions as +hardware IDs. + +Focused tests are in +`mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp`. +Typed basis coverage remains in +`mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp`. + +## Plan of Work + +The implementation first deletes the duplicate gateset files and rewires Weyl +synthesis to the target-derived adapter. It then removes the obsolete generated +pass, registration, CLI option, `QCOProgram` method, binding, stub, and their +menu tests. + +The new source retains the proven two-qubit window scanner but separates its +uses. The optimizer compares original and canonical entangler counts. Target +synthesis classifies actual mapped operations and rewrites only lowering needs. +Conformance performs an independent read-only walk and exact target query. + +The final work updates the existing Unreleased changelog entry without inventing +a pull-request reference or upgrade note, formats changed sources, runs focused +and repository-required validation, obtains an independent read-only review, +addresses findings, and creates one signed commit with `Assisted-by` and Simon +Hofmann `Co-authored-by` trailers. + +## Concrete Steps + +Run all commands from the repository root in the task's isolated worktree. +Configure with the repository's release preset and an LLVM/MLIR 22 installation: + + .agent/run.sh env MLIR_DIR= \ + cmake --preset release + +Build and run the three affected C++ suites: + + .agent/run.sh cmake --build build/release --target \ + mqt-core-mlir-unittest-target-synthesis \ + mqt-core-mlir-unittest-decomposition \ + mqt-core-mlir-unittests-compiler + + .agent/run.sh \ + build/release/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/\ + mqt-core-mlir-unittest-target-synthesis + + .agent/run.sh \ + build/release/mlir/unittests/Dialect/QCO/Transforms/Decomposition/\ + mqt-core-mlir-unittest-decomposition + + .agent/run.sh \ + build/release/mlir/unittests/Compiler/\ + mqt-core-mlir-unittests-compiler + +The observed summaries are: + + [ PASSED ] 16 tests. + [ PASSED ] 199 tests. + [ PASSED ] 215 tests. + +Finish validation with changed-file hooks, the repository lint suite, and diff +checks: + + .agent/run.sh prek run --files + .agent/run.sh uvx nox -s lint + .agent/run.sh git diff --check + .agent/run.sh git status --short + +## Validation and Acceptance + +Acceptance requires the following observable behavior: + +- Two adjacent constant CX operations optimize away with equivalent unitary + behavior, while a three-CX SWAP form, an isolated SWAP, and a runtime RXX run + remain quantum-structurally unchanged before routing. +- Target-native synthesis removes unsupported ordinary SWAP and produces only + operations accepted by a U/CX target, preserving the complete unitary. A + target-native SWAP remains unchanged without requiring a synthesis basis. +- A reverse-only symmetric entangler is emitted at the supported ordered locus + while preserving complete unitary behavior, and a native `qco.pow` shell is + checked without separately rejecting its implementation body. +- An absent operation set succeeds without synthesis. An explicit incomplete + target succeeds for supported operations and reports “no globally usable + synthesis basis” only when an unsupported operation actually needs lowering. +- A supported runtime-parameterized gate remains unchanged. An unsupported + runtime gate reports that its unitary matrix is unavailable at compile time. +- Conformance distinguishes sparse provider IDs and ordered direction, rejects + operation-type, arity, parameter-count, unknown-site, and measurement + mismatches, and traces QCO and SCF structured control flow. +- The duplicate enum/parser and all native-menu text, CLI, C++ program, Python, + and generated pass surfaces are absent. +- Focused tests, decomposition tests, compiler tests, changed-file checks, + repository lint, and `git diff --check` pass. +- The final commit is signed, carries the required `Assisted-by` and + `Co-authored-by: Simon Hofmann ` trailers, and is not + pushed. + +## Idempotence and Recovery + +Configuration, builds, tests, and hooks are repeatable in the task-local build +and cache directories. A failed pass test uses a disposable module fixture. +Network fetch failures during initial configuration should be retried with +authorized network access; source behavior must not be weakened to accommodate +an environment boundary. + +Do not reset or clean the worktree. Inspect status and remove only +task-generated artifacts if necessary. Do not modify another task's worktree or +shared branch metadata. External publication remains unauthorized. + +## Artifacts and Notes + +Initial development used CT-01 commit +`b6eb95521cb76224c137496f113f38b9ce295854`. The final one-commit SYN patch is +restacked directly onto the squash-merged CT-01 base +`f775395a25fddba0a3b54996416d1311bc6ebe71`. The stable patch ID before and after +the restack is `fb73dd9d29254dfca2edf68df7eb9bf983284cc4`, proving that no SYN +patch content changed during the history rewrite. + +Relevant #1969 source commits are `3be6d8e43` for target-native naming and +`1aa4c975b` for progressive post-routing synthesis and SWAP-removal coverage, +both authored by Simon Hofmann. + +Independent read-only review initially blocked the candidate on three issues: +reverse-only symmetric-entangler loci, a SWAP capability override, and nested +`qco.pow` body checking. The exact remediated source rebuilt successfully, all +sixteen focused tests passed, and the reviewer reported that all three findings +were closed with no new code blockers. The reviewer also reran +`git diff --check` against the initial development base. + +Pre-restack local validation rebuilt `mqt-core-mlir-unittest-target-synthesis`, +`mqt-core-mlir-unittest-decomposition`, and `mqt-core-mlir-unittests-compiler`, +then observed 16, 199, and 215 passing tests, respectively. Changed-file `prek` +hooks and `uvx nox -s lint` passed. The first final lint attempt encountered +sandbox DNS failure; the authorized network retry reused the worktree-local +environment and passed every hook. The four metadata messages for deliberately +deleted native-menu files were non-fatal and every reported hook passed. + +Post-restack validation rebuilt those three binaries from final CT-01 and again +observed 16, 199, and 215 passing tests. It also built +`MLIRQCOTransforms_verify_interface_header_sets`, +`MQTCompilerPipeline_verify_interface_header_sets`, +`MQTCompilerTarget_verify_interface_header_sets`, `mqt-cc`, and +`mqt-cc_verify_interface_header_sets`. The resulting CLI help contains neither +`--native-gates` nor `fuse-two-qubit-unitary-runs`. Stub generation built the +bindings with LLVM/MLIR 22.1.3 and produced no tracked diff. Five focused +`clang-tidy` invocations passed; the only diagnostics are three inherited +warnings on unchanged `Weyl.h` lines 167 and 171. Changed-file hooks, full +repository lint, stale-surface search outside historical plans, both worktree +and committed `git diff --check`, and the final status audit pass. + +## Interfaces and Dependencies + +`CompilerTarget` remains dependency-light and does not depend on transform +libraries. `MLIRQCOTransforms` links privately to `MQTCompilerTarget`. The +public, independently constructible factories are: + + std::unique_ptr createOptimizeTwoQubitUnitaryRuns(); + std::unique_ptr + createTargetNativeSynthesis(const CompilerTarget& target); + std::unique_ptr + createVerifyTargetConformance(const CompilerTarget& target); + +There is no textual target-native pass, synthetic target, native-gate menu, or +parallel capability model. diff --git a/CHANGELOG.md b/CHANGELOG.md index b0fdc3c2a4..05beb0eb69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,9 +32,9 @@ releases may include breaking changes. [**@li-mingbao**], [**@DRovara**], [**@MatthiasReumann**]) - ✨ Add decision diagram-based construction and simulation of static unitary QCO functions ([#1915]) ([**@simon1hofmann**]) -- ✨ Add a `fuse-two-qubit-unitary-runs` pass for fusing compile-time two-qubit - unitary windows via Weyl/KAK resynthesis ([#1865], [#1961]) - ([**@simon1hofmann**], [**@burgholzer**]) +- ✨ Add profitable target-independent two-qubit unitary-window optimization, + target-native post-routing synthesis, and exact operation/locus conformance + ([#1865], [#1961]) ([**@simon1hofmann**], [**@burgholzer**]) - ✨ Add a `decompose-multi-controlled` pass for decomposing controlled X, Z, RCCX, and constant-angle phase gates with a configurable `min-controls` threshold ([#1810]) ([**@simon1hofmann**]) diff --git a/bindings/mlir/register_mlir.cpp b/bindings/mlir/register_mlir.cpp index 0c096e0b80..e7478472b9 100644 --- a/bindings/mlir/register_mlir.cpp +++ b/bindings/mlir/register_mlir.cpp @@ -394,12 +394,6 @@ operations.)pb"); &mlir::QCOProgram::fuseSingleQubitUnitaryRuns>::call, nb::kw_only(), "basis"_a = "zyz", "Fuse single-qubit unitary runs into the chosen decomposition basis.") - .def("fuse_two_qubit_unitary_runs", - &BooleanMemberAdapter< - &mlir::QCOProgram::fuseTwoQubitUnitaryRuns>::call, - nb::kw_only(), "native_gates"_a, - "Lower unitaries to a non-empty native gate menu via two-qubit run " - "fusion.") .def("unroll_quantum_loops", &BooleanMemberAdapter<&mlir::QCOProgram::unrollQuantumLoops>::call, nb::kw_only(), "unroll_factor"_a = -1, diff --git a/mlir/include/mlir/Compiler/Programs.h b/mlir/include/mlir/Compiler/Programs.h index ac62a967c4..1366555197 100644 --- a/mlir/include/mlir/Compiler/Programs.h +++ b/mlir/include/mlir/Compiler/Programs.h @@ -187,9 +187,6 @@ class QCOProgram final : public Program { /// Fuse single-qubit unitary runs into the selected Euler basis. [[nodiscard]] bool fuseSingleQubitUnitaryRuns(std::string_view basis = "zyz"); - /// Lower unitaries to a non-empty native gate menu via two-qubit run fusion. - [[nodiscard]] bool fuseTwoQubitUnitaryRuns(std::string_view nativeGates); - /// Unroll loops containing quantum operations. [[nodiscard]] bool unrollQuantumLoops(int64_t factor = -1); diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeGateset.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeGateset.h deleted file mode 100644 index bb48a23383..0000000000 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeGateset.h +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#pragma once - -#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" -#include "mlir/Dialect/QCO/Utils/Matrix.h" - -#include - -#include -#include - -namespace mlir { -class Operation; -} // namespace mlir - -namespace mlir::qco::decomposition { - -/** - * @brief Gate token in a comma-separated native gateset (e.g. `"u,cx"`). - */ -enum class NativeGateKind : std::uint8_t { - U, - X, - SX, - RZ, - RX, - RY, - R, - RXX, - RYY, - RZX, - RZZ, - ISWAP, - CZ, - CX, - ECR, -}; - -struct TwoQubitNativeDecomposition; - -/** - * @brief Resolved native gateset for two-qubit Weyl synthesis. - * - * Use @ref parse to obtain a gateset with @p eulerBasis and @p entangler - * resolved from @p gates. When several entanglers appear, preference is - * **RXX > RYY > RZX > RZZ > iSWAP > CZ > CX > ECR** (alphabetic among two-qubit - * rotations; then discrete named gates; ECR last). Weyl synthesis emits - * `rxx`/`ryy`/`rzx`/`rzz` at a fixed angle of π/2. - */ -struct NativeGateset { - llvm::DenseSet gates; - std::optional eulerBasis; - std::optional entangler; - - /** - * @brief Parses a comma-separated native gateset (e.g. `"u,cx"`). - * - * @param nativeGates Comma-separated gate tokens. - * @return Parsed gateset, or `std::nullopt` when the gateset is unsupported. - */ - [[nodiscard]] static std::optional - parse(StringRef nativeGates); - - /** - * @brief Basis decomposition of @p target under this gateset, if supported. - */ - [[nodiscard]] std::optional - decomposeTarget(const Matrix4x4& target) const; - - /** - * @brief Whether @p op is already on this native gateset. - * - * `qco.barrier` and `qco.gphase` are always allowed. Single-qubit primitives - * are checked against @p gates. Single-control, single-target `qco.ctrl` - * shells with an `X`/`Z` body are accepted when `cx`/`cz` is present. - * `qco.rxx`, `qco.ryy`, `qco.rzx`, and `qco.rzz` are accepted when the - * corresponding token is present, including runtime-parameterized forms. - * Bare `qco.iswap` and `qco.ecr` are accepted when the corresponding token is - * present. All other ops are rejected. - */ - [[nodiscard]] bool allowsOp(Operation* op) const; -}; - -} // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/SynthesisBasis.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/SynthesisBasis.h new file mode 100644 index 0000000000..f8d775141c --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/SynthesisBasis.h @@ -0,0 +1,43 @@ +/* + * 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" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" + +namespace mlir::qco::decomposition { + +struct TwoQubitNativeDecomposition; + +/** + * @brief Typed Euler/Weyl basis adapted from one compiler target. + * + * This value contains only the transformation-specific adaptation needed by + * the decomposers. Gate capabilities and basis selection remain owned by + * @ref CompilerTarget. + */ +struct NativeSynthesisBasis { + EulerBasis singleQubit; + CompilerTarget::GateKind entangler; + + /// Adapt one target-selected synthesis basis to the decomposition layer. + [[nodiscard]] static NativeSynthesisBasis + fromCompilerTarget(CompilerTarget::SynthesisBasis basis); + + /** + * @brief Basis decomposition of @p target under this synthesis basis. + */ + [[nodiscard]] TwoQubitNativeDecomposition + decomposeTarget(const Matrix4x4& target) const; +}; + +} // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h index 16f885e7b5..d1dd2d8f4b 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h @@ -343,7 +343,7 @@ decomposeTwoQubitWithBasis( double basisFidelity = 1.0, std::optional numBasisUses = std::nullopt); -struct NativeGateset; +struct NativeSynthesisBasis; /** @brief Result of two-qubit synthesis, including its accumulated phase. */ struct SynthesizedUnitary2Q { @@ -352,10 +352,17 @@ struct SynthesizedUnitary2Q { double globalPhase = 0.0; }; -/** @brief Synthesizes a two-qubit unitary as gates allowed by @p spec. */ +/** + * @brief Synthesizes a two-qubit unitary in the selected target @p basis. + * + * Set @p reverseEntanglerOperands only when the basis entangler is + * operand-symmetric but the provider supports the reverse ordered locus. + * Logical qubit outputs remain paired with their corresponding inputs. + */ [[nodiscard]] FailureOr synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, Value qubit0, Value qubit1, const Matrix4x4& target, - const NativeGateset& spec); + const NativeSynthesisBasis& basis, + bool reverseEntanglerOperands = false); } // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h index 7444438a88..6ce151b6e3 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h @@ -14,6 +14,12 @@ #include #include +#include + +namespace mlir { +class CompilerTarget; +} // namespace mlir + namespace mlir::qco { #define GEN_PASS_DECL @@ -27,4 +33,21 @@ namespace mlir::qco { #define GEN_PASS_REGISTRATION #include "mlir/Dialect/QCO/Transforms/Passes.h.inc" // IWYU pragma: export +/** + * @brief Create the target-independent pre-routing two-qubit optimizer. + */ +[[nodiscard]] std::unique_ptr createOptimizeTwoQubitUnitaryRuns(); + +/** + * @brief Create post-routing synthesis for one immutable compiler target. + */ +[[nodiscard]] std::unique_ptr +createTargetNativeSynthesis(const CompilerTarget& target); + +/** + * @brief Create the final mapped-operation conformance verifier. + */ +[[nodiscard]] std::unique_ptr +createVerifyTargetConformance(const CompilerTarget& target); + } // namespace mlir::qco diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index 544689e71d..ad2f986533 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -67,80 +67,6 @@ def FuseSingleQubitUnitaryRuns "Target Euler basis (zyz, zxz, xzx, xyx, u, zsxx, r).">]; } -def FuseTwoQubitUnitaryRuns - : Pass<"fuse-two-qubit-unitary-runs", "mlir::ModuleOp"> { - let dependentDialects = ["mlir::qco::QCODialect", - "::mlir::arith::ArithDialect"]; - let summary = "Lower supported one- and two-qubit QCO unitaries to a " - "user-specified native gate menu."; - let description = [{ - Rewrites supported one- and two-qubit unitaries to match the comma-separated - `native-gates` gateset. Fusion and synthesis require unitary matrices that - are available at compile time. Already-native runtime-parameterized - operations remain valid, but an off-gateset operation without a compile-time - matrix cannot be synthesized and causes the pass to fail. - - `qco.barrier` and `qco.gphase` are preserved. A `qco.ctrl` is within the - two-qubit scope only when it has a single control and a single target. Every - entangler token listed in the menu is accepted as already native: bare - `qco.rxx`, `qco.ryy`, `qco.rzx`, `qco.rzz`, `qco.iswap`, or `qco.ecr`, or a - `qco.ctrl` with exactly one `qco.x` or `qco.z` body when `cx` or `cz` is - listed. Runtime-parameterized `rxx`/`ryy`/`rzx`/`rzz` operations remain - valid when their token is listed, although only operations with compile-time - matrices can participate in fusion or off-gateset synthesis. Entangler - preference (below) selects the Weyl synthesis basis only and does not - restrict which menu tokens count as native. Weyl synthesis emits - `rxx`/`ryy`/`rzx`/`rzz` at a fixed angle of π/2. Gates acting on more than - two qubits (e.g. multi-controlled `qco.ctrl`) are out of scope and left - untouched for a dedicated multi-controlled synthesis pass. - - The gateset is a comma-separated list of gate tokens (order not - significant) from which the pass resolves a single-qubit Euler basis - (generic `qco.u` when `u` is present; IBM-style surface gates when all of - `x`, `sx`, and `rz` are present; IQM-style `qco.r` when `r` is present; - or a supported rotation pair chosen from `rx`, `ry`, `rz`) plus one - two-qubit entangler from `rxx`, `ryy`, `rzx`, `rzz`, `iswap`, `cz`, `cx`, or - `ecr`. - - Recognised tokens: `u`, `x`, `sx`, `rz`, `rx`, `ry`, `r`, `rxx`, `ryy`, - `rzx`, `rzz`, `iswap`, `cz`, `cx`, `ecr`. An empty or whitespace-only - gateset is a no-op, which is the intended pipeline default when synthesis is - not needed. An unrecognised token causes the pass to fail. - - Example gatesets (each line is one illustrative gateset; pick one entangler - or list several if multiple are native): - - IBM: `x,sx,rz,cx`, `x,sx,rz,cz`, or `x,sx,rz,ecr` - - Generic single-qubit U: `u,rxx`, `u,ryy`, `u,rzx`, `u,rzz`, `u,iswap`, - `u,cz`, `u,cx`, or `u,ecr` - - IQM default: `r,cz` (or `r,cx` if CX is the native entangler) - - Rotation pair + entangler: `rx,rz,cx`, `rx,ry,cz`, `ry,rz,cx`, etc. - Supported pairs are exactly `rx`+`rz`, `rx`+`ry`, and `ry`+`rz`. - - When several entanglers are listed, synthesis preference is `rxx` > `ryy` > - `rzx` > `rzz` > `iswap` > `cz` > `cx` > `ecr` (alphabetic among two-qubit - rotations; then discrete named gates; ECR last). - - Stages: fuse single-qubit runs (reusing `fuse-single-qubit-unitary-runs`, - which also lowers lone off-gateset single-qubit gates); fuse two-qubit runs - and lower any remaining off-gateset two-qubit ops via Weyl synthesis; fuse - the single-qubit seams introduced by that synthesis. The pass fails if any - single- or two-qubit op remains off the native gateset afterwards; gates on - more than two qubits are left untouched and do not cause a failure. - - Lowering is deterministic: entangler preference is `rxx` > `ryy` > `rzx` > - `rzz` > `iswap` > `cz` > `cx` > `ecr`; single-qubit factors use the resolved - Euler basis; and two-qubit run replacement uses the minimal entangler count - from the synthesizer. - }]; - let options = [Option< - "nativeGates", "native-gates", "std::string", "\"\"", - "Comma-separated native gateset. Empty or whitespace-only is " - "a no-op. Tokens: u, x, sx, rz, rx, ry, r, rxx, ryy, rzx, rzz, iswap, " - "cz, cx, ecr. " - "Examples: x,sx,rz,cx; x,sx,rz,ecr; u,rxx; u,ryy; u,rzx; u,rzz; u,iswap; " - "u,cx; r,cz; rx,rz,cx; x,sx,rz,rzz.">]; -} - //===----------------------------------------------------------------------===// def QuantumLoopUnroll diff --git a/mlir/lib/Compiler/Programs.cpp b/mlir/lib/Compiler/Programs.cpp index b1fae369cc..c8dd6f2c21 100644 --- a/mlir/lib/Compiler/Programs.cpp +++ b/mlir/lib/Compiler/Programs.cpp @@ -362,21 +362,6 @@ bool QCOProgram::fuseSingleQubitUnitaryRuns(const std::string_view basis) { "failed to fuse single-qubit unitary runs")); } -bool QCOProgram::fuseTwoQubitUnitaryRuns(const std::string_view nativeGates) { - if (StringRef(nativeGates).trim().empty()) { - mod().emitError("the native gate menu must not be empty"); - return false; - } - qco::FuseTwoQubitUnitaryRunsOptions options; - options.nativeGates = nativeGates; - return succeeded(runPasses( - mod(), - [&options](OpPassManager& pm) { - pm.addPass(qco::createFuseTwoQubitUnitaryRuns(options)); - }, - "failed to fuse two-qubit unitary runs")); -} - bool QCOProgram::unrollQuantumLoops(const int64_t factor) { qco::QuantumLoopUnrollOptions options; options.unrollFactor = factor; diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeGateset.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeGateset.cpp deleted file mode 100644 index 74a0dd9cd7..0000000000 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeGateset.cpp +++ /dev/null @@ -1,274 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "mlir/Dialect/QCO/Transforms/Decomposition/NativeGateset.h" - -#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" -#include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" -#include "mlir/Dialect/QCO/Utils/Matrix.h" - -#include -#include -#include -#include -#include - -#include -#include -#include - -namespace mlir::qco::decomposition { - -static std::optional parseGateToken(StringRef name) { - return StringSwitch>(name) - .Case("u", NativeGateKind::U) - .Case("x", NativeGateKind::X) - .Case("sx", NativeGateKind::SX) - .Case("rz", NativeGateKind::RZ) - .Case("rx", NativeGateKind::RX) - .Case("ry", NativeGateKind::RY) - .Case("r", NativeGateKind::R) - .Case("rxx", NativeGateKind::RXX) - .Case("ryy", NativeGateKind::RYY) - .Case("rzx", NativeGateKind::RZX) - .Case("rzz", NativeGateKind::RZZ) - .Case("iswap", NativeGateKind::ISWAP) - .Case("cz", NativeGateKind::CZ) - .Case("cx", NativeGateKind::CX) - .Case("ecr", NativeGateKind::ECR) - .Default(std::nullopt); -} - -static std::optional> -parseGateSet(StringRef nativeGates) { - DenseSet gates; - SmallVector parts; - nativeGates.split(parts, ',', /*MaxSplit=*/-1, /*KeepEmpty=*/false); - for (StringRef part : parts) { - const auto token = part.trim().lower(); - if (token.empty()) { - continue; - } - const auto gate = parseGateToken(token); - if (!gate) { - return std::nullopt; - } - gates.insert(*gate); - } - return gates; -} - -/** - * @brief Resolves the preferred single-qubit Euler basis for a parsed gateset. - * - * Returns `std::nullopt` when no supported single-qubit synthesis strategy is - * present. Cached on @ref NativeGateset by @ref NativeGateset::parse. - */ -[[nodiscard]] static std::optional -resolveEulerBasis(const DenseSet& gates) { - if (gates.contains(NativeGateKind::U)) { - return EulerBasis::U; - } - if (gates.contains(NativeGateKind::X) && gates.contains(NativeGateKind::SX) && - gates.contains(NativeGateKind::RZ)) { - return EulerBasis::ZSXX; - } - if (gates.contains(NativeGateKind::R)) { - return EulerBasis::R; - } - if (gates.contains(NativeGateKind::RX) && - gates.contains(NativeGateKind::RZ)) { - return EulerBasis::XZX; - } - if (gates.contains(NativeGateKind::RX) && - gates.contains(NativeGateKind::RY)) { - return EulerBasis::XYX; - } - if (gates.contains(NativeGateKind::RY) && - gates.contains(NativeGateKind::RZ)) { - return EulerBasis::ZYZ; - } - return std::nullopt; -} - -/** - * @brief Picks the two-qubit entangler for Weyl synthesis. - * - * When multiple entanglers appear in the gateset, preference is - * **RXX > RYY > RZX > RZZ > iSWAP > CZ > CX > ECR**. - */ -[[nodiscard]] static std::optional -selectEntangler(const DenseSet& gates) { - if (gates.contains(NativeGateKind::RXX)) { - return NativeGateKind::RXX; - } - if (gates.contains(NativeGateKind::RYY)) { - return NativeGateKind::RYY; - } - if (gates.contains(NativeGateKind::RZX)) { - return NativeGateKind::RZX; - } - if (gates.contains(NativeGateKind::RZZ)) { - return NativeGateKind::RZZ; - } - if (gates.contains(NativeGateKind::ISWAP)) { - return NativeGateKind::ISWAP; - } - if (gates.contains(NativeGateKind::CZ)) { - return NativeGateKind::CZ; - } - if (gates.contains(NativeGateKind::CX)) { - return NativeGateKind::CX; - } - if (gates.contains(NativeGateKind::ECR)) { - return NativeGateKind::ECR; - } - return std::nullopt; -} - -static constexpr Matrix4x4 CANONICAL_CONTROLLED_X = - Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, // row 0 - 0.0, 1.0, 0.0, 0.0, // row 1 - 0.0, 0.0, 0.0, 1.0, // row 2 - 0.0, 0.0, 1.0, 0.0); // row 3 - -static constexpr Matrix4x4 CANONICAL_CONTROLLED_Z = - Matrix4x4::fromDiagonal(1., 1., 1., -1.); - -static const TwoQubitBasisDecomposer& -cachedNativeBasisDecomposer(NativeGateKind entangler) { - switch (entangler) { - case NativeGateKind::RXX: { - static const TwoQubitBasisDecomposer DECOMPOSER = - TwoQubitBasisDecomposer::create( - RXXOp::unitaryMatrix(std::numbers::pi / 2.0), 1.0); - return DECOMPOSER; - } - case NativeGateKind::RYY: { - static const TwoQubitBasisDecomposer DECOMPOSER = - TwoQubitBasisDecomposer::create( - RYYOp::unitaryMatrix(std::numbers::pi / 2.0), 1.0); - return DECOMPOSER; - } - case NativeGateKind::RZX: { - static const TwoQubitBasisDecomposer DECOMPOSER = - TwoQubitBasisDecomposer::create( - RZXOp::unitaryMatrix(std::numbers::pi / 2.0), 1.0); - return DECOMPOSER; - } - case NativeGateKind::RZZ: { - static const TwoQubitBasisDecomposer DECOMPOSER = - TwoQubitBasisDecomposer::create( - RZZOp::unitaryMatrix(std::numbers::pi / 2.0), 1.0); - return DECOMPOSER; - } - case NativeGateKind::ISWAP: { - static const TwoQubitBasisDecomposer DECOMPOSER = - TwoQubitBasisDecomposer::create(iSWAPOp::getUnitaryMatrix(), 1.0); - return DECOMPOSER; - } - case NativeGateKind::CZ: { - static const TwoQubitBasisDecomposer DECOMPOSER = - TwoQubitBasisDecomposer::create(CANONICAL_CONTROLLED_Z, 1.0); - return DECOMPOSER; - } - case NativeGateKind::CX: { - static const TwoQubitBasisDecomposer DECOMPOSER = - TwoQubitBasisDecomposer::create(CANONICAL_CONTROLLED_X, 1.0); - return DECOMPOSER; - } - case NativeGateKind::ECR: { - static const TwoQubitBasisDecomposer DECOMPOSER = - TwoQubitBasisDecomposer::create(ECROp::getUnitaryMatrix(), 1.0); - return DECOMPOSER; - } - default: - llvm_unreachable( - "only RXX/RYY/RZX/RZZ/ISWAP/CZ/CX/ECR are valid entanglers"); - } -} - -std::optional -NativeGateset::decomposeTarget(const Matrix4x4& target) const { - if (!entangler) { - return std::nullopt; - } - return cachedNativeBasisDecomposer(*entangler).decomposeTarget(target); -} - -static std::optional gateKindFor(UnitaryOpInterface op) { - return TypeSwitch>( - op.getOperation()) - .Case([](UOp) { return NativeGateKind::U; }) - .Case([](XOp) { return NativeGateKind::X; }) - .Case([](SXOp) { return NativeGateKind::SX; }) - .Case([](RZOp) { return NativeGateKind::RZ; }) - .Case([](RXOp) { return NativeGateKind::RX; }) - .Case([](RYOp) { return NativeGateKind::RY; }) - .Case([](ROp) { return NativeGateKind::R; }) - .Default([](Operation*) { return std::nullopt; }); -} - -static std::optional entanglerKindFor(CtrlOp ctrl) { - if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1 || - ctrl.getNumBodyUnitaries() != 1) { - return std::nullopt; - } - return TypeSwitch>( - ctrl.getBodyUnitary(0).getOperation()) - .Case([](XOp) { return NativeGateKind::CX; }) - .Case([](ZOp) { return NativeGateKind::CZ; }) - .Default([](Operation*) { return std::nullopt; }); -} - -bool NativeGateset::allowsOp(Operation* op) const { - return TypeSwitch(op) - .Case([](auto) { return true; }) - .Case([&](RXXOp) { return gates.contains(NativeGateKind::RXX); }) - .Case([&](RYYOp) { return gates.contains(NativeGateKind::RYY); }) - .Case([&](RZXOp) { return gates.contains(NativeGateKind::RZX); }) - .Case([&](RZZOp) { return gates.contains(NativeGateKind::RZZ); }) - .Case( - [&](iSWAPOp) { return gates.contains(NativeGateKind::ISWAP); }) - .Case([&](CtrlOp ctrl) { - const auto kind = entanglerKindFor(ctrl); - return kind && gates.contains(*kind); - }) - .Case([&](ECROp) { return gates.contains(NativeGateKind::ECR); }) - .Case([&](UnitaryOpInterface unitary) { - if (!unitary.isSingleQubit()) { - return false; - } - const auto gate = gateKindFor(unitary); - return gate && gates.contains(*gate); - }) - .Default([](Operation*) { return false; }); -} - -std::optional NativeGateset::parse(StringRef nativeGates) { - auto gates = parseGateSet(nativeGates); - if (!gates) { - return std::nullopt; - } - const auto euler = resolveEulerBasis(*gates); - const auto entangler = selectEntangler(*gates); - if (!euler || !entangler) { - return std::nullopt; - } - return NativeGateset{ - .gates = std::move(*gates), - .eulerBasis = euler, - .entangler = entangler, - }; -} - -} // namespace mlir::qco::decomposition diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/SynthesisBasis.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/SynthesisBasis.cpp new file mode 100644 index 0000000000..fcc8c20543 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/SynthesisBasis.cpp @@ -0,0 +1,125 @@ +/* + * 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/Dialect/QCO/Transforms/Decomposition/SynthesisBasis.h" + +#include "mlir/Compiler/Target.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" + +#include + +#include + +namespace mlir::qco::decomposition { + +using GateKind = CompilerTarget::GateKind; + +[[nodiscard]] static EulerBasis +toEulerBasis(const CompilerTarget::SingleQubitBasis basis) { + switch (basis) { + case CompilerTarget::SingleQubitBasis::U: + return EulerBasis::U; + case CompilerTarget::SingleQubitBasis::ZSXX: + return EulerBasis::ZSXX; + case CompilerTarget::SingleQubitBasis::R: + return EulerBasis::R; + case CompilerTarget::SingleQubitBasis::XZX: + return EulerBasis::XZX; + case CompilerTarget::SingleQubitBasis::XYX: + return EulerBasis::XYX; + case CompilerTarget::SingleQubitBasis::ZYZ: + return EulerBasis::ZYZ; + } + llvm_unreachable("unhandled compiler target single-qubit basis"); +} + +static constexpr Matrix4x4 CANONICAL_CONTROLLED_X = + Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, // row 0 + 0.0, 1.0, 0.0, 0.0, // row 1 + 0.0, 0.0, 0.0, 1.0, // row 2 + 0.0, 0.0, 1.0, 0.0); // row 3 + +static constexpr Matrix4x4 CANONICAL_CONTROLLED_Z = + Matrix4x4::fromDiagonal(1., 1., 1., -1.); + +static const TwoQubitBasisDecomposer& +cachedNativeBasisDecomposer(const GateKind entangler) { + switch (entangler) { + case GateKind::RXX: { + static const TwoQubitBasisDecomposer DECOMPOSER = + TwoQubitBasisDecomposer::create( + RXXOp::unitaryMatrix(std::numbers::pi / 2.0), 1.0); + return DECOMPOSER; + } + case GateKind::RYY: { + static const TwoQubitBasisDecomposer DECOMPOSER = + TwoQubitBasisDecomposer::create( + RYYOp::unitaryMatrix(std::numbers::pi / 2.0), 1.0); + return DECOMPOSER; + } + case GateKind::RZX: { + static const TwoQubitBasisDecomposer DECOMPOSER = + TwoQubitBasisDecomposer::create( + RZXOp::unitaryMatrix(std::numbers::pi / 2.0), 1.0); + return DECOMPOSER; + } + case GateKind::RZZ: { + static const TwoQubitBasisDecomposer DECOMPOSER = + TwoQubitBasisDecomposer::create( + RZZOp::unitaryMatrix(std::numbers::pi / 2.0), 1.0); + return DECOMPOSER; + } + case GateKind::ISWAP: { + static const TwoQubitBasisDecomposer DECOMPOSER = + TwoQubitBasisDecomposer::create(iSWAPOp::getUnitaryMatrix(), 1.0); + return DECOMPOSER; + } + case GateKind::CZ: { + static const TwoQubitBasisDecomposer DECOMPOSER = + TwoQubitBasisDecomposer::create(CANONICAL_CONTROLLED_Z, 1.0); + return DECOMPOSER; + } + case GateKind::CX: { + static const TwoQubitBasisDecomposer DECOMPOSER = + TwoQubitBasisDecomposer::create(CANONICAL_CONTROLLED_X, 1.0); + return DECOMPOSER; + } + case GateKind::ECR: { + static const TwoQubitBasisDecomposer DECOMPOSER = + TwoQubitBasisDecomposer::create(ECROp::getUnitaryMatrix(), 1.0); + return DECOMPOSER; + } + default: + llvm_unreachable( + "only RXX/RYY/RZX/RZZ/ISWAP/CZ/CX/ECR are valid entanglers"); + } +} + +TwoQubitNativeDecomposition +NativeSynthesisBasis::decomposeTarget(const Matrix4x4& target) const { + const auto decomposition = + cachedNativeBasisDecomposer(entangler).decomposeTarget(target); + if (!decomposition) { + llvm::reportFatalInternalError( + "target-selected entangler failed to decompose a two-qubit unitary"); + } + return *decomposition; +} + +NativeSynthesisBasis NativeSynthesisBasis::fromCompilerTarget( + const CompilerTarget::SynthesisBasis basis) { + return NativeSynthesisBasis{.singleQubit = toEulerBasis(basis.singleQubit), + .entangler = basis.entangler}; +} + +} // namespace mlir::qco::decomposition diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp index fbea6d05e6..c410336e3f 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp @@ -10,9 +10,10 @@ #include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" +#include "mlir/Compiler/Target.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/NativeGateset.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/SynthesisBasis.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" #include @@ -671,18 +672,15 @@ bool TwoQubitWeylDecomposition::applySpecialization( FailureOr synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, Value qubit0, Value qubit1, const Matrix4x4& target, - const NativeGateset& spec) { - const auto native = spec.decomposeTarget(target); - if (!native || !spec.eulerBasis) { - return failure(); - } - - double globalPhase = native->globalPhase; + const NativeSynthesisBasis& basis, + const bool reverseEntanglerOperands) { + const auto native = basis.decomposeTarget(target); + double globalPhase = native.globalPhase; Value wire0 = qubit0; Value wire1 = qubit1; - const auto& factors = native->singleQubitFactors; - const std::uint8_t numBasisUses = native->numBasisUses; + const auto& factors = native.singleQubitFactors; + const std::uint8_t numBasisUses = native.numBasisUses; const std::size_t requiredFactors = singleQubitFactorCount(numBasisUses); if (factors.size() != requiredFactors) { llvm::reportFatalInternalError(llvm::formatv( @@ -693,7 +691,7 @@ synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, Value qubit0, const auto emitFactor = [&](Value& wire, std::size_t index) { const auto synthesized = synthesizeUnitary1QEuler( builder, loc, wire, factors[index], /*runSize=*/0, - /*hasNonBasisGate=*/true, *spec.eulerBasis); + /*hasNonBasisGate=*/true, basis.singleQubit); if (!synthesized) { llvm::reportFatalInternalError(llvm::formatv( "synthesizeUnitary2QWeyl: euler synthesis failed for factor index " @@ -704,55 +702,62 @@ synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, Value qubit0, globalPhase += synthesized->globalPhase; }; const auto emitEntangler = [&]() { - if (spec.entangler == NativeGateKind::RXX) { - auto rxxOp = RXXOp::create(builder, loc, wire0, wire1, PI / 2.0); - wire0 = rxxOp.getOutputQubit(0); - wire1 = rxxOp.getOutputQubit(1); + Value& entanglerWire0 = reverseEntanglerOperands ? wire1 : wire0; + Value& entanglerWire1 = reverseEntanglerOperands ? wire0 : wire1; + if (basis.entangler == CompilerTarget::GateKind::RXX) { + auto rxxOp = + RXXOp::create(builder, loc, entanglerWire0, entanglerWire1, PI / 2.0); + entanglerWire0 = rxxOp.getOutputQubit(0); + entanglerWire1 = rxxOp.getOutputQubit(1); return; } - if (spec.entangler == NativeGateKind::RYY) { - auto ryyOp = RYYOp::create(builder, loc, wire0, wire1, PI / 2.0); - wire0 = ryyOp.getOutputQubit(0); - wire1 = ryyOp.getOutputQubit(1); + if (basis.entangler == CompilerTarget::GateKind::RYY) { + auto ryyOp = + RYYOp::create(builder, loc, entanglerWire0, entanglerWire1, PI / 2.0); + entanglerWire0 = ryyOp.getOutputQubit(0); + entanglerWire1 = ryyOp.getOutputQubit(1); return; } - if (spec.entangler == NativeGateKind::RZX) { - auto rzxOp = RZXOp::create(builder, loc, wire0, wire1, PI / 2.0); - wire0 = rzxOp.getOutputQubit(0); - wire1 = rzxOp.getOutputQubit(1); + if (basis.entangler == CompilerTarget::GateKind::RZX) { + auto rzxOp = + RZXOp::create(builder, loc, entanglerWire0, entanglerWire1, PI / 2.0); + entanglerWire0 = rzxOp.getOutputQubit(0); + entanglerWire1 = rzxOp.getOutputQubit(1); return; } - if (spec.entangler == NativeGateKind::RZZ) { - auto rzzOp = RZZOp::create(builder, loc, wire0, wire1, PI / 2.0); - wire0 = rzzOp.getOutputQubit(0); - wire1 = rzzOp.getOutputQubit(1); + if (basis.entangler == CompilerTarget::GateKind::RZZ) { + auto rzzOp = + RZZOp::create(builder, loc, entanglerWire0, entanglerWire1, PI / 2.0); + entanglerWire0 = rzzOp.getOutputQubit(0); + entanglerWire1 = rzzOp.getOutputQubit(1); return; } - if (spec.entangler == NativeGateKind::ISWAP) { - auto iswapOp = iSWAPOp::create(builder, loc, wire0, wire1); - wire0 = iswapOp.getOutputQubit(0); - wire1 = iswapOp.getOutputQubit(1); + if (basis.entangler == CompilerTarget::GateKind::ISWAP) { + auto iswapOp = + iSWAPOp::create(builder, loc, entanglerWire0, entanglerWire1); + entanglerWire0 = iswapOp.getOutputQubit(0); + entanglerWire1 = iswapOp.getOutputQubit(1); return; } - if (spec.entangler == NativeGateKind::CZ || - spec.entangler == NativeGateKind::CX) { - const bool emitCz = spec.entangler == NativeGateKind::CZ; - auto ctrlOp = - CtrlOp::create(builder, loc, wire0, wire1, [&](Value targetQubit) { + if (basis.entangler == CompilerTarget::GateKind::CZ || + basis.entangler == CompilerTarget::GateKind::CX) { + const bool emitCz = basis.entangler == CompilerTarget::GateKind::CZ; + auto ctrlOp = CtrlOp::create( + builder, loc, entanglerWire0, entanglerWire1, [&](Value targetQubit) { if (emitCz) { return ZOp::create(builder, loc, targetQubit).getOutputQubit(0); } return XOp::create(builder, loc, targetQubit).getOutputQubit(0); }); - wire0 = ctrlOp.getOutputControl(0); - wire1 = ctrlOp.getOutputTarget(0); + entanglerWire0 = ctrlOp.getOutputControl(0); + entanglerWire1 = ctrlOp.getOutputTarget(0); return; } - assert(spec.entangler == NativeGateKind::ECR && - "emitEntangler: unexpected NativeGateKind"); - auto ecrOp = ECROp::create(builder, loc, wire0, wire1); - wire0 = ecrOp.getOutputQubit(0); - wire1 = ecrOp.getOutputQubit(1); + assert(basis.entangler == CompilerTarget::GateKind::ECR && + "emitEntangler: unexpected compiler target gate"); + auto ecrOp = ECROp::create(builder, loc, entanglerWire0, entanglerWire1); + entanglerWire0 = ecrOp.getOutputQubit(0); + entanglerWire1 = ecrOp.getOutputQubit(1); }; for (std::uint8_t layer = 0; layer <= numBasisUses; ++layer) { diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp deleted file mode 100644 index f55e02da48..0000000000 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp +++ /dev/null @@ -1,475 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" -#include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/NativeGateset.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" -#include "mlir/Dialect/QCO/Transforms/Passes.h" -#include "mlir/Dialect/QCO/Utils/Matrix.h" -#include "mlir/Dialect/Utils/Transforms/GlobalPhaseNormalization.h" - -#include -#include -#include // IWYU pragma: keep (Passes.h.inc) -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -namespace mlir::qco { - -#define GEN_PASS_DEF_FUSETWOQUBITUNITARYRUNS -#include "mlir/Dialect/QCO/Transforms/Passes.h.inc" - -using decomposition::EulerBasis; -using decomposition::NativeGateset; -using decomposition::populateFuseSingleQubitUnitaryRunsPatterns; -using decomposition::synthesizeUnitary2QWeyl; - -namespace { - -/** Composed unitary and metadata for a fusable two-qubit run. */ -struct FusableTwoQubitRun { - SmallVector ops; ///< Members in program order. - Matrix4x4 composed = Matrix4x4::identity(); - unsigned numTwoQ = 0; ///< Number of two-qubit members (entanglers consumed). - bool hasNonNativeGate = false; ///< Any member off the native gateset. - Value tailA; ///< Current output wires of the run's tail. - Value tailB; -}; - -} // namespace - -// --- Run membership ------------------------------------------------------- // - -/// Whether `op` is nested under a `ctrl`/`inv` body. Such unitaries are handled -/// through their shell op, so the top-level walk skips them. -static bool isExcludedFromTopLevelUnitaryWalk(Operation* op) { - if (op->getParentOfType()) { - return true; - } - return !isa(op) && op->getParentOfType(); -} - -/// Whether `op` is a unitary shell the pass may rewrite at top level. -static bool isWalkableUnitaryShell(Operation* op) { - return !isa(op) && - !isExcludedFromTopLevelUnitaryWalk(op); -} - -/// Builds the constant 4x4 matrix for a two-qubit op (bare or single-target -/// `CtrlOp`). Returns false for a `CtrlOp` that is not -/// single-control/single-target, or an op whose matrix is not known at compile -/// time. -static bool assignTwoQubitOpMatrix(Operation* op, Matrix4x4& matrix) { - if (auto ctrl = dyn_cast(op)) { - if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { - return false; - } - return cast(ctrl.getOperation()) - .getUnitaryMatrix4x4(matrix); - } - auto unitary = cast(op); - assert(unitary.isTwoQubit() && - "only two-qubit unitary shells are passed to assignTwoQubitOpMatrix"); - return unitary.getUnitaryMatrix4x4(matrix); -} - -/// Whether `unitary` is a single-qubit gate that can join a run. -static bool isOneQubitRunMember(UnitaryOpInterface unitary) { - if (!unitary || !unitary.isSingleQubit() || - !isWalkableUnitaryShell(unitary.getOperation())) { - return false; - } - Matrix2x2 matrix; - return unitary.getUnitaryMatrix2x2(matrix); -} - -/// Whether `unitary` is a two-qubit gate that can join a run. -static bool isTwoQubitRunMember(UnitaryOpInterface unitary) { - if (!unitary || !unitary.isTwoQubit() || - !isWalkableUnitaryShell(unitary.getOperation())) { - return false; - } - Matrix4x4 matrix; - return assignTwoQubitOpMatrix(unitary.getOperation(), matrix); -} - -// --- Wire navigation ------------------------------------------------------ // - -/// The sole run-member consumer of `wire`, or a null interface when its unique -/// user cannot join a run. `wire` is single-use by qubit linearity. -static UnitaryOpInterface uniqueUnitaryUser(Value wire) { - assert(wire.hasOneUse() && - "qubit values are single-use, so a run tail has exactly one user"); - auto unitary = dyn_cast(*wire.user_begin()); - if (!unitary) { - return {}; - } - if (unitary.isTwoQubit()) { - return isTwoQubitRunMember(unitary) ? unitary : UnitaryOpInterface{}; - } - if (unitary.isSingleQubit()) { - return isOneQubitRunMember(unitary) ? unitary : UnitaryOpInterface{}; - } - return {}; -} - -/// Traces `wire` upstream through single-qubit gates to the two-qubit run -/// member terminating the chain, or `nullptr` if the chain is broken. -static Operation* twoQubitGateAtEndOfOneQChain(Value wire) { - Value cur = wire; - while (Operation* def = cur.getDefiningOp()) { - auto unitary = dyn_cast(def); - if (!unitary) { - return nullptr; - } - if (unitary.isTwoQubit()) { - return isTwoQubitRunMember(unitary) ? def : nullptr; - } - if (!isOneQubitRunMember(unitary)) { - return nullptr; - } - cur = unitary.getInputQubit(0); - } - return nullptr; -} - -/// Whether both input wires of `op` come from one earlier two-qubit run, making -/// `op` a continuation of that run rather than a fresh run start. -static bool feedsFromSameTwoQubitRun(UnitaryOpInterface op) { - const Value in0 = op.getInputQubit(0); - const Value in1 = op.getInputQubit(1); - assert(in0.hasOneUse() && in1.hasOneUse() && - "qubit values are single-use, so a run member consumes each input " - "exactly once"); - Operation* gate0 = twoQubitGateAtEndOfOneQChain(in0); - Operation* gate1 = twoQubitGateAtEndOfOneQChain(in1); - return gate0 != nullptr && gate0 == gate1; -} - -// --- Run scanning --------------------------------------------------------- // - -/// Appends a two-qubit gate to `run`, composing its matrix. No-op unless both -/// of `op`'s inputs are the run's current tail wires (in either order), keeping -/// the run confined to a single pair of wires. -static void absorbTwoQubitIntoRun(FusableTwoQubitRun& run, - UnitaryOpInterface op, - const NativeGateset& spec) { - Matrix4x4 opMatrix; - [[maybe_unused]] const bool assigned = - assignTwoQubitOpMatrix(op.getOperation(), opMatrix); - assert(assigned && "a two-qubit run member always exposes a 4x4 matrix"); - const Value in0 = op.getInputQubit(0); - const Value in1 = op.getInputQubit(1); - size_t id0 = 0; - size_t id1 = 1; - if (in0 == run.tailA && in1 == run.tailB) { - run.tailA = op.getOutputQubit(0); - run.tailB = op.getOutputQubit(1); - } else if (in0 == run.tailB && in1 == run.tailA) { - id0 = 1; - id1 = 0; - run.tailA = op.getOutputQubit(1); - run.tailB = op.getOutputQubit(0); - } else { - llvm_unreachable( - "a unique user of both tail wires connects to both of them"); - } - run.composed.premultiplyBy(opMatrix.reorderForQubits(id0, id1)); - run.ops.push_back(op.getOperation()); - ++run.numTwoQ; - run.hasNonNativeGate |= !spec.allowsOp(op.getOperation()); -} - -/// Appends a single-qubit gate on run wire `wireIndex` (0 = A, 1 = B). -static void absorbOneQubitIntoRun(FusableTwoQubitRun& run, - UnitaryOpInterface op, - const NativeGateset& spec, - unsigned wireIndex) { - Matrix2x2 raw; - [[maybe_unused]] const bool assigned = op.getUnitaryMatrix2x2(raw); - assert(assigned && "a single-qubit run member always exposes a 2x2 matrix"); - run.composed.premultiplyBy(raw.embedInTwoQubit(wireIndex)); - run.ops.push_back(op.getOperation()); - run.hasNonNativeGate |= !spec.allowsOp(op.getOperation()); - (wireIndex == 0 ? run.tailA : run.tailB) = op.getOutputQubit(0); -} - -/// Walks forward from `head`, composing the run's matrix and metadata. Absorbs -/// a following two-qubit gate when it keeps both run wires together, otherwise -/// the single-qubit gate first in program order; stops at the first boundary -/// that would split the run's two wires. -static FusableTwoQubitRun scanFusableTwoQubitRun(UnitaryOpInterface head, - const NativeGateset& spec) { - FusableTwoQubitRun run; - [[maybe_unused]] const bool assigned = - assignTwoQubitOpMatrix(head.getOperation(), run.composed); - assert(assigned && "a run head is a two-qubit member with a 4x4 matrix"); - run.tailA = head.getOutputQubit(0); - run.tailB = head.getOutputQubit(1); - run.ops.push_back(head.getOperation()); - run.numTwoQ = 1; - run.hasNonNativeGate |= !spec.allowsOp(head.getOperation()); - - while (true) { - UnitaryOpInterface nextOnA = uniqueUnitaryUser(run.tailA); - UnitaryOpInterface nextOnB = uniqueUnitaryUser(run.tailB); - const bool sameOp = - nextOnA && nextOnB && nextOnA.getOperation() == nextOnB.getOperation(); - - if (sameOp && nextOnA.isTwoQubit()) { - absorbTwoQubitIntoRun(run, nextOnA, spec); - continue; - } - - const bool aSingle = nextOnA && nextOnA.isSingleQubit() && !sameOp; - const bool bSingle = nextOnB && nextOnB.isSingleQubit() && !sameOp; - if (aSingle && bSingle && nextOnA->getBlock() != nextOnB->getBlock()) { - break; - } - if (aSingle && (!bSingle || nextOnA->isBeforeInBlock(nextOnB))) { - absorbOneQubitIntoRun(run, nextOnA, spec, /*wireIndex=*/0); - continue; - } - if (bSingle) { - absorbOneQubitIntoRun(run, nextOnB, spec, /*wireIndex=*/1); - continue; - } - break; - } - return run; -} - -/// Erases all run members, successors first so each is dead when erased. -static void eraseFusableRun(PatternRewriter& rewriter, - const FusableTwoQubitRun& run) { - for (Operation* member : llvm::reverse(run.ops)) { - rewriter.eraseOp(member); - } -} - -/// First single- or two-qubit unitary (including `ctrl` shells) still off the -/// native gateset, or `nullptr` if none. Used as the pass' convergence check. -/// Gates acting on more than two qubits are out of scope here (a dedicated -/// multi-controlled synthesis pass possibly lowers those) and are left -/// untouched rather than reported. -static Operation* findNonNativeOp(Operation* root, const NativeGateset& spec) { - Operation* found = nullptr; - root->walk([&](Operation* op) { - auto unitary = dyn_cast(op); - if (!unitary || !isWalkableUnitaryShell(op) || unitary.getNumQubits() > 2) { - return WalkResult::advance(); - } - if (spec.allowsOp(op)) { - return WalkResult::advance(); - } - found = op; - return WalkResult::interrupt(); - }); - return found; -} - -namespace { - -/// Fuses a maximal two-qubit run into one composed unitary and resynthesizes it -/// to the native gateset when beneficial. -struct FuseTwoQubitUnitaryRunsPattern final - : OpInterfaceRewritePattern { - FuseTwoQubitUnitaryRunsPattern(MLIRContext* ctx, NativeGateset specIn) - : OpInterfaceRewritePattern(ctx), spec(std::move(specIn)) {} - - NativeGateset spec; - - /// Whether `op` anchors a run: a two-qubit run member whose two wires are not - /// both fed by the same earlier run (which would make it a continuation). - static bool isRunStart(UnitaryOpInterface op) { - return isTwoQubitRunMember(op) && !feedsFromSameTwoQubitRun(op); - } - - /// Fuses the run anchored at `op` if it contains an off-gateset gate or Weyl - /// resynthesis uses fewer entanglers than the run's two-qubit members. - LogicalResult matchAndRewrite(UnitaryOpInterface op, - PatternRewriter& rewriter) const override { - if (!isRunStart(op)) { - return failure(); - } - - FusableTwoQubitRun run = scanFusableTwoQubitRun(op, spec); - if (run.ops.size() < 2) { - return failure(); - } - - const auto native = spec.decomposeTarget(run.composed); - if (!native || - (!run.hasNonNativeGate && native->numBasisUses >= run.numTwoQ)) { - return failure(); - } - - auto firstOp = cast(run.ops.front()); - rewriter.setInsertionPoint(firstOp); - const auto synthesized = synthesizeUnitary2QWeyl( - rewriter, firstOp.getLoc(), firstOp.getInputQubit(0), - firstOp.getInputQubit(1), run.composed, spec); - if (failed(synthesized)) { - firstOp->emitError("failed to emit synthesized two-qubit gate sequence"); - return failure(); - } - decomposition::emitGPhaseIfNeeded(rewriter, firstOp.getLoc(), - synthesized->globalPhase); - rewriter.replaceAllUsesWith(run.tailA, synthesized->qubit0); - rewriter.replaceAllUsesWith(run.tailB, synthesized->qubit1); - eraseFusableRun(rewriter, run); - return success(); - } -}; - -/// Lowers a single off-gateset two-qubit op (bare or single-target `CtrlOp`) to -/// the native entangler plus native single-qubit factors via Weyl synthesis. -/// Native two-qubit ops and fusable runs are left to -/// @ref FuseTwoQubitUnitaryRunsPattern. -struct LowerTwoQubitOpPattern final - : OpInterfaceRewritePattern { - LowerTwoQubitOpPattern(MLIRContext* ctx, NativeGateset specIn) - : OpInterfaceRewritePattern(ctx), spec(std::move(specIn)) {} - - NativeGateset spec; - - LogicalResult matchAndRewrite(UnitaryOpInterface op, - PatternRewriter& rewriter) const override { - if (!op.isTwoQubit()) { - return failure(); - } - Operation* raw = op.getOperation(); - if (!isWalkableUnitaryShell(raw) || spec.allowsOp(raw)) { - return failure(); - } - Matrix4x4 matrix; - if (!assignTwoQubitOpMatrix(raw, matrix)) { - return failure(); - } - - Value in0; - Value in1; - if (auto ctrl = dyn_cast(raw)) { - in0 = ctrl.getInputControl(0); - in1 = ctrl.getInputTarget(0); - } else { - in0 = op.getInputQubit(0); - in1 = op.getInputQubit(1); - } - - rewriter.setInsertionPoint(raw); - const auto synthesized = synthesizeUnitary2QWeyl(rewriter, raw->getLoc(), - in0, in1, matrix, spec); - if (failed(synthesized)) { - return failure(); - } - decomposition::emitGPhaseIfNeeded(rewriter, raw->getLoc(), - synthesized->globalPhase); - rewriter.replaceOp(raw, - ValueRange{synthesized->qubit0, synthesized->qubit1}); - return success(); - } -}; - -} // namespace - -/// Fuses single-qubit runs (and lowers lone off-gateset single-qubit ops) by -/// reusing the `fuse-single-qubit-unitary-runs` rewrite. `qco.ctrl` bodies are -/// skipped so the `X`/`Z` bodies of native entanglers are preserved. -static LogicalResult fuseSingleQubitRuns(ModuleOp moduleOp, - const EulerBasis basis) { - RewritePatternSet patterns(moduleOp.getContext()); - populateFuseSingleQubitUnitaryRunsPatterns(patterns, basis, - /*skipControlledBodies=*/true); - return applyPatternsGreedily(moduleOp, std::move(patterns)); -} - -/// Fuses two-qubit runs, then lowers any remaining off-gateset two-qubit ops. -static LogicalResult fuseAndLowerTwoQubitOps(ModuleOp moduleOp, - const NativeGateset& spec) { - MLIRContext* ctx = moduleOp.getContext(); - { - RewritePatternSet runPatterns(ctx); - runPatterns.add(ctx, spec); - if (failed(applyPatternsGreedily(moduleOp, std::move(runPatterns)))) { - return failure(); - } - } - RewritePatternSet lowerPatterns(ctx); - lowerPatterns.add(ctx, spec); - return applyPatternsGreedily(moduleOp, std::move(lowerPatterns)); -} - -namespace { - -struct FuseTwoQubitUnitaryRunsPass final - : impl::FuseTwoQubitUnitaryRunsBase { - using Base::Base; - - explicit FuseTwoQubitUnitaryRunsPass(FuseTwoQubitUnitaryRunsOptions options) - : Base(std::move(options)) {} - -protected: - void runOnOperation() override { - if (StringRef(nativeGates).trim().empty()) { - return; - } - const auto spec = NativeGateset::parse(nativeGates); - if (!spec) { - getOperation().emitError() << "unsupported native gateset (native-gates='" - << nativeGates << "')"; - signalPassFailure(); - return; - } - const EulerBasis basis = *spec->eulerBasis; - ModuleOp moduleOp = getOperation(); - - // 1. Fuse single-qubit runs (also lowers lone off-gateset single-qubit - // ops). - // 2. Fuse two-qubit runs and lower remaining off-gateset two-qubit ops. - // 3. Fuse the single-qubit seams introduced by two-qubit synthesis. - if (failed(fuseSingleQubitRuns(moduleOp, basis)) || - failed(fuseAndLowerTwoQubitOps(moduleOp, *spec)) || - failed(fuseSingleQubitRuns(moduleOp, basis))) { - signalPassFailure(); - return; - } - - if (Operation* leftover = findNonNativeOp(moduleOp, *spec)) { - leftover->emitError() - << "native gate synthesis: operation remains outside the native " - "gateset (native-gates='" - << nativeGates << "')"; - signalPassFailure(); - return; - } - if (failed(mlir::mqt::normalizeGlobalPhases(moduleOp))) { - signalPassFailure(); - } - } -}; - -} // namespace - -} // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp new file mode 100644 index 0000000000..eb81f7fc41 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp @@ -0,0 +1,765 @@ +/* + * 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/Target.h" +#include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/SynthesisBasis.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" +#include "mlir/Dialect/QCO/Transforms/Passes.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" +#include "mlir/Dialect/Utils/Transforms/GlobalPhaseNormalization.h" + +#include +#include +#include +#include // IWYU pragma: keep (Passes.h.inc) +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace mlir::qco { + +using decomposition::NativeSynthesisBasis; +using decomposition::synthesizeUnitary2QWeyl; + +namespace { + +/** Composed unitary and metadata for a fusable two-qubit run. */ +struct FusableTwoQubitRun { + SmallVector ops; ///< Members in program order. + Matrix4x4 composed = Matrix4x4::identity(); + unsigned numTwoQ = 0; ///< Number of two-qubit members (entanglers consumed). + Value tailA; ///< Current output wires of the run's tail. + Value tailB; +}; + +} // namespace + +// --- Run membership ------------------------------------------------------- // + +/// Whether `op` is nested under a modifier body. Such unitaries are handled +/// through their shell op, so the top-level walk skips them. +static bool isExcludedFromTopLevelUnitaryWalk(Operation* op) { + return op->getParentOfType() || op->getParentOfType() || + op->getParentOfType(); +} + +/// Whether `op` is a unitary shell the pass may rewrite at top level. +static bool isWalkableUnitaryShell(Operation* op) { + return !isa(op) && + !isExcludedFromTopLevelUnitaryWalk(op); +} + +/// Builds the constant 4x4 matrix for a two-qubit op (bare or single-target +/// `CtrlOp`). Returns false for a `CtrlOp` that is not +/// single-control/single-target, or an op whose matrix is not known at compile +/// time. +static bool assignTwoQubitOpMatrix(Operation* op, Matrix4x4& matrix) { + if (auto ctrl = dyn_cast(op)) { + if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { + return false; + } + return cast(ctrl.getOperation()) + .getUnitaryMatrix4x4(matrix); + } + auto unitary = cast(op); + assert(unitary.isTwoQubit() && + "only two-qubit unitary shells are passed to assignTwoQubitOpMatrix"); + return unitary.getUnitaryMatrix4x4(matrix); +} + +/// Whether `unitary` is a single-qubit gate that can join a run. +static bool isOneQubitRunMember(UnitaryOpInterface unitary) { + if (!unitary || !unitary.isSingleQubit() || + !isWalkableUnitaryShell(unitary.getOperation())) { + return false; + } + Matrix2x2 matrix; + return unitary.getUnitaryMatrix2x2(matrix); +} + +/// Whether `unitary` is a two-qubit gate that can join a run. +static bool isTwoQubitRunMember(UnitaryOpInterface unitary) { + if (!unitary || !unitary.isTwoQubit() || + !isWalkableUnitaryShell(unitary.getOperation())) { + return false; + } + Matrix4x4 matrix; + return assignTwoQubitOpMatrix(unitary.getOperation(), matrix); +} + +// --- Wire navigation ------------------------------------------------------ // + +/// The sole run-member consumer of `wire`, or a null interface when its unique +/// user cannot join a run. `wire` is single-use by qubit linearity. +static UnitaryOpInterface uniqueUnitaryUser(Value wire) { + assert(wire.hasOneUse() && + "qubit values are single-use, so a run tail has exactly one user"); + auto unitary = dyn_cast(*wire.user_begin()); + if (!unitary) { + return {}; + } + if (unitary.isTwoQubit()) { + return isTwoQubitRunMember(unitary) ? unitary : UnitaryOpInterface{}; + } + if (unitary.isSingleQubit()) { + return isOneQubitRunMember(unitary) ? unitary : UnitaryOpInterface{}; + } + return {}; +} + +/// Traces `wire` upstream through single-qubit gates to the two-qubit run +/// member terminating the chain, or `nullptr` if the chain is broken. +static Operation* twoQubitGateAtEndOfOneQChain(Value wire) { + Value cur = wire; + while (Operation* def = cur.getDefiningOp()) { + auto unitary = dyn_cast(def); + if (!unitary) { + return nullptr; + } + if (unitary.isTwoQubit()) { + return isTwoQubitRunMember(unitary) ? def : nullptr; + } + if (!isOneQubitRunMember(unitary)) { + return nullptr; + } + cur = unitary.getInputQubit(0); + } + return nullptr; +} + +/// Whether both input wires of `op` come from one earlier two-qubit run, making +/// `op` a continuation of that run rather than a fresh run start. +static bool feedsFromSameTwoQubitRun(UnitaryOpInterface op) { + const Value in0 = op.getInputQubit(0); + const Value in1 = op.getInputQubit(1); + assert(in0.hasOneUse() && in1.hasOneUse() && + "qubit values are single-use, so a run member consumes each input " + "exactly once"); + Operation* gate0 = twoQubitGateAtEndOfOneQChain(in0); + Operation* gate1 = twoQubitGateAtEndOfOneQChain(in1); + return gate0 != nullptr && gate0 == gate1; +} + +// --- Run scanning --------------------------------------------------------- // + +/// Appends a two-qubit gate to `run`, composing its matrix. No-op unless both +/// of `op`'s inputs are the run's current tail wires (in either order), keeping +/// the run confined to a single pair of wires. +static void absorbTwoQubitIntoRun(FusableTwoQubitRun& run, + UnitaryOpInterface op) { + Matrix4x4 opMatrix; + [[maybe_unused]] const bool assigned = + assignTwoQubitOpMatrix(op.getOperation(), opMatrix); + assert(assigned && "a two-qubit run member always exposes a 4x4 matrix"); + const Value in0 = op.getInputQubit(0); + const Value in1 = op.getInputQubit(1); + size_t id0 = 0; + size_t id1 = 1; + if (in0 == run.tailA && in1 == run.tailB) { + run.tailA = op.getOutputQubit(0); + run.tailB = op.getOutputQubit(1); + } else if (in0 == run.tailB && in1 == run.tailA) { + id0 = 1; + id1 = 0; + run.tailA = op.getOutputQubit(1); + run.tailB = op.getOutputQubit(0); + } else { + llvm_unreachable( + "a unique user of both tail wires connects to both of them"); + } + run.composed.premultiplyBy(opMatrix.reorderForQubits(id0, id1)); + run.ops.push_back(op.getOperation()); + ++run.numTwoQ; +} + +/// Appends a single-qubit gate on run wire `wireIndex` (0 = A, 1 = B). +static void absorbOneQubitIntoRun(FusableTwoQubitRun& run, + UnitaryOpInterface op, unsigned wireIndex) { + Matrix2x2 raw; + [[maybe_unused]] const bool assigned = op.getUnitaryMatrix2x2(raw); + assert(assigned && "a single-qubit run member always exposes a 2x2 matrix"); + run.composed.premultiplyBy(raw.embedInTwoQubit(wireIndex)); + run.ops.push_back(op.getOperation()); + (wireIndex == 0 ? run.tailA : run.tailB) = op.getOutputQubit(0); +} + +/// Walks forward from `head`, composing the run's matrix and metadata. Absorbs +/// a following two-qubit gate when it keeps both run wires together, otherwise +/// the single-qubit gate first in program order; stops at the first boundary +/// that would split the run's two wires. +static FusableTwoQubitRun scanFusableTwoQubitRun(UnitaryOpInterface head) { + FusableTwoQubitRun run; + [[maybe_unused]] const bool assigned = + assignTwoQubitOpMatrix(head.getOperation(), run.composed); + assert(assigned && "a run head is a two-qubit member with a 4x4 matrix"); + run.tailA = head.getOutputQubit(0); + run.tailB = head.getOutputQubit(1); + run.ops.push_back(head.getOperation()); + run.numTwoQ = 1; + + while (true) { + UnitaryOpInterface nextOnA = uniqueUnitaryUser(run.tailA); + UnitaryOpInterface nextOnB = uniqueUnitaryUser(run.tailB); + const bool sameOp = + nextOnA && nextOnB && nextOnA.getOperation() == nextOnB.getOperation(); + + if (sameOp && nextOnA.isTwoQubit()) { + absorbTwoQubitIntoRun(run, nextOnA); + continue; + } + + const bool aSingle = nextOnA && nextOnA.isSingleQubit() && !sameOp; + const bool bSingle = nextOnB && nextOnB.isSingleQubit() && !sameOp; + if (aSingle && bSingle && nextOnA->getBlock() != nextOnB->getBlock()) { + break; + } + if (aSingle && (!bSingle || nextOnA->isBeforeInBlock(nextOnB))) { + absorbOneQubitIntoRun(run, nextOnA, /*wireIndex=*/0); + continue; + } + if (bSingle) { + absorbOneQubitIntoRun(run, nextOnB, /*wireIndex=*/1); + continue; + } + break; + } + return run; +} + +/// Erases all run members, successors first so each is dead when erased. +static void eraseFusableRun(PatternRewriter& rewriter, + const FusableTwoQubitRun& run) { + for (Operation* member : llvm::reverse(run.ops)) { + rewriter.eraseOp(member); + } +} + +namespace { + +/// Fuses a maximal constant run only when generic resynthesis strictly reduces +/// its two-qubit operation count. +struct OptimizeTwoQubitUnitaryRunsPattern final + : OpInterfaceRewritePattern { + OptimizeTwoQubitUnitaryRunsPattern(MLIRContext* ctx, + NativeSynthesisBasis basisIn, + bool& changedIn) + : OpInterfaceRewritePattern(ctx), basis(basisIn), changed(&changedIn) {} + + NativeSynthesisBasis basis; + bool* changed; + + /// Whether `op` anchors a run: a two-qubit run member whose two wires are not + /// both fed by the same earlier run (which would make it a continuation). + static bool isRunStart(UnitaryOpInterface op) { + return isTwoQubitRunMember(op) && !feedsFromSameTwoQubitRun(op); + } + + /// Fuse the run anchored at `op` only if it has multiple constant members and + /// Weyl resynthesis uses fewer entanglers than the original run. + LogicalResult matchAndRewrite(UnitaryOpInterface op, + PatternRewriter& rewriter) const override { + if (!isRunStart(op)) { + return failure(); + } + + FusableTwoQubitRun run = scanFusableTwoQubitRun(op); + if (run.ops.size() < 2) { + return failure(); + } + + const auto native = basis.decomposeTarget(run.composed); + if (native.numBasisUses >= run.numTwoQ) { + return failure(); + } + + auto firstOp = cast(run.ops.front()); + rewriter.setInsertionPoint(firstOp); + const auto synthesized = synthesizeUnitary2QWeyl( + rewriter, firstOp.getLoc(), firstOp.getInputQubit(0), + firstOp.getInputQubit(1), run.composed, basis); + if (failed(synthesized)) { + firstOp->emitError("failed to emit synthesized two-qubit gate sequence"); + return failure(); + } + decomposition::emitGPhaseIfNeeded(rewriter, firstOp.getLoc(), + synthesized->globalPhase); + rewriter.replaceAllUsesWith(run.tailA, synthesized->qubit0); + rewriter.replaceAllUsesWith(run.tailB, synthesized->qubit1); + eraseFusableRun(rewriter, run); + *changed = true; + return success(); + } +}; + +} // namespace + +using SiteId = CompilerTarget::SiteId; + +static FailureOr resolveProviderSite(Value value) { + llvm::SmallDenseSet visited; + while (value) { + if (!visited.insert(value).second) { + return failure(); + } + + if (auto argument = dyn_cast(value)) { + Operation* const parent = argument.getOwner()->getParentOp(); + const auto index = argument.getArgNumber(); + if (auto ifOp = dyn_cast_or_null(parent); + ifOp && index < ifOp.getQubits().size()) { + value = ifOp.getQubits()[index]; + continue; + } + if (auto switchOp = dyn_cast_or_null(parent); + switchOp && index < switchOp.getTargets().size()) { + value = switchOp.getTargets()[index]; + continue; + } + if (auto forOp = dyn_cast_or_null(parent); + forOp && index > 0 && index <= forOp.getInits().size()) { + value = forOp.getInits()[index - 1]; + continue; + } + if (auto whileOp = dyn_cast_or_null(parent); + whileOp && index < whileOp.getInits().size()) { + value = whileOp.getInits()[index]; + continue; + } + return failure(); + } + + Operation* const definingOp = value.getDefiningOp(); + if (auto staticOp = dyn_cast_or_null(definingOp)) { + return static_cast(staticOp.getIndex()); + } + if (auto unitary = dyn_cast_or_null(definingOp)) { + value = unitary.getInputForOutput(value); + continue; + } + if (auto measureOp = dyn_cast_or_null(definingOp)) { + value = measureOp.getQubitIn(); + continue; + } + if (auto resetOp = dyn_cast_or_null(definingOp)) { + value = resetOp.getQubitIn(); + continue; + } + if (auto forOp = dyn_cast_or_null(definingOp)) { + auto result = dyn_cast(value); + if (!result) { + return failure(); + } + value = forOp.getTiedLoopInit(result)->get(); + continue; + } + if (auto whileOp = dyn_cast_or_null(definingOp)) { + auto result = dyn_cast(value); + if (!result || result.getResultNumber() >= whileOp.getInits().size()) { + return failure(); + } + value = whileOp.getInits()[result.getResultNumber()]; + continue; + } + if (auto ifOp = dyn_cast_or_null(definingOp)) { + auto result = dyn_cast(value); + if (!result) { + return failure(); + } + OpOperand* const input = ifOp.getTiedQubit(result); + if (input == nullptr) { + return failure(); + } + value = input->get(); + continue; + } + if (auto switchOp = dyn_cast_or_null(definingOp)) { + auto result = dyn_cast(value); + if (!result) { + return failure(); + } + OpOperand* const input = switchOp.getTiedTarget(result); + if (input == nullptr) { + return failure(); + } + value = input->get(); + continue; + } + return failure(); + } + return failure(); +} + +static FailureOr> providerLocus(Operation* operation) { + SmallVector qubits; + if (auto unitary = dyn_cast(operation)) { + llvm::append_range(qubits, unitary.getInputQubits()); + } else if (auto measureOp = dyn_cast(operation)) { + qubits.emplace_back(measureOp.getQubitIn()); + } else if (auto resetOp = dyn_cast(operation)) { + qubits.emplace_back(resetOp.getQubitIn()); + } else { + return failure(); + } + + SmallVector locus; + locus.reserve(qubits.size()); + for (const Value qubit : qubits) { + const auto site = resolveProviderSite(qubit); + if (failed(site)) { + return failure(); + } + locus.emplace_back(*site); + } + return locus; +} + +static bool requiresTargetSynthesis(Operation* operation, + const CompilerTarget& target, + const ArrayRef locus) { + return !target.supports(operation, locus); +} + +/// Whether a target-selected entangler must use the reverse provider locus. +/// +/// `CompilerTarget` selects an operand-symmetric gate as globally usable when +/// either ordered orientation is available. Preserve the logical wire order +/// while emitting that gate at the provider-supported orientation. +static bool useReverseEntanglerLocus(const CompilerTarget& target, + const CompilerTarget::GateKind entangler, + const ArrayRef locus) { + assert(locus.size() == 2 && "a synthesis entangler has two provider sites"); + if (target.supports(entangler, locus)) { + return false; + } + const std::array reverseLocus{locus[1], locus[0]}; + assert(target.supports(entangler, reverseLocus) && + "a globally selected entangler is available in one orientation"); + return true; +} + +static void appendProviderLocus(InFlightDiagnostic& diagnostic, + const ArrayRef locus) { + diagnostic << '['; + for (const auto [index, site] : llvm::enumerate(locus)) { + if (index != 0) { + diagnostic << ", "; + } + diagnostic << site; + } + diagnostic << ']'; +} + +namespace { + +struct SynthesisNeed { + Operation* operation; + SmallVector locus; +}; + +} // namespace + +static std::optional +findSynthesisNeed(Operation* root, const CompilerTarget& target) { + std::optional need; + root->walk([&](Operation* operation) { + auto unitary = dyn_cast(operation); + if (!unitary || !isWalkableUnitaryShell(operation) || + (unitary.getNumQubits() != 1 && unitary.getNumQubits() != 2)) { + return WalkResult::advance(); + } + auto locus = providerLocus(operation); + if (failed(locus) || !requiresTargetSynthesis(operation, target, *locus)) { + return WalkResult::advance(); + } + need.emplace( + SynthesisNeed{.operation = operation, .locus = std::move(*locus)}); + return WalkResult::interrupt(); + }); + return need; +} + +namespace { + +struct LowerTargetSingleQubitOpPattern final + : OpInterfaceRewritePattern { + LowerTargetSingleQubitOpPattern(MLIRContext* context, + const CompilerTarget& targetIn, + NativeSynthesisBasis basisIn) + : OpInterfaceRewritePattern(context), target(targetIn), basis(basisIn) {} + + CompilerTarget target; + NativeSynthesisBasis basis; + + LogicalResult matchAndRewrite(UnitaryOpInterface op, + PatternRewriter& rewriter) const override { + Operation* const operation = op.getOperation(); + if (!op.isSingleQubit() || !isWalkableUnitaryShell(operation)) { + return failure(); + } + const auto locus = providerLocus(operation); + if (failed(locus) || !requiresTargetSynthesis(operation, target, *locus)) { + return failure(); + } + Matrix2x2 matrix; + if (!op.getUnitaryMatrix2x2(matrix)) { + return failure(); + } + + rewriter.setInsertionPoint(operation); + const auto synthesized = decomposition::synthesizeUnitary1QEuler( + rewriter, operation->getLoc(), op.getInputQubit(0), matrix, + /*runSize=*/1, /*hasNonBasisGate=*/true, basis.singleQubit); + if (!synthesized) { + return failure(); + } + decomposition::emitGPhaseIfNeeded(rewriter, operation->getLoc(), + synthesized->globalPhase); + rewriter.replaceOp(operation, synthesized->qubit); + return success(); + } +}; + +struct LowerTargetTwoQubitOpPattern final + : OpInterfaceRewritePattern { + LowerTargetTwoQubitOpPattern(MLIRContext* context, + const CompilerTarget& targetIn, + NativeSynthesisBasis basisIn) + : OpInterfaceRewritePattern(context), target(targetIn), basis(basisIn) {} + + CompilerTarget target; + NativeSynthesisBasis basis; + + LogicalResult matchAndRewrite(UnitaryOpInterface op, + PatternRewriter& rewriter) const override { + Operation* const operation = op.getOperation(); + if (!op.isTwoQubit() || !isWalkableUnitaryShell(operation)) { + return failure(); + } + const auto locus = providerLocus(operation); + if (failed(locus) || !requiresTargetSynthesis(operation, target, *locus)) { + return failure(); + } + Matrix4x4 matrix; + if (!assignTwoQubitOpMatrix(operation, matrix)) { + return failure(); + } + + Value input0; + Value input1; + if (auto ctrl = dyn_cast(operation)) { + input0 = ctrl.getInputControl(0); + input1 = ctrl.getInputTarget(0); + } else { + input0 = op.getInputQubit(0); + input1 = op.getInputQubit(1); + } + + rewriter.setInsertionPoint(operation); + const auto synthesized = synthesizeUnitary2QWeyl( + rewriter, operation->getLoc(), input0, input1, matrix, basis, + useReverseEntanglerLocus(target, basis.entangler, *locus)); + if (failed(synthesized)) { + return failure(); + } + decomposition::emitGPhaseIfNeeded(rewriter, operation->getLoc(), + synthesized->globalPhase); + rewriter.replaceOp(operation, + ValueRange{synthesized->qubit0, synthesized->qubit1}); + return success(); + } +}; + +} // namespace + +static LogicalResult optimizeTwoQubitRuns(ModuleOp moduleOp) { + const auto basis = + NativeSynthesisBasis::fromCompilerTarget(CompilerTarget::SynthesisBasis{ + .singleQubit = CompilerTarget::SingleQubitBasis::U, + .entangler = CompilerTarget::GateKind::CX}); + bool changed = false; + RewritePatternSet patterns(moduleOp.getContext()); + patterns.add(moduleOp.getContext(), basis, + changed); + if (failed(applyPatternsGreedily(moduleOp, std::move(patterns)))) { + return failure(); + } + if (!changed) { + return success(); + } + return mlir::mqt::normalizeGlobalPhases(moduleOp); +} + +namespace { + +struct OptimizeTwoQubitUnitaryRunsPass final + : PassWrapper> { + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(OptimizeTwoQubitUnitaryRunsPass) + + void getDependentDialects(DialectRegistry& registry) const override { + registry.insert(); + } + +protected: + void runOnOperation() override { + ModuleOp moduleOp = getOperation(); + if (failed(optimizeTwoQubitRuns(moduleOp))) { + signalPassFailure(); + } + } +}; + +struct TargetNativeSynthesisPass final + : PassWrapper> { + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(TargetNativeSynthesisPass) + + explicit TargetNativeSynthesisPass(const CompilerTarget& targetIn) + : target(targetIn) {} + + void getDependentDialects(DialectRegistry& registry) const override { + registry.insert(); + } + +protected: + void runOnOperation() override { + if (!target.hasExplicitOperations()) { + return; + } + ModuleOp moduleOp = getOperation(); + auto need = findSynthesisNeed(moduleOp, target); + if (!need) { + return; + } + + const auto targetBasis = target.synthesisBasis(); + if (!targetBasis) { + auto diagnostic = need->operation->emitError() + << "target-native synthesis cannot lower operation '" + << need->operation->getName() + << "' at ordered provider locus "; + appendProviderLocus(diagnostic, need->locus); + diagnostic << ": the target has no globally usable synthesis basis"; + signalPassFailure(); + return; + } + const auto basis = NativeSynthesisBasis::fromCompilerTarget(*targetBasis); + RewritePatternSet patterns(&getContext()); + patterns.add( + &getContext(), target, basis); + if (failed(applyPatternsGreedily(moduleOp, std::move(patterns)))) { + signalPassFailure(); + return; + } + + need = findSynthesisNeed(moduleOp, target); + if (need) { + auto diagnostic = need->operation->emitError() + << "target-native synthesis cannot lower operation '" + << need->operation->getName() + << "' at ordered provider locus "; + appendProviderLocus(diagnostic, need->locus); + diagnostic << ": its unitary matrix is not available at compile time"; + signalPassFailure(); + return; + } + if (failed(mlir::mqt::normalizeGlobalPhases(moduleOp))) { + signalPassFailure(); + } + } + + CompilerTarget target; +}; + +struct VerifyTargetConformancePass final + : PassWrapper> { + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(VerifyTargetConformancePass) + + explicit VerifyTargetConformancePass(const CompilerTarget& targetIn) + : target(targetIn) {} + +protected: + void runOnOperation() override { + WalkResult result = getOperation()->walk([&](Operation* operation) { + size_t parameterCount = 0; + if (auto unitary = dyn_cast(operation)) { + if (isExcludedFromTopLevelUnitaryWalk(operation)) { + return WalkResult::advance(); + } + parameterCount = unitary.getNumParams(); + } else if (!isa(operation)) { + return WalkResult::advance(); + } + + const auto locus = providerLocus(operation); + if (failed(locus)) { + operation->emitError() + << "target conformance requires every hardware qubit operand to " + "trace to a qco.static provider site"; + return WalkResult::interrupt(); + } + if (target.supports(operation, *locus)) { + return WalkResult::advance(); + } + + auto diagnostic = operation->emitError() + << "target does not support operation '" + << operation->getName() << "' with arity " + << locus->size() << " and " << parameterCount + << " parameter(s) at ordered provider locus "; + appendProviderLocus(diagnostic, *locus); + return WalkResult::interrupt(); + }); + if (result.wasInterrupted()) { + signalPassFailure(); + } + } + + CompilerTarget target; +}; + +} // namespace + +std::unique_ptr createOptimizeTwoQubitUnitaryRuns() { + return std::make_unique(); +} + +std::unique_ptr +createTargetNativeSynthesis(const CompilerTarget& target) { + return std::make_unique(target); +} + +std::unique_ptr +createVerifyTargetConformance(const CompilerTarget& target) { + return std::make_unique(target); +} + +} // namespace mlir::qco diff --git a/mlir/lib/Support/Passes.cpp b/mlir/lib/Support/Passes.cpp index d906d72ff1..e0032aa3b6 100644 --- a/mlir/lib/Support/Passes.cpp +++ b/mlir/lib/Support/Passes.cpp @@ -50,7 +50,6 @@ void registerMQTCompilerPasses() { static const auto REGISTERED = [] { qco::registerDecomposeMultiControlled(); qco::registerFuseSingleQubitUnitaryRuns(); - qco::registerFuseTwoQubitUnitaryRuns(); qco::registerHadamardLifting(); qco::registerMeasurementLifting(); qco::registerMergeSingleQubitRotationGates(); diff --git a/mlir/tools/mqt-cc/mqt-cc.cpp b/mlir/tools/mqt-cc/mqt-cc.cpp index 0f22d57975..4dde094da1 100644 --- a/mlir/tools/mqt-cc/mqt-cc.cpp +++ b/mlir/tools/mqt-cc/mqt-cc.cpp @@ -85,13 +85,6 @@ static llvm::cl::opt outputFormat( "qir-adaptive, or jeff"), llvm::cl::value_desc("format"), llvm::cl::init("mlir")); -static llvm::cl::opt nativeGates( - "native-gates", - llvm::cl::desc( - "Comma-separated native gate menu for the fuse-two-qubit-unitary-runs " - "pass"), - llvm::cl::value_desc("csv"), llvm::cl::init("")); - namespace { enum class InputFormat : std::uint8_t { MLIR, QASM, Jeff }; enum class InputDialect : std::uint8_t { QC, QCO }; @@ -397,19 +390,6 @@ static int runCompiler(int argc, char** argv) { "QCO optimization.\n"; return 1; } - const llvm::StringRef nativeGateMenu = - llvm::StringRef(nativeGates.getValue()).trim(); - if (nativeGates.getNumOccurrences() > 0 && nativeGateMenu.empty()) { - llvm::errs() << "--native-gates must not be empty.\n"; - return 1; - } - if (nativeGates.getNumOccurrences() > 0 && - (*parsedOutputFormat == OutputFormat::QCImport || - *parsedOutputFormat == OutputFormat::QCO)) { - llvm::errs() << "--native-gates requires an output that passes through " - "QCO optimization.\n"; - return 1; - } if (enableDecomposeMultiControlled && !isDecomposeMultiControlledConfigValid( decomposeMultiControlledMinControls.getValue())) { @@ -459,12 +439,6 @@ static int runCompiler(int argc, char** argv) { populateDefaultQCOOptimizationPipeline(pm); } populateQCOCleanupPipeline(pm); - if (!nativeGateMenu.empty()) { - pm.addPass(qco::createFuseTwoQubitUnitaryRuns( - qco::FuseTwoQubitUnitaryRunsOptions{ - .nativeGates = nativeGateMenu.str(), - })); - } return success(); }))) { return 1; diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 8f5cdec5f7..503bab4098 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -918,9 +918,6 @@ cx q[0], q[2]; EXPECT_TRUE(qco.fuseSingleQubitUnitaryRuns("zyz")); EXPECT_NE(qco.str(), beforeFusion); - const auto beforeTwoQubitFusion = qco.str(); - EXPECT_TRUE(qco.fuseTwoQubitUnitaryRuns("u,cx")); - EXPECT_NE(qco.str(), beforeTwoQubitFusion); const std::vector> coupling = {{0, 1}, {1, 2}}; EXPECT_TRUE(qco.placeAndRoute(coupling)); @@ -1641,106 +1638,4 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(mlir::qir::singleControlledXOnIndividualQubits), false, true, "reuse-qubits,mqt-qco-default"})); -/** - * @brief Test: fuseTwoQubitUnitaryRuns lowers one- and two-qubit operations - * into native menus. - */ -TEST_F(CompilerPipelineTest, FuseTwoQubitUnitaryRunsLowersToNativeMenu) { - const std::string qasm = R"(OPENQASM 3.0; -include "stdgates.inc"; -qubit[2] q; -h q[0]; -swap q[0], q[1]; -)"; - auto qc = QCProgram::fromQASMString(qasm); - ASSERT_TRUE(qc); - auto qcoResult = std::move(*qc).intoQCO(); - ASSERT_TRUE(qcoResult); - auto qco = std::move(*qcoResult); - ASSERT_TRUE(qco.cleanup()); - EXPECT_NE(qco.str().find("qco.h"), std::string::npos); - EXPECT_NE(qco.str().find("qco.swap"), std::string::npos); - - EXPECT_TRUE(qco.fuseTwoQubitUnitaryRuns("x,sx,rz,cx")); - EXPECT_EQ(qco.str().find("qco.h"), std::string::npos); - EXPECT_EQ(qco.str().find("qco.swap"), std::string::npos); - EXPECT_NE(qco.str().find("qco.ctrl"), std::string::npos); - - auto qcU = QCProgram::fromQASMString(qasm); - ASSERT_TRUE(qcU); - auto qcoUResult = std::move(*qcU).intoQCO(); - ASSERT_TRUE(qcoUResult); - auto qcoU = std::move(*qcoUResult); - ASSERT_TRUE(qcoU.cleanup()); - EXPECT_TRUE(qcoU.fuseTwoQubitUnitaryRuns("u,cx")); - EXPECT_EQ(qcoU.str().find("qco.h"), std::string::npos); - EXPECT_EQ(qcoU.str().find("qco.swap"), std::string::npos); - EXPECT_NE(qcoU.str().find("qco.u"), std::string::npos); - EXPECT_NE(qcoU.str().find("qco.ctrl"), std::string::npos); - - auto qcExpanded = QCProgram::fromQASMString(qasm); - ASSERT_TRUE(qcExpanded); - auto qcoExpandedResult = std::move(*qcExpanded).intoQCO(); - ASSERT_TRUE(qcoExpandedResult); - auto qcoExpanded = std::move(*qcoExpandedResult); - ASSERT_TRUE(qcoExpanded.cleanup()); - EXPECT_TRUE(qcoExpanded.fuseTwoQubitUnitaryRuns("u,rx,rz,cx,cz")); - EXPECT_EQ(qcoExpanded.str().find("qco.h"), std::string::npos); - EXPECT_EQ(qcoExpanded.str().find("qco.swap"), std::string::npos); - EXPECT_NE(qcoExpanded.str().find("qco.ctrl"), std::string::npos); -} - -/** - * @brief Test: fuseTwoQubitUnitaryRuns rejects invalid native gate menus. - */ -TEST_F(CompilerPipelineTest, FuseTwoQubitUnitaryRunsRejectsInvalidMenus) { - const std::string qasm = R"(OPENQASM 3.0; -include "stdgates.inc"; -qubit[2] q; -h q[0]; -h q[1]; -)"; - auto qc = QCProgram::fromQASMString(qasm); - ASSERT_TRUE(qc); - auto qcoResult = std::move(*qc).intoQCO(); - ASSERT_TRUE(qcoResult); - auto qco = std::move(*qcoResult); - ASSERT_TRUE(qco.cleanup()); - EXPECT_FALSE(qco.fuseTwoQubitUnitaryRuns("cx,cz")); - - auto qcInvalid = QCProgram::fromQASMString(qasm); - ASSERT_TRUE(qcInvalid); - auto qcoInvalidResult = std::move(*qcInvalid).intoQCO(); - ASSERT_TRUE(qcoInvalidResult); - auto qcoInvalid = std::move(*qcoInvalidResult); - ASSERT_TRUE(qcoInvalid.cleanup()); - EXPECT_FALSE(qcoInvalid.fuseTwoQubitUnitaryRuns("not-a-gate")); -} - -/** - * @brief Test: the public API rejects an empty native gate menu. - */ -TEST_F(CompilerPipelineTest, FuseTwoQubitUnitaryRunsRejectsEmptyMenu) { - const std::string qasm = R"(OPENQASM 3.0; -include "stdgates.inc"; -qubit[2] q; -h q[0]; -h q[1]; -)"; - auto qc = QCProgram::fromQASMString(qasm); - ASSERT_TRUE(qc); - auto qcoResult = std::move(*qc).intoQCO(); - ASSERT_TRUE(qcoResult); - auto qco = std::move(*qcoResult); - ASSERT_TRUE(qco.cleanup()); - const auto before = qco.str(); - EXPECT_NE(before.find("qco.h"), std::string::npos); - - EXPECT_FALSE(qco.fuseTwoQubitUnitaryRuns("")); - EXPECT_EQ(qco.str(), before); - - EXPECT_FALSE(qco.fuseTwoQubitUnitaryRuns(" \t ")); - EXPECT_EQ(qco.str(), before); -} - } // namespace mqt::test::compiler diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index e5efedc328..43707f0c99 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -9,10 +9,11 @@ */ #include "dd/Package.hpp" +#include "mlir/Compiler/Target.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/NativeGateset.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/SynthesisBasis.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" #include "mlir/Dialect/QCO/Utils/DDFunctionality.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" @@ -456,7 +457,7 @@ computeTwoQubitUnitaryFromFunc(func::FuncOp funcOp) { [[nodiscard]] static Synthesized2QCircuit synthesize2QMatrix(MLIRContext* ctx, const Matrix4x4& target, - const NativeGateset& spec) { + const NativeSynthesisBasis& basis) { OwningOpRef mlirModule = ModuleOp::create(UnknownLoc::get(ctx)); OpBuilder builder(ctx); builder.setInsertionPointToStart(mlirModule->getBody()); @@ -469,8 +470,9 @@ synthesize2QMatrix(MLIRContext* ctx, const Matrix4x4& target, auto* entry = func.addEntryBlock(); builder.setInsertionPointToStart(entry); - const auto synthesized = synthesizeUnitary2QWeyl( - builder, loc, entry->getArgument(0), entry->getArgument(1), target, spec); + const auto synthesized = + synthesizeUnitary2QWeyl(builder, loc, entry->getArgument(0), + entry->getArgument(1), target, basis); if (failed(synthesized)) { ADD_FAILURE() << "synthesizeUnitary2QWeyl failed during test synthesis"; } else { @@ -482,8 +484,8 @@ synthesize2QMatrix(MLIRContext* ctx, const Matrix4x4& target, } static void expectSynthesized2QMatrix(MLIRContext* ctx, const Matrix4x4& target, - const NativeGateset& spec) { - const auto circuit = synthesize2QMatrix(ctx, target, spec); + const NativeSynthesisBasis& basis) { + const auto circuit = synthesize2QMatrix(ctx, target, basis); ASSERT_TRUE(succeeded(verify(*circuit.mlirModule))); const auto actual = computeTwoQubitUnitaryFromFunc(circuit.func); ASSERT_TRUE(succeeded(actual)); @@ -508,7 +510,7 @@ struct MlirTestContext { struct WeylSynthesisCase { const char* name; - const char* nativeGates; + CompilerTarget::SynthesisBasis basis; Matrix4x4 (*target)(); }; @@ -519,7 +521,7 @@ class WeylSynthesisTest : public testing::TestWithParam { void SetUp() override { mlir.setUp(); } }; -class NativeGatesetMlirTest : public testing::Test { +class WeylSynthesisMlirTest : public testing::Test { protected: MlirTestContext mlir; @@ -529,22 +531,27 @@ class NativeGatesetMlirTest : public testing::Test { } // namespace TEST_P(WeylSynthesisTest, PreservesTargetUnitary) { - const auto spec = NativeGateset::parse(GetParam().nativeGates); - ASSERT_TRUE(spec); - expectSynthesized2QMatrix(mlir.ctx(), GetParam().target(), *spec); + const auto basis = NativeSynthesisBasis::fromCompilerTarget(GetParam().basis); + expectSynthesized2QMatrix(mlir.ctx(), GetParam().target(), basis); } INSTANTIATE_TEST_SUITE_P( - Gatesets, WeylSynthesisTest, + TargetBases, WeylSynthesisTest, testing::Values( - WeylSynthesisCase{"CxGeneric", "u,cx", - [] { return TWO_QUBIT_CONTROLLED_X01; }}, - WeylSynthesisCase{"ProductGeneric", "u,cx", - [] { - return Matrix4x4::kron(RZOp::unitaryMatrix(1.0), - RYOp::unitaryMatrix(0.3)); - }}, - WeylSynthesisCase{"IbmBasic", "x,sx,rz,cx", + WeylSynthesisCase{ + "CxGeneric", + {CompilerTarget::SingleQubitBasis::U, CompilerTarget::GateKind::CX}, + [] { return TWO_QUBIT_CONTROLLED_X01; }}, + WeylSynthesisCase{ + "ProductGeneric", + {CompilerTarget::SingleQubitBasis::U, CompilerTarget::GateKind::CX}, + [] { + return Matrix4x4::kron(RZOp::unitaryMatrix(1.0), + RYOp::unitaryMatrix(0.3)); + }}, + WeylSynthesisCase{"IbmBasic", + {CompilerTarget::SingleQubitBasis::ZSXX, + CompilerTarget::GateKind::CX}, [] { return Matrix4x4::kron(HOp::getUnitaryMatrix(), Matrix2x2::identity()) * @@ -553,43 +560,56 @@ INSTANTIATE_TEST_SUITE_P( RYOp::unitaryMatrix(0.1)); }}, WeylSynthesisCase{ - "RxxGeneric", "u,rxx", + "RxxGeneric", + {CompilerTarget::SingleQubitBasis::U, + CompilerTarget::GateKind::RXX}, [] { return RXXOp::unitaryMatrix(std::numbers::pi / 2.0); }}, WeylSynthesisCase{ - "RyyGeneric", "u,ryy", + "RyyGeneric", + {CompilerTarget::SingleQubitBasis::U, + CompilerTarget::GateKind::RYY}, [] { return RYYOp::unitaryMatrix(std::numbers::pi / 2.0); }}, WeylSynthesisCase{ - "RzxGeneric", "u,rzx", + "RzxGeneric", + {CompilerTarget::SingleQubitBasis::U, + CompilerTarget::GateKind::RZX}, [] { return RZXOp::unitaryMatrix(std::numbers::pi / 2.0); }}, WeylSynthesisCase{ - "RzzGeneric", "u,rzz", + "RzzGeneric", + {CompilerTarget::SingleQubitBasis::U, + CompilerTarget::GateKind::RZZ}, [] { return RZZOp::unitaryMatrix(std::numbers::pi / 2.0); }}, - WeylSynthesisCase{"IswapGeneric", "u,iswap", + WeylSynthesisCase{"IswapGeneric", + {CompilerTarget::SingleQubitBasis::U, + CompilerTarget::GateKind::ISWAP}, [] { return iSWAPOp::getUnitaryMatrix(); }}, - WeylSynthesisCase{"CzGeneric", "u,cz", - [] { return TWO_QUBIT_CONTROLLED_Z; }}, - WeylSynthesisCase{"EcrGeneric", "u,ecr", + WeylSynthesisCase{ + "CzGeneric", + {CompilerTarget::SingleQubitBasis::U, CompilerTarget::GateKind::CZ}, + [] { return TWO_QUBIT_CONTROLLED_Z; }}, + WeylSynthesisCase{"EcrGeneric", + {CompilerTarget::SingleQubitBasis::U, + CompilerTarget::GateKind::ECR}, [] { return ECROp::getUnitaryMatrix(); }}), [](const testing::TestParamInfo& info) { return info.param.name; }); TEST(WeylSynthesisTest, IdentityRequiresNoEntanglers) { - for (const char* gateset : {"u,rxx", "u,ryy", "u,rzx", "u,rzz", "u,iswap", - "u,cz", "u,cx", "u,ecr"}) { - const auto spec = NativeGateset::parse(gateset); - ASSERT_TRUE(spec) << gateset; - const auto native = spec->decomposeTarget(Matrix4x4::identity()); - ASSERT_TRUE(native.has_value()) << gateset; - EXPECT_EQ(native->numBasisUses, 0U) << gateset; + for (const auto entangler : + {CompilerTarget::GateKind::RXX, CompilerTarget::GateKind::RYY, + CompilerTarget::GateKind::RZX, CompilerTarget::GateKind::RZZ, + CompilerTarget::GateKind::ISWAP, CompilerTarget::GateKind::CZ, + CompilerTarget::GateKind::CX, CompilerTarget::GateKind::ECR}) { + const auto basis = NativeSynthesisBasis::fromCompilerTarget( + {.singleQubit = CompilerTarget::SingleQubitBasis::U, + .entangler = entangler}); + const auto native = basis.decomposeTarget(Matrix4x4::identity()); + EXPECT_EQ(native.numBasisUses, 0U); } } -TEST(WeylSynthesisTest, RejectsGatesetWithoutEntangler) { - EXPECT_FALSE(NativeGateset::parse("u").has_value()); -} - -TEST_F(NativeGatesetMlirTest, ReconstructionRejectsUnhandledOps) { +TEST_F(WeylSynthesisMlirTest, ReconstructionRejectsUnhandledOps) { OpBuilder builder(mlir.ctx()); const Location loc = UnknownLoc::get(mlir.ctx()); const auto qubitTy = QubitType::get(mlir.ctx()); @@ -604,314 +624,3 @@ TEST_F(NativeGatesetMlirTest, ReconstructionRejectsUnhandledOps) { func::ReturnOp::create(builder, loc, ValueRange{meas.getQubitOut(), q1}); EXPECT_TRUE(failed(computeTwoQubitUnitaryFromFunc(func))); } - -TEST_F(NativeGatesetMlirTest, SynthesisFailsWithoutEulerBasis) { - const NativeGateset spec{.gates = {NativeGateKind::CX}}; - OpBuilder builder(mlir.ctx()); - const auto qubitTy = QubitType::get(mlir.ctx()); - const auto funcTy = - builder.getFunctionType({qubitTy, qubitTy}, {qubitTy, qubitTy}); - auto func = func::FuncOp::create(builder, UnknownLoc::get(mlir.ctx()), "main", - funcTy); - auto* entry = func.addEntryBlock(); - builder.setInsertionPointToStart(entry); - EXPECT_TRUE(failed(synthesizeUnitary2QWeyl( - builder, func.getLoc(), entry->getArgument(0), entry->getArgument(1), - TWO_QUBIT_CONTROLLED_X01, spec))); -} - -TEST_F(NativeGatesetMlirTest, SynthesisFailsWithoutEntangler) { - const NativeGateset spec{.gates = {NativeGateKind::U}}; - OpBuilder builder(mlir.ctx()); - const auto qubitTy = QubitType::get(mlir.ctx()); - const auto funcTy = - builder.getFunctionType({qubitTy, qubitTy}, {qubitTy, qubitTy}); - auto func = func::FuncOp::create(builder, UnknownLoc::get(mlir.ctx()), "main", - funcTy); - auto* entry = func.addEntryBlock(); - builder.setInsertionPointToStart(entry); - EXPECT_TRUE(failed(synthesizeUnitary2QWeyl( - builder, func.getLoc(), entry->getArgument(0), entry->getArgument(1), - TWO_QUBIT_CONTROLLED_X01, spec))); -} - -TEST(WeylSynthesisTest, EntanglerCountFailsWithoutEntangler) { - const NativeGateset spec{.gates = {NativeGateKind::U}}; - EXPECT_FALSE(spec.decomposeTarget(Matrix4x4::identity()).has_value()); -} - -TEST(NativeSpecTest, ParsesAndRejectsGatesets) { - const auto ibm = NativeGateset::parse("x,sx,rz,cx"); - ASSERT_TRUE(ibm); - EXPECT_TRUE(ibm->gates.contains(NativeGateKind::CX)); - EXPECT_TRUE(ibm->gates.contains(NativeGateKind::X)); - EXPECT_FALSE(NativeGateset::parse("x,sx,rz,not-a-gate").has_value()); - EXPECT_FALSE(NativeGateset::parse("u").has_value()); - - const auto whitespaceToken = NativeGateset::parse("u, ,cx"); - ASSERT_TRUE(whitespaceToken); - EXPECT_TRUE(whitespaceToken->gates.contains(NativeGateKind::U)); - EXPECT_TRUE(whitespaceToken->gates.contains(NativeGateKind::CX)); - - EXPECT_FALSE(NativeGateset::parse("x,sx,p,cx").has_value()); - EXPECT_FALSE(NativeGateset::parse("ry,p,cz").has_value()); - - const auto cxOnly = NativeGateset::parse("u,cx"); - ASSERT_TRUE(cxOnly); - EXPECT_TRUE(cxOnly->gates.contains(NativeGateKind::U)); - EXPECT_TRUE(cxOnly->gates.contains(NativeGateKind::CX)); - EXPECT_FALSE(cxOnly->gates.contains(NativeGateKind::CZ)); - EXPECT_FALSE(cxOnly->gates.contains(NativeGateKind::X)); - - const auto both = NativeGateset::parse("u,cx,cz"); - ASSERT_TRUE(both); - EXPECT_TRUE(both->gates.contains(NativeGateKind::CX)); - EXPECT_TRUE(both->gates.contains(NativeGateKind::CZ)); - EXPECT_EQ(both->entangler, NativeGateKind::CZ); - - const auto ecrOnly = NativeGateset::parse("u,ecr"); - ASSERT_TRUE(ecrOnly); - EXPECT_TRUE(ecrOnly->gates.contains(NativeGateKind::ECR)); - EXPECT_EQ(ecrOnly->entangler, NativeGateKind::ECR); - - // With CX/CZ also listed, CX/CZ win over ECR. - const auto cxCzOverEcr = NativeGateset::parse("u,cz,cx,ecr"); - ASSERT_TRUE(cxCzOverEcr); - EXPECT_EQ(cxCzOverEcr->entangler, NativeGateKind::CZ); - - const auto iswapOnly = NativeGateset::parse("u,iswap"); - ASSERT_TRUE(iswapOnly); - EXPECT_TRUE(iswapOnly->gates.contains(NativeGateKind::ISWAP)); - EXPECT_EQ(iswapOnly->entangler, NativeGateKind::ISWAP); - - // iSWAP beats CZ/CX/ECR; two-qubit rotations still win when present. - const auto iswapOverCtrlEcr = NativeGateset::parse("u,iswap,cz,cx,ecr"); - ASSERT_TRUE(iswapOverCtrlEcr); - EXPECT_EQ(iswapOverCtrlEcr->entangler, NativeGateKind::ISWAP); - - // DCX is not a supported native-basis token. - EXPECT_FALSE(NativeGateset::parse("u,dcx").has_value()); - EXPECT_FALSE(NativeGateset::parse("u,cx,dcx").has_value()); - - const auto rzxOnly = NativeGateset::parse("u,rzx"); - ASSERT_TRUE(rzxOnly); - EXPECT_EQ(rzxOnly->entangler, NativeGateKind::RZX); - - // Two-qubit rotations: RXX > RYY > RZX > RZZ (alphabetic). - const auto rxxOverRest = - NativeGateset::parse("u,rzx,rzz,ryy,rxx,iswap,cx,cz,ecr"); - ASSERT_TRUE(rxxOverRest); - EXPECT_EQ(rxxOverRest->entangler, NativeGateKind::RXX); - - const auto ryyOverRzxRzz = NativeGateset::parse("u,rzx,ryy,rzz,iswap,cx"); - ASSERT_TRUE(ryyOverRzxRzz); - EXPECT_EQ(ryyOverRzxRzz->entangler, NativeGateKind::RYY); - - const auto rzxOverRzz = NativeGateset::parse("u,rzx,rzz,iswap,cx,cz"); - ASSERT_TRUE(rzxOverRzz); - EXPECT_EQ(rzxOverRzz->entangler, NativeGateKind::RZX); - - const auto rzzOverDiscrete = NativeGateset::parse("u,rzz,iswap,cx,cz,ecr"); - ASSERT_TRUE(rzzOverDiscrete); - EXPECT_EQ(rzzOverDiscrete->entangler, NativeGateKind::RZZ); - - const auto rzzOnly = NativeGateset::parse("u,rzz"); - ASSERT_TRUE(rzzOnly); - EXPECT_EQ(rzzOnly->entangler, NativeGateKind::RZZ); - - const auto ryyOnly = NativeGateset::parse("u,ryy"); - ASSERT_TRUE(ryyOnly); - EXPECT_EQ(ryyOnly->entangler, NativeGateKind::RYY); - - const auto rxxOnly = NativeGateset::parse("u,rxx"); - ASSERT_TRUE(rxxOnly); - EXPECT_EQ(rxxOnly->entangler, NativeGateKind::RXX); - - const auto rotationThenDiscrete = - NativeGateset::parse("u,rzz,rxx,iswap,cz,cx,ecr"); - ASSERT_TRUE(rotationThenDiscrete); - EXPECT_EQ(rotationThenDiscrete->entangler, NativeGateKind::RXX); - - const auto withoutRxx = NativeGateset::parse("u,ryy,iswap,cz,cx,ecr"); - ASSERT_TRUE(withoutRxx); - EXPECT_EQ(withoutRxx->entangler, NativeGateKind::RYY); - - const auto withoutRotations = NativeGateset::parse("u,iswap,cz,cx,ecr"); - ASSERT_TRUE(withoutRotations); - EXPECT_EQ(withoutRotations->entangler, NativeGateKind::ISWAP); -} - -TEST(NativeSpecTest, RejectsGatesetWithoutSingleQubitStrategy) { - EXPECT_FALSE(NativeGateset::parse("cx").has_value()); - EXPECT_FALSE(NativeGateset::parse("cz").has_value()); - EXPECT_FALSE(NativeGateset::parse("rx,cx").has_value()); -} - -TEST(NativeSpecTest, ResolvesEulerBasisFromGateset) { - const auto uGateset = NativeGateset::parse("u,cx"); - ASSERT_TRUE(uGateset); - EXPECT_EQ(*uGateset->eulerBasis, EulerBasis::U); - - const auto zsxx = NativeGateset::parse("x,sx,rz,cx"); - ASSERT_TRUE(zsxx); - EXPECT_EQ(*zsxx->eulerBasis, EulerBasis::ZSXX); - - const auto rGateset = NativeGateset::parse("r,cz"); - ASSERT_TRUE(rGateset); - EXPECT_EQ(*rGateset->eulerBasis, EulerBasis::R); - - const auto xzx = NativeGateset::parse("rx,rz,cz"); - ASSERT_TRUE(xzx); - EXPECT_EQ(*xzx->eulerBasis, EulerBasis::XZX); - - const auto xyx = NativeGateset::parse("rx,ry,cz"); - ASSERT_TRUE(xyx); - EXPECT_EQ(*xyx->eulerBasis, EulerBasis::XYX); - - const auto zyz = NativeGateset::parse("ry,rz,cz"); - ASSERT_TRUE(zyz); - EXPECT_EQ(*zyz->eulerBasis, EulerBasis::ZYZ); -} - -TEST_F(NativeGatesetMlirTest, AllowsOpMatchesGateset) { - const auto spec = NativeGateset::parse("u,cx"); - ASSERT_TRUE(spec); - - OpBuilder builder(mlir.ctx()); - const Location loc = UnknownLoc::get(mlir.ctx()); - const auto qubitTy = QubitType::get(mlir.ctx()); - const auto funcTy = - builder.getFunctionType({qubitTy, qubitTy}, {qubitTy, qubitTy}); - auto func = func::FuncOp::create(builder, loc, "allows_op", funcTy); - auto* entry = func.addEntryBlock(); - builder.setInsertionPointToStart(entry); - Value q0 = entry->getArgument(0); - Value q1 = entry->getArgument(1); - - EXPECT_TRUE(spec->allowsOp( - BarrierOp::create(builder, loc, ValueRange{q0, q1}).getOperation())); - EXPECT_TRUE( - spec->allowsOp(GPhaseOp::create(builder, loc, 0.1).getOperation())); - EXPECT_TRUE(spec->allowsOp( - UOp::create(builder, loc, q0, 0.1, 0.2, 0.3).getOperation())); - - auto cx = CtrlOp::create(builder, loc, q0, q1, [&](Value target) { - return XOp::create(builder, loc, target).getOutputQubit(0); - }); - EXPECT_TRUE(spec->allowsOp(cx.getOperation())); - - auto cxWithInterleavedH = - CtrlOp::create(builder, loc, q0, q1, [&](Value target) { - auto wire = XOp::create(builder, loc, target).getOutputQubit(0); - return HOp::create(builder, loc, wire).getOutputQubit(0); - }); - EXPECT_FALSE(spec->allowsOp(cxWithInterleavedH.getOperation())); - - EXPECT_FALSE(spec->allowsOp(XOp::create(builder, loc, q0).getOperation())); - EXPECT_FALSE( - spec->allowsOp(RXXOp::create(builder, loc, q0, q1, 0.2).getOperation())); - EXPECT_FALSE( - spec->allowsOp(ECROp::create(builder, loc, q0, q1).getOperation())); - - const auto rzSpec = NativeGateset::parse("x,sx,rz,cx"); - ASSERT_TRUE(rzSpec); - EXPECT_TRUE( - rzSpec->allowsOp(RZOp::create(builder, loc, q0, 0.3).getOperation())); - EXPECT_FALSE( - rzSpec->allowsOp(POp::create(builder, loc, q0, 0.3).getOperation())); - - auto hCtrl = CtrlOp::create(builder, loc, q0, q1, [&](Value target) { - return HOp::create(builder, loc, target).getOutputQubit(0); - }); - EXPECT_FALSE(spec->allowsOp(hCtrl.getOperation())); - - const auto funcTy3 = builder.getFunctionType({qubitTy, qubitTy, qubitTy}, - {qubitTy, qubitTy, qubitTy}); - auto func3 = func::FuncOp::create(builder, loc, "allows_op_ccx", funcTy3); - auto* entry3 = func3.addEntryBlock(); - builder.setInsertionPointToStart(entry3); - Value c0 = entry3->getArgument(0); - Value c1 = entry3->getArgument(1); - Value target = entry3->getArgument(2); - auto ccx = - CtrlOp::create(builder, loc, ValueRange{c0, c1}, target, [&](Value t) { - return XOp::create(builder, loc, t).getOutputQubit(0); - }); - EXPECT_FALSE(spec->allowsOp(ccx.getOperation())); - - const auto czSpec = NativeGateset::parse("u,cz"); - ASSERT_TRUE(czSpec); - auto cz = CtrlOp::create(builder, loc, q0, q1, [&](Value t) { - return ZOp::create(builder, loc, t).getOutputQubit(0); - }); - EXPECT_TRUE(czSpec->allowsOp(cz.getOperation())); - EXPECT_FALSE(czSpec->allowsOp(cx.getOperation())); - - const auto rxxSpec = NativeGateset::parse("u,rxx"); - ASSERT_TRUE(rxxSpec); - EXPECT_TRUE(rxxSpec->allowsOp( - RXXOp::create(builder, loc, q0, q1, 0.2).getOperation())); - EXPECT_TRUE(rxxSpec->allowsOp( - RXXOp::create(builder, loc, q0, q1, std::numbers::pi / 2.0) - .getOperation())); - - const auto ryySpec = NativeGateset::parse("u,ryy"); - ASSERT_TRUE(ryySpec); - EXPECT_TRUE(ryySpec->allowsOp( - RYYOp::create(builder, loc, q0, q1, 0.25).getOperation())); - - const auto rzxSpec = NativeGateset::parse("u,rzx"); - ASSERT_TRUE(rzxSpec); - EXPECT_TRUE(rzxSpec->allowsOp( - RZXOp::create(builder, loc, q0, q1, 0.25).getOperation())); - - const auto rzzSpec = NativeGateset::parse("u,rzz"); - ASSERT_TRUE(rzzSpec); - EXPECT_TRUE(rzzSpec->allowsOp( - RZZOp::create(builder, loc, q0, q1, 0.3).getOperation())); - - const auto iswapSpec = NativeGateset::parse("u,iswap"); - ASSERT_TRUE(iswapSpec); - auto iswap = iSWAPOp::create(builder, loc, q0, q1); - EXPECT_TRUE(iswapSpec->allowsOp(iswap.getOperation())); - EXPECT_FALSE(iswapSpec->allowsOp(cx.getOperation())); - - const auto ecrSpec = NativeGateset::parse("u,ecr"); - ASSERT_TRUE(ecrSpec); - auto ecr = ECROp::create(builder, loc, q0, q1); - EXPECT_TRUE(ecrSpec->allowsOp(ecr.getOperation())); - EXPECT_FALSE(ecrSpec->allowsOp(cx.getOperation())); - - const FloatType f64Float = builder.getF64Type(); - const Type f64Ty = Type::getFromOpaquePointer(f64Float.getAsOpaquePointer()); - const auto funcTyTheta = - builder.getFunctionType({f64Ty, qubitTy, qubitTy}, {qubitTy, qubitTy}); - OpBuilder::InsertionGuard guard(builder); - builder.clearInsertionPoint(); - auto funcTheta = func::FuncOp::create( - builder, loc, "allows_op_runtime_two_qubit_rotations", funcTyTheta); - auto* entryTheta = funcTheta.addEntryBlock(); - builder.setInsertionPointToStart(entryTheta); - Value runtimeTheta = entryTheta->getArgument(0); - Value runtimeQ0 = entryTheta->getArgument(1); - Value runtimeQ1 = entryTheta->getArgument(2); - auto runtimeRxx = - RXXOp::create(builder, loc, runtimeQ0, runtimeQ1, runtimeTheta); - EXPECT_TRUE(rxxSpec->allowsOp(runtimeRxx.getOperation())); - EXPECT_FALSE(spec->allowsOp(runtimeRxx.getOperation())); - auto runtimeRyy = RYYOp::create(builder, loc, runtimeRxx.getOutputQubit(0), - runtimeRxx.getOutputQubit(1), runtimeTheta); - EXPECT_TRUE(ryySpec->allowsOp(runtimeRyy.getOperation())); - auto runtimeRzx = RZXOp::create(builder, loc, runtimeRyy.getOutputQubit(0), - runtimeRyy.getOutputQubit(1), runtimeTheta); - EXPECT_TRUE(rzxSpec->allowsOp(runtimeRzx.getOperation())); - auto runtimeRzz = RZZOp::create(builder, loc, runtimeRzx.getOutputQubit(0), - runtimeRzx.getOutputQubit(1), runtimeTheta); - EXPECT_TRUE(rzzSpec->allowsOp(runtimeRzz.getOperation())); - func::ReturnOp::create( - builder, loc, - ValueRange{runtimeRzz.getOutputQubit(0), runtimeRzz.getOutputQubit(1)}); - auto module = ModuleOp::create(loc); - module.getBody()->push_back(funcTheta); - EXPECT_TRUE(succeeded(verify(module))); -} diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt index e70a874187..5da4e9db7d 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt @@ -6,22 +6,22 @@ # # Licensed under the MIT License -set(target_name mqt-core-mlir-unittest-fuse-two-qubit-unitary-runs) -add_executable(${target_name} test_fuse_two_qubit_unitary_runs.cpp) +set(target_name mqt-core-mlir-unittest-target-synthesis) +add_executable(${target_name} test_target_synthesis.cpp) target_link_libraries( ${target_name} PRIVATE MLIRParser GTest::gtest_main - MLIRQCPrograms + MQTCompilerTarget MLIRQCOProgramBuilder MLIRQCODDFunctionality MLIRQCOUtils - MLIRQCToQCO MLIRQCOTransforms MLIRPass MLIRFuncDialect MLIRArithDialect + MLIRSCFDialect MLIRIR MLIRSupport LLVMSupport) diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp deleted file mode 100644 index 99d10331fe..0000000000 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp +++ /dev/null @@ -1,758 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "dd/DDDefinitions.hpp" -#include "dd/Package.hpp" -#include "mlir/Conversion/QCToQCO/QCToQCO.h" -#include "mlir/Dialect/QCO/IR/QCODialect.h" -#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" -#include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/NativeGateset.h" -#include "mlir/Dialect/QCO/Transforms/Passes.h" -#include "mlir/Dialect/QCO/Utils/DDFunctionality.h" -#include "mlir/Dialect/QCO/Utils/Matrix.h" -#include "qc_programs.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace mlir; -using namespace mlir::qco; - -using ProgramFn = Value (*)(mlir::qc::QCProgramBuilder&); - -// --- Native-gateset membership check ------------------------------------- // - -/// Returns true when every single- or two-qubit operation in @p moduleOp is -/// native to the gateset parsed from @p nativeGates. Operations nested inside a -/// controlled shell are validated through the shell itself, and gates acting on -/// more than two qubits are out of scope for this pass and thus ignored. -static bool allOpsNative(OwningOpRef& moduleOp, - StringRef nativeGates) { - const auto spec = decomposition::NativeGateset::parse(nativeGates); - if (!spec) { - return false; - } - bool ok = true; - std::ignore = moduleOp->walk([&](UnitaryOpInterface op) { - Operation* raw = op.getOperation(); - if (isa_and_present(raw->getParentOp()) || op.getNumQubits() > 2) { - return WalkResult::advance(); - } - if (!spec->allowsOp(raw)) { - ok = false; - return WalkResult::interrupt(); - } - return WalkResult::advance(); - }); - return ok; -} - -static void expectNormalizedPhaseScopes(Operation* root) { - for (auto& region : root->getRegions()) { - for (auto& block : region) { - const auto phases = llvm::to_vector(block.getOps()); - ASSERT_LE(phases.size(), 1); - if (!phases.empty()) { - EXPECT_EQ(phases.front()->getNextNode(), block.getTerminator()); - } - for (auto& nested : block) { - expectNormalizedPhaseScopes(&nested); - } - } - } -} - -// --- DD-based equivalence ------------------------------------------------ // - -[[nodiscard]] static func::FuncOp mainFunc(ModuleOp module) { - return *module.getBody()->getOps().begin(); -} - -[[nodiscard]] static size_t countStaticQubits(func::FuncOp func) { - size_t numQubits = 0; - for (StaticOp staticOp : func.getOps()) { - numQubits = - std::max(numQubits, static_cast(staticOp.getIndex()) + 1); - } - return numQubits; -} - -[[nodiscard]] static DynamicMatrix matrixFromDD(const dd::CMat& matrix) { - const auto dim = static_cast(matrix.size()); - DynamicMatrix out(dim); - for (int64_t row = 0; row < dim; ++row) { - for (int64_t col = 0; col < dim; ++col) { - out(row, col) = - matrix[static_cast(row)][static_cast(col)]; - } - } - return out; -} - -// --- Expressive circuits -------------------------------------------------- // -// -// A handful of circuits, which are crossed with the gateset table below. - -/// A bare SWAP (three-entangler class), the canonical two-qubit decomposition. -static Value swapTwoQ(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.staticQubit(0); - const auto q1 = b.staticQubit(1); - b.swap(q0, q1); - return b.intConstant(0); -} - -/// Rich single-qubit variety on both wires, followed by a two-qubit entangler. -static Value broadOneQThenCz(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.staticQubit(0); - const auto q1 = b.staticQubit(1); - b.x(q0); - b.y(q1); - b.h(q0); - b.sx(q1); - b.rx(0.13, q0); - b.ry(-0.47, q1); - b.rz(0.29, q0); - b.cz(q0, q1); - return b.intConstant(0); -} - -/// Long single-qubit run on one wire, then an entangler (Euler-run fusion). -static Value hstycxTwoQ(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.staticQubit(0); - const auto q1 = b.staticQubit(1); - b.h(q0); - b.s(q0); - b.t(q0); - b.y(q0); - b.cx(q0, q1); - return b.intConstant(0); -} - -/// Zero-angle rotations that must canonicalize away before the entangler. -static Value zeroAngleThenCz(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.staticQubit(0); - const auto q1 = b.staticQubit(1); - b.rx(0.0, q0); - b.ry(0.0, q1); - b.rz(0.0, q0); - b.p(0.0, q1); - b.cz(q0, q1); - return b.intConstant(0); -} - -/// Single-qubit gates surrounding an entangler on both sides. -static Value hCxSq1(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.staticQubit(0); - const auto q1 = b.staticQubit(1); - b.h(q0); - b.cx(q0, q1); - b.s(q1); - return b.intConstant(0); -} - -/// Three-qubit program with chained entanglers on overlapping pairs. -static Value threeQGhz(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.staticQubit(0); - const auto q1 = b.staticQubit(1); - const auto q2 = b.staticQubit(2); - b.h(q0); - b.cx(q0, q1); - b.cx(q1, q2); - return b.intConstant(0); -} - -/// Single-qubit gates wrapped in an inverse modifier (no entangler). -static Value inverseTwoX(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.staticQubit(0); - b.inv(q0, [&](Value qubit) { - b.x(qubit); - b.x(qubit); - }); - return b.intConstant(0); -} - -/// A controlled two-gate body that must be synthesized as a two-qubit unitary. -static Value controlledXH(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.staticQubit(0); - const auto q1 = b.staticQubit(1); - b.ctrl(q0, q1, [&](Value target) { - b.x(target); - b.h(target); - }); - return b.intConstant(0); -} - -// --- Fusion-window circuits ---------------------------------------------- // -// -// These probe window geometry (where fusion starts/stops), so they run on a -// single fixed gateset rather than the full table. - -static Value fusionCxCx(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.staticQubit(0); - const auto q1 = b.staticQubit(1); - b.cx(q0, q1); - b.cx(q0, q1); - return b.intConstant(0); -} - -static Value fusionHCxInterleavedTCx(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.staticQubit(0); - const auto q1 = b.staticQubit(1); - b.h(q0); - b.cx(q0, q1); - b.t(q1); - b.s(q0); - b.cx(q0, q1); - return b.intConstant(0); -} - -static Value fusionThreeLineCx(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.staticQubit(0); - const auto q1 = b.staticQubit(1); - const auto q2 = b.staticQubit(2); - b.cx(q0, q1); - b.cx(q1, q2); - b.cx(q0, q1); - return b.intConstant(0); -} - -static Value fusionCxRSharedOtherPair(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.staticQubit(0); - const auto q1 = b.staticQubit(1); - const auto q2 = b.staticQubit(2); - b.cx(q0, q1); - b.rz(0.17, q1); - b.cx(q1, q2); - return b.intConstant(0); -} - -static Value fusionCxBarrierCx(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.staticQubit(0); - const auto q1 = b.staticQubit(1); - b.cx(q0, q1); - b.barrier({q0, q1}); - b.cx(q0, q1); - return b.intConstant(0); -} - -/// A single-wire barrier between two entanglers: a non-walkable single-qubit -/// shell terminates the run scan on wire A (and breaks the run-start chain of -/// the second entangler), so neither entangler fuses. -static Value fusionCxSingleWireBarrierCx(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.staticQubit(0); - const auto q1 = b.staticQubit(1); - b.cx(q0, q1); - b.barrier({q0}); - b.cx(q0, q1); - return b.intConstant(0); -} - -/// An entangler followed by a three-qubit gate sharing both run wires: the run -/// scan must stop at the wider gate (it is neither a single- nor a two-qubit -/// run member) and leave it untouched, since gates on more than two qubits are -/// out of scope for this pass rather than a failure. -static Value fusionCxThenMultiControlledX(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.staticQubit(0); - const auto q1 = b.staticQubit(1); - const auto q2 = b.staticQubit(2); - b.cx(q0, q1); - b.mcx({q0, q1}, q2); - return b.intConstant(0); -} - -static Value fusionSwapCxPattern(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.staticQubit(0); - const auto q1 = b.staticQubit(1); - b.cx(q0, q1); - b.cx(q1, q0); - b.cx(q0, q1); - return b.intConstant(0); -} - -static Value fusionOffMenuGateInWindow(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.staticQubit(0); - const auto q1 = b.staticQubit(1); - b.cx(q0, q1); - b.h(q0); - b.cx(q0, q1); - return b.intConstant(0); -} - -static Value fusionDualWireOneQBetweenCx(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.staticQubit(0); - const auto q1 = b.staticQubit(1); - b.cx(q0, q1); - b.rz(0.11, q0); - b.ry(0.22, q1); - b.cx(q0, q1); - return b.intConstant(0); -} - -static Value determinismSwap(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.swap(q0, q1); - b.dealloc(q0); - b.dealloc(q1); - return b.intConstant(0); -} - -namespace { - -/// A named circuit builder, used as a test parameter. -struct NamedProgram { - const char* name; - ProgramFn program; -}; - -/// Native gatesets spanning every supported single-qubit basis and all -/// entangler families (RXX/RYY/RZX/RZZ/iSWAP/CZ/CX/ECR, plus multi-entangler -/// menus). Because the pass re-synthesizes each two-qubit window into the -/// target basis, every circuit is valid input for every gateset. -constexpr std::array GATESETS = { - "u,rxx", - "u,ryy", - "u,rzx", - "u,rzz", - "x,sx,rz,rzz", - // iSWAP - "u,iswap", - "x,sx,rz,iswap", - // CZ - "r,cz", // R - "ry,rz,cz", // ZYZ - "x,sx,rz,cz", // ZSXX - "u,cz", // U - // CX - "x,sx,rz,cx", // ZSXX - "u,cx", // U - "rx,rz,cx", // XZX - "rx,ry,cx", // XYX - // ECR - "u,ecr", - "x,sx,rz,ecr", - // Multiple entanglers (listed in preference order; rxx / iswap / cz win) - "u,rxx,ryy,rzx,rzz,iswap,cz,cx,ecr", - "u,iswap,cz,cx,ecr", - "u,cz,cx", -}; - -/// Gateset used for the fusion-window suite, which asserts on structure rather -/// than on native-basis coverage. -constexpr StringRef FUSION_GATESET = "u,cx"; - -/// Structural expectations for a fusion-window circuit under @ref -/// FUSION_GATESET. -struct FusionCase { - const char* name; - ProgramFn program; - std::optional exactCtrlCount; - std::optional minCtrlCount; - bool checkTwoQUnitary; -}; - -class FuseTwoQubitUnitaryRunsPassTest : public testing::Test { -protected: - void SetUp() override { - DialectRegistry registry; - registry.insert(); - context = std::make_unique(); - context->appendDialectRegistry(registry); - context->loadAllAvailableDialects(); - } - - static void runFusePipeline(OwningOpRef& moduleOp, - StringRef nativeGates) { - PassManager pm(moduleOp->getContext()); - pm.addPass(createQCToQCO()); - pm.addPass(createFuseTwoQubitUnitaryRuns(FuseTwoQubitUnitaryRunsOptions{ - .nativeGates = nativeGates.str(), - })); - ASSERT_TRUE(succeeded(pm.run(*moduleOp))); - } - - static void runQcToQco(OwningOpRef& moduleOp) { - PassManager pm(moduleOp->getContext()); - pm.addPass(createQCToQCO()); - ASSERT_TRUE(succeeded(pm.run(*moduleOp))); - } - - static void runTwoQFuse(OwningOpRef& moduleOp, - StringRef nativeGates) { - PassManager pm(moduleOp->getContext()); - pm.addPass(createFuseTwoQubitUnitaryRuns(FuseTwoQubitUnitaryRunsOptions{ - .nativeGates = nativeGates.str(), - })); - ASSERT_TRUE(succeeded(pm.run(*moduleOp))); - } - - static void expectQcoModulesEquivalent(const OwningOpRef& lhs, - const OwningOpRef& rhs) { - const auto lhsFunc = mainFunc(*lhs); - const auto rhsFunc = mainFunc(*rhs); - const auto numQubits = countStaticQubits(lhsFunc); - ASSERT_EQ(numQubits, countStaticQubits(rhsFunc)); - ASSERT_GT(numQubits, 0U); - - auto dd = std::make_unique(numQubits); - const auto lhsUnitary = buildFunctionality(lhsFunc, *dd); - ASSERT_TRUE(succeeded(lhsUnitary)); - const auto rhsUnitary = buildFunctionality(rhsFunc, *dd); - ASSERT_TRUE(succeeded(rhsUnitary)); - - const auto lhsMatrix = matrixFromDD(lhsUnitary->getMatrix(numQubits)); - const auto rhsMatrix = matrixFromDD(rhsUnitary->getMatrix(numQubits)); - dd->decRef(*lhsUnitary); - dd->decRef(*rhsUnitary); - EXPECT_TRUE(lhsMatrix.isApprox(rhsMatrix)); - } - - void expectEquivalentAndNativeAfterSynthesis(ProgramFn program, - StringRef nativeGates) { - auto expected = mlir::qc::QCProgramBuilder::build(context.get(), program); - runQcToQco(expected); - auto synthesized = - mlir::qc::QCProgramBuilder::build(context.get(), program); - runFusePipeline(synthesized, nativeGates); - EXPECT_TRUE(allOpsNative(synthesized, nativeGates)); - expectNormalizedPhaseScopes(synthesized->getOperation()); - expectQcoModulesEquivalent(expected, synthesized); - } - - void expectSynthesisFailure(ProgramFn program, StringRef nativeGates) { - auto moduleOp = mlir::qc::QCProgramBuilder::build(context.get(), program); - PassManager pm(moduleOp->getContext()); - pm.addPass(createQCToQCO()); - pm.addPass(createFuseTwoQubitUnitaryRuns(FuseTwoQubitUnitaryRunsOptions{ - .nativeGates = nativeGates.str(), - })); - EXPECT_TRUE(failed(pm.run(*moduleOp))); - } - - void expectSynthesisFailure( - SmallVector (*program)(mlir::qc::QCProgramBuilder&), - StringRef nativeGates) { - auto moduleOp = mlir::qc::QCProgramBuilder::build(context.get(), program); - PassManager pm(moduleOp->getContext()); - pm.addPass(createQCToQCO()); - pm.addPass(createFuseTwoQubitUnitaryRuns(FuseTwoQubitUnitaryRunsOptions{ - .nativeGates = nativeGates.str(), - })); - EXPECT_TRUE(failed(pm.run(*moduleOp))); - } - - void expectTwoQFusePreservesUnitary(ProgramFn program, - StringRef nativeGates) { - auto expected = mlir::qc::QCProgramBuilder::build(context.get(), program); - ASSERT_TRUE(expected); - runQcToQco(expected); - auto fused = mlir::qc::QCProgramBuilder::build(context.get(), program); - ASSERT_TRUE(fused); - runQcToQco(fused); - runTwoQFuse(fused, nativeGates); - ASSERT_TRUE(succeeded(verify(*fused))); - expectQcoModulesEquivalent(expected, fused); - } - - static size_t countCtrlOps(const OwningOpRef& moduleOp) { - size_t count = 0; - moduleOp.get()->walk([&](CtrlOp) { ++count; }); - return count; - } - - /// Counts unitaries acting on more than two qubits, i.e. gates left untouched - /// for the dedicated multi-controlled synthesis pass. - static size_t countWideGates(const OwningOpRef& moduleOp) { - size_t count = 0; - moduleOp.get()->walk([&](UnitaryOpInterface op) { - if (op.getNumQubits() > 2) { - ++count; - } - }); - return count; - } - - std::unique_ptr context; -}; - -TEST(FuseTwoQubitUnitaryRunsPassContract, RegistersArithDialectDependency) { - DialectRegistry registry; - const auto pass = createFuseTwoQubitUnitaryRuns(); - pass->getDependentDialects(registry); - - EXPECT_TRUE( - registry.getDialectAllocator(arith::ArithDialect::getDialectNamespace())); -} - -using SynthesisParam = std::tuple; - -class FuseTwoQubitSynthesisTest - : public FuseTwoQubitUnitaryRunsPassTest, - public testing::WithParamInterface {}; - -class FuseTwoQubitFusionTest : public FuseTwoQubitUnitaryRunsPassTest, - public testing::WithParamInterface { -}; - -} // namespace - -// --- Synthesis: every expressive circuit against every gateset ----------- // - -TEST_P(FuseTwoQubitSynthesisTest, IsNativeAndEquivalent) { - const auto& [circuit, gateset] = GetParam(); - expectEquivalentAndNativeAfterSynthesis(circuit.program, gateset); -} - -INSTANTIATE_TEST_SUITE_P( - Circuits, FuseTwoQubitSynthesisTest, - testing::Combine( - testing::Values(NamedProgram{"Swap", swapTwoQ}, - NamedProgram{"BroadOneQThenCz", broadOneQThenCz}, - NamedProgram{"HstyThenCx", hstycxTwoQ}, - NamedProgram{"ZeroAngleThenCz", zeroAngleThenCz}, - NamedProgram{"SurroundedCx", hCxSq1}, - NamedProgram{"ThreeQubitGhz", threeQGhz}, - NamedProgram{"InverseBody", inverseTwoX}, - NamedProgram{"ControlledBody", controlledXH}, - NamedProgram{"SingleWireBarrier", - fusionCxSingleWireBarrierCx}), - testing::ValuesIn(GATESETS)), - [](const testing::TestParamInfo& info) { - std::string gateset = std::get<1>(info.param); - std::ranges::replace(gateset, ',', '_'); - return std::string(std::get<0>(info.param).name) + "__" + gateset; - }); - -// --- Fusion windows: structural behavior on a fixed gateset -------------- // - -TEST_P(FuseTwoQubitFusionTest, WindowFusionBehavior) { - const FusionCase& c = GetParam(); - if (c.checkTwoQUnitary) { - expectTwoQFusePreservesUnitary(c.program, FUSION_GATESET); - } - auto module = mlir::qc::QCProgramBuilder::build(context.get(), c.program); - ASSERT_TRUE(module); - runQcToQco(module); - runTwoQFuse(module, FUSION_GATESET); - if (c.exactCtrlCount) { - EXPECT_EQ(countCtrlOps(module), *c.exactCtrlCount); - } - if (c.minCtrlCount) { - EXPECT_GE(countCtrlOps(module), *c.minCtrlCount); - } -} - -INSTANTIATE_TEST_SUITE_P( - Windows, FuseTwoQubitFusionTest, - testing::Values( - FusionCase{"AdjacentCxCancel", fusionCxCx, 0, std::nullopt, true}, - FusionCase{"InterleavedOneQ", fusionHCxInterleavedTCx, std::nullopt, - std::nullopt, true}, - FusionCase{"DifferentPairBoundary", fusionThreeLineCx, std::nullopt, 1, - false}, - FusionCase{"SharedWireOneQ", fusionCxRSharedOtherPair, std::nullopt, 2, - false}, - FusionCase{"BarrierBoundary", fusionCxBarrierCx, 2, std::nullopt, - false}, - FusionCase{"SingleWireBarrierBoundary", fusionCxSingleWireBarrierCx, 2, - std::nullopt, true}, - FusionCase{"SwappedWireOrder", fusionSwapCxPattern, std::nullopt, - std::nullopt, true}, - FusionCase{"OffMenuGateInWindow", fusionOffMenuGateInWindow, - std::nullopt, std::nullopt, true}, - FusionCase{"DualWireOneQBetweenCx", fusionDualWireOneQBetweenCx, - std::nullopt, std::nullopt, true}), - [](const testing::TestParamInfo& info) { - return info.param.name; - }); - -// --- Pass edge cases ----------------------------------------------------- // - -TEST_F(FuseTwoQubitUnitaryRunsPassTest, EmptyNativeGatesSkipsPass) { - auto module = mlir::qc::QCProgramBuilder::build(context.get(), fusionCxCx); - ASSERT_TRUE(module); - runQcToQco(module); - std::string before; - llvm::raw_string_ostream osBefore(before); - module->print(osBefore); - - PassManager pm(module->getContext()); - pm.addPass(createFuseTwoQubitUnitaryRuns(FuseTwoQubitUnitaryRunsOptions{ - .nativeGates = "", - })); - ASSERT_TRUE(succeeded(pm.run(*module))); - - std::string after; - llvm::raw_string_ostream osAfter(after); - module->print(osAfter); - EXPECT_EQ(before, after); -} - -TEST_F(FuseTwoQubitUnitaryRunsPassTest, FailsForInvalidNativeGateMenu) { - expectSynthesisFailure(mlir::qc::h, "not-a-gate"); -} - -TEST_F(FuseTwoQubitUnitaryRunsPassTest, - FailsForNativeGateMenuWithoutSingleQEmitter) { - expectSynthesisFailure(mlir::qc::singleControlledX, "cx,cz"); -} - -TEST_F(FuseTwoQubitUnitaryRunsPassTest, - PreservesRuntimeParameterizedNativeTwoQubitRotations) { - auto module = parseSourceString(R"mlir( - module { - func.func @main(%theta: f64) -> (!qco.qubit, !qco.qubit) { - %q0 = qco.static 0 : !qco.qubit - %q1 = qco.static 1 : !qco.qubit - %q2, %q3 = qco.rxx(%theta) %q0, %q1 : !qco.qubit, !qco.qubit -> !qco.qubit, !qco.qubit - %q4, %q5 = qco.ryy(%theta) %q2, %q3 : !qco.qubit, !qco.qubit -> !qco.qubit, !qco.qubit - %q6, %q7 = qco.rzx(%theta) %q4, %q5 : !qco.qubit, !qco.qubit -> !qco.qubit, !qco.qubit - %q8, %q9 = qco.rzz(%theta) %q6, %q7 : !qco.qubit, !qco.qubit -> !qco.qubit, !qco.qubit - return %q8, %q9 : !qco.qubit, !qco.qubit - } - } - )mlir", - context.get()); - ASSERT_TRUE(module); - - std::string before; - llvm::raw_string_ostream osBefore(before); - module->print(osBefore); - - PassManager pm(module->getContext()); - pm.addPass(createFuseTwoQubitUnitaryRuns(FuseTwoQubitUnitaryRunsOptions{ - .nativeGates = "u,rxx,ryy,rzx,rzz", - })); - ASSERT_TRUE(succeeded(pm.run(*module))); - - std::string after; - llvm::raw_string_ostream osAfter(after); - module->print(osAfter); - EXPECT_EQ(before, after); -} - -TEST_F(FuseTwoQubitUnitaryRunsPassTest, - FailsLocallyForRuntimeParameterizedResidual) { - // A runtime-angle `qco.rz` cannot be Euler-fused under `u,cx` and is not on - // the menu, so the convergence check must fail on that op (not the module). - auto module = parseSourceString(R"mlir( - module { - func.func @main(%theta: f64) -> !qco.qubit { - %q = qco.static 0 : !qco.qubit - %q1 = qco.rz(%theta) %q : !qco.qubit -> !qco.qubit - return %q1 : !qco.qubit - } - } - )mlir", - context.get()); - ASSERT_TRUE(module); - - Location rzLoc = UnknownLoc::get(context.get()); - module->walk([&](RZOp op) { - rzLoc = op.getLoc(); - return WalkResult::interrupt(); - }); - ASSERT_FALSE(isa(rzLoc)); - - bool sawLocalizedDiagnostic = false; - std::string diagnostics; - ScopedDiagnosticHandler handler(context.get(), [&](Diagnostic& diagnostic) { - diagnostics += diagnostic.str(); - if (StringRef(diagnostic.str()) - .contains("native gate synthesis: operation remains outside the " - "native gateset") && - diagnostic.getLocation() == rzLoc) { - sawLocalizedDiagnostic = true; - } - return success(); - }); - - PassManager pm(module->getContext()); - pm.addPass(createFuseTwoQubitUnitaryRuns(FuseTwoQubitUnitaryRunsOptions{ - .nativeGates = "u,cx", - })); - EXPECT_TRUE(failed(pm.run(*module))); - EXPECT_TRUE(sawLocalizedDiagnostic) << diagnostics; -} - -TEST_F(FuseTwoQubitUnitaryRunsPassTest, LeavesMultiControlledGateUntouched) { - // A multi-controlled gate is out of scope for this pass; it is left untouched - // (for a dedicated multi-controlled synthesis pass) and does not fail the - // run. - auto module = mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::multipleControlledX); - ASSERT_TRUE(module); - runFusePipeline(module, "x,sx,rz,cx"); - EXPECT_TRUE(allOpsNative(module, "x,sx,rz,cx")); - EXPECT_EQ(countWideGates(module), 1U); -} - -TEST_F(FuseTwoQubitUnitaryRunsPassTest, - LowersTwoQubitRunButLeavesWiderGateBoundary) { - // The `cx` is off-menu for this cz-family gateset, so it is Weyl-synthesized - // even though the run scan stops at the three-qubit boundary; the wider gate - // is left untouched, so the pass succeeds with the two-qubit run lowered. - auto module = mlir::qc::QCProgramBuilder::build(context.get(), - fusionCxThenMultiControlledX); - ASSERT_TRUE(module); - runFusePipeline(module, "u,cz"); - EXPECT_TRUE(allOpsNative(module, "u,cz")); - EXPECT_EQ(countWideGates(module), 1U); -} - -TEST_F(FuseTwoQubitUnitaryRunsPassTest, - CandidateSelectionIsDeterministicAcrossRuns) { - auto buildFn = [&] { - return mlir::qc::QCProgramBuilder::build(context.get(), determinismSwap); - }; - auto firstModule = buildFn(); - runFusePipeline(firstModule, "u,cx"); - auto secondModule = buildFn(); - runFusePipeline(secondModule, "u,cx"); - - std::string first; - std::string second; - llvm::raw_string_ostream osFirst(first); - llvm::raw_string_ostream osSecond(second); - firstModule->print(osFirst); - secondModule->print(osSecond); - EXPECT_EQ(first, second); -} diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp new file mode 100644 index 0000000000..9bfd871ba9 --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp @@ -0,0 +1,565 @@ +/* + * 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 "dd/DDDefinitions.hpp" +#include "dd/Package.hpp" +#include "mlir/Compiler/Target.h" +#include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h" +#include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Passes.h" +#include "mlir/Dialect/QCO/Utils/DDFunctionality.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" +#include "mlir/Dialect/QTensor/IR/QTensorDialect.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mqt::test::qco { + +using Target = mlir::CompilerTarget; +using Operation = Target::Operation; +using OperationLocus = Target::OperationLocus; +using Site = Target::Site; +using mlir::ModuleOp; +using mlir::OwningOpRef; +using mlir::Value; +using mlir::qco::CtrlOp; +using mlir::qco::QCOProgramBuilder; +using mlir::qco::RXXOp; +using mlir::qco::SWAPOp; + +[[nodiscard]] static mlir::func::FuncOp mainFunction(ModuleOp module) { + return *module.getBody()->getOps().begin(); +} + +[[nodiscard]] static size_t countStaticQubits(mlir::func::FuncOp function) { + size_t numQubits = 0; + for (auto staticOp : function.getOps()) { + numQubits = + std::max(numQubits, static_cast(staticOp.getIndex()) + 1); + } + return numQubits; +} + +[[nodiscard]] static mlir::qco::DynamicMatrix +matrixFromDD(const dd::CMat& matrix) { + const auto dimension = static_cast(matrix.size()); + mlir::qco::DynamicMatrix result(dimension); + for (int64_t row = 0; row < dimension; ++row) { + for (int64_t column = 0; column < dimension; ++column) { + result(row, column) = + matrix[static_cast(row)][static_cast(column)]; + } + } + return result; +} + +static void expectEquivalent(const OwningOpRef& expected, + const OwningOpRef& actual) { + const auto expectedFunction = mainFunction(*expected); + const auto actualFunction = mainFunction(*actual); + const auto numQubits = countStaticQubits(expectedFunction); + ASSERT_EQ(numQubits, countStaticQubits(actualFunction)); + ASSERT_GT(numQubits, 0U); + + auto package = std::make_unique(numQubits); + const auto expectedUnitary = + mlir::qco::buildFunctionality(expectedFunction, *package); + ASSERT_TRUE(mlir::succeeded(expectedUnitary)); + const auto actualUnitary = + mlir::qco::buildFunctionality(actualFunction, *package); + ASSERT_TRUE(mlir::succeeded(actualUnitary)); + + const auto expectedMatrix = + matrixFromDD(expectedUnitary->getMatrix(numQubits)); + const auto actualMatrix = matrixFromDD(actualUnitary->getMatrix(numQubits)); + package->decRef(*expectedUnitary); + package->decRef(*actualUnitary); + EXPECT_TRUE(expectedMatrix.isApprox(actualMatrix)); +} + +template [[nodiscard]] static size_t countOps(ModuleOp module) { + size_t count = 0; + module.walk([&](Op) { ++count; }); + return count; +} + +[[nodiscard]] static std::string printModule(ModuleOp module) { + std::string result; + llvm::raw_string_ostream stream(result); + module.print(stream); + return result; +} + +[[nodiscard]] static mlir::LogicalResult +runPass(ModuleOp module, std::unique_ptr pass) { + mlir::PassManager manager(module.getContext()); + manager.addPass(std::move(pass)); + return manager.run(module); +} + +[[nodiscard]] static Target makeUCxTarget(std::vector sites = std::vector{ + Site{0}, Site{1}}) { + return Target{std::move(sites), std::nullopt, + std::vector{Operation{"u", 1, 3}, Operation{"cx", 2, 0}}}; +} + +namespace { + +class TargetSynthesisTest : public testing::Test { +protected: + void SetUp() override { + mlir::DialectRegistry registry; + registry.insert(); + context = std::make_unique(); + context->appendDialectRegistry(registry); + context->loadAllAvailableDialects(); + } + + [[nodiscard]] OwningOpRef + build(const mlir::function_ref& builder) const { + return QCOProgramBuilder::build(context.get(), builder); + } + + [[nodiscard]] std::string + expectFailure(ModuleOp module, std::unique_ptr pass) const { + std::string diagnostics; + mlir::ScopedDiagnosticHandler handler(context.get(), + [&](mlir::Diagnostic& diagnostic) { + diagnostics += diagnostic.str(); + diagnostics += '\n'; + return mlir::success(); + }); + EXPECT_TRUE(mlir::failed(runPass(module, std::move(pass)))); + return diagnostics; + } + + std::unique_ptr context; +}; + +} // namespace + +TEST(TargetSynthesisPassContract, FactoriesAreIndependentlyConstructible) { + const Target target{2}; + auto optimization = mlir::qco::createOptimizeTwoQubitUnitaryRuns(); + auto synthesis = mlir::qco::createTargetNativeSynthesis(target); + auto conformance = mlir::qco::createVerifyTargetConformance(target); + + ASSERT_NE(optimization, nullptr); + ASSERT_NE(synthesis, nullptr); + ASSERT_NE(conformance, nullptr); + + mlir::DialectRegistry optimizationDialects; + optimization->getDependentDialects(optimizationDialects); + EXPECT_TRUE(optimizationDialects.getDialectAllocator( + mlir::arith::ArithDialect::getDialectNamespace())); + + mlir::DialectRegistry synthesisDialects; + synthesis->getDependentDialects(synthesisDialects); + EXPECT_TRUE(synthesisDialects.getDialectAllocator( + mlir::arith::ArithDialect::getDialectNamespace())); +} + +TEST_F(TargetSynthesisTest, PreRoutingOptimizationRequiresStrictImprovement) { + const auto adjacentCx = [](QCOProgramBuilder& builder) { + auto q0 = builder.staticQubit(0); + auto q1 = builder.staticQubit(1); + std::tie(q0, q1) = builder.cx(q0, q1); + std::tie(q0, q1) = builder.cx(q0, q1); + return builder.intConstant(0); + }; + auto expected = build(adjacentCx); + auto optimized = build(adjacentCx); + ASSERT_TRUE(mlir::succeeded( + runPass(*optimized, mlir::qco::createOptimizeTwoQubitUnitaryRuns()))); + EXPECT_EQ(countOps(*optimized), 0U); + expectEquivalent(expected, optimized); + + auto nonImproving = build([](QCOProgramBuilder& builder) { + auto q0 = builder.staticQubit(0); + auto q1 = builder.staticQubit(1); + std::tie(q0, q1) = builder.cx(q0, q1); + std::tie(q1, q0) = builder.cx(q1, q0); + std::tie(q0, q1) = builder.cx(q0, q1); + return builder.intConstant(0); + }); + ASSERT_TRUE(mlir::succeeded( + runPass(*nonImproving, mlir::qco::createOptimizeTwoQubitUnitaryRuns()))); + EXPECT_EQ(countOps(*nonImproving), 3U); +} + +TEST_F(TargetSynthesisTest, PreRoutingOptimizationLeavesIndividualOpsAlone) { + auto module = build([](QCOProgramBuilder& builder) { + auto q0 = builder.staticQubit(0); + auto q1 = builder.staticQubit(1); + std::tie(q0, q1) = builder.swap(q0, q1); + return builder.intConstant(0); + }); + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createOptimizeTwoQubitUnitaryRuns()))); + EXPECT_EQ(countOps(*module), 1U); + EXPECT_EQ(countOps(*module), 0U); +} + +TEST_F(TargetSynthesisTest, + PreRoutingOptimizationLeavesRuntimeParameterizedRunsAlone) { + auto module = mlir::parseSourceString(R"mlir( + module { + func.func @main(%theta: f64) -> (!qco.qubit, !qco.qubit) { + %q0 = qco.static 0 : !qco.qubit + %q1 = qco.static 1 : !qco.qubit + %q2, %q3 = qco.rxx(%theta) %q0, %q1 : !qco.qubit, !qco.qubit -> !qco.qubit, !qco.qubit + %q4, %q5 = qco.rxx(%theta) %q2, %q3 : !qco.qubit, !qco.qubit -> !qco.qubit, !qco.qubit + return %q4, %q5 : !qco.qubit, !qco.qubit + } + } + )mlir", + context.get()); + ASSERT_TRUE(module); + const auto before = printModule(*module); + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createOptimizeTwoQubitUnitaryRuns()))); + EXPECT_EQ(countOps(*module), 2U); + EXPECT_EQ(printModule(*module), before); +} + +TEST_F(TargetSynthesisTest, TargetNativeSynthesisRemovesOrdinarySwap) { + const auto swap = [](QCOProgramBuilder& builder) { + auto q0 = builder.staticQubit(0); + auto q1 = builder.staticQubit(1); + std::tie(q0, q1) = builder.swap(q0, q1); + return builder.intConstant(0); + }; + auto expected = build(swap); + auto synthesized = build(swap); + const auto target = makeUCxTarget(); + + ASSERT_TRUE(mlir::succeeded( + runPass(*synthesized, mlir::qco::createTargetNativeSynthesis(target)))); + EXPECT_EQ(countOps(*synthesized), 0U); + EXPECT_GT(countOps(*synthesized), 0U); + ASSERT_TRUE(mlir::succeeded( + runPass(*synthesized, mlir::qco::createVerifyTargetConformance(target)))); + ASSERT_TRUE(mlir::succeeded(mlir::verify(*synthesized))); + expectEquivalent(expected, synthesized); +} + +TEST_F(TargetSynthesisTest, TargetNativeSynthesisPreservesNativeSwap) { + auto module = build([](QCOProgramBuilder& builder) { + auto q0 = builder.staticQubit(0); + auto q1 = builder.staticQubit(1); + std::tie(q0, q1) = builder.swap(q0, q1); + return builder.intConstant(0); + }); + const Target swapTarget{2, std::nullopt, + std::vector{Operation{"swap", 2, 0}}}; + ASSERT_FALSE(swapTarget.synthesisBasis()); + const auto before = printModule(*module); + + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createTargetNativeSynthesis(swapTarget)))); + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createVerifyTargetConformance(swapTarget)))); + EXPECT_EQ(countOps(*module), 1U); + EXPECT_EQ(printModule(*module), before); +} + +TEST_F(TargetSynthesisTest, + TargetNativeSynthesisUsesSupportedSymmetricOrientation) { + const auto swap = [](QCOProgramBuilder& builder) { + auto q0 = builder.staticQubit(0); + auto q1 = builder.staticQubit(1); + std::tie(q0, q1) = builder.swap(q0, q1); + return builder.intConstant(0); + }; + auto expected = build(swap); + auto synthesized = build(swap); + const Target reverseOnly{ + std::vector{Site{0}, Site{1}}, std::nullopt, + std::vector{Operation{"u", 1, 3}, + Operation{"cz", 2, 0, std::vector{OperationLocus{{1, 0}}}}}}; + ASSERT_TRUE(reverseOnly.synthesisBasis()); + ASSERT_EQ(reverseOnly.synthesisBasis()->entangler, Target::GateKind::CZ); + + ASSERT_TRUE(mlir::succeeded(runPass( + *synthesized, mlir::qco::createTargetNativeSynthesis(reverseOnly)))); + EXPECT_EQ(countOps(*synthesized), 0U); + EXPECT_GT(countOps(*synthesized), 0U); + ASSERT_TRUE(mlir::succeeded(runPass( + *synthesized, mlir::qco::createVerifyTargetConformance(reverseOnly)))); + ASSERT_TRUE(mlir::succeeded(mlir::verify(*synthesized))); + expectEquivalent(expected, synthesized); +} + +TEST_F(TargetSynthesisTest, AbsentOperationSetTreatsEveryOperationAsNative) { + auto module = build([](QCOProgramBuilder& builder) { + auto qubit = builder.staticQubit(0); + qubit = builder.h(qubit); + return builder.intConstant(0); + }); + const Target permissive{1}; + const auto before = printModule(*module); + + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createTargetNativeSynthesis(permissive)))); + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createVerifyTargetConformance(permissive)))); + EXPECT_EQ(printModule(*module), before); +} + +TEST_F(TargetSynthesisTest, NativePowShellHidesItsImplementationBody) { + auto module = build([](QCOProgramBuilder& builder) { + auto qubit = builder.staticQubit(0); + qubit = builder.pow(2.0, qubit, + [&](Value argument) { return builder.h(argument); }); + return builder.intConstant(0); + }); + const Target powOnly{1, std::nullopt, std::vector{Operation{"pow", 1, 1}}}; + ASSERT_FALSE(powOnly.synthesisBasis()); + const auto before = printModule(*module); + + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createTargetNativeSynthesis(powOnly)))); + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createVerifyTargetConformance(powOnly)))); + EXPECT_EQ(printModule(*module), before); +} + +TEST_F(TargetSynthesisTest, MissingBasisIsDiagnosedOnlyWhenLoweringIsNeeded) { + const Target hOnly{1, std::nullopt, std::vector{Operation{"h", 1, 0}}}; + ASSERT_FALSE(hOnly.synthesisBasis()); + + auto supported = build([](QCOProgramBuilder& builder) { + auto qubit = builder.staticQubit(0); + qubit = builder.h(qubit); + return builder.intConstant(0); + }); + const auto before = printModule(*supported); + ASSERT_TRUE(mlir::succeeded( + runPass(*supported, mlir::qco::createTargetNativeSynthesis(hOnly)))); + ASSERT_TRUE(mlir::succeeded( + runPass(*supported, mlir::qco::createVerifyTargetConformance(hOnly)))); + EXPECT_EQ(printModule(*supported), before); + + auto unsupported = build([](QCOProgramBuilder& builder) { + auto qubit = builder.staticQubit(0); + qubit = builder.x(qubit); + return builder.intConstant(0); + }); + const auto diagnostics = expectFailure( + *unsupported, mlir::qco::createTargetNativeSynthesis(hOnly)); + EXPECT_NE(diagnostics.find("target-native synthesis cannot lower operation " + "'qco.x' at ordered provider locus [0]"), + std::string::npos) + << diagnostics; + EXPECT_NE(diagnostics.find("no globally usable synthesis basis"), + std::string::npos) + << diagnostics; +} + +TEST_F(TargetSynthesisTest, SupportedRuntimeParameterizedGateStaysUntouched) { + auto module = mlir::parseSourceString(R"mlir( + module { + func.func @main(%theta: f64) -> (!qco.qubit, !qco.qubit) { + %q0 = qco.static 0 : !qco.qubit + %q1 = qco.static 1 : !qco.qubit + %q2, %q3 = qco.rxx(%theta) %q0, %q1 : !qco.qubit, !qco.qubit -> !qco.qubit, !qco.qubit + return %q2, %q3 : !qco.qubit, !qco.qubit + } + } + )mlir", + context.get()); + ASSERT_TRUE(module); + const Target target{ + 2, std::nullopt, + std::vector{Operation{"u", 1, 3}, Operation{"rxx", 2, 1}}}; + const auto before = printModule(*module); + + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createTargetNativeSynthesis(target)))); + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createVerifyTargetConformance(target)))); + EXPECT_EQ(printModule(*module), before); +} + +TEST_F(TargetSynthesisTest, + UnsupportedRuntimeParameterizedGateHasLocalDiagnostic) { + auto module = mlir::parseSourceString(R"mlir( + module { + func.func @main(%theta: f64) -> (!qco.qubit, !qco.qubit) { + %q0 = qco.static 0 : !qco.qubit + %q1 = qco.static 1 : !qco.qubit + %q2, %q3 = qco.rxx(%theta) %q0, %q1 : !qco.qubit, !qco.qubit -> !qco.qubit, !qco.qubit + return %q2, %q3 : !qco.qubit, !qco.qubit + } + } + )mlir", + context.get()); + ASSERT_TRUE(module); + const auto diagnostics = expectFailure( + *module, mlir::qco::createTargetNativeSynthesis(makeUCxTarget())); + EXPECT_NE(diagnostics.find("target-native synthesis cannot lower operation " + "'qco.rxx' at ordered provider locus [0, 1]"), + std::string::npos) + << diagnostics; + EXPECT_NE(diagnostics.find("unitary matrix is not available at compile time"), + std::string::npos) + << diagnostics; +} + +TEST_F(TargetSynthesisTest, ConformanceUsesProviderIdsAndOrderedLoci) { + const Target directional{ + std::vector{Site{10}, Site{20}}, std::nullopt, + std::vector{ + Operation{"cx", 2, 0, std::vector{OperationLocus{{10, 20}}}}}}; + ASSERT_FALSE(directional.synthesisBasis()); + + auto supported = build([](QCOProgramBuilder& builder) { + auto q10 = builder.staticQubit(10); + auto q20 = builder.staticQubit(20); + std::tie(q10, q20) = builder.cx(q10, q20); + return builder.intConstant(0); + }); + ASSERT_TRUE(mlir::succeeded(runPass( + *supported, mlir::qco::createTargetNativeSynthesis(directional)))); + ASSERT_TRUE(mlir::succeeded(runPass( + *supported, mlir::qco::createVerifyTargetConformance(directional)))); + + auto reversed = build([](QCOProgramBuilder& builder) { + auto q10 = builder.staticQubit(10); + auto q20 = builder.staticQubit(20); + std::tie(q20, q10) = builder.cx(q20, q10); + return builder.intConstant(0); + }); + const auto diagnostics = expectFailure( + *reversed, mlir::qco::createVerifyTargetConformance(directional)); + EXPECT_NE(diagnostics.find("'qco.ctrl' with arity 2 and 0 parameter(s)"), + std::string::npos) + << diagnostics; + EXPECT_NE(diagnostics.find("[20, 10]"), std::string::npos) << diagnostics; +} + +TEST_F(TargetSynthesisTest, ConformanceChecksTypeArityParametersAndSite) { + const auto expectUnsupported = [&](const Target& target, + OwningOpRef module, + const std::string& operation, + const std::string& details, + const std::string& locus) { + const auto diagnostics = expectFailure( + *module, mlir::qco::createVerifyTargetConformance(target)); + EXPECT_NE(diagnostics.find(operation), std::string::npos) << diagnostics; + EXPECT_NE(diagnostics.find(details), std::string::npos) << diagnostics; + EXPECT_NE(diagnostics.find(locus), std::string::npos) << diagnostics; + }; + + expectUnsupported(Target{std::vector{Site{10}}, std::nullopt, + std::vector{Operation{"x", 1, 0}}}, + build([](QCOProgramBuilder& builder) { + auto qubit = builder.staticQubit(10); + qubit = builder.h(qubit); + return builder.intConstant(0); + }), + "'qco.h'", "arity 1 and 0 parameter(s)", "[10]"); + + expectUnsupported(Target{std::vector{Site{10}, Site{20}}, std::nullopt, + std::vector{Operation{"x", 2, 0}}}, + build([](QCOProgramBuilder& builder) { + auto qubit = builder.staticQubit(10); + qubit = builder.x(qubit); + return builder.intConstant(0); + }), + "'qco.x'", "arity 1 and 0 parameter(s)", "[10]"); + + expectUnsupported(Target{std::vector{Site{10}}, std::nullopt, + std::vector{Operation{"rz", 1, 0}}}, + build([](QCOProgramBuilder& builder) { + auto qubit = builder.staticQubit(10); + qubit = builder.rz(0.25, qubit); + return builder.intConstant(0); + }), + "'qco.rz'", "arity 1 and 1 parameter(s)", "[10]"); + + expectUnsupported(Target{std::vector{Site{10}}, std::nullopt, + std::vector{Operation{"x", 1, 0}}}, + build([](QCOProgramBuilder& builder) { + auto qubit = builder.staticQubit(30); + qubit = builder.x(qubit); + return builder.intConstant(0); + }), + "'qco.x'", "arity 1 and 0 parameter(s)", "[30]"); +} + +TEST_F(TargetSynthesisTest, ConformanceChecksNonUnitaryCapabilities) { + auto module = build([](QCOProgramBuilder& builder) { + auto qubit = builder.staticQubit(0); + auto [measured, result] = builder.measure(qubit); + static_cast(result); + measured = builder.reset(measured); + return builder.intConstant(0); + }); + const Target xOnly{1, std::nullopt, std::vector{Operation{"x", 1, 0}}}; + const auto diagnostics = + expectFailure(*module, mlir::qco::createVerifyTargetConformance(xOnly)); + EXPECT_NE(diagnostics.find("'qco.measure' with arity 1 and 0 parameter(s)"), + std::string::npos) + << diagnostics; +} + +TEST_F(TargetSynthesisTest, ConformanceTracesStructuredControlFlow) { + auto module = build([](QCOProgramBuilder& builder) { + auto qubit = builder.staticQubit(10); + qubit = builder.qcoIf( + true, qubit, [&](Value argument) { return builder.h(argument); }, + [&](Value argument) { return builder.h(argument); }); + qubit = + builder.scfFor(0, 1, 1, qubit, [&](Value, mlir::ValueRange arguments) { + return mlir::SmallVector{builder.x(arguments[0])}; + })[0]; + qubit = builder.h(qubit); + return builder.intConstant(0); + }); + const Target target{std::vector{Site{10}}, std::nullopt, + std::vector{Operation{"h", 1, 0}, Operation{"x", 1, 0}}}; + + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createTargetNativeSynthesis(target)))); + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createVerifyTargetConformance(target)))); +} + +} // namespace mqt::test::qco diff --git a/python/mqt/core/mlir.pyi b/python/mqt/core/mlir.pyi index 771fda6f26..58e8f53c5c 100644 --- a/python/mqt/core/mlir.pyi +++ b/python/mqt/core/mlir.pyi @@ -150,9 +150,6 @@ class QCOProgram(Program): def fuse_single_qubit_unitary_runs(self, *, basis: str = "zyz") -> None: """Fuse single-qubit unitary runs into the chosen decomposition basis.""" - def fuse_two_qubit_unitary_runs(self, *, native_gates: str) -> None: - """Lower unitaries to a non-empty native gate menu via two-qubit run fusion.""" - def unroll_quantum_loops(self, *, unroll_factor: int = -1) -> None: """Unroll quantum loops, optionally using a maximum unroll factor.""" diff --git a/test/python/test_mlir.py b/test/python/test_mlir.py index 3dc2dd4c3a..5fffae2715 100644 --- a/test/python/test_mlir.py +++ b/test/python/test_mlir.py @@ -365,20 +365,6 @@ def test_typed_programs_normalize_global_phases() -> None: assert qco.ir == once -def test_qco_program_two_qubit_fusion_requires_native_gates() -> None: - """Require callers to provide a non-empty native gate menu.""" - qco = compile_program(QASM_STRING, output=OutputFormat.QCO) - assert isinstance(qco, QCOProgram) - - with pytest.raises(TypeError, match="incompatible function arguments"): - qco.fuse_two_qubit_unitary_runs() # ty: ignore[missing-argument] - - with pytest.raises(RuntimeError, match="MLIR operation failed"): - qco.fuse_two_qubit_unitary_runs(native_gates="") - - qco.fuse_two_qubit_unitary_runs(native_gates="u,cx") - - def test_qco_program_decomposes_multi_controlled() -> None: """Decompose multi-controlled gates through the typed QCOProgram API.""" qc = QuantumComputation(3) From 00fab3ca2a37dd624b699360ce261c199d66425a Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Mon, 3 Aug 2026 17:43:40 +0200 Subject: [PATCH 2/6] =?UTF-8?q?=F0=9F=93=9D=20Link=20SYN-01=20in=20the=20c?= =?UTF-8?q?hangelog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assisted-by: GPT-5.6 via Codex --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05beb0eb69..2be12f99bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,7 @@ releases may include breaking changes. QCO functions ([#1915]) ([**@simon1hofmann**]) - ✨ Add profitable target-independent two-qubit unitary-window optimization, target-native post-routing synthesis, and exact operation/locus conformance - ([#1865], [#1961]) ([**@simon1hofmann**], [**@burgholzer**]) + ([#1865], [#1961], [#1998]) ([**@simon1hofmann**], [**@burgholzer**]) - ✨ Add a `decompose-multi-controlled` pass for decomposing controlled X, Z, RCCX, and constant-angle phase gates with a configurable `min-controls` threshold ([#1810]) ([**@simon1hofmann**]) @@ -709,6 +709,7 @@ for previous changelogs._ +[#1998]: https://github.com/munich-quantum-toolkit/core/pull/1998 [#1997]: https://github.com/munich-quantum-toolkit/core/pull/1997 [#1992]: https://github.com/munich-quantum-toolkit/core/pull/1992 [#1986]: https://github.com/munich-quantum-toolkit/core/pull/1986 From fe0a1a1e2d1fb5674b089a767793f876bfd9b206 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Mon, 3 Aug 2026 18:25:35 +0200 Subject: [PATCH 3/6] =?UTF-8?q?=F0=9F=94=A7=20Remove=20the=20obsolete=20QC?= =?UTF-8?q?O=20transform=20include?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assisted-by: GPT-5.6 via Codex --- mlir/tools/mqt-cc/mqt-cc.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/mlir/tools/mqt-cc/mqt-cc.cpp b/mlir/tools/mqt-cc/mqt-cc.cpp index 4dde094da1..69870df47a 100644 --- a/mlir/tools/mqt-cc/mqt-cc.cpp +++ b/mlir/tools/mqt-cc/mqt-cc.cpp @@ -17,7 +17,6 @@ #include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QC/Translation/TranslateQASM3ToQC.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" -#include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" #include "mlir/Support/Passes.h" From 343a094d907b9ad5f1d31f23700a1e3771a1f53b Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Mon, 3 Aug 2026 19:41:43 +0200 Subject: [PATCH 4/6] =?UTF-8?q?=F0=9F=A7=AA=20Cover=20target=20synthesis?= =?UTF-8?q?=20contracts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assisted-by: GPT-5.6 via Codex --- .agent/plans/1687-syn-01-target-synthesis.md | 91 +++++++++++----- .../NativeSynthesis/test_target_synthesis.cpp | 101 ++++++++++++++++++ 2 files changed, 167 insertions(+), 25 deletions(-) diff --git a/.agent/plans/1687-syn-01-target-synthesis.md b/.agent/plans/1687-syn-01-target-synthesis.md index 40e7eb128c..dfc7b858d6 100644 --- a/.agent/plans/1687-syn-01-target-synthesis.md +++ b/.agent/plans/1687-syn-01-target-synthesis.md @@ -62,6 +62,18 @@ arity, parameter, and site mismatches. interface and stub checks, focused `clang-tidy`, hooks, lint, stale-surface search, and diff audits on the final CT-01 base, then amended this evidence into the single signed commit. +- [x] (2026-08-03 19:55Z) Published draft PR #1998, fixed its sole changed-file + `clang-tidy` finding, and added four focused regressions after Codecov + exposed untested adapter, lowering, optimization, and provenance paths. + The resulting 20/199 focused tests pass and the two implementation files + reach 91% combined local line coverage. +- [x] (2026-08-03 20:43Z) Observed terminal all-green CI for exact published + head `b0a520372`, including 92.1% C++ patch coverage and strict + documentation, then restacked the four SYN commits onto merged MAP-01 + commit `c9e0c0ca5`. Resolved the compiler test by retaining MAP-01's + undirected coupling input while removing the obsolete native-menu call, + retained both PR links in the changelog, and passed 20/199/215 tests plus + the focused high-level mapping API test. ## Milestones @@ -163,10 +175,10 @@ GitHub without separate authorization. unitary shell with a region body. Synthesis and conformance must classify the shell and skip its implementation body. - Observation: Independent review also identified non-blocking follow-up - opportunities: failure-atomic preflight, sink-only source validation, - dedicated `index_switch`/`while` regressions, all single-qubit adapter bases, - and more legacy scanner regressions. These are classified as future hardening - rather than expanding this slice after its requested contracts pass. + opportunities: failure-atomic preflight, sink-only source validation, and more + legacy scanner regressions. Dedicated `index_switch`/`while` and single-qubit + adapter regressions were subsequently added because they cover valid behavior + that Codecov showed was otherwise untested. ## Decision Log @@ -206,16 +218,17 @@ GitHub without separate authorization. represented by generic pass options, and separate factories make each stage independently testable and benchmarkable. Date/Author: 2026-08-03, GPT-5.6 via Codex. -- Decision: Preserve Simon Hofmann's authorship in the final commit. Rationale: - the implementation materially carries forward the progressive post-routing - behavior and SWAP-removal coverage from his #1969 work while excluding the - unapproved Python and CLI designs. Date/Author: 2026-08-03, GPT-5.6 via Codex. +- Decision: Preserve Simon Hofmann's authorship in the implementation commit. + Rationale: the implementation materially carries forward the progressive + post-routing behavior and SWAP-removal coverage from his #1969 work while + excluding the unapproved Python and CLI designs. Date/Author: 2026-08-03, + GPT-5.6 via Codex. ## Outcomes & Retrospective The implementation now has one capability authority and three separately -observable transform stages. On the final CT-01 base, sixteen focused tests pass -along with all 199 decomposition and 215 compiler tests. The relevant CMake +observable transform stages. On the merged MAP-01 base, twenty focused tests +pass along with all 199 decomposition and 215 compiler tests. The relevant CMake interface-header targets, CLI build and surface checks, and full Python stub regeneration pass; regeneration leaves the tracked stubs unchanged. Independent review approved the remediated code with no remaining code blockers. @@ -223,8 +236,10 @@ Changed-file hooks, full repository lint, stale-surface search, and `git diff --check` pass. Focused `clang-tidy` is clean for the new synthesis source, basis adapter, and both modified test sources; the Weyl source reports only three pre-existing warnings on unchanged header lines. The result is -contained in one signed local commit directly on the final CT-01 squash and is -not pushed. +published as draft PR #1998. Its pre-MAP exact head passed every check, +including 92.1% C++ patch coverage. The four focused commits are now restacked +onto merged MAP-01, and replacement exact-head CI is the remaining publication +gate. The main design lesson is that target support and resynthesis profitability must not share a configuration surface. The canonical U/CX optimizer is useful @@ -272,11 +287,12 @@ uses. The optimizer compares original and canonical entangler counts. Target synthesis classifies actual mapped operations and rewrites only lowering needs. Conformance performs an independent read-only walk and exact target query. -The final work updates the existing Unreleased changelog entry without inventing -a pull-request reference or upgrade note, formats changed sources, runs focused -and repository-required validation, obtains an independent read-only review, -addresses findings, and creates one signed commit with `Assisted-by` and Simon -Hofmann `Co-authored-by` trailers. +The final work updates the existing Unreleased changelog entry without an +upgrade note, formats changed sources, runs focused and repository-required +validation, obtains independent read-only reviews, and publishes signed, focused +commits. The implementation commit preserves Simon Hofmann's `Co-authored-by` +trailer; later metadata, lint, and coverage follow-ups carry the required +`Assisted-by` trailer. ## Concrete Steps @@ -305,9 +321,9 @@ Build and run the three affected C++ suites: build/release/mlir/unittests/Compiler/\ mqt-core-mlir-unittests-compiler -The observed summaries are: +The current observed summaries are: - [ PASSED ] 16 tests. + [ PASSED ] 20 tests. [ PASSED ] 199 tests. [ PASSED ] 215 tests. @@ -344,9 +360,9 @@ Acceptance requires the following observable behavior: and generated pass surfaces are absent. - Focused tests, decomposition tests, compiler tests, changed-file checks, repository lint, and `git diff --check` pass. -- The final commit is signed, carries the required `Assisted-by` and - `Co-authored-by: Simon Hofmann ` trailers, and is not - pushed. +- Every commit is signed and carries the required `Assisted-by` trailer. The + implementation commit additionally carries + `Co-authored-by: Simon Hofmann `. ## Idempotence and Recovery @@ -358,16 +374,18 @@ an environment boundary. Do not reset or clean the worktree. Inspect status and remove only task-generated artifacts if necessary. Do not modify another task's worktree or -shared branch metadata. External publication remains unauthorized. +shared branch metadata. External publication requires separate authorization; +draft PR #1998 was published only after that authorization was granted. ## Artifacts and Notes Initial development used CT-01 commit -`b6eb95521cb76224c137496f113f38b9ce295854`. The final one-commit SYN patch is +`b6eb95521cb76224c137496f113f38b9ce295854`. The SYN implementation patch is restacked directly onto the squash-merged CT-01 base `f775395a25fddba0a3b54996416d1311bc6ebe71`. The stable patch ID before and after the restack is `fb73dd9d29254dfca2edf68df7eb9bf983284cc4`, proving that no SYN -patch content changed during the history rewrite. +implementation content changed during the history rewrite. Changelog, +changed-file lint, and coverage follow-ups remain separate focused commits. Relevant #1969 source commits are `3be6d8e43` for target-native naming and `1aa4c975b` for progressive post-routing synthesis and SWAP-removal coverage, @@ -401,6 +419,23 @@ warnings on unchanged `Weyl.h` lines 167 and 171. Changed-file hooks, full repository lint, stale-surface search outside historical plans, both worktree and committed `git diff --check`, and the final status audit pass. +The coverage follow-up added four behavioral regressions for every supported +single-qubit basis mapping, profitable interleaved two-qubit optimization, +constant single-qubit target lowering, and site tracing through `scf.while`, +`qco.index_switch`, measurement, and reset. The target-synthesis and +decomposition suites now pass 20 and 199 tests. Together, `SynthesisBasis.cpp` +and `TargetSynthesis.cpp` reach 91% local line coverage; the remaining uncovered +lines are defensive invalid-state paths rather than normal target-compilation +behavior. + +After MAP-01 merged as `c9e0c0ca5`, the four SYN commits were rebased onto that +commit. The integration range-diff removes only a compiler-target link already +provided by MAP-01, retains MAP-01's canonical undirected coupling input in the +compiler API test, and keeps both PR links in the changelog. The lint and +coverage follow-up patches remain identical. Fresh release builds pass 20 target +synthesis, 199 decomposition, and 215 compiler tests, including the focused +`QCOProgramOptimizationAPIs` test. + ## Interfaces and Dependencies `CompilerTarget` remains dependency-light and does not depend on transform @@ -415,3 +450,9 @@ public, independently constructible factories are: There is no textual target-native pass, synthetic target, native-gate menu, or parallel capability model. + +Revision note (2026-08-03): Updated the completed plan after PR publication, CI +feedback, and the MAP-01 merge. This records the focused coverage regressions, +current test and coverage evidence, multi-commit publication state, the +integration conflict resolutions, and the fact that Simon Hofmann's authorship +belongs to the implementation commit rather than every follow-up. diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp index 9bfd871ba9..dddddeab4e 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp @@ -14,6 +14,8 @@ #include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/SynthesisBasis.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/QCO/Utils/DDFunctionality.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" @@ -38,6 +40,7 @@ #include #include +#include #include #include #include @@ -57,6 +60,7 @@ using mlir::ModuleOp; using mlir::OwningOpRef; using mlir::Value; using mlir::qco::CtrlOp; +using mlir::qco::HOp; using mlir::qco::QCOProgramBuilder; using mlir::qco::RXXOp; using mlir::qco::SWAPOp; @@ -195,6 +199,26 @@ TEST(TargetSynthesisPassContract, FactoriesAreIndependentlyConstructible) { mlir::arith::ArithDialect::getDialectNamespace())); } +TEST(TargetSynthesisPassContract, AdaptsEverySupportedSynthesisBasis) { + using mlir::qco::decomposition::EulerBasis; + using mlir::qco::decomposition::NativeSynthesisBasis; + + constexpr std::array singleQubitBases{ + std::pair{Target::SingleQubitBasis::U, EulerBasis::U}, + std::pair{Target::SingleQubitBasis::ZSXX, EulerBasis::ZSXX}, + std::pair{Target::SingleQubitBasis::R, EulerBasis::R}, + std::pair{Target::SingleQubitBasis::XZX, EulerBasis::XZX}, + std::pair{Target::SingleQubitBasis::XYX, EulerBasis::XYX}, + std::pair{Target::SingleQubitBasis::ZYZ, EulerBasis::ZYZ}, + }; + for (const auto [targetBasis, decompositionBasis] : singleQubitBases) { + const auto adapted = NativeSynthesisBasis::fromCompilerTarget( + {.singleQubit = targetBasis, .entangler = Target::GateKind::CX}); + EXPECT_EQ(adapted.singleQubit, decompositionBasis); + EXPECT_EQ(adapted.entangler, Target::GateKind::CX); + } +} + TEST_F(TargetSynthesisTest, PreRoutingOptimizationRequiresStrictImprovement) { const auto adjacentCx = [](QCOProgramBuilder& builder) { auto q0 = builder.staticQubit(0); @@ -223,6 +247,26 @@ TEST_F(TargetSynthesisTest, PreRoutingOptimizationRequiresStrictImprovement) { EXPECT_EQ(countOps(*nonImproving), 3U); } +TEST_F(TargetSynthesisTest, + PreRoutingOptimizationFusesInterleavedSingleQubitGates) { + const auto interleaved = [](QCOProgramBuilder& builder) { + auto q0 = builder.staticQubit(0); + auto q1 = builder.staticQubit(1); + std::tie(q0, q1) = builder.cx(q0, q1); + q1 = builder.x(q1); + q0 = builder.z(q0); + std::tie(q0, q1) = builder.cx(q0, q1); + return builder.intConstant(0); + }; + auto expected = build(interleaved); + auto optimized = build(interleaved); + + ASSERT_TRUE(mlir::succeeded( + runPass(*optimized, mlir::qco::createOptimizeTwoQubitUnitaryRuns()))); + EXPECT_EQ(countOps(*optimized), 0U); + expectEquivalent(expected, optimized); +} + TEST_F(TargetSynthesisTest, PreRoutingOptimizationLeavesIndividualOpsAlone) { auto module = build([](QCOProgramBuilder& builder) { auto q0 = builder.staticQubit(0); @@ -279,6 +323,25 @@ TEST_F(TargetSynthesisTest, TargetNativeSynthesisRemovesOrdinarySwap) { expectEquivalent(expected, synthesized); } +TEST_F(TargetSynthesisTest, + TargetNativeSynthesisLowersConstantSingleQubitGate) { + const auto hadamard = [](QCOProgramBuilder& builder) { + auto qubit = builder.staticQubit(0); + qubit = builder.h(qubit); + return builder.intConstant(0); + }; + auto expected = build(hadamard); + auto synthesized = build(hadamard); + const auto target = makeUCxTarget(); + + ASSERT_TRUE(mlir::succeeded( + runPass(*synthesized, mlir::qco::createTargetNativeSynthesis(target)))); + EXPECT_EQ(countOps(*synthesized), 0U); + ASSERT_TRUE(mlir::succeeded( + runPass(*synthesized, mlir::qco::createVerifyTargetConformance(target)))); + expectEquivalent(expected, synthesized); +} + TEST_F(TargetSynthesisTest, TargetNativeSynthesisPreservesNativeSwap) { auto module = build([](QCOProgramBuilder& builder) { auto q0 = builder.staticQubit(0); @@ -562,4 +625,42 @@ TEST_F(TargetSynthesisTest, ConformanceTracesStructuredControlFlow) { runPass(*module, mlir::qco::createVerifyTargetConformance(target)))); } +TEST_F(TargetSynthesisTest, ConformanceTracesWhileSwitchMeasurementAndReset) { + auto module = build([](QCOProgramBuilder& builder) { + auto qubit = builder.staticQubit(10); + qubit = builder + .scfWhile( + mlir::ValueRange{qubit}, + [&](mlir::ValueRange arguments) { + auto [measured, result] = + builder.measure(arguments.front()); + builder.scfCondition(result, measured); + return mlir::SmallVector{measured}; + }, + [&](mlir::ValueRange arguments) { + return mlir::SmallVector{ + builder.reset(arguments.front())}; + }) + .front(); + qubit = builder.qcoIndexSwitch( + 0, qubit, mlir::SmallVector{0}, + mlir::SmallVector>{ + [&](Value argument) { return builder.h(argument); }}, + [&](Value argument) { return builder.h(argument); }); + auto [measured, result] = builder.measure(qubit); + static_cast(result); + static_cast(builder.h(builder.reset(measured))); + return builder.intConstant(0); + }); + const Target target{std::vector{Site{10}}, std::nullopt, + std::vector{Operation{"h", 1, 0}, + Operation{"measure", 1, 0}, + Operation{"reset", 1, 0}}}; + + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createTargetNativeSynthesis(target)))); + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createVerifyTargetConformance(target)))); +} + } // namespace mqt::test::qco From 49edf86d5c28e0334d2e5f488d53fe2a31b07d98 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Mon, 3 Aug 2026 22:00:36 +0200 Subject: [PATCH 5/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20target=20sy?= =?UTF-8?q?nthesis=20invariants?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make CompilerTarget's single-qubit basis authoritative, normalize symmetric gate support, and remove operand-reversal machinery. Use U/CZ fusion with preplanned direct rewrites, cached provider tracing, and failure-atomic target synthesis. Assisted-by: GPT-5.6 via Codex --- .agent/plans/1687-syn-01-target-synthesis.md | 216 +++++---- mlir/include/mlir/Compiler/Target.h | 22 +- .../QCO/Transforms/Decomposition/Euler.h | 30 +- .../Transforms/Decomposition/SynthesisBasis.h | 43 -- .../QCO/Transforms/Decomposition/Weyl.h | 26 +- mlir/lib/Compiler/Target.cpp | 30 +- .../lib/Dialect/QCO/Transforms/CMakeLists.txt | 3 +- .../QCO/Transforms/Decomposition/Euler.cpp | 74 ++-- .../Decomposition/SynthesisBasis.cpp | 125 ------ .../QCO/Transforms/Decomposition/Weyl.cpp | 160 ++++--- .../FuseSingleQubitUnitaryRuns.cpp | 42 +- .../NativeSynthesis/TargetSynthesis.cpp | 414 ++++++++---------- .../Compiler/test_compiler_target.cpp | 11 +- .../test_euler_decomposition.cpp | 27 +- .../Decomposition/test_weyl_decomposition.cpp | 32 +- .../NativeSynthesis/test_target_synthesis.cpp | 80 ++-- 16 files changed, 628 insertions(+), 707 deletions(-) delete mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/SynthesisBasis.h delete mode 100644 mlir/lib/Dialect/QCO/Transforms/Decomposition/SynthesisBasis.cpp diff --git a/.agent/plans/1687-syn-01-target-synthesis.md b/.agent/plans/1687-syn-01-target-synthesis.md index dfc7b858d6..940ad7f7fd 100644 --- a/.agent/plans/1687-syn-01-target-synthesis.md +++ b/.agent/plans/1687-syn-01-target-synthesis.md @@ -21,8 +21,8 @@ actual type, arity, parameter count, provider site IDs, and ordered locus. After this change, C++ pipeline code can construct these three stages independently. Focused tests demonstrate a profitable CX cancellation, preserve isolated and runtime-parameterized gates before routing, lower SWAP to a -target-selected U/CX basis after routing, and reject direction, operation, -arity, parameter, and site mismatches. +target-selected basis after routing, and reject direction, operation, arity, +parameter, and site mismatches. ## Progress @@ -47,9 +47,9 @@ arity, parameter, and site mismatches. completed source formatting, changed-file checks, repository lint, and initial diff checks. - [x] (2026-08-03 19:01Z) Resolved all three blockers from independent read-only - review: reverse-only symmetric-entangler loci, native SWAP authority, and - native `qco.pow` body handling. The reviewer approved the remediated code - and the focused suite passes all sixteen tests. + review: single-orientation symmetric-entangler loci, native SWAP + authority, and native `qco.pow` body handling. The reviewer approved the + remediated code and the focused suite passes all sixteen tests. - [x] (2026-08-03 19:20Z) Rebuilt and reran all three affected suites, reran changed-file hooks and full repository lint, completed focused `clang-tidy` checks and the final diff audit, and prepared one signed @@ -74,6 +74,18 @@ arity, parameter, and site mismatches. undirected coupling input while removing the obsolete native-menu call, retained both PR links in the changelog, and passed 20/199/215 tests plus the focused high-level mapping API test. +- [x] (2026-08-03 19:54Z) Addressed the latest design and efficiency review as + one simplification: made `CompilerTarget::SingleQubitBasis` the only basis + enum, deleted the decomposition adapter, removed entangler operand + reversal and redundant matrix assertions, selected U/CZ for generic + fusion, reused Weyl decompositions, cached provider-site provenance, and + made target lowering failure-atomic. A direct preplanned `IRRewriter` + traversal replaces general greedy machinery. Rebuilt the public + interface-header targets and observed 21/21 target-synthesis, 199/199 + decomposition, and 215/215 compiler tests. Focused changed-source + `clang-tidy`, targeted hooks, full repository lint, stale-surface search, + and `git diff --check` pass. A fresh independent exact-working-tree review + approved the result with no correctness, bloat, or efficiency findings. ## Milestones @@ -83,11 +95,10 @@ The completed first milestone removes `mlir/Dialect/QCO/Transforms/Decomposition/NativeGateset.h` and its source. Gate identity, provider aliases, arity, parameter counts, global capability checks, and basis selection now remain in `mlir/include/mlir/Compiler/Target.h` and -`mlir/lib/Compiler/Target.cpp`. The new decomposition-only -`NativeSynthesisBasis` adapts the target-selected single-qubit basis to -`EulerBasis` and caches a Weyl decomposer for the target-selected entangler. It -contains no parser, menu, aliases, target classification, or independent basis -selection. +`mlir/lib/Compiler/Target.cpp`. `CompilerTarget::SingleQubitBasis` is also the +single type consumed by Euler and Weyl synthesis; the duplicate `EulerBasis` +enum and `NativeSynthesisBasis` adapter are deleted. Weyl retains only cached +decomposers for the target-selected entangler. The generated `fuse-two-qubit-unitary-runs` pass and all of its string-menu surfaces are removed rather than retained as a synthetic target. The later PIPE @@ -97,7 +108,7 @@ the three typed C++ pass factories. ### Milestone 2: Separate optimization, lowering, and verification The completed pre-routing stage scans maximal constant windows on one pair of -linear QCO wires. It evaluates a canonical U/CX Weyl decomposition and rewrites +linear QCO wires. It evaluates a canonical U/CZ Weyl decomposition and rewrites only when the synthesized entangler count is strictly smaller than the window's original two-qubit operation count. It does not accept a target and does not rewrite isolated or runtime-parameterized gates. @@ -107,36 +118,36 @@ whether each one- or two-qubit unitary is supported at its ordered provider locus. Supported operations, including supported runtime-parameterized gates, remain untouched. Unsupported constant operations and all ordinary `qco.swap` operations that the target does not support are lowered through the target's -globally usable synthesis basis. A target-native SWAP remains untouched. For a -symmetric basis entangler available only at the reverse ordered locus, emission -reverses the entangler operands while preserving logical wire identity. The pass -asks for the basis only after finding an actual lowering need, so an incomplete -target can still accept an already-conforming program. An unsupported runtime -gate receives an operation-local matrix diagnostic. +globally usable synthesis basis. A target-native SWAP remains untouched. +Recognized operand-symmetric gates are semantically bidirectional even though +raw provider loci retain their reported order, so synthesis emits every +entangler in logical wire order. The pass preflights every lowering need before +mutation, asks for a basis only when needed, and reports an unsupported runtime +gate without partially rewriting the module. The completed conformance stage traces each qubit operand back to `qco.static`, including values passing through QCO `if`/`index_switch` and SCF `for`/`while` regions. It then calls `CompilerTarget::supports` on the real operation and ordered provider site IDs. It checks unitary, measurement, and reset operations and reports the actual operation spelling, arity, parameter count, and locus. +Pass-local path compression makes repeated provider-site tracing linear in the +valid SSA lineage rather than repeatedly walking to `qco.static`. ### Milestone 3: Prove the contracts and hand off one atomic change The focused target-synthesis test binary now proves the three stage boundaries, including unitary equivalence for profitable optimization, SWAP lowering, and a -reverse-only symmetric entangler. It also proves that native SWAP and `qco.pow` -shells remain untouched. The decomposition and compiler suites prove that typed -bases still cover every supported entangler and that removing the old high-level -menu API does not break the remaining compiler pipeline. Changelog curation, -formatting and lint, and independent review are complete. After CT-01 was -squash-merged, the same patch restacked without content conflicts onto the final -target implementation. Post-restack validation and the final signed amend -complete this milestone. +single reported CZ orientation with bidirectional semantic support. It also +proves explicit CZ emission, failure-atomic diagnostics, and that native SWAP +and `qco.pow` shells remain untouched. The decomposition and compiler suites +prove that the shared basis type covers every supported entangler and that +removing the old high-level menu API does not break the remaining compiler +pipeline. This slice must not add `compileForTarget`, alter default pipeline composition, or remove the coupling-only `placeAndRoute` overload; those are PIPE -responsibilities. It must not push, open or modify a pull request, or mutate -GitHub without separate authorization. +responsibilities. Publishing this latest revision or otherwise mutating GitHub +requires separate revision-scoped authorization. ## Surprises & Discoveries @@ -152,9 +163,9 @@ GitHub without separate authorization. `CompilerTarget` permits sparse provider IDs. Evidence: the conformance tests use sites 10 and 20 and distinguish loci `[10, 20]` and `[20, 10]`. - Observation: Applying an MLIR greedy rewrite driver can reorder an unrelated - constant even when the quantum pattern does not match. The preservation test - therefore asserts that the isolated SWAP remains and no entangler is - introduced, rather than requiring byte-identical generic IR. + constant even when the quantum pattern does not match. The target-specific + transforms now precompute their work and use `IRRewriter` directly, so a no-op + optimization preserves the module byte-for-byte. - Observation: Initial configuration required network access to fetch pinned repository dependencies. Once fetched into the worktree-local build tree, focused compilation and tests were repeatable without source workarounds. @@ -167,18 +178,21 @@ GitHub without separate authorization. decomposition, optional routing, and late native synthesis, with a test that routed SWAPs disappear. Its `targetNative` Python duck typing and coupling CLI are intentionally excluded. -- Observation: `CompilerTarget` deliberately accepts an operand-symmetric - entangler as globally usable when either ordered orientation is available, - while exact operation support remains order-sensitive. Target-native emission - must therefore choose the supported orientation per mapped locus. +- Observation: IQM reports each CZ edge once. Raw `Operation::loci()` and + `Operation::supports()` therefore remain ordered, while semantic + `CompilerTarget::supports()` normalizes recognized operand-symmetric gates + across both orientations. This establishes the bidirectional invariant before + synthesis and removes direction handling from emission. - Observation: `qco.pow`, like `qco.ctrl` and `qco.inv`, is a target-visible unitary shell with a region body. Synthesis and conformance must classify the shell and skip its implementation body. -- Observation: Independent review also identified non-blocking follow-up - opportunities: failure-atomic preflight, sink-only source validation, and more - legacy scanner regressions. Dedicated `index_switch`/`while` and single-qubit - adapter regressions were subsequently added because they cover valid behavior - that Codecov showed was otherwise untested. +- Observation: A generic walk rewrite driver cannot safely anchor this + multi-operation fusion at the run head because the rewrite erases operations + the driver has not visited. Precollecting non-overlapping run heads and using + `IRRewriter` directly is both safer and lighter. +- Observation: Repeated provider-site tracing was quadratic on long SSA wire + chains, and profitable windows decomposed the same two-qubit matrix twice. + Path compression and split decomposition/emission remove both costs. ## Decision Log @@ -187,26 +201,31 @@ GitHub without separate authorization. intentionally moves callers to a typed `CompilerTarget`; a synthetic menu target would preserve the parallel configuration model this change removes. Date/Author: 2026-08-03, GPT-5.6 via Codex. -- Decision: Keep `NativeSynthesisBasis` as a minimal decomposition-only adapter - directly constructed from `CompilerTarget::SynthesisBasis`. Rationale: Euler - emission and cached Weyl decomposition need transform-specific values, but - target capability semantics and basis selection must remain centralized. +- Decision: Use `CompilerTarget::SingleQubitBasis` directly throughout target, + Euler, and Weyl code and delete `NativeSynthesisBasis`. Rationale: one enum + and one synthesis-basis value remove a conversion switch, adapter files, and + an otherwise redundant coverage test while preserving dependency direction. Date/Author: 2026-08-03, GPT-5.6 via Codex. - Decision: Measure pre-routing profitability by a strict reduction in two-qubit - basis uses and materialize a canonical U/CX sequence only after the comparison + basis uses and materialize a canonical U/CZ sequence only after the comparison succeeds. Rationale: two-qubit operations drive routing cost, and selecting a hardware basis before routing would conflate optimization with target - legality. Date/Author: 2026-08-03, GPT-5.6 via Codex. + legality; CZ avoids introducing an arbitrary control direction. Date/Author: + 2026-08-03, GPT-5.6 via Codex. - Decision: Let `CompilerTarget::supports` decide whether an ordinary `qco.swap` requires post-routing lowering. Rationale: the target is the sole capability authority; routing SWAPs still lower on ordinary targets that do not report SWAP, while a target-native SWAP must remain legal even when the target has no global synthesis basis. Date/Author: 2026-08-03, GPT-5.6 via Codex. -- Decision: For an operand-symmetric target basis supported only at the reverse - ordered locus, emit each entangler with reversed operands and map its results - back to the original logical wires. Rationale: global target basis selection - accepts either orientation for symmetric gates, but final conformance checks - the exact provider order. Date/Author: 2026-08-03, GPT-5.6 via Codex. +- Decision: Normalize recognized operand-symmetric capabilities inside + `CompilerTarget` and require every globally selected entangler to be + semantically supported in both directions. Rationale: raw calibration loci + retain provider order, IQM need not duplicate CZ data, and synthesis carries + no operand-reversal option. Date/Author: 2026-08-03, GPT-5.6 via Codex. +- Decision: Preflight all target-lowering needs, cache provider provenance, and + apply planned rewrites directly with `IRRewriter`. Rationale: failure remains + atomic, valid lineage tracing is amortized linear, and generic greedy/fixpoint + work is unnecessary. Date/Author: 2026-08-03, GPT-5.6 via Codex. - Decision: Do not require `CompilerTarget::synthesisBasis()` at pass construction or pass entry. Rationale: absent operations mean all operations are native, and an incomplete explicit target can still describe a conforming @@ -227,22 +246,25 @@ GitHub without separate authorization. ## Outcomes & Retrospective The implementation now has one capability authority and three separately -observable transform stages. On the merged MAP-01 base, twenty focused tests +observable transform stages. On the merged MAP-01 base, twenty-one focused tests pass along with all 199 decomposition and 215 compiler tests. The relevant CMake interface-header targets, CLI build and surface checks, and full Python stub -regeneration pass; regeneration leaves the tracked stubs unchanged. Independent -review approved the remediated code with no remaining code blockers. -Changed-file hooks, full repository lint, stale-surface search, and -`git diff --check` pass. Focused `clang-tidy` is clean for the new synthesis -source, basis adapter, and both modified test sources; the Weyl source reports -only three pre-existing warnings on unchanged header lines. The result is -published as draft PR #1998. Its pre-MAP exact head passed every check, -including 92.1% C++ patch coverage. The four focused commits are now restacked -onto merged MAP-01, and replacement exact-head CI is the remaining publication -gate. +regeneration pass; regeneration leaves the tracked stubs unchanged. The latest +working-tree review removes the decomposition basis adapter and general greedy +rewrite machinery, while adding bidirectional symmetric-capability, explicit CZ +emission, and failure-atomic lowering regressions. Changed-file hooks, full +repository lint, stale-surface search, and `git diff --check` pass. Focused +`clang-tidy` is clean for every changed implementation and test source and for +the changed `Target.h` and `Euler.h` surfaces; `Weyl.h` reports only three +pre-existing warnings on unchanged lines. Draft PR #1998's pre-MAP exact head +passed every check, including 92.1% C++ patch coverage. Publication and +replacement exact-head CI for this latest review revision remain. A fresh +independent review approved the exact working tree with no must-fix or +efficiency findings after independently rerunning the target-synthesis, +compiler-target, and decomposition tests. The main design lesson is that target support and resynthesis profitability must -not share a configuration surface. The canonical U/CX optimizer is useful +not share a configuration surface. The canonical U/CZ optimizer is useful without hardware knowledge, while post-routing lowering and conformance require the exact target and provider-site order. @@ -250,19 +272,20 @@ the exact target and provider-site order. `mlir/include/mlir/Compiler/Target.h` defines the immutable target. Its `supports(Operation*, locus)` query recognizes QCO operation semantics and -checks provider site IDs, ordered loci, arity, and parameter count. Its -`synthesisBasis()` query returns one globally usable single-qubit basis and -entangler only when both exist. +checks provider site IDs, semantic loci, arity, and parameter count while raw +operation loci retain provider order. Its `synthesisBasis()` query returns one +globally usable single-qubit basis and entangler only when both exist. `mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp` contains the constant-window scanner, three pass implementations, provider-site tracing, and diagnostics. Public factory declarations live in `mlir/include/mlir/Dialect/QCO/Transforms/Passes.h`. -`mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/SynthesisBasis.h` and -its source adapt the typed target basis for the existing Euler and Weyl -emitters. `mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp` emits the -selected entangler and single-qubit factors. +`mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h` aliases the +target-owned `SingleQubitBasis`; no decomposition-layer basis DTO remains. +`mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp` caches the selected +entangler decomposer, returns a prepared decomposition, and emits its +single-qubit factors without recomputing it. QCO qubits use linear static single assignment: each operation consumes a qubit value and returns its successor. A mapped hardware qubit starts at `qco.static`; @@ -277,10 +300,10 @@ Typed basis coverage remains in ## Plan of Work -The implementation first deletes the duplicate gateset files and rewires Weyl -synthesis to the target-derived adapter. It then removes the obsolete generated -pass, registration, CLI option, `QCOProgram` method, binding, stub, and their -menu tests. +The implementation deletes the duplicate gateset and basis-adapter files and +rewires Euler and Weyl synthesis directly to the target-owned basis type. It +also removes the obsolete generated pass, registration, CLI option, `QCOProgram` +method, binding, stub, and their menu tests. The new source retains the proven two-qubit window scanner but separates its uses. The optimizer compares original and canonical entangler counts. Target @@ -323,7 +346,7 @@ Build and run the three affected C++ suites: The current observed summaries are: - [ PASSED ] 20 tests. + [ PASSED ] 21 tests. [ PASSED ] 199 tests. [ PASSED ] 215 tests. @@ -343,16 +366,20 @@ Acceptance requires the following observable behavior: behavior, while a three-CX SWAP form, an isolated SWAP, and a runtime RXX run remain quantum-structurally unchanged before routing. - Target-native synthesis removes unsupported ordinary SWAP and produces only - operations accepted by a U/CX target, preserving the complete unitary. A - target-native SWAP remains unchanged without requiring a synthesis basis. -- A reverse-only symmetric entangler is emitted at the supported ordered locus - while preserving complete unitary behavior, and a native `qco.pow` shell is - checked without separately rejecting its implementation body. + operations accepted by the selected target basis, preserving the complete + unitary. A target-native SWAP remains unchanged without requiring a synthesis + basis. +- One reported orientation of a recognized symmetric entangler establishes + semantic support in both directions while raw provider data remains ordered. + Synthesis preserves complete unitary behavior without an operand-reversal + option, and a native `qco.pow` shell is checked without separately rejecting + its implementation body. - An absent operation set succeeds without synthesis. An explicit incomplete target succeeds for supported operations and reports “no globally usable synthesis basis” only when an unsupported operation actually needs lowering. - A supported runtime-parameterized gate remains unchanged. An unsupported - runtime gate reports that its unitary matrix is unavailable at compile time. + runtime gate reports that its unitary matrix is unavailable at compile time + without partially rewriting an earlier constant gate. - Conformance distinguishes sparse provider IDs and ordered direction, rejects operation-type, arity, parameter-count, unknown-site, and measurement mismatches, and traces QCO and SCF structured control flow. @@ -392,11 +419,11 @@ Relevant #1969 source commits are `3be6d8e43` for target-native naming and both authored by Simon Hofmann. Independent read-only review initially blocked the candidate on three issues: -reverse-only symmetric-entangler loci, a SWAP capability override, and nested -`qco.pow` body checking. The exact remediated source rebuilt successfully, all -sixteen focused tests passed, and the reviewer reported that all three findings -were closed with no new code blockers. The reviewer also reran -`git diff --check` against the initial development base. +single-orientation symmetric-entangler loci, a SWAP capability override, and +nested `qco.pow` body checking. The exact remediated source rebuilt +successfully, all sixteen focused tests passed, and the reviewer reported that +all three findings were closed with no new code blockers. The reviewer also +reran `git diff --check` against the initial development base. Pre-restack local validation rebuilt `mqt-core-mlir-unittest-target-synthesis`, `mqt-core-mlir-unittest-decomposition`, and `mqt-core-mlir-unittests-compiler`, @@ -419,14 +446,12 @@ warnings on unchanged `Weyl.h` lines 167 and 171. Changed-file hooks, full repository lint, stale-surface search outside historical plans, both worktree and committed `git diff --check`, and the final status audit pass. -The coverage follow-up added four behavioral regressions for every supported -single-qubit basis mapping, profitable interleaved two-qubit optimization, -constant single-qubit target lowering, and site tracing through `scf.while`, -`qco.index_switch`, measurement, and reset. The target-synthesis and -decomposition suites now pass 20 and 199 tests. Together, `SynthesisBasis.cpp` -and `TargetSynthesis.cpp` reach 91% local line coverage; the remaining uncovered -lines are defensive invalid-state paths rather than normal target-compilation -behavior. +The coverage follow-up added regressions for profitable interleaved two-qubit +optimization, constant single-qubit target lowering, and site tracing through +`scf.while`, `qco.index_switch`, measurement, and reset. The subsequent +simplification deletes the basis-adapter coverage test and adds structural CZ +emission plus failure-atomic lowering coverage. The target-synthesis and +decomposition suites now pass 21 and 199 tests. After MAP-01 merged as `c9e0c0ca5`, the four SYN commits were rebased onto that commit. The integration range-diff removes only a compiler-target link already @@ -439,8 +464,9 @@ synthesis, 199 decomposition, and 215 compiler tests, including the focused ## Interfaces and Dependencies `CompilerTarget` remains dependency-light and does not depend on transform -libraries. `MLIRQCOTransforms` links privately to `MQTCompilerTarget`. The -public, independently constructible factories are: +libraries. `MLIRQCOTransforms` publicly links `MQTCompilerTarget` because its +public decomposition headers use compiler-target basis types. The public, +independently constructible factories are: std::unique_ptr createOptimizeTwoQubitUnitaryRuns(); std::unique_ptr diff --git a/mlir/include/mlir/Compiler/Target.h b/mlir/include/mlir/Compiler/Target.h index 9f48d795d3..e2fa6c6d0b 100644 --- a/mlir/include/mlir/Compiler/Target.h +++ b/mlir/include/mlir/Compiler/Target.h @@ -192,7 +192,15 @@ class CompilerTarget { /** * @brief Recognized globally usable single-qubit synthesis basis. */ - enum class SingleQubitBasis : uint8_t { U, ZSXX, R, XZX, XYX, ZYZ }; + enum class SingleQubitBasis : uint8_t { + U, ///< `U(theta, phi, lambda)`. + ZSXX, ///< `RZ` / `SX` / `X` synthesis via a ZYZ decomposition. + R, ///< XYX synthesis expressed with `R(theta, phi)`. + XZX, ///< `RX(phi) * RZ(theta) * RX(lambda)`. + XYX, ///< `RX(phi) * RY(theta) * RX(lambda)`. + ZYZ, ///< `RZ(phi) * RY(theta) * RZ(lambda)`. + ZXZ, ///< `RZ(phi) * RX(theta) * RZ(lambda)`. + }; /** * @brief One single-qubit basis and entangler usable across the target. @@ -300,21 +308,25 @@ class CompilerTarget { [[nodiscard]] llvm::ArrayRef operations() const noexcept; /** - * @brief Return whether a canonical/provider operation is supported at an - * ordered hardware locus. + * @brief Return whether an operation is semantically supported at a hardware + * locus. + * + * @details Raw provider loci remain ordered. Recognized operand-symmetric + * gates are supported in both orientations of each reported two-site locus. */ [[nodiscard]] bool supportsOperation(llvm::StringRef name, llvm::ArrayRef locus, std::optional numParameters = std::nullopt) const; /** - * @brief Return whether a QCO operation is supported at an ordered locus. + * @brief Return whether a QCO operation is semantically supported at a locus. */ [[nodiscard]] bool supports(::mlir::Operation* operation, llvm::ArrayRef locus) const; /** - * @brief Return whether a recognized gate is supported at an ordered locus. + * @brief Return whether a recognized gate is semantically supported at a + * locus. */ [[nodiscard]] bool supports(GateKind gate, llvm::ArrayRef locus) const; diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h index bec84c0447..32449fab04 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h @@ -10,6 +10,7 @@ #pragma once +#include "mlir/Compiler/Target.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" #include @@ -17,7 +18,6 @@ #include #include -#include #include namespace mlir { @@ -26,18 +26,7 @@ class RewritePatternSet; namespace mlir::qco::decomposition { -/** - * @brief Native gate sets for single-qubit Euler synthesis. - */ -enum class EulerBasis : std::uint8_t { - ZYZ = 0, ///< `RZ(phi) * RY(theta) * RZ(lambda)`. - ZXZ = 1, ///< `RZ(phi) * RX(theta) * RZ(lambda)`. - XZX = 2, ///< `RX(phi) * RZ(theta) * RX(lambda)`. - XYX = 3, ///< `RX(phi) * RY(theta) * RX(lambda)`. - U = 4, ///< `U(theta, phi, lambda)`. - ZSXX = 5, ///< `RZ` / `SX` / `X` synthesis via ZYZ decomposition. - R = 6, ///< `R(.,0) * R(.,pi/2) * R(.,0)` (XYX with `Rx`/`Ry` as `R`). -}; +using SingleQubitBasis = CompilerTarget::SingleQubitBasis; /** * @brief Parses a basis name (e.g. `zyz`, `zsxx`; case-insensitive). @@ -45,7 +34,8 @@ enum class EulerBasis : std::uint8_t { * @param basis The basis name. * @return The parsed basis, or `std::nullopt` if unrecognized. */ -[[nodiscard]] std::optional parseEulerBasis(StringRef basis); +[[nodiscard]] std::optional +parseSingleQubitBasis(StringRef basis); /** * @brief Euler angles `(theta, phi, lambda)` and global phase for a 2x2 @@ -53,7 +43,7 @@ enum class EulerBasis : std::uint8_t { * * The decomposition obeys `matrix == e^{i*phase} * K(phi) * A(theta) * * K(lambda)` where `(K, A)` are the rotation axes of the chosen @ref - * EulerBasis. + * SingleQubitBasis. */ struct EulerAngles { double theta = 0.0; ///< Middle rotation angle. @@ -77,11 +67,11 @@ struct SynthesizedUnitary1Q { * @brief Extracts `(theta, phi, lambda, phase)` of @p matrix in @p basis. * * @param matrix The single-qubit unitary to decompose. - * @param basis The target Euler basis. + * @param basis The single-qubit synthesis basis. * @return The extracted Euler angles and global phase. */ [[nodiscard]] EulerAngles anglesFromUnitary(const Matrix2x2& matrix, - EulerBasis basis); + SingleQubitBasis basis); /** * @brief Synthesizes a composed single-qubit unitary as gates in @p basis. @@ -96,14 +86,14 @@ struct SynthesizedUnitary1Q { * @param composed Composed unitary to synthesize. * @param runSize Number of gates in the run. * @param hasNonBasisGate Whether the run contains a gate outside @p basis. - * @param basis The target Euler basis. + * @param basis The single-qubit synthesis basis. * @return The synthesized qubit and correction, or `std::nullopt` if synthesis * is skipped. */ [[nodiscard]] std::optional synthesizeUnitary1QEuler(OpBuilder& builder, Location loc, Value qubit, const Matrix2x2& composed, std::size_t runSize, - bool hasNonBasisGate, EulerBasis basis); + bool hasNonBasisGate, SingleQubitBasis basis); /** * @brief Materializes one accumulated phase correction when needed. @@ -122,7 +112,7 @@ void emitGPhaseIfNeeded(OpBuilder& builder, Location loc, double phase); * bodies are left untouched. */ void populateFuseSingleQubitUnitaryRunsPatterns( - RewritePatternSet& patterns, EulerBasis basis, + RewritePatternSet& patterns, SingleQubitBasis basis, bool skipControlledBodies = false); } // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/SynthesisBasis.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/SynthesisBasis.h deleted file mode 100644 index f8d775141c..0000000000 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/SynthesisBasis.h +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#pragma once - -#include "mlir/Compiler/Target.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" -#include "mlir/Dialect/QCO/Utils/Matrix.h" - -namespace mlir::qco::decomposition { - -struct TwoQubitNativeDecomposition; - -/** - * @brief Typed Euler/Weyl basis adapted from one compiler target. - * - * This value contains only the transformation-specific adaptation needed by - * the decomposers. Gate capabilities and basis selection remain owned by - * @ref CompilerTarget. - */ -struct NativeSynthesisBasis { - EulerBasis singleQubit; - CompilerTarget::GateKind entangler; - - /// Adapt one target-selected synthesis basis to the decomposition layer. - [[nodiscard]] static NativeSynthesisBasis - fromCompilerTarget(CompilerTarget::SynthesisBasis basis); - - /** - * @brief Basis decomposition of @p target under this synthesis basis. - */ - [[nodiscard]] TwoQubitNativeDecomposition - decomposeTarget(const Matrix4x4& target) const; -}; - -} // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h index d1dd2d8f4b..a8efbc87fb 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h @@ -10,6 +10,7 @@ #pragma once +#include "mlir/Compiler/Target.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" #include @@ -343,8 +344,6 @@ decomposeTwoQubitWithBasis( double basisFidelity = 1.0, std::optional numBasisUses = std::nullopt); -struct NativeSynthesisBasis; - /** @brief Result of two-qubit synthesis, including its accumulated phase. */ struct SynthesizedUnitary2Q { Value qubit0; @@ -353,16 +352,19 @@ struct SynthesizedUnitary2Q { }; /** - * @brief Synthesizes a two-qubit unitary in the selected target @p basis. - * - * Set @p reverseEntanglerOperands only when the basis entangler is - * operand-symmetric but the provider supports the reverse ordered locus. - * Logical qubit outputs remain paired with their corresponding inputs. + * @brief Decomposes a two-qubit unitary using @p entangler. + */ +[[nodiscard]] TwoQubitNativeDecomposition +decomposeUnitary2QWeyl(const Matrix4x4& target, + CompilerTarget::GateKind entangler); + +/** + * @brief Emits a prepared two-qubit decomposition in the selected target + * @p basis. */ -[[nodiscard]] FailureOr -synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, Value qubit0, - Value qubit1, const Matrix4x4& target, - const NativeSynthesisBasis& basis, - bool reverseEntanglerOperands = false); +[[nodiscard]] SynthesizedUnitary2Q +emitUnitary2QWeyl(OpBuilder& builder, Location loc, Value qubit0, Value qubit1, + const TwoQubitNativeDecomposition& decomposition, + CompilerTarget::SynthesisBasis basis); } // namespace mlir::qco::decomposition diff --git a/mlir/lib/Compiler/Target.cpp b/mlir/lib/Compiler/Target.cpp index 555231e3d1..cc7eceb745 100644 --- a/mlir/lib/Compiler/Target.cpp +++ b/mlir/lib/Compiler/Target.cpp @@ -544,11 +544,28 @@ bool CompilerTarget::Storage::supportsOperation( if (found == capabilities.end()) { return false; } - return llvm::any_of(found->second, [&](const auto index) { - const auto& operation = (*operations)[index]; - return (!numParameters || operation.numParameters() == *numParameters) && - operation.supports(locus); - }); + const auto supportsExactLocus = [&](const ArrayRef exactLocus) { + return llvm::any_of(found->second, [&](const auto index) { + const auto& operation = (*operations)[index]; + return (!numParameters || operation.numParameters() == *numParameters) && + operation.supports(exactLocus); + }); + }; + if (supportsExactLocus(locus)) { + return true; + } + + const auto* const specification = + std::ranges::find_if(GATE_SPECIFICATIONS, [&](const auto& candidate) { + return candidate.name == canonical; + }); + if (locus.size() != 2 || specification == GATE_SPECIFICATIONS.end() || + !specification->symmetric || + (numParameters && specification->numParameters != *numParameters)) { + return false; + } + const std::array reverseLocus{locus[1], locus[0]}; + return supportsExactLocus(reverseLocus); } bool CompilerTarget::Storage::gateIsGloballySupported( @@ -572,8 +589,7 @@ bool CompilerTarget::Storage::gateIsGloballySupported( specification.numParameters); const auto supportsReverse = supportsOperation(specification.name, reverse, specification.numParameters); - return specification.symmetric ? supportsForward || supportsReverse - : supportsForward && supportsReverse; + return supportsForward && supportsReverse; }; if (couplings) { diff --git a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt index 3427ad5280..a7adcd0cb4 100644 --- a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt +++ b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt @@ -12,8 +12,9 @@ add_mlir_library( MLIRQCOTransforms ${PASSES_SOURCES} LINK_LIBS - PRIVATE + PUBLIC MQTCompilerTarget + PRIVATE MLIRQCODialect MLIRQCOUtils MLIRQTensorUtils diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index 72890114d3..857c63354f 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -175,22 +175,23 @@ void emitGPhaseIfNeeded(OpBuilder& builder, Location loc, const double phase) { .phase = phase - (0.5 * (phi + lambda))}; } -EulerAngles anglesFromUnitary(const Matrix2x2& matrix, const EulerBasis basis) { +EulerAngles anglesFromUnitary(const Matrix2x2& matrix, + const SingleQubitBasis basis) { switch (basis) { - case EulerBasis::ZYZ: - case EulerBasis::ZSXX: + case SingleQubitBasis::ZYZ: + case SingleQubitBasis::ZSXX: return paramsZYZ(matrix); - case EulerBasis::ZXZ: + case SingleQubitBasis::ZXZ: return paramsZXZ(matrix); - case EulerBasis::XZX: + case SingleQubitBasis::XZX: return paramsXZX(matrix); - case EulerBasis::XYX: - case EulerBasis::R: + case SingleQubitBasis::XYX: + case SingleQubitBasis::R: return paramsXYX(matrix); - case EulerBasis::U: + case SingleQubitBasis::U: return paramsU(matrix); default: - llvm_unreachable("invalid Euler basis"); + llvm_unreachable("invalid single-qubit synthesis basis"); } } @@ -254,7 +255,8 @@ struct Unitary1QEulerPlan { * @param angles The angles to use for the decomposition. * @param basis The basis to use for the decomposition. */ - void appendDecomposition(const EulerAngles& angles, const EulerBasis basis) { + void appendDecomposition(const EulerAngles& angles, + const SingleQubitBasis basis) { if (isNearZeroRotationAngle(angles.theta) && isNearZeroRotationAngle(angles.phi) && isNearZeroRotationAngle(angles.lambda)) { @@ -264,20 +266,20 @@ struct Unitary1QEulerPlan { if (isNearZeroRotationAngle(angles.theta)) { switch (basis) { - case EulerBasis::ZYZ: - case EulerBasis::ZXZ: - case EulerBasis::ZSXX: + case SingleQubitBasis::ZYZ: + case SingleQubitBasis::ZXZ: + case SingleQubitBasis::ZSXX: appendRotation(SynthesisStep::Kind::RZ, angles.phi + angles.lambda); break; - case EulerBasis::XZX: - case EulerBasis::XYX: + case SingleQubitBasis::XZX: + case SingleQubitBasis::XYX: appendRotation(SynthesisStep::Kind::RX, angles.phi + angles.lambda); break; - case EulerBasis::R: + case SingleQubitBasis::R: appendRStep(angles.phi + angles.lambda, 0.0); break; - case EulerBasis::U: + case SingleQubitBasis::U: steps.emplace_back(SynthesisStep::Kind::U, 0.0, angles.phi, angles.lambda); break; @@ -287,43 +289,43 @@ struct Unitary1QEulerPlan { } switch (basis) { - case EulerBasis::ZYZ: + case SingleQubitBasis::ZYZ: appendRotation(SynthesisStep::Kind::RZ, angles.lambda); steps.emplace_back(SynthesisStep::Kind::RY, angles.theta); appendRotation(SynthesisStep::Kind::RZ, angles.phi); phase = angles.phase; break; - case EulerBasis::ZXZ: + case SingleQubitBasis::ZXZ: appendRotation(SynthesisStep::Kind::RZ, angles.lambda); steps.emplace_back(SynthesisStep::Kind::RX, angles.theta); appendRotation(SynthesisStep::Kind::RZ, angles.phi); phase = angles.phase; break; - case EulerBasis::XZX: + case SingleQubitBasis::XZX: appendRotation(SynthesisStep::Kind::RX, angles.lambda); steps.emplace_back(SynthesisStep::Kind::RZ, angles.theta); appendRotation(SynthesisStep::Kind::RX, angles.phi); phase = angles.phase; break; - case EulerBasis::XYX: + case SingleQubitBasis::XYX: appendRotation(SynthesisStep::Kind::RX, angles.lambda); steps.emplace_back(SynthesisStep::Kind::RY, angles.theta); appendRotation(SynthesisStep::Kind::RX, angles.phi); phase = angles.phase; break; - case EulerBasis::R: + case SingleQubitBasis::R: appendRStep(angles.lambda, 0.0); steps.emplace_back(SynthesisStep::Kind::R, angles.theta, std::numbers::pi / 2.0); appendRStep(angles.phi, 0.0); phase = angles.phase; break; - case EulerBasis::U: + case SingleQubitBasis::U: steps.emplace_back(SynthesisStep::Kind::U, angles.theta, angles.phi, angles.lambda); phase = angles.phase; break; - case EulerBasis::ZSXX: { + case SingleQubitBasis::ZSXX: { constexpr double pi = std::numbers::pi; constexpr double halfPi = std::numbers::pi / 2.0; constexpr double quarterPi = std::numbers::pi / 4.0; @@ -363,7 +365,8 @@ struct Unitary1QEulerPlan { * @return Planned gate sequence and optional global phase. */ [[nodiscard]] static Unitary1QEulerPlan -planUnitary1QEuler(const Matrix2x2& targetMatrix, const EulerBasis basis) { +planUnitary1QEuler(const Matrix2x2& targetMatrix, + const SingleQubitBasis basis) { Unitary1QEulerPlan plan; if (targetMatrix.isApprox(Matrix2x2::identity())) { return plan; @@ -416,22 +419,23 @@ emitUnitary1QEulerPlan(OpBuilder& builder, Location loc, Value qubit, return {.qubit = qubit, .globalPhase = plan.phase}; } -std::optional parseEulerBasis(StringRef basis) { - return StringSwitch>(basis.lower()) - .Case("zyz", EulerBasis::ZYZ) - .Case("zxz", EulerBasis::ZXZ) - .Case("xzx", EulerBasis::XZX) - .Case("xyx", EulerBasis::XYX) - .Case("u", EulerBasis::U) - .Case("zsxx", EulerBasis::ZSXX) - .Case("r", EulerBasis::R) +std::optional parseSingleQubitBasis(StringRef basis) { + return StringSwitch>(basis.lower()) + .Case("zyz", SingleQubitBasis::ZYZ) + .Case("zxz", SingleQubitBasis::ZXZ) + .Case("xzx", SingleQubitBasis::XZX) + .Case("xyx", SingleQubitBasis::XYX) + .Case("u", SingleQubitBasis::U) + .Case("zsxx", SingleQubitBasis::ZSXX) + .Case("r", SingleQubitBasis::R) .Default(std::nullopt); } std::optional synthesizeUnitary1QEuler(OpBuilder& builder, Location loc, Value qubit, const Matrix2x2& composed, const std::size_t runSize, - const bool hasNonBasisGate, const EulerBasis basis) { + const bool hasNonBasisGate, + const SingleQubitBasis basis) { const Unitary1QEulerPlan plan = planUnitary1QEuler(composed, basis); if (!hasNonBasisGate && runSize <= plan.gateCount()) { return std::nullopt; diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/SynthesisBasis.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/SynthesisBasis.cpp deleted file mode 100644 index fcc8c20543..0000000000 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/SynthesisBasis.cpp +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "mlir/Dialect/QCO/Transforms/Decomposition/SynthesisBasis.h" - -#include "mlir/Compiler/Target.h" -#include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" -#include "mlir/Dialect/QCO/Utils/Matrix.h" - -#include - -#include - -namespace mlir::qco::decomposition { - -using GateKind = CompilerTarget::GateKind; - -[[nodiscard]] static EulerBasis -toEulerBasis(const CompilerTarget::SingleQubitBasis basis) { - switch (basis) { - case CompilerTarget::SingleQubitBasis::U: - return EulerBasis::U; - case CompilerTarget::SingleQubitBasis::ZSXX: - return EulerBasis::ZSXX; - case CompilerTarget::SingleQubitBasis::R: - return EulerBasis::R; - case CompilerTarget::SingleQubitBasis::XZX: - return EulerBasis::XZX; - case CompilerTarget::SingleQubitBasis::XYX: - return EulerBasis::XYX; - case CompilerTarget::SingleQubitBasis::ZYZ: - return EulerBasis::ZYZ; - } - llvm_unreachable("unhandled compiler target single-qubit basis"); -} - -static constexpr Matrix4x4 CANONICAL_CONTROLLED_X = - Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, // row 0 - 0.0, 1.0, 0.0, 0.0, // row 1 - 0.0, 0.0, 0.0, 1.0, // row 2 - 0.0, 0.0, 1.0, 0.0); // row 3 - -static constexpr Matrix4x4 CANONICAL_CONTROLLED_Z = - Matrix4x4::fromDiagonal(1., 1., 1., -1.); - -static const TwoQubitBasisDecomposer& -cachedNativeBasisDecomposer(const GateKind entangler) { - switch (entangler) { - case GateKind::RXX: { - static const TwoQubitBasisDecomposer DECOMPOSER = - TwoQubitBasisDecomposer::create( - RXXOp::unitaryMatrix(std::numbers::pi / 2.0), 1.0); - return DECOMPOSER; - } - case GateKind::RYY: { - static const TwoQubitBasisDecomposer DECOMPOSER = - TwoQubitBasisDecomposer::create( - RYYOp::unitaryMatrix(std::numbers::pi / 2.0), 1.0); - return DECOMPOSER; - } - case GateKind::RZX: { - static const TwoQubitBasisDecomposer DECOMPOSER = - TwoQubitBasisDecomposer::create( - RZXOp::unitaryMatrix(std::numbers::pi / 2.0), 1.0); - return DECOMPOSER; - } - case GateKind::RZZ: { - static const TwoQubitBasisDecomposer DECOMPOSER = - TwoQubitBasisDecomposer::create( - RZZOp::unitaryMatrix(std::numbers::pi / 2.0), 1.0); - return DECOMPOSER; - } - case GateKind::ISWAP: { - static const TwoQubitBasisDecomposer DECOMPOSER = - TwoQubitBasisDecomposer::create(iSWAPOp::getUnitaryMatrix(), 1.0); - return DECOMPOSER; - } - case GateKind::CZ: { - static const TwoQubitBasisDecomposer DECOMPOSER = - TwoQubitBasisDecomposer::create(CANONICAL_CONTROLLED_Z, 1.0); - return DECOMPOSER; - } - case GateKind::CX: { - static const TwoQubitBasisDecomposer DECOMPOSER = - TwoQubitBasisDecomposer::create(CANONICAL_CONTROLLED_X, 1.0); - return DECOMPOSER; - } - case GateKind::ECR: { - static const TwoQubitBasisDecomposer DECOMPOSER = - TwoQubitBasisDecomposer::create(ECROp::getUnitaryMatrix(), 1.0); - return DECOMPOSER; - } - default: - llvm_unreachable( - "only RXX/RYY/RZX/RZZ/ISWAP/CZ/CX/ECR are valid entanglers"); - } -} - -TwoQubitNativeDecomposition -NativeSynthesisBasis::decomposeTarget(const Matrix4x4& target) const { - const auto decomposition = - cachedNativeBasisDecomposer(entangler).decomposeTarget(target); - if (!decomposition) { - llvm::reportFatalInternalError( - "target-selected entangler failed to decompose a two-qubit unitary"); - } - return *decomposition; -} - -NativeSynthesisBasis NativeSynthesisBasis::fromCompilerTarget( - const CompilerTarget::SynthesisBasis basis) { - return NativeSynthesisBasis{.singleQubit = toEulerBasis(basis.singleQubit), - .entangler = basis.entangler}; -} - -} // namespace mlir::qco::decomposition diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp index c410336e3f..414eed5466 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp @@ -13,7 +13,6 @@ #include "mlir/Compiler/Target.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/SynthesisBasis.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" #include @@ -22,7 +21,6 @@ #include #include #include -#include #include #include @@ -73,6 +71,67 @@ struct ChamberState { static constexpr double PI = std::numbers::pi; static constexpr double PI_OVER_4 = PI / 4.0; +static constexpr Matrix4x4 CANONICAL_CONTROLLED_X = + Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, // row 0 + 0.0, 1.0, 0.0, 0.0, // row 1 + 0.0, 0.0, 0.0, 1.0, // row 2 + 0.0, 0.0, 1.0, 0.0); // row 3 +static constexpr Matrix4x4 CANONICAL_CONTROLLED_Z = + Matrix4x4::fromDiagonal(1., 1., 1., -1.); + +static const TwoQubitBasisDecomposer& +cachedNativeBasisDecomposer(const CompilerTarget::GateKind entangler) { + using GateKind = CompilerTarget::GateKind; + switch (entangler) { + case GateKind::RXX: { + static const TwoQubitBasisDecomposer DECOMPOSER = + TwoQubitBasisDecomposer::create( + RXXOp::unitaryMatrix(std::numbers::pi / 2.0), 1.0); + return DECOMPOSER; + } + case GateKind::RYY: { + static const TwoQubitBasisDecomposer DECOMPOSER = + TwoQubitBasisDecomposer::create( + RYYOp::unitaryMatrix(std::numbers::pi / 2.0), 1.0); + return DECOMPOSER; + } + case GateKind::RZX: { + static const TwoQubitBasisDecomposer DECOMPOSER = + TwoQubitBasisDecomposer::create( + RZXOp::unitaryMatrix(std::numbers::pi / 2.0), 1.0); + return DECOMPOSER; + } + case GateKind::RZZ: { + static const TwoQubitBasisDecomposer DECOMPOSER = + TwoQubitBasisDecomposer::create( + RZZOp::unitaryMatrix(std::numbers::pi / 2.0), 1.0); + return DECOMPOSER; + } + case GateKind::ISWAP: { + static const TwoQubitBasisDecomposer DECOMPOSER = + TwoQubitBasisDecomposer::create(iSWAPOp::getUnitaryMatrix(), 1.0); + return DECOMPOSER; + } + case GateKind::CZ: { + static const TwoQubitBasisDecomposer DECOMPOSER = + TwoQubitBasisDecomposer::create(CANONICAL_CONTROLLED_Z, 1.0); + return DECOMPOSER; + } + case GateKind::CX: { + static const TwoQubitBasisDecomposer DECOMPOSER = + TwoQubitBasisDecomposer::create(CANONICAL_CONTROLLED_X, 1.0); + return DECOMPOSER; + } + case GateKind::ECR: { + static const TwoQubitBasisDecomposer DECOMPOSER = + TwoQubitBasisDecomposer::create(ECROp::getUnitaryMatrix(), 1.0); + return DECOMPOSER; + } + default: + llvm_unreachable( + "only RXX/RYY/RZX/RZZ/ISWAP/CZ/CX/ECR are valid entanglers"); + } +} static constexpr Matrix2x2 I_PAULI_X = Matrix2x2::fromElements(0, 1i, 1i, 0); static constexpr Matrix2x2 I_PAULI_Y = Matrix2x2::fromElements(0, 1, -1, 0); @@ -598,9 +657,9 @@ bool TwoQubitWeylDecomposition::applySpecialization( } case Specialization::ControlledEquiv: { const auto [k2ltheta, k2lphi, k2llambda, k2lphase] = - anglesFromUnitary(k2l_, EulerBasis::XYX); + anglesFromUnitary(k2l_, SingleQubitBasis::XYX); const auto [k2rtheta, k2rphi, k2rlambda, k2rphase] = - anglesFromUnitary(k2r_, EulerBasis::XYX); + anglesFromUnitary(k2r_, SingleQubitBasis::XYX); b_ = 0.; c_ = 0.; globalPhase_ = globalPhase_ + k2lphase + k2rphase; @@ -612,9 +671,9 @@ bool TwoQubitWeylDecomposition::applySpecialization( } case Specialization::MirrorControlledEquiv: { const auto [k2ltheta, k2lphi, k2llambda, k2lphase] = - anglesFromUnitary(k2l_, EulerBasis::ZYZ); + anglesFromUnitary(k2l_, SingleQubitBasis::ZYZ); const auto [k2rtheta, k2rphi, k2rlambda, k2rphase] = - anglesFromUnitary(k2r_, EulerBasis::ZYZ); + anglesFromUnitary(k2r_, SingleQubitBasis::ZYZ); a_ = PI_OVER_4; b_ = PI_OVER_4; globalPhase_ = globalPhase_ + k2lphase + k2rphase; @@ -626,7 +685,7 @@ bool TwoQubitWeylDecomposition::applySpecialization( } case Specialization::FSimaabEquiv: { const auto [k2ltheta, k2lphi, k2llambda, k2lphase] = - anglesFromUnitary(k2l_, EulerBasis::ZYZ); + anglesFromUnitary(k2l_, SingleQubitBasis::ZYZ); const auto ab = (a_ + b_) / 2.; a_ = ab; b_ = ab; @@ -639,7 +698,7 @@ bool TwoQubitWeylDecomposition::applySpecialization( } case Specialization::FSimabbEquiv: { const auto [k2ltheta, k2lphi, k2llambda, k2lphase] = - anglesFromUnitary(k2l_, EulerBasis::XYX); + anglesFromUnitary(k2l_, SingleQubitBasis::XYX); const auto bc = (b_ + c_) / 2.; b_ = bc; c_ = bc; @@ -652,7 +711,7 @@ bool TwoQubitWeylDecomposition::applySpecialization( } case Specialization::FSimabmbEquiv: { const auto [k2ltheta, k2lphi, k2llambda, k2lphase] = - anglesFromUnitary(k2l_, EulerBasis::XYX); + anglesFromUnitary(k2l_, SingleQubitBasis::XYX); const auto bc = (b_ - c_) / 2.; b_ = bc; c_ = -bc; @@ -669,22 +728,32 @@ bool TwoQubitWeylDecomposition::applySpecialization( return flippedFromOriginal; } -FailureOr -synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, Value qubit0, - Value qubit1, const Matrix4x4& target, - const NativeSynthesisBasis& basis, - const bool reverseEntanglerOperands) { - const auto native = basis.decomposeTarget(target); - double globalPhase = native.globalPhase; +TwoQubitNativeDecomposition +decomposeUnitary2QWeyl(const Matrix4x4& target, + const CompilerTarget::GateKind entangler) { + auto decomposition = + cachedNativeBasisDecomposer(entangler).decomposeTarget(target); + if (!decomposition) { + llvm::reportFatalInternalError( + "target-selected entangler failed to decompose a two-qubit unitary"); + } + return std::move(*decomposition); +} + +SynthesizedUnitary2Q +emitUnitary2QWeyl(OpBuilder& builder, Location loc, Value qubit0, Value qubit1, + const TwoQubitNativeDecomposition& decomposition, + const CompilerTarget::SynthesisBasis basis) { + double globalPhase = decomposition.globalPhase; Value wire0 = qubit0; Value wire1 = qubit1; - const auto& factors = native.singleQubitFactors; - const std::uint8_t numBasisUses = native.numBasisUses; + const auto& factors = decomposition.singleQubitFactors; + const std::uint8_t numBasisUses = decomposition.numBasisUses; const std::size_t requiredFactors = singleQubitFactorCount(numBasisUses); if (factors.size() != requiredFactors) { llvm::reportFatalInternalError(llvm::formatv( - "synthesizeUnitary2QWeyl: expected {0} single-qubit factors for " + "emitUnitary2QWeyl: expected {0} single-qubit factors for " "numBasisUses = {1}, got {2}", requiredFactors, numBasisUses, factors.size())); } @@ -694,7 +763,7 @@ synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, Value qubit0, /*hasNonBasisGate=*/true, basis.singleQubit); if (!synthesized) { llvm::reportFatalInternalError(llvm::formatv( - "synthesizeUnitary2QWeyl: euler synthesis failed for factor index " + "emitUnitary2QWeyl: euler synthesis failed for factor index " "{0} (layer {1}, qubit {2})", index, index / 2, (index % 2 == 0) ? 1 : 0)); } @@ -702,62 +771,55 @@ synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, Value qubit0, globalPhase += synthesized->globalPhase; }; const auto emitEntangler = [&]() { - Value& entanglerWire0 = reverseEntanglerOperands ? wire1 : wire0; - Value& entanglerWire1 = reverseEntanglerOperands ? wire0 : wire1; if (basis.entangler == CompilerTarget::GateKind::RXX) { - auto rxxOp = - RXXOp::create(builder, loc, entanglerWire0, entanglerWire1, PI / 2.0); - entanglerWire0 = rxxOp.getOutputQubit(0); - entanglerWire1 = rxxOp.getOutputQubit(1); + auto rxxOp = RXXOp::create(builder, loc, wire0, wire1, PI / 2.0); + wire0 = rxxOp.getOutputQubit(0); + wire1 = rxxOp.getOutputQubit(1); return; } if (basis.entangler == CompilerTarget::GateKind::RYY) { - auto ryyOp = - RYYOp::create(builder, loc, entanglerWire0, entanglerWire1, PI / 2.0); - entanglerWire0 = ryyOp.getOutputQubit(0); - entanglerWire1 = ryyOp.getOutputQubit(1); + auto ryyOp = RYYOp::create(builder, loc, wire0, wire1, PI / 2.0); + wire0 = ryyOp.getOutputQubit(0); + wire1 = ryyOp.getOutputQubit(1); return; } if (basis.entangler == CompilerTarget::GateKind::RZX) { - auto rzxOp = - RZXOp::create(builder, loc, entanglerWire0, entanglerWire1, PI / 2.0); - entanglerWire0 = rzxOp.getOutputQubit(0); - entanglerWire1 = rzxOp.getOutputQubit(1); + auto rzxOp = RZXOp::create(builder, loc, wire0, wire1, PI / 2.0); + wire0 = rzxOp.getOutputQubit(0); + wire1 = rzxOp.getOutputQubit(1); return; } if (basis.entangler == CompilerTarget::GateKind::RZZ) { - auto rzzOp = - RZZOp::create(builder, loc, entanglerWire0, entanglerWire1, PI / 2.0); - entanglerWire0 = rzzOp.getOutputQubit(0); - entanglerWire1 = rzzOp.getOutputQubit(1); + auto rzzOp = RZZOp::create(builder, loc, wire0, wire1, PI / 2.0); + wire0 = rzzOp.getOutputQubit(0); + wire1 = rzzOp.getOutputQubit(1); return; } if (basis.entangler == CompilerTarget::GateKind::ISWAP) { - auto iswapOp = - iSWAPOp::create(builder, loc, entanglerWire0, entanglerWire1); - entanglerWire0 = iswapOp.getOutputQubit(0); - entanglerWire1 = iswapOp.getOutputQubit(1); + auto iswapOp = iSWAPOp::create(builder, loc, wire0, wire1); + wire0 = iswapOp.getOutputQubit(0); + wire1 = iswapOp.getOutputQubit(1); return; } if (basis.entangler == CompilerTarget::GateKind::CZ || basis.entangler == CompilerTarget::GateKind::CX) { const bool emitCz = basis.entangler == CompilerTarget::GateKind::CZ; - auto ctrlOp = CtrlOp::create( - builder, loc, entanglerWire0, entanglerWire1, [&](Value targetQubit) { + auto ctrlOp = + CtrlOp::create(builder, loc, wire0, wire1, [&](Value targetQubit) { if (emitCz) { return ZOp::create(builder, loc, targetQubit).getOutputQubit(0); } return XOp::create(builder, loc, targetQubit).getOutputQubit(0); }); - entanglerWire0 = ctrlOp.getOutputControl(0); - entanglerWire1 = ctrlOp.getOutputTarget(0); + wire0 = ctrlOp.getOutputControl(0); + wire1 = ctrlOp.getOutputTarget(0); return; } assert(basis.entangler == CompilerTarget::GateKind::ECR && "emitEntangler: unexpected compiler target gate"); - auto ecrOp = ECROp::create(builder, loc, entanglerWire0, entanglerWire1); - entanglerWire0 = ecrOp.getOutputQubit(0); - entanglerWire1 = ecrOp.getOutputQubit(1); + auto ecrOp = ECROp::create(builder, loc, wire0, wire1); + wire0 = ecrOp.getOutputQubit(0); + wire1 = ecrOp.getOutputQubit(1); }; for (std::uint8_t layer = 0; layer <= numBasisUses; ++layer) { diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp index d5121617c7..4e69f2572b 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp @@ -68,27 +68,29 @@ static std::optional getRunMemberMatrix(UnitaryOpInterface gate) { * @brief Whether `op` is a gate that Euler synthesis emits for `basis`. * * @param op The operation to classify. - * @param basis The target Euler basis. + * @param basis The single-qubit synthesis basis. * @return Whether `op` is in the gate set for `basis`. */ static bool isTargetBasisGate(Operation* op, - const decomposition::EulerBasis basis) { - using decomposition::EulerBasis; + const decomposition::SingleQubitBasis basis) { + using decomposition::SingleQubitBasis; return TypeSwitch(op) .Case([&](auto) { - return basis == EulerBasis::ZYZ || basis == EulerBasis::ZXZ || - basis == EulerBasis::XZX || basis == EulerBasis::ZSXX; + return basis == SingleQubitBasis::ZYZ || + basis == SingleQubitBasis::ZXZ || + basis == SingleQubitBasis::XZX || + basis == SingleQubitBasis::ZSXX; }) .Case([&](auto) { - return basis == EulerBasis::ZYZ || basis == EulerBasis::XYX; + return basis == SingleQubitBasis::ZYZ || basis == SingleQubitBasis::XYX; }) .Case([&](auto) { - return basis == EulerBasis::ZXZ || basis == EulerBasis::XZX || - basis == EulerBasis::XYX; + return basis == SingleQubitBasis::ZXZ || + basis == SingleQubitBasis::XZX || basis == SingleQubitBasis::XYX; }) - .Case([&](auto) { return basis == EulerBasis::U; }) - .Case([&](auto) { return basis == EulerBasis::ZSXX; }) - .Case([&](auto) { return basis == EulerBasis::R; }) + .Case([&](auto) { return basis == SingleQubitBasis::U; }) + .Case([&](auto) { return basis == SingleQubitBasis::ZSXX; }) + .Case([&](auto) { return basis == SingleQubitBasis::R; }) .Default([](auto) { return false; }); } @@ -97,12 +99,12 @@ static bool isTargetBasisGate(Operation* op, * * @param head First gate of the run. * @param headMatrix Matrix already obtained while identifying the run head. - * @param basis Target Euler basis. + * @param basis Single-qubit synthesis basis. * @return Composed matrix, gate count, and run tail. */ -static FusableRunScan scanFusableRun(UnitaryOpInterface head, - const Matrix2x2& headMatrix, - const decomposition::EulerBasis basis) { +static FusableRunScan +scanFusableRun(UnitaryOpInterface head, const Matrix2x2& headMatrix, + const decomposition::SingleQubitBasis basis) { FusableRunScan scan; for (auto* op : WireRange(head.getOutputTarget(0))) { auto member = dyn_cast_or_null(op); @@ -151,12 +153,12 @@ namespace { struct FuseSingleQubitUnitaryRunsPattern final : OpInterfaceRewritePattern { FuseSingleQubitUnitaryRunsPattern(MLIRContext* context, - const decomposition::EulerBasis basis, + const decomposition::SingleQubitBasis basis, const bool skipControlledBodies) : OpInterfaceRewritePattern(context), basis(basis), skipControlledBodies(skipControlledBodies) {} - decomposition::EulerBasis basis; + decomposition::SingleQubitBasis basis; bool skipControlledBodies; /** @@ -220,10 +222,10 @@ struct FuseSingleQubitUnitaryRunsPass final void runOnOperation() override { auto moduleOp = getOperation(); - const auto parsed = decomposition::parseEulerBasis(basis); + const auto parsed = decomposition::parseSingleQubitBasis(basis); if (!parsed) { moduleOp.emitError() - << "Invalid Euler basis '" << basis + << "Invalid single-qubit synthesis basis '" << basis << "'. Expected one of: zyz, zxz, xzx, xyx, u, zsxx, r."; signalPassFailure(); return; @@ -247,7 +249,7 @@ struct FuseSingleQubitUnitaryRunsPass final namespace mlir::qco::decomposition { void populateFuseSingleQubitUnitaryRunsPatterns( - RewritePatternSet& patterns, const EulerBasis basis, + RewritePatternSet& patterns, const SingleQubitBasis basis, const bool skipControlledBodies) { patterns.add(patterns.getContext(), basis, skipControlledBodies); diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp index eb81f7fc41..a22657db1b 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp @@ -13,12 +13,12 @@ #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/SynthesisBasis.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" #include "mlir/Dialect/Utils/Transforms/GlobalPhaseNormalization.h" +#include #include #include #include @@ -37,9 +37,7 @@ #include #include #include -#include -#include #include #include #include @@ -48,8 +46,8 @@ namespace mlir::qco { -using decomposition::NativeSynthesisBasis; -using decomposition::synthesizeUnitary2QWeyl; +using decomposition::decomposeUnitary2QWeyl; +using decomposition::emitUnitary2QWeyl; namespace { @@ -97,44 +95,47 @@ static bool assignTwoQubitOpMatrix(Operation* op, Matrix4x4& matrix) { return unitary.getUnitaryMatrix4x4(matrix); } -/// Whether `unitary` is a single-qubit gate that can join a run. -static bool isOneQubitRunMember(UnitaryOpInterface unitary) { +/// Return the constant matrix when `unitary` is a single-qubit run member. +static std::optional +oneQubitRunMemberMatrix(UnitaryOpInterface unitary) { if (!unitary || !unitary.isSingleQubit() || !isWalkableUnitaryShell(unitary.getOperation())) { - return false; + return std::nullopt; } Matrix2x2 matrix; - return unitary.getUnitaryMatrix2x2(matrix); + if (!unitary.getUnitaryMatrix2x2(matrix)) { + return std::nullopt; + } + return matrix; } -/// Whether `unitary` is a two-qubit gate that can join a run. -static bool isTwoQubitRunMember(UnitaryOpInterface unitary) { +/// Return the constant matrix when `unitary` is a two-qubit run member. +static std::optional +twoQubitRunMemberMatrix(UnitaryOpInterface unitary) { if (!unitary || !unitary.isTwoQubit() || !isWalkableUnitaryShell(unitary.getOperation())) { - return false; + return std::nullopt; } Matrix4x4 matrix; - return assignTwoQubitOpMatrix(unitary.getOperation(), matrix); + if (!assignTwoQubitOpMatrix(unitary.getOperation(), matrix)) { + return std::nullopt; + } + return matrix; } // --- Wire navigation ------------------------------------------------------ // -/// The sole run-member consumer of `wire`, or a null interface when its unique -/// user cannot join a run. `wire` is single-use by qubit linearity. +/// The sole walkable one- or two-qubit consumer of `wire`, or a null interface. +/// `wire` is single-use by qubit linearity. static UnitaryOpInterface uniqueUnitaryUser(Value wire) { assert(wire.hasOneUse() && "qubit values are single-use, so a run tail has exactly one user"); auto unitary = dyn_cast(*wire.user_begin()); - if (!unitary) { + if (!unitary || !isWalkableUnitaryShell(unitary.getOperation()) || + (!unitary.isSingleQubit() && !unitary.isTwoQubit())) { return {}; } - if (unitary.isTwoQubit()) { - return isTwoQubitRunMember(unitary) ? unitary : UnitaryOpInterface{}; - } - if (unitary.isSingleQubit()) { - return isOneQubitRunMember(unitary) ? unitary : UnitaryOpInterface{}; - } - return {}; + return unitary; } /// Traces `wire` upstream through single-qubit gates to the two-qubit run @@ -147,9 +148,9 @@ static Operation* twoQubitGateAtEndOfOneQChain(Value wire) { return nullptr; } if (unitary.isTwoQubit()) { - return isTwoQubitRunMember(unitary) ? def : nullptr; + return twoQubitRunMemberMatrix(unitary) ? def : nullptr; } - if (!isOneQubitRunMember(unitary)) { + if (!oneQubitRunMemberMatrix(unitary)) { return nullptr; } cur = unitary.getInputQubit(0); @@ -176,11 +177,8 @@ static bool feedsFromSameTwoQubitRun(UnitaryOpInterface op) { /// of `op`'s inputs are the run's current tail wires (in either order), keeping /// the run confined to a single pair of wires. static void absorbTwoQubitIntoRun(FusableTwoQubitRun& run, - UnitaryOpInterface op) { - Matrix4x4 opMatrix; - [[maybe_unused]] const bool assigned = - assignTwoQubitOpMatrix(op.getOperation(), opMatrix); - assert(assigned && "a two-qubit run member always exposes a 4x4 matrix"); + UnitaryOpInterface op, + const Matrix4x4& opMatrix) { const Value in0 = op.getInputQubit(0); const Value in1 = op.getInputQubit(1); size_t id0 = 0; @@ -204,11 +202,10 @@ static void absorbTwoQubitIntoRun(FusableTwoQubitRun& run, /// Appends a single-qubit gate on run wire `wireIndex` (0 = A, 1 = B). static void absorbOneQubitIntoRun(FusableTwoQubitRun& run, - UnitaryOpInterface op, unsigned wireIndex) { - Matrix2x2 raw; - [[maybe_unused]] const bool assigned = op.getUnitaryMatrix2x2(raw); - assert(assigned && "a single-qubit run member always exposes a 2x2 matrix"); - run.composed.premultiplyBy(raw.embedInTwoQubit(wireIndex)); + UnitaryOpInterface op, + const Matrix2x2& opMatrix, + unsigned wireIndex) { + run.composed.premultiplyBy(opMatrix.embedInTwoQubit(wireIndex)); run.ops.push_back(op.getOperation()); (wireIndex == 0 ? run.tailA : run.tailB) = op.getOutputQubit(0); } @@ -217,11 +214,10 @@ static void absorbOneQubitIntoRun(FusableTwoQubitRun& run, /// a following two-qubit gate when it keeps both run wires together, otherwise /// the single-qubit gate first in program order; stops at the first boundary /// that would split the run's two wires. -static FusableTwoQubitRun scanFusableTwoQubitRun(UnitaryOpInterface head) { +static FusableTwoQubitRun scanFusableTwoQubitRun(UnitaryOpInterface head, + const Matrix4x4& headMatrix) { FusableTwoQubitRun run; - [[maybe_unused]] const bool assigned = - assignTwoQubitOpMatrix(head.getOperation(), run.composed); - assert(assigned && "a run head is a two-qubit member with a 4x4 matrix"); + run.composed = headMatrix; run.tailA = head.getOutputQubit(0); run.tailB = head.getOutputQubit(1); run.ops.push_back(head.getOperation()); @@ -234,21 +230,29 @@ static FusableTwoQubitRun scanFusableTwoQubitRun(UnitaryOpInterface head) { nextOnA && nextOnB && nextOnA.getOperation() == nextOnB.getOperation(); if (sameOp && nextOnA.isTwoQubit()) { - absorbTwoQubitIntoRun(run, nextOnA); + const auto matrix = twoQubitRunMemberMatrix(nextOnA); + if (!matrix) { + break; + } + absorbTwoQubitIntoRun(run, nextOnA, *matrix); continue; } - const bool aSingle = nextOnA && nextOnA.isSingleQubit() && !sameOp; - const bool bSingle = nextOnB && nextOnB.isSingleQubit() && !sameOp; + const auto matrixA = + !sameOp ? oneQubitRunMemberMatrix(nextOnA) : std::nullopt; + const auto matrixB = + !sameOp ? oneQubitRunMemberMatrix(nextOnB) : std::nullopt; + const bool aSingle = matrixA.has_value(); + const bool bSingle = matrixB.has_value(); if (aSingle && bSingle && nextOnA->getBlock() != nextOnB->getBlock()) { break; } if (aSingle && (!bSingle || nextOnA->isBeforeInBlock(nextOnB))) { - absorbOneQubitIntoRun(run, nextOnA, /*wireIndex=*/0); + absorbOneQubitIntoRun(run, nextOnA, *matrixA, /*wireIndex=*/0); continue; } if (bSingle) { - absorbOneQubitIntoRun(run, nextOnB, /*wireIndex=*/1); + absorbOneQubitIntoRun(run, nextOnB, *matrixB, /*wireIndex=*/1); continue; } break; @@ -257,80 +261,62 @@ static FusableTwoQubitRun scanFusableTwoQubitRun(UnitaryOpInterface head) { } /// Erases all run members, successors first so each is dead when erased. -static void eraseFusableRun(PatternRewriter& rewriter, +static void eraseFusableRun(RewriterBase& rewriter, const FusableTwoQubitRun& run) { for (Operation* member : llvm::reverse(run.ops)) { rewriter.eraseOp(member); } } -namespace { - /// Fuses a maximal constant run only when generic resynthesis strictly reduces /// its two-qubit operation count. -struct OptimizeTwoQubitUnitaryRunsPattern final - : OpInterfaceRewritePattern { - OptimizeTwoQubitUnitaryRunsPattern(MLIRContext* ctx, - NativeSynthesisBasis basisIn, - bool& changedIn) - : OpInterfaceRewritePattern(ctx), basis(basisIn), changed(&changedIn) {} - - NativeSynthesisBasis basis; - bool* changed; - - /// Whether `op` anchors a run: a two-qubit run member whose two wires are not - /// both fed by the same earlier run (which would make it a continuation). - static bool isRunStart(UnitaryOpInterface op) { - return isTwoQubitRunMember(op) && !feedsFromSameTwoQubitRun(op); +static bool optimizeTwoQubitRun(IRRewriter& rewriter, UnitaryOpInterface head, + const Matrix4x4& headMatrix, + const CompilerTarget::SynthesisBasis basis) { + FusableTwoQubitRun run = scanFusableTwoQubitRun(head, headMatrix); + if (run.ops.size() < 2) { + return false; } - /// Fuse the run anchored at `op` only if it has multiple constant members and - /// Weyl resynthesis uses fewer entanglers than the original run. - LogicalResult matchAndRewrite(UnitaryOpInterface op, - PatternRewriter& rewriter) const override { - if (!isRunStart(op)) { - return failure(); - } - - FusableTwoQubitRun run = scanFusableTwoQubitRun(op); - if (run.ops.size() < 2) { - return failure(); - } - - const auto native = basis.decomposeTarget(run.composed); - if (native.numBasisUses >= run.numTwoQ) { - return failure(); - } - - auto firstOp = cast(run.ops.front()); - rewriter.setInsertionPoint(firstOp); - const auto synthesized = synthesizeUnitary2QWeyl( - rewriter, firstOp.getLoc(), firstOp.getInputQubit(0), - firstOp.getInputQubit(1), run.composed, basis); - if (failed(synthesized)) { - firstOp->emitError("failed to emit synthesized two-qubit gate sequence"); - return failure(); - } - decomposition::emitGPhaseIfNeeded(rewriter, firstOp.getLoc(), - synthesized->globalPhase); - rewriter.replaceAllUsesWith(run.tailA, synthesized->qubit0); - rewriter.replaceAllUsesWith(run.tailB, synthesized->qubit1); - eraseFusableRun(rewriter, run); - *changed = true; - return success(); + const auto native = decomposeUnitary2QWeyl(run.composed, basis.entangler); + if (native.numBasisUses >= run.numTwoQ) { + return false; } -}; -} // namespace + auto firstOp = cast(run.ops.front()); + rewriter.setInsertionPoint(firstOp); + const auto synthesized = + emitUnitary2QWeyl(rewriter, firstOp.getLoc(), firstOp.getInputQubit(0), + firstOp.getInputQubit(1), native, basis); + decomposition::emitGPhaseIfNeeded(rewriter, firstOp.getLoc(), + synthesized.globalPhase); + rewriter.replaceAllUsesWith(run.tailA, synthesized.qubit0); + rewriter.replaceAllUsesWith(run.tailB, synthesized.qubit1); + eraseFusableRun(rewriter, run); + return true; +} using SiteId = CompilerTarget::SiteId; -static FailureOr resolveProviderSite(Value value) { +static FailureOr +resolveProviderSite(Value value, DenseMap& resolvedSites) { llvm::SmallDenseSet visited; + SmallVector path; + const auto remember = [&](const SiteId site) { + for (const Value traversed : path) { + resolvedSites.try_emplace(traversed, site); + } + return site; + }; while (value) { + if (const auto cached = resolvedSites.find(value); + cached != resolvedSites.end()) { + return remember(cached->second); + } if (!visited.insert(value).second) { return failure(); } + path.emplace_back(value); if (auto argument = dyn_cast(value)) { Operation* const parent = argument.getOwner()->getParentOp(); @@ -360,7 +346,7 @@ static FailureOr resolveProviderSite(Value value) { Operation* const definingOp = value.getDefiningOp(); if (auto staticOp = dyn_cast_or_null(definingOp)) { - return static_cast(staticOp.getIndex()); + return remember(static_cast(staticOp.getIndex())); } if (auto unitary = dyn_cast_or_null(definingOp)) { value = unitary.getInputForOutput(value); @@ -419,7 +405,8 @@ static FailureOr resolveProviderSite(Value value) { return failure(); } -static FailureOr> providerLocus(Operation* operation) { +static FailureOr> +providerLocus(Operation* operation, DenseMap& resolvedSites) { SmallVector qubits; if (auto unitary = dyn_cast(operation)) { llvm::append_range(qubits, unitary.getInputQubits()); @@ -434,7 +421,7 @@ static FailureOr> providerLocus(Operation* operation) { SmallVector locus; locus.reserve(qubits.size()); for (const Value qubit : qubits) { - const auto site = resolveProviderSite(qubit); + const auto site = resolveProviderSite(qubit, resolvedSites); if (failed(site)) { return failure(); } @@ -449,24 +436,6 @@ static bool requiresTargetSynthesis(Operation* operation, return !target.supports(operation, locus); } -/// Whether a target-selected entangler must use the reverse provider locus. -/// -/// `CompilerTarget` selects an operand-symmetric gate as globally usable when -/// either ordered orientation is available. Preserve the logical wire order -/// while emitting that gate at the provider-supported orientation. -static bool useReverseEntanglerLocus(const CompilerTarget& target, - const CompilerTarget::GateKind entangler, - const ArrayRef locus) { - assert(locus.size() == 2 && "a synthesis entangler has two provider sites"); - if (target.supports(entangler, locus)) { - return false; - } - const std::array reverseLocus{locus[1], locus[0]}; - assert(target.supports(entangler, reverseLocus) && - "a globally selected entangler is available in one orientation"); - return true; -} - static void appendProviderLocus(InFlightDiagnostic& diagnostic, const ArrayRef locus) { diagnostic << '['; @@ -486,132 +455,116 @@ struct SynthesisNeed { SmallVector locus; }; +struct SynthesisPlan { + std::optional firstNeed; + std::optional matrixUnavailable; + SmallVector operations; +}; + } // namespace -static std::optional -findSynthesisNeed(Operation* root, const CompilerTarget& target) { - std::optional need; +static SynthesisPlan planTargetSynthesis(Operation* root, + const CompilerTarget& target) { + SynthesisPlan plan; + DenseMap resolvedSites; root->walk([&](Operation* operation) { auto unitary = dyn_cast(operation); if (!unitary || !isWalkableUnitaryShell(operation) || (unitary.getNumQubits() != 1 && unitary.getNumQubits() != 2)) { return WalkResult::advance(); } - auto locus = providerLocus(operation); + auto locus = providerLocus(operation, resolvedSites); if (failed(locus) || !requiresTargetSynthesis(operation, target, *locus)) { return WalkResult::advance(); } - need.emplace( + if (!plan.firstNeed) { + plan.firstNeed.emplace( + SynthesisNeed{.operation = operation, .locus = *locus}); + } + + if (unitary.isSingleQubit()) { + Matrix2x2 matrix; + if (unitary.getUnitaryMatrix2x2(matrix)) { + plan.operations.emplace_back(operation); + return WalkResult::advance(); + } + } else { + Matrix4x4 matrix; + if (assignTwoQubitOpMatrix(operation, matrix)) { + plan.operations.emplace_back(operation); + return WalkResult::advance(); + } + } + plan.matrixUnavailable.emplace( SynthesisNeed{.operation = operation, .locus = std::move(*locus)}); return WalkResult::interrupt(); }); - return need; + return plan; } -namespace { - -struct LowerTargetSingleQubitOpPattern final - : OpInterfaceRewritePattern { - LowerTargetSingleQubitOpPattern(MLIRContext* context, - const CompilerTarget& targetIn, - NativeSynthesisBasis basisIn) - : OpInterfaceRewritePattern(context), target(targetIn), basis(basisIn) {} - - CompilerTarget target; - NativeSynthesisBasis basis; - - LogicalResult matchAndRewrite(UnitaryOpInterface op, - PatternRewriter& rewriter) const override { - Operation* const operation = op.getOperation(); - if (!op.isSingleQubit() || !isWalkableUnitaryShell(operation)) { - return failure(); - } - const auto locus = providerLocus(operation); - if (failed(locus) || !requiresTargetSynthesis(operation, target, *locus)) { - return failure(); - } +static void lowerTargetOperation(IRRewriter& rewriter, UnitaryOpInterface op, + const CompilerTarget::SynthesisBasis basis) { + Operation* const operation = op.getOperation(); + rewriter.setInsertionPoint(operation); + if (op.isSingleQubit()) { Matrix2x2 matrix; - if (!op.getUnitaryMatrix2x2(matrix)) { - return failure(); - } - - rewriter.setInsertionPoint(operation); + op.getUnitaryMatrix2x2(matrix); const auto synthesized = decomposition::synthesizeUnitary1QEuler( rewriter, operation->getLoc(), op.getInputQubit(0), matrix, /*runSize=*/1, /*hasNonBasisGate=*/true, basis.singleQubit); if (!synthesized) { - return failure(); + llvm::reportFatalInternalError( + "target single-qubit basis failed to synthesize a unitary matrix"); } decomposition::emitGPhaseIfNeeded(rewriter, operation->getLoc(), synthesized->globalPhase); rewriter.replaceOp(operation, synthesized->qubit); - return success(); + return; } -}; - -struct LowerTargetTwoQubitOpPattern final - : OpInterfaceRewritePattern { - LowerTargetTwoQubitOpPattern(MLIRContext* context, - const CompilerTarget& targetIn, - NativeSynthesisBasis basisIn) - : OpInterfaceRewritePattern(context), target(targetIn), basis(basisIn) {} - CompilerTarget target; - NativeSynthesisBasis basis; + Matrix4x4 matrix; + assignTwoQubitOpMatrix(operation, matrix); + Value input0; + Value input1; + if (auto ctrl = dyn_cast(operation)) { + input0 = ctrl.getInputControl(0); + input1 = ctrl.getInputTarget(0); + } else { + input0 = op.getInputQubit(0); + input1 = op.getInputQubit(1); + } - LogicalResult matchAndRewrite(UnitaryOpInterface op, - PatternRewriter& rewriter) const override { - Operation* const operation = op.getOperation(); - if (!op.isTwoQubit() || !isWalkableUnitaryShell(operation)) { - return failure(); - } - const auto locus = providerLocus(operation); - if (failed(locus) || !requiresTargetSynthesis(operation, target, *locus)) { - return failure(); - } - Matrix4x4 matrix; - if (!assignTwoQubitOpMatrix(operation, matrix)) { - return failure(); - } + const auto native = decomposeUnitary2QWeyl(matrix, basis.entangler); + const auto synthesized = emitUnitary2QWeyl(rewriter, operation->getLoc(), + input0, input1, native, basis); + decomposition::emitGPhaseIfNeeded(rewriter, operation->getLoc(), + synthesized.globalPhase); + rewriter.replaceOp(operation, + ValueRange{synthesized.qubit0, synthesized.qubit1}); +} - Value input0; - Value input1; - if (auto ctrl = dyn_cast(operation)) { - input0 = ctrl.getInputControl(0); - input1 = ctrl.getInputTarget(0); - } else { - input0 = op.getInputQubit(0); - input1 = op.getInputQubit(1); - } +static LogicalResult optimizeTwoQubitRuns(ModuleOp moduleOp) { + constexpr CompilerTarget::SynthesisBasis basis{ + .singleQubit = CompilerTarget::SingleQubitBasis::U, + .entangler = CompilerTarget::GateKind::CZ}; - rewriter.setInsertionPoint(operation); - const auto synthesized = synthesizeUnitary2QWeyl( - rewriter, operation->getLoc(), input0, input1, matrix, basis, - useReverseEntanglerLocus(target, basis.entangler, *locus)); - if (failed(synthesized)) { - return failure(); + SmallVector runHeads; + moduleOp.walk([&](Operation* operation) { + auto unitary = dyn_cast(operation); + const auto matrix = twoQubitRunMemberMatrix(unitary); + if (matrix && !feedsFromSameTwoQubitRun(unitary)) { + runHeads.emplace_back(operation); } - decomposition::emitGPhaseIfNeeded(rewriter, operation->getLoc(), - synthesized->globalPhase); - rewriter.replaceOp(operation, - ValueRange{synthesized->qubit0, synthesized->qubit1}); - return success(); - } -}; - -} // namespace + }); -static LogicalResult optimizeTwoQubitRuns(ModuleOp moduleOp) { - const auto basis = - NativeSynthesisBasis::fromCompilerTarget(CompilerTarget::SynthesisBasis{ - .singleQubit = CompilerTarget::SingleQubitBasis::U, - .entangler = CompilerTarget::GateKind::CX}); bool changed = false; - RewritePatternSet patterns(moduleOp.getContext()); - patterns.add(moduleOp.getContext(), basis, - changed); - if (failed(applyPatternsGreedily(moduleOp, std::move(patterns)))) { - return failure(); + IRRewriter rewriter(moduleOp.getContext()); + for (Operation* operation : runHeads) { + auto unitary = cast(operation); + const auto matrix = twoQubitRunMemberMatrix(unitary); + if (matrix) { + changed |= optimizeTwoQubitRun(rewriter, unitary, *matrix, basis); + } } if (!changed) { return success(); @@ -655,42 +608,38 @@ struct TargetNativeSynthesisPass final return; } ModuleOp moduleOp = getOperation(); - auto need = findSynthesisNeed(moduleOp, target); - if (!need) { + const auto plan = planTargetSynthesis(moduleOp, target); + if (!plan.firstNeed) { return; } const auto targetBasis = target.synthesisBasis(); if (!targetBasis) { - auto diagnostic = need->operation->emitError() + auto diagnostic = plan.firstNeed->operation->emitError() << "target-native synthesis cannot lower operation '" - << need->operation->getName() + << plan.firstNeed->operation->getName() << "' at ordered provider locus "; - appendProviderLocus(diagnostic, need->locus); + appendProviderLocus(diagnostic, plan.firstNeed->locus); diagnostic << ": the target has no globally usable synthesis basis"; signalPassFailure(); return; } - const auto basis = NativeSynthesisBasis::fromCompilerTarget(*targetBasis); - RewritePatternSet patterns(&getContext()); - patterns.add( - &getContext(), target, basis); - if (failed(applyPatternsGreedily(moduleOp, std::move(patterns)))) { - signalPassFailure(); - return; - } - - need = findSynthesisNeed(moduleOp, target); - if (need) { - auto diagnostic = need->operation->emitError() + if (plan.matrixUnavailable) { + auto diagnostic = plan.matrixUnavailable->operation->emitError() << "target-native synthesis cannot lower operation '" - << need->operation->getName() + << plan.matrixUnavailable->operation->getName() << "' at ordered provider locus "; - appendProviderLocus(diagnostic, need->locus); + appendProviderLocus(diagnostic, plan.matrixUnavailable->locus); diagnostic << ": its unitary matrix is not available at compile time"; signalPassFailure(); return; } + + IRRewriter rewriter(&getContext()); + for (Operation* operation : plan.operations) { + lowerTargetOperation(rewriter, cast(operation), + *targetBasis); + } if (failed(mlir::mqt::normalizeGlobalPhases(moduleOp))) { signalPassFailure(); } @@ -708,6 +657,7 @@ struct VerifyTargetConformancePass final protected: void runOnOperation() override { + DenseMap resolvedSites; WalkResult result = getOperation()->walk([&](Operation* operation) { size_t parameterCount = 0; if (auto unitary = dyn_cast(operation)) { @@ -719,7 +669,7 @@ struct VerifyTargetConformancePass final return WalkResult::advance(); } - const auto locus = providerLocus(operation); + const auto locus = providerLocus(operation, resolvedSites); if (failed(locus)) { operation->emitError() << "target conformance requires every hardware qubit operand to " diff --git a/mlir/unittests/Compiler/test_compiler_target.cpp b/mlir/unittests/Compiler/test_compiler_target.cpp index a4f9462a84..c5c9661b1f 100644 --- a/mlir/unittests/Compiler/test_compiler_target.cpp +++ b/mlir/unittests/Compiler/test_compiler_target.cpp @@ -238,7 +238,7 @@ TEST(CompilerTargetTest, DistinguishesAbsentAndEmptyOperationSets) { EXPECT_FALSE(closed.synthesisBasis()); } -TEST(CompilerTargetTest, PreservesOrderedLociAndResolvesGlobalBasis) { +TEST(CompilerTargetTest, PreservesRawLociAndResolvesBidirectionalBasis) { const std::vector chain{{0, 1}, {1, 2}}; const Operation globalU{"U3", 1, 3}; const Operation symmetricCZ{ @@ -247,8 +247,11 @@ TEST(CompilerTargetTest, PreservesOrderedLociAndResolvesGlobalBasis) { EXPECT_TRUE(symmetric.supportsOperation("u", {0}, 3)); EXPECT_TRUE(symmetric.supportsOperation(" U3 ", {2}, 3)); + ASSERT_EQ(symmetric.operations().size(), 2U); + EXPECT_TRUE(symmetric.operations()[1].supports({1, 0})); + EXPECT_FALSE(symmetric.operations()[1].supports({0, 1})); EXPECT_TRUE(symmetric.supports(GateKind::CZ, {1, 0})); - EXPECT_FALSE(symmetric.supports(GateKind::CZ, {0, 1})); + EXPECT_TRUE(symmetric.supports(GateKind::CZ, {0, 1})); EXPECT_TRUE( llvm::is_contained(symmetric.globallySupportedGates(), GateKind::CZ)); ASSERT_TRUE(symmetric.synthesisBasis()); @@ -297,7 +300,9 @@ TEST(CompilerTargetTest, ClassifiesEveryEntanglerOrientation) { const Target target{3, chain, std::vector{globalU, oneOrientation}}; EXPECT_TRUE(llvm::is_contained(target.globallySupportedGates(), gate)); EXPECT_TRUE(target.supports(gate, {1, 0})); - EXPECT_FALSE(target.supports(gate, {0, 1})); + EXPECT_TRUE(target.supports(gate, {0, 1})); + EXPECT_TRUE(target.supports(gate, {1, 2})); + EXPECT_TRUE(target.supports(gate, {2, 1})); ASSERT_TRUE(target.synthesisBasis()); EXPECT_EQ(target.synthesisBasis()->entangler, gate); } diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp index 397b7c7a26..c6e2e1bb7f 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -59,7 +59,7 @@ using namespace mlir; using namespace mlir::qco; using namespace mlir::qco::decomposition; -using enum EulerBasis; +using enum SingleQubitBasis; // File layout: // 1. Fixtures and parametric test types @@ -100,7 +100,7 @@ struct SynthesizedCircuit { class EulerSynthesisExactTest : public testing::TestWithParam< - std::tuple> {}; + std::tuple> {}; } // namespace @@ -256,7 +256,7 @@ template } [[nodiscard]] static std::size_t countBasisGates(func::FuncOp funcOp, - EulerBasis basis) { + SingleQubitBasis basis) { switch (basis) { case ZYZ: return countZYZGates(funcOp); @@ -277,7 +277,8 @@ template } [[nodiscard]] static SynthesizedCircuit -synthesizeMatrix(MLIRContext* ctx, const Matrix2x2& matrix, EulerBasis basis) { +synthesizeMatrix(MLIRContext* ctx, const Matrix2x2& matrix, + SingleQubitBasis basis) { OwningOpRef mlirModule = ModuleOp::create(UnknownLoc::get(ctx)); OpBuilder builder(ctx); builder.setInsertionPointToStart(mlirModule->getBody()); @@ -303,13 +304,13 @@ synthesizeMatrix(MLIRContext* ctx, const Matrix2x2& matrix, EulerBasis basis) { [[nodiscard]] static std::size_t expectedGateCount(MLIRContext* ctx, const Matrix2x2& segment, - EulerBasis basis) { + SingleQubitBasis basis) { return countBasisGates(synthesizeMatrix(ctx, segment, basis).func, basis); } static void checkSynthesizedReferenceExtras(MLIRContext* ctx, func::FuncOp funcOp, - EulerBasis basis, + SingleQubitBasis basis, const Matrix2x2& matrix) { if (basis == U) { EXPECT_EQ(countOps(funcOp), expectedGateCount(ctx, matrix, basis)); @@ -327,7 +328,7 @@ static void checkSynthesizedReferenceExtras(MLIRContext* ctx, template static void expectSynthesizedMatrix(MLIRContext* ctx, const Matrix2x2& matrix, - EulerBasis basis, + SingleQubitBasis basis, ExtraChecksT extraChecks) { const auto circuit = synthesizeMatrix(ctx, matrix, basis); ASSERT_TRUE(succeeded(verify(*circuit.mlirModule))); @@ -457,7 +458,7 @@ TEST(EulerSynthesisTest, RandomReconstructionAllBases) { for (int i = 0; i < 200; ++i) { const auto original = randomUnitaryMatrix(rng); forEachBasis([&fx, &original](StringRef basisStr) { - const auto parsed = parseEulerBasis(basisStr); + const auto parsed = parseSingleQubitBasis(basisStr); ASSERT_TRUE(parsed) << "basis=" << basisStr.str(); const auto circuit = synthesizeMatrix(fx.ctx(), original, *parsed); ASSERT_TRUE(succeeded(verify(*circuit.mlirModule))) @@ -555,7 +556,7 @@ TEST(EulerAnglesCoverageTest, Mod2PiPreservesNonFinitePhase) { //===----------------------------------------------------------------------===// [[nodiscard]] static bool isAllowedBasisGate(const Operation& op, - EulerBasis basis) { + SingleQubitBasis basis) { switch (basis) { case ZYZ: return isa(op); @@ -580,7 +581,7 @@ template [[nodiscard]] static bool inParent(Operation* op) { } static WalkResult visitBasisGateOp(Operation* op, StringRef basis, - EulerBasis parsedBasis) { + SingleQubitBasis parsedBasis) { if (isa(*op)) { return WalkResult::advance(); } @@ -617,7 +618,7 @@ template } static void expectBasisGatesOnly(func::FuncOp funcOp, StringRef basis) { - const auto parsed = parseEulerBasis(basis); + const auto parsed = parseSingleQubitBasis(basis); ASSERT_TRUE(parsed) << basis.str(); funcOp.walk( @@ -655,7 +656,7 @@ template } static void expectSplitFixtureSegments(func::FuncOp funcOp, StringRef basis, MLIRContext* ctx) { - const auto parsed = parseEulerBasis(basis); + const auto parsed = parseSingleQubitBasis(basis); ASSERT_TRUE(parsed) << basis.str(); const std::size_t ht = expectedGateCount(ctx, splitFixtureHTSegmentMatrix(), *parsed); @@ -686,7 +687,7 @@ template static void expectSplitFixtureSegments(func::FuncOp funcOp, StringRef basis, MLIRContext* ctx, BoundaryPred isBoundary) { - const auto parsed = parseEulerBasis(basis); + const auto parsed = parseSingleQubitBasis(basis); ASSERT_TRUE(parsed) << basis.str(); const std::size_t ht = expectedGateCount(ctx, splitFixtureHTSegmentMatrix(), *parsed); diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index 43707f0c99..8e82369894 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -13,7 +13,6 @@ #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/SynthesisBasis.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" #include "mlir/Dialect/QCO/Utils/DDFunctionality.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" @@ -457,7 +456,7 @@ computeTwoQubitUnitaryFromFunc(func::FuncOp funcOp) { [[nodiscard]] static Synthesized2QCircuit synthesize2QMatrix(MLIRContext* ctx, const Matrix4x4& target, - const NativeSynthesisBasis& basis) { + const CompilerTarget::SynthesisBasis basis) { OwningOpRef mlirModule = ModuleOp::create(UnknownLoc::get(ctx)); OpBuilder builder(ctx); builder.setInsertionPointToStart(mlirModule->getBody()); @@ -470,21 +469,19 @@ synthesize2QMatrix(MLIRContext* ctx, const Matrix4x4& target, auto* entry = func.addEntryBlock(); builder.setInsertionPointToStart(entry); + const auto decomposition = decomposeUnitary2QWeyl(target, basis.entangler); const auto synthesized = - synthesizeUnitary2QWeyl(builder, loc, entry->getArgument(0), - entry->getArgument(1), target, basis); - if (failed(synthesized)) { - ADD_FAILURE() << "synthesizeUnitary2QWeyl failed during test synthesis"; - } else { - emitGPhaseIfNeeded(builder, loc, synthesized->globalPhase); - func::ReturnOp::create( - builder, loc, ValueRange{synthesized->qubit0, synthesized->qubit1}); - } + emitUnitary2QWeyl(builder, loc, entry->getArgument(0), + entry->getArgument(1), decomposition, basis); + emitGPhaseIfNeeded(builder, loc, synthesized.globalPhase); + func::ReturnOp::create(builder, loc, + ValueRange{synthesized.qubit0, synthesized.qubit1}); return {.mlirModule = std::move(mlirModule), .func = func}; } -static void expectSynthesized2QMatrix(MLIRContext* ctx, const Matrix4x4& target, - const NativeSynthesisBasis& basis) { +static void +expectSynthesized2QMatrix(MLIRContext* ctx, const Matrix4x4& target, + const CompilerTarget::SynthesisBasis basis) { const auto circuit = synthesize2QMatrix(ctx, target, basis); ASSERT_TRUE(succeeded(verify(*circuit.mlirModule))); const auto actual = computeTwoQubitUnitaryFromFunc(circuit.func); @@ -531,8 +528,7 @@ class WeylSynthesisMlirTest : public testing::Test { } // namespace TEST_P(WeylSynthesisTest, PreservesTargetUnitary) { - const auto basis = NativeSynthesisBasis::fromCompilerTarget(GetParam().basis); - expectSynthesized2QMatrix(mlir.ctx(), GetParam().target(), basis); + expectSynthesized2QMatrix(mlir.ctx(), GetParam().target(), GetParam().basis); } INSTANTIATE_TEST_SUITE_P( @@ -601,10 +597,8 @@ TEST(WeylSynthesisTest, IdentityRequiresNoEntanglers) { CompilerTarget::GateKind::RZX, CompilerTarget::GateKind::RZZ, CompilerTarget::GateKind::ISWAP, CompilerTarget::GateKind::CZ, CompilerTarget::GateKind::CX, CompilerTarget::GateKind::ECR}) { - const auto basis = NativeSynthesisBasis::fromCompilerTarget( - {.singleQubit = CompilerTarget::SingleQubitBasis::U, - .entangler = entangler}); - const auto native = basis.decomposeTarget(Matrix4x4::identity()); + const auto native = + decomposeUnitary2QWeyl(Matrix4x4::identity(), entangler); EXPECT_EQ(native.numBasisUses, 0U); } } diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp index dddddeab4e..85a09939fc 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp @@ -14,8 +14,6 @@ #include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/SynthesisBasis.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/QCO/Utils/DDFunctionality.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" @@ -64,6 +62,8 @@ using mlir::qco::HOp; using mlir::qco::QCOProgramBuilder; using mlir::qco::RXXOp; using mlir::qco::SWAPOp; +using mlir::qco::XOp; +using mlir::qco::ZOp; [[nodiscard]] static mlir::func::FuncOp mainFunction(ModuleOp module) { return *module.getBody()->getOps().begin(); @@ -199,26 +199,6 @@ TEST(TargetSynthesisPassContract, FactoriesAreIndependentlyConstructible) { mlir::arith::ArithDialect::getDialectNamespace())); } -TEST(TargetSynthesisPassContract, AdaptsEverySupportedSynthesisBasis) { - using mlir::qco::decomposition::EulerBasis; - using mlir::qco::decomposition::NativeSynthesisBasis; - - constexpr std::array singleQubitBases{ - std::pair{Target::SingleQubitBasis::U, EulerBasis::U}, - std::pair{Target::SingleQubitBasis::ZSXX, EulerBasis::ZSXX}, - std::pair{Target::SingleQubitBasis::R, EulerBasis::R}, - std::pair{Target::SingleQubitBasis::XZX, EulerBasis::XZX}, - std::pair{Target::SingleQubitBasis::XYX, EulerBasis::XYX}, - std::pair{Target::SingleQubitBasis::ZYZ, EulerBasis::ZYZ}, - }; - for (const auto [targetBasis, decompositionBasis] : singleQubitBases) { - const auto adapted = NativeSynthesisBasis::fromCompilerTarget( - {.singleQubit = targetBasis, .entangler = Target::GateKind::CX}); - EXPECT_EQ(adapted.singleQubit, decompositionBasis); - EXPECT_EQ(adapted.entangler, Target::GateKind::CX); - } -} - TEST_F(TargetSynthesisTest, PreRoutingOptimizationRequiresStrictImprovement) { const auto adjacentCx = [](QCOProgramBuilder& builder) { auto q0 = builder.staticQubit(0); @@ -267,6 +247,26 @@ TEST_F(TargetSynthesisTest, expectEquivalent(expected, optimized); } +TEST_F(TargetSynthesisTest, PreRoutingOptimizationEmitsSymmetricEntangler) { + const auto reducible = [](QCOProgramBuilder& builder) { + auto q0 = builder.staticQubit(0); + auto q1 = builder.staticQubit(1); + std::tie(q0, q1) = builder.cx(q0, q1); + std::tie(q1, q0) = builder.cx(q1, q0); + std::tie(q1, q0) = builder.cx(q1, q0); + return builder.intConstant(0); + }; + auto expected = build(reducible); + auto optimized = build(reducible); + + ASSERT_TRUE(mlir::succeeded( + runPass(*optimized, mlir::qco::createOptimizeTwoQubitUnitaryRuns()))); + EXPECT_EQ(countOps(*optimized), 1U); + EXPECT_EQ(countOps(*optimized), 1U); + EXPECT_EQ(countOps(*optimized), 0U); + expectEquivalent(expected, optimized); +} + TEST_F(TargetSynthesisTest, PreRoutingOptimizationLeavesIndividualOpsAlone) { auto module = build([](QCOProgramBuilder& builder) { auto q0 = builder.staticQubit(0); @@ -274,10 +274,12 @@ TEST_F(TargetSynthesisTest, PreRoutingOptimizationLeavesIndividualOpsAlone) { std::tie(q0, q1) = builder.swap(q0, q1); return builder.intConstant(0); }); + const auto before = printModule(*module); ASSERT_TRUE(mlir::succeeded( runPass(*module, mlir::qco::createOptimizeTwoQubitUnitaryRuns()))); EXPECT_EQ(countOps(*module), 1U); EXPECT_EQ(countOps(*module), 0U); + EXPECT_EQ(printModule(*module), before); } TEST_F(TargetSynthesisTest, @@ -363,7 +365,7 @@ TEST_F(TargetSynthesisTest, TargetNativeSynthesisPreservesNativeSwap) { } TEST_F(TargetSynthesisTest, - TargetNativeSynthesisUsesSupportedSymmetricOrientation) { + TargetNativeSynthesisUsesBidirectionalSymmetricCapability) { const auto swap = [](QCOProgramBuilder& builder) { auto q0 = builder.staticQubit(0); auto q1 = builder.staticQubit(1); @@ -372,19 +374,19 @@ TEST_F(TargetSynthesisTest, }; auto expected = build(swap); auto synthesized = build(swap); - const Target reverseOnly{ + const Target oneOrientation{ std::vector{Site{0}, Site{1}}, std::nullopt, std::vector{Operation{"u", 1, 3}, Operation{"cz", 2, 0, std::vector{OperationLocus{{1, 0}}}}}}; - ASSERT_TRUE(reverseOnly.synthesisBasis()); - ASSERT_EQ(reverseOnly.synthesisBasis()->entangler, Target::GateKind::CZ); + ASSERT_TRUE(oneOrientation.synthesisBasis()); + ASSERT_EQ(oneOrientation.synthesisBasis()->entangler, Target::GateKind::CZ); ASSERT_TRUE(mlir::succeeded(runPass( - *synthesized, mlir::qco::createTargetNativeSynthesis(reverseOnly)))); + *synthesized, mlir::qco::createTargetNativeSynthesis(oneOrientation)))); EXPECT_EQ(countOps(*synthesized), 0U); EXPECT_GT(countOps(*synthesized), 0U); ASSERT_TRUE(mlir::succeeded(runPass( - *synthesized, mlir::qco::createVerifyTargetConformance(reverseOnly)))); + *synthesized, mlir::qco::createVerifyTargetConformance(oneOrientation)))); ASSERT_TRUE(mlir::succeeded(mlir::verify(*synthesized))); expectEquivalent(expected, synthesized); } @@ -505,6 +507,28 @@ TEST_F(TargetSynthesisTest, << diagnostics; } +TEST_F(TargetSynthesisTest, + UnsupportedRuntimeParameterizedGateDoesNotPartiallyRewrite) { + auto module = mlir::parseSourceString(R"mlir( + module { + func.func @main(%theta: f64) -> (!qco.qubit, !qco.qubit) { + %q0 = qco.static 0 : !qco.qubit + %q1 = qco.static 1 : !qco.qubit + %q2 = qco.h %q0 : !qco.qubit -> !qco.qubit + %q3, %q4 = qco.rxx(%theta) %q2, %q1 : !qco.qubit, !qco.qubit -> !qco.qubit, !qco.qubit + return %q3, %q4 : !qco.qubit, !qco.qubit + } + } + )mlir", + context.get()); + ASSERT_TRUE(module); + const auto before = printModule(*module); + + static_cast(expectFailure( + *module, mlir::qco::createTargetNativeSynthesis(makeUCxTarget()))); + EXPECT_EQ(printModule(*module), before); +} + TEST_F(TargetSynthesisTest, ConformanceUsesProviderIdsAndOrderedLoci) { const Target directional{ std::vector{Site{10}, Site{20}}, std::nullopt, From 4d598062a71f228d50463061b0b171bb7f30dc6a Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Tue, 4 Aug 2026 00:01:48 +0200 Subject: [PATCH 6/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20target=20ca?= =?UTF-8?q?pabilities=20and=20terminology?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assisted-by: GPT-5.6 via Codex --- .agent/plans/1687-ct-01-compiler-target.md | 187 ++++----- .agent/plans/1687-iqm-01-models.md | 47 +-- .agent/plans/1687-map-01-target-mapping.md | 22 +- .agent/plans/1687-syn-01-target-synthesis.md | 272 +++++++------ CHANGELOG.md | 8 +- docs/qdmi/sc_device.md | 12 +- mlir/include/mlir/Compiler/Target.h | 101 ++--- .../mlir/Dialect/QCO/Transforms/Passes.h | 4 +- .../mlir/Dialect/QCO/Transforms/Passes.td | 2 +- mlir/lib/Compiler/Target.cpp | 361 +++++------------- .../NativeSynthesis/TargetSynthesis.cpp | 271 ++++--------- .../Compiler/test_compiler_target.cpp | 204 ++++------ .../QCO/Transforms/Mapping/test_mapping.cpp | 2 +- .../NativeSynthesis/test_target_synthesis.cpp | 233 +++++------ .../Utils/test_global_phase_normalization.cpp | 34 -- python/mqt/core/plugins/qiskit/backend.py | 2 +- test/qdmi/devices/sc/test_configuration.cpp | 10 +- 17 files changed, 671 insertions(+), 1101 deletions(-) diff --git a/.agent/plans/1687-ct-01-compiler-target.md b/.agent/plans/1687-ct-01-compiler-target.md index 7a28406fd6..3a24088698 100644 --- a/.agent/plans/1687-ct-01-compiler-target.md +++ b/.agent/plans/1687-ct-01-compiler-target.md @@ -14,15 +14,16 @@ compiler can rely on without linking a live QDMI device, the FoMaC wrapper, or the legacy Core IR. After this change, C++ compiler code can construct a `mlir::CompilerTarget` either from a qubit count or from detailed site, topology, and operation data. It can cheaply copy that target, translate between -provider site identifiers and dense compiler vertices, ask whether an MLIR QCO -operation is native at an ordered hardware locus, and inspect one typed native -synthesis basis that is usable across the complete target. +target site identifiers and dense compiler vertices, ask whether an MLIR QCO +operation is native, and inspect one typed synthesis basis. Operation +capabilities are homogeneous across the target; ordered site tuples retain only +site-specific calibration data. The focused compiler unit test demonstrates the result. It constructs targets -with sparse signed site identifiers, provider gate aliases, calibrated operation -loci, and one-way topology input; verifies canonicalized topology and -capabilities; creates real QCO operations; and observes correct support answers. -Invalid metadata is rejected once at construction. +with sparse signed site identifiers, device gate aliases, calibrated operation +site tuples, and one-way topology input; verifies canonicalized topology and +homogeneous capabilities; creates real QCO operations; and observes correct +support answers. Invalid metadata is rejected once at construction. ## Progress @@ -41,17 +42,17 @@ Invalid metadata is rejected once at construction. `Target.h` to its build-tree public header file set, and excluded that header from `MQTCompilerPipeline`. - [x] (2026-08-03 11:52Z) Added eight focused tests covering all four - constructor forms, validation, cheap copies, topology, every required - symmetric and directional entangler, synthesis-basis resolution, and real - QCO operations. + constructor forms, validation, cheap copies, topology, every required + recognized entangler, synthesis-basis resolution, and real QCO operations. - [x] (2026-08-03 11:56Z) Built the target and interface-header verification target, passed 8 focused and all 218 compiler tests, ran changed-file clang-tidy without local diagnostics, passed the full repository lint, and passed `git diff --check`. - [x] (2026-08-03 11:56Z) Completed an independent pre-commit review and - resolved all four blocking findings: intrinsic locus validation, complete - entangler-table coverage, C++/policy diagnostics, and the stale plan. The - final tree is prepared for one signed atomic commit without a push. + resolved all four blocking findings: intrinsic site tuple validation, + complete entangler-table coverage, C++/policy diagnostics, and the stale + plan. The final tree is prepared for one signed atomic commit without a + push. - [x] (2026-08-03 12:35Z) Diagnosed the exact-head CI failures after publication. Moved five file-local helpers out of the anonymous namespace so LLVM 22's preferred static-linkage check passes. Rebuilt the target, @@ -66,6 +67,12 @@ Invalid metadata is rejected once at construction. retaining the repository's modernize-use-auto contract. Rebuilt the target and interface header and passed the 8 focused and all 218 compiler tests plus the exact relevant LLVM checks. +- [x] (2026-08-03) Simplified operation support to one homogeneous target-wide + capability per name, arity, and parameter count. Standardized on + `SiteTuple`, retained tuples only as calibration metadata, removed + directional probing, and reduced the public queries to + `supportsOperation(name, arity, parameters)`, `supports(Operation*)`, and + `supports(GateKind)`. ## Milestones @@ -73,21 +80,21 @@ Invalid metadata is rejected once at construction. The goal was a compiler-owned model that can be constructed without QDMI, FoMaC, CoreIR, or a live device. The work added value types for timing units, sites, -operation loci, and operation capabilities plus a shared immutable target -storage object. The result preserves provider metadata while validating the -cross-object contract once, including timing units, ordered loci, and explicit -operation absence versus emptiness. The focused construction and rejection tests -prove the observable contract. +operation site tuples, and operation capabilities plus a shared immutable target +storage object. The result preserves device metadata while validating the +cross-object contract once, including timing units, calibration site tuples, and +explicit operation absence versus emptiness. The focused construction and +rejection tests prove the observable contract. ### Milestone 2: Centralize topology and synthesis facts The goal was to prevent later mapping and synthesis stages from rebuilding -provider-specific representations. The work canonicalized explicit undirected +device-specific representations. The work canonicalized explicit undirected couplings, cached dense adjacency and all-pairs distances, indexed operation capabilities, and derived a typed target-wide synthesis basis. The result gives MAP-01 a reusable distance query and gives SYN-01 a stable gate enum without a -dependency on transformation code. Sparse-site, distance, ordered-locus, and -complete entangler-orientation tests prove these paths. +dependency on transformation code. Sparse-site, distance, calibration, and +complete entangler tests prove these paths. ### Milestone 3: Isolate and verify the foundation @@ -110,8 +117,8 @@ an explicit series-level item described below. - Observation: QCO already exposes `UnitaryOpInterface::getBaseSymbol()`, `getNumQubits()`, and `getNumParams()`. Evidence: `mlir/include/mlir/Dialect/QCO/IR/QCOInterfaces.td` defines all three - operations. `CompilerTarget::supports(Operation*, locus)` can use that - interface rather than enumerate every primitive operation. + operations. `CompilerTarget::supports(Operation*)` can use that interface + rather than enumerate every primitive operation. - Observation: The synthesis implementation currently owns a separate `NativeGateKind` plus Euler and Weyl machinery. Pulling that header into the target would make the foundation depend on synthesis details. The target can @@ -128,11 +135,10 @@ an explicit series-level item described below. The `Target.h` file set therefore establishes build-tree ownership and interface-header verification, not an installed consumer SDK. - Observation: Independent review caught inconsistent validation in the - standalone `Operation::supports` query and the all-native fast path, missing - table-driven coverage for six entanglers, changed-file clang-tidy findings, - and stale plan sections. The final implementation validates those query - boundaries, covers all eight required entanglers, and passes the repeated - static checks. + all-native fast path, missing table-driven coverage for six entanglers, + changed-file clang-tidy findings, and stale plan sections. The final + implementation validates those query boundaries, covers all eight required + entanglers, and passes the repeated static checks. - Observation: CI's LLVM 22 lint enables `llvm-prefer-static-over-anonymous-namespace`, which the local repository lint does not surface. Five file-local helpers therefore needed explicit `static` @@ -185,35 +191,33 @@ an explicit series-level item described below. passes simple. Date/Author: 2026-08-03, GPT-5.6 via Codex. - Decision: Treat an absent operation collection as every operation being native; distinguish it from a present empty collection, which supports no - hardware operation. Preserve provider names while caching a lower-case - canonical name and the aliases `prx` to `r`, `u3` to `u`, and `cnot` to `cx`. - Rationale: This retains provider metadata and adopts the useful, narrowly - scoped alias insight from Simon Hofmann's #1969 work without importing its - targeting, CLI, Python, or synthesis APIs. The final commit will preserve - Simon Hofmann's authorship because this semantic source is materially reused. - Date/Author: 2026-08-03, GPT-5.6 via Codex. + hardware operation. Preserve reported operation names while caching a + lower-case canonical name and the aliases `prx` to `r`, `u3` to `u`, and + `cnot` to `cx`. Rationale: This retains operation metadata and adopts the + useful, narrowly scoped alias insight from Simon Hofmann's #1969 work without + importing its targeting, CLI, Python, or synthesis APIs. The final commit will + preserve Simon Hofmann's authorship because this semantic source is materially + reused. Date/Author: 2026-08-03, GPT-5.6 via Codex. - Decision: Require every operation capability to have a positive fixed arity. Rationale: Final conformance cannot soundly validate an operation whose arity - is unknown; the later device adapter must reject a provider operation that - omits it rather than transferring ambiguity into the compiler model. - Date/Author: 2026-08-03, GPT-5.6 via Codex. + is unknown; the later device adapter must reject a device operation that omits + it rather than transferring ambiguity into the compiler model. Date/Author: + 2026-08-03, GPT-5.6 via Codex. - Decision: Store an optional target-wide duration unit and positive finite scale factor, and require it whenever a site T1/T2, operation duration, or - locus duration is present. T1/T2 must be positive; operation and locus - durations may be zero for virtual gates. Rationale: Raw `uint64_t` timing - metadata is not self-describing without the QDMI unit/scale contract, while - the existing SC device schema intentionally permits nonnegative operation - durations. Date/Author: 2026-08-03, GPT-5.6 via Codex. -- Decision: A gate is globally usable when its matching canonical capability - covers every site for a one-qubit gate or every routing edge for a two-qubit - gate. Operand-symmetric entanglers (`cz`, `rxx`, `ryy`, `rzz`, and `iswap`) - may cover an undirected edge in either orientation. Directional entanglers - (`cx`, `ecr`, and `rzx`) must cover both ordered orientations. An all-to-all - target applies the same rule to every distinct pair. `supports(gate, locus)` - remains strictly ordered. Rationale: A single synthesis basis advertised for - the whole target must not be inferred from a provider operation that is - unavailable on some sites or from an unimplemented operand-reversal - assumption. Date/Author: 2026-08-03, GPT-5.6 via Codex. + site-tuple duration is present. T1/T2 must be positive; operation and site + tuple durations may be zero for virtual gates. Rationale: Raw `uint64_t` + timing metadata is not self-describing without the QDMI unit/scale contract, + while the existing SC device schema intentionally permits nonnegative + operation durations. Date/Author: 2026-08-03, GPT-5.6 via Codex. +- Decision: Treat each operation capability as homogeneous across the target. + Site tuples retain ordered duration and fidelity overrides but do not alter + support. A recognized gate is available when its canonical name, arity, and + parameter count match one capability; two-qubit gates consequently work in + either operand orientation. Rationale: Current compilation targets expose one + gate set, while site variation is calibration metadata. This removes + directional probing and per-site capability reconstruction from every compiler + pass. Date/Author: 2026-08-03, GPT-5.6 via Codex. - Decision: Accept a target whose capabilities do not form a complete synthesis basis and expose `std::nullopt` from `synthesisBasis()`. Rationale: Such a target remains useful for support checking and future diagnostics; SYN-01 can @@ -277,18 +281,17 @@ Its operation declarations are generated from `mlir/include/mlir/Dialect/QCO/IR/QCOOps.td`, and `mlir/include/mlir/Dialect/QCO/IR/QCOInterfaces.td` defines `UnitaryOpInterface`. A dense compiler vertex is a zero-based position used by -routing algorithms. A hardware site identifier is the provider-visible signed +routing algorithms. A hardware site identifier is the device-visible signed integer that later appears in `qco.static`; the two are not interchangeable when -providers use sparse identifiers. +devices use sparse identifiers. A capability states that an operation with a canonical name, arity, and -parameter count is allowed at an ordered locus. A locus is an ordered list of -hardware site identifiers. An absent locus collection means the capability is -global for every valid tuple of its arity; a present empty collection means it -supports no tuple. A synthesis basis is one recognized single-qubit Euler basis -plus one recognized two-qubit entangling gate that is usable over the entire -target. CT-01 only describes that basis; the existing native-synthesis -implementation remains under `mlir/lib/Dialect/QCO/Transforms/Decomposition/`. +parameter count is available throughout the target. A site tuple is an ordered +list of hardware site identifiers with optional duration and fidelity overrides; +it does not restrict support. A synthesis basis is one recognized single-qubit +Euler basis plus one recognized two-qubit entangling gate. CT-01 only describes +that basis; the existing native-synthesis implementation remains under +`mlir/lib/Dialect/QCO/Transforms/Decomposition/`. The focused compiler unit test executable is configured by `mlir/unittests/Compiler/CMakeLists.txt` and produced at @@ -301,7 +304,7 @@ verification target. ## Plan of Work Add `mlir/include/mlir/Compiler/Target.h`. Define -`CompilerTarget::DurationUnit`, `Site`, `OperationLocus`, and `Operation` as +`CompilerTarget::DurationUnit`, `Site`, `SiteTuple`, and `Operation` as immutable public value types with constructors that validate their local scalar data. Operation arity is mandatory. Define `GateKind`, `SingleQubitBasis`, and `SynthesisBasis` as typed compiler capabilities. Define named and unnamed @@ -313,8 +316,8 @@ Add `mlir/lib/Compiler/Target.cpp`. Build a private shared `Storage` object, validate cross-references and the timing-unit invariant, cache site identifiers and the site-to-vertex map, canonicalize and validate explicit topology, build its dense adjacency and all-pairs distance matrix, check connectivity once, -group operations by canonical name, determine globally supported typed gates, -and resolve the preferred synthesis basis. Implement queries for metadata, dense +group operations by canonical name, determine the supported typed gates, and +resolve the preferred synthesis basis. Implement queries for metadata, dense mapping, adjacency and distance, canonical operation support, typed gate support, and QCO `Operation*` support. Structural QCO barrier and global-phase operations require no hardware capability; controlled single-X and single-Z @@ -334,9 +337,9 @@ closure is exported by the PIPE/INT series. Add `mlir/unittests/Compiler/test_compiler_target.cpp` and include it in the existing compiler test executable. Cover both constructor families, optional metadata, cheap shared copies, sparse site mappings, topology normalization, -every validation boundary, absent versus empty operation semantics, provider -aliases, ordered loci, global gate coverage, basis preference, and real QCO -operation support. Link the test executable to `MQTCompilerTarget`. +every validation boundary, absent versus empty operation semantics, gate +aliases, ordered site tuples, typed gate coverage, basis preference, and real +QCO operation support. Link the test executable to `MQTCompilerTarget`. Do not edit `cmake/ExternalDependencies.cmake` unless the scoped QDMI `SYSTEM` setting appears through an in-scope integration update. It is absent at the @@ -389,34 +392,32 @@ mutation is authorized. Construction from a count produces sites `0..N-1`; construction from detailed sites preserves order, optional target and site names, T1/T2, operation -duration/fidelity, and ordered loci. A copied target returns views backed by the -same immutable storage. +duration/fidelity, and ordered site tuples. A copied target returns views backed +by the same immutable storage. Negative or duplicate site identifiers, an empty target, count overflow, zero operation arity, zero coherence times, invalid fidelity values, timing data -without a valid duration unit/scale, malformed/duplicate loci, unknown site -references, self-couplings, and disconnected explicit topologies throw -`std::invalid_argument`. Zero operation and locus durations remain valid. +without a valid duration unit/scale, malformed/duplicate site tuples, unknown +site references, self-couplings, and disconnected explicit topologies throw +`std::invalid_argument`. Zero operation and site-tuple durations remain valid. Reversed and duplicate topology input is normalized to one sorted undirected edge. An absent topology reports all distinct site pairs as adjacent and distance one. An explicit topology reports only its validated edges and returns cached shortest-path distances. Dense vertex/site translations are stable for sparse, -unsorted provider identifiers. +unsorted target identifiers. An absent operation collection supports every well-formed hardware operation. A present empty collection supports none except structural barrier and -global-phase operations. Provider spelling is retained while canonical lookup -recognizes case, whitespace, `prx`, `u3`, and `cnot`. Ordered loci remain -directional. - -The typed global gate view excludes a capability that is missing on one site or -one routing edge. Symmetric entanglers may use either locus orientation; -directional entanglers require both. `synthesisBasis()` exists only when one -complete supported single-qubit basis and one globally supported entangler -exist, with documented deterministic preference; an incomplete basis does not -invalidate the target. `supports(Operation*, locus)` agrees with typed +global-phase operations. Reported operation spelling is retained while canonical +lookup recognizes case, whitespace, `prx`, `u3`, and `cnot`. Site tuples +preserve ordered calibration data without restricting operation support. + +The typed gate view follows the homogeneous operation set. `synthesisBasis()` +exists only when one complete supported single-qubit basis and one supported +entangler exist, with documented deterministic preference; an incomplete basis +does not invalidate the target. `supports(Operation*)` agrees with typed capability queries for actual QCO primitives and controlled X/Z operations. `MQTCompilerTarget` builds as its own MLIR library. Its direct link interface @@ -483,7 +484,7 @@ At completion, `mlir/Compiler/Target.h` defines: using Coupling = std::pair; class DurationUnit; class Site; - class OperationLocus; + class SiteTuple; class Operation; enum class GateKind : uint8_t; enum class SingleQubitBasis : uint8_t; @@ -509,11 +510,11 @@ At completion, `mlir/Compiler/Target.h` defines: const std::optional& durationUnit() const; bool hasExplicitOperations() const; ArrayRef operations() const; - bool supportsOperation(StringRef name, ArrayRef locus, + bool supportsOperation(StringRef name, size_t numQubits, std::optional numParameters) const; - bool supports(Operation* operation, ArrayRef locus) const; - bool supports(GateKind gate, ArrayRef locus) const; - ArrayRef globallySupportedGates() const; + bool supports(Operation* operation) const; + bool supports(GateKind gate) const; + ArrayRef supportedGates() const; std::optional synthesisBasis() const; }; } @@ -528,10 +529,10 @@ Revision note (2026-08-03): Created the initial self-contained plan after exact checkout verification and source/provenance research. The implementation and validation sections will be updated as evidence replaces planned behavior. -Revision note (2026-08-03): Refined global entangler coverage before -implementation: symmetric operations may cover an undirected edge in one -orientation, while directional operations require both ordered orientations. -Also recorded that incomplete synthesis capability is a valid target state. +Revision note (2026-08-03): Replaced the initial per-site entangler-coverage +model with homogeneous operation capabilities. Site tuples now retain +calibration only, and incomplete synthesis capability remains a valid target +state. Revision note (2026-08-03): Refined the foundation while the first implementation draft was still uncompiled: operation arity is mandatory, timing diff --git a/.agent/plans/1687-iqm-01-models.md b/.agent/plans/1687-iqm-01-models.md index 5add73df6b..f636cb2411 100644 --- a/.agent/plans/1687-iqm-01-models.md +++ b/.agent/plans/1687-iqm-01-models.md @@ -63,8 +63,8 @@ and command-line integration belong to later changes. ## Surprises & Discoveries - Observation: the SC schema already represents optional per-qubit T1/T2 and - optional per-locus duration/fidelity. It only lacks the optional site name - that QDMI already exposes through `QDMI_SITE_PROPERTY_NAME`. + optional per-site-tuple duration/fidelity. It only lacks the optional site + name that QDMI already exposes through `QDMI_SITE_PROPERTY_NAME`. - Observation: the IQM provider obtains site names from the ordered `qubits` array of the static architecture response and assigns the array position as the QDMI site index. IQM qubits in these captures use `QB1` through `QB20` for @@ -88,8 +88,8 @@ and command-line integration belong to later changes. `api/v1/quantum-computers/%s/artifacts/static-quantum-architectures`, `api/v1/calibration-sets/%s/%s/dynamic-quantum-architecture`, and `api/v1/calibration-sets/%s/%s/metrics`. The first two select the computer and - return names/topology, the third returns default-calibration operation loci, - and the fourth returns T1/T2 and fidelity metrics. + return names/topology, the third returns default-calibration operation site + tuples, and the fourth returns T1/T2 and fidelity metrics. - Observation: CTest registers the individual GoogleTest cases, not targets named after the three executables. A target-name regular expression therefore selected no tests; running each built executable directly exercised 171 tests. @@ -115,7 +115,7 @@ and command-line integration belong to later changes. nanosecond-level coherence precision through unit `us` with scale factor `0.001`. Date/Author: 2026-08-03, Codex. - Decision: do not add any duration field. Rationale: neither the captured - provider data nor the IQM QDMI interface supplied operation durations. + device data nor the IQM QDMI interface supplied operation durations. Date/Author: 2026-08-03, Codex. - Decision: extend `mqt_configure_qdmi_device` with a `CONFIGURATIONS` list whose entries pair a stable registry ID with the basename of an existing @@ -176,24 +176,25 @@ entry has exactly an ID and filename and that the filename is among loops then package all three SC JSON files. Expand configuration tests to assert the model names, qubit and edge counts, -exact operation names, calibration coverage, locus-fidelity counts, site-name -endpoints, and complete absence of operation durations. Add schema coverage for -name-only overrides and empty-name rejection. Expand provider tests to prove -configured names are queryable and absent names remain unsupported. Expand -registry and driver tests to prove that five built-in definitions exist, both -stable IDs use packaged configuration files, and both models can be opened. +exact operation names, calibration coverage, per-site-tuple fidelity counts, +site-name endpoints, and complete absence of operation durations. Add schema +coverage for name-only overrides and empty-name rejection. Expand provider tests +to prove configured names are queryable and absent names remain unsupported. +Expand registry and driver tests to prove that five built-in definitions exist, +both stable IDs use packaged configuration files, and both models can be opened. Update `docs/qdmi/sc_device.md` to describe optional names and the two shared models. Record that topology and site names came from the static architecture, -operation loci from the default dynamic architecture, and T1/T2/fidelities from -default calibration-set quality metrics retrieved on 2 August 2026. State that -the files contain no credentials or calibration identifiers and intentionally -omit unavailable durations. In this ExecPlan, pin the interpretation to -QDMI-on-IQM revision `5bef1d49245ae17877203618ad65865405fab361` and record its -source paths and the fact that no provider source, documentation text, or raw -service response is redistributed. Add one narrowly scoped `Unreleased` -changelog entry without inventing a pull request reference; add the reference in -a post-publication follow-up. +operation site tuples from the default dynamic architecture, and +T1/T2/fidelities from default calibration-set quality metrics retrieved on 2 +August 2026. State that the files contain no credentials or calibration +identifiers and intentionally omit unavailable durations. In this ExecPlan, pin +the interpretation to QDMI-on-IQM revision +`5bef1d49245ae17877203618ad65865405fab361` and record its source paths and the +fact that no provider source, documentation text, or raw service response is +redistributed. Add one narrowly scoped `Unreleased` changelog entry without +inventing a pull request reference; add the reference in a post-publication +follow-up. ## Milestones @@ -273,12 +274,12 @@ The implementation is accepted when: - Garnet parses as 20 sites and 30 ordered topology edges, and Emerald parses as 54 sites and 90 ordered topology edges. -- Both assets expose exactly `r`, `cz`, and `measure`; all recorded per-locus - fidelities remain available. +- Both assets expose exactly `r`, `cz`, and `measure`; all recorded + per-site-tuple fidelities remain available. - Garnet exposes 20 T1 and 20 T2 values. Emerald exposes 53 T1 and 54 T2 values. - Site indices remain zero-based while reported names are `QB1` through `QB20` or `QB54`, respectively. -- No operation or locus in either model reports a duration. +- No operation or site tuple in either model reports a duration. - The IDs `mqt.sc.iqm.garnet` and `mqt.sc.iqm.emerald` resolve to the SC provider plus the correct packaged JSON file in a build-tree registry. - The two JSON files are included in the provider's runtime-file property and diff --git a/.agent/plans/1687-map-01-target-mapping.md b/.agent/plans/1687-map-01-target-mapping.md index fa73df0dcf..5dce01d8ff 100644 --- a/.agent/plans/1687-map-01-target-mapping.md +++ b/.agent/plans/1687-map-01-target-mapping.md @@ -12,7 +12,7 @@ repository root. After this change, callers can run the QCO `place-and-route` pass against an immutable `mlir::CompilerTarget` instead of constructing a second graph wrapper from a dense coupling set. The mapper uses the target's validated topology for -placement and routing, emits provider-defined site identifiers in `qco.static` +placement and routing, emits device-defined site identifiers in `qco.static` operations, and continues to insert ordinary `qco.swap` operations without making target-native gate or direction decisions. @@ -49,7 +49,7 @@ pipeline integration task. allocations and higher-arity unitaries before mutation, and reuses its records for routing and placement. - [x] (2026-08-03 13:09Z) Materialized only program qubits and vacant target - vertices used by the selected routing plan, with provider site IDs in + vertices used by the selected routing plan, with target site IDs in output. - [x] (2026-08-03 13:09Z) Added focused mapping, diagnostic, allocation, sparse-workspace, empty-operation-set, and noncontiguous-ID tests while @@ -131,7 +131,7 @@ pipeline integration task. - Decision: use dense zero-based compiler vertices for `Layout`, A* search, and restoration, and translate through `CompilerTarget::siteForVertex` only when creating `qco.static`. Rationale: layout algorithms require dense indices, - while output IR must retain provider-defined identifiers. Date/Author: + while output IR must retain device-defined identifiers. Date/Author: 2026-08-03, Codex. - Decision: route every one-qubit unitary directly and every two-qubit unitary solely according to undirected target adjacency. Do not query operation @@ -167,7 +167,7 @@ pipeline integration task. Implementation and validation are complete. The mapper now owns a cheap `CompilerTarget` value, uses its dense topology throughout placement and -routing, translates to provider site IDs only for `qco.static`, supports scalar +routing, translates to target site IDs only for `qco.static`, supports scalar and mixed allocation forms, and materializes only active or preview-touched workspace qubits. The single discovery/planning walk makes unsupported nested allocations and higher-arity operations fail before mutation. @@ -183,7 +183,7 @@ same result against the merged compiler-target foundation. `mlir::CompilerTarget`, declared in `mlir/include/mlir/Compiler/Target.h` and implemented in `mlir/lib/Compiler/Target.cpp`, is an immutable target model. -Provider site identifiers can be sparse or noncontiguous, but the target stores +Device site identifiers can be sparse or noncontiguous, but the target stores them in a stable site order and exposes dense compiler vertices for algorithms. An absent topology denotes all-to-all connectivity; an explicit topology is validated as a connected undirected graph and has cached distances. @@ -268,7 +268,7 @@ comments, pass documentation, and diagnostics so they describe target topology and the supported one-/two-qubit boundary accurately. Refactor the mapping test fixture to hold a `CompilerTarget`. Adapt executable -checking to translate static provider IDs back to dense vertices before testing +checking to translate static target IDs back to dense vertices before testing adjacency. Preserve the existing nine-qubit grid and every current regression. Add focused tests that demonstrate arbitrary one- and two-qubit unitary names are routed without native-capability checks; nested higher-arity operations and @@ -332,8 +332,8 @@ The implementation is accepted when the mapping test binary passes every pre-existing test, including index-switch vote-and-restore, and the following new behavior is observed. -A `CompilerTarget` with noncontiguous provider IDs maps using dense internal -vertices, while every emitted `qco.static` index is one of the provider IDs. +A `CompilerTarget` with noncontiguous target IDs maps using dense internal +vertices, while every emitted `qco.static` index is one of the target IDs. Arbitrary one- and two-qubit QCO unitaries pass through the topology-only mapper; two-qubit operations are adjacent after routing regardless of target operation metadata or operand direction. Inserted routing operations remain @@ -389,7 +389,7 @@ The current restack evidence is: Patch comparison: final range-diff differs only in this plan; non-plan stable patch IDs match Current target API: - dense vertices plus provider site IDs, validated connected topology, + dense vertices plus target site IDs, validated connected topology, cached adjacency, neighbours, distances, and maximum degree Current mapper: CompilerTarget directly, with no coupling-set overload or adapter @@ -460,8 +460,8 @@ declares: `MappingPass` owns a `CompilerTarget` value. It uses only `numQubits`, `siteForVertex`, `areAdjacent`, `distanceBetween`, `forEachNeighbour`, and -`maxDegree` from that target. Operation-capability, native-gate, calibration, -duration, fidelity, and directed-locus APIs are deliberately out of scope. +`maxDegree` from that target. Operation capabilities, native gates, and +calibration metadata are deliberately out of scope. `MLIRQCOTransforms` and `MQTCompilerPipeline` depend on `MQTCompilerTarget`. The mapping test target also links the target library directly when needed. No new diff --git a/.agent/plans/1687-syn-01-target-synthesis.md b/.agent/plans/1687-syn-01-target-synthesis.md index 940ad7f7fd..afa61430b9 100644 --- a/.agent/plans/1687-syn-01-target-synthesis.md +++ b/.agent/plans/1687-syn-01-target-synthesis.md @@ -1,4 +1,4 @@ -# Split target-independent optimization from target-native synthesis +# Split target-independent gate fusion from target-native synthesis This ExecPlan is a living document. The sections `Progress`, `Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must @@ -9,20 +9,20 @@ repository root. ## Purpose / Big Picture -Two-qubit optimization and hardware lowering must answer different questions. -Before routing, the compiler should rewrite a constant unitary window only when -doing so strictly reduces its two-qubit gate count, without choosing a hardware -basis. After routing, the compiler should lower operations that the real -`mlir::CompilerTarget` does not support at their mapped provider sites and -remove routing SWAPs on targets that do not declare SWAP native. A final, -independently runnable pass must reject any operation that is not legal for its -actual type, arity, parameter count, provider site IDs, and ordered locus. +Two-qubit gate fusion and hardware lowering must answer different questions. +Before routing, target-independent two-qubit gate fusion should rewrite a +sequence only when doing so strictly reduces its two-qubit gate count, without +choosing a hardware basis. After routing, the compiler should lower operations +that the real `mlir::CompilerTarget` does not support and remove routing SWAPs +on targets that do not declare SWAP native. A final, independently runnable pass +rejects unsupported operation types, arities, and parameter counts as well as +dynamic allocations and unknown static target sites. After this change, C++ pipeline code can construct these three stages independently. Focused tests demonstrate a profitable CX cancellation, preserve isolated and runtime-parameterized gates before routing, lower SWAP to a -target-selected basis after routing, and reject direction, operation, arity, -parameter, and site mismatches. +target-selected basis after routing, and reject operation, arity, parameter, +allocation, and site mismatches. ## Progress @@ -38,8 +38,8 @@ parameter, and site mismatches. textual menu, CLI, C++ `QCOProgram`, Python binding/stub, and menu-specific tests. Added only a minimal decomposition adapter from `CompilerTarget::SynthesisBasis`. -- [x] (2026-08-03 16:18Z) Implemented independently constructible pre-routing - optimization, target-native synthesis, and target-conformance passes. +- [x] (2026-08-03 16:18Z) Implemented independently constructible two-qubit gate + fusion, target-native synthesis, and target-conformance passes. - [x] (2026-08-03 17:02Z) Replaced the broad menu suite with sixteen focused stage-contract tests and built and ran the target-synthesis, decomposition, and compiler unit-test binaries successfully. @@ -47,7 +47,7 @@ parameter, and site mismatches. completed source formatting, changed-file checks, repository lint, and initial diff checks. - [x] (2026-08-03 19:01Z) Resolved all three blockers from independent read-only - review: single-orientation symmetric-entangler loci, native SWAP + review: single-orientation symmetric-entangler site tuples, native SWAP authority, and native `qco.pow` body handling. The reviewer approved the remediated code and the focused suite passes all sixteen tests. - [x] (2026-08-03 19:20Z) Rebuilt and reran all three affected suites, reran @@ -64,9 +64,9 @@ parameter, and site mismatches. amended this evidence into the single signed commit. - [x] (2026-08-03 19:55Z) Published draft PR #1998, fixed its sole changed-file `clang-tidy` finding, and added four focused regressions after Codecov - exposed untested adapter, lowering, optimization, and provenance paths. - The resulting 20/199 focused tests pass and the two implementation files - reach 91% combined local line coverage. + exposed untested adapter, lowering, fusion, and static-site paths. The + resulting 20/199 focused tests pass and the two implementation files reach + 91% combined local line coverage. - [x] (2026-08-03 20:43Z) Observed terminal all-green CI for exact published head `b0a520372`, including 92.1% C++ patch coverage and strict documentation, then restacked the four SYN commits onto merged MAP-01 @@ -78,14 +78,22 @@ parameter, and site mismatches. one simplification: made `CompilerTarget::SingleQubitBasis` the only basis enum, deleted the decomposition adapter, removed entangler operand reversal and redundant matrix assertions, selected U/CZ for generic - fusion, reused Weyl decompositions, cached provider-site provenance, and + fusion, reused Weyl decompositions, validated target sites directly, and made target lowering failure-atomic. A direct preplanned `IRRewriter` traversal replaces general greedy machinery. Rebuilt the public - interface-header targets and observed 21/21 target-synthesis, 199/199 + interface-header targets and observed 20/20 target-synthesis, 199/199 decomposition, and 215/215 compiler tests. Focused changed-source `clang-tidy`, targeted hooks, full repository lint, stale-surface search, and `git diff --check` pass. A fresh independent exact-working-tree review approved the result with no correctness, bloat, or efficiency findings. +- [x] (2026-08-03) Removed the flaky large-scope global-phase timing test, + adopted `SiteTuple`/target terminology, renamed the pre-routing factory to + `createFuseTwoQubitGates`, and simplified `CompilerTarget` to homogeneous + operation capabilities. Removed directional fallback, per-operation site + tracing, and the two control-flow tests that existed only for that tracer; + conformance now validates allocation form, quantum function inputs, and + static target IDs directly. Focused release builds pass all 21 + target-synthesis tests and all 8 compiler-target tests. ## Milestones @@ -93,7 +101,7 @@ parameter, and site mismatches. The completed first milestone removes `mlir/Dialect/QCO/Transforms/Decomposition/NativeGateset.h` and its source. Gate -identity, provider aliases, arity, parameter counts, global capability checks, +identity, gate aliases, arity, parameter counts, homogeneous capability checks, and basis selection now remain in `mlir/include/mlir/Compiler/Target.h` and `mlir/lib/Compiler/Target.cpp`. `CompilerTarget::SingleQubitBasis` is also the single type consumed by Euler and Weyl synthesis; the duplicate `EulerBasis` @@ -105,44 +113,39 @@ surfaces are removed rather than retained as a synthetic target. The later PIPE slice owns high-level target pipeline composition, so this slice exposes only the three typed C++ pass factories. -### Milestone 2: Separate optimization, lowering, and verification - -The completed pre-routing stage scans maximal constant windows on one pair of -linear QCO wires. It evaluates a canonical U/CZ Weyl decomposition and rewrites -only when the synthesized entangler count is strictly smaller than the window's -original two-qubit operation count. It does not accept a target and does not -rewrite isolated or runtime-parameterized gates. - -The completed post-routing stage first asks the supplied `CompilerTarget` -whether each one- or two-qubit unitary is supported at its ordered provider -locus. Supported operations, including supported runtime-parameterized gates, -remain untouched. Unsupported constant operations and all ordinary `qco.swap` -operations that the target does not support are lowered through the target's -globally usable synthesis basis. A target-native SWAP remains untouched. -Recognized operand-symmetric gates are semantically bidirectional even though -raw provider loci retain their reported order, so synthesis emits every -entangler in logical wire order. The pass preflights every lowering need before -mutation, asks for a basis only when needed, and reports an unsupported runtime -gate without partially rewriting the module. - -The completed conformance stage traces each qubit operand back to `qco.static`, -including values passing through QCO `if`/`index_switch` and SCF `for`/`while` -regions. It then calls `CompilerTarget::supports` on the real operation and -ordered provider site IDs. It checks unitary, measurement, and reset operations -and reports the actual operation spelling, arity, parameter count, and locus. -Pass-local path compression makes repeated provider-site tracing linear in the -valid SSA lineage rather than repeatedly walking to `qco.static`. +### Milestone 2: Separate fusion, lowering, and verification + +The completed target-independent two-qubit gate-fusion stage scans maximal +constant sequences on one pair of linear QCO wires. It evaluates a canonical +U/CZ Weyl decomposition and rewrites only when the synthesized entangler count +is strictly smaller than the sequence's original two-qubit operation count. It +does not accept a target and does not rewrite isolated or runtime-parameterized +gates. + +The completed post-routing stage asks the supplied `CompilerTarget` whether each +one- or two-qubit unitary belongs to its homogeneous operation set. Supported +operations, including supported runtime-parameterized gates, remain untouched. +Unsupported constant operations and ordinary `qco.swap` operations are lowered +through the target's usable synthesis basis. A target-native SWAP remains +untouched. The pass preflights every lowering need before mutation, asks for a +basis only when needed, and reports an unsupported runtime gate without +partially rewriting the module. + +The completed conformance stage checks each real unitary, measurement, and reset +operation against that same homogeneous capability set. It rejects dynamic qubit +allocations and `qco.static` identifiers absent from the target, without tracing +every operation operand through its SSA lineage. Diagnostics report the actual +operation spelling, arity, and parameter count. ### Milestone 3: Prove the contracts and hand off one atomic change The focused target-synthesis test binary now proves the three stage boundaries, -including unitary equivalence for profitable optimization, SWAP lowering, and a -single reported CZ orientation with bidirectional semantic support. It also -proves explicit CZ emission, failure-atomic diagnostics, and that native SWAP -and `qco.pow` shells remain untouched. The decomposition and compiler suites -prove that the shared basis type covers every supported entangler and that -removing the old high-level menu API does not break the remaining compiler -pipeline. +including unitary equivalence for gate fusion, SWAP lowering, and homogeneous CZ +support. It also proves explicit CZ emission, failure-atomic diagnostics, +static-site validation, dynamic-allocation rejection, and that native SWAP and +`qco.pow` shells remain untouched. The decomposition and compiler suites prove +that the shared basis type covers every supported entangler and that removing +the old high-level menu API does not break the remaining compiler pipeline. This slice must not add `compileForTarget`, alter default pipeline composition, or remove the coupling-only `placeAndRoute` overload; those are PIPE @@ -152,20 +155,21 @@ requires separate revision-scoped authorization. ## Surprises & Discoveries - Observation: The old two-qubit pass performed four jobs: single-qubit - target-basis lowering, two-qubit optimization, isolated two-qubit lowering, - and residual menu checking. Evidence: its `hasNonNativeGate` condition could + target-basis lowering, two-qubit gate fusion, isolated two-qubit lowering, and + residual menu checking. Evidence: its `hasNonNativeGate` condition could rewrite a pre-routing window without reducing the entangler count. - Observation: CT-01 already recognizes all fifteen gates understood by the - deleted menu, including provider aliases and the same entangler preference. - The old `NativeGateset` duplicated the enum, parser switch, basis resolution, - and operation classifier. + deleted menu, including gate aliases and the same entangler preference. The + old `NativeGateset` duplicated the enum, parser switch, basis resolution, and + operation classifier. - Observation: Mapping emits `qco.static` with hardware identifiers, while - `CompilerTarget` permits sparse provider IDs. Evidence: the conformance tests - use sites 10 and 20 and distinguish loci `[10, 20]` and `[20, 10]`. + `CompilerTarget` permits sparse target IDs. Conformance only needs to validate + those declarations once; operation capabilities are homogeneous and do not + require operand-by-operand site tracing. - Observation: Applying an MLIR greedy rewrite driver can reorder an unrelated constant even when the quantum pattern does not match. The target-specific transforms now precompute their work and use `IRRewriter` directly, so a no-op - optimization preserves the module byte-for-byte. + fusion preserves the module byte-for-byte. - Observation: Initial configuration required network access to fetch pinned repository dependencies. Once fetched into the worktree-local build tree, focused compilation and tests were repeatable without source workarounds. @@ -178,11 +182,10 @@ requires separate revision-scoped authorization. decomposition, optional routing, and late native synthesis, with a test that routed SWAPs disappear. Its `targetNative` Python duck typing and coupling CLI are intentionally excluded. -- Observation: IQM reports each CZ edge once. Raw `Operation::loci()` and - `Operation::supports()` therefore remain ordered, while semantic - `CompilerTarget::supports()` normalizes recognized operand-symmetric gates - across both orientations. This establishes the bidirectional invariant before - synthesis and removes direction handling from emission. +- Observation: Current targets expose homogeneous gate sets. Ordered + `Operation::siteTuples()` retain calibration data, while + `CompilerTarget::supports()` depends only on canonical operation name, arity, + and parameter count. - Observation: `qco.pow`, like `qco.ctrl` and `qco.inv`, is a target-visible unitary shell with a region body. Synthesis and conformance must classify the shell and skip its implementation body. @@ -190,9 +193,10 @@ requires separate revision-scoped authorization. multi-operation fusion at the run head because the rewrite erases operations the driver has not visited. Precollecting non-overlapping run heads and using `IRRewriter` directly is both safer and lighter. -- Observation: Repeated provider-site tracing was quadratic on long SSA wire - chains, and profitable windows decomposed the same two-qubit matrix twice. - Path compression and split decomposition/emission remove both costs. +- Observation: The earlier per-operation target-site tracer was unnecessary once + support became homogeneous. Removing it eliminates both its quadratic worst + case and its structured-control-flow special cases. Profitable windows also + reuse the same prepared Weyl decomposition for counting and emission. ## Decision Log @@ -217,21 +221,22 @@ requires separate revision-scoped authorization. authority; routing SWAPs still lower on ordinary targets that do not report SWAP, while a target-native SWAP must remain legal even when the target has no global synthesis basis. Date/Author: 2026-08-03, GPT-5.6 via Codex. -- Decision: Normalize recognized operand-symmetric capabilities inside - `CompilerTarget` and require every globally selected entangler to be - semantically supported in both directions. Rationale: raw calibration loci - retain provider order, IQM need not duplicate CZ data, and synthesis carries - no operand-reversal option. Date/Author: 2026-08-03, GPT-5.6 via Codex. -- Decision: Preflight all target-lowering needs, cache provider provenance, and - apply planned rewrites directly with `IRRewriter`. Rationale: failure remains - atomic, valid lineage tracing is amortized linear, and generic greedy/fixpoint - work is unnecessary. Date/Author: 2026-08-03, GPT-5.6 via Codex. +- Decision: Treat operation capabilities as homogeneous across a target. Ordered + site tuples retain calibration only; synthesis and conformance query canonical + name, arity, and parameter count without directional fallback. Rationale: + current target gate sets are uniform and bidirectional, so site tracing and + reverse probes add code without changing compilation behavior. Date/Author: + 2026-08-03, GPT-5.6 via Codex. +- Decision: Preflight all target-lowering needs and apply planned rewrites + directly with `IRRewriter`. Rationale: failure remains atomic and generic + greedy/fixpoint work is unnecessary. Date/Author: 2026-08-03, GPT-5.6 via + Codex. - Decision: Do not require `CompilerTarget::synthesisBasis()` at pass construction or pass entry. Rationale: absent operations mean all operations are native, and an incomplete explicit target can still describe a conforming program. Missing-basis failure matters only after an unsupported operation actually needs lowering. Date/Author: 2026-08-03, GPT-5.6 via Codex. -- Decision: Keep optimization, target-native synthesis, and conformance as +- Decision: Keep gate fusion, target-native synthesis, and conformance as separate manual factories rather than textual passes. Rationale: `CompilerTarget` is an immutable typed C++ value that cannot be faithfully represented by generic pass options, and separate factories make each stage @@ -245,40 +250,34 @@ requires separate revision-scoped authorization. ## Outcomes & Retrospective -The implementation now has one capability authority and three separately -observable transform stages. On the merged MAP-01 base, twenty-one focused tests -pass along with all 199 decomposition and 215 compiler tests. The relevant CMake -interface-header targets, CLI build and surface checks, and full Python stub -regeneration pass; regeneration leaves the tracked stubs unchanged. The latest -working-tree review removes the decomposition basis adapter and general greedy -rewrite machinery, while adding bidirectional symmetric-capability, explicit CZ -emission, and failure-atomic lowering regressions. Changed-file hooks, full -repository lint, stale-surface search, and `git diff --check` pass. Focused -`clang-tidy` is clean for every changed implementation and test source and for -the changed `Target.h` and `Euler.h` surfaces; `Weyl.h` reports only three -pre-existing warnings on unchanged lines. Draft PR #1998's pre-MAP exact head -passed every check, including 92.1% C++ patch coverage. Publication and -replacement exact-head CI for this latest review revision remain. A fresh -independent review approved the exact working tree with no must-fix or -efficiency findings after independently rerunning the target-synthesis, -compiler-target, and decomposition tests. - -The main design lesson is that target support and resynthesis profitability must -not share a configuration surface. The canonical U/CZ optimizer is useful -without hardware knowledge, while post-routing lowering and conformance require -the exact target and provider-site order. +The implementation now has one homogeneous capability authority and three +separately observable transform stages. The latest cleanup removes the +decomposition basis adapter, general greedy rewrite machinery, directional +capability fallback, and per-operation target-site tracer. It retains explicit +CZ emission and failure-atomic lowering while adding direct static-site and +dynamic-allocation and quantum-function-input conformance coverage. Final +release builds pass 21 target-synthesis, 215 compiler, 33 dialect-utils, 27 +mapping, and 199 decomposition tests. The SC device suite passes 41 tests with +one expected job-ID skip. Both affected interface-header targets build, all +repository hooks pass, focused LLVM 22.1.8 `clang-tidy` reports no new +diagnostics, and an independent review approves the exact working tree. + +The main design lesson is that target support and fusion profitability must not +share a configuration surface. Canonical U/CZ gate fusion is useful without +hardware knowledge, while post-routing lowering and conformance require the +target's homogeneous operation set and declared static sites. ## Context and Orientation `mlir/include/mlir/Compiler/Target.h` defines the immutable target. Its -`supports(Operation*, locus)` query recognizes QCO operation semantics and -checks provider site IDs, semantic loci, arity, and parameter count while raw -operation loci retain provider order. Its `synthesisBasis()` query returns one -globally usable single-qubit basis and entangler only when both exist. +`supports(Operation*)` query recognizes QCO operation semantics and checks +canonical name, arity, and parameter count. Ordered operation site tuples retain +calibration only. Its `synthesisBasis()` query returns one usable single-qubit +basis and entangler only when both exist. `mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp` contains -the constant-window scanner, three pass implementations, provider-site tracing, -and diagnostics. Public factory declarations live in +the two-qubit gate-fusion scanner, three pass implementations, static-site +validation, and diagnostics. Public factory declarations live in `mlir/include/mlir/Dialect/QCO/Transforms/Passes.h`. `mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h` aliases the @@ -288,10 +287,10 @@ entangler decomposer, returns a prepared decomposition, and emits its single-qubit factors without recomputing it. QCO qubits use linear static single assignment: each operation consumes a qubit -value and returns its successor. A mapped hardware qubit starts at `qco.static`; -structured control flow passes the value through block arguments and results. -Conformance must trace that value chain rather than interpret SSA positions as -hardware IDs. +value and returns its successor. After mapping, `qco.static` operations declare +the assigned target sites. Since operation capabilities are homogeneous, +conformance validates these declarations once and does not trace each operand's +SSA lineage. Focused tests are in `mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp`. @@ -305,10 +304,11 @@ rewires Euler and Weyl synthesis directly to the target-owned basis type. It also removes the obsolete generated pass, registration, CLI option, `QCOProgram` method, binding, stub, and their menu tests. -The new source retains the proven two-qubit window scanner but separates its -uses. The optimizer compares original and canonical entangler counts. Target -synthesis classifies actual mapped operations and rewrites only lowering needs. -Conformance performs an independent read-only walk and exact target query. +The new source retains the proven two-qubit gate-fusion scanner but separates +its uses. The fusion pass compares original and canonical entangler counts. +Target synthesis classifies actual mapped operations and rewrites only lowering +needs. Conformance performs an independent read-only walk and exact target +query. The final work updates the existing Unreleased changelog entry without an upgrade note, formats changed sources, runs focused and repository-required @@ -362,27 +362,26 @@ checks: Acceptance requires the following observable behavior: -- Two adjacent constant CX operations optimize away with equivalent unitary +- Two adjacent constant CX operations fuse away with equivalent unitary behavior, while a three-CX SWAP form, an isolated SWAP, and a runtime RXX run remain quantum-structurally unchanged before routing. - Target-native synthesis removes unsupported ordinary SWAP and produces only operations accepted by the selected target basis, preserving the complete unitary. A target-native SWAP remains unchanged without requiring a synthesis basis. -- One reported orientation of a recognized symmetric entangler establishes - semantic support in both directions while raw provider data remains ordered. - Synthesis preserves complete unitary behavior without an operand-reversal - option, and a native `qco.pow` shell is checked without separately rejecting - its implementation body. +- Homogeneous operation capabilities apply in both operand orientations while + ordered site tuples retain calibration data only. Synthesis preserves complete + unitary behavior without an operand-reversal option, and a native `qco.pow` + shell is checked without separately rejecting its implementation body. - An absent operation set succeeds without synthesis. An explicit incomplete - target succeeds for supported operations and reports “no globally usable - synthesis basis” only when an unsupported operation actually needs lowering. + target succeeds for supported operations and reports “no usable synthesis + basis” only when an unsupported operation actually needs lowering. - A supported runtime-parameterized gate remains unchanged. An unsupported runtime gate reports that its unitary matrix is unavailable at compile time without partially rewriting an earlier constant gate. -- Conformance distinguishes sparse provider IDs and ordered direction, rejects - operation-type, arity, parameter-count, unknown-site, and measurement - mismatches, and traces QCO and SCF structured control flow. +- Conformance accepts sparse target IDs, rejects operation-type, arity, + parameter-count, unknown-site, measurement, and dynamic-allocation mismatches, + and does not reconstruct per-operation site provenance. - The duplicate enum/parser and all native-menu text, CLI, C++ program, Python, and generated pass surfaces are absent. - Focused tests, decomposition tests, compiler tests, changed-file checks, @@ -419,8 +418,8 @@ Relevant #1969 source commits are `3be6d8e43` for target-native naming and both authored by Simon Hofmann. Independent read-only review initially blocked the candidate on three issues: -single-orientation symmetric-entangler loci, a SWAP capability override, and -nested `qco.pow` body checking. The exact remediated source rebuilt +single-orientation symmetric-entangler site tuples, a SWAP capability override, +and nested `qco.pow` body checking. The exact remediated source rebuilt successfully, all sixteen focused tests passed, and the reviewer reported that all three findings were closed with no new code blockers. The reviewer also reran `git diff --check` against the initial development base. @@ -446,18 +445,17 @@ warnings on unchanged `Weyl.h` lines 167 and 171. Changed-file hooks, full repository lint, stale-surface search outside historical plans, both worktree and committed `git diff --check`, and the final status audit pass. -The coverage follow-up added regressions for profitable interleaved two-qubit -optimization, constant single-qubit target lowering, and site tracing through -`scf.while`, `qco.index_switch`, measurement, and reset. The subsequent -simplification deletes the basis-adapter coverage test and adds structural CZ -emission plus failure-atomic lowering coverage. The target-synthesis and -decomposition suites now pass 21 and 199 tests. +The coverage follow-up added regressions for interleaved two-qubit gate fusion, +constant single-qubit target lowering, and the former site tracer. The latest +simplification deletes tracer-only control-flow tests and adds direct +static-site and dynamic-allocation coverage alongside structural CZ emission and +failure-atomic lowering. After MAP-01 merged as `c9e0c0ca5`, the four SYN commits were rebased onto that commit. The integration range-diff removes only a compiler-target link already provided by MAP-01, retains MAP-01's canonical undirected coupling input in the compiler API test, and keeps both PR links in the changelog. The lint and -coverage follow-up patches remain identical. Fresh release builds pass 20 target +coverage follow-up patches remain identical. Fresh release builds pass 21 target synthesis, 199 decomposition, and 215 compiler tests, including the focused `QCOProgramOptimizationAPIs` test. @@ -468,7 +466,7 @@ libraries. `MLIRQCOTransforms` publicly links `MQTCompilerTarget` because its public decomposition headers use compiler-target basis types. The public, independently constructible factories are: - std::unique_ptr createOptimizeTwoQubitUnitaryRuns(); + std::unique_ptr createFuseTwoQubitGates(); std::unique_ptr createTargetNativeSynthesis(const CompilerTarget& target); std::unique_ptr diff --git a/CHANGELOG.md b/CHANGELOG.md index 73a66eb5d0..b31838c8ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,9 +32,9 @@ releases may include breaking changes. [**@li-mingbao**], [**@DRovara**], [**@MatthiasReumann**]) - ✨ Add decision diagram-based construction and simulation of static unitary QCO functions ([#1915]) ([**@simon1hofmann**]) -- ✨ Add profitable target-independent two-qubit unitary-window optimization, - target-native post-routing synthesis, and exact operation/locus conformance - ([#1865], [#1961], [#1998]) ([**@simon1hofmann**], [**@burgholzer**]) +- ✨ Add target-independent two-qubit gate fusion, target-native post-routing + synthesis, and operation-capability and static-site conformance ([#1865], + [#1961], [#1998]) ([**@simon1hofmann**], [**@burgholzer**]) - ✨ Add a `decompose-multi-controlled` pass for decomposing controlled X, Z, RCCX, and constant-angle phase gates with a configurable `min-controls` threshold ([#1810]) ([**@simon1hofmann**]) @@ -68,7 +68,7 @@ releases may include breaking changes. [#1676], [#1706], [#1776], [#1836], [#1934]) ([**@denialhaag**], [**@burgholzer**]) - ✨ Add a `place-and-route` pass for mapping scalar- and tensor-allocated - circuits to compiler-target topologies while preserving provider site IDs and + circuits to compiler-target topologies while preserving target site IDs and materializing routing workspace on demand ([#1537], [#1547], [#1568], [#1581], [#1583], [#1588], [#1600], [#1664], [#1709], [#1716], [#1748], [#1805], [#1870], [#1904], [#1911], [#1951], [#1997]) ([**@MatthiasReumann**], diff --git a/docs/qdmi/sc_device.md b/docs/qdmi/sc_device.md index 833657056a..2186c99135 100644 --- a/docs/qdmi/sc_device.md +++ b/docs/qdmi/sc_device.md @@ -86,12 +86,12 @@ and `mqt.sc.iqm.emerald`. The snapshots were derived from authenticated IQM architecture and calibration data retrieved on 2 August 2026 through [QDMI-on-IQM](https://github.com/iqm-finland/QDMI-on-IQM). Site names and -topology came from each static architecture, operation loci from its default -dynamic architecture, and available T1, T2, and per-locus fidelities from its -default calibration-set quality metrics. Coherence times were rounded to the -nearest nanosecond and stored as integral values using unit `us` and scale -factor `0.001`. They are historical compiler-facing snapshots, not claims about -current hardware calibration. +topology came from each static architecture, operation sites from its default +dynamic architecture, and available T1, T2, and fidelities for individual site +tuples from its default calibration-set quality metrics. Coherence times were +rounded to the nearest nanosecond and stored as integral values using unit `us` +and scale factor `0.001`. They are historical compiler-facing snapshots, not +claims about current hardware calibration. The IQM interface did not report operation durations, so the files intentionally omit them. diff --git a/mlir/include/mlir/Compiler/Target.h b/mlir/include/mlir/Compiler/Target.h index e2fa6c6d0b..b39bf9f668 100644 --- a/mlir/include/mlir/Compiler/Target.h +++ b/mlir/include/mlir/Compiler/Target.h @@ -27,10 +27,9 @@ namespace mlir { class Operation; /** - * @brief Immutable, provider-independent description of an MLIR compiler - * target. + * @brief Immutable description of an MLIR compiler target. * - * @details Hardware sites retain their provider-defined nonnegative i64 + * @details Hardware sites retain their target-defined nonnegative i64 * identifiers. Routing algorithms use dense zero-based vertices in site order. * An absent topology means all-to-all connectivity. An absent operation set * means that every operation is native; a present empty set means that no @@ -53,7 +52,7 @@ class CompilerTarget { public: DurationUnit(std::string unit, double scaleFactor); - /// Return the provider-defined duration unit. + /// Return the target's duration unit. [[nodiscard]] llvm::StringRef unit() const noexcept; /// Return the positive finite multiplier for raw timing values. @@ -65,7 +64,7 @@ class CompilerTarget { }; /** - * @brief A hardware site and its optional provider metadata. + * @brief A hardware site and its optional target metadata. */ class Site { public: @@ -73,10 +72,10 @@ class CompilerTarget { std::optional t1 = std::nullopt, std::optional t2 = std::nullopt); - /// Return the provider-defined nonnegative site identifier. + /// Return the target-defined nonnegative site identifier. [[nodiscard]] SiteId id() const noexcept; - /// Return the provider-defined site name, if available. + /// Return the reported site name, if available. [[nodiscard]] std::optional name() const noexcept; /// Return the raw T1 coherence time, if available. @@ -93,15 +92,15 @@ class CompilerTarget { }; /** - * @brief An ordered hardware locus and its optional calibration data. + * @brief Calibration data for an ordered tuple of hardware sites. */ - class OperationLocus { + class SiteTuple { public: - explicit OperationLocus(std::vector sites, - std::optional duration = std::nullopt, - std::optional fidelity = std::nullopt); + explicit SiteTuple(std::vector sites, + std::optional duration = std::nullopt, + std::optional fidelity = std::nullopt); - /// Return the ordered provider site identifiers. + /// Return the ordered target site identifiers. [[nodiscard]] llvm::ArrayRef sites() const noexcept; /// Return the raw operation duration, if available. @@ -117,22 +116,22 @@ class CompilerTarget { }; /** - * @brief An operation capability reported by a target provider. + * @brief An operation capability described by a target. * - * @details The provider name is retained verbatim while @ref canonicalName - * contains its normalized compiler spelling. An absent locus set means the - * operation applies to every valid ordered tuple of its arity; a present - * empty set supports no locus. + * @details The reported name is retained verbatim while + * @ref canonicalName contains its normalized compiler spelling. Operations + * are available throughout the target; site tuples carry optional + * site-specific calibration data only. */ class Operation { public: - Operation(std::string providerName, size_t numQubits, size_t numParameters, - std::optional> loci = std::nullopt, + Operation(std::string name, size_t numQubits, size_t numParameters, + std::vector siteTuples = {}, std::optional duration = std::nullopt, std::optional fidelity = std::nullopt); - /// Return the exact operation name reported by the provider. - [[nodiscard]] llvm::StringRef providerName() const noexcept; + /// Return the exact reported operation name. + [[nodiscard]] llvm::StringRef name() const noexcept; /// Return the canonical lower-case compiler operation name. [[nodiscard]] llvm::StringRef canonicalName() const noexcept; @@ -143,11 +142,8 @@ class CompilerTarget { /// Return the number of real-valued operation parameters. [[nodiscard]] size_t numParameters() const noexcept; - /// Return whether this operation is available at every valid locus. - [[nodiscard]] bool hasGlobalLoci() const noexcept; - - /// Return the explicitly supported ordered loci. - [[nodiscard]] llvm::ArrayRef loci() const noexcept; + /// Return ordered site-specific calibration data. + [[nodiscard]] llvm::ArrayRef siteTuples() const noexcept; /// Return the raw default operation duration, if available. [[nodiscard]] std::optional duration() const noexcept; @@ -155,15 +151,12 @@ class CompilerTarget { /// Return the default operation fidelity, if available. [[nodiscard]] std::optional fidelity() const noexcept; - /// Return whether this capability supports an ordered hardware locus. - [[nodiscard]] bool supports(llvm::ArrayRef locus) const; - private: - std::string providerName_; + std::string name_; std::string canonicalName_; size_t numQubits_; size_t numParameters_; - std::optional> loci_; + std::vector siteTuples_; std::optional duration_; std::optional fidelity_; }; @@ -232,7 +225,7 @@ class CompilerTarget { std::optional durationUnit = std::nullopt); /** - * @brief Construct an unnamed target from detailed provider sites. + * @brief Construct an unnamed target from detailed sites. */ explicit CompilerTarget( std::vector sites, @@ -241,7 +234,7 @@ class CompilerTarget { std::optional durationUnit = std::nullopt); /** - * @brief Construct a named target from detailed provider sites. + * @brief Construct a named target from detailed sites. */ CompilerTarget( std::string name, std::vector sites, @@ -254,7 +247,7 @@ class CompilerTarget { CompilerTarget& operator=(const CompilerTarget&) noexcept = default; ~CompilerTarget() = default; - /// Return the target/device name, if provided. + /// Return the target name, if provided. [[nodiscard]] std::optional name() const noexcept; /// Return the unit shared by all raw timing metadata, if provided. @@ -267,20 +260,20 @@ class CompilerTarget { /// Return detailed sites in dense compiler-vertex order. [[nodiscard]] llvm::ArrayRef sites() const noexcept; - /// Return provider site identifiers in dense compiler-vertex order. + /// Return target site identifiers in dense compiler-vertex order. [[nodiscard]] llvm::ArrayRef siteIds() const noexcept; - /// Return the dense compiler vertex for a provider site identifier. + /// Return the dense compiler vertex for a target site identifier. [[nodiscard]] std::optional vertexForSite(SiteId site) const noexcept; - /// Return the provider site identifier for a dense compiler vertex. + /// Return the target site identifier for a dense compiler vertex. [[nodiscard]] SiteId siteForVertex(size_t vertex) const; /// Return whether the target contains an explicit coupling topology. [[nodiscard]] bool hasExplicitTopology() const noexcept; /** - * @brief Return sorted canonical undirected couplings in provider site IDs. + * @brief Return sorted canonical undirected couplings in target site IDs. */ [[nodiscard]] llvm::ArrayRef couplings() const noexcept; @@ -304,36 +297,24 @@ class CompilerTarget { /// Return whether the target contains an explicit operation set. [[nodiscard]] bool hasExplicitOperations() const noexcept; - /// Return provider operation capabilities in provider order. + /// Return operation capabilities in reported order. [[nodiscard]] llvm::ArrayRef operations() const noexcept; /** - * @brief Return whether an operation is semantically supported at a hardware - * locus. - * - * @details Raw provider loci remain ordered. Recognized operand-symmetric - * gates are supported in both orientations of each reported two-site locus. + * @brief Return whether an operation capability is supported by the target. */ [[nodiscard]] bool - supportsOperation(llvm::StringRef name, llvm::ArrayRef locus, + supportsOperation(llvm::StringRef name, size_t numQubits, std::optional numParameters = std::nullopt) const; - /** - * @brief Return whether a QCO operation is semantically supported at a locus. - */ - [[nodiscard]] bool supports(::mlir::Operation* operation, - llvm::ArrayRef locus) const; + /// Return whether a QCO operation is supported by the target. + [[nodiscard]] bool supports(::mlir::Operation* operation) const; - /** - * @brief Return whether a recognized gate is semantically supported at a - * locus. - */ - [[nodiscard]] bool supports(GateKind gate, - llvm::ArrayRef locus) const; + /// Return whether a recognized gate is supported by the target. + [[nodiscard]] bool supports(GateKind gate) const; - /// Return recognized gates that are usable across the complete target. - [[nodiscard]] llvm::ArrayRef - globallySupportedGates() const noexcept; + /// Return the recognized gates supported by the target. + [[nodiscard]] llvm::ArrayRef supportedGates() const noexcept; /// Return one complete globally usable synthesis basis, if available. [[nodiscard]] std::optional synthesisBasis() const noexcept; diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h index 6ce151b6e3..de6e78952d 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h @@ -34,9 +34,9 @@ namespace mlir::qco { #include "mlir/Dialect/QCO/Transforms/Passes.h.inc" // IWYU pragma: export /** - * @brief Create the target-independent pre-routing two-qubit optimizer. + * @brief Create target-independent two-qubit gate fusion. */ -[[nodiscard]] std::unique_ptr createOptimizeTwoQubitUnitaryRuns(); +[[nodiscard]] std::unique_ptr createFuseTwoQubitGates(); /** * @brief Create post-routing synthesis for one immutable compiler target. diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index ad2f986533..20975689b5 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -99,7 +99,7 @@ def MappingPass : Pass<"place-and-route", "mlir::ModuleOp"> { uses only the target's undirected coupling topology: arbitrary one-qubit operations pass through, and every two-qubit operation is routed to adjacent target vertices. Higher-arity unitaries must be decomposed before mapping. - Provider-defined site identifiers are retained in the resulting + Target-defined site identifiers are retained in the resulting `qco.static` operations. First, the pass assigns static qubits to the dynamically allocated ones by creating an initial dynamic-to-static diff --git a/mlir/lib/Compiler/Target.cpp b/mlir/lib/Compiler/Target.cpp index cc7eceb745..eaaaa55c83 100644 --- a/mlir/lib/Compiler/Target.cpp +++ b/mlir/lib/Compiler/Target.cpp @@ -14,7 +14,6 @@ #include "mlir/Dialect/QCO/IR/QCOOps.h" #include -#include #include #include #include @@ -44,107 +43,61 @@ using GateKind = CompilerTarget::GateKind; using SiteId = CompilerTarget::SiteId; struct GateSpecification { - GateKind kind; + GateKind kind{}; llvm::StringLiteral name; - size_t numQubits; - size_t numParameters; - bool symmetric; + size_t numQubits{}; + size_t numParameters{}; }; constexpr std::array GATE_SPECIFICATIONS{ - GateSpecification{.kind = GateKind::U, - .name = "u", - .numQubits = 1, - .numParameters = 3, - .symmetric = false}, - GateSpecification{.kind = GateKind::X, - .name = "x", - .numQubits = 1, - .numParameters = 0, - .symmetric = false}, - GateSpecification{.kind = GateKind::SX, - .name = "sx", - .numQubits = 1, - .numParameters = 0, - .symmetric = false}, - GateSpecification{.kind = GateKind::RZ, - .name = "rz", - .numQubits = 1, - .numParameters = 1, - .symmetric = false}, - GateSpecification{.kind = GateKind::RX, - .name = "rx", - .numQubits = 1, - .numParameters = 1, - .symmetric = false}, - GateSpecification{.kind = GateKind::RY, - .name = "ry", - .numQubits = 1, - .numParameters = 1, - .symmetric = false}, - GateSpecification{.kind = GateKind::R, - .name = "r", - .numQubits = 1, - .numParameters = 2, - .symmetric = false}, + GateSpecification{ + .kind = GateKind::U, .name = "u", .numQubits = 1, .numParameters = 3}, + GateSpecification{ + .kind = GateKind::X, .name = "x", .numQubits = 1, .numParameters = 0}, + GateSpecification{ + .kind = GateKind::SX, .name = "sx", .numQubits = 1, .numParameters = 0}, + GateSpecification{ + .kind = GateKind::RZ, .name = "rz", .numQubits = 1, .numParameters = 1}, + GateSpecification{ + .kind = GateKind::RX, .name = "rx", .numQubits = 1, .numParameters = 1}, + GateSpecification{ + .kind = GateKind::RY, .name = "ry", .numQubits = 1, .numParameters = 1}, + GateSpecification{ + .kind = GateKind::R, .name = "r", .numQubits = 1, .numParameters = 2}, GateSpecification{.kind = GateKind::RXX, .name = "rxx", .numQubits = 2, - .numParameters = 1, - .symmetric = true}, + .numParameters = 1}, GateSpecification{.kind = GateKind::RYY, .name = "ryy", .numQubits = 2, - .numParameters = 1, - .symmetric = true}, + .numParameters = 1}, GateSpecification{.kind = GateKind::RZX, .name = "rzx", .numQubits = 2, - .numParameters = 1, - .symmetric = false}, + .numParameters = 1}, GateSpecification{.kind = GateKind::RZZ, .name = "rzz", .numQubits = 2, - .numParameters = 1, - .symmetric = true}, + .numParameters = 1}, GateSpecification{.kind = GateKind::ISWAP, .name = "iswap", .numQubits = 2, - .numParameters = 0, - .symmetric = true}, - GateSpecification{.kind = GateKind::CZ, - .name = "cz", - .numQubits = 2, - .numParameters = 0, - .symmetric = true}, - GateSpecification{.kind = GateKind::CX, - .name = "cx", - .numQubits = 2, - .numParameters = 0, - .symmetric = false}, + .numParameters = 0}, + GateSpecification{ + .kind = GateKind::CZ, .name = "cz", .numQubits = 2, .numParameters = 0}, + GateSpecification{ + .kind = GateKind::CX, .name = "cx", .numQubits = 2, .numParameters = 0}, GateSpecification{.kind = GateKind::ECR, .name = "ecr", .numQubits = 2, - .numParameters = 0, - .symmetric = false}, + .numParameters = 0}, }; } // namespace -[[nodiscard]] static const GateSpecification& -gateSpecification(const GateKind gate) { - // NOLINTNEXTLINE(readability-qualified-auto) - const auto found = - std::ranges::find(GATE_SPECIFICATIONS, gate, &GateSpecification::kind); - if (found == GATE_SPECIFICATIONS.end()) { - throw std::invalid_argument("Unknown compiler target gate kind"); - } - return *found; -} - -[[nodiscard]] static std::string -canonicalOperationName(const StringRef providerName) { - auto canonical = providerName.trim().lower(); +[[nodiscard]] static std::string canonicalOperationName(const StringRef name) { + auto canonical = name.trim().lower(); if (canonical == "prx") { canonical = "r"; } else if (canonical == "u3") { @@ -243,48 +196,45 @@ std::optional CompilerTarget::Site::t2() const noexcept { return t2_; } -CompilerTarget::OperationLocus::OperationLocus( - std::vector sites, const std::optional duration, - const std::optional fidelity) +CompilerTarget::SiteTuple::SiteTuple(std::vector sites, + const std::optional duration, + const std::optional fidelity) : sites_(std::move(sites)), duration_(duration), fidelity_(fidelity) { - DenseSet uniqueSites; - uniqueSites.reserve(sites_.size()); + std::set uniqueSites; for (const auto site : sites_) { if (site < 0) { throw std::invalid_argument( - "Compiler target operation locus contains a negative site ID"); + "Compiler target site tuple contains a negative site ID"); } if (!uniqueSites.insert(site).second) { throw std::invalid_argument( - "Compiler target operation locus contains a duplicate site"); + "Compiler target site tuple contains a duplicate site"); } } - validateFidelity(fidelity_, "Compiler target operation locus fidelity"); + validateFidelity(fidelity_, "Compiler target site-tuple fidelity"); } -ArrayRef CompilerTarget::OperationLocus::sites() const noexcept { +ArrayRef CompilerTarget::SiteTuple::sites() const noexcept { return sites_; } -std::optional -CompilerTarget::OperationLocus::duration() const noexcept { +std::optional CompilerTarget::SiteTuple::duration() const noexcept { return duration_; } -std::optional -CompilerTarget::OperationLocus::fidelity() const noexcept { +std::optional CompilerTarget::SiteTuple::fidelity() const noexcept { return fidelity_; } -CompilerTarget::Operation::Operation( - std::string providerName, const size_t numQubits, - const size_t numParameters, std::optional> loci, - const std::optional duration, - const std::optional fidelity) - : providerName_(std::move(providerName)), - canonicalName_(canonicalOperationName(providerName_)), +CompilerTarget::Operation::Operation(std::string name, const size_t numQubits, + const size_t numParameters, + std::vector siteTuples, + const std::optional duration, + const std::optional fidelity) + : name_(std::move(name)), canonicalName_(canonicalOperationName(name_)), numQubits_(numQubits), numParameters_(numParameters), - loci_(std::move(loci)), duration_(duration), fidelity_(fidelity) { + siteTuples_(std::move(siteTuples)), duration_(duration), + fidelity_(fidelity) { if (canonicalName_.empty()) { throw std::invalid_argument( "Compiler target operation name must not be empty"); @@ -295,26 +245,22 @@ CompilerTarget::Operation::Operation( } validateFidelity(fidelity_, "Compiler target operation fidelity"); - if (!loci_) { - return; - } - std::set> uniqueLoci; - for (const auto& locus : *loci_) { - if (locus.sites().size() != numQubits_) { + std::set> uniqueSiteCombinations; + for (const auto& siteTuple : siteTuples_) { + if (siteTuple.sites().size() != numQubits_) { throw std::invalid_argument( - "Compiler target operation locus does not match its declared arity"); + "Compiler target operation site tuple does not match its arity"); } - if (!uniqueLoci.emplace(locus.sites().begin(), locus.sites().end()) + if (!uniqueSiteCombinations + .emplace(siteTuple.sites().begin(), siteTuple.sites().end()) .second) { throw std::invalid_argument( - "Compiler target operation contains a duplicate locus"); + "Compiler target operation contains a duplicate site tuple"); } } } -StringRef CompilerTarget::Operation::providerName() const noexcept { - return providerName_; -} +StringRef CompilerTarget::Operation::name() const noexcept { return name_; } StringRef CompilerTarget::Operation::canonicalName() const noexcept { return canonicalName_; @@ -328,16 +274,9 @@ size_t CompilerTarget::Operation::numParameters() const noexcept { return numParameters_; } -bool CompilerTarget::Operation::hasGlobalLoci() const noexcept { - return !loci_; -} - -ArrayRef -CompilerTarget::Operation::loci() const noexcept { - if (!loci_) { - return {}; - } - return *loci_; +ArrayRef +CompilerTarget::Operation::siteTuples() const noexcept { + return siteTuples_; } std::optional CompilerTarget::Operation::duration() const noexcept { @@ -348,34 +287,15 @@ std::optional CompilerTarget::Operation::fidelity() const noexcept { return fidelity_; } -bool CompilerTarget::Operation::supports(const ArrayRef locus) const { - if (locus.size() != numQubits_) { - return false; - } - llvm::SmallDenseSet uniqueSites; - uniqueSites.reserve(locus.size()); - if (!llvm::all_of(locus, [&](const auto site) { - return site >= 0 && uniqueSites.insert(site).second; - })) { - return false; - } - return !loci_ || llvm::any_of(*loci_, [&](const auto& candidate) { - return std::ranges::equal(candidate.sites(), locus); - }); -} - struct CompilerTarget::Storage { Storage(std::optional targetName, std::vector targetSites, std::optional> targetCouplings, std::optional> targetOperations, std::optional targetDurationUnit); - [[nodiscard]] bool validLocus(ArrayRef locus) const; [[nodiscard]] bool - supportsOperation(StringRef name, ArrayRef locus, + supportsOperation(StringRef name, size_t numQubits, std::optional numParameters) const; - [[nodiscard]] bool gateIsGloballySupported(GateKind gate) const; - [[nodiscard]] bool hasGlobalGate(GateKind gate) const; [[nodiscard]] std::optional resolveSynthesisBasis() const; std::optional name; @@ -389,7 +309,7 @@ struct CompilerTarget::Storage { size_t maximumDegree = 0; std::optional> operations; llvm::StringMap> capabilities; - SmallVector globalGates; + SmallVector supportedGates; std::optional basis; }; @@ -484,12 +404,16 @@ CompilerTarget::Storage::Storage( if (operations) { for (const auto [index, operation] : llvm::enumerate(*operations)) { - for (const auto& locus : operation.loci()) { - if (llvm::any_of(locus.sites(), [&](const auto site) { + if (operation.numQubits() > sites.size()) { + throw std::invalid_argument( + "Compiler target operation arity exceeds its site count"); + } + for (const auto& siteTuple : operation.siteTuples()) { + if (llvm::any_of(siteTuple.sites(), [&](const auto site) { return !siteToVertex.contains(site); })) { - throw std::invalid_argument( - "Compiler target operation locus references an unknown site"); + throw std::invalid_argument("Compiler target operation site tuple " + "references an unknown site"); } } capabilities[operation.canonicalName()].emplace_back(index); @@ -502,8 +426,8 @@ CompilerTarget::Storage::Storage( const auto hasOperationTiming = operations && llvm::any_of(*operations, [](const auto& operation) { return operation.duration().has_value() || - llvm::any_of(operation.loci(), [](const auto& locus) { - return locus.duration().has_value(); + llvm::any_of(operation.siteTuples(), [](const auto& siteTuple) { + return siteTuple.duration().has_value(); }); }); if ((hasSiteTiming || hasOperationTiming) && !durationUnit) { @@ -512,29 +436,19 @@ CompilerTarget::Storage::Storage( } for (const auto& specification : GATE_SPECIFICATIONS) { - if (gateIsGloballySupported(specification.kind)) { - globalGates.emplace_back(specification.kind); + if (supportsOperation(specification.name, specification.numQubits, + specification.numParameters)) { + supportedGates.emplace_back(specification.kind); } } basis = resolveSynthesisBasis(); } -bool CompilerTarget::Storage::validLocus(const ArrayRef locus) const { - llvm::SmallDenseSet uniqueSites; - uniqueSites.reserve(locus.size()); - return llvm::all_of(locus, [&](const auto site) { - return siteToVertex.contains(site) && uniqueSites.insert(site).second; - }); -} - bool CompilerTarget::Storage::supportsOperation( - const StringRef operationName, const ArrayRef locus, + const StringRef operationName, const size_t numQubits, const std::optional numParameters) const { - if (!validLocus(locus)) { - return false; - } const auto canonical = canonicalOperationName(operationName); - if (canonical.empty() || locus.empty()) { + if (canonical.empty() || numQubits == 0 || numQubits > sites.size()) { return false; } if (!operations) { @@ -544,88 +458,31 @@ bool CompilerTarget::Storage::supportsOperation( if (found == capabilities.end()) { return false; } - const auto supportsExactLocus = [&](const ArrayRef exactLocus) { - return llvm::any_of(found->second, [&](const auto index) { - const auto& operation = (*operations)[index]; - return (!numParameters || operation.numParameters() == *numParameters) && - operation.supports(exactLocus); - }); - }; - if (supportsExactLocus(locus)) { - return true; - } - - const auto* const specification = - std::ranges::find_if(GATE_SPECIFICATIONS, [&](const auto& candidate) { - return candidate.name == canonical; - }); - if (locus.size() != 2 || specification == GATE_SPECIFICATIONS.end() || - !specification->symmetric || - (numParameters && specification->numParameters != *numParameters)) { - return false; - } - const std::array reverseLocus{locus[1], locus[0]}; - return supportsExactLocus(reverseLocus); -} - -bool CompilerTarget::Storage::gateIsGloballySupported( - const GateKind gate) const { - const auto& specification = gateSpecification(gate); - if (specification.numQubits == 1) { - return llvm::all_of(siteIds, [&](const auto site) { - const std::array locus{site}; - return supportsOperation(specification.name, locus, - specification.numParameters); - }); - } - if (sites.size() < 2) { - return false; - } - - const auto supportedOnEdge = [&](const SiteId first, const SiteId second) { - const std::array forward{first, second}; - const std::array reverse{second, first}; - const auto supportsForward = supportsOperation(specification.name, forward, - specification.numParameters); - const auto supportsReverse = supportsOperation(specification.name, reverse, - specification.numParameters); - return supportsForward && supportsReverse; - }; - - if (couplings) { - return llvm::all_of(*couplings, [&](const auto& coupling) { - return supportedOnEdge(coupling.first, coupling.second); - }); - } - for (size_t first = 0; first < siteIds.size(); ++first) { - for (size_t second = first + 1; second < siteIds.size(); ++second) { - if (!supportedOnEdge(siteIds[first], siteIds[second])) { - return false; - } - } - } - return true; -} - -bool CompilerTarget::Storage::hasGlobalGate(const GateKind gate) const { - return llvm::is_contained(globalGates, gate); + return llvm::any_of(found->second, [&](const auto index) { + const auto& operation = (*operations)[index]; + return operation.numQubits() == numQubits && + (!numParameters || operation.numParameters() == *numParameters); + }); } std::optional CompilerTarget::Storage::resolveSynthesisBasis() const { + const auto supports = [&](const GateKind gate) { + return llvm::is_contained(supportedGates, gate); + }; std::optional singleQubit; - if (hasGlobalGate(GateKind::U)) { + if (supports(GateKind::U)) { singleQubit = SingleQubitBasis::U; - } else if (hasGlobalGate(GateKind::X) && hasGlobalGate(GateKind::SX) && - hasGlobalGate(GateKind::RZ)) { + } else if (supports(GateKind::X) && supports(GateKind::SX) && + supports(GateKind::RZ)) { singleQubit = SingleQubitBasis::ZSXX; - } else if (hasGlobalGate(GateKind::R)) { + } else if (supports(GateKind::R)) { singleQubit = SingleQubitBasis::R; - } else if (hasGlobalGate(GateKind::RX) && hasGlobalGate(GateKind::RZ)) { + } else if (supports(GateKind::RX) && supports(GateKind::RZ)) { singleQubit = SingleQubitBasis::XZX; - } else if (hasGlobalGate(GateKind::RX) && hasGlobalGate(GateKind::RY)) { + } else if (supports(GateKind::RX) && supports(GateKind::RY)) { singleQubit = SingleQubitBasis::XYX; - } else if (hasGlobalGate(GateKind::RY) && hasGlobalGate(GateKind::RZ)) { + } else if (supports(GateKind::RY) && supports(GateKind::RZ)) { singleQubit = SingleQubitBasis::ZYZ; } @@ -636,7 +493,7 @@ CompilerTarget::Storage::resolveSynthesisBasis() const { // NOLINTNEXTLINE(readability-qualified-auto) const auto entangler = std::ranges::find_if(entanglerPreference, [&](const auto candidate) { - return hasGlobalGate(candidate); + return supports(candidate); }); if (!singleQubit || entangler == entanglerPreference.end()) { return std::nullopt; @@ -808,21 +665,17 @@ CompilerTarget::operations() const noexcept { } bool CompilerTarget::supportsOperation( - const StringRef operationName, const ArrayRef locus, + const StringRef operationName, const size_t numQubits, const std::optional numParameters) const { - return storage_->supportsOperation(operationName, locus, numParameters); + return storage_->supportsOperation(operationName, numQubits, numParameters); } -bool CompilerTarget::supports(::mlir::Operation* operation, - const ArrayRef locus) const { - if (operation == nullptr || !storage_->validLocus(locus)) { +bool CompilerTarget::supports(::mlir::Operation* operation) const { + if (operation == nullptr) { return false; } if (auto unitary = dyn_cast(operation)) { - if (unitary.getNumQubits() != locus.size()) { - return false; - } if (isa(operation)) { return true; } @@ -832,35 +685,31 @@ bool CompilerTarget::supports(::mlir::Operation* operation, controlled.getNumBodyUnitaries() == 1) { auto* const body = controlled.getBodyUnitary(0).getOperation(); if (isa(body)) { - return storage_->supportsOperation("cx", locus, 0); + return storage_->supportsOperation("cx", 2, 0); } if (isa(body)) { - return storage_->supportsOperation("cz", locus, 0); + return storage_->supportsOperation("cz", 2, 0); } } - return storage_->supportsOperation(unitary.getBaseSymbol(), locus, + return storage_->supportsOperation(unitary.getBaseSymbol(), + unitary.getNumQubits(), unitary.getNumParams()); } if (isa(operation)) { - return locus.size() == 1 && - storage_->supportsOperation("measure", locus, 0); + return storage_->supportsOperation("measure", 1, 0); } if (isa(operation)) { - return locus.size() == 1 && storage_->supportsOperation("reset", locus, 0); + return storage_->supportsOperation("reset", 1, 0); } return false; } -bool CompilerTarget::supports(const GateKind gate, - const ArrayRef locus) const { - const auto& specification = gateSpecification(gate); - return locus.size() == specification.numQubits && - storage_->supportsOperation(specification.name, locus, - specification.numParameters); +bool CompilerTarget::supports(const GateKind gate) const { + return llvm::is_contained(storage_->supportedGates, gate); } -ArrayRef CompilerTarget::globallySupportedGates() const noexcept { - return storage_->globalGates; +ArrayRef CompilerTarget::supportedGates() const noexcept { + return storage_->supportedGates; } std::optional diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp index a22657db1b..c153a9cdb6 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp @@ -16,22 +16,20 @@ #include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" +#include "mlir/Dialect/QTensor/IR/QTensorOps.h" #include "mlir/Dialect/Utils/Transforms/GlobalPhaseNormalization.h" -#include -#include #include #include #include // IWYU pragma: keep (Passes.h.inc) -#include -#include #include -#include +#include #include #include #include #include #include +#include #include #include #include @@ -42,7 +40,6 @@ #include #include #include -#include namespace mlir::qco { @@ -270,7 +267,7 @@ static void eraseFusableRun(RewriterBase& rewriter, /// Fuses a maximal constant run only when generic resynthesis strictly reduces /// its two-qubit operation count. -static bool optimizeTwoQubitRun(IRRewriter& rewriter, UnitaryOpInterface head, +static bool fuseTwoQubitGateRun(IRRewriter& rewriter, UnitaryOpInterface head, const Matrix4x4& headMatrix, const CompilerTarget::SynthesisBasis basis) { FusableTwoQubitRun run = scanFusableTwoQubitRun(head, headMatrix); @@ -296,168 +293,16 @@ static bool optimizeTwoQubitRun(IRRewriter& rewriter, UnitaryOpInterface head, return true; } -using SiteId = CompilerTarget::SiteId; - -static FailureOr -resolveProviderSite(Value value, DenseMap& resolvedSites) { - llvm::SmallDenseSet visited; - SmallVector path; - const auto remember = [&](const SiteId site) { - for (const Value traversed : path) { - resolvedSites.try_emplace(traversed, site); - } - return site; - }; - while (value) { - if (const auto cached = resolvedSites.find(value); - cached != resolvedSites.end()) { - return remember(cached->second); - } - if (!visited.insert(value).second) { - return failure(); - } - path.emplace_back(value); - - if (auto argument = dyn_cast(value)) { - Operation* const parent = argument.getOwner()->getParentOp(); - const auto index = argument.getArgNumber(); - if (auto ifOp = dyn_cast_or_null(parent); - ifOp && index < ifOp.getQubits().size()) { - value = ifOp.getQubits()[index]; - continue; - } - if (auto switchOp = dyn_cast_or_null(parent); - switchOp && index < switchOp.getTargets().size()) { - value = switchOp.getTargets()[index]; - continue; - } - if (auto forOp = dyn_cast_or_null(parent); - forOp && index > 0 && index <= forOp.getInits().size()) { - value = forOp.getInits()[index - 1]; - continue; - } - if (auto whileOp = dyn_cast_or_null(parent); - whileOp && index < whileOp.getInits().size()) { - value = whileOp.getInits()[index]; - continue; - } - return failure(); - } - - Operation* const definingOp = value.getDefiningOp(); - if (auto staticOp = dyn_cast_or_null(definingOp)) { - return remember(static_cast(staticOp.getIndex())); - } - if (auto unitary = dyn_cast_or_null(definingOp)) { - value = unitary.getInputForOutput(value); - continue; - } - if (auto measureOp = dyn_cast_or_null(definingOp)) { - value = measureOp.getQubitIn(); - continue; - } - if (auto resetOp = dyn_cast_or_null(definingOp)) { - value = resetOp.getQubitIn(); - continue; - } - if (auto forOp = dyn_cast_or_null(definingOp)) { - auto result = dyn_cast(value); - if (!result) { - return failure(); - } - value = forOp.getTiedLoopInit(result)->get(); - continue; - } - if (auto whileOp = dyn_cast_or_null(definingOp)) { - auto result = dyn_cast(value); - if (!result || result.getResultNumber() >= whileOp.getInits().size()) { - return failure(); - } - value = whileOp.getInits()[result.getResultNumber()]; - continue; - } - if (auto ifOp = dyn_cast_or_null(definingOp)) { - auto result = dyn_cast(value); - if (!result) { - return failure(); - } - OpOperand* const input = ifOp.getTiedQubit(result); - if (input == nullptr) { - return failure(); - } - value = input->get(); - continue; - } - if (auto switchOp = dyn_cast_or_null(definingOp)) { - auto result = dyn_cast(value); - if (!result) { - return failure(); - } - OpOperand* const input = switchOp.getTiedTarget(result); - if (input == nullptr) { - return failure(); - } - value = input->get(); - continue; - } - return failure(); - } - return failure(); -} - -static FailureOr> -providerLocus(Operation* operation, DenseMap& resolvedSites) { - SmallVector qubits; - if (auto unitary = dyn_cast(operation)) { - llvm::append_range(qubits, unitary.getInputQubits()); - } else if (auto measureOp = dyn_cast(operation)) { - qubits.emplace_back(measureOp.getQubitIn()); - } else if (auto resetOp = dyn_cast(operation)) { - qubits.emplace_back(resetOp.getQubitIn()); - } else { - return failure(); - } - - SmallVector locus; - locus.reserve(qubits.size()); - for (const Value qubit : qubits) { - const auto site = resolveProviderSite(qubit, resolvedSites); - if (failed(site)) { - return failure(); - } - locus.emplace_back(*site); - } - return locus; -} - static bool requiresTargetSynthesis(Operation* operation, - const CompilerTarget& target, - const ArrayRef locus) { - return !target.supports(operation, locus); -} - -static void appendProviderLocus(InFlightDiagnostic& diagnostic, - const ArrayRef locus) { - diagnostic << '['; - for (const auto [index, site] : llvm::enumerate(locus)) { - if (index != 0) { - diagnostic << ", "; - } - diagnostic << site; - } - diagnostic << ']'; + const CompilerTarget& target) { + return !target.supports(operation); } namespace { -struct SynthesisNeed { - Operation* operation; - SmallVector locus; -}; - struct SynthesisPlan { - std::optional firstNeed; - std::optional matrixUnavailable; + Operation* firstNeed = nullptr; + Operation* matrixUnavailable = nullptr; SmallVector operations; }; @@ -466,20 +311,17 @@ struct SynthesisPlan { static SynthesisPlan planTargetSynthesis(Operation* root, const CompilerTarget& target) { SynthesisPlan plan; - DenseMap resolvedSites; root->walk([&](Operation* operation) { auto unitary = dyn_cast(operation); if (!unitary || !isWalkableUnitaryShell(operation) || (unitary.getNumQubits() != 1 && unitary.getNumQubits() != 2)) { return WalkResult::advance(); } - auto locus = providerLocus(operation, resolvedSites); - if (failed(locus) || !requiresTargetSynthesis(operation, target, *locus)) { + if (!requiresTargetSynthesis(operation, target)) { return WalkResult::advance(); } - if (!plan.firstNeed) { - plan.firstNeed.emplace( - SynthesisNeed{.operation = operation, .locus = *locus}); + if (plan.firstNeed == nullptr) { + plan.firstNeed = operation; } if (unitary.isSingleQubit()) { @@ -495,8 +337,7 @@ static SynthesisPlan planTargetSynthesis(Operation* root, return WalkResult::advance(); } } - plan.matrixUnavailable.emplace( - SynthesisNeed{.operation = operation, .locus = std::move(*locus)}); + plan.matrixUnavailable = operation; return WalkResult::interrupt(); }); return plan; @@ -543,7 +384,7 @@ static void lowerTargetOperation(IRRewriter& rewriter, UnitaryOpInterface op, ValueRange{synthesized.qubit0, synthesized.qubit1}); } -static LogicalResult optimizeTwoQubitRuns(ModuleOp moduleOp) { +static LogicalResult fuseTwoQubitGates(ModuleOp moduleOp) { constexpr CompilerTarget::SynthesisBasis basis{ .singleQubit = CompilerTarget::SingleQubitBasis::U, .entangler = CompilerTarget::GateKind::CZ}; @@ -563,7 +404,7 @@ static LogicalResult optimizeTwoQubitRuns(ModuleOp moduleOp) { auto unitary = cast(operation); const auto matrix = twoQubitRunMemberMatrix(unitary); if (matrix) { - changed |= optimizeTwoQubitRun(rewriter, unitary, *matrix, basis); + changed |= fuseTwoQubitGateRun(rewriter, unitary, *matrix, basis); } } if (!changed) { @@ -574,9 +415,9 @@ static LogicalResult optimizeTwoQubitRuns(ModuleOp moduleOp) { namespace { -struct OptimizeTwoQubitUnitaryRunsPass final - : PassWrapper> { - MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(OptimizeTwoQubitUnitaryRunsPass) +struct FuseTwoQubitGatesPass final + : PassWrapper> { + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(FuseTwoQubitGatesPass) void getDependentDialects(DialectRegistry& registry) const override { registry.insert(); @@ -585,7 +426,7 @@ struct OptimizeTwoQubitUnitaryRunsPass final protected: void runOnOperation() override { ModuleOp moduleOp = getOperation(); - if (failed(optimizeTwoQubitRuns(moduleOp))) { + if (failed(fuseTwoQubitGates(moduleOp))) { signalPassFailure(); } } @@ -609,28 +450,24 @@ struct TargetNativeSynthesisPass final } ModuleOp moduleOp = getOperation(); const auto plan = planTargetSynthesis(moduleOp, target); - if (!plan.firstNeed) { + if (plan.firstNeed == nullptr) { return; } const auto targetBasis = target.synthesisBasis(); if (!targetBasis) { - auto diagnostic = plan.firstNeed->operation->emitError() - << "target-native synthesis cannot lower operation '" - << plan.firstNeed->operation->getName() - << "' at ordered provider locus "; - appendProviderLocus(diagnostic, plan.firstNeed->locus); - diagnostic << ": the target has no globally usable synthesis basis"; + plan.firstNeed->emitError() + << "target-native synthesis cannot lower operation '" + << plan.firstNeed->getName() + << "': the target has no usable synthesis basis"; signalPassFailure(); return; } - if (plan.matrixUnavailable) { - auto diagnostic = plan.matrixUnavailable->operation->emitError() - << "target-native synthesis cannot lower operation '" - << plan.matrixUnavailable->operation->getName() - << "' at ordered provider locus "; - appendProviderLocus(diagnostic, plan.matrixUnavailable->locus); - diagnostic << ": its unitary matrix is not available at compile time"; + if (plan.matrixUnavailable != nullptr) { + plan.matrixUnavailable->emitError() + << "target-native synthesis cannot lower operation '" + << plan.matrixUnavailable->getName() + << "': its unitary matrix is not available at compile time"; signalPassFailure(); return; } @@ -657,35 +494,57 @@ struct VerifyTargetConformancePass final protected: void runOnOperation() override { - DenseMap resolvedSites; WalkResult result = getOperation()->walk([&](Operation* operation) { + if (auto function = dyn_cast(operation); + function && + llvm::any_of(function.getArgumentTypes(), [](const auto type) { + if (isa(type)) { + return true; + } + const auto tensor = dyn_cast(type); + return tensor && isa(tensor.getElementType()); + })) { + function.emitError() + << "target conformance requires quantum function inputs to be " + "assigned to qco.static target sites"; + return WalkResult::interrupt(); + } + if (auto staticOp = dyn_cast(operation)) { + const auto site = + static_cast(staticOp.getIndex()); + if (target.vertexForSite(site)) { + return WalkResult::advance(); + } + staticOp.emitError() << "target does not contain static site " << site; + return WalkResult::interrupt(); + } + if (isa(operation)) { + operation->emitError() + << "target conformance requires qubits to be assigned to " + "qco.static target sites"; + return WalkResult::interrupt(); + } + + size_t arity = 1; size_t parameterCount = 0; if (auto unitary = dyn_cast(operation)) { if (isExcludedFromTopLevelUnitaryWalk(operation)) { return WalkResult::advance(); } + arity = unitary.getNumQubits(); parameterCount = unitary.getNumParams(); } else if (!isa(operation)) { return WalkResult::advance(); } - const auto locus = providerLocus(operation, resolvedSites); - if (failed(locus)) { - operation->emitError() - << "target conformance requires every hardware qubit operand to " - "trace to a qco.static provider site"; - return WalkResult::interrupt(); - } - if (target.supports(operation, *locus)) { + if (target.supports(operation)) { return WalkResult::advance(); } auto diagnostic = operation->emitError() << "target does not support operation '" - << operation->getName() << "' with arity " - << locus->size() << " and " << parameterCount - << " parameter(s) at ordered provider locus "; - appendProviderLocus(diagnostic, *locus); + << operation->getName() << "' with arity " << arity + << " and " << parameterCount << " parameter(s)"; return WalkResult::interrupt(); }); if (result.wasInterrupted()) { @@ -698,8 +557,8 @@ struct VerifyTargetConformancePass final } // namespace -std::unique_ptr createOptimizeTwoQubitUnitaryRuns() { - return std::make_unique(); +std::unique_ptr createFuseTwoQubitGates() { + return std::make_unique(); } std::unique_ptr diff --git a/mlir/unittests/Compiler/test_compiler_target.cpp b/mlir/unittests/Compiler/test_compiler_target.cpp index c5c9661b1f..90c731eaf3 100644 --- a/mlir/unittests/Compiler/test_compiler_target.cpp +++ b/mlir/unittests/Compiler/test_compiler_target.cpp @@ -43,9 +43,9 @@ using Coupling = Target::Coupling; using DurationUnit = Target::DurationUnit; using GateKind = Target::GateKind; using Operation = Target::Operation; -using OperationLocus = Target::OperationLocus; using Site = Target::Site; using SiteId = Target::SiteId; +using SiteTuple = Target::SiteTuple; TEST(CompilerTargetTest, ConstructsDetailedNamedTargetAndSharesStorage) { std::vector sites; @@ -56,8 +56,7 @@ TEST(CompilerTargetTest, ConstructsDetailedNamedTargetAndSharesStorage) { std::vector operations; operations.emplace_back( " PRX ", 1, 2, - std::vector{OperationLocus{{7}, 0, 0.99}, OperationLocus{{2}, 5, 0.98}}, - 0, 0.97); + std::vector{SiteTuple{{7}, 0, 0.99}, SiteTuple{{2}, 5, 0.98}}, 0, 0.97); const Target target{"device", std::move(sites), std::vector{{11, 2}, {2, 11}, {7, 2}}, @@ -78,15 +77,15 @@ TEST(CompilerTargetTest, ConstructsDetailedNamedTargetAndSharesStorage) { EXPECT_EQ(*target.sites()[0].name(), "left"); EXPECT_EQ(target.sites()[0].t1(), 100); EXPECT_EQ(target.sites()[0].t2(), 80); - EXPECT_EQ(target.operations()[0].providerName(), " PRX "); + EXPECT_EQ(target.operations()[0].name(), " PRX "); EXPECT_EQ(target.operations()[0].canonicalName(), "r"); EXPECT_EQ(target.operations()[0].numQubits(), 1); EXPECT_EQ(target.operations()[0].numParameters(), 2); EXPECT_EQ(target.operations()[0].duration(), 0); EXPECT_EQ(target.operations()[0].fidelity(), 0.97); - ASSERT_EQ(target.operations()[0].loci().size(), 2); - EXPECT_EQ(target.operations()[0].loci()[0].duration(), 0); - EXPECT_EQ(target.operations()[0].loci()[0].fidelity(), 0.99); + ASSERT_EQ(target.operations()[0].siteTuples().size(), 2); + EXPECT_EQ(target.operations()[0].siteTuples()[0].duration(), 0); + EXPECT_EQ(target.operations()[0].siteTuples()[0].fidelity(), 0.99); EXPECT_EQ(copy.sites().data(), target.sites().data()); EXPECT_EQ(copy.couplings().data(), target.couplings().data()); @@ -166,22 +165,20 @@ TEST(CompilerTargetTest, RejectsInvalidMetadata) { static_cast( DurationUnit{"ns", std::numeric_limits::infinity()}); }); - expectInvalid([] { static_cast(OperationLocus{{0, 0}}); }); - expectInvalid( - [] { static_cast(OperationLocus{{0}, std::nullopt, -0.1}); }); + expectInvalid([] { static_cast(SiteTuple{{0, 0}}); }); + expectInvalid([] { static_cast(SiteTuple{{0}, std::nullopt, -0.1}); }); expectInvalid([] { static_cast(Operation{"", 1, 0}); }); expectInvalid([] { static_cast(Operation{"x", 0, 0}); }); expectInvalid([] { - static_cast( - Operation{"x", 1, 0, std::vector{OperationLocus{{0, 1}}}}); + static_cast(Operation{"x", 1, 0, std::vector{SiteTuple{{0, 1}}}}); }); expectInvalid([] { - static_cast(Operation{ - "x", 1, 0, std::vector{OperationLocus{{0}}, OperationLocus{{0}}}}); + static_cast( + Operation{"x", 1, 0, std::vector{SiteTuple{{0}}, SiteTuple{{0}}}}); }); expectInvalid([] { - static_cast(Operation{"x", 1, 0, std::nullopt, std::nullopt, - std::numeric_limits::quiet_NaN()}); + static_cast(Operation{ + "x", 1, 0, {}, std::nullopt, std::numeric_limits::quiet_NaN()}); }); expectInvalid([] { static_cast(Target{std::vector{}}); }); @@ -191,12 +188,12 @@ TEST(CompilerTargetTest, RejectsInvalidMetadata) { static_cast(Target{std::vector{Site{0, std::nullopt, 1}}}); }); expectInvalid([] { - static_cast(Target{ - 1, std::nullopt, std::vector{Operation{"x", 1, 0, std::nullopt, 1}}}); + static_cast( + Target{1, std::nullopt, std::vector{Operation{"x", 1, 0, {}, 1}}}); }); expectInvalid([] { std::vector operations; - operations.emplace_back("x", 1, 0, std::vector{OperationLocus{{0}, 1}}); + operations.emplace_back("x", 1, 0, std::vector{SiteTuple{{0}, 1}}); static_cast(Target{1, std::nullopt, std::move(operations)}); }); expectInvalid( @@ -207,124 +204,80 @@ TEST(CompilerTargetTest, RejectsInvalidMetadata) { [] { static_cast(Target{3, std::vector{{0, 1}}}); }); expectInvalid([] { std::vector operations; - operations.emplace_back("x", 1, 0, std::vector{OperationLocus{{2}}}); + operations.emplace_back("x", 1, 0, std::vector{SiteTuple{{2}}}); static_cast(Target{2, std::nullopt, std::move(operations)}); }); + expectInvalid([] { + static_cast( + Target{1, std::nullopt, std::vector{Operation{"cx", 2, 0}}}); + }); } TEST(CompilerTargetTest, DistinguishesAbsentAndEmptyOperationSets) { const Target permissive{2}; const Target closed{2, std::nullopt, std::vector{}}; - const Operation globalX{"x", 1, 0}; - const Operation globalCX{"cx", 2, 0}; EXPECT_FALSE(permissive.hasExplicitOperations()); EXPECT_TRUE(permissive.operations().empty()); - EXPECT_TRUE(permissive.supportsOperation("provider.operation", {0})); - EXPECT_TRUE(permissive.supports(GateKind::CX, {0, 1})); - EXPECT_FALSE(permissive.supportsOperation("x", {0, 0})); - EXPECT_FALSE(permissive.supportsOperation("x", {2})); - EXPECT_FALSE(permissive.supportsOperation("", {0})); - EXPECT_FALSE(permissive.supportsOperation(" ", {0})); - EXPECT_FALSE(permissive.supportsOperation("x", {})); - EXPECT_FALSE(globalX.supports({-1})); - EXPECT_FALSE(globalCX.supports({0, 0})); + EXPECT_TRUE(permissive.supportsOperation("device.operation", 1)); + EXPECT_TRUE(permissive.supports(GateKind::CX)); + EXPECT_FALSE(permissive.supportsOperation("", 1)); + EXPECT_FALSE(permissive.supportsOperation(" ", 1)); + EXPECT_FALSE(permissive.supportsOperation("x", 0)); + EXPECT_FALSE(permissive.supportsOperation("x", 3)); EXPECT_TRUE(closed.hasExplicitOperations()); EXPECT_TRUE(closed.operations().empty()); - EXPECT_FALSE(closed.supportsOperation("x", {0})); - EXPECT_FALSE(closed.supports(GateKind::CX, {0, 1})); - EXPECT_TRUE(closed.globallySupportedGates().empty()); + EXPECT_FALSE(closed.supportsOperation("x", 1)); + EXPECT_FALSE(closed.supports(GateKind::CX)); + EXPECT_TRUE(closed.supportedGates().empty()); EXPECT_FALSE(closed.synthesisBasis()); } -TEST(CompilerTargetTest, PreservesRawLociAndResolvesBidirectionalBasis) { +TEST(CompilerTargetTest, PreservesCalibrationAndResolvesHomogeneousBasis) { const std::vector chain{{0, 1}, {1, 2}}; const Operation globalU{"U3", 1, 3}; - const Operation symmetricCZ{ - "cz", 2, 0, std::vector{OperationLocus{{1, 0}}, OperationLocus{{1, 2}}}}; - const Target symmetric{3, chain, std::vector{globalU, symmetricCZ}}; - - EXPECT_TRUE(symmetric.supportsOperation("u", {0}, 3)); - EXPECT_TRUE(symmetric.supportsOperation(" U3 ", {2}, 3)); - ASSERT_EQ(symmetric.operations().size(), 2U); - EXPECT_TRUE(symmetric.operations()[1].supports({1, 0})); - EXPECT_FALSE(symmetric.operations()[1].supports({0, 1})); - EXPECT_TRUE(symmetric.supports(GateKind::CZ, {1, 0})); - EXPECT_TRUE(symmetric.supports(GateKind::CZ, {0, 1})); - EXPECT_TRUE( - llvm::is_contained(symmetric.globallySupportedGates(), GateKind::CZ)); - ASSERT_TRUE(symmetric.synthesisBasis()); - EXPECT_EQ(symmetric.synthesisBasis()->singleQubit, - Target::SingleQubitBasis::U); - EXPECT_EQ(symmetric.synthesisBasis()->entangler, GateKind::CZ); - - const Operation oneWayCX{ - "CNOT", 2, 0, - std::vector{OperationLocus{{0, 1}}, OperationLocus{{1, 2}}}}; - const Target oneWay{3, chain, std::vector{globalU, oneWayCX}}; - EXPECT_TRUE(oneWay.supports(GateKind::CX, {0, 1})); - EXPECT_FALSE(oneWay.supports(GateKind::CX, {1, 0})); - EXPECT_FALSE( - llvm::is_contained(oneWay.globallySupportedGates(), GateKind::CX)); - EXPECT_FALSE(oneWay.synthesisBasis()); - - const Operation twoWayCX{ - "cnot", 2, 0, - std::vector{OperationLocus{{0, 1}}, OperationLocus{{1, 0}}, - OperationLocus{{1, 2}}, OperationLocus{{2, 1}}}}; - const Target twoWay{3, chain, std::vector{globalU, twoWayCX}}; - EXPECT_TRUE( - llvm::is_contained(twoWay.globallySupportedGates(), GateKind::CX)); - ASSERT_TRUE(twoWay.synthesisBasis()); - EXPECT_EQ(twoWay.synthesisBasis()->entangler, GateKind::CX); + const Operation cz{"cz", 2, 0, std::vector{SiteTuple{{1, 0}, 5, 0.99}}}; + const Target target{3, chain, std::vector{globalU, cz}, + DurationUnit{"ns", 1.}}; + + EXPECT_TRUE(target.supportsOperation("u", 1, 3)); + EXPECT_TRUE(target.supportsOperation(" U3 ", 1, 3)); + EXPECT_TRUE(target.supports(GateKind::CZ)); + EXPECT_TRUE(llvm::is_contained(target.supportedGates(), GateKind::CZ)); + ASSERT_EQ(target.operations().size(), 2U); + ASSERT_EQ(target.operations()[1].siteTuples().size(), 1U); + EXPECT_EQ(target.operations()[1].siteTuples()[0].sites(), + (llvm::ArrayRef{1, 0})); + EXPECT_EQ(target.operations()[1].siteTuples()[0].duration(), 5); + EXPECT_EQ(target.operations()[1].siteTuples()[0].fidelity(), 0.99); + ASSERT_TRUE(target.synthesisBasis()); + EXPECT_EQ(target.synthesisBasis()->singleQubit, Target::SingleQubitBasis::U); + EXPECT_EQ(target.synthesisBasis()->entangler, GateKind::CZ); } -TEST(CompilerTargetTest, ClassifiesEveryEntanglerOrientation) { +TEST(CompilerTargetTest, ClassifiesEveryEntangler) { using Entangler = std::tuple; - const std::array symmetricEntanglers{ - Entangler{GateKind::CZ, "cz", 0}, Entangler{GateKind::RXX, "rxx", 1}, - Entangler{GateKind::RYY, "ryy", 1}, Entangler{GateKind::RZZ, "rzz", 1}, - Entangler{GateKind::ISWAP, "iswap", 0}}; - const std::array directionalEntanglers{Entangler{GateKind::CX, "cx", 0}, - Entangler{GateKind::ECR, "ecr", 0}, - Entangler{GateKind::RZX, "rzx", 1}}; + const std::array entanglers{Entangler{GateKind::CZ, "cz", 0}, + Entangler{GateKind::RXX, "rxx", 1}, + Entangler{GateKind::RYY, "ryy", 1}, + Entangler{GateKind::RZZ, "rzz", 1}, + Entangler{GateKind::ISWAP, "iswap", 0}, + Entangler{GateKind::CX, "cx", 0}, + Entangler{GateKind::ECR, "ecr", 0}, + Entangler{GateKind::RZX, "rzx", 1}}; const std::vector chain{{0, 1}, {1, 2}}; const Operation globalU{"u", 1, 3}; - for (const auto& [gate, name, numParameters] : symmetricEntanglers) { + for (const auto& [gate, name, numParameters] : entanglers) { SCOPED_TRACE(name); - const Operation oneOrientation{ - std::string{name}, 2, numParameters, - std::vector{OperationLocus{{1, 0}}, OperationLocus{{1, 2}}}}; - const Target target{3, chain, std::vector{globalU, oneOrientation}}; - EXPECT_TRUE(llvm::is_contained(target.globallySupportedGates(), gate)); - EXPECT_TRUE(target.supports(gate, {1, 0})); - EXPECT_TRUE(target.supports(gate, {0, 1})); - EXPECT_TRUE(target.supports(gate, {1, 2})); - EXPECT_TRUE(target.supports(gate, {2, 1})); + const Operation operation{std::string{name}, 2, numParameters}; + const Target target{3, chain, std::vector{globalU, operation}}; + EXPECT_TRUE(llvm::is_contained(target.supportedGates(), gate)); + EXPECT_TRUE(target.supports(gate)); ASSERT_TRUE(target.synthesisBasis()); EXPECT_EQ(target.synthesisBasis()->entangler, gate); } - - for (const auto& [gate, name, numParameters] : directionalEntanglers) { - SCOPED_TRACE(name); - const Operation oneOrientation{ - std::string{name}, 2, numParameters, - std::vector{OperationLocus{{0, 1}}, OperationLocus{{1, 2}}}}; - const Target oneWay{3, chain, std::vector{globalU, oneOrientation}}; - EXPECT_FALSE(llvm::is_contained(oneWay.globallySupportedGates(), gate)); - EXPECT_FALSE(oneWay.synthesisBasis()); - - const Operation bothOrientations{ - std::string{name}, 2, numParameters, - std::vector{OperationLocus{{0, 1}}, OperationLocus{{1, 0}}, - OperationLocus{{1, 2}}, OperationLocus{{2, 1}}}}; - const Target twoWay{3, chain, std::vector{globalU, bothOrientations}}; - EXPECT_TRUE(llvm::is_contained(twoWay.globallySupportedGates(), gate)); - ASSERT_TRUE(twoWay.synthesisBasis()); - EXPECT_EQ(twoWay.synthesisBasis()->entangler, gate); - } } TEST(CompilerTargetTest, SupportsRealQCOOperationsAndStructuralOps) { @@ -384,25 +337,24 @@ TEST(CompilerTargetTest, SupportsRealQCOOperationsAndStructuralOps) { const Target target{ std::vector{Site{10}, Site{20}}, std::nullopt, - std::vector{Operation{"x", 1, 0}, Operation{"measure", 1, 0}, - Operation{"reset", 1, 0}, - Operation{"cnot", 2, 0, - std::vector{OperationLocus{{10, 20}}, - OperationLocus{{20, 10}}}}}}; - EXPECT_TRUE(target.supports(x, {10})); - EXPECT_FALSE(target.supports(x, {10, 20})); - EXPECT_TRUE(target.supports(cx, {10, 20})); - EXPECT_TRUE(target.supports(measure, {20})); - EXPECT_TRUE(target.supports(reset, {10})); - EXPECT_TRUE(target.supports(barrier, {10, 20})); - EXPECT_TRUE(target.supports(gphase, {})); - EXPECT_FALSE(target.supports(nullptr, {10})); + std::vector{ + Operation{"x", 1, 0}, Operation{"measure", 1, 0}, + Operation{"reset", 1, 0}, + Operation{"cnot", 2, 0, + std::vector{SiteTuple{{10, 20}}, SiteTuple{{20, 10}}}}}}; + EXPECT_TRUE(target.supports(x)); + EXPECT_TRUE(target.supports(cx)); + EXPECT_TRUE(target.supports(measure)); + EXPECT_TRUE(target.supports(reset)); + EXPECT_TRUE(target.supports(barrier)); + EXPECT_TRUE(target.supports(gphase)); + EXPECT_FALSE(target.supports(nullptr)); const Target closed{2, std::nullopt, std::vector{}}; - EXPECT_TRUE(closed.supports(barrier, {0, 1})); - EXPECT_TRUE(closed.supports(gphase, {})); - EXPECT_FALSE(closed.supports(x, {0})); - EXPECT_FALSE(closed.supports(measure, {0})); + EXPECT_TRUE(closed.supports(barrier)); + EXPECT_TRUE(closed.supports(gphase)); + EXPECT_FALSE(closed.supports(x)); + EXPECT_FALSE(closed.supports(measure)); } } // namespace diff --git a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp index ded44fc35d..fa7d68f75d 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp @@ -607,7 +607,7 @@ TEST_P(MappingPassTest, MapTopologyOnlyWithEmptyOperationSet) { EXPECT_GT(numSwaps, 0); } -TEST_P(MappingPassTest, PreserveNoncontiguousProviderSiteIds) { +TEST_P(MappingPassTest, PreserveNoncontiguousTargetSiteIds) { std::vector sites; sites.emplace_back(7); sites.emplace_back(19); diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp index 85a09939fc..bfa2c07d65 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp @@ -20,11 +20,9 @@ #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" #include -#include #include #include #include -#include #include #include #include @@ -38,7 +36,6 @@ #include #include -#include #include #include #include @@ -52,7 +49,6 @@ namespace mqt::test::qco { using Target = mlir::CompilerTarget; using Operation = Target::Operation; -using OperationLocus = Target::OperationLocus; using Site = Target::Site; using mlir::ModuleOp; using mlir::OwningOpRef; @@ -148,8 +144,7 @@ class TargetSynthesisTest : public testing::Test { void SetUp() override { mlir::DialectRegistry registry; registry.insert(); + mlir::qco::QCODialect, mlir::qtensor::QTensorDialect>(); context = std::make_unique(); context->appendDialectRegistry(registry); context->loadAllAvailableDialects(); @@ -180,17 +175,17 @@ class TargetSynthesisTest : public testing::Test { TEST(TargetSynthesisPassContract, FactoriesAreIndependentlyConstructible) { const Target target{2}; - auto optimization = mlir::qco::createOptimizeTwoQubitUnitaryRuns(); + auto fusion = mlir::qco::createFuseTwoQubitGates(); auto synthesis = mlir::qco::createTargetNativeSynthesis(target); auto conformance = mlir::qco::createVerifyTargetConformance(target); - ASSERT_NE(optimization, nullptr); + ASSERT_NE(fusion, nullptr); ASSERT_NE(synthesis, nullptr); ASSERT_NE(conformance, nullptr); - mlir::DialectRegistry optimizationDialects; - optimization->getDependentDialects(optimizationDialects); - EXPECT_TRUE(optimizationDialects.getDialectAllocator( + mlir::DialectRegistry fusionDialects; + fusion->getDependentDialects(fusionDialects); + EXPECT_TRUE(fusionDialects.getDialectAllocator( mlir::arith::ArithDialect::getDialectNamespace())); mlir::DialectRegistry synthesisDialects; @@ -199,7 +194,7 @@ TEST(TargetSynthesisPassContract, FactoriesAreIndependentlyConstructible) { mlir::arith::ArithDialect::getDialectNamespace())); } -TEST_F(TargetSynthesisTest, PreRoutingOptimizationRequiresStrictImprovement) { +TEST_F(TargetSynthesisTest, TwoQubitGateFusionRequiresStrictImprovement) { const auto adjacentCx = [](QCOProgramBuilder& builder) { auto q0 = builder.staticQubit(0); auto q1 = builder.staticQubit(1); @@ -210,7 +205,7 @@ TEST_F(TargetSynthesisTest, PreRoutingOptimizationRequiresStrictImprovement) { auto expected = build(adjacentCx); auto optimized = build(adjacentCx); ASSERT_TRUE(mlir::succeeded( - runPass(*optimized, mlir::qco::createOptimizeTwoQubitUnitaryRuns()))); + runPass(*optimized, mlir::qco::createFuseTwoQubitGates()))); EXPECT_EQ(countOps(*optimized), 0U); expectEquivalent(expected, optimized); @@ -223,12 +218,12 @@ TEST_F(TargetSynthesisTest, PreRoutingOptimizationRequiresStrictImprovement) { return builder.intConstant(0); }); ASSERT_TRUE(mlir::succeeded( - runPass(*nonImproving, mlir::qco::createOptimizeTwoQubitUnitaryRuns()))); + runPass(*nonImproving, mlir::qco::createFuseTwoQubitGates()))); EXPECT_EQ(countOps(*nonImproving), 3U); } TEST_F(TargetSynthesisTest, - PreRoutingOptimizationFusesInterleavedSingleQubitGates) { + TwoQubitGateFusionFusesInterleavedSingleQubitGates) { const auto interleaved = [](QCOProgramBuilder& builder) { auto q0 = builder.staticQubit(0); auto q1 = builder.staticQubit(1); @@ -242,12 +237,12 @@ TEST_F(TargetSynthesisTest, auto optimized = build(interleaved); ASSERT_TRUE(mlir::succeeded( - runPass(*optimized, mlir::qco::createOptimizeTwoQubitUnitaryRuns()))); + runPass(*optimized, mlir::qco::createFuseTwoQubitGates()))); EXPECT_EQ(countOps(*optimized), 0U); expectEquivalent(expected, optimized); } -TEST_F(TargetSynthesisTest, PreRoutingOptimizationEmitsSymmetricEntangler) { +TEST_F(TargetSynthesisTest, TwoQubitGateFusionEmitsSymmetricEntangler) { const auto reducible = [](QCOProgramBuilder& builder) { auto q0 = builder.staticQubit(0); auto q1 = builder.staticQubit(1); @@ -260,14 +255,14 @@ TEST_F(TargetSynthesisTest, PreRoutingOptimizationEmitsSymmetricEntangler) { auto optimized = build(reducible); ASSERT_TRUE(mlir::succeeded( - runPass(*optimized, mlir::qco::createOptimizeTwoQubitUnitaryRuns()))); + runPass(*optimized, mlir::qco::createFuseTwoQubitGates()))); EXPECT_EQ(countOps(*optimized), 1U); EXPECT_EQ(countOps(*optimized), 1U); EXPECT_EQ(countOps(*optimized), 0U); expectEquivalent(expected, optimized); } -TEST_F(TargetSynthesisTest, PreRoutingOptimizationLeavesIndividualOpsAlone) { +TEST_F(TargetSynthesisTest, TwoQubitGateFusionLeavesIndividualOpsAlone) { auto module = build([](QCOProgramBuilder& builder) { auto q0 = builder.staticQubit(0); auto q1 = builder.staticQubit(1); @@ -275,15 +270,15 @@ TEST_F(TargetSynthesisTest, PreRoutingOptimizationLeavesIndividualOpsAlone) { return builder.intConstant(0); }); const auto before = printModule(*module); - ASSERT_TRUE(mlir::succeeded( - runPass(*module, mlir::qco::createOptimizeTwoQubitUnitaryRuns()))); + ASSERT_TRUE( + mlir::succeeded(runPass(*module, mlir::qco::createFuseTwoQubitGates()))); EXPECT_EQ(countOps(*module), 1U); EXPECT_EQ(countOps(*module), 0U); EXPECT_EQ(printModule(*module), before); } TEST_F(TargetSynthesisTest, - PreRoutingOptimizationLeavesRuntimeParameterizedRunsAlone) { + TwoQubitGateFusionLeavesRuntimeParameterizedRunsAlone) { auto module = mlir::parseSourceString(R"mlir( module { func.func @main(%theta: f64) -> (!qco.qubit, !qco.qubit) { @@ -298,8 +293,8 @@ TEST_F(TargetSynthesisTest, context.get()); ASSERT_TRUE(module); const auto before = printModule(*module); - ASSERT_TRUE(mlir::succeeded( - runPass(*module, mlir::qco::createOptimizeTwoQubitUnitaryRuns()))); + ASSERT_TRUE( + mlir::succeeded(runPass(*module, mlir::qco::createFuseTwoQubitGates()))); EXPECT_EQ(countOps(*module), 2U); EXPECT_EQ(printModule(*module), before); } @@ -364,8 +359,7 @@ TEST_F(TargetSynthesisTest, TargetNativeSynthesisPreservesNativeSwap) { EXPECT_EQ(printModule(*module), before); } -TEST_F(TargetSynthesisTest, - TargetNativeSynthesisUsesBidirectionalSymmetricCapability) { +TEST_F(TargetSynthesisTest, TargetNativeSynthesisUsesHomogeneousCapability) { const auto swap = [](QCOProgramBuilder& builder) { auto q0 = builder.staticQubit(0); auto q1 = builder.staticQubit(1); @@ -374,19 +368,17 @@ TEST_F(TargetSynthesisTest, }; auto expected = build(swap); auto synthesized = build(swap); - const Target oneOrientation{ - std::vector{Site{0}, Site{1}}, std::nullopt, - std::vector{Operation{"u", 1, 3}, - Operation{"cz", 2, 0, std::vector{OperationLocus{{1, 0}}}}}}; - ASSERT_TRUE(oneOrientation.synthesisBasis()); - ASSERT_EQ(oneOrientation.synthesisBasis()->entangler, Target::GateKind::CZ); - - ASSERT_TRUE(mlir::succeeded(runPass( - *synthesized, mlir::qco::createTargetNativeSynthesis(oneOrientation)))); + const Target target{2, std::nullopt, + std::vector{Operation{"u", 1, 3}, Operation{"cz", 2, 0}}}; + ASSERT_TRUE(target.synthesisBasis()); + ASSERT_EQ(target.synthesisBasis()->entangler, Target::GateKind::CZ); + + ASSERT_TRUE(mlir::succeeded( + runPass(*synthesized, mlir::qco::createTargetNativeSynthesis(target)))); EXPECT_EQ(countOps(*synthesized), 0U); EXPECT_GT(countOps(*synthesized), 0U); - ASSERT_TRUE(mlir::succeeded(runPass( - *synthesized, mlir::qco::createVerifyTargetConformance(oneOrientation)))); + ASSERT_TRUE(mlir::succeeded( + runPass(*synthesized, mlir::qco::createVerifyTargetConformance(target)))); ASSERT_TRUE(mlir::succeeded(mlir::verify(*synthesized))); expectEquivalent(expected, synthesized); } @@ -449,11 +441,10 @@ TEST_F(TargetSynthesisTest, MissingBasisIsDiagnosedOnlyWhenLoweringIsNeeded) { const auto diagnostics = expectFailure( *unsupported, mlir::qco::createTargetNativeSynthesis(hOnly)); EXPECT_NE(diagnostics.find("target-native synthesis cannot lower operation " - "'qco.x' at ordered provider locus [0]"), + "'qco.x'"), std::string::npos) << diagnostics; - EXPECT_NE(diagnostics.find("no globally usable synthesis basis"), - std::string::npos) + EXPECT_NE(diagnostics.find("no usable synthesis basis"), std::string::npos) << diagnostics; } @@ -499,7 +490,7 @@ TEST_F(TargetSynthesisTest, const auto diagnostics = expectFailure( *module, mlir::qco::createTargetNativeSynthesis(makeUCxTarget())); EXPECT_NE(diagnostics.find("target-native synthesis cannot lower operation " - "'qco.rxx' at ordered provider locus [0, 1]"), + "'qco.rxx'"), std::string::npos) << diagnostics; EXPECT_NE(diagnostics.find("unitary matrix is not available at compile time"), @@ -529,49 +520,90 @@ TEST_F(TargetSynthesisTest, EXPECT_EQ(printModule(*module), before); } -TEST_F(TargetSynthesisTest, ConformanceUsesProviderIdsAndOrderedLoci) { - const Target directional{ - std::vector{Site{10}, Site{20}}, std::nullopt, - std::vector{ - Operation{"cx", 2, 0, std::vector{OperationLocus{{10, 20}}}}}}; - ASSERT_FALSE(directional.synthesisBasis()); +TEST_F(TargetSynthesisTest, + ConformanceUsesHomogeneousCapabilitiesAndValidatesSites) { + const Target target{std::vector{Site{10}, Site{20}}, std::nullopt, + std::vector{Operation{"cx", 2, 0}}}; + ASSERT_FALSE(target.synthesisBasis()); - auto supported = build([](QCOProgramBuilder& builder) { + auto reversed = build([](QCOProgramBuilder& builder) { auto q10 = builder.staticQubit(10); auto q20 = builder.staticQubit(20); - std::tie(q10, q20) = builder.cx(q10, q20); + std::tie(q20, q10) = builder.cx(q20, q10); return builder.intConstant(0); }); - ASSERT_TRUE(mlir::succeeded(runPass( - *supported, mlir::qco::createTargetNativeSynthesis(directional)))); - ASSERT_TRUE(mlir::succeeded(runPass( - *supported, mlir::qco::createVerifyTargetConformance(directional)))); + ASSERT_TRUE(mlir::succeeded( + runPass(*reversed, mlir::qco::createTargetNativeSynthesis(target)))); + ASSERT_TRUE(mlir::succeeded( + runPass(*reversed, mlir::qco::createVerifyTargetConformance(target)))); - auto reversed = build([](QCOProgramBuilder& builder) { - auto q10 = builder.staticQubit(10); + auto unknownSite = build([](QCOProgramBuilder& builder) { + auto q30 = builder.staticQubit(30); auto q20 = builder.staticQubit(20); - std::tie(q20, q10) = builder.cx(q20, q10); + std::tie(q30, q20) = builder.cx(q30, q20); return builder.intConstant(0); }); const auto diagnostics = expectFailure( - *reversed, mlir::qco::createVerifyTargetConformance(directional)); - EXPECT_NE(diagnostics.find("'qco.ctrl' with arity 2 and 0 parameter(s)"), + *unknownSite, mlir::qco::createVerifyTargetConformance(target)); + EXPECT_NE(diagnostics.find("target does not contain static site 30"), + std::string::npos) + << diagnostics; +} + +TEST_F(TargetSynthesisTest, ConformanceRejectsDynamicAllocations) { + const Target target{1, std::nullopt, std::vector{Operation{"x", 1, 0}}}; + const auto expectDynamicAllocationFailure = + [&](OwningOpRef module) { + const auto diagnostics = expectFailure( + *module, mlir::qco::createVerifyTargetConformance(target)); + EXPECT_NE( + diagnostics.find("requires qubits to be assigned to qco.static"), + std::string::npos) + << diagnostics; + }; + + expectDynamicAllocationFailure(build([](QCOProgramBuilder& builder) { + auto qubit = builder.allocQubit(); + qubit = builder.x(qubit); + return builder.intConstant(0); + })); + expectDynamicAllocationFailure(build([](QCOProgramBuilder& builder) { + auto qubits = builder.allocQubitRegister(1); + qubits[0] = builder.x(qubits[0]); + return builder.intConstant(0); + })); +} + +TEST_F(TargetSynthesisTest, ConformanceRejectsQuantumFunctionInputs) { + auto module = mlir::parseSourceString(R"mlir( + module { + func.func @main(%qubit: !qco.qubit) -> !qco.qubit { + %result = qco.x %qubit : !qco.qubit -> !qco.qubit + return %result : !qco.qubit + } + } + )mlir", + context.get()); + ASSERT_TRUE(module); + const Target target{1, std::nullopt, std::vector{Operation{"x", 1, 0}}}; + + const auto diagnostics = + expectFailure(*module, mlir::qco::createVerifyTargetConformance(target)); + EXPECT_NE(diagnostics.find("requires quantum function inputs to be assigned " + "to qco.static target sites"), std::string::npos) << diagnostics; - EXPECT_NE(diagnostics.find("[20, 10]"), std::string::npos) << diagnostics; } -TEST_F(TargetSynthesisTest, ConformanceChecksTypeArityParametersAndSite) { +TEST_F(TargetSynthesisTest, ConformanceChecksTypeArityAndParameters) { const auto expectUnsupported = [&](const Target& target, OwningOpRef module, const std::string& operation, - const std::string& details, - const std::string& locus) { + const std::string& details) { const auto diagnostics = expectFailure( *module, mlir::qco::createVerifyTargetConformance(target)); EXPECT_NE(diagnostics.find(operation), std::string::npos) << diagnostics; EXPECT_NE(diagnostics.find(details), std::string::npos) << diagnostics; - EXPECT_NE(diagnostics.find(locus), std::string::npos) << diagnostics; }; expectUnsupported(Target{std::vector{Site{10}}, std::nullopt, @@ -581,7 +613,7 @@ TEST_F(TargetSynthesisTest, ConformanceChecksTypeArityParametersAndSite) { qubit = builder.h(qubit); return builder.intConstant(0); }), - "'qco.h'", "arity 1 and 0 parameter(s)", "[10]"); + "'qco.h'", "arity 1 and 0 parameter(s)"); expectUnsupported(Target{std::vector{Site{10}, Site{20}}, std::nullopt, std::vector{Operation{"x", 2, 0}}}, @@ -590,7 +622,7 @@ TEST_F(TargetSynthesisTest, ConformanceChecksTypeArityParametersAndSite) { qubit = builder.x(qubit); return builder.intConstant(0); }), - "'qco.x'", "arity 1 and 0 parameter(s)", "[10]"); + "'qco.x'", "arity 1 and 0 parameter(s)"); expectUnsupported(Target{std::vector{Site{10}}, std::nullopt, std::vector{Operation{"rz", 1, 0}}}, @@ -599,16 +631,7 @@ TEST_F(TargetSynthesisTest, ConformanceChecksTypeArityParametersAndSite) { qubit = builder.rz(0.25, qubit); return builder.intConstant(0); }), - "'qco.rz'", "arity 1 and 1 parameter(s)", "[10]"); - - expectUnsupported(Target{std::vector{Site{10}}, std::nullopt, - std::vector{Operation{"x", 1, 0}}}, - build([](QCOProgramBuilder& builder) { - auto qubit = builder.staticQubit(30); - qubit = builder.x(qubit); - return builder.intConstant(0); - }), - "'qco.x'", "arity 1 and 0 parameter(s)", "[30]"); + "'qco.rz'", "arity 1 and 1 parameter(s)"); } TEST_F(TargetSynthesisTest, ConformanceChecksNonUnitaryCapabilities) { @@ -627,64 +650,4 @@ TEST_F(TargetSynthesisTest, ConformanceChecksNonUnitaryCapabilities) { << diagnostics; } -TEST_F(TargetSynthesisTest, ConformanceTracesStructuredControlFlow) { - auto module = build([](QCOProgramBuilder& builder) { - auto qubit = builder.staticQubit(10); - qubit = builder.qcoIf( - true, qubit, [&](Value argument) { return builder.h(argument); }, - [&](Value argument) { return builder.h(argument); }); - qubit = - builder.scfFor(0, 1, 1, qubit, [&](Value, mlir::ValueRange arguments) { - return mlir::SmallVector{builder.x(arguments[0])}; - })[0]; - qubit = builder.h(qubit); - return builder.intConstant(0); - }); - const Target target{std::vector{Site{10}}, std::nullopt, - std::vector{Operation{"h", 1, 0}, Operation{"x", 1, 0}}}; - - ASSERT_TRUE(mlir::succeeded( - runPass(*module, mlir::qco::createTargetNativeSynthesis(target)))); - ASSERT_TRUE(mlir::succeeded( - runPass(*module, mlir::qco::createVerifyTargetConformance(target)))); -} - -TEST_F(TargetSynthesisTest, ConformanceTracesWhileSwitchMeasurementAndReset) { - auto module = build([](QCOProgramBuilder& builder) { - auto qubit = builder.staticQubit(10); - qubit = builder - .scfWhile( - mlir::ValueRange{qubit}, - [&](mlir::ValueRange arguments) { - auto [measured, result] = - builder.measure(arguments.front()); - builder.scfCondition(result, measured); - return mlir::SmallVector{measured}; - }, - [&](mlir::ValueRange arguments) { - return mlir::SmallVector{ - builder.reset(arguments.front())}; - }) - .front(); - qubit = builder.qcoIndexSwitch( - 0, qubit, mlir::SmallVector{0}, - mlir::SmallVector>{ - [&](Value argument) { return builder.h(argument); }}, - [&](Value argument) { return builder.h(argument); }); - auto [measured, result] = builder.measure(qubit); - static_cast(result); - static_cast(builder.h(builder.reset(measured))); - return builder.intConstant(0); - }); - const Target target{std::vector{Site{10}}, std::nullopt, - std::vector{Operation{"h", 1, 0}, - Operation{"measure", 1, 0}, - Operation{"reset", 1, 0}}}; - - ASSERT_TRUE(mlir::succeeded( - runPass(*module, mlir::qco::createTargetNativeSynthesis(target)))); - ASSERT_TRUE(mlir::succeeded( - runPass(*module, mlir::qco::createVerifyTargetConformance(target)))); -} - } // namespace mqt::test::qco diff --git a/mlir/unittests/Dialect/Utils/test_global_phase_normalization.cpp b/mlir/unittests/Dialect/Utils/test_global_phase_normalization.cpp index 6efa75a811..a31a1ea4ef 100644 --- a/mlir/unittests/Dialect/Utils/test_global_phase_normalization.cpp +++ b/mlir/unittests/Dialect/Utils/test_global_phase_normalization.cpp @@ -806,40 +806,6 @@ TEST_F(GlobalPhaseNormalizationTest, VerifiesPracticalConstantAngleRange) { } } -TEST_F(GlobalPhaseNormalizationTest, ScalesLinearlyAcrossLargePhaseScopes) { - constexpr std::array sizes{1'000, 10'000, 100'000}; - std::vector durations; - durations.reserve(sizes.size()); - - for (const auto size : sizes) { - SCOPED_TRACE(size); - OwningOpRef moduleOp = ModuleOp::create(UnknownLoc::get(context.get())); - OpBuilder builder(context.get()); - builder.setInsertionPointToStart(moduleOp->getBody()); - const auto loc = moduleOp->getLoc(); - auto function = func::FuncOp::create(builder, loc, "test", - builder.getFunctionType({}, {})); - auto* entry = function.addEntryBlock(); - builder.setInsertionPointToStart(entry); - const auto angle = utils::constantFromScalar(builder, loc, 0.001); - for (std::size_t i = 0; i < size; ++i) { - qco::GPhaseOp::create(builder, loc, angle); - } - func::ReturnOp::create(builder, loc); - - const auto start = std::chrono::steady_clock::now(); - ASSERT_TRUE(mlir::mqt::normalizeGlobalPhases(*moduleOp).succeeded()); - durations.emplace_back(std::chrono::steady_clock::now() - start); - EXPECT_EQ(llvm::range_size(function.getBody().getOps()), 1); - } - - RecordProperty("normalize_1000_ns", durations[0].count()); - RecordProperty("normalize_10000_ns", durations[1].count()); - RecordProperty("normalize_100000_ns", durations[2].count()); - EXPECT_LT(durations[1].count(), durations[0].count() * 50); - EXPECT_LT(durations[2].count(), durations[1].count() * 50); -} - TEST_F(GlobalPhaseNormalizationTest, ScalesLinearlyAcrossNestedDynamicIntegralPowers) { constexpr std::array depths{128, 256, 512, 1'024}; diff --git a/python/mqt/core/plugins/qiskit/backend.py b/python/mqt/core/plugins/qiskit/backend.py index df131e7911..dc14f94548 100644 --- a/python/mqt/core/plugins/qiskit/backend.py +++ b/python/mqt/core/plugins/qiskit/backend.py @@ -264,7 +264,7 @@ def _add_operation_to_target( Subclasses may override this to customize how an individual device operation is represented in the Target, e.g. substituting fictional - qubit-qubit loci for an operation that natively acts on non-qubit + pairs of qubit sites for an operation that natively acts on non-qubit sites (such as a qubit-resonator gate). Args: diff --git a/test/qdmi/devices/sc/test_configuration.cpp b/test/qdmi/devices/sc/test_configuration.cpp index 7089657c5d..0b92fdc4de 100644 --- a/test/qdmi/devices/sc/test_configuration.cpp +++ b/test/qdmi/devices/sc/test_configuration.cpp @@ -69,7 +69,7 @@ TEST(ScConfigurationTest, ParsesBundledDeviceStrictly) { TEST(ScConfigurationTest, ParsesIqmDeviceModels) { const auto assertModel = [](const Device& device, const std::string& name, const size_t qubits, const size_t couplings, - const size_t czLoci) { + const size_t czSiteTuples) { EXPECT_EQ(device.name, name); EXPECT_EQ(device.numQubits, qubits); EXPECT_EQ(device.couplings.size(), couplings); @@ -91,16 +91,16 @@ TEST(ScConfigurationTest, ParsesIqmDeviceModels) { EXPECT_EQ(device.operations[1].name, "cz"); EXPECT_EQ(device.operations[1].numParameters, 0); EXPECT_EQ(device.operations[1].numQubits, 2); - EXPECT_EQ(device.operations[1].siteOverrides.size(), czLoci); + EXPECT_EQ(device.operations[1].siteOverrides.size(), czSiteTuples); EXPECT_EQ(device.operations[2].name, "measure"); EXPECT_EQ(device.operations[2].numParameters, 0); EXPECT_EQ(device.operations[2].numQubits, 1); EXPECT_EQ(device.operations[2].siteOverrides.size(), qubits); for (const auto& operation : device.operations) { EXPECT_FALSE(operation.duration); - for (const auto& locus : operation.siteOverrides) { - EXPECT_FALSE(locus.duration); - EXPECT_TRUE(locus.fidelity); + for (const auto& siteOverride : operation.siteOverrides) { + EXPECT_FALSE(siteOverride.duration); + EXPECT_TRUE(siteOverride.fidelity); } } };