diff --git a/.agent/plans/directional-gate-mapping.md b/.agent/plans/directional-gate-mapping.md new file mode 100644 index 0000000000..56b5d2b257 --- /dev/null +++ b/.agent/plans/directional-gate-mapping.md @@ -0,0 +1,182 @@ +# Compile directional target gates through native synthesis + +This ExecPlan follows `.agent/PLANS.md` and records the supported contract, +implementation, and validation for ordered compiler-target applicability. + +## Purpose / Big Picture + +Compile gates for devices that support an entangler in only one operand order. +Routing makes operands adjacent; native synthesis repairs their direction and +final conformance checks exact physical sites. Alternating CX directions on two +adjacent sites must not introduce routing SWAPs. + +## Progress + +- [x] (2026-09-03) Preserve ordered operation support and calibration metadata. +- [x] (2026-09-04) Remove directional routing and its dedicated wrapper. +- [x] (2026-09-04) Replace ambiguous-site analysis with a checked staged walk. +- [x] (2026-09-04) Remove whole-module cloning for failed synthesis. +- [x] (2026-09-04) Add focused regressions and align public pass documentation. +- [x] (2026-09-04) Apply specialist, adversarial, and Ponytail Review feedback. +- [x] (2026-09-04) Pass 303 focused tests, documentation, and repository lint. +- [x] (2026-09-04) Attempt full C++ lint and analyze changed sources directly; + record the unrelated build blocker below. +- [x] (2026-09-04) Unify site tuples across the target, attributes, QDMI, and + Python. +- [x] (2026-09-04) Remove synthesis planning and repeated matrix extraction. +- [x] (2026-09-04) Validate the revised model and obtain adversarial review. +- [x] (2026-09-04) Accept plain Python placements and positional MLIR tuple + sites. +- [x] (2026-09-04) Consolidate target lookups and restore LLVM containers. +- [ ] (2026-09-04) Validate compact syntax and the final synthesis + simplification. + +## Decision Log + +On 2026-09-03 the maintainer approved adjacency-only routing. Direction repair +belongs to synthesis. Weighted routing edges remain possible future work; there +is no current need for an extra cost wrapper. + +On 2026-09-03 the maintainer approved requiring one known physical site per +quantum value, equal branch-result sites, and site-preserving loop backedges. +These conditions are checked, including for all-to-all placement and standalone +passes. Ordinary structured control flow remains supported. + +On 2026-09-03 the maintainer approved removing synthesis rollback. Compilation +runs in place; callers must not rely on program contents after failure. Generic +capability tuples and constant-time ordered-pair support queries remain part of +the target model. + +On 2026-09-04 the maintainer approved one `site_tuples` list and no +applicability enum. An empty list means general applicability; a nonempty list +contains every supported ordered placement with optional calibration. Missing +values inherit operation defaults. The QDMI adapter omits operations reported +with no supported placements and retains uncalibrated supported tuples. + +Plain Python tuples and lists denote uncalibrated placements. Explicit +`SiteTuple` values remain available for calibration. MLIR prints positional +sites as `<[4, 7]>`, with named optional calibration fields. + +## Surprises & Discoveries + +An executed two-site probe produced five native CXs with directional routing and +two with direct synthesis. The extra SWAP is avoidable. Another valid-IR probe +passed a site through `scf.execute_region`; name-only fallback incorrectly +accepted a reversed CX. Unknown site transfers must fail with a diagnostic. + +Explicit mapping realigns structured region exits to physical slots. All-to-all +placement only replaces allocations, so site consistency must be checked rather +than assumed. Runtime symmetric gates such as RXX need direct operand reordering +because their matrix is unavailable at compile time. + +LLVM 23 dense maps track occupancy separately and no longer reserve sentinel +keys. Standard LLVM dense containers therefore support the full nonnegative +site-ID range without custom traits. One per-operation tuple set borrows keys +from immutable target storage and replaces arity-specific lookup caches. + +## Context and Orientation + +`mlir/lib/Compiler/Target.cpp` owns immutable target capabilities and basis +selection. A usable synthesis basis supplies one-qubit gates on every site and +an entangler on every routing edge in at least one direction. Each supported +site tuple may carry calibration overrides. + +`mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp` performs placement and +routing. `mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp` +then assigns exact sites, preserves or reorders native gates, and decomposes +other supported gates. Its conformance pass checks emitted capabilities. + +## Plan of Work + +First remove `mlir/include/mlir/Compiler/MappingTarget.h`, its implementation +and dedicated tests, and restore topology-only mapping and build wiring. Prove +that alternating CXs need no routing SWAPs and only two native entanglers. + +Next use MLIR's staged operation walk and one site map. Propagate sites through +unitaries, reset, and measurement, seed supported region arguments, and compare +branch results and loop backedges. Reject unknown or conflicting sites. Remove +the module clone and duplicate planning. Preserve matrix/output permutation for +directional synthesis and direct symmetric operand reordering. + +Finally update `docs/mlir/target_compilation.md`, pass descriptions, and the +existing changelog entry. Keep regression tests in the established native +synthesis and compiler test suites. Obtain independent reviews after the first +implementation, then incorporate the separate Ponytail Review findings. + +## Concrete Steps and Validation + +Run from the repository root with the configured LLVM/MLIR 23 installation: + + cmake --preset release + cmake --build --preset release --target mqt-core-mlir-unittests-compiler mqt-core-mlir-unittest-mapping mqt-core-mlir-unittest-target-synthesis mqt-core-mlir-unittest-mqt-ir -j 8 + build/release/mlir/unittests/Compiler/mqt-core-mlir-unittests-compiler + build/release/mlir/unittests/Dialect/QCO/Transforms/Mapping/mqt-core-mlir-unittest-mapping + build/release/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/mqt-core-mlir-unittest-target-synthesis + build/release/mlir/unittests/Dialect/MQT/IR/mqt-core-mlir-unittest-mqt-ir + uvx nox -s stubs + SKBUILD_CMAKE_ARGS=-DBUILD_MQT_CORE_QDMI_SC_DEVICE=ON uvx nox -s tests-3.13 -- test/python/test_mlir.py -q + uvx nox -s cpp-lint + uvx nox --non-interactive -s docs + git diff --check + uvx nox -s lint + +Successful output must verify, retain exact ordered target applicability and +quantum semantics, and support consistent if/switch/for/while site transfers. +Unknown sites, conflicting branch exits, and changing loop-backedge sites must +be diagnosed. Failed compilation need not preserve input IR. C++, Python, and +serialized targets use only `site_tuples` for ordered availability and +calibration. + +The tuple simplification removes duplicate lists, attributes, and validation +from C++, MLIR, QDMI, and Python. Native synthesis processes users before their +producers, keeping original site facts valid while rewriting each operation +immediately. Use the existing bounded site walk: generic control-flow interfaces +prune known loop edges and require extra exceptions for this contract. + +Fusion also visits operations in reverse order. When a run head fuses its +successors, those operations have already been visited. This removes the +run-head snapshot and duplicate matrix extraction, and can expose earlier +cancellations when a later run disappears. Each rewrite still strictly reduces +the number of two-qubit operations. + +## Idempotence and Recovery + +Builds and checks are repeatable. Preserve unrelated changes and keep generated +build output untracked. No dependency additions or generated-file edits are +needed. + +## Outcomes & Retrospective + +The target model now has one tuple list with optional calibration. Its enum, +duplicate lists, attributes, validators, and serialization paths are removed. +Synthesis checks and rewrites each gate in reverse order, without a separate +plan or repeated matrix extraction. The tuple-model round removed 284 production +lines. The compact-syntax and lookup round removes another 91 production lines: +a shared LLVM tuple cache, fewer single-use wrappers, and direct reverse fusion. +Python accepts plain tuples or lists; explicit `SiteTuple` values add +calibration. MLIR prints positional tuple sites. + +Specialist and adversarial review found no remaining blockers. Adversarial +review retained a compact shared matrix guard for unsupported multi-target +control shells; its regression verifies that the input is valid and linear +before checking the diagnostic. Ordinary dependent rewrites retain semantic +equivalence. + +All 307 focused C++ tests pass: compiler 153, mapping 94, target synthesis 44, +and MQT IR 16. All 51 Python MLIR tests pass. Python stubs are regenerated, and +strict documentation and repository lint pass. Full C++ lint stops before +analysis because unchanged QIR runtime test executables have unresolved QTensor +symbols. Building the ten changed C++ translation units directly succeeds; the +same whole-file linter reports zero findings across all ten files. No lint +configuration or unrelated build wiring was changed. + +The final specialist, adversarial, and Ponytail reviews found no further useful +deletion within the supported contract. The new fusion regression cancels +`CX01, CX02, CX02, CX01` and checks decision-diagram equivalence. Compact syntax +checks cover mixed calibrated placements and calibration roundtrips. Cache +checks cover maximum site IDs and retained target copies. The initial Python run +reused a package without the test device; rebuilding with +`BUILD_MQT_CORE_QDMI_SC_DEVICE=ON` resolves both device fixture errors. + +Revision note: aligned the scope with the approved routing, site, and failure +contracts while retaining exact device metadata. diff --git a/CHANGELOG.md b/CHANGELOG.md index c42ac704ff..89e756e28a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,9 +38,10 @@ releases may include breaking changes. direct lowering and dense-array helpers for supported compiler inputs ([#1915], [#1973], [#2077], [#2078], [#2079], [#2334]) ([**@simon1hofmann**], [**@burgholzer**]) -- ✨ Add immutable MLIR compiler targets, QDMI device integration, and target - compilation through C++, Python, and `mqt-cc` ([#1687], [#1993], [#1999], - [#2049]) ([**@MatthiasReumann**], [**@simon1hofmann**], [**@burgholzer**]) +- ✨ Add immutable MLIR compiler targets, QDMI device integration, ordered + operation applicability, directional native synthesis, and target compilation + through C++, Python, and `mqt-cc` ([#1687], [#1993], [#1999], [#2049], + [#2285]) ([**@MatthiasReumann**], [**@simon1hofmann**], [**@burgholzer**]) #### Import and export @@ -889,6 +890,7 @@ for previous changelogs._ [#2315]: https://github.com/munich-quantum-toolkit/core/pull/2315 [#2299]: https://github.com/munich-quantum-toolkit/core/pull/2299 [#2298]: https://github.com/munich-quantum-toolkit/core/pull/2298 +[#2285]: https://github.com/munich-quantum-toolkit/core/pull/2285 [#2284]: https://github.com/munich-quantum-toolkit/core/pull/2284 [#2283]: https://github.com/munich-quantum-toolkit/core/pull/2283 [#2288]: https://github.com/munich-quantum-toolkit/core/pull/2288 diff --git a/bindings/mlir/register_mlir.cpp b/bindings/mlir/register_mlir.cpp index 1c59629a45..17bbf81098 100644 --- a/bindings/mlir/register_mlir.cpp +++ b/bindings/mlir/register_mlir.cpp @@ -530,7 +530,7 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); auto siteTuple = nb::class_( compilerTarget, "SiteTuple", - "Calibration data for an ordered tuple of target sites."); + "A supported ordered placement with optional calibration."); siteTuple .def( "__init__", @@ -555,6 +555,9 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); .def_prop_ro("fidelity", &mlir::CompilerTarget::SiteTuple::fidelity, "The operation fidelity, if available."); + nb::implicitly_convertible, + mlir::CompilerTarget::SiteTuple>(); + nb::enum_( compilerTarget, "OperationArityKind", "How an operation capability accepts qubit widths.") @@ -580,7 +583,8 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); auto targetOperation = nb::class_( compilerTarget, "Operation", - "A homogeneous target-wide operation capability and its calibration."); + "A target operation capability, calibration, and ordered " + "applicability."); targetOperation .def( "__init__", @@ -644,7 +648,8 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); return std::vector( operation.siteTuples().begin(), operation.siteTuples().end()); }, - "Ordered site-specific calibration data.") + "Supported ordered placements with optional calibration; empty means " + "general applicability.") .def_prop_ro("duration", &mlir::CompilerTarget::Operation::duration, "The raw default duration, if available.") .def_prop_ro("fidelity", &mlir::CompilerTarget::Operation::fidelity, @@ -905,11 +910,17 @@ either unrestricted or explicitly enumerated native-operation support.)pb"); .def( "supports_operation", [](const mlir::CompilerTarget& target, const std::string_view name, - const size_t arity, const std::optional numParameters) { + const size_t arity, const std::optional numParameters, + const std::optional>& + sites) { + if (sites) { + return target.supportsOperation(name, arity, numParameters, + *sites); + } return target.supportsOperation(name, arity, numParameters); }, "name"_a, "arity"_a, "num_parameters"_a = nb::none(), - "Whether the target supports an operation."); + "sites"_a = nb::none(), "Whether the target supports an operation."); auto program = nb::class_( m, "Program", R"pb(Base class for a typed MLIR compiler program. diff --git a/bindings/patterns.txt b/bindings/patterns.txt index d2a24897c9..46db30f7e4 100644 --- a/bindings/patterns.txt +++ b/bindings/patterns.txt @@ -135,7 +135,7 @@ mqt\.core\.mlir\.CompilerTarget\.Operation\.__init__$: name: str, arity: int | CompilerTarget.OperationArity, num_parameters: int, - site_tuples: Sequence[CompilerTarget.SiteTuple] | None = None, + site_tuples: Sequence[CompilerTarget.SiteTuple | Sequence[int]] | None = None, duration: int | None = None, fidelity: float | None = None, ) -> None: diff --git a/docs/mlir/target_compilation.md b/docs/mlir/target_compilation.md index 6a096e949b..c50244c583 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -2,9 +2,9 @@ An MLIR {code}`mlir::CompilerTarget` is an immutable snapshot of a circuit-model device. It contains the device sites, topology, native operations, and available -calibration data. Compilation decomposes supported multi-qubit operations, -optimizes and maps the program, synthesizes native gates, and verifies that the -result conforms to the target. +calibration and ordered-applicability data. Compilation decomposes supported +multi-qubit operations, optimizes and maps the program, synthesizes native +gates, and verifies that the result conforms to the target. The snapshot is independent of its originating QDMI session. It can therefore be stored, copied cheaply, and reused for multiple compilations. @@ -41,7 +41,12 @@ target = CompilerTarget( num_parameters=1, ), CompilerTarget.Operation("u", arity=1, num_parameters=3), - CompilerTarget.Operation("cx", arity=2, num_parameters=0), + CompilerTarget.Operation( + "cx", + arity=2, + num_parameters=0, + site_tuples=[(1, 0), (1, 2)], + ), CompilerTarget.Operation("measure", arity=1, num_parameters=0), CompilerTarget.Operation("reset", arity=1, num_parameters=0), ]), @@ -58,10 +63,26 @@ not provide a complete connectivity model and a representable native-operation set. An explicit operation arity is either fixed or variadic with a positive, inclusive minimum. Fixed zero represents a global-phase operation. A variadic capability accepts every total width from its minimum through the target's site -count; site-specific calibration tuples are therefore available only for fixed, -positive arities. Structural and program-format constructs are not +count; site tuples are therefore available only for fixed, positive arities. An +empty `site_tuples` list makes an operation available on every valid placement. +A nonempty list contains all supported ordered placements. Each tuple may carry +calibration values; omitted values inherit the operation-wide defaults. Retain +placements without calibration in this list, and omit operations that are not +available anywhere. Structural and program-format constructs are not compiler-target operations. +Use plain tuples for placements without calibration. Use +`CompilerTarget.SiteTuple([1, 0], duration=40, fidelity=0.99)` to attach +calibration to a placement; both forms can appear in the same list. + +Routing uses undirected adjacency; native synthesis repairs unsupported operand +directions. Target compilation requires a known static physical site for each +qubit. Structured branch exits must agree on sites, and loop backedges must +preserve the entry sites. Unsupported or inconsistent site transfers are +diagnosed, including after all-to-all placement. A synthesis basis must provide +the same one-qubit gate family on every site and an entangler on every routing +edge in at least one direction. + Target synthesis preserves a native `gphase`. If the target does not support `gphase`, target synthesis preserves relative phase effects and removes only the unobservable global phase of the entry point. @@ -136,9 +157,9 @@ if (!qco || !qco->compileForTarget(*target)) { } ``` -The adapter accepts circuit-model devices whose operations are available -throughout the topology in both operand orientations. Operand-symmetric gates, -such as CZ, may report each edge once. Operations with arity above two must +The adapter accepts circuit-model devices whose two-qubit operations cover every +topology edge in at least one operand orientation and preserves the exact +ordered tuples reported by the device. Operations with arity above two must report every ordered tuple of distinct sites. Neutral-atom zone models require a different compilation model and are rejected with a diagnostic. diff --git a/mlir/include/mlir/Compiler/QDMIAdapter.h b/mlir/include/mlir/Compiler/QDMIAdapter.h index 979a7286d0..ab3577e6b0 100644 --- a/mlir/include/mlir/Compiler/QDMIAdapter.h +++ b/mlir/include/mlir/Compiler/QDMIAdapter.h @@ -29,8 +29,10 @@ namespace mlir { * * @details The returned target owns all queried metadata and remains valid * after the originating device and session have been destroyed. Neutral-atom - * zone models and site-dependent operation support are not supported by the - * circuit-model compiler pipeline. + * zone models are not supported. Explicit QDMI site lists must cover every + * site for one-qubit operations, every undirected topology edge for two-qubit + * operations, and every ordered tuple of distinct sites for higher arities. + * Each supported ordered placement carries optional calibration data. */ [[nodiscard]] llvm::Expected compilerTargetFromDevice(const qdmi::Device& device); diff --git a/mlir/include/mlir/Compiler/Target.h b/mlir/include/mlir/Compiler/Target.h index 41ac93832d..e341e1a9dc 100644 --- a/mlir/include/mlir/Compiler/Target.h +++ b/mlir/include/mlir/Compiler/Target.h @@ -125,10 +125,10 @@ class CompilerTarget { std::optional t2_; }; - /// Calibration data for an ordered tuple of hardware sites. + /// One supported ordered placement and its optional calibration data. class SiteTuple { public: - /// Create validated calibration data for a site tuple. + /// Create a validated site tuple with optional calibration overrides. [[nodiscard]] static llvm::Expected create(std::vector sites, std::optional duration = std::nullopt, @@ -137,10 +137,10 @@ class CompilerTarget { /// Return the ordered target site identifiers. [[nodiscard]] llvm::ArrayRef sites() const noexcept; - /// Return the raw operation duration, if available. + /// Return the raw duration override; nullopt uses the operation default. [[nodiscard]] std::optional duration() const noexcept; - /// Return the operation fidelity, if available. + /// Return the fidelity override; nullopt uses the operation default. [[nodiscard]] std::optional fidelity() const noexcept; private: @@ -156,8 +156,9 @@ class CompilerTarget { /// /// 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. + /// with no site tuples are generally applicable. A nonempty list gives all + /// supported ordered placements. Missing tuple calibration values inherit + /// the operation defaults. class Operation { public: /// The accepted number of qubits for an operation capability. @@ -216,7 +217,7 @@ class CompilerTarget { /// Return the number of real-valued operation parameters. [[nodiscard]] size_t numParameters() const noexcept; - /// Return ordered site-specific calibration data. + /// Return all supported ordered placements, or empty for general support. [[nodiscard]] llvm::ArrayRef siteTuples() const noexcept; /// Return the raw default operation duration, if available. @@ -390,12 +391,25 @@ class CompilerTarget { supportsOperation(llvm::StringRef name, size_t arity, std::optional numParameters = std::nullopt) const; + /// Return whether an operation capability is supported on ordered sites. + [[nodiscard]] bool supportsOperation(llvm::StringRef name, size_t arity, + std::optional numParameters, + llvm::ArrayRef sites) const; + /// Return whether a QCO operation is supported. [[nodiscard]] bool supports(::mlir::Operation* operation) const; - /// Return whether a recognized gate is supported. + /// Return whether a QCO operation is supported on ordered target sites. + [[nodiscard]] bool supports(::mlir::Operation* operation, + llvm::ArrayRef sites) const; + + /// Return whether a recognized gate is supported by the target. [[nodiscard]] bool supports(GateKind gate) const; + /// Return whether a recognized gate is supported on ordered target sites. + [[nodiscard]] bool supports(GateKind gate, + llvm::ArrayRef sites) const; + /// Return the recognized gates supported by the target. [[nodiscard]] llvm::ArrayRef supportedGates() const noexcept; @@ -416,7 +430,9 @@ class CompilerTarget { Connectivity connectivity, NativeOperations nativeOperations, std::optional durationUnit); - [[nodiscard]] llvm::ArrayRef explicitNeighbours(size_t vertex) const; + [[nodiscard]] bool + supportsImpl(::mlir::Operation* operation, + std::optional> sites) const; std::shared_ptr storage_; }; diff --git a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td index 35e686edbb..03083a2aa6 100644 --- a/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td +++ b/mlir/include/mlir/Dialect/MQT/IR/MQTDialect.td @@ -127,16 +127,18 @@ def CouplingAttr : MQTAttr<"Coupling", "coupling"> { } def SiteTupleAttr : MQTAttr<"SiteTuple", "site_tuple"> { - let summary = "Calibration data for an ordered site tuple"; + let summary = "One supported ordered placement with optional calibration"; let description = [{ - The tuple defines one placement of a native operation. For example, - `#mqt.site_tuple` records an ordered + The tuple records one supported ordered placement and optional calibration + overrides. Missing calibration values inherit the operation defaults. + For example, + `#mqt.site_tuple<[4, 7], duration = 40>` records an ordered two-site placement with a raw duration of 40. }]; let parameters = (ins MQTArrayRefParameter<"int64_t">:$sites, MQTOptionalUInt64Parameter<>:$duration, OptionalParameter<"FloatAttr">:$fidelity); - let assemblyFormat = "`<` struct(params) `>`"; + let assemblyFormat = "`<` $sites (`,` struct($duration, $fidelity)^)? `>`"; let genVerifyDecl = 1; } @@ -158,13 +160,14 @@ def NativeOperationAttr : MQTAttr<"NativeOperation", "native_operation"> { let summary = "One native compiler-target operation"; let description = [{ The operation records its spelling, arity, parameter count, and optional - global or site-specific calibration data. The following example records a - placed controlled-X operation: + global or site-specific calibration data. An empty site-tuple list means + general applicability; a nonempty list gives all supported ordered + placements. The following example records a directional controlled-X operation: ```mlir #mqt.native_operation, - num_parameters = 0, site_tuples = []> + num_parameters = 0, site_tuples = [<[4, 7]>]> ``` }]; let parameters = (ins "StringAttr":$name, "OperationArityAttr":$arity, @@ -193,7 +196,7 @@ def CompilationTargetAttr : MQTAttr<"CompilationTarget", "compilation_target"> { native_operations = explicit, operations = [, - num_parameters = 0, site_tuples = []>]> + num_parameters = 0, site_tuples = [<[4, 7]>]>]> ``` }]; let parameters = (ins OptionalParameter<"StringAttr">:$name, diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h index 33a70cede8..25eb69810f 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h @@ -47,15 +47,14 @@ namespace mlir::qco { createDecomposeMultiControlled(const CompilerTarget& target, uint64_t minQubits = 3); -/** - * @brief Create post-routing synthesis for one immutable compiler target. - */ +/// Create post-routing synthesis for one immutable compiler target. +/// Each qubit must have a known static site. Structured branch exits must agree +/// on sites and loop backedges must preserve their entry sites. +/// The input may be modified on failure. [[nodiscard]] std::unique_ptr createTargetNativeSynthesis(const CompilerTarget& target); -/** - * @brief Create the final mapped-operation conformance verifier. - */ +/// Create the final mapped-operation verifier, requiring known static sites. [[nodiscard]] std::unique_ptr createVerifyTargetConformance(const CompilerTarget& target); diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index 1974f80faf..3ba1af6181 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -146,15 +146,17 @@ def MappingPass : Pass<"place-and-route", "mlir::ModuleOp"> { - `f(n) = g(n) + h(n)` - `g(n) = alpha * depth(n)` - - `h(n) = sum(pow(lambda, i) * h(L, p) for [i, L] in enumerate(layers))` + - `h(n) = sum(pow(lambda, i) * h(gate, p) for [i, gate] in enumerate(window))` Where: - `p` is the dynamic-to-static mapping associated with search node `n`. - - `layers` is an array of layers with size `1 + nlookahead`. + - `window` contains at most `1 + nlookahead` two-qubit operations in program order. - `depth(n)` returns the distance from the node `n` to the root node. - `dist(i, j)` returns the distance between the qubits `i` and `j` on the target's coupling graph. - - `h(L, p) := sum(dist(p[gate.first], p[gate.second]) for gate in L)` + - `h(gate, p)` is `dist(p[gate.first], p[gate.second]) - 1`. + + Routing uses undirected target connectivity. Target-native synthesis realizes operations and inserted SWAPs in a supported operand direction. To iteratively refine the mapping, the pass performs multiple forward and backward traversals of the circuit. In each traversal, the pass routes the circuit and updates the dynamic-to-static mapping based on the routing decisions diff --git a/mlir/lib/Compiler/QDMIAdapter.cpp b/mlir/lib/Compiler/QDMIAdapter.cpp index ad215d07e7..98f495c135 100644 --- a/mlir/lib/Compiler/QDMIAdapter.cpp +++ b/mlir/lib/Compiler/QDMIAdapter.cpp @@ -15,7 +15,6 @@ #include "qdmi/driver/Driver.hpp" #include -#include #include #include #include @@ -127,15 +126,6 @@ allToAllCouplingCount(size_t numSites) { return llvm::checkedMulUnsigned(first, second); } -[[nodiscard]] static bool -isSwapInvariantOperation(llvm::StringRef operationName) { - const auto canonicalName = operationName.trim().lower(); - return llvm::StringSwitch(canonicalName) - .Cases({"cz", "swap", "iswap"}, true) - .Cases({"rxx", "ryy", "rzz"}, true) - .Default(false); -} - [[nodiscard]] static llvm::Error validateHomogeneousSupport( const qdmi::Operation& operation, size_t arity, const std::vector& flattenedSites, @@ -257,25 +247,10 @@ isSwapInvariantOperation(llvm::StringRef operationName) { return supportedCouplings.contains(coupling); }); } - if (auto error = requireRepresentableOperation( - coversTarget, deviceName, operationName, - couplings ? "support is not homogeneous across all topology edges" - : "support is not homogeneous across all-to-all site " - "pairs")) { - return error; - } - return requireRepresentableOperation( - isSwapInvariantOperation(operationName) || - std::ranges::all_of( - supportedCouplings, - [&](const auto& coupling) { - return reportedTuples.contains(coupling) && - reportedTuples.contains(CompilerTarget::Coupling{ - coupling.second, coupling.first}); - }), - deviceName, operationName, - "both orientations must be available on every supported site pair"); + coversTarget, deviceName, operationName, + couplings ? "support is not homogeneous across all topology edges" + : "support is not homogeneous across all-to-all site pairs"); } [[nodiscard]] static llvm::Expected> @@ -299,12 +274,12 @@ snapshotDurationUnit(const qdmi::Device& device) { } [[nodiscard]] static llvm::Expected> -snapshotSiteTuples(const qdmi::Operation& operation, size_t arity, - const std::vector& flattenedSites, - std::optional defaultDuration, - std::optional defaultFidelity) { - std::vector siteTuples; - siteTuples.reserve(flattenedSites.size() / arity); +snapshotOperationSites(const qdmi::Operation& operation, size_t arity, + const std::vector& flattenedSites, + std::optional defaultDuration, + std::optional defaultFidelity, bool variadic) { + std::vector result; + result.reserve(flattenedSites.size() / arity); for (size_t offset = 0; offset < flattenedSites.size(); offset += arity) { std::vector sites; std::vector siteIds; @@ -322,16 +297,20 @@ snapshotSiteTuples(const qdmi::Operation& operation, size_t arity, const auto duration = operation.getDuration(sites); const auto fidelity = operation.getFidelity(sites); - if (duration != defaultDuration || fidelity != defaultFidelity) { - auto siteTuple = CompilerTarget::SiteTuple::create(std::move(siteIds), - duration, fidelity); + const bool hasSiteCalibration = + duration != defaultDuration || fidelity != defaultFidelity; + if (!variadic || hasSiteCalibration) { + auto siteTuple = CompilerTarget::SiteTuple::create( + std::move(siteIds), + duration == defaultDuration ? std::nullopt : duration, + fidelity == defaultFidelity ? std::nullopt : fidelity); if (!siteTuple) { return siteTuple.takeError(); } - siteTuples.emplace_back(std::move(*siteTuple)); + result.emplace_back(std::move(*siteTuple)); } } - return siteTuples; + return result; } [[nodiscard]] static llvm::Expected @@ -363,6 +342,9 @@ snapshotOperations( return error; } const auto flattenedSites = operation.getSites(); + if (*arity > 0 && flattenedSites && flattenedSites->empty()) { + continue; + } if (auto error = requireRepresentableOperation( *arity == 0 || flattenedSites || homogeneousOperationSupport, deviceName, operation.getName(), @@ -385,8 +367,9 @@ snapshotOperations( deviceSites, couplings, deviceName)) { return error; } - auto tuples = snapshotSiteTuples(operation, *arity, *flattenedSites, - duration, fidelity); + auto tuples = + snapshotOperationSites(operation, *arity, *flattenedSites, duration, + fidelity, hasArbitraryPositiveControls); if (!tuples) { return tuples.takeError(); } diff --git a/mlir/lib/Compiler/Target.cpp b/mlir/lib/Compiler/Target.cpp index d72309d422..627abaef2a 100644 --- a/mlir/lib/Compiler/Target.cpp +++ b/mlir/lib/Compiler/Target.cpp @@ -14,6 +14,9 @@ #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include +#include +#include #include #include #include @@ -36,8 +39,6 @@ #include #include #include -#include -#include #include #include @@ -244,7 +245,7 @@ llvm::Expected CompilerTarget::SiteTuple::create(std::vector sites, std::optional duration, std::optional fidelity) { - std::unordered_set uniqueSites; + llvm::SmallDenseSet uniqueSites; for (const auto site : sites) { if (site < 0) { return invalidTarget( @@ -339,18 +340,18 @@ llvm::Expected CompilerTarget::Operation::create( "Compiler target zero-arity operation cannot contain site tuples"); } - SmallVector> uniqueSiteCombinations; + llvm::SmallDenseSet> uniqueSiteCombinations; for (const auto& siteTuple : siteTuples) { if (!arity.accepts(siteTuple.sites().size())) { return invalidTarget( "Compiler target operation site tuple does not match its arity"); } - if (llvm::is_contained(uniqueSiteCombinations, siteTuple.sites())) { + if (!uniqueSiteCombinations.insert(siteTuple.sites()).second) { return invalidTarget( "Compiler target operation contains a duplicate site tuple"); } - uniqueSiteCombinations.emplace_back(siteTuple.sites()); } + return Operation(std::move(name), std::move(canonicalName), arity, numParameters, std::move(siteTuples), duration, fidelity); } @@ -427,29 +428,23 @@ struct CompilerTarget::Storage { SmallVector targetOperations, std::optional targetDurationUnit); - [[nodiscard]] static llvm::Expected> - create(std::optional targetName, std::vector targetSites, - Connectivity::Kind targetConnectivityKind, - SmallVector targetCouplings, - NativeOperations::Kind targetNativeOperationsKind, - SmallVector targetOperations, - std::optional targetDurationUnit); - [[nodiscard]] llvm::Error initialize(); [[nodiscard]] bool supportsOperation(StringRef name, size_t arity, - std::optional numParameters) const; - [[nodiscard]] bool - supportsVariadicOperation(StringRef name, size_t arity, - std::optional numParameters) const; + std::optional numParameters, + std::optional> orderedSites = std::nullopt, + bool variadicOnly = false) const; + [[nodiscard]] bool supportsGate( + GateKind gate, + std::optional> orderedSites = std::nullopt) const; [[nodiscard]] std::optional resolveSynthesisBasis() const; std::optional name; std::optional durationUnit; std::vector sites; SmallVector siteIds; - std::unordered_map siteToVertex; + llvm::DenseMap siteToVertex; Connectivity::Kind connectivityKind; SmallVector couplings; SmallVector> adjacency; @@ -458,6 +453,8 @@ struct CompilerTarget::Storage { NativeOperations::Kind nativeOperationsKind; SmallVector operations; llvm::StringMap> capabilities; + /// Keys borrow the immutable site tuples owned by operations. + std::vector>> operationSites; SmallVector supportedGates; std::optional basis; }; @@ -475,24 +472,6 @@ CompilerTarget::Storage::Storage( nativeOperationsKind(targetNativeOperationsKind), operations(std::move(targetOperations)) {} -llvm::Expected> -CompilerTarget::Storage::create( - std::optional targetName, std::vector targetSites, - Connectivity::Kind targetConnectivityKind, - SmallVector targetCouplings, - NativeOperations::Kind targetNativeOperationsKind, - SmallVector targetOperations, - std::optional targetDurationUnit) { - auto storage = std::make_shared( - std::move(targetName), std::move(targetSites), targetConnectivityKind, - std::move(targetCouplings), targetNativeOperationsKind, - std::move(targetOperations), std::move(targetDurationUnit)); - if (auto error = storage->initialize()) { - return std::move(error); - } - return std::shared_ptr(std::move(storage)); -} - llvm::Error CompilerTarget::Storage::initialize() { if (name && name->empty()) { return invalidTarget("Compiler target name must not be empty when present"); @@ -529,8 +508,8 @@ llvm::Error CompilerTarget::Storage::initialize() { adjacency.resize(sites.size()); for (const auto& [source, target] : couplings) { - const auto sourceVertex = siteToVertex.at(source); - const auto targetVertex = siteToVertex.at(target); + const auto sourceVertex = siteToVertex.lookup(source); + const auto targetVertex = siteToVertex.lookup(target); adjacency[sourceVertex].emplace_back(targetVertex); adjacency[targetVertex].emplace_back(sourceVertex); } @@ -571,6 +550,7 @@ llvm::Error CompilerTarget::Storage::initialize() { } if (nativeOperationsKind == NativeOperations::Kind::Explicit) { + operationSites.resize(operations.size()); for (const auto [index, operation] : llvm::enumerate(operations)) { if (operation.arity().value() > sites.size()) { if (operation.arity().kind() == Operation::Arity::Kind::Variadic) { @@ -580,13 +560,17 @@ llvm::Error CompilerTarget::Storage::initialize() { return invalidTarget( "Compiler target operation arity exceeds its site count"); } + auto& supportedSites = operationSites[index]; + supportedSites.reserve(operation.siteTuples().size()); for (const auto& siteTuple : operation.siteTuples()) { - if (llvm::any_of(siteTuple.sites(), [&](const auto site) { + auto tupleSites = siteTuple.sites(); + if (llvm::any_of(tupleSites, [&](const auto site) { return !siteToVertex.contains(site); })) { return invalidTarget("Compiler target operation site tuple " "references an unknown site"); } + supportedSites.insert(tupleSites); } capabilities[operation.canonicalName()].emplace_back(index); } @@ -608,16 +592,7 @@ llvm::Error CompilerTarget::Storage::initialize() { } for (const auto& specification : GATE_SPECIFICATIONS) { - const bool supportsControlledBase = - (specification.kind == GateKind::CX && - supportsVariadicOperation("x", specification.arity, - specification.numParameters)) || - (specification.kind == GateKind::CZ && - supportsVariadicOperation("z", specification.arity, - specification.numParameters)); - if (supportsControlledBase || - supportsOperation(specification.name, specification.arity, - specification.numParameters)) { + if (supportsGate(specification.kind)) { supportedGates.emplace_back(specification.kind); } } @@ -626,12 +601,21 @@ llvm::Error CompilerTarget::Storage::initialize() { } bool CompilerTarget::Storage::supportsOperation( - StringRef operationName, size_t arity, - std::optional numParameters) const { + StringRef operationName, size_t arity, std::optional numParameters, + std::optional> orderedSites, bool variadicOnly) const { const auto canonical = canonicalOperationName(operationName); - if (canonical.empty() || arity > sites.size()) { + if (canonical.empty() || arity > sites.size() || + (orderedSites && orderedSites->size() != arity)) { return false; } + if (orderedSites) { + for (const auto [index, site] : llvm::enumerate(*orderedSites)) { + if (!siteToVertex.contains(site) || + llvm::is_contained(orderedSites->take_front(index), site)) { + return false; + } + } + } if (nativeOperationsKind == NativeOperations::Kind::Unrestricted) { return true; } @@ -641,63 +625,119 @@ bool CompilerTarget::Storage::supportsOperation( } return llvm::any_of(found->second, [&](const auto index) { const auto& operation = operations[index]; - return operation.arity().accepts(arity) && - (!numParameters || operation.numParameters() == *numParameters); + return (!variadicOnly || + operation.arity().kind() == Operation::Arity::Kind::Variadic) && + operation.arity().accepts(arity) && + (!numParameters || operation.numParameters() == *numParameters) && + (!orderedSites || operation.siteTuples().empty() || + operationSites[index].contains(*orderedSites)); }); } -bool CompilerTarget::Storage::supportsVariadicOperation( - StringRef operationName, size_t arity, - std::optional numParameters) const { - const auto canonical = canonicalOperationName(operationName); - if (canonical.empty() || arity > sites.size()) { - return false; - } - if (nativeOperationsKind == NativeOperations::Kind::Unrestricted) { +bool CompilerTarget::Storage::supportsGate( + GateKind gate, std::optional> orderedSites) const { + if ((gate == GateKind::CX && + supportsOperation("x", 2, 0, orderedSites, /*variadicOnly=*/true)) || + (gate == GateKind::CZ && + supportsOperation("z", 2, 0, orderedSites, /*variadicOnly=*/true))) { return true; } - const auto found = capabilities.find(canonical); - if (found == capabilities.end()) { - return false; - } - return llvm::any_of(found->second, [&](const auto index) { - const auto& operation = operations[index]; - return operation.arity().kind() == Operation::Arity::Kind::Variadic && - operation.arity().accepts(arity) && - (!numParameters || operation.numParameters() == *numParameters); - }); + const decltype(GATE_SPECIFICATIONS.cbegin()) specification = + std::ranges::find(GATE_SPECIFICATIONS, gate, &GateSpecification::kind); + assert(specification != GATE_SPECIFICATIONS.end() && + "unknown compiler target gate"); + return supportsOperation(specification->name, specification->arity, + specification->numParameters, orderedSites); } std::optional CompilerTarget::Storage::resolveSynthesisBasis() const { - const auto supports = [&](GateKind gate) { - return llvm::is_contained(supportedGates, gate); + const auto supportsEveryPlacement = [&](StringRef operationName, size_t arity, + size_t numParameters, + bool variadicOnly = false) { + if (nativeOperationsKind == NativeOperations::Kind::Unrestricted) { + return true; + } + const auto found = capabilities.find(operationName); + if (found == capabilities.end()) { + return false; + } + return llvm::any_of(found->second, [&](const auto index) { + const auto& operation = operations[index]; + return (!variadicOnly || + operation.arity().kind() == Operation::Arity::Kind::Variadic) && + operation.arity().accepts(arity) && + operation.numParameters() == numParameters && + operation.siteTuples().empty(); + }); + }; + const auto supportsOnEverySite = [&](GateKind gate) { + return llvm::all_of(siteIds, [&](SiteId site) { + return supportsGate(gate, ArrayRef(&site, 1)); + }); }; std::optional singleQubit; - if (supports(GateKind::U)) { + if (supportsOnEverySite(GateKind::U)) { singleQubit = SingleQubitBasis::U; - } else if (supports(GateKind::X) && supports(GateKind::SX) && - supports(GateKind::RZ)) { + } else if (supportsOnEverySite(GateKind::X) && + supportsOnEverySite(GateKind::SX) && + supportsOnEverySite(GateKind::RZ)) { singleQubit = SingleQubitBasis::ZSXX; - } else if (supports(GateKind::R)) { + } else if (supportsOnEverySite(GateKind::R)) { singleQubit = SingleQubitBasis::R; - } else if (supports(GateKind::RX) && supports(GateKind::RZ)) { + } else if (supportsOnEverySite(GateKind::RX) && + supportsOnEverySite(GateKind::RZ)) { singleQubit = SingleQubitBasis::XZX; - } else if (supports(GateKind::RX) && supports(GateKind::RY)) { + } else if (supportsOnEverySite(GateKind::RX) && + supportsOnEverySite(GateKind::RY)) { singleQubit = SingleQubitBasis::XYX; - } else if (supports(GateKind::RY) && supports(GateKind::RZ)) { + } else if (supportsOnEverySite(GateKind::RY) && + supportsOnEverySite(GateKind::RZ)) { singleQubit = SingleQubitBasis::ZYZ; } + const auto supportsOnEveryCoupling = [&](GateKind gate) { + if (sites.size() < 2) { + return false; + } + if ((gate == GateKind::CX && supportsEveryPlacement("x", 2, 0, true)) || + (gate == GateKind::CZ && supportsEveryPlacement("z", 2, 0, true))) { + return true; + } + const decltype(GATE_SPECIFICATIONS.cbegin()) specification = + std::ranges::find(GATE_SPECIFICATIONS, gate, &GateSpecification::kind); + assert(specification != GATE_SPECIFICATIONS.end() && + "unknown compiler target gate"); + if (supportsEveryPlacement(specification->name, specification->arity, + specification->numParameters)) { + return true; + } + const auto supportsPair = [&](SiteId source, SiteId target) { + const std::array forward{source, target}; + const std::array reverse{target, source}; + return supportsGate(gate, forward) || supportsGate(gate, reverse); + }; + if (connectivityKind == Connectivity::Kind::Explicit) { + return llvm::all_of(couplings, [&](const auto& coupling) { + return supportsPair(coupling.first, coupling.second); + }); + } + for (size_t source = 0; source < siteIds.size(); ++source) { + for (size_t target = source + 1; target < siteIds.size(); ++target) { + if (!supportsPair(siteIds[source], siteIds[target])) { + return false; + } + } + } + return true; + }; + constexpr std::array entanglerPreference{ GateKind::RXX, GateKind::RYY, GateKind::RZX, GateKind::RZZ, GateKind::ISWAP, GateKind::CZ, GateKind::CX, GateKind::ECR, }; - // NOLINTNEXTLINE(readability-qualified-auto) - const auto entangler = - std::ranges::find_if(entanglerPreference, [&](const auto candidate) { - return supports(candidate); - }); + const decltype(entanglerPreference.cbegin()) entangler = + std::ranges::find_if(entanglerPreference, supportsOnEveryCoupling); if (!singleQubit || entangler == entanglerPreference.end()) { return std::nullopt; } @@ -867,14 +907,14 @@ CompilerTarget::createImpl(std::optional name, std::vector sites, Connectivity connectivity, NativeOperations nativeOperations, std::optional durationUnit) { - auto storage = Storage::create( + auto storage = std::make_shared( std::move(name), std::move(sites), connectivity.kind_, std::move(connectivity.couplings_), nativeOperations.kind_, std::move(nativeOperations.operations_), std::move(durationUnit)); - if (!storage) { - return storage.takeError(); + if (auto error = storage->initialize()) { + return std::move(error); } - return CompilerTarget(std::move(*storage)); + return CompilerTarget(std::move(storage)); } CompilerTarget::CompilerTarget(std::shared_ptr storage) @@ -938,8 +978,8 @@ bool CompilerTarget::areAdjacent(size_t source, size_t target) const { void CompilerTarget::forEachNeighbour( size_t vertex, llvm::function_ref callback) const { + assert(vertex < numSites() && "Compiler target vertex is out of range"); if (connectivityKind() == Connectivity::Kind::AllToAll) { - assert(vertex < numSites() && "Compiler target vertex is out of range"); for (size_t neighbour = 0; neighbour < numSites(); ++neighbour) { if (neighbour != vertex) { callback(neighbour); @@ -947,7 +987,7 @@ void CompilerTarget::forEachNeighbour( } return; } - for (const auto neighbour : explicitNeighbours(vertex)) { + for (const auto neighbour : storage_->adjacency[vertex]) { callback(neighbour); } } @@ -961,11 +1001,6 @@ size_t CompilerTarget::distanceBetween(size_t source, size_t target) const { return storage_->distances[(source * numSites()) + target]; } -ArrayRef CompilerTarget::explicitNeighbours(size_t vertex) const { - assert(vertex < numSites() && "Compiler target vertex is out of range"); - return storage_->adjacency[vertex]; -} - size_t CompilerTarget::maxDegree() const noexcept { return storage_->maximumDegree; } @@ -986,7 +1021,24 @@ bool CompilerTarget::supportsOperation( return storage_->supportsOperation(operationName, arity, numParameters); } +bool CompilerTarget::supportsOperation(StringRef operationName, size_t arity, + std::optional numParameters, + ArrayRef sites) const { + return storage_->supportsOperation(operationName, arity, numParameters, + sites); +} + bool CompilerTarget::supports(::mlir::Operation* operation) const { + return supportsImpl(operation, std::nullopt); +} + +bool CompilerTarget::supports(::mlir::Operation* operation, + ArrayRef sites) const { + return supportsImpl(operation, sites); +} + +bool CompilerTarget::supportsImpl(::mlir::Operation* operation, + std::optional> sites) const { if (operation == nullptr) { return false; } @@ -1004,31 +1056,32 @@ bool CompilerTarget::supports(::mlir::Operation* operation) const { if (body.getNumQubits() != controlled.getNumTargets()) { return false; } - if (storage_->supportsVariadicOperation(body.getBaseSymbol(), - controlled.getNumQubits(), - body.getNumParams())) { + if (storage_->supportsOperation(body.getBaseSymbol(), + controlled.getNumQubits(), + body.getNumParams(), sites, + /*variadicOnly=*/true)) { return true; } if (controlled.getNumControls() != 1 || controlled.getNumTargets() != 1) { return false; } if (isa(body.getOperation())) { - return storage_->supportsOperation("cx", 2, 0); + return storage_->supportsOperation("cx", 2, 0, sites); } if (isa(body.getOperation())) { - return storage_->supportsOperation("cz", 2, 0); + return storage_->supportsOperation("cz", 2, 0, sites); } return false; } return storage_->supportsOperation(unitary.getBaseSymbol(), unitary.getNumQubits(), - unitary.getNumParams()); + unitary.getNumParams(), sites); } if (isa(operation)) { - return storage_->supportsOperation("measure", 1, 0); + return storage_->supportsOperation("measure", 1, 0, sites); } if (isa(operation)) { - return storage_->supportsOperation("reset", 1, 0); + return storage_->supportsOperation("reset", 1, 0, sites); } return false; } @@ -1037,6 +1090,10 @@ bool CompilerTarget::supports(GateKind gate) const { return llvm::is_contained(storage_->supportedGates, gate); } +bool CompilerTarget::supports(GateKind gate, ArrayRef sites) const { + return storage_->supportsGate(gate, sites); +} + ArrayRef CompilerTarget::supportedGates() const noexcept { return storage_->supportedGates; } diff --git a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp index 556dc5e863..464a295662 100644 --- a/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp +++ b/mlir/lib/Dialect/MQT/IR/MQTDialect.cpp @@ -20,9 +20,9 @@ #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QTensor/IR/QTensorOps.h" +#include #include #include -#include #include #include // IWYU pragma: keep #include @@ -182,19 +182,19 @@ LogicalResult NativeOperationAttr::verify( << "compiler target zero-arity operation cannot contain site tuples"; } - SmallVector> seen; + llvm::SmallDenseSet> seen; seen.reserve(siteTuples.size()); for (const SiteTupleAttr siteTuple : siteTuples) { if (siteTuple.getSites().size() != arity.getValue()) { return emitError() << "compiler target operation site tuple does not match its arity"; } - if (llvm::is_contained(seen, siteTuple.getSites())) { + if (!seen.insert(siteTuple.getSites()).second) { return emitError() << "compiler target operation contains a duplicate site tuple"; } - seen.emplace_back(siteTuple.getSites()); } + return success(); } diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp index 2df7dd8655..273a8ac923 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp @@ -20,17 +20,23 @@ #include "mlir/Dialect/QCO/Utils/Matrix.h" #include "mlir/Dialect/QTensor/IR/QTensorOps.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 #include @@ -38,10 +44,12 @@ #include #include +#include #include #include #include #include +#include namespace mlir::qco { @@ -76,22 +84,10 @@ static bool isWalkableUnitaryShell(Operation* 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); +/// Multi-target control bodies lack a supported operand-to-matrix mapping. +static bool assignTwoQubitOpMatrix(UnitaryOpInterface op, Matrix4x4& matrix) { + return (!isa(op) || op.getNumControls() == 1) && + op.getUnitaryMatrix4x4(matrix); } /// Return the constant matrix when `unitary` is a single-qubit run member. @@ -101,11 +97,7 @@ oneQubitRunMemberMatrix(UnitaryOpInterface unitary) { !isWalkableUnitaryShell(unitary.getOperation())) { return std::nullopt; } - Matrix2x2 matrix; - if (!unitary.getUnitaryMatrix2x2(matrix)) { - return std::nullopt; - } - return matrix; + return unitary.getUnitaryMatrix(); } /// Return the constant matrix when `unitary` is a two-qubit run member. @@ -116,7 +108,7 @@ twoQubitRunMemberMatrix(UnitaryOpInterface unitary) { return std::nullopt; } Matrix4x4 matrix; - if (!assignTwoQubitOpMatrix(unitary.getOperation(), matrix)) { + if (!assignTwoQubitOpMatrix(unitary, matrix)) { return std::nullopt; } return matrix; @@ -271,7 +263,7 @@ static void eraseFusableRun(RewriterBase& rewriter, /// its two-qubit operation count. static bool fuseTwoQubitGateRun(IRRewriter& rewriter, UnitaryOpInterface head, const Matrix4x4& headMatrix, - const CompilerTarget::SynthesisBasis basis) { + CompilerTarget::SynthesisBasis basis) { FusableTwoQubitRun run = scanFusableTwoQubitRun(head, headMatrix); if (run.ops.size() < 2) { return false; @@ -295,9 +287,126 @@ static bool fuseTwoQubitGateRun(IRRewriter& rewriter, UnitaryOpInterface head, return true; } -static bool requiresTargetSynthesis(Operation* operation, - const CompilerTarget& target) { - return !target.supports(operation); +namespace { + +using SiteId = CompilerTarget::SiteId; +using SiteMap = DenseMap; + +} // namespace + +static SmallVector getQubitValues(ValueRange values) { + return llvm::to_vector(llvm::make_filter_range( + values, [](Value value) { return isa(value.getType()); })); +} + +/// Propagate exact sites, rejecting unknown inputs or inconsistent joins. +static LogicalResult propagateSites(ValueRange inputs, ValueRange outputs, + SiteMap& sites) { + auto inputQubits = getQubitValues(inputs); + auto outputQubits = getQubitValues(outputs); + if (inputQubits.size() != outputQubits.size()) { + return failure(); + } + for (auto [input, output] : llvm::zip_equal(inputQubits, outputQubits)) { + auto found = sites.find(input); + if (found == sites.end()) { + return failure(); + } + const auto site = found->second; + const auto [position, inserted] = sites.try_emplace(output, site); + if (!inserted && position->second != site) { + return failure(); + } + } + return success(); +} + +/// Visit each region once. Branches must agree and loop backedges must retain +/// the entry sites; neither rule is implied by all-to-all placement. +static FailureOr collectStaticSites(Operation* root) { + SiteMap sites; + auto result = root->walk([&](Operation* operation, const WalkStage& stage) { + const auto propagate = [&](ValueRange inputs, ValueRange outputs) { + if (succeeded(propagateSites(inputs, outputs, sites))) { + return WalkResult::advance(); + } + operation->emitError("target compilation requires known, consistent " + "static sites across branches and loop backedges"); + return WalkResult::interrupt(); + }; + 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 compilation requires quantum function inputs to be " + "assigned to qco.static target sites"; + return WalkResult::interrupt(); + } + if (isa(operation)) { + operation->emitError() + << "target compilation requires qubits to be assigned to " + "qco.static target sites"; + return WalkResult::interrupt(); + } + if (auto staticOp = dyn_cast(operation)) { + sites.try_emplace(staticOp.getQubit(), staticOp.getIndex()); + } else if (isa(operation)) { + if (propagate(operation->getOperands(), operation->getResults()) + .wasInterrupted()) { + return WalkResult::interrupt(); + } + return WalkResult::skip(); + } else if (isa(operation)) { + if (!stage.isBeforeAllRegions()) { + auto& region = operation->getRegion(stage.getNextRegion() - 1); + auto yielded = region.front().getTerminator()->getOperands(); + if (isa(operation) && + propagate(yielded, region.getArguments()).wasInterrupted()) { + return WalkResult::interrupt(); + } + auto outputs = isa(operation) && stage.isAfterRegion(1) + ? ValueRange(operation->getRegion(0).getArguments()) + : ValueRange(operation->getResults()); + if (propagate(yielded, outputs).wasInterrupted()) { + return WalkResult::interrupt(); + } + } + if (!stage.isAfterAllRegions()) { + auto& region = operation->getRegion(stage.getNextRegion()); + if (!region.hasOneBlock()) { + operation->emitError("target compilation requires single-block " + "structured control flow"); + return WalkResult::interrupt(); + } + auto inputs = + isa(operation) && stage.isBeforeRegion(1) + ? operation->getRegion(0).front().getTerminator()->getOperands() + : operation->getOperands(); + return propagate(inputs, region.getArguments()); + } + } + return WalkResult::advance(); + }); + if (result.wasInterrupted()) { + return failure(); + } + return sites; +} + +/// Collection has validated the inputs of every unitary, reset, and measure. +static SmallVector getOperationSites(Operation* operation, + const SiteMap& sites) { + SmallVector result; + for (Value qubit : getQubitValues(operation->getOperands())) { + result.push_back(sites.at(qubit)); + } + return result; } /// Normalize relative phase effects and discard only the unobservable global @@ -322,66 +431,67 @@ static LogicalResult prepareGlobalPhases(ModuleOp moduleOp, return success(); } -namespace { - -struct SynthesisPlan { - Operation* firstNeed = nullptr; - Operation* matrixUnavailable = nullptr; - SmallVector operations; -}; - -} // namespace - -static SynthesisPlan planTargetSynthesis(Operation* root, - const CompilerTarget& target) { - SynthesisPlan plan; - root->walk([&](Operation* operation) { - auto unitary = dyn_cast(operation); - if (!unitary || !isWalkableUnitaryShell(operation) || - (unitary.getNumQubits() != 1 && unitary.getNumQubits() != 2)) { - return WalkResult::advance(); - } - if (!requiresTargetSynthesis(operation, target)) { - return WalkResult::advance(); - } - if (plan.firstNeed == nullptr) { - plan.firstNeed = operation; - } +static bool isOperandSwapInvariant(UnitaryOpInterface unitary) { + Operation* operation = unitary.getOperation(); + if (isa(operation)) { + return true; + } + auto controlled = dyn_cast(operation); + return controlled && controlled.getNumControls() == 1 && + controlled.getNumTargets() == 1 && + controlled.getNumBodyUnitaries() == 1 && + isa(controlled.getBodyUnitary(0).getOperation()); +} - if (unitary.isSingleQubit()) { - Matrix2x2 matrix; - if (unitary.getUnitaryMatrix2x2(matrix) || - decomposition::canSynthesizeParameterizedUnitary1Q(operation)) { - 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 = operation; - return WalkResult::interrupt(); - }); - return plan; +static void reorderTwoQubitOperation(IRRewriter& rewriter, + UnitaryOpInterface unitary) { + IRMapping mapping; + mapping.map(unitary.getInputQubit(0), unitary.getInputQubit(1)); + mapping.map(unitary.getInputQubit(1), unitary.getInputQubit(0)); + rewriter.setInsertionPoint(unitary); + auto reordered = cast( + rewriter.clone(*unitary.getOperation(), mapping)); + rewriter.replaceOp( + unitary.getOperation(), + ValueRange{reordered.getOutputQubit(1), reordered.getOutputQubit(0)}); } -static void lowerTargetOperation(IRRewriter& rewriter, UnitaryOpInterface op, - const CompilerTarget::SynthesisBasis basis) { +static LogicalResult synthesizeTargetOperation( + IRRewriter& rewriter, UnitaryOpInterface op, const CompilerTarget& target, + const std::optional& basis, + ArrayRef sites) { Operation* const operation = op.getOperation(); + if (target.supports(operation, sites)) { + return success(); + } + if (op.isTwoQubit() && isOperandSwapInvariant(op) && + target.supports(operation, std::array{sites[1], sites[0]})) { + reorderTwoQubitOperation(rewriter, op); + return success(); + } + const auto unsupported = [&](StringRef reason) -> LogicalResult { + return operation->emitError() + << "target-native synthesis cannot lower operation '" + << operation->getName() << "': " << reason; + }; + if (!basis) { + return unsupported("the target has no usable synthesis basis"); + } rewriter.setInsertionPoint(operation); if (op.isSingleQubit()) { Matrix2x2 matrix; if (!op.getUnitaryMatrix2x2(matrix)) { + if (!decomposition::canSynthesizeParameterizedUnitary1Q(operation)) { + return unsupported( + "its unitary matrix is not available at compile time"); + } decomposition::synthesizeParameterizedUnitary1Q(rewriter, operation, - basis.singleQubit); - return; + basis->singleQubit); + return success(); } const auto synthesized = decomposition::synthesizeUnitary1QEuler( rewriter, operation->getLoc(), op.getInputQubit(0), matrix, - /*runSize=*/1, /*hasNonBasisGate=*/true, basis.singleQubit); + /*runSize=*/1, /*hasNonBasisGate=*/true, basis->singleQubit); if (!synthesized) { llvm::reportFatalInternalError( "target single-qubit basis failed to synthesize a unitary matrix"); @@ -389,28 +499,40 @@ static void lowerTargetOperation(IRRewriter& rewriter, UnitaryOpInterface op, decomposition::emitGPhaseIfNeeded(rewriter, operation->getLoc(), synthesized->globalPhase); rewriter.replaceOp(operation, synthesized->qubit); - return; + return success(); } 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); + if (!assignTwoQubitOpMatrix(op, matrix)) { + return unsupported("its unitary matrix is not available at compile time"); + } + const bool reverseEntangler = !target.supports(basis->entangler, sites); + if (reverseEntangler && + !target.supports(basis->entangler, std::array{sites[1], sites[0]})) { + return operation->emitError() + << "no supported synthesis-basis placement is known for its " + "static sites"; } + Value input0 = op.getInputQubit(0); + Value input1 = op.getInputQubit(1); - const auto native = decomposeUnitary2QWeyl(matrix, basis.entangler); + if (reverseEntangler) { + matrix = matrix.reorderForQubits(1, 0); + std::swap(input0, input1); + } + const auto native = decomposeUnitary2QWeyl(matrix, basis->entangler); const auto synthesized = emitUnitary2QWeyl(rewriter, operation->getLoc(), - input0, input1, native, basis); + input0, input1, native, *basis); decomposition::emitGPhaseIfNeeded(rewriter, operation->getLoc(), synthesized.globalPhase); - rewriter.replaceOp(operation, - ValueRange{synthesized.qubit0, synthesized.qubit1}); + if (reverseEntangler) { + rewriter.replaceOp(operation, + ValueRange{synthesized.qubit1, synthesized.qubit0}); + } else { + rewriter.replaceOp(operation, + ValueRange{synthesized.qubit0, synthesized.qubit1}); + } + return success(); } static LogicalResult fuseTwoQubitGates(ModuleOp moduleOp) { @@ -418,24 +540,17 @@ static LogicalResult fuseTwoQubitGates(ModuleOp moduleOp) { .singleQubit = CompilerTarget::SingleQubitBasis::U, .entangler = CompilerTarget::GateKind::CZ}; - SmallVector runHeads; - moduleOp.walk([&](Operation* operation) { - auto unitary = dyn_cast(operation); - const auto matrix = twoQubitRunMemberMatrix(unitary); - if (matrix && !feedsFromSameTwoQubitRun(unitary)) { - runHeads.emplace_back(operation); - } - }); - bool changed = false; IRRewriter rewriter(moduleOp.getContext()); - for (Operation* operation : runHeads) { - auto unitary = cast(operation); - const auto matrix = twoQubitRunMemberMatrix(unitary); - if (matrix) { - changed |= fuseTwoQubitGateRun(rewriter, unitary, *matrix, basis); - } - } + /// A run's successors have already been visited when its head erases them. + moduleOp->walk( + [&](Operation* operation) { + auto unitary = dyn_cast(operation); + const auto matrix = twoQubitRunMemberMatrix(unitary); + if (matrix && !feedsFromSameTwoQubitRun(unitary)) { + changed |= fuseTwoQubitGateRun(rewriter, unitary, *matrix, basis); + } + }); if (!changed) { return success(); } @@ -479,36 +594,36 @@ struct TargetNativeSynthesisPass final return; } ModuleOp moduleOp = getOperation(); - if (failed(prepareGlobalPhases(moduleOp, target))) { - signalPassFailure(); - return; - } - const auto plan = planTargetSynthesis(moduleOp, target); - if (plan.firstNeed == nullptr) { - return; - } const auto targetBasis = target.synthesisBasis(); - if (!targetBasis) { - plan.firstNeed->emitError() - << "target-native synthesis cannot lower operation '" - << plan.firstNeed->getName() - << "': the target has no usable synthesis basis"; + if (failed(prepareGlobalPhases(moduleOp, target))) { signalPassFailure(); return; } - 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"; + auto sites = collectStaticSites(moduleOp); + if (failed(sites)) { signalPassFailure(); return; } IRRewriter rewriter(&getContext()); - for (Operation* operation : plan.operations) { - lowerTargetOperation(rewriter, cast(operation), - *targetBasis); + /// Rewrite users before producers so each unvisited operation retains its + /// original operands and their collected sites. + const auto result = moduleOp->walk( + [&](Operation* operation) { + auto unitary = dyn_cast(operation); + if (!unitary || !isWalkableUnitaryShell(operation) || + (!unitary.isSingleQubit() && !unitary.isTwoQubit())) { + return WalkResult::advance(); + } + return failed(synthesizeTargetOperation( + rewriter, unitary, target, targetBasis, + getOperationSites(operation, *sites))) + ? WalkResult::interrupt() + : WalkResult::advance(); + }); + if (result.wasInterrupted()) { + signalPassFailure(); + return; } if (failed(prepareGlobalPhases(moduleOp, target))) { signalPassFailure(); @@ -527,21 +642,12 @@ struct VerifyTargetConformancePass final protected: void runOnOperation() override { + auto sites = collectStaticSites(getOperation()); + if (failed(sites)) { + signalPassFailure(); + return; + } 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()); @@ -551,12 +657,6 @@ struct VerifyTargetConformancePass final 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; @@ -570,7 +670,8 @@ struct VerifyTargetConformancePass final return WalkResult::advance(); } - if (target.supports(operation)) { + auto operationSites = getOperationSites(operation, *sites); + if (target.supports(operation, operationSites)) { return WalkResult::advance(); } diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index d4f73ae744..5d570ab9b6 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -33,6 +33,7 @@ #include #include +#include #include #include #include @@ -1480,6 +1481,61 @@ gphase(0.5); EXPECT_EQ(globalPhases, 1U); } +TEST_F(CompilerPipelineTest, QCOProgramCompilesForOneWayEntangler) { + constexpr llvm::StringLiteral source = R"qasm(OPENQASM 3.0; +include "stdgates.inc"; +qubit[2] q; +cx q[0], q[1]; +cx q[1], q[0]; +)qasm"; + auto qc = QCProgram::fromQASMString(source.str()); + ASSERT_TRUE(qc); + auto qco = std::move(*qc).intoQCO(); + ASSERT_TRUE(qco); + + using TargetOperation = CompilerTarget::Operation; + using SiteId = CompilerTarget::SiteId; + std::vector operations{ + llvm::cantFail(TargetOperation::create("u", 1, 3)), + llvm::cantFail(TargetOperation::create( + "cx", 2, 0, + {llvm::cantFail(CompilerTarget::SiteTuple::create({0, 1}))}))}; + const auto target = llvm::cantFail(CompilerTarget::create( + 2, CompilerTarget::Connectivity::fromCouplings({{0, 1}}), + CompilerTarget::NativeOperations::fromOperations(operations))); + + ASSERT_TRUE(qco->compileForTarget(target)); + auto compiled = parseRecordedModule(qco->str()); + ASSERT_TRUE(compiled); + EXPECT_TRUE(verify(*compiled).succeeded()); + + llvm::DenseMap sites; + size_t numTwoQubitOperations = 0; + for (Operation& operation : + ::mlir::mqt::getEntryPoint(compiled.get()).getFunctionBody().getOps()) { + if (auto staticOp = dyn_cast(operation)) { + sites.try_emplace(staticOp.getQubit(), staticOp.getIndex()); + continue; + } + auto unitary = dyn_cast(operation); + if (!unitary) { + continue; + } + if (unitary.getNumQubits() == 2) { + ++numTwoQubitOperations; + const llvm::SmallVector orderedSites{ + sites.at(unitary.getInputQubit(0)), + sites.at(unitary.getInputQubit(1))}; + EXPECT_TRUE(target.supports(&operation, orderedSites)); + } + for (const auto [input, output] : + llvm::zip_equal(unitary.getInputQubits(), unitary.getOutputQubits())) { + sites.try_emplace(output, sites.at(input)); + } + } + EXPECT_GT(numTwoQubitOperations, 0U); +} + TEST_F(CompilerPipelineTest, QCOProgramCompilesDynamicRunForSupportedTargets) { constexpr llvm::StringLiteral source = R"mlir(module { func.func @main(%theta: f64 {mqt.input_name = "theta"}) attributes {mqt.entry_point} { diff --git a/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp b/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp index 307ab095fe..778d06a8c8 100644 --- a/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp +++ b/mlir/unittests/Compiler/test_compiler_qdmi_adapter.cpp @@ -21,8 +21,10 @@ #include #include +#include #include #include +#include using mlir::CompilerTarget; @@ -108,12 +110,18 @@ TEST(CompilerQDMIAdapterTest, InfersDDSIMTargetFacts) { CompilerTarget::Operation::Arity::Kind::Variadic) << name.str(); EXPECT_EQ(operation.arity().value(), minimum) << name.str(); + EXPECT_TRUE(operation.siteTuples().empty()) << name.str(); EXPECT_TRUE( target.supportsOperation(name, minimum, operation.numParameters())) << name.str(); EXPECT_TRUE( target.supportsOperation(name, minimum + 4, operation.numParameters())) << name.str(); + std::vector sites(minimum + 4); + std::iota(sites.begin(), sites.end(), 0); + EXPECT_TRUE(target.supportsOperation(name, minimum + 4, + operation.numParameters(), sites)) + << name.str(); } EXPECT_TRUE(target.supportsOperation("gphase", 0, 1)); EXPECT_EQ(target.supportsOperation("h", 1, 0), true); @@ -157,17 +165,50 @@ TEST(CompilerQDMIAdapterTest, SnapshotsHomogeneousHigherArityOperation) { const auto target = llvm::cantFail(mlir::compilerTargetFromDevice(device)); EXPECT_TRUE(target.supportsOperation("ccnot", 3, 0)); + EXPECT_TRUE(target.supportsOperation("ccnot", 3, 0, {0, 1, 2})); + EXPECT_TRUE(target.supportsOperation("ccnot", 3, 0, {2, 1, 0})); + EXPECT_FALSE(target.supportsOperation("ccnot", 3, 0, {0, 1, 3})); } -TEST(CompilerQDMIAdapterTest, RejectsDirectionalOperationWithoutReverseSites) { +TEST(CompilerQDMIAdapterTest, PreservesOneWayDirectionalOperationSupport) { qdmi::DeviceSessionConfig overrides; overrides.deviceConfiguration = qdmi::FileDeviceConfiguration{ MQT_CORE_MLIR_DIRECTIONAL_ONE_WAY_SC_CONFIG}; const auto device = qdmi::Session::openDevice("mqt.sc.default", overrides); - auto target = mlir::compilerTargetFromDevice(device); - ASSERT_FALSE(target); - const auto message = llvm::toString(target.takeError()); - EXPECT_NE(message.find("both orientations"), std::string::npos); + const auto target = llvm::cantFail(mlir::compilerTargetFromDevice(device)); + + ASSERT_EQ(target.couplings().size(), 1U); + const auto& cx = findOperation(target, "cx"); + ASSERT_EQ(cx.siteTuples().size(), 1U); + EXPECT_EQ(cx.siteTuples()[0].sites(), + (llvm::ArrayRef{0, 1})); + EXPECT_FALSE(cx.siteTuples()[0].duration()); + EXPECT_FALSE(cx.siteTuples()[0].fidelity()); + EXPECT_TRUE(target.supportsOperation("cx", 2, 0, {0, 1})); + EXPECT_FALSE(target.supportsOperation("cx", 2, 0, {1, 0})); + ASSERT_TRUE(target.synthesisBasis()); + EXPECT_EQ(target.synthesisBasis()->entangler, CompilerTarget::GateKind::CX); +} + +TEST(CompilerQDMIAdapterTest, OmitsOperationsWithNoSupportedPlacements) { + qdmi::DeviceSessionConfig overrides; + overrides.deviceConfiguration = qdmi::InlineDeviceConfiguration{.json = R"({ + "schema-version": 1, + "name": "Unavailable operation", + "numQubits": 1, + "durationUnit": {"unit": "ns", "scaleFactor": 1}, + "qubitProperties": {"defaults": {}, "overrides": []}, + "couplings": [], + "operations": [ + {"name": "x", "numQubits": 1, "numParameters": 0, "sites": []} + ] + })"}; + const auto device = qdmi::Session::openDevice("mqt.sc.default", overrides); + const auto target = llvm::cantFail(mlir::compilerTargetFromDevice(device)); + EXPECT_EQ(target.nativeOperationsKind(), + CompilerTarget::NativeOperations::Kind::Explicit); + EXPECT_TRUE(target.operations().empty()); + EXPECT_FALSE(target.supportsOperation("x", 1, 0, {0})); } TEST(CompilerQDMIAdapterTest, @@ -180,6 +221,8 @@ TEST(CompilerQDMIAdapterTest, ASSERT_EQ(target.couplings().size(), 1); const auto& cx = findOperation(target, "cx"); + EXPECT_TRUE(target.supportsOperation("cx", 2, 0, {0, 1})); + EXPECT_TRUE(target.supportsOperation("cx", 2, 0, {1, 0})); ASSERT_EQ(cx.siteTuples().size(), 2); EXPECT_EQ(cx.siteTuples()[0].sites(), (llvm::ArrayRef{0, 1})); diff --git a/mlir/unittests/Compiler/test_compiler_target.cpp b/mlir/unittests/Compiler/test_compiler_target.cpp index 2955d69e0e..46eb413622 100644 --- a/mlir/unittests/Compiler/test_compiler_target.cpp +++ b/mlir/unittests/Compiler/test_compiler_target.cpp @@ -71,18 +71,19 @@ TEST(CompilerTargetTest, ConstructsDetailedNamedTargetAndSharesStorage) { std::vector operations; std::vector siteTuples{valid(SiteTuple::create({7}, 0, 0.99)), - valid(SiteTuple::create({2}, 5, 0.98))}; + valid(SiteTuple::create({2}, 5, 0.98)), + valid(SiteTuple::create({11}))}; operations.emplace_back( valid(Operation::create(" PRX ", 1, 2, std::move(siteTuples), 0, 0.97))); - const auto target = valid( + auto target = valid( Target::create("device", std::move(sites), Connectivity::fromCouplings({{11, 2}, {2, 11}, {7, 2}}), NativeOperations::fromOperations(operations), valid(DurationUnit::create("ns", 0.5)))); - // The copy itself is the behavior under test: both objects must share the - // immutable backing storage. - // NOLINTNEXTLINE(performance-unnecessary-copy-initialization) + /// The copy itself is the behavior under test: both objects must share the + /// immutable backing storage. + /// NOLINTNEXTLINE(performance-unnecessary-copy-initialization) const auto copy = target; ASSERT_TRUE(target.name()); @@ -102,13 +103,20 @@ TEST(CompilerTargetTest, ConstructsDetailedNamedTargetAndSharesStorage) { 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].siteTuples().size(), 2); + ASSERT_EQ(target.operations()[0].siteTuples().size(), 3); 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()); EXPECT_EQ(copy.operations().data(), target.operations().data()); + + operations.clear(); + target = valid(Target::create(1, Connectivity::allToAll(), + NativeOperations::unrestricted())); + EXPECT_TRUE(copy.supportsOperation("r", 1, 2, {7})); + EXPECT_TRUE(copy.supportsOperation("r", 1, 2, {2})); + EXPECT_TRUE(copy.supportsOperation("r", 1, 2, {11})); } TEST(CompilerTargetTest, ConstructsDenseUnnamedAllToAllTarget) { @@ -180,6 +188,8 @@ TEST(CompilerTargetTest, PreservesFullNonnegativeSiteIdDomain) { (llvm::ArrayRef{{nextSite, maxSite}})); EXPECT_EQ(target.operations().front().siteTuples().front().sites(), (llvm::ArrayRef{maxSite, nextSite})); + EXPECT_TRUE(target.supportsOperation("cx", 2, 0, {maxSite, nextSite})); + EXPECT_FALSE(target.supportsOperation("cx", 2, 0, {nextSite, maxSite})); } TEST(CompilerTargetTest, CanonicalizesConnectedTopologyAndCachesDistances) { @@ -368,8 +378,11 @@ TEST(CompilerTargetTest, DistinguishesOperationSupport) { TEST(CompilerTargetTest, PreservesCalibrationAndResolvesHomogeneousBasis) { const std::vector chain{{0, 1}, {1, 2}}; const auto globalU = valid(Operation::create("U3", 1, 3)); - const auto cz = valid(Operation::create( - "cz", 2, 0, std::vector{valid(SiteTuple::create({1, 0}, 5, 0.99))})); + const auto cz = valid( + Operation::create("cz", 2, 0, + std::vector{valid(SiteTuple::create({1, 0}, 5, 0.99)), + valid(SiteTuple::create({1, 2}))}, + 7, 0.98)); const auto target = valid(Target::create(3, Connectivity::fromCouplings(chain), NativeOperations::fromOperations({globalU, cz}), @@ -380,11 +393,17 @@ TEST(CompilerTargetTest, PreservesCalibrationAndResolvesHomogeneousBasis) { EXPECT_EQ(target.supports(GateKind::CZ), true); EXPECT_TRUE(llvm::is_contained(target.supportedGates(), GateKind::CZ)); ASSERT_EQ(target.operations().size(), 2U); - ASSERT_EQ(target.operations()[1].siteTuples().size(), 1U); + ASSERT_EQ(target.operations()[1].siteTuples().size(), 2U); 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); + EXPECT_FALSE(target.operations()[1].siteTuples()[1].duration()); + EXPECT_FALSE(target.operations()[1].siteTuples()[1].fidelity()); + EXPECT_EQ(target.operations()[1].duration(), 7); + EXPECT_EQ(target.operations()[1].fidelity(), 0.98); + EXPECT_TRUE(target.supports(GateKind::CZ, {1, 2})); + EXPECT_FALSE(target.supports(GateKind::CZ, {2, 1})); ASSERT_TRUE(target.synthesisBasis()); EXPECT_EQ(target.synthesisBasis()->singleQubit, Target::SingleQubitBasis::U); EXPECT_EQ(target.synthesisBasis()->entangler, GateKind::CZ); @@ -417,6 +436,8 @@ TEST(CompilerTargetTest, RoundTripsTypedCompilationTargetAttribute) { EXPECT_EQ(reconstructed.materialize(context), attribute); EXPECT_EQ(reconstructed.couplings(), target.couplings()); EXPECT_EQ(reconstructed.supportsOperation("r", 1, 2), true); + EXPECT_TRUE(reconstructed.supportsOperation("r", 1, 2, {7})); + EXPECT_FALSE(reconstructed.supportsOperation("r", 1, 2, {11})); EXPECT_EQ(reconstructed.supportsOperation("gphase", 0, 1), true); EXPECT_EQ(reconstructed.supportsOperation("h", 3, 0), true); EXPECT_EQ(reconstructed.operations()[1].arity(), Arity::fixed(0)); @@ -424,6 +445,31 @@ TEST(CompilerTargetTest, RoundTripsTypedCompilationTargetAttribute) { EXPECT_EQ(reconstructed.synthesisBasis(), target.synthesisBasis()); } +TEST(CompilerTargetTest, SupportsMaximumSiteIds) { + constexpr auto maxSite = std::numeric_limits::max(); + constexpr auto nextSite = maxSite - 1; + std::vector sites{valid(Site::create(nextSite)), + valid(Site::create(maxSite))}; + const auto x = valid( + Operation::create("x", 1, 0, + std::vector{valid(SiteTuple::create({nextSite})), + valid(SiteTuple::create({maxSite}))})); + const auto cx = valid(Operation::create( + "cx", 2, 0, std::vector{valid(SiteTuple::create({nextSite, maxSite}))})); + const auto target = + valid(Target::create(std::move(sites), Connectivity::allToAll(), + NativeOperations::fromOperations({x, cx}))); + + EXPECT_TRUE(target.supports(GateKind::X, {nextSite})); + EXPECT_TRUE(target.supports(GateKind::X, {maxSite})); + EXPECT_TRUE(target.supports(GateKind::CX, {nextSite, maxSite})); + + mlir::MLIRContext context; + context.loadDialect(); + const auto attribute = target.materialize(context); + EXPECT_EQ(valid(Target::create(attribute)).materialize(context), attribute); +} + TEST(CompilerTargetTest, RoundTripsSupportedTargetStates) { mlir::MLIRContext context; context.loadDialect(); @@ -455,6 +501,50 @@ TEST(CompilerTargetTest, RoundTripsSupportedTargetStates) { "Compiler target topology must be connected"); } +TEST(CompilerTargetTest, EnforcesExactOrderedOperationApplicability) { + std::vector sites{valid(Site::create(10)), valid(Site::create(20)), + valid(Site::create(30))}; + const auto globalU = valid(Operation::create("u", 1, 3)); + const auto restrictedX = valid(Operation::create( + "x", 1, 0, std::vector{valid(SiteTuple::create({10}))})); + const auto directionalCX = + valid(Operation::create("cx", 2, 0, + std::vector{valid(SiteTuple::create({10, 20})), + valid(SiteTuple::create({20, 30}))})); + const auto exactCZ = valid(Operation::create( + "cz", 2, 0, std::vector{valid(SiteTuple::create({10, 20}))})); + const auto threeQubit = valid( + Operation::create("device.operation", 3, 0, + std::vector{valid(SiteTuple::create({10, 20, 30}))})); + const auto target = valid(Target::create( + std::move(sites), Connectivity::fromCouplings({{10, 20}, {20, 30}}), + NativeOperations::fromOperations( + {globalU, restrictedX, directionalCX, exactCZ, threeQubit}))); + + EXPECT_TRUE(globalU.siteTuples().empty()); + EXPECT_FALSE(restrictedX.siteTuples().empty()); + EXPECT_FALSE(directionalCX.siteTuples().empty()); + + EXPECT_TRUE(target.supports(GateKind::U, {30})); + EXPECT_TRUE(target.supports(GateKind::X, {10})); + EXPECT_FALSE(target.supports(GateKind::X, {20})); + EXPECT_TRUE(target.supports(GateKind::CX, {10, 20})); + EXPECT_FALSE(target.supports(GateKind::CX, {20, 10})); + EXPECT_TRUE(target.supports(GateKind::CX, {20, 30})); + EXPECT_FALSE(target.supports(GateKind::CX, {30, 20})); + EXPECT_FALSE(target.supports(GateKind::CX, {10, 30})); + EXPECT_TRUE(target.supports(GateKind::CZ, {10, 20})); + EXPECT_FALSE(target.supports(GateKind::CZ, {20, 10})); + EXPECT_FALSE(target.supports(GateKind::ECR)); + EXPECT_FALSE(target.supports(GateKind::ECR, {10, 20})); + EXPECT_FALSE(target.supports(GateKind::CX, {10})); + EXPECT_FALSE(target.supports(GateKind::CX, {10, 10})); + EXPECT_FALSE(target.supports(GateKind::CX, {10, 40})); + EXPECT_TRUE(target.supportsOperation("device.operation", 3, 0, {10, 20, 30})); + EXPECT_FALSE( + target.supportsOperation("device.operation", 3, 0, {30, 20, 10})); +} + TEST(CompilerTargetTest, ClassifiesEveryEntangler) { using Entangler = std::tuple; const std::array entanglers{Entangler{GateKind::CZ, "cz", 0}, @@ -506,6 +596,19 @@ TEST(CompilerTargetTest, DerivesControlledEntanglersFromVariadicBases) { } } +TEST(CompilerTargetTest, ResolvesLargeAllToAllVariadicBasis) { + constexpr size_t numSites = 65'535; + const auto target = valid(Target::create( + numSites, Connectivity::allToAll(), + NativeOperations::fromOperations( + {valid(Operation::create("u", 1, 3)), + valid(Operation::create("x", Arity::variadic(1), 0))}))); + + ASSERT_TRUE(target.synthesisBasis()); + EXPECT_EQ(target.synthesisBasis()->singleQubit, Target::SingleQubitBasis::U); + EXPECT_EQ(target.synthesisBasis()->entangler, GateKind::CX); +} + TEST(CompilerTargetTest, SupportsRealQCOOperationsAndStructuralOps) { mlir::DialectRegistry registry; registry.insertgetOperation(), {10})); const auto closed = valid(Target::create( 2, Connectivity::allToAll(), NativeOperations::fromOperations({}))); diff --git a/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp b/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp index 9af028efe4..07aa36b0f9 100644 --- a/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp +++ b/mlir/unittests/Dialect/MQT/IR/test_mqt_ir.cpp @@ -34,6 +34,7 @@ #include #include +#include #include #include @@ -143,7 +144,8 @@ TEST_F(MQTIRTest, RoundTripsTypedCompilationTarget) { operations = [ , - num_parameters = 0, site_tuples = []>, + num_parameters = 0, + site_tuples = [<[4, 7], fidelity = 9.900000e-01 : f64>]>, , num_parameters = 1, site_tuples = []>, @@ -165,12 +167,46 @@ TEST_F(MQTIRTest, RoundTripsTypedCompilationTarget) { EXPECT_EQ(compilationTarget.getOperations()[1].getArity().getValue(), 0U); EXPECT_EQ(compilationTarget.getOperations()[2].getArity().getKind(), mqt::OperationArityKind::Variadic); - EXPECT_TRUE( - compilationTarget.getOperations().front().getSiteTuples().empty()); + ASSERT_EQ(compilationTarget.getOperations()[0].getSiteTuples().size(), 1U); + const auto tuple = compilationTarget.getOperations()[0].getSiteTuples()[0]; + EXPECT_EQ(tuple.getSites(), (ArrayRef{4, 7})); + EXPECT_EQ(tuple.getFidelity().getValueAsDouble(), 0.99); + EXPECT_TRUE(compilationTarget.getOperations()[1].getSiteTuples().empty()); + EXPECT_TRUE(compilationTarget.getOperations()[2].getSiteTuples().empty()); EXPECT_EQ(roundTrip(compilationTarget), compilationTarget); } +TEST_F(MQTIRTest, RoundTripsMaximumSiteIds) { + const auto compilationTarget = parseAttr(R"mlir(#mqt.compilation_target< + sites = [, ], + connectivity = all_to_all, couplings = [], + native_operations = explicit, + operations = [, + num_parameters = 0, + site_tuples = [<[9223372036854775806, + 9223372036854775807]>]>]>)mlir"); + ASSERT_TRUE(compilationTarget); + EXPECT_EQ(roundTrip(compilationTarget), compilationTarget); +} + +TEST_F(MQTIRTest, RoundTripsSiteTupleCalibration) { + const auto durationOnly = dyn_cast_if_present( + parseAttr(R"mlir(#mqt.site_tuple<[4, 7], duration = 0>)mlir")); + ASSERT_TRUE(durationOnly); + EXPECT_EQ(durationOnly.getDuration(), 0U); + EXPECT_FALSE(durationOnly.getFidelity()); + EXPECT_EQ(roundTrip(durationOnly), durationOnly); + + const auto calibrated = dyn_cast_if_present(parseAttr( + R"mlir(#mqt.site_tuple<[4, 7], fidelity = 9.900000e-01 : f64, duration = 40>)mlir")); + ASSERT_TRUE(calibrated); + EXPECT_EQ(calibrated.getDuration(), 40U); + EXPECT_EQ(calibrated.getFidelity().getValueAsDouble(), 0.99); + EXPECT_EQ(roundTrip(calibrated), calibrated); +} + TEST_F(MQTIRTest, RepresentsUnrestrictedTargetFacts) { const auto unrestricted = dyn_cast_if_present( parseAttr(R"mlir(#mqt.compilation_target< @@ -195,10 +231,10 @@ TEST_F(MQTIRTest, RejectsInvalidTargetLeaves) { EXPECT_FALSE(parseAttr(R"mlir(#mqt.site)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.site)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.coupling)mlir")); - EXPECT_FALSE(parseAttr(R"mlir(#mqt.site_tuple)mlir")); - EXPECT_FALSE( - parseAttr(R"mlir(#mqt.site_tuple)mlir")); - EXPECT_FALSE(parseAttr(R"mlir(#mqt.site_tuple)mlir")); + EXPECT_FALSE(parseAttr(R"mlir(#mqt.site_tuple<[-1]>)mlir")); + EXPECT_FALSE(parseAttr(R"mlir(#mqt.site_tuple<[0], duration =>)mlir")); + EXPECT_FALSE(parseAttr(R"mlir(#mqt.site_tuple<[0], fidelity = 1.100000e+00 : f64>)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.operation_arity< kind = variadic, value = 0>)mlir")); @@ -207,20 +243,20 @@ TEST_F(MQTIRTest, RejectsInvalidTargetLeaves) { num_parameters = 0, site_tuples = []>)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.native_operation, - num_parameters = 1, site_tuples = []>)mlir")); + num_parameters = 1, site_tuples = [<[0]>]>)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.native_operation, - num_parameters = 0, site_tuples = []>)mlir")); + num_parameters = 0, site_tuples = [<[0]>]>)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.native_operation, - num_parameters = 0, site_tuples = []>)mlir")); + num_parameters = 0, site_tuples = [<[0]>]>)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.native_operation, num_parameters = 0, - site_tuples = [, ]>)mlir")); + site_tuples = [<[0]>, <[0]>]>)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.native_operation, - num_parameters = 0, site_tuples = [], duration =>)mlir")); + num_parameters = 0, site_tuples = [], duration =>>)mlir")); } TEST_F(MQTIRTest, RejectsInvalidCompilationTargets) { @@ -259,7 +295,7 @@ TEST_F(MQTIRTest, RejectsInvalidCompilationTargets) { native_operations = explicit, operations = [, - num_parameters = 0, site_tuples = []>]>)mlir")); + num_parameters = 0, site_tuples = [<[1]>]>]>)mlir")); EXPECT_FALSE(parseAttr(R"mlir(#mqt.compilation_target< sites = [], connectivity = all_to_all, couplings = [], native_operations = explicit, 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 28d3e660a4..b33e90cac0 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp @@ -15,6 +15,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/QCOUtils.h" +#include "mlir/Dialect/QCO/Transforms/Mapping/Mapping.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/QCO/Utils/DDFunctionality.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" @@ -26,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -61,6 +64,7 @@ using Connectivity = Target::Connectivity; using NativeOperations = Target::NativeOperations; using Operation = Target::Operation; using Site = Target::Site; +using SiteTuple = Target::SiteTuple; using mlir::ModuleOp; using mlir::OwningOpRef; using mlir::Value; @@ -165,6 +169,23 @@ makeUCxTarget(std::optional> sites = std::nullopt) { NativeOperations::fromOperations(operations))); } +[[nodiscard]] static Target +makeOneWayUCxTarget(Connectivity connectivity = Connectivity::allToAll()) { + std::vector operations{ + valid(Operation::create("u", 1, 3)), + valid(Operation::create("cx", 2, 0, {valid(SiteTuple::create({1, 0}))})), + valid(Operation::create("gphase", 0, 1))}; + return valid(Target::create(2, std::move(connectivity), + NativeOperations::fromOperations(operations))); +} + +[[nodiscard]] static Target makeOneWayRxxTarget() { + std::vector operations{valid( + Operation::create("rxx", 2, 1, {valid(SiteTuple::create({1, 0}))}))}; + return valid(Target::create(2, Connectivity::allToAll(), + NativeOperations::fromOperations(operations))); +} + [[nodiscard]] static mlir::DenseElementsAttr denseMatrix(QCOProgramBuilder& builder, const int64_t dimension, const llvm::ArrayRef> values) { @@ -207,7 +228,8 @@ class TargetSynthesisTest : public testing::Test { void SetUp() override { mlir::DialectRegistry registry; registry.insert(); + mlir::qco::QCODialect, mlir::qtensor::QTensorDialect, + mlir::scf::SCFDialect>(); context = std::make_unique(); context->appendDialectRegistry(registry); context->loadAllAvailableDialects(); @@ -306,6 +328,26 @@ TEST_F(TargetSynthesisTest, expectEquivalent(expected, optimized); } +TEST_F(TargetSynthesisTest, TwoQubitGateFusionExposesEarlierRunContinuations) { + const auto adjacentRuns = [](QCOProgramBuilder& builder) { + auto q0 = builder.staticQubit(0); + auto q1 = builder.staticQubit(1); + auto q2 = builder.staticQubit(2); + std::tie(q0, q1) = builder.cx(q0, q1); + std::tie(q0, q2) = builder.cx(q0, q2); + std::tie(q0, q2) = builder.cx(q0, q2); + std::tie(q0, q1) = builder.cx(q0, q1); + return builder.intConstant(0); + }; + auto expected = build(adjacentRuns); + auto optimized = build(adjacentRuns); + + ASSERT_TRUE(mlir::succeeded( + runPass(*optimized, mlir::qco::createFuseTwoQubitGates()))); + EXPECT_EQ(countOps(*optimized), 0U); + expectEquivalent(expected, optimized); +} + TEST_F(TargetSynthesisTest, TwoQubitGateFusionEmitsSymmetricEntangler) { const auto reducible = [](QCOProgramBuilder& builder) { auto q0 = builder.staticQubit(0); @@ -384,6 +426,399 @@ TEST_F(TargetSynthesisTest, TargetNativeSynthesisRemovesOrdinarySwap) { expectEquivalent(expected, synthesized); } +TEST_F(TargetSynthesisTest, + TargetNativeSynthesisKeepsSupportedEntanglerDirection) { + auto module = build([](QCOProgramBuilder& builder) { + auto q0 = builder.staticQubit(0); + auto q1 = builder.staticQubit(1); + std::tie(q1, q0) = builder.cx(q1, q0); + return builder.intConstant(0); + }); + const auto target = makeOneWayUCxTarget(); + const auto before = printModule(*module); + + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createTargetNativeSynthesis(target)))); + EXPECT_EQ(printModule(*module), before); + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createVerifyTargetConformance(target)))); +} + +TEST_F(TargetSynthesisTest, + TargetNativeSynthesisReversesEntanglerWithoutChangingSemantics) { + const auto circuit = [](QCOProgramBuilder& builder) { + auto q0 = builder.staticQubit(0); + auto q1 = builder.staticQubit(1); + q0 = builder.h(q0); + std::tie(q0, q1) = builder.cx(q0, q1); + q1 = builder.h(q1); + return builder.intConstant(0); + }; + auto expected = build(circuit); + auto synthesized = build(circuit); + const auto before = printModule(*synthesized); + const auto target = makeOneWayUCxTarget(); + + ASSERT_TRUE(mlir::succeeded( + runPass(*synthesized, mlir::qco::createTargetNativeSynthesis(target)))); + EXPECT_NE(printModule(*synthesized), before); + ASSERT_TRUE(mlir::succeeded( + runPass(*synthesized, mlir::qco::createVerifyTargetConformance(target)))); + ASSERT_TRUE(mlir::succeeded(mlir::verify(*synthesized))); + expectEquivalent(expected, synthesized); +} + +TEST_F(TargetSynthesisTest, MappingLeavesDirectionRepairToSynthesis) { + auto moduleOp = build([](QCOProgramBuilder& builder) { + auto q0 = builder.allocQubit(); + auto q1 = builder.allocQubit(); + std::tie(q0, q1) = builder.cx(q0, q1); + std::tie(q1, q0) = builder.cx(q1, q0); + return builder.intConstant(0); + }); + const auto target = + makeOneWayUCxTarget(Connectivity::fromCouplings({{0, 1}})); + ASSERT_TRUE(mlir::succeeded( + runPass(*moduleOp, + mlir::qco::createMappingPass( + target, mlir::qco::MappingPassOptions{ + .niterations = 1, .ntrials = 1, .seed = 42})))); + EXPECT_EQ(countOps(*moduleOp), 0U); + auto expected = + mlir::OwningOpRef(mlir::cast(moduleOp->clone())); + ASSERT_TRUE(mlir::succeeded( + runPass(*moduleOp, mlir::qco::createTargetNativeSynthesis(target)))); + EXPECT_EQ(countOps(*moduleOp), 2U); + ASSERT_TRUE(mlir::succeeded( + runPass(*moduleOp, mlir::qco::createVerifyTargetConformance(target)))); + expectEquivalent(expected, moduleOp); +} + +TEST_F(TargetSynthesisTest, RejectsUnknownSitesWithoutWideningNativeSupport) { + auto moduleOp = mlir::parseSourceString(R"mlir( + module { + func.func @main() { + %q0 = qco.static 0 : !qco.qubit + %q1 = qco.static 1 : !qco.qubit + %r = scf.execute_region -> !qco.qubit { + scf.yield %q0 : !qco.qubit + } + %c, %t = qco.ctrl(%r) targets(%a = %q1) { + %x = qco.x %a : !qco.qubit -> !qco.qubit + qco.yield %x : !qco.qubit + } : ({!qco.qubit}, {!qco.qubit}) -> ({!qco.qubit}, {!qco.qubit}) + qco.sink %c : !qco.qubit + qco.sink %t : !qco.qubit + return + } + } + )mlir", + context.get()); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(mlir::succeeded(mlir::verify(*moduleOp))); + ASSERT_TRUE(mlir::succeeded(mlir::qco::verifyLinearity(*moduleOp))); + const auto target = makeOneWayUCxTarget(); + for (auto pass : {false, true}) { + const auto diagnostics = expectFailure( + *moduleOp, pass ? mlir::qco::createTargetNativeSynthesis(target) + : mlir::qco::createVerifyTargetConformance(target)); + EXPECT_NE(diagnostics.find("static sites"), std::string::npos); + } +} + +TEST_F(TargetSynthesisTest, ConformanceRejectsUnsupportedEntanglerDirection) { + auto module = build([](QCOProgramBuilder& builder) { + auto q0 = builder.staticQubit(0); + auto q1 = builder.staticQubit(1); + std::tie(q0, q1) = builder.cx(q0, q1); + return builder.intConstant(0); + }); + const auto diagnostics = expectFailure( + *module, mlir::qco::createVerifyTargetConformance(makeOneWayUCxTarget())); + EXPECT_NE(diagnostics.find("target does not support operation 'qco.ctrl'"), + std::string::npos) + << diagnostics; +} + +TEST_F(TargetSynthesisTest, + TargetNativeSynthesisTracksSitesThroughStructuredControlFlow) { + auto module = build([](QCOProgramBuilder& builder) { + auto q0 = builder.staticQubit(0); + auto q1 = builder.staticQubit(1); + const auto outputs = + builder.qcoIf(true, ValueRange{q0, q1}, [&](ValueRange arguments) { + auto first = arguments[0]; + auto second = arguments[1]; + std::tie(first, second) = builder.cx(first, second); + return mlir::SmallVector{first, second}; + }); + for (Value output : outputs) { + builder.sink(output); + } + return builder.intConstant(0); + }); + const auto target = makeOneWayUCxTarget(); + + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createTargetNativeSynthesis(target)))); + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createVerifyTargetConformance(target)))); + ASSERT_TRUE(mlir::succeeded(mlir::verify(*module))); +} + +TEST_F(TargetSynthesisTest, + TargetNativeSynthesisTracksSitesThroughAllStructuredOperations) { + auto module = mlir::parseSourceString(R"mlir( + module { + func.func @main() { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %false = arith.constant false + %q0 = qco.static 0 : !qco.qubit + %q1 = qco.static 1 : !qco.qubit + %f0, %f1 = scf.for %i = %c0 to %c1 step %c1 + iter_args(%a = %q0, %b = %q1) + -> (!qco.qubit, !qco.qubit) { + %h0 = qco.h %a : !qco.qubit -> !qco.qubit + %h1 = qco.h %b : !qco.qubit -> !qco.qubit + scf.yield %h0, %h1 : !qco.qubit, !qco.qubit + } + %w0, %w1 = scf.while (%a = %f0, %b = %f1) + : (!qco.qubit, !qco.qubit) -> (!qco.qubit, !qco.qubit) { + %h0 = qco.h %a : !qco.qubit -> !qco.qubit + %h1 = qco.h %b : !qco.qubit -> !qco.qubit + scf.condition(%false) %h0, %h1 : !qco.qubit, !qco.qubit + } do { + ^bb0(%a: !qco.qubit, %b: !qco.qubit): + %h0 = qco.h %a : !qco.qubit -> !qco.qubit + %h1 = qco.h %b : !qco.qubit -> !qco.qubit + scf.yield %h0, %h1 : !qco.qubit, !qco.qubit + } + %i0, %i1 = qco.index_switch %c0 -> (!qco.qubit, !qco.qubit) + case 0 args(%a = %w0, %b = %w1) { + %h0 = qco.h %a : !qco.qubit -> !qco.qubit + %h1 = qco.h %b : !qco.qubit -> !qco.qubit + qco.yield %h0, %h1 : !qco.qubit, !qco.qubit + } + default args(%a = %w0, %b = %w1) { + qco.yield %a, %b : !qco.qubit, !qco.qubit + } + qco.sink %i0 : !qco.qubit + qco.sink %i1 : !qco.qubit + return + } + } + )mlir", + context.get()); + ASSERT_TRUE(module); + const auto target = makeOneWayUCxTarget(); + + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createTargetNativeSynthesis(target)))); + EXPECT_EQ(countOps(*module), 0U); + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createVerifyTargetConformance(target)))); + ASSERT_TRUE(mlir::succeeded(mlir::verify(*module))); +} + +TEST_F(TargetSynthesisTest, + TargetNativeSynthesisRejectsAmbiguousSingleQubitSite) { + auto module = build([](QCOProgramBuilder& builder) { + auto q0 = builder.staticQubit(0); + auto q1 = builder.staticQubit(1); + const auto outputs = builder.qcoIf( + true, ValueRange{q0, q1}, + [](ValueRange arguments) { + return mlir::SmallVector{arguments[0], arguments[1]}; + }, + [](ValueRange arguments) { + return mlir::SmallVector{arguments[1], arguments[0]}; + }); + auto h = builder.h(outputs[0]); + builder.sink(h); + builder.sink(outputs[1]); + return builder.intConstant(0); + }); + const auto target = makeOneWayUCxTarget(); + + const auto diagnostics = + expectFailure(*module, mlir::qco::createTargetNativeSynthesis(target)); + EXPECT_NE(diagnostics.find("consistent static sites"), std::string::npos); +} + +TEST_F(TargetSynthesisTest, TargetNativeSynthesisRejectsAmbiguousBranchSites) { + auto module = mlir::parseSourceString(R"mlir( + module { + func.func @main() { + %true = arith.constant true + %q0 = qco.static 0 : !qco.qubit + %q1 = qco.static 1 : !qco.qubit + %r0, %r1 = qco.if %true args(%a = %q0, %b = %q1) + -> (!qco.qubit, !qco.qubit) { + qco.yield %a, %b : !qco.qubit, !qco.qubit + } else args(%a = %q0, %b = %q1) { + qco.yield %b, %a : !qco.qubit, !qco.qubit + } + %c, %t = qco.ctrl(%r0) targets(%arg = %r1) { + %x = qco.x %arg : !qco.qubit -> !qco.qubit + qco.yield %x : !qco.qubit + } : ({!qco.qubit}, {!qco.qubit}) + -> ({!qco.qubit}, {!qco.qubit}) + qco.sink %c : !qco.qubit + qco.sink %t : !qco.qubit + return + } + } + )mlir", + context.get()); + ASSERT_TRUE(module); + + const auto diagnostics = expectFailure( + *module, mlir::qco::createTargetNativeSynthesis(makeOneWayUCxTarget())); + EXPECT_NE(diagnostics.find("consistent static sites"), std::string::npos) + << diagnostics; +} + +TEST_F(TargetSynthesisTest, RejectsLoopCarriedSitePermutations) { + constexpr std::array loops{ + R"mlir( + %r0, %r1 = scf.for %i = %c0 to %n step %c1 + iter_args(%a = %q0, %b = %q1) -> (!qco.qubit, !qco.qubit) { + scf.yield %b, %a : !qco.qubit, !qco.qubit + } + )mlir", + R"mlir( + %r0, %r1 = scf.while (%a = %q0, %b = %q1) + : (!qco.qubit, !qco.qubit) -> (!qco.qubit, !qco.qubit) { + scf.condition(%continue) %a, %b : !qco.qubit, !qco.qubit + } do { + ^bb0(%a: !qco.qubit, %b: !qco.qubit): + scf.yield %b, %a : !qco.qubit, !qco.qubit + } + )mlir"}; + for (const auto* loop : loops) { + const std::string source = std::string{R"mlir( + module { + func.func @main(%n: index, %continue: i1) { + %c0 = arith.constant 0 : index + %c1 = arith.constant 1 : index + %q0 = qco.static 0 : !qco.qubit + %q1 = qco.static 1 : !qco.qubit + )mlir"} + loop + R"mlir( + qco.sink %r0 : !qco.qubit + qco.sink %r1 : !qco.qubit + return + } + } + )mlir"; + auto moduleOp = mlir::parseSourceString(source, context.get()); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(mlir::succeeded(mlir::verify(*moduleOp))); + const auto diagnostics = expectFailure( + *moduleOp, + mlir::qco::createTargetNativeSynthesis(makeOneWayUCxTarget())); + EXPECT_NE(diagnostics.find("consistent static sites"), std::string::npos); + } +} + +TEST_F(TargetSynthesisTest, WhileResultsMayDifferFromLoopEntrySites) { + auto moduleOp = mlir::parseSourceString(R"mlir( + module { + func.func @main(%continue: i1) { + %q0 = qco.static 0 : !qco.qubit + %q1 = qco.static 1 : !qco.qubit + %r0, %r1 = scf.while (%a = %q0, %b = %q1) + : (!qco.qubit, !qco.qubit) -> (!qco.qubit, !qco.qubit) { + scf.condition(%continue) %b, %a : !qco.qubit, !qco.qubit + } do { + ^bb0(%a: !qco.qubit, %b: !qco.qubit): + scf.yield %b, %a : !qco.qubit, !qco.qubit + } + %c, %t = qco.ctrl(%r0) targets(%a = %r1) { + %x = qco.x %a : !qco.qubit -> !qco.qubit + qco.yield %x : !qco.qubit + } : ({!qco.qubit}, {!qco.qubit}) -> ({!qco.qubit}, {!qco.qubit}) + qco.sink %c : !qco.qubit + qco.sink %t : !qco.qubit + return + } + } + )mlir", + context.get()); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(mlir::succeeded(mlir::verify(*moduleOp))); + const auto target = makeOneWayUCxTarget(); + const auto before = printModule(*moduleOp); + ASSERT_TRUE(mlir::succeeded( + runPass(*moduleOp, mlir::qco::createTargetNativeSynthesis(target)))); + EXPECT_EQ(printModule(*moduleOp), before); + ASSERT_TRUE(mlir::succeeded( + runPass(*moduleOp, mlir::qco::createVerifyTargetConformance(target)))); +} + +TEST_F(TargetSynthesisTest, AcceptsMatchingBranchSitePermutations) { + auto moduleOp = build([](QCOProgramBuilder& builder) { + auto q0 = builder.staticQubit(0); + auto q1 = builder.staticQubit(1); + const auto swap = [](ValueRange args) { + return mlir::SmallVector{args[1], args[0]}; + }; + auto outputs = builder.qcoIf(true, ValueRange{q0, q1}, swap, swap); + std::tie(q0, q1) = builder.cx(outputs[0], outputs[1]); + return builder.intConstant(0); + }); + const auto target = makeOneWayUCxTarget(); + ASSERT_TRUE(mlir::succeeded( + runPass(*moduleOp, mlir::qco::createTargetNativeSynthesis(target)))); + ASSERT_TRUE(mlir::succeeded( + runPass(*moduleOp, mlir::qco::createVerifyTargetConformance(target)))); +} + +TEST_F(TargetSynthesisTest, + TargetNativeSynthesisRejectsIncompleteGlobalSingleQubitBasis) { + auto module = build([](QCOProgramBuilder& builder) { + auto qubit = builder.staticQubit(1); + qubit = builder.h(qubit); + return builder.intConstant(0); + }); + const auto target = valid(Target::create( + 2, Connectivity::allToAll(), + NativeOperations::fromOperations( + {valid(Operation::create("u", 1, 3, {valid(SiteTuple::create({0}))})), + valid(Operation::create("cx", 2, 0))}))); + + const auto diagnostics = + expectFailure(*module, mlir::qco::createTargetNativeSynthesis(target)); + EXPECT_NE(diagnostics.find("no usable synthesis basis"), std::string::npos) + << diagnostics; +} + +TEST_F(TargetSynthesisTest, + TargetNativeSynthesisRejectsNonadjacentSynthesisPlacement) { + auto module = build([](QCOProgramBuilder& builder) { + auto q1 = builder.staticQubit(0); + auto q2 = builder.staticQubit(2); + std::tie(q1, q2) = builder.swap(q1, q2); + return builder.intConstant(0); + }); + const auto target = valid(Target::create( + 3, Connectivity::fromCouplings({{0, 1}, {1, 2}}), + NativeOperations::fromOperations( + {valid(Operation::create("u", 1, 3)), + valid(Operation::create("cx", 2, 0, + {valid(SiteTuple::create({0, 1})), + valid(SiteTuple::create({1, 2}))})), + valid(Operation::create("gphase", 0, 1))}))); + + const auto diagnostics = + expectFailure(*module, mlir::qco::createTargetNativeSynthesis(target)); + EXPECT_NE(diagnostics.find( + "no supported synthesis-basis placement is known for its " + "static sites"), + std::string::npos) + << diagnostics; +} + TEST_F(TargetSynthesisTest, TargetNativeSynthesisLowersConstantSingleQubitGate) { const auto hadamard = [](QCOProgramBuilder& builder) { @@ -668,6 +1103,39 @@ TEST_F(TargetSynthesisTest, NativePowShellHidesItsImplementationBody) { EXPECT_EQ(printModule(*module), before); } +TEST_F(TargetSynthesisTest, RejectsUnsupportedMultiTargetControlShell) { + auto moduleOp = mlir::parseSourceString(R"mlir( + module { + func.func @main() { + %q0 = qco.static 0 : !qco.qubit + %q1 = qco.static 1 : !qco.qubit + %r0, %r1 = "qco.ctrl"(%q0, %q1) <{ + operandSegmentSizes = array, + resultSegmentSizes = array + }> ({ + ^bb0(%a: !qco.qubit, %b: !qco.qubit): + %c, %t = qco.ctrl(%b) targets(%x = %a) { + %flipped = qco.x %x : !qco.qubit -> !qco.qubit + qco.yield %flipped : !qco.qubit + } : ({!qco.qubit}, {!qco.qubit}) -> ({!qco.qubit}, {!qco.qubit}) + qco.yield %t, %c : !qco.qubit, !qco.qubit + }) : (!qco.qubit, !qco.qubit) -> (!qco.qubit, !qco.qubit) + qco.sink %r0 : !qco.qubit + qco.sink %r1 : !qco.qubit + return + } + } + )mlir", + context.get()); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(mlir::succeeded(mlir::verify(*moduleOp))); + ASSERT_TRUE(mlir::succeeded(mlir::qco::verifyLinearity(*moduleOp))); + const auto diagnostics = expectFailure( + *moduleOp, mlir::qco::createTargetNativeSynthesis(makeUCxTarget())); + EXPECT_NE(diagnostics.find("unitary matrix is not available"), + std::string::npos); +} + TEST_F(TargetSynthesisTest, MissingBasisIsDiagnosedOnlyWhenLoweringIsNeeded) { const auto hOnly = valid(Target::create( 1, Connectivity::allToAll(), @@ -728,6 +1196,46 @@ TEST_F(TargetSynthesisTest, SupportedRuntimeParameterizedGateStaysUntouched) { EXPECT_EQ(printModule(*module), before); } +TEST_F(TargetSynthesisTest, + RuntimeRxxUsesReverseNativeTupleWithoutSynthesisBasis) { + 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 target = makeOneWayRxxTarget(); + ASSERT_FALSE(target.synthesisBasis()); + + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createTargetNativeSynthesis(target)))); + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createVerifyTargetConformance(target)))); + ASSERT_TRUE(mlir::succeeded(mlir::verify(*module))); + + RXXOp rxx; + module->walk([&](RXXOp candidate) { rxx = candidate; }); + ASSERT_TRUE(rxx); + auto unitary = mlir::cast(rxx.getOperation()); + auto input0 = unitary.getInputQubit(0).getDefiningOp(); + auto input1 = unitary.getInputQubit(1).getDefiningOp(); + ASSERT_TRUE(input0); + ASSERT_TRUE(input1); + EXPECT_EQ(input0.getIndex(), 1U); + EXPECT_EQ(input1.getIndex(), 0U); + + auto returnOp = mlir::cast( + mainFunction(*module).getBody().front().getTerminator()); + EXPECT_EQ(returnOp.getOperand(0), unitary.getOutputQubit(1)); + EXPECT_EQ(returnOp.getOperand(1), unitary.getOutputQubit(0)); +} + TEST_F(TargetSynthesisTest, UnsupportedRuntimeParameterizedGateHasLocalDiagnostic) { auto module = mlir::parseSourceString(R"mlir( @@ -754,12 +1262,14 @@ TEST_F(TargetSynthesisTest, } TEST_F(TargetSynthesisTest, - UnsupportedRuntimeParameterizedGateDoesNotPartiallyRewrite) { + UnsupportedRuntimeParameterizedGateWithGlobalPhaseIsDiagnosed) { 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 + %phase = arith.constant 0.25 : f64 + qco.gphase(%phase) %q2 = qco.rz(%theta) %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 @@ -768,11 +1278,11 @@ TEST_F(TargetSynthesisTest, )mlir", context.get()); ASSERT_TRUE(module); - const auto before = printModule(*module); - static_cast(expectFailure( - *module, mlir::qco::createTargetNativeSynthesis(makeUCxTarget()))); - EXPECT_EQ(printModule(*module), before); + const auto diagnostics = expectFailure( + *module, mlir::qco::createTargetNativeSynthesis(makeUCxTarget())); + EXPECT_NE(diagnostics.find("unitary matrix is not available"), + std::string::npos); } TEST_F(TargetSynthesisTest, diff --git a/python/mqt/core/mlir.pyi b/python/mqt/core/mlir.pyi index 0f6076f9d7..7487b315ff 100644 --- a/python/mqt/core/mlir.pyi +++ b/python/mqt/core/mlir.pyi @@ -140,7 +140,7 @@ class CompilerTarget: """The raw T2 coherence time, if available.""" class SiteTuple: - """Calibration data for an ordered tuple of target sites.""" + """A supported ordered placement with optional calibration.""" def __init__( self, sites: Sequence[int], duration: int | None = None, fidelity: float | None = None @@ -187,14 +187,14 @@ class CompilerTarget: """Whether this arity accepts a concrete width.""" class Operation: - """A homogeneous target-wide operation capability and its calibration.""" + """A target operation capability, calibration, and ordered applicability.""" def __init__( self, name: str, arity: int | CompilerTarget.OperationArity, num_parameters: int, - site_tuples: Sequence[CompilerTarget.SiteTuple] | None = None, + site_tuples: Sequence[CompilerTarget.SiteTuple | Sequence[int]] | None = None, duration: int | None = None, fidelity: float | None = None, ) -> None: ... @@ -216,7 +216,7 @@ class CompilerTarget: @property def site_tuples(self) -> list[CompilerTarget.SiteTuple]: - """Ordered site-specific calibration data.""" + """Supported ordered placements with optional calibration; empty means general applicability.""" @property def duration(self) -> int | None: @@ -385,7 +385,9 @@ class CompilerTarget: def synthesis_basis(self) -> CompilerTarget.SynthesisBasis | None: """A complete target-wide synthesis basis, if available.""" - def supports_operation(self, name: str, arity: int, num_parameters: int | None = None) -> bool: + def supports_operation( + self, name: str, arity: int, num_parameters: int | None = None, sites: Sequence[int] | None = None + ) -> bool: """Whether the target supports an operation.""" class Program: diff --git a/test/python/test_mlir.py b/test/python/test_mlir.py index 9f571e832c..10cba46ce7 100644 --- a/test/python/test_mlir.py +++ b/test/python/test_mlir.py @@ -449,7 +449,14 @@ def test_compiler_target_constructors_preserve_python_api() -> None: CompilerTarget.Site(20, "q1"), ] site_tuple = CompilerTarget.SiteTuple([10, 20], duration=10, fidelity=0.99) - operation = CompilerTarget.Operation("cx", 2, 0, site_tuples=[site_tuple], duration=20, fidelity=0.98) + operation = CompilerTarget.Operation( + "cx", + 2, + 0, + site_tuples=[site_tuple], + duration=20, + fidelity=0.98, + ) fixed_zero = CompilerTarget.OperationArity.fixed(0) variadic = CompilerTarget.OperationArity.variadic(2) global_phase = CompilerTarget.Operation("gphase", fixed_zero, 1) @@ -486,6 +493,11 @@ def test_compiler_target_constructors_preserve_python_api() -> None: assert site_tuple.sites == [10, 20] assert len(operation.site_tuples) == 1 assert operation.site_tuples[0].sites == [10, 20] + assert not CompilerTarget.Operation("x", 1, 0).site_tuples + assert targets[0].supports_operation("ecr", 2, sites=[0, 1]) + assert not targets[2].supports_operation("ecr", 2, sites=[10, 20]) + assert targets[2].supports_operation("cx", 2, sites=[10, 20]) + assert not targets[2].supports_operation("cx", 2, sites=[20, 10]) assert operation.arity.kind == CompilerTarget.OperationArityKind.FIXED assert operation.arity.value == 2 assert global_phase.arity.kind == CompilerTarget.OperationArityKind.FIXED @@ -500,6 +512,25 @@ def test_compiler_target_constructors_preserve_python_api() -> None: assert duration_unit.unit == "ns" +@pytest.mark.parametrize("arity", [2, CompilerTarget.OperationArity.fixed(2)]) +def test_compiler_target_accepts_plain_site_tuples(arity: int | CompilerTarget.OperationArity) -> None: + """Mix plain placements and calibrated tuples without widening support.""" + operation = CompilerTarget.Operation( + "cx", arity, 0, site_tuples=[(1, 0), [1, 2], CompilerTarget.SiteTuple([2, 0], fidelity=0.99)] + ) + target = CompilerTarget( + 3, + connectivity=CompilerTarget.Connectivity.all_to_all(), + native_operations=CompilerTarget.NativeOperations([operation]), + ) + assert [entry.sites for entry in operation.site_tuples] == [[1, 0], [1, 2], [2, 0]] + assert [entry.fidelity for entry in operation.site_tuples] == [None, None, 0.99] + assert target.supports_operation("cx", 2, sites=[1, 0]) + assert not target.supports_operation("cx", 2, sites=[0, 1]) + with pytest.raises(ValueError, match="site tuple does not match its arity"): + CompilerTarget.Operation("cx", arity, 0, site_tuples=[(0,)]) + + def test_compiler_target_construction_preserves_validation_errors() -> None: """Translate explicit C++ construction errors to Python ``ValueError``.""" with pytest.raises(TypeError): @@ -533,6 +564,8 @@ def test_compiler_target_construction_preserves_validation_errors() -> None: 0, site_tuples=[CompilerTarget.SiteTuple([0, 1])], ) + with pytest.raises(ValueError, match="site tuple does not match its arity"): + CompilerTarget.Operation("cx", 2, 0, site_tuples=[CompilerTarget.SiteTuple([0])]) def test_compiler_target_snapshots_qdmi_device(garnet_target: CompilerTarget) -> None: