diff --git a/.agent/plans/1687-ct-01-compiler-target.md b/.agent/plans/1687-ct-01-compiler-target.md index 7a28406fd6..3a24088698 100644 --- a/.agent/plans/1687-ct-01-compiler-target.md +++ b/.agent/plans/1687-ct-01-compiler-target.md @@ -14,15 +14,16 @@ compiler can rely on without linking a live QDMI device, the FoMaC wrapper, or the legacy Core IR. After this change, C++ compiler code can construct a `mlir::CompilerTarget` either from a qubit count or from detailed site, topology, and operation data. It can cheaply copy that target, translate between -provider site identifiers and dense compiler vertices, ask whether an MLIR QCO -operation is native at an ordered hardware locus, and inspect one typed native -synthesis basis that is usable across the complete target. +target site identifiers and dense compiler vertices, ask whether an MLIR QCO +operation is native, and inspect one typed synthesis basis. Operation +capabilities are homogeneous across the target; ordered site tuples retain only +site-specific calibration data. The focused compiler unit test demonstrates the result. It constructs targets -with sparse signed site identifiers, provider gate aliases, calibrated operation -loci, and one-way topology input; verifies canonicalized topology and -capabilities; creates real QCO operations; and observes correct support answers. -Invalid metadata is rejected once at construction. +with sparse signed site identifiers, device gate aliases, calibrated operation +site tuples, and one-way topology input; verifies canonicalized topology and +homogeneous capabilities; creates real QCO operations; and observes correct +support answers. Invalid metadata is rejected once at construction. ## Progress @@ -41,17 +42,17 @@ Invalid metadata is rejected once at construction. `Target.h` to its build-tree public header file set, and excluded that header from `MQTCompilerPipeline`. - [x] (2026-08-03 11:52Z) Added eight focused tests covering all four - constructor forms, validation, cheap copies, topology, every required - symmetric and directional entangler, synthesis-basis resolution, and real - QCO operations. + constructor forms, validation, cheap copies, topology, every required + recognized entangler, synthesis-basis resolution, and real QCO operations. - [x] (2026-08-03 11:56Z) Built the target and interface-header verification target, passed 8 focused and all 218 compiler tests, ran changed-file clang-tidy without local diagnostics, passed the full repository lint, and passed `git diff --check`. - [x] (2026-08-03 11:56Z) Completed an independent pre-commit review and - resolved all four blocking findings: intrinsic locus validation, complete - entangler-table coverage, C++/policy diagnostics, and the stale plan. The - final tree is prepared for one signed atomic commit without a push. + resolved all four blocking findings: intrinsic site tuple validation, + complete entangler-table coverage, C++/policy diagnostics, and the stale + plan. The final tree is prepared for one signed atomic commit without a + push. - [x] (2026-08-03 12:35Z) Diagnosed the exact-head CI failures after publication. Moved five file-local helpers out of the anonymous namespace so LLVM 22's preferred static-linkage check passes. Rebuilt the target, @@ -66,6 +67,12 @@ Invalid metadata is rejected once at construction. retaining the repository's modernize-use-auto contract. Rebuilt the target and interface header and passed the 8 focused and all 218 compiler tests plus the exact relevant LLVM checks. +- [x] (2026-08-03) Simplified operation support to one homogeneous target-wide + capability per name, arity, and parameter count. Standardized on + `SiteTuple`, retained tuples only as calibration metadata, removed + directional probing, and reduced the public queries to + `supportsOperation(name, arity, parameters)`, `supports(Operation*)`, and + `supports(GateKind)`. ## Milestones @@ -73,21 +80,21 @@ Invalid metadata is rejected once at construction. The goal was a compiler-owned model that can be constructed without QDMI, FoMaC, CoreIR, or a live device. The work added value types for timing units, sites, -operation loci, and operation capabilities plus a shared immutable target -storage object. The result preserves provider metadata while validating the -cross-object contract once, including timing units, ordered loci, and explicit -operation absence versus emptiness. The focused construction and rejection tests -prove the observable contract. +operation site tuples, and operation capabilities plus a shared immutable target +storage object. The result preserves device metadata while validating the +cross-object contract once, including timing units, calibration site tuples, and +explicit operation absence versus emptiness. The focused construction and +rejection tests prove the observable contract. ### Milestone 2: Centralize topology and synthesis facts The goal was to prevent later mapping and synthesis stages from rebuilding -provider-specific representations. The work canonicalized explicit undirected +device-specific representations. The work canonicalized explicit undirected couplings, cached dense adjacency and all-pairs distances, indexed operation capabilities, and derived a typed target-wide synthesis basis. The result gives MAP-01 a reusable distance query and gives SYN-01 a stable gate enum without a -dependency on transformation code. Sparse-site, distance, ordered-locus, and -complete entangler-orientation tests prove these paths. +dependency on transformation code. Sparse-site, distance, calibration, and +complete entangler tests prove these paths. ### Milestone 3: Isolate and verify the foundation @@ -110,8 +117,8 @@ an explicit series-level item described below. - Observation: QCO already exposes `UnitaryOpInterface::getBaseSymbol()`, `getNumQubits()`, and `getNumParams()`. Evidence: `mlir/include/mlir/Dialect/QCO/IR/QCOInterfaces.td` defines all three - operations. `CompilerTarget::supports(Operation*, locus)` can use that - interface rather than enumerate every primitive operation. + operations. `CompilerTarget::supports(Operation*)` can use that interface + rather than enumerate every primitive operation. - Observation: The synthesis implementation currently owns a separate `NativeGateKind` plus Euler and Weyl machinery. Pulling that header into the target would make the foundation depend on synthesis details. The target can @@ -128,11 +135,10 @@ an explicit series-level item described below. The `Target.h` file set therefore establishes build-tree ownership and interface-header verification, not an installed consumer SDK. - Observation: Independent review caught inconsistent validation in the - standalone `Operation::supports` query and the all-native fast path, missing - table-driven coverage for six entanglers, changed-file clang-tidy findings, - and stale plan sections. The final implementation validates those query - boundaries, covers all eight required entanglers, and passes the repeated - static checks. + all-native fast path, missing table-driven coverage for six entanglers, + changed-file clang-tidy findings, and stale plan sections. The final + implementation validates those query boundaries, covers all eight required + entanglers, and passes the repeated static checks. - Observation: CI's LLVM 22 lint enables `llvm-prefer-static-over-anonymous-namespace`, which the local repository lint does not surface. Five file-local helpers therefore needed explicit `static` @@ -185,35 +191,33 @@ an explicit series-level item described below. passes simple. Date/Author: 2026-08-03, GPT-5.6 via Codex. - Decision: Treat an absent operation collection as every operation being native; distinguish it from a present empty collection, which supports no - hardware operation. Preserve provider names while caching a lower-case - canonical name and the aliases `prx` to `r`, `u3` to `u`, and `cnot` to `cx`. - Rationale: This retains provider metadata and adopts the useful, narrowly - scoped alias insight from Simon Hofmann's #1969 work without importing its - targeting, CLI, Python, or synthesis APIs. The final commit will preserve - Simon Hofmann's authorship because this semantic source is materially reused. - Date/Author: 2026-08-03, GPT-5.6 via Codex. + hardware operation. Preserve reported operation names while caching a + lower-case canonical name and the aliases `prx` to `r`, `u3` to `u`, and + `cnot` to `cx`. Rationale: This retains operation metadata and adopts the + useful, narrowly scoped alias insight from Simon Hofmann's #1969 work without + importing its targeting, CLI, Python, or synthesis APIs. The final commit will + preserve Simon Hofmann's authorship because this semantic source is materially + reused. Date/Author: 2026-08-03, GPT-5.6 via Codex. - Decision: Require every operation capability to have a positive fixed arity. Rationale: Final conformance cannot soundly validate an operation whose arity - is unknown; the later device adapter must reject a provider operation that - omits it rather than transferring ambiguity into the compiler model. - Date/Author: 2026-08-03, GPT-5.6 via Codex. + is unknown; the later device adapter must reject a device operation that omits + it rather than transferring ambiguity into the compiler model. Date/Author: + 2026-08-03, GPT-5.6 via Codex. - Decision: Store an optional target-wide duration unit and positive finite scale factor, and require it whenever a site T1/T2, operation duration, or - locus duration is present. T1/T2 must be positive; operation and locus - durations may be zero for virtual gates. Rationale: Raw `uint64_t` timing - metadata is not self-describing without the QDMI unit/scale contract, while - the existing SC device schema intentionally permits nonnegative operation - durations. Date/Author: 2026-08-03, GPT-5.6 via Codex. -- Decision: A gate is globally usable when its matching canonical capability - covers every site for a one-qubit gate or every routing edge for a two-qubit - gate. Operand-symmetric entanglers (`cz`, `rxx`, `ryy`, `rzz`, and `iswap`) - may cover an undirected edge in either orientation. Directional entanglers - (`cx`, `ecr`, and `rzx`) must cover both ordered orientations. An all-to-all - target applies the same rule to every distinct pair. `supports(gate, locus)` - remains strictly ordered. Rationale: A single synthesis basis advertised for - the whole target must not be inferred from a provider operation that is - unavailable on some sites or from an unimplemented operand-reversal - assumption. Date/Author: 2026-08-03, GPT-5.6 via Codex. + site-tuple duration is present. T1/T2 must be positive; operation and site + tuple durations may be zero for virtual gates. Rationale: Raw `uint64_t` + timing metadata is not self-describing without the QDMI unit/scale contract, + while the existing SC device schema intentionally permits nonnegative + operation durations. Date/Author: 2026-08-03, GPT-5.6 via Codex. +- Decision: Treat each operation capability as homogeneous across the target. + Site tuples retain ordered duration and fidelity overrides but do not alter + support. A recognized gate is available when its canonical name, arity, and + parameter count match one capability; two-qubit gates consequently work in + either operand orientation. Rationale: Current compilation targets expose one + gate set, while site variation is calibration metadata. This removes + directional probing and per-site capability reconstruction from every compiler + pass. Date/Author: 2026-08-03, GPT-5.6 via Codex. - Decision: Accept a target whose capabilities do not form a complete synthesis basis and expose `std::nullopt` from `synthesisBasis()`. Rationale: Such a target remains useful for support checking and future diagnostics; SYN-01 can @@ -277,18 +281,17 @@ Its operation declarations are generated from `mlir/include/mlir/Dialect/QCO/IR/QCOOps.td`, and `mlir/include/mlir/Dialect/QCO/IR/QCOInterfaces.td` defines `UnitaryOpInterface`. A dense compiler vertex is a zero-based position used by -routing algorithms. A hardware site identifier is the provider-visible signed +routing algorithms. A hardware site identifier is the device-visible signed integer that later appears in `qco.static`; the two are not interchangeable when -providers use sparse identifiers. +devices use sparse identifiers. A capability states that an operation with a canonical name, arity, and -parameter count is allowed at an ordered locus. A locus is an ordered list of -hardware site identifiers. An absent locus collection means the capability is -global for every valid tuple of its arity; a present empty collection means it -supports no tuple. A synthesis basis is one recognized single-qubit Euler basis -plus one recognized two-qubit entangling gate that is usable over the entire -target. CT-01 only describes that basis; the existing native-synthesis -implementation remains under `mlir/lib/Dialect/QCO/Transforms/Decomposition/`. +parameter count is available throughout the target. A site tuple is an ordered +list of hardware site identifiers with optional duration and fidelity overrides; +it does not restrict support. A synthesis basis is one recognized single-qubit +Euler basis plus one recognized two-qubit entangling gate. CT-01 only describes +that basis; the existing native-synthesis implementation remains under +`mlir/lib/Dialect/QCO/Transforms/Decomposition/`. The focused compiler unit test executable is configured by `mlir/unittests/Compiler/CMakeLists.txt` and produced at @@ -301,7 +304,7 @@ verification target. ## Plan of Work Add `mlir/include/mlir/Compiler/Target.h`. Define -`CompilerTarget::DurationUnit`, `Site`, `OperationLocus`, and `Operation` as +`CompilerTarget::DurationUnit`, `Site`, `SiteTuple`, and `Operation` as immutable public value types with constructors that validate their local scalar data. Operation arity is mandatory. Define `GateKind`, `SingleQubitBasis`, and `SynthesisBasis` as typed compiler capabilities. Define named and unnamed @@ -313,8 +316,8 @@ Add `mlir/lib/Compiler/Target.cpp`. Build a private shared `Storage` object, validate cross-references and the timing-unit invariant, cache site identifiers and the site-to-vertex map, canonicalize and validate explicit topology, build its dense adjacency and all-pairs distance matrix, check connectivity once, -group operations by canonical name, determine globally supported typed gates, -and resolve the preferred synthesis basis. Implement queries for metadata, dense +group operations by canonical name, determine the supported typed gates, and +resolve the preferred synthesis basis. Implement queries for metadata, dense mapping, adjacency and distance, canonical operation support, typed gate support, and QCO `Operation*` support. Structural QCO barrier and global-phase operations require no hardware capability; controlled single-X and single-Z @@ -334,9 +337,9 @@ closure is exported by the PIPE/INT series. Add `mlir/unittests/Compiler/test_compiler_target.cpp` and include it in the existing compiler test executable. Cover both constructor families, optional metadata, cheap shared copies, sparse site mappings, topology normalization, -every validation boundary, absent versus empty operation semantics, provider -aliases, ordered loci, global gate coverage, basis preference, and real QCO -operation support. Link the test executable to `MQTCompilerTarget`. +every validation boundary, absent versus empty operation semantics, gate +aliases, ordered site tuples, typed gate coverage, basis preference, and real +QCO operation support. Link the test executable to `MQTCompilerTarget`. Do not edit `cmake/ExternalDependencies.cmake` unless the scoped QDMI `SYSTEM` setting appears through an in-scope integration update. It is absent at the @@ -389,34 +392,32 @@ mutation is authorized. Construction from a count produces sites `0..N-1`; construction from detailed sites preserves order, optional target and site names, T1/T2, operation -duration/fidelity, and ordered loci. A copied target returns views backed by the -same immutable storage. +duration/fidelity, and ordered site tuples. A copied target returns views backed +by the same immutable storage. Negative or duplicate site identifiers, an empty target, count overflow, zero operation arity, zero coherence times, invalid fidelity values, timing data -without a valid duration unit/scale, malformed/duplicate loci, unknown site -references, self-couplings, and disconnected explicit topologies throw -`std::invalid_argument`. Zero operation and locus durations remain valid. +without a valid duration unit/scale, malformed/duplicate site tuples, unknown +site references, self-couplings, and disconnected explicit topologies throw +`std::invalid_argument`. Zero operation and site-tuple durations remain valid. Reversed and duplicate topology input is normalized to one sorted undirected edge. An absent topology reports all distinct site pairs as adjacent and distance one. An explicit topology reports only its validated edges and returns cached shortest-path distances. Dense vertex/site translations are stable for sparse, -unsorted provider identifiers. +unsorted target identifiers. An absent operation collection supports every well-formed hardware operation. A present empty collection supports none except structural barrier and -global-phase operations. Provider spelling is retained while canonical lookup -recognizes case, whitespace, `prx`, `u3`, and `cnot`. Ordered loci remain -directional. - -The typed global gate view excludes a capability that is missing on one site or -one routing edge. Symmetric entanglers may use either locus orientation; -directional entanglers require both. `synthesisBasis()` exists only when one -complete supported single-qubit basis and one globally supported entangler -exist, with documented deterministic preference; an incomplete basis does not -invalidate the target. `supports(Operation*, locus)` agrees with typed +global-phase operations. Reported operation spelling is retained while canonical +lookup recognizes case, whitespace, `prx`, `u3`, and `cnot`. Site tuples +preserve ordered calibration data without restricting operation support. + +The typed gate view follows the homogeneous operation set. `synthesisBasis()` +exists only when one complete supported single-qubit basis and one supported +entangler exist, with documented deterministic preference; an incomplete basis +does not invalidate the target. `supports(Operation*)` agrees with typed capability queries for actual QCO primitives and controlled X/Z operations. `MQTCompilerTarget` builds as its own MLIR library. Its direct link interface @@ -483,7 +484,7 @@ At completion, `mlir/Compiler/Target.h` defines: using Coupling = std::pair; class DurationUnit; class Site; - class OperationLocus; + class SiteTuple; class Operation; enum class GateKind : uint8_t; enum class SingleQubitBasis : uint8_t; @@ -509,11 +510,11 @@ At completion, `mlir/Compiler/Target.h` defines: const std::optional& durationUnit() const; bool hasExplicitOperations() const; ArrayRef operations() const; - bool supportsOperation(StringRef name, ArrayRef locus, + bool supportsOperation(StringRef name, size_t numQubits, std::optional numParameters) const; - bool supports(Operation* operation, ArrayRef locus) const; - bool supports(GateKind gate, ArrayRef locus) const; - ArrayRef globallySupportedGates() const; + bool supports(Operation* operation) const; + bool supports(GateKind gate) const; + ArrayRef supportedGates() const; std::optional synthesisBasis() const; }; } @@ -528,10 +529,10 @@ Revision note (2026-08-03): Created the initial self-contained plan after exact checkout verification and source/provenance research. The implementation and validation sections will be updated as evidence replaces planned behavior. -Revision note (2026-08-03): Refined global entangler coverage before -implementation: symmetric operations may cover an undirected edge in one -orientation, while directional operations require both ordered orientations. -Also recorded that incomplete synthesis capability is a valid target state. +Revision note (2026-08-03): Replaced the initial per-site entangler-coverage +model with homogeneous operation capabilities. Site tuples now retain +calibration only, and incomplete synthesis capability remains a valid target +state. Revision note (2026-08-03): Refined the foundation while the first implementation draft was still uncompiled: operation arity is mandatory, timing diff --git a/.agent/plans/1687-iqm-01-models.md b/.agent/plans/1687-iqm-01-models.md index 5add73df6b..f636cb2411 100644 --- a/.agent/plans/1687-iqm-01-models.md +++ b/.agent/plans/1687-iqm-01-models.md @@ -63,8 +63,8 @@ and command-line integration belong to later changes. ## Surprises & Discoveries - Observation: the SC schema already represents optional per-qubit T1/T2 and - optional per-locus duration/fidelity. It only lacks the optional site name - that QDMI already exposes through `QDMI_SITE_PROPERTY_NAME`. + optional per-site-tuple duration/fidelity. It only lacks the optional site + name that QDMI already exposes through `QDMI_SITE_PROPERTY_NAME`. - Observation: the IQM provider obtains site names from the ordered `qubits` array of the static architecture response and assigns the array position as the QDMI site index. IQM qubits in these captures use `QB1` through `QB20` for @@ -88,8 +88,8 @@ and command-line integration belong to later changes. `api/v1/quantum-computers/%s/artifacts/static-quantum-architectures`, `api/v1/calibration-sets/%s/%s/dynamic-quantum-architecture`, and `api/v1/calibration-sets/%s/%s/metrics`. The first two select the computer and - return names/topology, the third returns default-calibration operation loci, - and the fourth returns T1/T2 and fidelity metrics. + return names/topology, the third returns default-calibration operation site + tuples, and the fourth returns T1/T2 and fidelity metrics. - Observation: CTest registers the individual GoogleTest cases, not targets named after the three executables. A target-name regular expression therefore selected no tests; running each built executable directly exercised 171 tests. @@ -115,7 +115,7 @@ and command-line integration belong to later changes. nanosecond-level coherence precision through unit `us` with scale factor `0.001`. Date/Author: 2026-08-03, Codex. - Decision: do not add any duration field. Rationale: neither the captured - provider data nor the IQM QDMI interface supplied operation durations. + device data nor the IQM QDMI interface supplied operation durations. Date/Author: 2026-08-03, Codex. - Decision: extend `mqt_configure_qdmi_device` with a `CONFIGURATIONS` list whose entries pair a stable registry ID with the basename of an existing @@ -176,24 +176,25 @@ entry has exactly an ID and filename and that the filename is among loops then package all three SC JSON files. Expand configuration tests to assert the model names, qubit and edge counts, -exact operation names, calibration coverage, locus-fidelity counts, site-name -endpoints, and complete absence of operation durations. Add schema coverage for -name-only overrides and empty-name rejection. Expand provider tests to prove -configured names are queryable and absent names remain unsupported. Expand -registry and driver tests to prove that five built-in definitions exist, both -stable IDs use packaged configuration files, and both models can be opened. +exact operation names, calibration coverage, per-site-tuple fidelity counts, +site-name endpoints, and complete absence of operation durations. Add schema +coverage for name-only overrides and empty-name rejection. Expand provider tests +to prove configured names are queryable and absent names remain unsupported. +Expand registry and driver tests to prove that five built-in definitions exist, +both stable IDs use packaged configuration files, and both models can be opened. Update `docs/qdmi/sc_device.md` to describe optional names and the two shared models. Record that topology and site names came from the static architecture, -operation loci from the default dynamic architecture, and T1/T2/fidelities from -default calibration-set quality metrics retrieved on 2 August 2026. State that -the files contain no credentials or calibration identifiers and intentionally -omit unavailable durations. In this ExecPlan, pin the interpretation to -QDMI-on-IQM revision `5bef1d49245ae17877203618ad65865405fab361` and record its -source paths and the fact that no provider source, documentation text, or raw -service response is redistributed. Add one narrowly scoped `Unreleased` -changelog entry without inventing a pull request reference; add the reference in -a post-publication follow-up. +operation site tuples from the default dynamic architecture, and +T1/T2/fidelities from default calibration-set quality metrics retrieved on 2 +August 2026. State that the files contain no credentials or calibration +identifiers and intentionally omit unavailable durations. In this ExecPlan, pin +the interpretation to QDMI-on-IQM revision +`5bef1d49245ae17877203618ad65865405fab361` and record its source paths and the +fact that no provider source, documentation text, or raw service response is +redistributed. Add one narrowly scoped `Unreleased` changelog entry without +inventing a pull request reference; add the reference in a post-publication +follow-up. ## Milestones @@ -273,12 +274,12 @@ The implementation is accepted when: - Garnet parses as 20 sites and 30 ordered topology edges, and Emerald parses as 54 sites and 90 ordered topology edges. -- Both assets expose exactly `r`, `cz`, and `measure`; all recorded per-locus - fidelities remain available. +- Both assets expose exactly `r`, `cz`, and `measure`; all recorded + per-site-tuple fidelities remain available. - Garnet exposes 20 T1 and 20 T2 values. Emerald exposes 53 T1 and 54 T2 values. - Site indices remain zero-based while reported names are `QB1` through `QB20` or `QB54`, respectively. -- No operation or locus in either model reports a duration. +- No operation or site tuple in either model reports a duration. - The IDs `mqt.sc.iqm.garnet` and `mqt.sc.iqm.emerald` resolve to the SC provider plus the correct packaged JSON file in a build-tree registry. - The two JSON files are included in the provider's runtime-file property and diff --git a/.agent/plans/1687-map-01-target-mapping.md b/.agent/plans/1687-map-01-target-mapping.md index fa73df0dcf..5dce01d8ff 100644 --- a/.agent/plans/1687-map-01-target-mapping.md +++ b/.agent/plans/1687-map-01-target-mapping.md @@ -12,7 +12,7 @@ repository root. After this change, callers can run the QCO `place-and-route` pass against an immutable `mlir::CompilerTarget` instead of constructing a second graph wrapper from a dense coupling set. The mapper uses the target's validated topology for -placement and routing, emits provider-defined site identifiers in `qco.static` +placement and routing, emits device-defined site identifiers in `qco.static` operations, and continues to insert ordinary `qco.swap` operations without making target-native gate or direction decisions. @@ -49,7 +49,7 @@ pipeline integration task. allocations and higher-arity unitaries before mutation, and reuses its records for routing and placement. - [x] (2026-08-03 13:09Z) Materialized only program qubits and vacant target - vertices used by the selected routing plan, with provider site IDs in + vertices used by the selected routing plan, with target site IDs in output. - [x] (2026-08-03 13:09Z) Added focused mapping, diagnostic, allocation, sparse-workspace, empty-operation-set, and noncontiguous-ID tests while @@ -131,7 +131,7 @@ pipeline integration task. - Decision: use dense zero-based compiler vertices for `Layout`, A* search, and restoration, and translate through `CompilerTarget::siteForVertex` only when creating `qco.static`. Rationale: layout algorithms require dense indices, - while output IR must retain provider-defined identifiers. Date/Author: + while output IR must retain device-defined identifiers. Date/Author: 2026-08-03, Codex. - Decision: route every one-qubit unitary directly and every two-qubit unitary solely according to undirected target adjacency. Do not query operation @@ -167,7 +167,7 @@ pipeline integration task. Implementation and validation are complete. The mapper now owns a cheap `CompilerTarget` value, uses its dense topology throughout placement and -routing, translates to provider site IDs only for `qco.static`, supports scalar +routing, translates to target site IDs only for `qco.static`, supports scalar and mixed allocation forms, and materializes only active or preview-touched workspace qubits. The single discovery/planning walk makes unsupported nested allocations and higher-arity operations fail before mutation. @@ -183,7 +183,7 @@ same result against the merged compiler-target foundation. `mlir::CompilerTarget`, declared in `mlir/include/mlir/Compiler/Target.h` and implemented in `mlir/lib/Compiler/Target.cpp`, is an immutable target model. -Provider site identifiers can be sparse or noncontiguous, but the target stores +Device site identifiers can be sparse or noncontiguous, but the target stores them in a stable site order and exposes dense compiler vertices for algorithms. An absent topology denotes all-to-all connectivity; an explicit topology is validated as a connected undirected graph and has cached distances. @@ -268,7 +268,7 @@ comments, pass documentation, and diagnostics so they describe target topology and the supported one-/two-qubit boundary accurately. Refactor the mapping test fixture to hold a `CompilerTarget`. Adapt executable -checking to translate static provider IDs back to dense vertices before testing +checking to translate static target IDs back to dense vertices before testing adjacency. Preserve the existing nine-qubit grid and every current regression. Add focused tests that demonstrate arbitrary one- and two-qubit unitary names are routed without native-capability checks; nested higher-arity operations and @@ -332,8 +332,8 @@ The implementation is accepted when the mapping test binary passes every pre-existing test, including index-switch vote-and-restore, and the following new behavior is observed. -A `CompilerTarget` with noncontiguous provider IDs maps using dense internal -vertices, while every emitted `qco.static` index is one of the provider IDs. +A `CompilerTarget` with noncontiguous target IDs maps using dense internal +vertices, while every emitted `qco.static` index is one of the target IDs. Arbitrary one- and two-qubit QCO unitaries pass through the topology-only mapper; two-qubit operations are adjacent after routing regardless of target operation metadata or operand direction. Inserted routing operations remain @@ -389,7 +389,7 @@ The current restack evidence is: Patch comparison: final range-diff differs only in this plan; non-plan stable patch IDs match Current target API: - dense vertices plus provider site IDs, validated connected topology, + dense vertices plus target site IDs, validated connected topology, cached adjacency, neighbours, distances, and maximum degree Current mapper: CompilerTarget directly, with no coupling-set overload or adapter @@ -460,8 +460,8 @@ declares: `MappingPass` owns a `CompilerTarget` value. It uses only `numQubits`, `siteForVertex`, `areAdjacent`, `distanceBetween`, `forEachNeighbour`, and -`maxDegree` from that target. Operation-capability, native-gate, calibration, -duration, fidelity, and directed-locus APIs are deliberately out of scope. +`maxDegree` from that target. Operation capabilities, native gates, and +calibration metadata are deliberately out of scope. `MLIRQCOTransforms` and `MQTCompilerPipeline` depend on `MQTCompilerTarget`. The mapping test target also links the target library directly when needed. No new diff --git a/.agent/plans/1687-syn-01-target-synthesis.md b/.agent/plans/1687-syn-01-target-synthesis.md new file mode 100644 index 0000000000..afa61430b9 --- /dev/null +++ b/.agent/plans/1687-syn-01-target-synthesis.md @@ -0,0 +1,482 @@ +# Split target-independent gate fusion from target-native synthesis + +This ExecPlan is a living document. The sections `Progress`, +`Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must +be kept up to date as work proceeds. + +This ExecPlan must be maintained in accordance with `.agent/PLANS.md` from the +repository root. + +## Purpose / Big Picture + +Two-qubit gate fusion and hardware lowering must answer different questions. +Before routing, target-independent two-qubit gate fusion should rewrite a +sequence only when doing so strictly reduces its two-qubit gate count, without +choosing a hardware basis. After routing, the compiler should lower operations +that the real `mlir::CompilerTarget` does not support and remove routing SWAPs +on targets that do not declare SWAP native. A final, independently runnable pass +rejects unsupported operation types, arities, and parameter counts as well as +dynamic allocations and unknown static target sites. + +After this change, C++ pipeline code can construct these three stages +independently. Focused tests demonstrate a profitable CX cancellation, preserve +isolated and runtime-parameterized gates before routing, lower SWAP to a +target-selected basis after routing, and reject operation, arity, parameter, +allocation, and site mismatches. + +## Progress + +- [x] (2026-08-03 13:09Z) Verified the assigned isolated worktree, clean status, + initial CT-01 development base `b6eb95521cb76224c137496f113f38b9ce295854`, + and repository policy; read `AGENTS.md`, `docs/ai_usage.md`, + `.agent/PLANS.md`, and the MQT PR remediation instructions. +- [x] (2026-08-03 13:42Z) Inspected `CompilerTarget`, the native-menu parser, + Euler and Weyl decomposition, two-qubit run scanning, mapping output, + compiler and Python APIs, focused tests, the changelog, and Simon + Hofmann's useful #1969 commits. +- [x] (2026-08-03 15:35Z) Removed the duplicate gate enum/parser and obsolete + textual menu, CLI, C++ `QCOProgram`, Python binding/stub, and + menu-specific tests. Added only a minimal decomposition adapter from + `CompilerTarget::SynthesisBasis`. +- [x] (2026-08-03 16:18Z) Implemented independently constructible two-qubit gate + fusion, target-native synthesis, and target-conformance passes. +- [x] (2026-08-03 17:02Z) Replaced the broad menu suite with sixteen focused + stage-contract tests and built and ran the target-synthesis, + decomposition, and compiler unit-test binaries successfully. +- [x] (2026-08-03 18:33Z) Updated the existing Unreleased changelog entry and + completed source formatting, changed-file checks, repository lint, and + initial diff checks. +- [x] (2026-08-03 19:01Z) Resolved all three blockers from independent read-only + review: single-orientation symmetric-entangler site tuples, native SWAP + authority, and native `qco.pow` body handling. The reviewer approved the + remediated code and the focused suite passes all sixteen tests. +- [x] (2026-08-03 19:20Z) Rebuilt and reran all three affected suites, reran + changed-file hooks and full repository lint, completed focused + `clang-tidy` checks and the final diff audit, and prepared one signed + atomic commit for local handoff without publication. +- [x] (2026-08-03 19:30Z) Restacked the sole SYN commit without content + conflicts onto the final CT-01 squash + `f775395a25fddba0a3b54996416d1311bc6ebe71`. The old and restacked commits + have the same stable patch ID. +- [x] (2026-08-03 19:45Z) Repeated the requested builds, 16/199/215 tests, + interface and stub checks, focused `clang-tidy`, hooks, lint, + stale-surface search, and diff audits on the final CT-01 base, then + amended this evidence into the single signed commit. +- [x] (2026-08-03 19:55Z) Published draft PR #1998, fixed its sole changed-file + `clang-tidy` finding, and added four focused regressions after Codecov + exposed untested adapter, lowering, fusion, and static-site paths. The + resulting 20/199 focused tests pass and the two implementation files reach + 91% combined local line coverage. +- [x] (2026-08-03 20:43Z) Observed terminal all-green CI for exact published + head `b0a520372`, including 92.1% C++ patch coverage and strict + documentation, then restacked the four SYN commits onto merged MAP-01 + commit `c9e0c0ca5`. Resolved the compiler test by retaining MAP-01's + undirected coupling input while removing the obsolete native-menu call, + retained both PR links in the changelog, and passed 20/199/215 tests plus + the focused high-level mapping API test. +- [x] (2026-08-03 19:54Z) Addressed the latest design and efficiency review as + one simplification: made `CompilerTarget::SingleQubitBasis` the only basis + enum, deleted the decomposition adapter, removed entangler operand + reversal and redundant matrix assertions, selected U/CZ for generic + fusion, reused Weyl decompositions, validated target sites directly, and + made target lowering failure-atomic. A direct preplanned `IRRewriter` + traversal replaces general greedy machinery. Rebuilt the public + interface-header targets and observed 20/20 target-synthesis, 199/199 + decomposition, and 215/215 compiler tests. Focused changed-source + `clang-tidy`, targeted hooks, full repository lint, stale-surface search, + and `git diff --check` pass. A fresh independent exact-working-tree review + approved the result with no correctness, bloat, or efficiency findings. +- [x] (2026-08-03) Removed the flaky large-scope global-phase timing test, + adopted `SiteTuple`/target terminology, renamed the pre-routing factory to + `createFuseTwoQubitGates`, and simplified `CompilerTarget` to homogeneous + operation capabilities. Removed directional fallback, per-operation site + tracing, and the two control-flow tests that existed only for that tracer; + conformance now validates allocation form, quantum function inputs, and + static target IDs directly. Focused release builds pass all 21 + target-synthesis tests and all 8 compiler-target tests. + +## Milestones + +### Milestone 1: Make `CompilerTarget` the only capability model + +The completed first milestone removes +`mlir/Dialect/QCO/Transforms/Decomposition/NativeGateset.h` and its source. Gate +identity, gate aliases, arity, parameter counts, homogeneous capability checks, +and basis selection now remain in `mlir/include/mlir/Compiler/Target.h` and +`mlir/lib/Compiler/Target.cpp`. `CompilerTarget::SingleQubitBasis` is also the +single type consumed by Euler and Weyl synthesis; the duplicate `EulerBasis` +enum and `NativeSynthesisBasis` adapter are deleted. Weyl retains only cached +decomposers for the target-selected entangler. + +The generated `fuse-two-qubit-unitary-runs` pass and all of its string-menu +surfaces are removed rather than retained as a synthetic target. The later PIPE +slice owns high-level target pipeline composition, so this slice exposes only +the three typed C++ pass factories. + +### Milestone 2: Separate fusion, lowering, and verification + +The completed target-independent two-qubit gate-fusion stage scans maximal +constant sequences on one pair of linear QCO wires. It evaluates a canonical +U/CZ Weyl decomposition and rewrites only when the synthesized entangler count +is strictly smaller than the sequence's original two-qubit operation count. It +does not accept a target and does not rewrite isolated or runtime-parameterized +gates. + +The completed post-routing stage asks the supplied `CompilerTarget` whether each +one- or two-qubit unitary belongs to its homogeneous operation set. Supported +operations, including supported runtime-parameterized gates, remain untouched. +Unsupported constant operations and ordinary `qco.swap` operations are lowered +through the target's usable synthesis basis. A target-native SWAP remains +untouched. The pass preflights every lowering need before mutation, asks for a +basis only when needed, and reports an unsupported runtime gate without +partially rewriting the module. + +The completed conformance stage checks each real unitary, measurement, and reset +operation against that same homogeneous capability set. It rejects dynamic qubit +allocations and `qco.static` identifiers absent from the target, without tracing +every operation operand through its SSA lineage. Diagnostics report the actual +operation spelling, arity, and parameter count. + +### Milestone 3: Prove the contracts and hand off one atomic change + +The focused target-synthesis test binary now proves the three stage boundaries, +including unitary equivalence for gate fusion, SWAP lowering, and homogeneous CZ +support. It also proves explicit CZ emission, failure-atomic diagnostics, +static-site validation, dynamic-allocation rejection, and that native SWAP and +`qco.pow` shells remain untouched. The decomposition and compiler suites prove +that the shared basis type covers every supported entangler and that removing +the old high-level menu API does not break the remaining compiler pipeline. + +This slice must not add `compileForTarget`, alter default pipeline composition, +or remove the coupling-only `placeAndRoute` overload; those are PIPE +responsibilities. Publishing this latest revision or otherwise mutating GitHub +requires separate revision-scoped authorization. + +## Surprises & Discoveries + +- Observation: The old two-qubit pass performed four jobs: single-qubit + target-basis lowering, two-qubit gate fusion, isolated two-qubit lowering, and + residual menu checking. Evidence: its `hasNonNativeGate` condition could + rewrite a pre-routing window without reducing the entangler count. +- Observation: CT-01 already recognizes all fifteen gates understood by the + deleted menu, including gate aliases and the same entangler preference. The + old `NativeGateset` duplicated the enum, parser switch, basis resolution, and + operation classifier. +- Observation: Mapping emits `qco.static` with hardware identifiers, while + `CompilerTarget` permits sparse target IDs. Conformance only needs to validate + those declarations once; operation capabilities are homogeneous and do not + require operand-by-operand site tracing. +- Observation: Applying an MLIR greedy rewrite driver can reorder an unrelated + constant even when the quantum pattern does not match. The target-specific + transforms now precompute their work and use `IRRewriter` directly, so a no-op + fusion preserves the module byte-for-byte. +- Observation: Initial configuration required network access to fetch pinned + repository dependencies. Once fetched into the worktree-local build tree, + focused compilation and tests were repeatable without source workarounds. +- Observation: The first post-restack stub check selected the Python-packaged + CMake launcher and an unpinned MLIR, then failed during configuration. + Pointing the check at the real CMake binary and local LLVM/MLIR 22.1.3 + installation built the bindings, regenerated every stub, and left the tracked + stubs unchanged. +- Observation: Pull request #1969 established the useful progressive ordering of + decomposition, optional routing, and late native synthesis, with a test that + routed SWAPs disappear. Its `targetNative` Python duck typing and coupling CLI + are intentionally excluded. +- Observation: Current targets expose homogeneous gate sets. Ordered + `Operation::siteTuples()` retain calibration data, while + `CompilerTarget::supports()` depends only on canonical operation name, arity, + and parameter count. +- Observation: `qco.pow`, like `qco.ctrl` and `qco.inv`, is a target-visible + unitary shell with a region body. Synthesis and conformance must classify the + shell and skip its implementation body. +- Observation: A generic walk rewrite driver cannot safely anchor this + multi-operation fusion at the run head because the rewrite erases operations + the driver has not visited. Precollecting non-overlapping run heads and using + `IRRewriter` directly is both safer and lighter. +- Observation: The earlier per-operation target-site tracer was unnecessary once + support became homogeneous. Removing it eliminates both its quadratic worst + case and its structured-control-flow special cases. Profitable windows also + reuse the same prepared Weyl decomposition for counting and emission. + +## Decision Log + +- Decision: Remove the native-menu pass and all high-level menu APIs in this + slice instead of retaining a compatibility adapter. Rationale: the series + intentionally moves callers to a typed `CompilerTarget`; a synthetic menu + target would preserve the parallel configuration model this change removes. + Date/Author: 2026-08-03, GPT-5.6 via Codex. +- Decision: Use `CompilerTarget::SingleQubitBasis` directly throughout target, + Euler, and Weyl code and delete `NativeSynthesisBasis`. Rationale: one enum + and one synthesis-basis value remove a conversion switch, adapter files, and + an otherwise redundant coverage test while preserving dependency direction. + Date/Author: 2026-08-03, GPT-5.6 via Codex. +- Decision: Measure pre-routing profitability by a strict reduction in two-qubit + basis uses and materialize a canonical U/CZ sequence only after the comparison + succeeds. Rationale: two-qubit operations drive routing cost, and selecting a + hardware basis before routing would conflate optimization with target + legality; CZ avoids introducing an arbitrary control direction. Date/Author: + 2026-08-03, GPT-5.6 via Codex. +- Decision: Let `CompilerTarget::supports` decide whether an ordinary `qco.swap` + requires post-routing lowering. Rationale: the target is the sole capability + authority; routing SWAPs still lower on ordinary targets that do not report + SWAP, while a target-native SWAP must remain legal even when the target has no + global synthesis basis. Date/Author: 2026-08-03, GPT-5.6 via Codex. +- Decision: Treat operation capabilities as homogeneous across a target. Ordered + site tuples retain calibration only; synthesis and conformance query canonical + name, arity, and parameter count without directional fallback. Rationale: + current target gate sets are uniform and bidirectional, so site tracing and + reverse probes add code without changing compilation behavior. Date/Author: + 2026-08-03, GPT-5.6 via Codex. +- Decision: Preflight all target-lowering needs and apply planned rewrites + directly with `IRRewriter`. Rationale: failure remains atomic and generic + greedy/fixpoint work is unnecessary. Date/Author: 2026-08-03, GPT-5.6 via + Codex. +- Decision: Do not require `CompilerTarget::synthesisBasis()` at pass + construction or pass entry. Rationale: absent operations mean all operations + are native, and an incomplete explicit target can still describe a conforming + program. Missing-basis failure matters only after an unsupported operation + actually needs lowering. Date/Author: 2026-08-03, GPT-5.6 via Codex. +- Decision: Keep gate fusion, target-native synthesis, and conformance as + separate manual factories rather than textual passes. Rationale: + `CompilerTarget` is an immutable typed C++ value that cannot be faithfully + represented by generic pass options, and separate factories make each stage + independently testable and benchmarkable. Date/Author: 2026-08-03, GPT-5.6 via + Codex. +- Decision: Preserve Simon Hofmann's authorship in the implementation commit. + Rationale: the implementation materially carries forward the progressive + post-routing behavior and SWAP-removal coverage from his #1969 work while + excluding the unapproved Python and CLI designs. Date/Author: 2026-08-03, + GPT-5.6 via Codex. + +## Outcomes & Retrospective + +The implementation now has one homogeneous capability authority and three +separately observable transform stages. The latest cleanup removes the +decomposition basis adapter, general greedy rewrite machinery, directional +capability fallback, and per-operation target-site tracer. It retains explicit +CZ emission and failure-atomic lowering while adding direct static-site and +dynamic-allocation and quantum-function-input conformance coverage. Final +release builds pass 21 target-synthesis, 215 compiler, 33 dialect-utils, 27 +mapping, and 199 decomposition tests. The SC device suite passes 41 tests with +one expected job-ID skip. Both affected interface-header targets build, all +repository hooks pass, focused LLVM 22.1.8 `clang-tidy` reports no new +diagnostics, and an independent review approves the exact working tree. + +The main design lesson is that target support and fusion profitability must not +share a configuration surface. Canonical U/CZ gate fusion is useful without +hardware knowledge, while post-routing lowering and conformance require the +target's homogeneous operation set and declared static sites. + +## Context and Orientation + +`mlir/include/mlir/Compiler/Target.h` defines the immutable target. Its +`supports(Operation*)` query recognizes QCO operation semantics and checks +canonical name, arity, and parameter count. Ordered operation site tuples retain +calibration only. Its `synthesisBasis()` query returns one usable single-qubit +basis and entangler only when both exist. + +`mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp` contains +the two-qubit gate-fusion scanner, three pass implementations, static-site +validation, and diagnostics. Public factory declarations live in +`mlir/include/mlir/Dialect/QCO/Transforms/Passes.h`. + +`mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h` aliases the +target-owned `SingleQubitBasis`; no decomposition-layer basis DTO remains. +`mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp` caches the selected +entangler decomposer, returns a prepared decomposition, and emits its +single-qubit factors without recomputing it. + +QCO qubits use linear static single assignment: each operation consumes a qubit +value and returns its successor. After mapping, `qco.static` operations declare +the assigned target sites. Since operation capabilities are homogeneous, +conformance validates these declarations once and does not trace each operand's +SSA lineage. + +Focused tests are in +`mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp`. +Typed basis coverage remains in +`mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp`. + +## Plan of Work + +The implementation deletes the duplicate gateset and basis-adapter files and +rewires Euler and Weyl synthesis directly to the target-owned basis type. It +also removes the obsolete generated pass, registration, CLI option, `QCOProgram` +method, binding, stub, and their menu tests. + +The new source retains the proven two-qubit gate-fusion scanner but separates +its uses. The fusion pass compares original and canonical entangler counts. +Target synthesis classifies actual mapped operations and rewrites only lowering +needs. Conformance performs an independent read-only walk and exact target +query. + +The final work updates the existing Unreleased changelog entry without an +upgrade note, formats changed sources, runs focused and repository-required +validation, obtains independent read-only reviews, and publishes signed, focused +commits. The implementation commit preserves Simon Hofmann's `Co-authored-by` +trailer; later metadata, lint, and coverage follow-ups carry the required +`Assisted-by` trailer. + +## Concrete Steps + +Run all commands from the repository root in the task's isolated worktree. +Configure with the repository's release preset and an LLVM/MLIR 22 installation: + + .agent/run.sh env MLIR_DIR= \ + cmake --preset release + +Build and run the three affected C++ suites: + + .agent/run.sh cmake --build build/release --target \ + mqt-core-mlir-unittest-target-synthesis \ + mqt-core-mlir-unittest-decomposition \ + mqt-core-mlir-unittests-compiler + + .agent/run.sh \ + build/release/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/\ + mqt-core-mlir-unittest-target-synthesis + + .agent/run.sh \ + build/release/mlir/unittests/Dialect/QCO/Transforms/Decomposition/\ + mqt-core-mlir-unittest-decomposition + + .agent/run.sh \ + build/release/mlir/unittests/Compiler/\ + mqt-core-mlir-unittests-compiler + +The current observed summaries are: + + [ PASSED ] 21 tests. + [ PASSED ] 199 tests. + [ PASSED ] 215 tests. + +Finish validation with changed-file hooks, the repository lint suite, and diff +checks: + + .agent/run.sh prek run --files + .agent/run.sh uvx nox -s lint + .agent/run.sh git diff --check + .agent/run.sh git status --short + +## Validation and Acceptance + +Acceptance requires the following observable behavior: + +- Two adjacent constant CX operations fuse away with equivalent unitary + behavior, while a three-CX SWAP form, an isolated SWAP, and a runtime RXX run + remain quantum-structurally unchanged before routing. +- Target-native synthesis removes unsupported ordinary SWAP and produces only + operations accepted by the selected target basis, preserving the complete + unitary. A target-native SWAP remains unchanged without requiring a synthesis + basis. +- Homogeneous operation capabilities apply in both operand orientations while + ordered site tuples retain calibration data only. Synthesis preserves complete + unitary behavior without an operand-reversal option, and a native `qco.pow` + shell is checked without separately rejecting its implementation body. +- An absent operation set succeeds without synthesis. An explicit incomplete + target succeeds for supported operations and reports “no usable synthesis + basis” only when an unsupported operation actually needs lowering. +- A supported runtime-parameterized gate remains unchanged. An unsupported + runtime gate reports that its unitary matrix is unavailable at compile time + without partially rewriting an earlier constant gate. +- Conformance accepts sparse target IDs, rejects operation-type, arity, + parameter-count, unknown-site, measurement, and dynamic-allocation mismatches, + and does not reconstruct per-operation site provenance. +- The duplicate enum/parser and all native-menu text, CLI, C++ program, Python, + and generated pass surfaces are absent. +- Focused tests, decomposition tests, compiler tests, changed-file checks, + repository lint, and `git diff --check` pass. +- Every commit is signed and carries the required `Assisted-by` trailer. The + implementation commit additionally carries + `Co-authored-by: Simon Hofmann `. + +## Idempotence and Recovery + +Configuration, builds, tests, and hooks are repeatable in the task-local build +and cache directories. A failed pass test uses a disposable module fixture. +Network fetch failures during initial configuration should be retried with +authorized network access; source behavior must not be weakened to accommodate +an environment boundary. + +Do not reset or clean the worktree. Inspect status and remove only +task-generated artifacts if necessary. Do not modify another task's worktree or +shared branch metadata. External publication requires separate authorization; +draft PR #1998 was published only after that authorization was granted. + +## Artifacts and Notes + +Initial development used CT-01 commit +`b6eb95521cb76224c137496f113f38b9ce295854`. The SYN implementation patch is +restacked directly onto the squash-merged CT-01 base +`f775395a25fddba0a3b54996416d1311bc6ebe71`. The stable patch ID before and after +the restack is `fb73dd9d29254dfca2edf68df7eb9bf983284cc4`, proving that no SYN +implementation content changed during the history rewrite. Changelog, +changed-file lint, and coverage follow-ups remain separate focused commits. + +Relevant #1969 source commits are `3be6d8e43` for target-native naming and +`1aa4c975b` for progressive post-routing synthesis and SWAP-removal coverage, +both authored by Simon Hofmann. + +Independent read-only review initially blocked the candidate on three issues: +single-orientation symmetric-entangler site tuples, a SWAP capability override, +and nested `qco.pow` body checking. The exact remediated source rebuilt +successfully, all sixteen focused tests passed, and the reviewer reported that +all three findings were closed with no new code blockers. The reviewer also +reran `git diff --check` against the initial development base. + +Pre-restack local validation rebuilt `mqt-core-mlir-unittest-target-synthesis`, +`mqt-core-mlir-unittest-decomposition`, and `mqt-core-mlir-unittests-compiler`, +then observed 16, 199, and 215 passing tests, respectively. Changed-file `prek` +hooks and `uvx nox -s lint` passed. The first final lint attempt encountered +sandbox DNS failure; the authorized network retry reused the worktree-local +environment and passed every hook. The four metadata messages for deliberately +deleted native-menu files were non-fatal and every reported hook passed. + +Post-restack validation rebuilt those three binaries from final CT-01 and again +observed 16, 199, and 215 passing tests. It also built +`MLIRQCOTransforms_verify_interface_header_sets`, +`MQTCompilerPipeline_verify_interface_header_sets`, +`MQTCompilerTarget_verify_interface_header_sets`, `mqt-cc`, and +`mqt-cc_verify_interface_header_sets`. The resulting CLI help contains neither +`--native-gates` nor `fuse-two-qubit-unitary-runs`. Stub generation built the +bindings with LLVM/MLIR 22.1.3 and produced no tracked diff. Five focused +`clang-tidy` invocations passed; the only diagnostics are three inherited +warnings on unchanged `Weyl.h` lines 167 and 171. Changed-file hooks, full +repository lint, stale-surface search outside historical plans, both worktree +and committed `git diff --check`, and the final status audit pass. + +The coverage follow-up added regressions for interleaved two-qubit gate fusion, +constant single-qubit target lowering, and the former site tracer. The latest +simplification deletes tracer-only control-flow tests and adds direct +static-site and dynamic-allocation coverage alongside structural CZ emission and +failure-atomic lowering. + +After MAP-01 merged as `c9e0c0ca5`, the four SYN commits were rebased onto that +commit. The integration range-diff removes only a compiler-target link already +provided by MAP-01, retains MAP-01's canonical undirected coupling input in the +compiler API test, and keeps both PR links in the changelog. The lint and +coverage follow-up patches remain identical. Fresh release builds pass 21 target +synthesis, 199 decomposition, and 215 compiler tests, including the focused +`QCOProgramOptimizationAPIs` test. + +## Interfaces and Dependencies + +`CompilerTarget` remains dependency-light and does not depend on transform +libraries. `MLIRQCOTransforms` publicly links `MQTCompilerTarget` because its +public decomposition headers use compiler-target basis types. The public, +independently constructible factories are: + + std::unique_ptr createFuseTwoQubitGates(); + std::unique_ptr + createTargetNativeSynthesis(const CompilerTarget& target); + std::unique_ptr + createVerifyTargetConformance(const CompilerTarget& target); + +There is no textual target-native pass, synthetic target, native-gate menu, or +parallel capability model. + +Revision note (2026-08-03): Updated the completed plan after PR publication, CI +feedback, and the MAP-01 merge. This records the focused coverage regressions, +current test and coverage evidence, multi-commit publication state, the +integration conflict resolutions, and the fact that Simon Hofmann's authorship +belongs to the implementation commit rather than every follow-up. diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f93a0527f..b31838c8ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,9 +32,9 @@ releases may include breaking changes. [**@li-mingbao**], [**@DRovara**], [**@MatthiasReumann**]) - ✨ Add decision diagram-based construction and simulation of static unitary QCO functions ([#1915]) ([**@simon1hofmann**]) -- ✨ Add a `fuse-two-qubit-unitary-runs` pass for fusing compile-time two-qubit - unitary windows via Weyl/KAK resynthesis ([#1865], [#1961]) - ([**@simon1hofmann**], [**@burgholzer**]) +- ✨ Add target-independent two-qubit gate fusion, target-native post-routing + synthesis, and operation-capability and static-site conformance ([#1865], + [#1961], [#1998]) ([**@simon1hofmann**], [**@burgholzer**]) - ✨ Add a `decompose-multi-controlled` pass for decomposing controlled X, Z, RCCX, and constant-angle phase gates with a configurable `min-controls` threshold ([#1810]) ([**@simon1hofmann**]) @@ -68,7 +68,7 @@ releases may include breaking changes. [#1676], [#1706], [#1776], [#1836], [#1934]) ([**@denialhaag**], [**@burgholzer**]) - ✨ Add a `place-and-route` pass for mapping scalar- and tensor-allocated - circuits to compiler-target topologies while preserving provider site IDs and + circuits to compiler-target topologies while preserving target site IDs and materializing routing workspace on demand ([#1537], [#1547], [#1568], [#1581], [#1583], [#1588], [#1600], [#1664], [#1709], [#1716], [#1748], [#1805], [#1870], [#1904], [#1911], [#1951], [#1997]) ([**@MatthiasReumann**], @@ -710,6 +710,7 @@ for previous changelogs._ +[#1998]: https://github.com/munich-quantum-toolkit/core/pull/1998 [#1997]: https://github.com/munich-quantum-toolkit/core/pull/1997 [#1994]: https://github.com/munich-quantum-toolkit/core/pull/1994 [#1992]: https://github.com/munich-quantum-toolkit/core/pull/1992 diff --git a/bindings/mlir/register_mlir.cpp b/bindings/mlir/register_mlir.cpp index 0c096e0b80..e7478472b9 100644 --- a/bindings/mlir/register_mlir.cpp +++ b/bindings/mlir/register_mlir.cpp @@ -394,12 +394,6 @@ operations.)pb"); &mlir::QCOProgram::fuseSingleQubitUnitaryRuns>::call, nb::kw_only(), "basis"_a = "zyz", "Fuse single-qubit unitary runs into the chosen decomposition basis.") - .def("fuse_two_qubit_unitary_runs", - &BooleanMemberAdapter< - &mlir::QCOProgram::fuseTwoQubitUnitaryRuns>::call, - nb::kw_only(), "native_gates"_a, - "Lower unitaries to a non-empty native gate menu via two-qubit run " - "fusion.") .def("unroll_quantum_loops", &BooleanMemberAdapter<&mlir::QCOProgram::unrollQuantumLoops>::call, nb::kw_only(), "unroll_factor"_a = -1, diff --git a/docs/qdmi/sc_device.md b/docs/qdmi/sc_device.md index 833657056a..2186c99135 100644 --- a/docs/qdmi/sc_device.md +++ b/docs/qdmi/sc_device.md @@ -86,12 +86,12 @@ and `mqt.sc.iqm.emerald`. The snapshots were derived from authenticated IQM architecture and calibration data retrieved on 2 August 2026 through [QDMI-on-IQM](https://github.com/iqm-finland/QDMI-on-IQM). Site names and -topology came from each static architecture, operation loci from its default -dynamic architecture, and available T1, T2, and per-locus fidelities from its -default calibration-set quality metrics. Coherence times were rounded to the -nearest nanosecond and stored as integral values using unit `us` and scale -factor `0.001`. They are historical compiler-facing snapshots, not claims about -current hardware calibration. +topology came from each static architecture, operation sites from its default +dynamic architecture, and available T1, T2, and fidelities for individual site +tuples from its default calibration-set quality metrics. Coherence times were +rounded to the nearest nanosecond and stored as integral values using unit `us` +and scale factor `0.001`. They are historical compiler-facing snapshots, not +claims about current hardware calibration. The IQM interface did not report operation durations, so the files intentionally omit them. diff --git a/mlir/include/mlir/Compiler/Programs.h b/mlir/include/mlir/Compiler/Programs.h index ac62a967c4..1366555197 100644 --- a/mlir/include/mlir/Compiler/Programs.h +++ b/mlir/include/mlir/Compiler/Programs.h @@ -187,9 +187,6 @@ class QCOProgram final : public Program { /// Fuse single-qubit unitary runs into the selected Euler basis. [[nodiscard]] bool fuseSingleQubitUnitaryRuns(std::string_view basis = "zyz"); - /// Lower unitaries to a non-empty native gate menu via two-qubit run fusion. - [[nodiscard]] bool fuseTwoQubitUnitaryRuns(std::string_view nativeGates); - /// Unroll loops containing quantum operations. [[nodiscard]] bool unrollQuantumLoops(int64_t factor = -1); diff --git a/mlir/include/mlir/Compiler/Target.h b/mlir/include/mlir/Compiler/Target.h index 9f48d795d3..b39bf9f668 100644 --- a/mlir/include/mlir/Compiler/Target.h +++ b/mlir/include/mlir/Compiler/Target.h @@ -27,10 +27,9 @@ namespace mlir { class Operation; /** - * @brief Immutable, provider-independent description of an MLIR compiler - * target. + * @brief Immutable description of an MLIR compiler target. * - * @details Hardware sites retain their provider-defined nonnegative i64 + * @details Hardware sites retain their target-defined nonnegative i64 * identifiers. Routing algorithms use dense zero-based vertices in site order. * An absent topology means all-to-all connectivity. An absent operation set * means that every operation is native; a present empty set means that no @@ -53,7 +52,7 @@ class CompilerTarget { public: DurationUnit(std::string unit, double scaleFactor); - /// Return the provider-defined duration unit. + /// Return the target's duration unit. [[nodiscard]] llvm::StringRef unit() const noexcept; /// Return the positive finite multiplier for raw timing values. @@ -65,7 +64,7 @@ class CompilerTarget { }; /** - * @brief A hardware site and its optional provider metadata. + * @brief A hardware site and its optional target metadata. */ class Site { public: @@ -73,10 +72,10 @@ class CompilerTarget { std::optional t1 = std::nullopt, std::optional t2 = std::nullopt); - /// Return the provider-defined nonnegative site identifier. + /// Return the target-defined nonnegative site identifier. [[nodiscard]] SiteId id() const noexcept; - /// Return the provider-defined site name, if available. + /// Return the reported site name, if available. [[nodiscard]] std::optional name() const noexcept; /// Return the raw T1 coherence time, if available. @@ -93,15 +92,15 @@ class CompilerTarget { }; /** - * @brief An ordered hardware locus and its optional calibration data. + * @brief Calibration data for an ordered tuple of hardware sites. */ - class OperationLocus { + class SiteTuple { public: - explicit OperationLocus(std::vector sites, - std::optional duration = std::nullopt, - std::optional fidelity = std::nullopt); + explicit SiteTuple(std::vector sites, + std::optional duration = std::nullopt, + std::optional fidelity = std::nullopt); - /// Return the ordered provider site identifiers. + /// Return the ordered target site identifiers. [[nodiscard]] llvm::ArrayRef sites() const noexcept; /// Return the raw operation duration, if available. @@ -117,22 +116,22 @@ class CompilerTarget { }; /** - * @brief An operation capability reported by a target provider. + * @brief An operation capability described by a target. * - * @details The provider name is retained verbatim while @ref canonicalName - * contains its normalized compiler spelling. An absent locus set means the - * operation applies to every valid ordered tuple of its arity; a present - * empty set supports no locus. + * @details The reported name is retained verbatim while + * @ref canonicalName contains its normalized compiler spelling. Operations + * are available throughout the target; site tuples carry optional + * site-specific calibration data only. */ class Operation { public: - Operation(std::string providerName, size_t numQubits, size_t numParameters, - std::optional> loci = std::nullopt, + Operation(std::string name, size_t numQubits, size_t numParameters, + std::vector siteTuples = {}, std::optional duration = std::nullopt, std::optional fidelity = std::nullopt); - /// Return the exact operation name reported by the provider. - [[nodiscard]] llvm::StringRef providerName() const noexcept; + /// Return the exact reported operation name. + [[nodiscard]] llvm::StringRef name() const noexcept; /// Return the canonical lower-case compiler operation name. [[nodiscard]] llvm::StringRef canonicalName() const noexcept; @@ -143,11 +142,8 @@ class CompilerTarget { /// Return the number of real-valued operation parameters. [[nodiscard]] size_t numParameters() const noexcept; - /// Return whether this operation is available at every valid locus. - [[nodiscard]] bool hasGlobalLoci() const noexcept; - - /// Return the explicitly supported ordered loci. - [[nodiscard]] llvm::ArrayRef loci() const noexcept; + /// Return ordered site-specific calibration data. + [[nodiscard]] llvm::ArrayRef siteTuples() const noexcept; /// Return the raw default operation duration, if available. [[nodiscard]] std::optional duration() const noexcept; @@ -155,15 +151,12 @@ class CompilerTarget { /// Return the default operation fidelity, if available. [[nodiscard]] std::optional fidelity() const noexcept; - /// Return whether this capability supports an ordered hardware locus. - [[nodiscard]] bool supports(llvm::ArrayRef locus) const; - private: - std::string providerName_; + std::string name_; std::string canonicalName_; size_t numQubits_; size_t numParameters_; - std::optional> loci_; + std::vector siteTuples_; std::optional duration_; std::optional fidelity_; }; @@ -192,7 +185,15 @@ class CompilerTarget { /** * @brief Recognized globally usable single-qubit synthesis basis. */ - enum class SingleQubitBasis : uint8_t { U, ZSXX, R, XZX, XYX, ZYZ }; + enum class SingleQubitBasis : uint8_t { + U, ///< `U(theta, phi, lambda)`. + ZSXX, ///< `RZ` / `SX` / `X` synthesis via a ZYZ decomposition. + R, ///< XYX synthesis expressed with `R(theta, phi)`. + XZX, ///< `RX(phi) * RZ(theta) * RX(lambda)`. + XYX, ///< `RX(phi) * RY(theta) * RX(lambda)`. + ZYZ, ///< `RZ(phi) * RY(theta) * RZ(lambda)`. + ZXZ, ///< `RZ(phi) * RX(theta) * RZ(lambda)`. + }; /** * @brief One single-qubit basis and entangler usable across the target. @@ -224,7 +225,7 @@ class CompilerTarget { std::optional durationUnit = std::nullopt); /** - * @brief Construct an unnamed target from detailed provider sites. + * @brief Construct an unnamed target from detailed sites. */ explicit CompilerTarget( std::vector sites, @@ -233,7 +234,7 @@ class CompilerTarget { std::optional durationUnit = std::nullopt); /** - * @brief Construct a named target from detailed provider sites. + * @brief Construct a named target from detailed sites. */ CompilerTarget( std::string name, std::vector sites, @@ -246,7 +247,7 @@ class CompilerTarget { CompilerTarget& operator=(const CompilerTarget&) noexcept = default; ~CompilerTarget() = default; - /// Return the target/device name, if provided. + /// Return the target name, if provided. [[nodiscard]] std::optional name() const noexcept; /// Return the unit shared by all raw timing metadata, if provided. @@ -259,20 +260,20 @@ class CompilerTarget { /// Return detailed sites in dense compiler-vertex order. [[nodiscard]] llvm::ArrayRef sites() const noexcept; - /// Return provider site identifiers in dense compiler-vertex order. + /// Return target site identifiers in dense compiler-vertex order. [[nodiscard]] llvm::ArrayRef siteIds() const noexcept; - /// Return the dense compiler vertex for a provider site identifier. + /// Return the dense compiler vertex for a target site identifier. [[nodiscard]] std::optional vertexForSite(SiteId site) const noexcept; - /// Return the provider site identifier for a dense compiler vertex. + /// Return the target site identifier for a dense compiler vertex. [[nodiscard]] SiteId siteForVertex(size_t vertex) const; /// Return whether the target contains an explicit coupling topology. [[nodiscard]] bool hasExplicitTopology() const noexcept; /** - * @brief Return sorted canonical undirected couplings in provider site IDs. + * @brief Return sorted canonical undirected couplings in target site IDs. */ [[nodiscard]] llvm::ArrayRef couplings() const noexcept; @@ -296,32 +297,24 @@ class CompilerTarget { /// Return whether the target contains an explicit operation set. [[nodiscard]] bool hasExplicitOperations() const noexcept; - /// Return provider operation capabilities in provider order. + /// Return operation capabilities in reported order. [[nodiscard]] llvm::ArrayRef operations() const noexcept; /** - * @brief Return whether a canonical/provider operation is supported at an - * ordered hardware locus. + * @brief Return whether an operation capability is supported by the target. */ [[nodiscard]] bool - supportsOperation(llvm::StringRef name, llvm::ArrayRef locus, + supportsOperation(llvm::StringRef name, size_t numQubits, std::optional numParameters = std::nullopt) const; - /** - * @brief Return whether a QCO operation is supported at an ordered locus. - */ - [[nodiscard]] bool supports(::mlir::Operation* operation, - llvm::ArrayRef locus) const; + /// Return whether a QCO operation is supported by the target. + [[nodiscard]] bool supports(::mlir::Operation* operation) const; - /** - * @brief Return whether a recognized gate is supported at an ordered locus. - */ - [[nodiscard]] bool supports(GateKind gate, - llvm::ArrayRef locus) const; + /// Return whether a recognized gate is supported by the target. + [[nodiscard]] bool supports(GateKind gate) const; - /// Return recognized gates that are usable across the complete target. - [[nodiscard]] llvm::ArrayRef - globallySupportedGates() const noexcept; + /// Return the recognized gates supported by the target. + [[nodiscard]] llvm::ArrayRef supportedGates() const noexcept; /// Return one complete globally usable synthesis basis, if available. [[nodiscard]] std::optional synthesisBasis() const noexcept; diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h index bec84c0447..32449fab04 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h @@ -10,6 +10,7 @@ #pragma once +#include "mlir/Compiler/Target.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" #include @@ -17,7 +18,6 @@ #include #include -#include #include namespace mlir { @@ -26,18 +26,7 @@ class RewritePatternSet; namespace mlir::qco::decomposition { -/** - * @brief Native gate sets for single-qubit Euler synthesis. - */ -enum class EulerBasis : std::uint8_t { - ZYZ = 0, ///< `RZ(phi) * RY(theta) * RZ(lambda)`. - ZXZ = 1, ///< `RZ(phi) * RX(theta) * RZ(lambda)`. - XZX = 2, ///< `RX(phi) * RZ(theta) * RX(lambda)`. - XYX = 3, ///< `RX(phi) * RY(theta) * RX(lambda)`. - U = 4, ///< `U(theta, phi, lambda)`. - ZSXX = 5, ///< `RZ` / `SX` / `X` synthesis via ZYZ decomposition. - R = 6, ///< `R(.,0) * R(.,pi/2) * R(.,0)` (XYX with `Rx`/`Ry` as `R`). -}; +using SingleQubitBasis = CompilerTarget::SingleQubitBasis; /** * @brief Parses a basis name (e.g. `zyz`, `zsxx`; case-insensitive). @@ -45,7 +34,8 @@ enum class EulerBasis : std::uint8_t { * @param basis The basis name. * @return The parsed basis, or `std::nullopt` if unrecognized. */ -[[nodiscard]] std::optional parseEulerBasis(StringRef basis); +[[nodiscard]] std::optional +parseSingleQubitBasis(StringRef basis); /** * @brief Euler angles `(theta, phi, lambda)` and global phase for a 2x2 @@ -53,7 +43,7 @@ enum class EulerBasis : std::uint8_t { * * The decomposition obeys `matrix == e^{i*phase} * K(phi) * A(theta) * * K(lambda)` where `(K, A)` are the rotation axes of the chosen @ref - * EulerBasis. + * SingleQubitBasis. */ struct EulerAngles { double theta = 0.0; ///< Middle rotation angle. @@ -77,11 +67,11 @@ struct SynthesizedUnitary1Q { * @brief Extracts `(theta, phi, lambda, phase)` of @p matrix in @p basis. * * @param matrix The single-qubit unitary to decompose. - * @param basis The target Euler basis. + * @param basis The single-qubit synthesis basis. * @return The extracted Euler angles and global phase. */ [[nodiscard]] EulerAngles anglesFromUnitary(const Matrix2x2& matrix, - EulerBasis basis); + SingleQubitBasis basis); /** * @brief Synthesizes a composed single-qubit unitary as gates in @p basis. @@ -96,14 +86,14 @@ struct SynthesizedUnitary1Q { * @param composed Composed unitary to synthesize. * @param runSize Number of gates in the run. * @param hasNonBasisGate Whether the run contains a gate outside @p basis. - * @param basis The target Euler basis. + * @param basis The single-qubit synthesis basis. * @return The synthesized qubit and correction, or `std::nullopt` if synthesis * is skipped. */ [[nodiscard]] std::optional synthesizeUnitary1QEuler(OpBuilder& builder, Location loc, Value qubit, const Matrix2x2& composed, std::size_t runSize, - bool hasNonBasisGate, EulerBasis basis); + bool hasNonBasisGate, SingleQubitBasis basis); /** * @brief Materializes one accumulated phase correction when needed. @@ -122,7 +112,7 @@ void emitGPhaseIfNeeded(OpBuilder& builder, Location loc, double phase); * bodies are left untouched. */ void populateFuseSingleQubitUnitaryRunsPatterns( - RewritePatternSet& patterns, EulerBasis basis, + RewritePatternSet& patterns, SingleQubitBasis basis, bool skipControlledBodies = false); } // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeGateset.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeGateset.h deleted file mode 100644 index bb48a23383..0000000000 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeGateset.h +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#pragma once - -#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" -#include "mlir/Dialect/QCO/Utils/Matrix.h" - -#include - -#include -#include - -namespace mlir { -class Operation; -} // namespace mlir - -namespace mlir::qco::decomposition { - -/** - * @brief Gate token in a comma-separated native gateset (e.g. `"u,cx"`). - */ -enum class NativeGateKind : std::uint8_t { - U, - X, - SX, - RZ, - RX, - RY, - R, - RXX, - RYY, - RZX, - RZZ, - ISWAP, - CZ, - CX, - ECR, -}; - -struct TwoQubitNativeDecomposition; - -/** - * @brief Resolved native gateset for two-qubit Weyl synthesis. - * - * Use @ref parse to obtain a gateset with @p eulerBasis and @p entangler - * resolved from @p gates. When several entanglers appear, preference is - * **RXX > RYY > RZX > RZZ > iSWAP > CZ > CX > ECR** (alphabetic among two-qubit - * rotations; then discrete named gates; ECR last). Weyl synthesis emits - * `rxx`/`ryy`/`rzx`/`rzz` at a fixed angle of π/2. - */ -struct NativeGateset { - llvm::DenseSet gates; - std::optional eulerBasis; - std::optional entangler; - - /** - * @brief Parses a comma-separated native gateset (e.g. `"u,cx"`). - * - * @param nativeGates Comma-separated gate tokens. - * @return Parsed gateset, or `std::nullopt` when the gateset is unsupported. - */ - [[nodiscard]] static std::optional - parse(StringRef nativeGates); - - /** - * @brief Basis decomposition of @p target under this gateset, if supported. - */ - [[nodiscard]] std::optional - decomposeTarget(const Matrix4x4& target) const; - - /** - * @brief Whether @p op is already on this native gateset. - * - * `qco.barrier` and `qco.gphase` are always allowed. Single-qubit primitives - * are checked against @p gates. Single-control, single-target `qco.ctrl` - * shells with an `X`/`Z` body are accepted when `cx`/`cz` is present. - * `qco.rxx`, `qco.ryy`, `qco.rzx`, and `qco.rzz` are accepted when the - * corresponding token is present, including runtime-parameterized forms. - * Bare `qco.iswap` and `qco.ecr` are accepted when the corresponding token is - * present. All other ops are rejected. - */ - [[nodiscard]] bool allowsOp(Operation* op) const; -}; - -} // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h index 16f885e7b5..a8efbc87fb 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h @@ -10,6 +10,7 @@ #pragma once +#include "mlir/Compiler/Target.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" #include @@ -343,8 +344,6 @@ decomposeTwoQubitWithBasis( double basisFidelity = 1.0, std::optional numBasisUses = std::nullopt); -struct NativeGateset; - /** @brief Result of two-qubit synthesis, including its accumulated phase. */ struct SynthesizedUnitary2Q { Value qubit0; @@ -352,10 +351,20 @@ struct SynthesizedUnitary2Q { double globalPhase = 0.0; }; -/** @brief Synthesizes a two-qubit unitary as gates allowed by @p spec. */ -[[nodiscard]] FailureOr -synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, Value qubit0, - Value qubit1, const Matrix4x4& target, - const NativeGateset& spec); +/** + * @brief Decomposes a two-qubit unitary using @p entangler. + */ +[[nodiscard]] TwoQubitNativeDecomposition +decomposeUnitary2QWeyl(const Matrix4x4& target, + CompilerTarget::GateKind entangler); + +/** + * @brief Emits a prepared two-qubit decomposition in the selected target + * @p basis. + */ +[[nodiscard]] SynthesizedUnitary2Q +emitUnitary2QWeyl(OpBuilder& builder, Location loc, Value qubit0, Value qubit1, + const TwoQubitNativeDecomposition& decomposition, + CompilerTarget::SynthesisBasis basis); } // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h index 7444438a88..de6e78952d 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h @@ -14,6 +14,12 @@ #include #include +#include + +namespace mlir { +class CompilerTarget; +} // namespace mlir + namespace mlir::qco { #define GEN_PASS_DECL @@ -27,4 +33,21 @@ namespace mlir::qco { #define GEN_PASS_REGISTRATION #include "mlir/Dialect/QCO/Transforms/Passes.h.inc" // IWYU pragma: export +/** + * @brief Create target-independent two-qubit gate fusion. + */ +[[nodiscard]] std::unique_ptr createFuseTwoQubitGates(); + +/** + * @brief Create post-routing synthesis for one immutable compiler target. + */ +[[nodiscard]] std::unique_ptr +createTargetNativeSynthesis(const CompilerTarget& target); + +/** + * @brief Create the final mapped-operation conformance verifier. + */ +[[nodiscard]] std::unique_ptr +createVerifyTargetConformance(const CompilerTarget& target); + } // namespace mlir::qco diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index 544689e71d..20975689b5 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -67,80 +67,6 @@ def FuseSingleQubitUnitaryRuns "Target Euler basis (zyz, zxz, xzx, xyx, u, zsxx, r).">]; } -def FuseTwoQubitUnitaryRuns - : Pass<"fuse-two-qubit-unitary-runs", "mlir::ModuleOp"> { - let dependentDialects = ["mlir::qco::QCODialect", - "::mlir::arith::ArithDialect"]; - let summary = "Lower supported one- and two-qubit QCO unitaries to a " - "user-specified native gate menu."; - let description = [{ - Rewrites supported one- and two-qubit unitaries to match the comma-separated - `native-gates` gateset. Fusion and synthesis require unitary matrices that - are available at compile time. Already-native runtime-parameterized - operations remain valid, but an off-gateset operation without a compile-time - matrix cannot be synthesized and causes the pass to fail. - - `qco.barrier` and `qco.gphase` are preserved. A `qco.ctrl` is within the - two-qubit scope only when it has a single control and a single target. Every - entangler token listed in the menu is accepted as already native: bare - `qco.rxx`, `qco.ryy`, `qco.rzx`, `qco.rzz`, `qco.iswap`, or `qco.ecr`, or a - `qco.ctrl` with exactly one `qco.x` or `qco.z` body when `cx` or `cz` is - listed. Runtime-parameterized `rxx`/`ryy`/`rzx`/`rzz` operations remain - valid when their token is listed, although only operations with compile-time - matrices can participate in fusion or off-gateset synthesis. Entangler - preference (below) selects the Weyl synthesis basis only and does not - restrict which menu tokens count as native. Weyl synthesis emits - `rxx`/`ryy`/`rzx`/`rzz` at a fixed angle of π/2. Gates acting on more than - two qubits (e.g. multi-controlled `qco.ctrl`) are out of scope and left - untouched for a dedicated multi-controlled synthesis pass. - - The gateset is a comma-separated list of gate tokens (order not - significant) from which the pass resolves a single-qubit Euler basis - (generic `qco.u` when `u` is present; IBM-style surface gates when all of - `x`, `sx`, and `rz` are present; IQM-style `qco.r` when `r` is present; - or a supported rotation pair chosen from `rx`, `ry`, `rz`) plus one - two-qubit entangler from `rxx`, `ryy`, `rzx`, `rzz`, `iswap`, `cz`, `cx`, or - `ecr`. - - Recognised tokens: `u`, `x`, `sx`, `rz`, `rx`, `ry`, `r`, `rxx`, `ryy`, - `rzx`, `rzz`, `iswap`, `cz`, `cx`, `ecr`. An empty or whitespace-only - gateset is a no-op, which is the intended pipeline default when synthesis is - not needed. An unrecognised token causes the pass to fail. - - Example gatesets (each line is one illustrative gateset; pick one entangler - or list several if multiple are native): - - IBM: `x,sx,rz,cx`, `x,sx,rz,cz`, or `x,sx,rz,ecr` - - Generic single-qubit U: `u,rxx`, `u,ryy`, `u,rzx`, `u,rzz`, `u,iswap`, - `u,cz`, `u,cx`, or `u,ecr` - - IQM default: `r,cz` (or `r,cx` if CX is the native entangler) - - Rotation pair + entangler: `rx,rz,cx`, `rx,ry,cz`, `ry,rz,cx`, etc. - Supported pairs are exactly `rx`+`rz`, `rx`+`ry`, and `ry`+`rz`. - - When several entanglers are listed, synthesis preference is `rxx` > `ryy` > - `rzx` > `rzz` > `iswap` > `cz` > `cx` > `ecr` (alphabetic among two-qubit - rotations; then discrete named gates; ECR last). - - Stages: fuse single-qubit runs (reusing `fuse-single-qubit-unitary-runs`, - which also lowers lone off-gateset single-qubit gates); fuse two-qubit runs - and lower any remaining off-gateset two-qubit ops via Weyl synthesis; fuse - the single-qubit seams introduced by that synthesis. The pass fails if any - single- or two-qubit op remains off the native gateset afterwards; gates on - more than two qubits are left untouched and do not cause a failure. - - Lowering is deterministic: entangler preference is `rxx` > `ryy` > `rzx` > - `rzz` > `iswap` > `cz` > `cx` > `ecr`; single-qubit factors use the resolved - Euler basis; and two-qubit run replacement uses the minimal entangler count - from the synthesizer. - }]; - let options = [Option< - "nativeGates", "native-gates", "std::string", "\"\"", - "Comma-separated native gateset. Empty or whitespace-only is " - "a no-op. Tokens: u, x, sx, rz, rx, ry, r, rxx, ryy, rzx, rzz, iswap, " - "cz, cx, ecr. " - "Examples: x,sx,rz,cx; x,sx,rz,ecr; u,rxx; u,ryy; u,rzx; u,rzz; u,iswap; " - "u,cx; r,cz; rx,rz,cx; x,sx,rz,rzz.">]; -} - //===----------------------------------------------------------------------===// def QuantumLoopUnroll @@ -173,7 +99,7 @@ def MappingPass : Pass<"place-and-route", "mlir::ModuleOp"> { uses only the target's undirected coupling topology: arbitrary one-qubit operations pass through, and every two-qubit operation is routed to adjacent target vertices. Higher-arity unitaries must be decomposed before mapping. - Provider-defined site identifiers are retained in the resulting + Target-defined site identifiers are retained in the resulting `qco.static` operations. First, the pass assigns static qubits to the dynamically allocated ones by creating an initial dynamic-to-static diff --git a/mlir/lib/Compiler/Programs.cpp b/mlir/lib/Compiler/Programs.cpp index b1fae369cc..c8dd6f2c21 100644 --- a/mlir/lib/Compiler/Programs.cpp +++ b/mlir/lib/Compiler/Programs.cpp @@ -362,21 +362,6 @@ bool QCOProgram::fuseSingleQubitUnitaryRuns(const std::string_view basis) { "failed to fuse single-qubit unitary runs")); } -bool QCOProgram::fuseTwoQubitUnitaryRuns(const std::string_view nativeGates) { - if (StringRef(nativeGates).trim().empty()) { - mod().emitError("the native gate menu must not be empty"); - return false; - } - qco::FuseTwoQubitUnitaryRunsOptions options; - options.nativeGates = nativeGates; - return succeeded(runPasses( - mod(), - [&options](OpPassManager& pm) { - pm.addPass(qco::createFuseTwoQubitUnitaryRuns(options)); - }, - "failed to fuse two-qubit unitary runs")); -} - bool QCOProgram::unrollQuantumLoops(const int64_t factor) { qco::QuantumLoopUnrollOptions options; options.unrollFactor = factor; diff --git a/mlir/lib/Compiler/Target.cpp b/mlir/lib/Compiler/Target.cpp index 555231e3d1..eaaaa55c83 100644 --- a/mlir/lib/Compiler/Target.cpp +++ b/mlir/lib/Compiler/Target.cpp @@ -14,7 +14,6 @@ #include "mlir/Dialect/QCO/IR/QCOOps.h" #include -#include #include #include #include @@ -44,107 +43,61 @@ using GateKind = CompilerTarget::GateKind; using SiteId = CompilerTarget::SiteId; struct GateSpecification { - GateKind kind; + GateKind kind{}; llvm::StringLiteral name; - size_t numQubits; - size_t numParameters; - bool symmetric; + size_t numQubits{}; + size_t numParameters{}; }; constexpr std::array GATE_SPECIFICATIONS{ - GateSpecification{.kind = GateKind::U, - .name = "u", - .numQubits = 1, - .numParameters = 3, - .symmetric = false}, - GateSpecification{.kind = GateKind::X, - .name = "x", - .numQubits = 1, - .numParameters = 0, - .symmetric = false}, - GateSpecification{.kind = GateKind::SX, - .name = "sx", - .numQubits = 1, - .numParameters = 0, - .symmetric = false}, - GateSpecification{.kind = GateKind::RZ, - .name = "rz", - .numQubits = 1, - .numParameters = 1, - .symmetric = false}, - GateSpecification{.kind = GateKind::RX, - .name = "rx", - .numQubits = 1, - .numParameters = 1, - .symmetric = false}, - GateSpecification{.kind = GateKind::RY, - .name = "ry", - .numQubits = 1, - .numParameters = 1, - .symmetric = false}, - GateSpecification{.kind = GateKind::R, - .name = "r", - .numQubits = 1, - .numParameters = 2, - .symmetric = false}, + GateSpecification{ + .kind = GateKind::U, .name = "u", .numQubits = 1, .numParameters = 3}, + GateSpecification{ + .kind = GateKind::X, .name = "x", .numQubits = 1, .numParameters = 0}, + GateSpecification{ + .kind = GateKind::SX, .name = "sx", .numQubits = 1, .numParameters = 0}, + GateSpecification{ + .kind = GateKind::RZ, .name = "rz", .numQubits = 1, .numParameters = 1}, + GateSpecification{ + .kind = GateKind::RX, .name = "rx", .numQubits = 1, .numParameters = 1}, + GateSpecification{ + .kind = GateKind::RY, .name = "ry", .numQubits = 1, .numParameters = 1}, + GateSpecification{ + .kind = GateKind::R, .name = "r", .numQubits = 1, .numParameters = 2}, GateSpecification{.kind = GateKind::RXX, .name = "rxx", .numQubits = 2, - .numParameters = 1, - .symmetric = true}, + .numParameters = 1}, GateSpecification{.kind = GateKind::RYY, .name = "ryy", .numQubits = 2, - .numParameters = 1, - .symmetric = true}, + .numParameters = 1}, GateSpecification{.kind = GateKind::RZX, .name = "rzx", .numQubits = 2, - .numParameters = 1, - .symmetric = false}, + .numParameters = 1}, GateSpecification{.kind = GateKind::RZZ, .name = "rzz", .numQubits = 2, - .numParameters = 1, - .symmetric = true}, + .numParameters = 1}, GateSpecification{.kind = GateKind::ISWAP, .name = "iswap", .numQubits = 2, - .numParameters = 0, - .symmetric = true}, - GateSpecification{.kind = GateKind::CZ, - .name = "cz", - .numQubits = 2, - .numParameters = 0, - .symmetric = true}, - GateSpecification{.kind = GateKind::CX, - .name = "cx", - .numQubits = 2, - .numParameters = 0, - .symmetric = false}, + .numParameters = 0}, + GateSpecification{ + .kind = GateKind::CZ, .name = "cz", .numQubits = 2, .numParameters = 0}, + GateSpecification{ + .kind = GateKind::CX, .name = "cx", .numQubits = 2, .numParameters = 0}, GateSpecification{.kind = GateKind::ECR, .name = "ecr", .numQubits = 2, - .numParameters = 0, - .symmetric = false}, + .numParameters = 0}, }; } // namespace -[[nodiscard]] static const GateSpecification& -gateSpecification(const GateKind gate) { - // NOLINTNEXTLINE(readability-qualified-auto) - const auto found = - std::ranges::find(GATE_SPECIFICATIONS, gate, &GateSpecification::kind); - if (found == GATE_SPECIFICATIONS.end()) { - throw std::invalid_argument("Unknown compiler target gate kind"); - } - return *found; -} - -[[nodiscard]] static std::string -canonicalOperationName(const StringRef providerName) { - auto canonical = providerName.trim().lower(); +[[nodiscard]] static std::string canonicalOperationName(const StringRef name) { + auto canonical = name.trim().lower(); if (canonical == "prx") { canonical = "r"; } else if (canonical == "u3") { @@ -243,48 +196,45 @@ std::optional CompilerTarget::Site::t2() const noexcept { return t2_; } -CompilerTarget::OperationLocus::OperationLocus( - std::vector sites, const std::optional duration, - const std::optional fidelity) +CompilerTarget::SiteTuple::SiteTuple(std::vector sites, + const std::optional duration, + const std::optional fidelity) : sites_(std::move(sites)), duration_(duration), fidelity_(fidelity) { - DenseSet uniqueSites; - uniqueSites.reserve(sites_.size()); + std::set uniqueSites; for (const auto site : sites_) { if (site < 0) { throw std::invalid_argument( - "Compiler target operation locus contains a negative site ID"); + "Compiler target site tuple contains a negative site ID"); } if (!uniqueSites.insert(site).second) { throw std::invalid_argument( - "Compiler target operation locus contains a duplicate site"); + "Compiler target site tuple contains a duplicate site"); } } - validateFidelity(fidelity_, "Compiler target operation locus fidelity"); + validateFidelity(fidelity_, "Compiler target site-tuple fidelity"); } -ArrayRef CompilerTarget::OperationLocus::sites() const noexcept { +ArrayRef CompilerTarget::SiteTuple::sites() const noexcept { return sites_; } -std::optional -CompilerTarget::OperationLocus::duration() const noexcept { +std::optional CompilerTarget::SiteTuple::duration() const noexcept { return duration_; } -std::optional -CompilerTarget::OperationLocus::fidelity() const noexcept { +std::optional CompilerTarget::SiteTuple::fidelity() const noexcept { return fidelity_; } -CompilerTarget::Operation::Operation( - std::string providerName, const size_t numQubits, - const size_t numParameters, std::optional> loci, - const std::optional duration, - const std::optional fidelity) - : providerName_(std::move(providerName)), - canonicalName_(canonicalOperationName(providerName_)), +CompilerTarget::Operation::Operation(std::string name, const size_t numQubits, + const size_t numParameters, + std::vector siteTuples, + const std::optional duration, + const std::optional fidelity) + : name_(std::move(name)), canonicalName_(canonicalOperationName(name_)), numQubits_(numQubits), numParameters_(numParameters), - loci_(std::move(loci)), duration_(duration), fidelity_(fidelity) { + siteTuples_(std::move(siteTuples)), duration_(duration), + fidelity_(fidelity) { if (canonicalName_.empty()) { throw std::invalid_argument( "Compiler target operation name must not be empty"); @@ -295,26 +245,22 @@ CompilerTarget::Operation::Operation( } validateFidelity(fidelity_, "Compiler target operation fidelity"); - if (!loci_) { - return; - } - std::set> uniqueLoci; - for (const auto& locus : *loci_) { - if (locus.sites().size() != numQubits_) { + std::set> uniqueSiteCombinations; + for (const auto& siteTuple : siteTuples_) { + if (siteTuple.sites().size() != numQubits_) { throw std::invalid_argument( - "Compiler target operation locus does not match its declared arity"); + "Compiler target operation site tuple does not match its arity"); } - if (!uniqueLoci.emplace(locus.sites().begin(), locus.sites().end()) + if (!uniqueSiteCombinations + .emplace(siteTuple.sites().begin(), siteTuple.sites().end()) .second) { throw std::invalid_argument( - "Compiler target operation contains a duplicate locus"); + "Compiler target operation contains a duplicate site tuple"); } } } -StringRef CompilerTarget::Operation::providerName() const noexcept { - return providerName_; -} +StringRef CompilerTarget::Operation::name() const noexcept { return name_; } StringRef CompilerTarget::Operation::canonicalName() const noexcept { return canonicalName_; @@ -328,16 +274,9 @@ size_t CompilerTarget::Operation::numParameters() const noexcept { return numParameters_; } -bool CompilerTarget::Operation::hasGlobalLoci() const noexcept { - return !loci_; -} - -ArrayRef -CompilerTarget::Operation::loci() const noexcept { - if (!loci_) { - return {}; - } - return *loci_; +ArrayRef +CompilerTarget::Operation::siteTuples() const noexcept { + return siteTuples_; } std::optional CompilerTarget::Operation::duration() const noexcept { @@ -348,34 +287,15 @@ std::optional CompilerTarget::Operation::fidelity() const noexcept { return fidelity_; } -bool CompilerTarget::Operation::supports(const ArrayRef locus) const { - if (locus.size() != numQubits_) { - return false; - } - llvm::SmallDenseSet uniqueSites; - uniqueSites.reserve(locus.size()); - if (!llvm::all_of(locus, [&](const auto site) { - return site >= 0 && uniqueSites.insert(site).second; - })) { - return false; - } - return !loci_ || llvm::any_of(*loci_, [&](const auto& candidate) { - return std::ranges::equal(candidate.sites(), locus); - }); -} - struct CompilerTarget::Storage { Storage(std::optional targetName, std::vector targetSites, std::optional> targetCouplings, std::optional> targetOperations, std::optional targetDurationUnit); - [[nodiscard]] bool validLocus(ArrayRef locus) const; [[nodiscard]] bool - supportsOperation(StringRef name, ArrayRef locus, + supportsOperation(StringRef name, size_t numQubits, std::optional numParameters) const; - [[nodiscard]] bool gateIsGloballySupported(GateKind gate) const; - [[nodiscard]] bool hasGlobalGate(GateKind gate) const; [[nodiscard]] std::optional resolveSynthesisBasis() const; std::optional name; @@ -389,7 +309,7 @@ struct CompilerTarget::Storage { size_t maximumDegree = 0; std::optional> operations; llvm::StringMap> capabilities; - SmallVector globalGates; + SmallVector supportedGates; std::optional basis; }; @@ -484,12 +404,16 @@ CompilerTarget::Storage::Storage( if (operations) { for (const auto [index, operation] : llvm::enumerate(*operations)) { - for (const auto& locus : operation.loci()) { - if (llvm::any_of(locus.sites(), [&](const auto site) { + if (operation.numQubits() > sites.size()) { + throw std::invalid_argument( + "Compiler target operation arity exceeds its site count"); + } + for (const auto& siteTuple : operation.siteTuples()) { + if (llvm::any_of(siteTuple.sites(), [&](const auto site) { return !siteToVertex.contains(site); })) { - throw std::invalid_argument( - "Compiler target operation locus references an unknown site"); + throw std::invalid_argument("Compiler target operation site tuple " + "references an unknown site"); } } capabilities[operation.canonicalName()].emplace_back(index); @@ -502,8 +426,8 @@ CompilerTarget::Storage::Storage( const auto hasOperationTiming = operations && llvm::any_of(*operations, [](const auto& operation) { return operation.duration().has_value() || - llvm::any_of(operation.loci(), [](const auto& locus) { - return locus.duration().has_value(); + llvm::any_of(operation.siteTuples(), [](const auto& siteTuple) { + return siteTuple.duration().has_value(); }); }); if ((hasSiteTiming || hasOperationTiming) && !durationUnit) { @@ -512,29 +436,19 @@ CompilerTarget::Storage::Storage( } for (const auto& specification : GATE_SPECIFICATIONS) { - if (gateIsGloballySupported(specification.kind)) { - globalGates.emplace_back(specification.kind); + if (supportsOperation(specification.name, specification.numQubits, + specification.numParameters)) { + supportedGates.emplace_back(specification.kind); } } basis = resolveSynthesisBasis(); } -bool CompilerTarget::Storage::validLocus(const ArrayRef locus) const { - llvm::SmallDenseSet uniqueSites; - uniqueSites.reserve(locus.size()); - return llvm::all_of(locus, [&](const auto site) { - return siteToVertex.contains(site) && uniqueSites.insert(site).second; - }); -} - bool CompilerTarget::Storage::supportsOperation( - const StringRef operationName, const ArrayRef locus, + const StringRef operationName, const size_t numQubits, const std::optional numParameters) const { - if (!validLocus(locus)) { - return false; - } const auto canonical = canonicalOperationName(operationName); - if (canonical.empty() || locus.empty()) { + if (canonical.empty() || numQubits == 0 || numQubits > sites.size()) { return false; } if (!operations) { @@ -546,70 +460,29 @@ bool CompilerTarget::Storage::supportsOperation( } return llvm::any_of(found->second, [&](const auto index) { const auto& operation = (*operations)[index]; - return (!numParameters || operation.numParameters() == *numParameters) && - operation.supports(locus); + return operation.numQubits() == numQubits && + (!numParameters || operation.numParameters() == *numParameters); }); } -bool CompilerTarget::Storage::gateIsGloballySupported( - const GateKind gate) const { - const auto& specification = gateSpecification(gate); - if (specification.numQubits == 1) { - return llvm::all_of(siteIds, [&](const auto site) { - const std::array locus{site}; - return supportsOperation(specification.name, locus, - specification.numParameters); - }); - } - if (sites.size() < 2) { - return false; - } - - const auto supportedOnEdge = [&](const SiteId first, const SiteId second) { - const std::array forward{first, second}; - const std::array reverse{second, first}; - const auto supportsForward = supportsOperation(specification.name, forward, - specification.numParameters); - const auto supportsReverse = supportsOperation(specification.name, reverse, - specification.numParameters); - return specification.symmetric ? supportsForward || supportsReverse - : supportsForward && supportsReverse; - }; - - if (couplings) { - return llvm::all_of(*couplings, [&](const auto& coupling) { - return supportedOnEdge(coupling.first, coupling.second); - }); - } - for (size_t first = 0; first < siteIds.size(); ++first) { - for (size_t second = first + 1; second < siteIds.size(); ++second) { - if (!supportedOnEdge(siteIds[first], siteIds[second])) { - return false; - } - } - } - return true; -} - -bool CompilerTarget::Storage::hasGlobalGate(const GateKind gate) const { - return llvm::is_contained(globalGates, gate); -} - std::optional CompilerTarget::Storage::resolveSynthesisBasis() const { + const auto supports = [&](const GateKind gate) { + return llvm::is_contained(supportedGates, gate); + }; std::optional singleQubit; - if (hasGlobalGate(GateKind::U)) { + if (supports(GateKind::U)) { singleQubit = SingleQubitBasis::U; - } else if (hasGlobalGate(GateKind::X) && hasGlobalGate(GateKind::SX) && - hasGlobalGate(GateKind::RZ)) { + } else if (supports(GateKind::X) && supports(GateKind::SX) && + supports(GateKind::RZ)) { singleQubit = SingleQubitBasis::ZSXX; - } else if (hasGlobalGate(GateKind::R)) { + } else if (supports(GateKind::R)) { singleQubit = SingleQubitBasis::R; - } else if (hasGlobalGate(GateKind::RX) && hasGlobalGate(GateKind::RZ)) { + } else if (supports(GateKind::RX) && supports(GateKind::RZ)) { singleQubit = SingleQubitBasis::XZX; - } else if (hasGlobalGate(GateKind::RX) && hasGlobalGate(GateKind::RY)) { + } else if (supports(GateKind::RX) && supports(GateKind::RY)) { singleQubit = SingleQubitBasis::XYX; - } else if (hasGlobalGate(GateKind::RY) && hasGlobalGate(GateKind::RZ)) { + } else if (supports(GateKind::RY) && supports(GateKind::RZ)) { singleQubit = SingleQubitBasis::ZYZ; } @@ -620,7 +493,7 @@ CompilerTarget::Storage::resolveSynthesisBasis() const { // NOLINTNEXTLINE(readability-qualified-auto) const auto entangler = std::ranges::find_if(entanglerPreference, [&](const auto candidate) { - return hasGlobalGate(candidate); + return supports(candidate); }); if (!singleQubit || entangler == entanglerPreference.end()) { return std::nullopt; @@ -792,21 +665,17 @@ CompilerTarget::operations() const noexcept { } bool CompilerTarget::supportsOperation( - const StringRef operationName, const ArrayRef locus, + const StringRef operationName, const size_t numQubits, const std::optional numParameters) const { - return storage_->supportsOperation(operationName, locus, numParameters); + return storage_->supportsOperation(operationName, numQubits, numParameters); } -bool CompilerTarget::supports(::mlir::Operation* operation, - const ArrayRef locus) const { - if (operation == nullptr || !storage_->validLocus(locus)) { +bool CompilerTarget::supports(::mlir::Operation* operation) const { + if (operation == nullptr) { return false; } if (auto unitary = dyn_cast(operation)) { - if (unitary.getNumQubits() != locus.size()) { - return false; - } if (isa(operation)) { return true; } @@ -816,35 +685,31 @@ bool CompilerTarget::supports(::mlir::Operation* operation, controlled.getNumBodyUnitaries() == 1) { auto* const body = controlled.getBodyUnitary(0).getOperation(); if (isa(body)) { - return storage_->supportsOperation("cx", locus, 0); + return storage_->supportsOperation("cx", 2, 0); } if (isa(body)) { - return storage_->supportsOperation("cz", locus, 0); + return storage_->supportsOperation("cz", 2, 0); } } - return storage_->supportsOperation(unitary.getBaseSymbol(), locus, + return storage_->supportsOperation(unitary.getBaseSymbol(), + unitary.getNumQubits(), unitary.getNumParams()); } if (isa(operation)) { - return locus.size() == 1 && - storage_->supportsOperation("measure", locus, 0); + return storage_->supportsOperation("measure", 1, 0); } if (isa(operation)) { - return locus.size() == 1 && storage_->supportsOperation("reset", locus, 0); + return storage_->supportsOperation("reset", 1, 0); } return false; } -bool CompilerTarget::supports(const GateKind gate, - const ArrayRef locus) const { - const auto& specification = gateSpecification(gate); - return locus.size() == specification.numQubits && - storage_->supportsOperation(specification.name, locus, - specification.numParameters); +bool CompilerTarget::supports(const GateKind gate) const { + return llvm::is_contained(storage_->supportedGates, gate); } -ArrayRef CompilerTarget::globallySupportedGates() const noexcept { - return storage_->globalGates; +ArrayRef CompilerTarget::supportedGates() const noexcept { + return storage_->supportedGates; } std::optional diff --git a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt index 3427ad5280..a7adcd0cb4 100644 --- a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt +++ b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt @@ -12,8 +12,9 @@ add_mlir_library( MLIRQCOTransforms ${PASSES_SOURCES} LINK_LIBS - PRIVATE + PUBLIC MQTCompilerTarget + PRIVATE MLIRQCODialect MLIRQCOUtils MLIRQTensorUtils diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index 72890114d3..857c63354f 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -175,22 +175,23 @@ void emitGPhaseIfNeeded(OpBuilder& builder, Location loc, const double phase) { .phase = phase - (0.5 * (phi + lambda))}; } -EulerAngles anglesFromUnitary(const Matrix2x2& matrix, const EulerBasis basis) { +EulerAngles anglesFromUnitary(const Matrix2x2& matrix, + const SingleQubitBasis basis) { switch (basis) { - case EulerBasis::ZYZ: - case EulerBasis::ZSXX: + case SingleQubitBasis::ZYZ: + case SingleQubitBasis::ZSXX: return paramsZYZ(matrix); - case EulerBasis::ZXZ: + case SingleQubitBasis::ZXZ: return paramsZXZ(matrix); - case EulerBasis::XZX: + case SingleQubitBasis::XZX: return paramsXZX(matrix); - case EulerBasis::XYX: - case EulerBasis::R: + case SingleQubitBasis::XYX: + case SingleQubitBasis::R: return paramsXYX(matrix); - case EulerBasis::U: + case SingleQubitBasis::U: return paramsU(matrix); default: - llvm_unreachable("invalid Euler basis"); + llvm_unreachable("invalid single-qubit synthesis basis"); } } @@ -254,7 +255,8 @@ struct Unitary1QEulerPlan { * @param angles The angles to use for the decomposition. * @param basis The basis to use for the decomposition. */ - void appendDecomposition(const EulerAngles& angles, const EulerBasis basis) { + void appendDecomposition(const EulerAngles& angles, + const SingleQubitBasis basis) { if (isNearZeroRotationAngle(angles.theta) && isNearZeroRotationAngle(angles.phi) && isNearZeroRotationAngle(angles.lambda)) { @@ -264,20 +266,20 @@ struct Unitary1QEulerPlan { if (isNearZeroRotationAngle(angles.theta)) { switch (basis) { - case EulerBasis::ZYZ: - case EulerBasis::ZXZ: - case EulerBasis::ZSXX: + case SingleQubitBasis::ZYZ: + case SingleQubitBasis::ZXZ: + case SingleQubitBasis::ZSXX: appendRotation(SynthesisStep::Kind::RZ, angles.phi + angles.lambda); break; - case EulerBasis::XZX: - case EulerBasis::XYX: + case SingleQubitBasis::XZX: + case SingleQubitBasis::XYX: appendRotation(SynthesisStep::Kind::RX, angles.phi + angles.lambda); break; - case EulerBasis::R: + case SingleQubitBasis::R: appendRStep(angles.phi + angles.lambda, 0.0); break; - case EulerBasis::U: + case SingleQubitBasis::U: steps.emplace_back(SynthesisStep::Kind::U, 0.0, angles.phi, angles.lambda); break; @@ -287,43 +289,43 @@ struct Unitary1QEulerPlan { } switch (basis) { - case EulerBasis::ZYZ: + case SingleQubitBasis::ZYZ: appendRotation(SynthesisStep::Kind::RZ, angles.lambda); steps.emplace_back(SynthesisStep::Kind::RY, angles.theta); appendRotation(SynthesisStep::Kind::RZ, angles.phi); phase = angles.phase; break; - case EulerBasis::ZXZ: + case SingleQubitBasis::ZXZ: appendRotation(SynthesisStep::Kind::RZ, angles.lambda); steps.emplace_back(SynthesisStep::Kind::RX, angles.theta); appendRotation(SynthesisStep::Kind::RZ, angles.phi); phase = angles.phase; break; - case EulerBasis::XZX: + case SingleQubitBasis::XZX: appendRotation(SynthesisStep::Kind::RX, angles.lambda); steps.emplace_back(SynthesisStep::Kind::RZ, angles.theta); appendRotation(SynthesisStep::Kind::RX, angles.phi); phase = angles.phase; break; - case EulerBasis::XYX: + case SingleQubitBasis::XYX: appendRotation(SynthesisStep::Kind::RX, angles.lambda); steps.emplace_back(SynthesisStep::Kind::RY, angles.theta); appendRotation(SynthesisStep::Kind::RX, angles.phi); phase = angles.phase; break; - case EulerBasis::R: + case SingleQubitBasis::R: appendRStep(angles.lambda, 0.0); steps.emplace_back(SynthesisStep::Kind::R, angles.theta, std::numbers::pi / 2.0); appendRStep(angles.phi, 0.0); phase = angles.phase; break; - case EulerBasis::U: + case SingleQubitBasis::U: steps.emplace_back(SynthesisStep::Kind::U, angles.theta, angles.phi, angles.lambda); phase = angles.phase; break; - case EulerBasis::ZSXX: { + case SingleQubitBasis::ZSXX: { constexpr double pi = std::numbers::pi; constexpr double halfPi = std::numbers::pi / 2.0; constexpr double quarterPi = std::numbers::pi / 4.0; @@ -363,7 +365,8 @@ struct Unitary1QEulerPlan { * @return Planned gate sequence and optional global phase. */ [[nodiscard]] static Unitary1QEulerPlan -planUnitary1QEuler(const Matrix2x2& targetMatrix, const EulerBasis basis) { +planUnitary1QEuler(const Matrix2x2& targetMatrix, + const SingleQubitBasis basis) { Unitary1QEulerPlan plan; if (targetMatrix.isApprox(Matrix2x2::identity())) { return plan; @@ -416,22 +419,23 @@ emitUnitary1QEulerPlan(OpBuilder& builder, Location loc, Value qubit, return {.qubit = qubit, .globalPhase = plan.phase}; } -std::optional parseEulerBasis(StringRef basis) { - return StringSwitch>(basis.lower()) - .Case("zyz", EulerBasis::ZYZ) - .Case("zxz", EulerBasis::ZXZ) - .Case("xzx", EulerBasis::XZX) - .Case("xyx", EulerBasis::XYX) - .Case("u", EulerBasis::U) - .Case("zsxx", EulerBasis::ZSXX) - .Case("r", EulerBasis::R) +std::optional parseSingleQubitBasis(StringRef basis) { + return StringSwitch>(basis.lower()) + .Case("zyz", SingleQubitBasis::ZYZ) + .Case("zxz", SingleQubitBasis::ZXZ) + .Case("xzx", SingleQubitBasis::XZX) + .Case("xyx", SingleQubitBasis::XYX) + .Case("u", SingleQubitBasis::U) + .Case("zsxx", SingleQubitBasis::ZSXX) + .Case("r", SingleQubitBasis::R) .Default(std::nullopt); } std::optional synthesizeUnitary1QEuler(OpBuilder& builder, Location loc, Value qubit, const Matrix2x2& composed, const std::size_t runSize, - const bool hasNonBasisGate, const EulerBasis basis) { + const bool hasNonBasisGate, + const SingleQubitBasis basis) { const Unitary1QEulerPlan plan = planUnitary1QEuler(composed, basis); if (!hasNonBasisGate && runSize <= plan.gateCount()) { return std::nullopt; diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeGateset.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeGateset.cpp deleted file mode 100644 index 74a0dd9cd7..0000000000 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeGateset.cpp +++ /dev/null @@ -1,274 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "mlir/Dialect/QCO/Transforms/Decomposition/NativeGateset.h" - -#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" -#include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" -#include "mlir/Dialect/QCO/Utils/Matrix.h" - -#include -#include -#include -#include -#include - -#include -#include -#include - -namespace mlir::qco::decomposition { - -static std::optional parseGateToken(StringRef name) { - return StringSwitch>(name) - .Case("u", NativeGateKind::U) - .Case("x", NativeGateKind::X) - .Case("sx", NativeGateKind::SX) - .Case("rz", NativeGateKind::RZ) - .Case("rx", NativeGateKind::RX) - .Case("ry", NativeGateKind::RY) - .Case("r", NativeGateKind::R) - .Case("rxx", NativeGateKind::RXX) - .Case("ryy", NativeGateKind::RYY) - .Case("rzx", NativeGateKind::RZX) - .Case("rzz", NativeGateKind::RZZ) - .Case("iswap", NativeGateKind::ISWAP) - .Case("cz", NativeGateKind::CZ) - .Case("cx", NativeGateKind::CX) - .Case("ecr", NativeGateKind::ECR) - .Default(std::nullopt); -} - -static std::optional> -parseGateSet(StringRef nativeGates) { - DenseSet gates; - SmallVector parts; - nativeGates.split(parts, ',', /*MaxSplit=*/-1, /*KeepEmpty=*/false); - for (StringRef part : parts) { - const auto token = part.trim().lower(); - if (token.empty()) { - continue; - } - const auto gate = parseGateToken(token); - if (!gate) { - return std::nullopt; - } - gates.insert(*gate); - } - return gates; -} - -/** - * @brief Resolves the preferred single-qubit Euler basis for a parsed gateset. - * - * Returns `std::nullopt` when no supported single-qubit synthesis strategy is - * present. Cached on @ref NativeGateset by @ref NativeGateset::parse. - */ -[[nodiscard]] static std::optional -resolveEulerBasis(const DenseSet& gates) { - if (gates.contains(NativeGateKind::U)) { - return EulerBasis::U; - } - if (gates.contains(NativeGateKind::X) && gates.contains(NativeGateKind::SX) && - gates.contains(NativeGateKind::RZ)) { - return EulerBasis::ZSXX; - } - if (gates.contains(NativeGateKind::R)) { - return EulerBasis::R; - } - if (gates.contains(NativeGateKind::RX) && - gates.contains(NativeGateKind::RZ)) { - return EulerBasis::XZX; - } - if (gates.contains(NativeGateKind::RX) && - gates.contains(NativeGateKind::RY)) { - return EulerBasis::XYX; - } - if (gates.contains(NativeGateKind::RY) && - gates.contains(NativeGateKind::RZ)) { - return EulerBasis::ZYZ; - } - return std::nullopt; -} - -/** - * @brief Picks the two-qubit entangler for Weyl synthesis. - * - * When multiple entanglers appear in the gateset, preference is - * **RXX > RYY > RZX > RZZ > iSWAP > CZ > CX > ECR**. - */ -[[nodiscard]] static std::optional -selectEntangler(const DenseSet& gates) { - if (gates.contains(NativeGateKind::RXX)) { - return NativeGateKind::RXX; - } - if (gates.contains(NativeGateKind::RYY)) { - return NativeGateKind::RYY; - } - if (gates.contains(NativeGateKind::RZX)) { - return NativeGateKind::RZX; - } - if (gates.contains(NativeGateKind::RZZ)) { - return NativeGateKind::RZZ; - } - if (gates.contains(NativeGateKind::ISWAP)) { - return NativeGateKind::ISWAP; - } - if (gates.contains(NativeGateKind::CZ)) { - return NativeGateKind::CZ; - } - if (gates.contains(NativeGateKind::CX)) { - return NativeGateKind::CX; - } - if (gates.contains(NativeGateKind::ECR)) { - return NativeGateKind::ECR; - } - return std::nullopt; -} - -static constexpr Matrix4x4 CANONICAL_CONTROLLED_X = - Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, // row 0 - 0.0, 1.0, 0.0, 0.0, // row 1 - 0.0, 0.0, 0.0, 1.0, // row 2 - 0.0, 0.0, 1.0, 0.0); // row 3 - -static constexpr Matrix4x4 CANONICAL_CONTROLLED_Z = - Matrix4x4::fromDiagonal(1., 1., 1., -1.); - -static const TwoQubitBasisDecomposer& -cachedNativeBasisDecomposer(NativeGateKind entangler) { - switch (entangler) { - case NativeGateKind::RXX: { - static const TwoQubitBasisDecomposer DECOMPOSER = - TwoQubitBasisDecomposer::create( - RXXOp::unitaryMatrix(std::numbers::pi / 2.0), 1.0); - return DECOMPOSER; - } - case NativeGateKind::RYY: { - static const TwoQubitBasisDecomposer DECOMPOSER = - TwoQubitBasisDecomposer::create( - RYYOp::unitaryMatrix(std::numbers::pi / 2.0), 1.0); - return DECOMPOSER; - } - case NativeGateKind::RZX: { - static const TwoQubitBasisDecomposer DECOMPOSER = - TwoQubitBasisDecomposer::create( - RZXOp::unitaryMatrix(std::numbers::pi / 2.0), 1.0); - return DECOMPOSER; - } - case NativeGateKind::RZZ: { - static const TwoQubitBasisDecomposer DECOMPOSER = - TwoQubitBasisDecomposer::create( - RZZOp::unitaryMatrix(std::numbers::pi / 2.0), 1.0); - return DECOMPOSER; - } - case NativeGateKind::ISWAP: { - static const TwoQubitBasisDecomposer DECOMPOSER = - TwoQubitBasisDecomposer::create(iSWAPOp::getUnitaryMatrix(), 1.0); - return DECOMPOSER; - } - case NativeGateKind::CZ: { - static const TwoQubitBasisDecomposer DECOMPOSER = - TwoQubitBasisDecomposer::create(CANONICAL_CONTROLLED_Z, 1.0); - return DECOMPOSER; - } - case NativeGateKind::CX: { - static const TwoQubitBasisDecomposer DECOMPOSER = - TwoQubitBasisDecomposer::create(CANONICAL_CONTROLLED_X, 1.0); - return DECOMPOSER; - } - case NativeGateKind::ECR: { - static const TwoQubitBasisDecomposer DECOMPOSER = - TwoQubitBasisDecomposer::create(ECROp::getUnitaryMatrix(), 1.0); - return DECOMPOSER; - } - default: - llvm_unreachable( - "only RXX/RYY/RZX/RZZ/ISWAP/CZ/CX/ECR are valid entanglers"); - } -} - -std::optional -NativeGateset::decomposeTarget(const Matrix4x4& target) const { - if (!entangler) { - return std::nullopt; - } - return cachedNativeBasisDecomposer(*entangler).decomposeTarget(target); -} - -static std::optional gateKindFor(UnitaryOpInterface op) { - return TypeSwitch>( - op.getOperation()) - .Case([](UOp) { return NativeGateKind::U; }) - .Case([](XOp) { return NativeGateKind::X; }) - .Case([](SXOp) { return NativeGateKind::SX; }) - .Case([](RZOp) { return NativeGateKind::RZ; }) - .Case([](RXOp) { return NativeGateKind::RX; }) - .Case([](RYOp) { return NativeGateKind::RY; }) - .Case([](ROp) { return NativeGateKind::R; }) - .Default([](Operation*) { return std::nullopt; }); -} - -static std::optional entanglerKindFor(CtrlOp ctrl) { - if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1 || - ctrl.getNumBodyUnitaries() != 1) { - return std::nullopt; - } - return TypeSwitch>( - ctrl.getBodyUnitary(0).getOperation()) - .Case([](XOp) { return NativeGateKind::CX; }) - .Case([](ZOp) { return NativeGateKind::CZ; }) - .Default([](Operation*) { return std::nullopt; }); -} - -bool NativeGateset::allowsOp(Operation* op) const { - return TypeSwitch(op) - .Case([](auto) { return true; }) - .Case([&](RXXOp) { return gates.contains(NativeGateKind::RXX); }) - .Case([&](RYYOp) { return gates.contains(NativeGateKind::RYY); }) - .Case([&](RZXOp) { return gates.contains(NativeGateKind::RZX); }) - .Case([&](RZZOp) { return gates.contains(NativeGateKind::RZZ); }) - .Case( - [&](iSWAPOp) { return gates.contains(NativeGateKind::ISWAP); }) - .Case([&](CtrlOp ctrl) { - const auto kind = entanglerKindFor(ctrl); - return kind && gates.contains(*kind); - }) - .Case([&](ECROp) { return gates.contains(NativeGateKind::ECR); }) - .Case([&](UnitaryOpInterface unitary) { - if (!unitary.isSingleQubit()) { - return false; - } - const auto gate = gateKindFor(unitary); - return gate && gates.contains(*gate); - }) - .Default([](Operation*) { return false; }); -} - -std::optional NativeGateset::parse(StringRef nativeGates) { - auto gates = parseGateSet(nativeGates); - if (!gates) { - return std::nullopt; - } - const auto euler = resolveEulerBasis(*gates); - const auto entangler = selectEntangler(*gates); - if (!euler || !entangler) { - return std::nullopt; - } - return NativeGateset{ - .gates = std::move(*gates), - .eulerBasis = euler, - .entangler = entangler, - }; -} - -} // namespace mlir::qco::decomposition diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp index fbea6d05e6..414eed5466 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp @@ -10,9 +10,9 @@ #include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" +#include "mlir/Compiler/Target.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/NativeGateset.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" #include @@ -21,7 +21,6 @@ #include #include #include -#include #include #include @@ -72,6 +71,67 @@ struct ChamberState { static constexpr double PI = std::numbers::pi; static constexpr double PI_OVER_4 = PI / 4.0; +static constexpr Matrix4x4 CANONICAL_CONTROLLED_X = + Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, // row 0 + 0.0, 1.0, 0.0, 0.0, // row 1 + 0.0, 0.0, 0.0, 1.0, // row 2 + 0.0, 0.0, 1.0, 0.0); // row 3 +static constexpr Matrix4x4 CANONICAL_CONTROLLED_Z = + Matrix4x4::fromDiagonal(1., 1., 1., -1.); + +static const TwoQubitBasisDecomposer& +cachedNativeBasisDecomposer(const CompilerTarget::GateKind entangler) { + using GateKind = CompilerTarget::GateKind; + switch (entangler) { + case GateKind::RXX: { + static const TwoQubitBasisDecomposer DECOMPOSER = + TwoQubitBasisDecomposer::create( + RXXOp::unitaryMatrix(std::numbers::pi / 2.0), 1.0); + return DECOMPOSER; + } + case GateKind::RYY: { + static const TwoQubitBasisDecomposer DECOMPOSER = + TwoQubitBasisDecomposer::create( + RYYOp::unitaryMatrix(std::numbers::pi / 2.0), 1.0); + return DECOMPOSER; + } + case GateKind::RZX: { + static const TwoQubitBasisDecomposer DECOMPOSER = + TwoQubitBasisDecomposer::create( + RZXOp::unitaryMatrix(std::numbers::pi / 2.0), 1.0); + return DECOMPOSER; + } + case GateKind::RZZ: { + static const TwoQubitBasisDecomposer DECOMPOSER = + TwoQubitBasisDecomposer::create( + RZZOp::unitaryMatrix(std::numbers::pi / 2.0), 1.0); + return DECOMPOSER; + } + case GateKind::ISWAP: { + static const TwoQubitBasisDecomposer DECOMPOSER = + TwoQubitBasisDecomposer::create(iSWAPOp::getUnitaryMatrix(), 1.0); + return DECOMPOSER; + } + case GateKind::CZ: { + static const TwoQubitBasisDecomposer DECOMPOSER = + TwoQubitBasisDecomposer::create(CANONICAL_CONTROLLED_Z, 1.0); + return DECOMPOSER; + } + case GateKind::CX: { + static const TwoQubitBasisDecomposer DECOMPOSER = + TwoQubitBasisDecomposer::create(CANONICAL_CONTROLLED_X, 1.0); + return DECOMPOSER; + } + case GateKind::ECR: { + static const TwoQubitBasisDecomposer DECOMPOSER = + TwoQubitBasisDecomposer::create(ECROp::getUnitaryMatrix(), 1.0); + return DECOMPOSER; + } + default: + llvm_unreachable( + "only RXX/RYY/RZX/RZZ/ISWAP/CZ/CX/ECR are valid entanglers"); + } +} static constexpr Matrix2x2 I_PAULI_X = Matrix2x2::fromElements(0, 1i, 1i, 0); static constexpr Matrix2x2 I_PAULI_Y = Matrix2x2::fromElements(0, 1, -1, 0); @@ -597,9 +657,9 @@ bool TwoQubitWeylDecomposition::applySpecialization( } case Specialization::ControlledEquiv: { const auto [k2ltheta, k2lphi, k2llambda, k2lphase] = - anglesFromUnitary(k2l_, EulerBasis::XYX); + anglesFromUnitary(k2l_, SingleQubitBasis::XYX); const auto [k2rtheta, k2rphi, k2rlambda, k2rphase] = - anglesFromUnitary(k2r_, EulerBasis::XYX); + anglesFromUnitary(k2r_, SingleQubitBasis::XYX); b_ = 0.; c_ = 0.; globalPhase_ = globalPhase_ + k2lphase + k2rphase; @@ -611,9 +671,9 @@ bool TwoQubitWeylDecomposition::applySpecialization( } case Specialization::MirrorControlledEquiv: { const auto [k2ltheta, k2lphi, k2llambda, k2lphase] = - anglesFromUnitary(k2l_, EulerBasis::ZYZ); + anglesFromUnitary(k2l_, SingleQubitBasis::ZYZ); const auto [k2rtheta, k2rphi, k2rlambda, k2rphase] = - anglesFromUnitary(k2r_, EulerBasis::ZYZ); + anglesFromUnitary(k2r_, SingleQubitBasis::ZYZ); a_ = PI_OVER_4; b_ = PI_OVER_4; globalPhase_ = globalPhase_ + k2lphase + k2rphase; @@ -625,7 +685,7 @@ bool TwoQubitWeylDecomposition::applySpecialization( } case Specialization::FSimaabEquiv: { const auto [k2ltheta, k2lphi, k2llambda, k2lphase] = - anglesFromUnitary(k2l_, EulerBasis::ZYZ); + anglesFromUnitary(k2l_, SingleQubitBasis::ZYZ); const auto ab = (a_ + b_) / 2.; a_ = ab; b_ = ab; @@ -638,7 +698,7 @@ bool TwoQubitWeylDecomposition::applySpecialization( } case Specialization::FSimabbEquiv: { const auto [k2ltheta, k2lphi, k2llambda, k2lphase] = - anglesFromUnitary(k2l_, EulerBasis::XYX); + anglesFromUnitary(k2l_, SingleQubitBasis::XYX); const auto bc = (b_ + c_) / 2.; b_ = bc; c_ = bc; @@ -651,7 +711,7 @@ bool TwoQubitWeylDecomposition::applySpecialization( } case Specialization::FSimabmbEquiv: { const auto [k2ltheta, k2lphi, k2llambda, k2lphase] = - anglesFromUnitary(k2l_, EulerBasis::XYX); + anglesFromUnitary(k2l_, SingleQubitBasis::XYX); const auto bc = (b_ - c_) / 2.; b_ = bc; c_ = -bc; @@ -668,35 +728,42 @@ bool TwoQubitWeylDecomposition::applySpecialization( return flippedFromOriginal; } -FailureOr -synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, Value qubit0, - Value qubit1, const Matrix4x4& target, - const NativeGateset& spec) { - const auto native = spec.decomposeTarget(target); - if (!native || !spec.eulerBasis) { - return failure(); +TwoQubitNativeDecomposition +decomposeUnitary2QWeyl(const Matrix4x4& target, + const CompilerTarget::GateKind entangler) { + auto decomposition = + cachedNativeBasisDecomposer(entangler).decomposeTarget(target); + if (!decomposition) { + llvm::reportFatalInternalError( + "target-selected entangler failed to decompose a two-qubit unitary"); } + return std::move(*decomposition); +} - double globalPhase = native->globalPhase; +SynthesizedUnitary2Q +emitUnitary2QWeyl(OpBuilder& builder, Location loc, Value qubit0, Value qubit1, + const TwoQubitNativeDecomposition& decomposition, + const CompilerTarget::SynthesisBasis basis) { + double globalPhase = decomposition.globalPhase; Value wire0 = qubit0; Value wire1 = qubit1; - const auto& factors = native->singleQubitFactors; - const std::uint8_t numBasisUses = native->numBasisUses; + const auto& factors = decomposition.singleQubitFactors; + const std::uint8_t numBasisUses = decomposition.numBasisUses; const std::size_t requiredFactors = singleQubitFactorCount(numBasisUses); if (factors.size() != requiredFactors) { llvm::reportFatalInternalError(llvm::formatv( - "synthesizeUnitary2QWeyl: expected {0} single-qubit factors for " + "emitUnitary2QWeyl: expected {0} single-qubit factors for " "numBasisUses = {1}, got {2}", requiredFactors, numBasisUses, factors.size())); } const auto emitFactor = [&](Value& wire, std::size_t index) { const auto synthesized = synthesizeUnitary1QEuler( builder, loc, wire, factors[index], /*runSize=*/0, - /*hasNonBasisGate=*/true, *spec.eulerBasis); + /*hasNonBasisGate=*/true, basis.singleQubit); if (!synthesized) { llvm::reportFatalInternalError(llvm::formatv( - "synthesizeUnitary2QWeyl: euler synthesis failed for factor index " + "emitUnitary2QWeyl: euler synthesis failed for factor index " "{0} (layer {1}, qubit {2})", index, index / 2, (index % 2 == 0) ? 1 : 0)); } @@ -704,39 +771,39 @@ synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, Value qubit0, globalPhase += synthesized->globalPhase; }; const auto emitEntangler = [&]() { - if (spec.entangler == NativeGateKind::RXX) { + if (basis.entangler == CompilerTarget::GateKind::RXX) { auto rxxOp = RXXOp::create(builder, loc, wire0, wire1, PI / 2.0); wire0 = rxxOp.getOutputQubit(0); wire1 = rxxOp.getOutputQubit(1); return; } - if (spec.entangler == NativeGateKind::RYY) { + if (basis.entangler == CompilerTarget::GateKind::RYY) { auto ryyOp = RYYOp::create(builder, loc, wire0, wire1, PI / 2.0); wire0 = ryyOp.getOutputQubit(0); wire1 = ryyOp.getOutputQubit(1); return; } - if (spec.entangler == NativeGateKind::RZX) { + if (basis.entangler == CompilerTarget::GateKind::RZX) { auto rzxOp = RZXOp::create(builder, loc, wire0, wire1, PI / 2.0); wire0 = rzxOp.getOutputQubit(0); wire1 = rzxOp.getOutputQubit(1); return; } - if (spec.entangler == NativeGateKind::RZZ) { + if (basis.entangler == CompilerTarget::GateKind::RZZ) { auto rzzOp = RZZOp::create(builder, loc, wire0, wire1, PI / 2.0); wire0 = rzzOp.getOutputQubit(0); wire1 = rzzOp.getOutputQubit(1); return; } - if (spec.entangler == NativeGateKind::ISWAP) { + if (basis.entangler == CompilerTarget::GateKind::ISWAP) { auto iswapOp = iSWAPOp::create(builder, loc, wire0, wire1); wire0 = iswapOp.getOutputQubit(0); wire1 = iswapOp.getOutputQubit(1); return; } - if (spec.entangler == NativeGateKind::CZ || - spec.entangler == NativeGateKind::CX) { - const bool emitCz = spec.entangler == NativeGateKind::CZ; + if (basis.entangler == CompilerTarget::GateKind::CZ || + basis.entangler == CompilerTarget::GateKind::CX) { + const bool emitCz = basis.entangler == CompilerTarget::GateKind::CZ; auto ctrlOp = CtrlOp::create(builder, loc, wire0, wire1, [&](Value targetQubit) { if (emitCz) { @@ -748,8 +815,8 @@ synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, Value qubit0, wire1 = ctrlOp.getOutputTarget(0); return; } - assert(spec.entangler == NativeGateKind::ECR && - "emitEntangler: unexpected NativeGateKind"); + assert(basis.entangler == CompilerTarget::GateKind::ECR && + "emitEntangler: unexpected compiler target gate"); auto ecrOp = ECROp::create(builder, loc, wire0, wire1); wire0 = ecrOp.getOutputQubit(0); wire1 = ecrOp.getOutputQubit(1); diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp index d5121617c7..4e69f2572b 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp @@ -68,27 +68,29 @@ static std::optional getRunMemberMatrix(UnitaryOpInterface gate) { * @brief Whether `op` is a gate that Euler synthesis emits for `basis`. * * @param op The operation to classify. - * @param basis The target Euler basis. + * @param basis The single-qubit synthesis basis. * @return Whether `op` is in the gate set for `basis`. */ static bool isTargetBasisGate(Operation* op, - const decomposition::EulerBasis basis) { - using decomposition::EulerBasis; + const decomposition::SingleQubitBasis basis) { + using decomposition::SingleQubitBasis; return TypeSwitch(op) .Case([&](auto) { - return basis == EulerBasis::ZYZ || basis == EulerBasis::ZXZ || - basis == EulerBasis::XZX || basis == EulerBasis::ZSXX; + return basis == SingleQubitBasis::ZYZ || + basis == SingleQubitBasis::ZXZ || + basis == SingleQubitBasis::XZX || + basis == SingleQubitBasis::ZSXX; }) .Case([&](auto) { - return basis == EulerBasis::ZYZ || basis == EulerBasis::XYX; + return basis == SingleQubitBasis::ZYZ || basis == SingleQubitBasis::XYX; }) .Case([&](auto) { - return basis == EulerBasis::ZXZ || basis == EulerBasis::XZX || - basis == EulerBasis::XYX; + return basis == SingleQubitBasis::ZXZ || + basis == SingleQubitBasis::XZX || basis == SingleQubitBasis::XYX; }) - .Case([&](auto) { return basis == EulerBasis::U; }) - .Case([&](auto) { return basis == EulerBasis::ZSXX; }) - .Case([&](auto) { return basis == EulerBasis::R; }) + .Case([&](auto) { return basis == SingleQubitBasis::U; }) + .Case([&](auto) { return basis == SingleQubitBasis::ZSXX; }) + .Case([&](auto) { return basis == SingleQubitBasis::R; }) .Default([](auto) { return false; }); } @@ -97,12 +99,12 @@ static bool isTargetBasisGate(Operation* op, * * @param head First gate of the run. * @param headMatrix Matrix already obtained while identifying the run head. - * @param basis Target Euler basis. + * @param basis Single-qubit synthesis basis. * @return Composed matrix, gate count, and run tail. */ -static FusableRunScan scanFusableRun(UnitaryOpInterface head, - const Matrix2x2& headMatrix, - const decomposition::EulerBasis basis) { +static FusableRunScan +scanFusableRun(UnitaryOpInterface head, const Matrix2x2& headMatrix, + const decomposition::SingleQubitBasis basis) { FusableRunScan scan; for (auto* op : WireRange(head.getOutputTarget(0))) { auto member = dyn_cast_or_null(op); @@ -151,12 +153,12 @@ namespace { struct FuseSingleQubitUnitaryRunsPattern final : OpInterfaceRewritePattern { FuseSingleQubitUnitaryRunsPattern(MLIRContext* context, - const decomposition::EulerBasis basis, + const decomposition::SingleQubitBasis basis, const bool skipControlledBodies) : OpInterfaceRewritePattern(context), basis(basis), skipControlledBodies(skipControlledBodies) {} - decomposition::EulerBasis basis; + decomposition::SingleQubitBasis basis; bool skipControlledBodies; /** @@ -220,10 +222,10 @@ struct FuseSingleQubitUnitaryRunsPass final void runOnOperation() override { auto moduleOp = getOperation(); - const auto parsed = decomposition::parseEulerBasis(basis); + const auto parsed = decomposition::parseSingleQubitBasis(basis); if (!parsed) { moduleOp.emitError() - << "Invalid Euler basis '" << basis + << "Invalid single-qubit synthesis basis '" << basis << "'. Expected one of: zyz, zxz, xzx, xyx, u, zsxx, r."; signalPassFailure(); return; @@ -247,7 +249,7 @@ struct FuseSingleQubitUnitaryRunsPass final namespace mlir::qco::decomposition { void populateFuseSingleQubitUnitaryRunsPatterns( - RewritePatternSet& patterns, const EulerBasis basis, + RewritePatternSet& patterns, const SingleQubitBasis basis, const bool skipControlledBodies) { patterns.add(patterns.getContext(), basis, skipControlledBodies); diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp deleted file mode 100644 index f55e02da48..0000000000 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp +++ /dev/null @@ -1,475 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" -#include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/NativeGateset.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" -#include "mlir/Dialect/QCO/Transforms/Passes.h" -#include "mlir/Dialect/QCO/Utils/Matrix.h" -#include "mlir/Dialect/Utils/Transforms/GlobalPhaseNormalization.h" - -#include -#include -#include // IWYU pragma: keep (Passes.h.inc) -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -namespace mlir::qco { - -#define GEN_PASS_DEF_FUSETWOQUBITUNITARYRUNS -#include "mlir/Dialect/QCO/Transforms/Passes.h.inc" - -using decomposition::EulerBasis; -using decomposition::NativeGateset; -using decomposition::populateFuseSingleQubitUnitaryRunsPatterns; -using decomposition::synthesizeUnitary2QWeyl; - -namespace { - -/** Composed unitary and metadata for a fusable two-qubit run. */ -struct FusableTwoQubitRun { - SmallVector ops; ///< Members in program order. - Matrix4x4 composed = Matrix4x4::identity(); - unsigned numTwoQ = 0; ///< Number of two-qubit members (entanglers consumed). - bool hasNonNativeGate = false; ///< Any member off the native gateset. - Value tailA; ///< Current output wires of the run's tail. - Value tailB; -}; - -} // namespace - -// --- Run membership ------------------------------------------------------- // - -/// Whether `op` is nested under a `ctrl`/`inv` body. Such unitaries are handled -/// through their shell op, so the top-level walk skips them. -static bool isExcludedFromTopLevelUnitaryWalk(Operation* op) { - if (op->getParentOfType()) { - return true; - } - return !isa(op) && op->getParentOfType(); -} - -/// Whether `op` is a unitary shell the pass may rewrite at top level. -static bool isWalkableUnitaryShell(Operation* op) { - return !isa(op) && - !isExcludedFromTopLevelUnitaryWalk(op); -} - -/// Builds the constant 4x4 matrix for a two-qubit op (bare or single-target -/// `CtrlOp`). Returns false for a `CtrlOp` that is not -/// single-control/single-target, or an op whose matrix is not known at compile -/// time. -static bool assignTwoQubitOpMatrix(Operation* op, Matrix4x4& matrix) { - if (auto ctrl = dyn_cast(op)) { - if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { - return false; - } - return cast(ctrl.getOperation()) - .getUnitaryMatrix4x4(matrix); - } - auto unitary = cast(op); - assert(unitary.isTwoQubit() && - "only two-qubit unitary shells are passed to assignTwoQubitOpMatrix"); - return unitary.getUnitaryMatrix4x4(matrix); -} - -/// Whether `unitary` is a single-qubit gate that can join a run. -static bool isOneQubitRunMember(UnitaryOpInterface unitary) { - if (!unitary || !unitary.isSingleQubit() || - !isWalkableUnitaryShell(unitary.getOperation())) { - return false; - } - Matrix2x2 matrix; - return unitary.getUnitaryMatrix2x2(matrix); -} - -/// Whether `unitary` is a two-qubit gate that can join a run. -static bool isTwoQubitRunMember(UnitaryOpInterface unitary) { - if (!unitary || !unitary.isTwoQubit() || - !isWalkableUnitaryShell(unitary.getOperation())) { - return false; - } - Matrix4x4 matrix; - return assignTwoQubitOpMatrix(unitary.getOperation(), matrix); -} - -// --- Wire navigation ------------------------------------------------------ // - -/// The sole run-member consumer of `wire`, or a null interface when its unique -/// user cannot join a run. `wire` is single-use by qubit linearity. -static UnitaryOpInterface uniqueUnitaryUser(Value wire) { - assert(wire.hasOneUse() && - "qubit values are single-use, so a run tail has exactly one user"); - auto unitary = dyn_cast(*wire.user_begin()); - if (!unitary) { - return {}; - } - if (unitary.isTwoQubit()) { - return isTwoQubitRunMember(unitary) ? unitary : UnitaryOpInterface{}; - } - if (unitary.isSingleQubit()) { - return isOneQubitRunMember(unitary) ? unitary : UnitaryOpInterface{}; - } - return {}; -} - -/// Traces `wire` upstream through single-qubit gates to the two-qubit run -/// member terminating the chain, or `nullptr` if the chain is broken. -static Operation* twoQubitGateAtEndOfOneQChain(Value wire) { - Value cur = wire; - while (Operation* def = cur.getDefiningOp()) { - auto unitary = dyn_cast(def); - if (!unitary) { - return nullptr; - } - if (unitary.isTwoQubit()) { - return isTwoQubitRunMember(unitary) ? def : nullptr; - } - if (!isOneQubitRunMember(unitary)) { - return nullptr; - } - cur = unitary.getInputQubit(0); - } - return nullptr; -} - -/// Whether both input wires of `op` come from one earlier two-qubit run, making -/// `op` a continuation of that run rather than a fresh run start. -static bool feedsFromSameTwoQubitRun(UnitaryOpInterface op) { - const Value in0 = op.getInputQubit(0); - const Value in1 = op.getInputQubit(1); - assert(in0.hasOneUse() && in1.hasOneUse() && - "qubit values are single-use, so a run member consumes each input " - "exactly once"); - Operation* gate0 = twoQubitGateAtEndOfOneQChain(in0); - Operation* gate1 = twoQubitGateAtEndOfOneQChain(in1); - return gate0 != nullptr && gate0 == gate1; -} - -// --- Run scanning --------------------------------------------------------- // - -/// Appends a two-qubit gate to `run`, composing its matrix. No-op unless both -/// of `op`'s inputs are the run's current tail wires (in either order), keeping -/// the run confined to a single pair of wires. -static void absorbTwoQubitIntoRun(FusableTwoQubitRun& run, - UnitaryOpInterface op, - const NativeGateset& spec) { - Matrix4x4 opMatrix; - [[maybe_unused]] const bool assigned = - assignTwoQubitOpMatrix(op.getOperation(), opMatrix); - assert(assigned && "a two-qubit run member always exposes a 4x4 matrix"); - const Value in0 = op.getInputQubit(0); - const Value in1 = op.getInputQubit(1); - size_t id0 = 0; - size_t id1 = 1; - if (in0 == run.tailA && in1 == run.tailB) { - run.tailA = op.getOutputQubit(0); - run.tailB = op.getOutputQubit(1); - } else if (in0 == run.tailB && in1 == run.tailA) { - id0 = 1; - id1 = 0; - run.tailA = op.getOutputQubit(1); - run.tailB = op.getOutputQubit(0); - } else { - llvm_unreachable( - "a unique user of both tail wires connects to both of them"); - } - run.composed.premultiplyBy(opMatrix.reorderForQubits(id0, id1)); - run.ops.push_back(op.getOperation()); - ++run.numTwoQ; - run.hasNonNativeGate |= !spec.allowsOp(op.getOperation()); -} - -/// Appends a single-qubit gate on run wire `wireIndex` (0 = A, 1 = B). -static void absorbOneQubitIntoRun(FusableTwoQubitRun& run, - UnitaryOpInterface op, - const NativeGateset& spec, - unsigned wireIndex) { - Matrix2x2 raw; - [[maybe_unused]] const bool assigned = op.getUnitaryMatrix2x2(raw); - assert(assigned && "a single-qubit run member always exposes a 2x2 matrix"); - run.composed.premultiplyBy(raw.embedInTwoQubit(wireIndex)); - run.ops.push_back(op.getOperation()); - run.hasNonNativeGate |= !spec.allowsOp(op.getOperation()); - (wireIndex == 0 ? run.tailA : run.tailB) = op.getOutputQubit(0); -} - -/// Walks forward from `head`, composing the run's matrix and metadata. Absorbs -/// a following two-qubit gate when it keeps both run wires together, otherwise -/// the single-qubit gate first in program order; stops at the first boundary -/// that would split the run's two wires. -static FusableTwoQubitRun scanFusableTwoQubitRun(UnitaryOpInterface head, - const NativeGateset& spec) { - FusableTwoQubitRun run; - [[maybe_unused]] const bool assigned = - assignTwoQubitOpMatrix(head.getOperation(), run.composed); - assert(assigned && "a run head is a two-qubit member with a 4x4 matrix"); - run.tailA = head.getOutputQubit(0); - run.tailB = head.getOutputQubit(1); - run.ops.push_back(head.getOperation()); - run.numTwoQ = 1; - run.hasNonNativeGate |= !spec.allowsOp(head.getOperation()); - - while (true) { - UnitaryOpInterface nextOnA = uniqueUnitaryUser(run.tailA); - UnitaryOpInterface nextOnB = uniqueUnitaryUser(run.tailB); - const bool sameOp = - nextOnA && nextOnB && nextOnA.getOperation() == nextOnB.getOperation(); - - if (sameOp && nextOnA.isTwoQubit()) { - absorbTwoQubitIntoRun(run, nextOnA, spec); - continue; - } - - const bool aSingle = nextOnA && nextOnA.isSingleQubit() && !sameOp; - const bool bSingle = nextOnB && nextOnB.isSingleQubit() && !sameOp; - if (aSingle && bSingle && nextOnA->getBlock() != nextOnB->getBlock()) { - break; - } - if (aSingle && (!bSingle || nextOnA->isBeforeInBlock(nextOnB))) { - absorbOneQubitIntoRun(run, nextOnA, spec, /*wireIndex=*/0); - continue; - } - if (bSingle) { - absorbOneQubitIntoRun(run, nextOnB, spec, /*wireIndex=*/1); - continue; - } - break; - } - return run; -} - -/// Erases all run members, successors first so each is dead when erased. -static void eraseFusableRun(PatternRewriter& rewriter, - const FusableTwoQubitRun& run) { - for (Operation* member : llvm::reverse(run.ops)) { - rewriter.eraseOp(member); - } -} - -/// First single- or two-qubit unitary (including `ctrl` shells) still off the -/// native gateset, or `nullptr` if none. Used as the pass' convergence check. -/// Gates acting on more than two qubits are out of scope here (a dedicated -/// multi-controlled synthesis pass possibly lowers those) and are left -/// untouched rather than reported. -static Operation* findNonNativeOp(Operation* root, const NativeGateset& spec) { - Operation* found = nullptr; - root->walk([&](Operation* op) { - auto unitary = dyn_cast(op); - if (!unitary || !isWalkableUnitaryShell(op) || unitary.getNumQubits() > 2) { - return WalkResult::advance(); - } - if (spec.allowsOp(op)) { - return WalkResult::advance(); - } - found = op; - return WalkResult::interrupt(); - }); - return found; -} - -namespace { - -/// Fuses a maximal two-qubit run into one composed unitary and resynthesizes it -/// to the native gateset when beneficial. -struct FuseTwoQubitUnitaryRunsPattern final - : OpInterfaceRewritePattern { - FuseTwoQubitUnitaryRunsPattern(MLIRContext* ctx, NativeGateset specIn) - : OpInterfaceRewritePattern(ctx), spec(std::move(specIn)) {} - - NativeGateset spec; - - /// Whether `op` anchors a run: a two-qubit run member whose two wires are not - /// both fed by the same earlier run (which would make it a continuation). - static bool isRunStart(UnitaryOpInterface op) { - return isTwoQubitRunMember(op) && !feedsFromSameTwoQubitRun(op); - } - - /// Fuses the run anchored at `op` if it contains an off-gateset gate or Weyl - /// resynthesis uses fewer entanglers than the run's two-qubit members. - LogicalResult matchAndRewrite(UnitaryOpInterface op, - PatternRewriter& rewriter) const override { - if (!isRunStart(op)) { - return failure(); - } - - FusableTwoQubitRun run = scanFusableTwoQubitRun(op, spec); - if (run.ops.size() < 2) { - return failure(); - } - - const auto native = spec.decomposeTarget(run.composed); - if (!native || - (!run.hasNonNativeGate && native->numBasisUses >= run.numTwoQ)) { - return failure(); - } - - auto firstOp = cast(run.ops.front()); - rewriter.setInsertionPoint(firstOp); - const auto synthesized = synthesizeUnitary2QWeyl( - rewriter, firstOp.getLoc(), firstOp.getInputQubit(0), - firstOp.getInputQubit(1), run.composed, spec); - if (failed(synthesized)) { - firstOp->emitError("failed to emit synthesized two-qubit gate sequence"); - return failure(); - } - decomposition::emitGPhaseIfNeeded(rewriter, firstOp.getLoc(), - synthesized->globalPhase); - rewriter.replaceAllUsesWith(run.tailA, synthesized->qubit0); - rewriter.replaceAllUsesWith(run.tailB, synthesized->qubit1); - eraseFusableRun(rewriter, run); - return success(); - } -}; - -/// Lowers a single off-gateset two-qubit op (bare or single-target `CtrlOp`) to -/// the native entangler plus native single-qubit factors via Weyl synthesis. -/// Native two-qubit ops and fusable runs are left to -/// @ref FuseTwoQubitUnitaryRunsPattern. -struct LowerTwoQubitOpPattern final - : OpInterfaceRewritePattern { - LowerTwoQubitOpPattern(MLIRContext* ctx, NativeGateset specIn) - : OpInterfaceRewritePattern(ctx), spec(std::move(specIn)) {} - - NativeGateset spec; - - LogicalResult matchAndRewrite(UnitaryOpInterface op, - PatternRewriter& rewriter) const override { - if (!op.isTwoQubit()) { - return failure(); - } - Operation* raw = op.getOperation(); - if (!isWalkableUnitaryShell(raw) || spec.allowsOp(raw)) { - return failure(); - } - Matrix4x4 matrix; - if (!assignTwoQubitOpMatrix(raw, matrix)) { - return failure(); - } - - Value in0; - Value in1; - if (auto ctrl = dyn_cast(raw)) { - in0 = ctrl.getInputControl(0); - in1 = ctrl.getInputTarget(0); - } else { - in0 = op.getInputQubit(0); - in1 = op.getInputQubit(1); - } - - rewriter.setInsertionPoint(raw); - const auto synthesized = synthesizeUnitary2QWeyl(rewriter, raw->getLoc(), - in0, in1, matrix, spec); - if (failed(synthesized)) { - return failure(); - } - decomposition::emitGPhaseIfNeeded(rewriter, raw->getLoc(), - synthesized->globalPhase); - rewriter.replaceOp(raw, - ValueRange{synthesized->qubit0, synthesized->qubit1}); - return success(); - } -}; - -} // namespace - -/// Fuses single-qubit runs (and lowers lone off-gateset single-qubit ops) by -/// reusing the `fuse-single-qubit-unitary-runs` rewrite. `qco.ctrl` bodies are -/// skipped so the `X`/`Z` bodies of native entanglers are preserved. -static LogicalResult fuseSingleQubitRuns(ModuleOp moduleOp, - const EulerBasis basis) { - RewritePatternSet patterns(moduleOp.getContext()); - populateFuseSingleQubitUnitaryRunsPatterns(patterns, basis, - /*skipControlledBodies=*/true); - return applyPatternsGreedily(moduleOp, std::move(patterns)); -} - -/// Fuses two-qubit runs, then lowers any remaining off-gateset two-qubit ops. -static LogicalResult fuseAndLowerTwoQubitOps(ModuleOp moduleOp, - const NativeGateset& spec) { - MLIRContext* ctx = moduleOp.getContext(); - { - RewritePatternSet runPatterns(ctx); - runPatterns.add(ctx, spec); - if (failed(applyPatternsGreedily(moduleOp, std::move(runPatterns)))) { - return failure(); - } - } - RewritePatternSet lowerPatterns(ctx); - lowerPatterns.add(ctx, spec); - return applyPatternsGreedily(moduleOp, std::move(lowerPatterns)); -} - -namespace { - -struct FuseTwoQubitUnitaryRunsPass final - : impl::FuseTwoQubitUnitaryRunsBase { - using Base::Base; - - explicit FuseTwoQubitUnitaryRunsPass(FuseTwoQubitUnitaryRunsOptions options) - : Base(std::move(options)) {} - -protected: - void runOnOperation() override { - if (StringRef(nativeGates).trim().empty()) { - return; - } - const auto spec = NativeGateset::parse(nativeGates); - if (!spec) { - getOperation().emitError() << "unsupported native gateset (native-gates='" - << nativeGates << "')"; - signalPassFailure(); - return; - } - const EulerBasis basis = *spec->eulerBasis; - ModuleOp moduleOp = getOperation(); - - // 1. Fuse single-qubit runs (also lowers lone off-gateset single-qubit - // ops). - // 2. Fuse two-qubit runs and lower remaining off-gateset two-qubit ops. - // 3. Fuse the single-qubit seams introduced by two-qubit synthesis. - if (failed(fuseSingleQubitRuns(moduleOp, basis)) || - failed(fuseAndLowerTwoQubitOps(moduleOp, *spec)) || - failed(fuseSingleQubitRuns(moduleOp, basis))) { - signalPassFailure(); - return; - } - - if (Operation* leftover = findNonNativeOp(moduleOp, *spec)) { - leftover->emitError() - << "native gate synthesis: operation remains outside the native " - "gateset (native-gates='" - << nativeGates << "')"; - signalPassFailure(); - return; - } - if (failed(mlir::mqt::normalizeGlobalPhases(moduleOp))) { - signalPassFailure(); - } - } -}; - -} // namespace - -} // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp new file mode 100644 index 0000000000..c153a9cdb6 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TargetSynthesis.cpp @@ -0,0 +1,574 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Compiler/Target.h" +#include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" +#include "mlir/Dialect/QCO/Transforms/Passes.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" +#include "mlir/Dialect/QTensor/IR/QTensorOps.h" +#include "mlir/Dialect/Utils/Transforms/GlobalPhaseNormalization.h" + +#include +#include +#include // IWYU pragma: keep (Passes.h.inc) +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace mlir::qco { + +using decomposition::decomposeUnitary2QWeyl; +using decomposition::emitUnitary2QWeyl; + +namespace { + +/** Composed unitary and metadata for a fusable two-qubit run. */ +struct FusableTwoQubitRun { + SmallVector ops; ///< Members in program order. + Matrix4x4 composed = Matrix4x4::identity(); + unsigned numTwoQ = 0; ///< Number of two-qubit members (entanglers consumed). + Value tailA; ///< Current output wires of the run's tail. + Value tailB; +}; + +} // namespace + +// --- Run membership ------------------------------------------------------- // + +/// Whether `op` is nested under a modifier body. Such unitaries are handled +/// through their shell op, so the top-level walk skips them. +static bool isExcludedFromTopLevelUnitaryWalk(Operation* op) { + return op->getParentOfType() || op->getParentOfType() || + op->getParentOfType(); +} + +/// Whether `op` is a unitary shell the pass may rewrite at top level. +static bool isWalkableUnitaryShell(Operation* op) { + return !isa(op) && + !isExcludedFromTopLevelUnitaryWalk(op); +} + +/// Builds the constant 4x4 matrix for a two-qubit op (bare or single-target +/// `CtrlOp`). Returns false for a `CtrlOp` that is not +/// single-control/single-target, or an op whose matrix is not known at compile +/// time. +static bool assignTwoQubitOpMatrix(Operation* op, Matrix4x4& matrix) { + if (auto ctrl = dyn_cast(op)) { + if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { + return false; + } + return cast(ctrl.getOperation()) + .getUnitaryMatrix4x4(matrix); + } + auto unitary = cast(op); + assert(unitary.isTwoQubit() && + "only two-qubit unitary shells are passed to assignTwoQubitOpMatrix"); + return unitary.getUnitaryMatrix4x4(matrix); +} + +/// Return the constant matrix when `unitary` is a single-qubit run member. +static std::optional +oneQubitRunMemberMatrix(UnitaryOpInterface unitary) { + if (!unitary || !unitary.isSingleQubit() || + !isWalkableUnitaryShell(unitary.getOperation())) { + return std::nullopt; + } + Matrix2x2 matrix; + if (!unitary.getUnitaryMatrix2x2(matrix)) { + return std::nullopt; + } + return matrix; +} + +/// Return the constant matrix when `unitary` is a two-qubit run member. +static std::optional +twoQubitRunMemberMatrix(UnitaryOpInterface unitary) { + if (!unitary || !unitary.isTwoQubit() || + !isWalkableUnitaryShell(unitary.getOperation())) { + return std::nullopt; + } + Matrix4x4 matrix; + if (!assignTwoQubitOpMatrix(unitary.getOperation(), matrix)) { + return std::nullopt; + } + return matrix; +} + +// --- Wire navigation ------------------------------------------------------ // + +/// The sole walkable one- or two-qubit consumer of `wire`, or a null interface. +/// `wire` is single-use by qubit linearity. +static UnitaryOpInterface uniqueUnitaryUser(Value wire) { + assert(wire.hasOneUse() && + "qubit values are single-use, so a run tail has exactly one user"); + auto unitary = dyn_cast(*wire.user_begin()); + if (!unitary || !isWalkableUnitaryShell(unitary.getOperation()) || + (!unitary.isSingleQubit() && !unitary.isTwoQubit())) { + return {}; + } + return unitary; +} + +/// Traces `wire` upstream through single-qubit gates to the two-qubit run +/// member terminating the chain, or `nullptr` if the chain is broken. +static Operation* twoQubitGateAtEndOfOneQChain(Value wire) { + Value cur = wire; + while (Operation* def = cur.getDefiningOp()) { + auto unitary = dyn_cast(def); + if (!unitary) { + return nullptr; + } + if (unitary.isTwoQubit()) { + return twoQubitRunMemberMatrix(unitary) ? def : nullptr; + } + if (!oneQubitRunMemberMatrix(unitary)) { + return nullptr; + } + cur = unitary.getInputQubit(0); + } + return nullptr; +} + +/// Whether both input wires of `op` come from one earlier two-qubit run, making +/// `op` a continuation of that run rather than a fresh run start. +static bool feedsFromSameTwoQubitRun(UnitaryOpInterface op) { + const Value in0 = op.getInputQubit(0); + const Value in1 = op.getInputQubit(1); + assert(in0.hasOneUse() && in1.hasOneUse() && + "qubit values are single-use, so a run member consumes each input " + "exactly once"); + Operation* gate0 = twoQubitGateAtEndOfOneQChain(in0); + Operation* gate1 = twoQubitGateAtEndOfOneQChain(in1); + return gate0 != nullptr && gate0 == gate1; +} + +// --- Run scanning --------------------------------------------------------- // + +/// Appends a two-qubit gate to `run`, composing its matrix. No-op unless both +/// of `op`'s inputs are the run's current tail wires (in either order), keeping +/// the run confined to a single pair of wires. +static void absorbTwoQubitIntoRun(FusableTwoQubitRun& run, + UnitaryOpInterface op, + const Matrix4x4& opMatrix) { + const Value in0 = op.getInputQubit(0); + const Value in1 = op.getInputQubit(1); + size_t id0 = 0; + size_t id1 = 1; + if (in0 == run.tailA && in1 == run.tailB) { + run.tailA = op.getOutputQubit(0); + run.tailB = op.getOutputQubit(1); + } else if (in0 == run.tailB && in1 == run.tailA) { + id0 = 1; + id1 = 0; + run.tailA = op.getOutputQubit(1); + run.tailB = op.getOutputQubit(0); + } else { + llvm_unreachable( + "a unique user of both tail wires connects to both of them"); + } + run.composed.premultiplyBy(opMatrix.reorderForQubits(id0, id1)); + run.ops.push_back(op.getOperation()); + ++run.numTwoQ; +} + +/// Appends a single-qubit gate on run wire `wireIndex` (0 = A, 1 = B). +static void absorbOneQubitIntoRun(FusableTwoQubitRun& run, + UnitaryOpInterface op, + const Matrix2x2& opMatrix, + unsigned wireIndex) { + run.composed.premultiplyBy(opMatrix.embedInTwoQubit(wireIndex)); + run.ops.push_back(op.getOperation()); + (wireIndex == 0 ? run.tailA : run.tailB) = op.getOutputQubit(0); +} + +/// Walks forward from `head`, composing the run's matrix and metadata. Absorbs +/// a following two-qubit gate when it keeps both run wires together, otherwise +/// the single-qubit gate first in program order; stops at the first boundary +/// that would split the run's two wires. +static FusableTwoQubitRun scanFusableTwoQubitRun(UnitaryOpInterface head, + const Matrix4x4& headMatrix) { + FusableTwoQubitRun run; + run.composed = headMatrix; + run.tailA = head.getOutputQubit(0); + run.tailB = head.getOutputQubit(1); + run.ops.push_back(head.getOperation()); + run.numTwoQ = 1; + + while (true) { + UnitaryOpInterface nextOnA = uniqueUnitaryUser(run.tailA); + UnitaryOpInterface nextOnB = uniqueUnitaryUser(run.tailB); + const bool sameOp = + nextOnA && nextOnB && nextOnA.getOperation() == nextOnB.getOperation(); + + if (sameOp && nextOnA.isTwoQubit()) { + const auto matrix = twoQubitRunMemberMatrix(nextOnA); + if (!matrix) { + break; + } + absorbTwoQubitIntoRun(run, nextOnA, *matrix); + continue; + } + + const auto matrixA = + !sameOp ? oneQubitRunMemberMatrix(nextOnA) : std::nullopt; + const auto matrixB = + !sameOp ? oneQubitRunMemberMatrix(nextOnB) : std::nullopt; + const bool aSingle = matrixA.has_value(); + const bool bSingle = matrixB.has_value(); + if (aSingle && bSingle && nextOnA->getBlock() != nextOnB->getBlock()) { + break; + } + if (aSingle && (!bSingle || nextOnA->isBeforeInBlock(nextOnB))) { + absorbOneQubitIntoRun(run, nextOnA, *matrixA, /*wireIndex=*/0); + continue; + } + if (bSingle) { + absorbOneQubitIntoRun(run, nextOnB, *matrixB, /*wireIndex=*/1); + continue; + } + break; + } + return run; +} + +/// Erases all run members, successors first so each is dead when erased. +static void eraseFusableRun(RewriterBase& rewriter, + const FusableTwoQubitRun& run) { + for (Operation* member : llvm::reverse(run.ops)) { + rewriter.eraseOp(member); + } +} + +/// Fuses a maximal constant run only when generic resynthesis strictly reduces +/// its two-qubit operation count. +static bool fuseTwoQubitGateRun(IRRewriter& rewriter, UnitaryOpInterface head, + const Matrix4x4& headMatrix, + const CompilerTarget::SynthesisBasis basis) { + FusableTwoQubitRun run = scanFusableTwoQubitRun(head, headMatrix); + if (run.ops.size() < 2) { + return false; + } + + const auto native = decomposeUnitary2QWeyl(run.composed, basis.entangler); + if (native.numBasisUses >= run.numTwoQ) { + return false; + } + + auto firstOp = cast(run.ops.front()); + rewriter.setInsertionPoint(firstOp); + const auto synthesized = + emitUnitary2QWeyl(rewriter, firstOp.getLoc(), firstOp.getInputQubit(0), + firstOp.getInputQubit(1), native, basis); + decomposition::emitGPhaseIfNeeded(rewriter, firstOp.getLoc(), + synthesized.globalPhase); + rewriter.replaceAllUsesWith(run.tailA, synthesized.qubit0); + rewriter.replaceAllUsesWith(run.tailB, synthesized.qubit1); + eraseFusableRun(rewriter, run); + return true; +} + +static bool requiresTargetSynthesis(Operation* operation, + const CompilerTarget& target) { + return !target.supports(operation); +} + +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; + } + + if (unitary.isSingleQubit()) { + Matrix2x2 matrix; + if (unitary.getUnitaryMatrix2x2(matrix)) { + plan.operations.emplace_back(operation); + return WalkResult::advance(); + } + } else { + Matrix4x4 matrix; + if (assignTwoQubitOpMatrix(operation, matrix)) { + plan.operations.emplace_back(operation); + return WalkResult::advance(); + } + } + plan.matrixUnavailable = operation; + return WalkResult::interrupt(); + }); + return plan; +} + +static void lowerTargetOperation(IRRewriter& rewriter, UnitaryOpInterface op, + const CompilerTarget::SynthesisBasis basis) { + Operation* const operation = op.getOperation(); + rewriter.setInsertionPoint(operation); + if (op.isSingleQubit()) { + Matrix2x2 matrix; + op.getUnitaryMatrix2x2(matrix); + const auto synthesized = decomposition::synthesizeUnitary1QEuler( + rewriter, operation->getLoc(), op.getInputQubit(0), matrix, + /*runSize=*/1, /*hasNonBasisGate=*/true, basis.singleQubit); + if (!synthesized) { + llvm::reportFatalInternalError( + "target single-qubit basis failed to synthesize a unitary matrix"); + } + decomposition::emitGPhaseIfNeeded(rewriter, operation->getLoc(), + synthesized->globalPhase); + rewriter.replaceOp(operation, synthesized->qubit); + return; + } + + 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); + } + + const auto native = decomposeUnitary2QWeyl(matrix, basis.entangler); + const auto synthesized = emitUnitary2QWeyl(rewriter, operation->getLoc(), + input0, input1, native, basis); + decomposition::emitGPhaseIfNeeded(rewriter, operation->getLoc(), + synthesized.globalPhase); + rewriter.replaceOp(operation, + ValueRange{synthesized.qubit0, synthesized.qubit1}); +} + +static LogicalResult fuseTwoQubitGates(ModuleOp moduleOp) { + constexpr CompilerTarget::SynthesisBasis basis{ + .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); + } + } + if (!changed) { + return success(); + } + return mlir::mqt::normalizeGlobalPhases(moduleOp); +} + +namespace { + +struct FuseTwoQubitGatesPass final + : PassWrapper> { + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(FuseTwoQubitGatesPass) + + void getDependentDialects(DialectRegistry& registry) const override { + registry.insert(); + } + +protected: + void runOnOperation() override { + ModuleOp moduleOp = getOperation(); + if (failed(fuseTwoQubitGates(moduleOp))) { + signalPassFailure(); + } + } +}; + +struct TargetNativeSynthesisPass final + : PassWrapper> { + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(TargetNativeSynthesisPass) + + explicit TargetNativeSynthesisPass(const CompilerTarget& targetIn) + : target(targetIn) {} + + void getDependentDialects(DialectRegistry& registry) const override { + registry.insert(); + } + +protected: + void runOnOperation() override { + if (!target.hasExplicitOperations()) { + return; + } + ModuleOp moduleOp = getOperation(); + 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"; + 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"; + signalPassFailure(); + return; + } + + IRRewriter rewriter(&getContext()); + for (Operation* operation : plan.operations) { + lowerTargetOperation(rewriter, cast(operation), + *targetBasis); + } + if (failed(mlir::mqt::normalizeGlobalPhases(moduleOp))) { + signalPassFailure(); + } + } + + CompilerTarget target; +}; + +struct VerifyTargetConformancePass final + : PassWrapper> { + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(VerifyTargetConformancePass) + + explicit VerifyTargetConformancePass(const CompilerTarget& targetIn) + : target(targetIn) {} + +protected: + void runOnOperation() override { + WalkResult result = getOperation()->walk([&](Operation* operation) { + if (auto function = dyn_cast(operation); + function && + llvm::any_of(function.getArgumentTypes(), [](const auto type) { + if (isa(type)) { + return true; + } + const auto tensor = dyn_cast(type); + return tensor && isa(tensor.getElementType()); + })) { + function.emitError() + << "target conformance requires quantum function inputs to be " + "assigned to qco.static target sites"; + return WalkResult::interrupt(); + } + if (auto staticOp = dyn_cast(operation)) { + const auto site = + static_cast(staticOp.getIndex()); + if (target.vertexForSite(site)) { + return WalkResult::advance(); + } + staticOp.emitError() << "target does not contain static site " << site; + return WalkResult::interrupt(); + } + if (isa(operation)) { + operation->emitError() + << "target conformance requires qubits to be assigned to " + "qco.static target sites"; + return WalkResult::interrupt(); + } + + size_t arity = 1; + size_t parameterCount = 0; + if (auto unitary = dyn_cast(operation)) { + if (isExcludedFromTopLevelUnitaryWalk(operation)) { + return WalkResult::advance(); + } + arity = unitary.getNumQubits(); + parameterCount = unitary.getNumParams(); + } else if (!isa(operation)) { + return WalkResult::advance(); + } + + if (target.supports(operation)) { + return WalkResult::advance(); + } + + auto diagnostic = operation->emitError() + << "target does not support operation '" + << operation->getName() << "' with arity " << arity + << " and " << parameterCount << " parameter(s)"; + return WalkResult::interrupt(); + }); + if (result.wasInterrupted()) { + signalPassFailure(); + } + } + + CompilerTarget target; +}; + +} // namespace + +std::unique_ptr createFuseTwoQubitGates() { + return std::make_unique(); +} + +std::unique_ptr +createTargetNativeSynthesis(const CompilerTarget& target) { + return std::make_unique(target); +} + +std::unique_ptr +createVerifyTargetConformance(const CompilerTarget& target) { + return std::make_unique(target); +} + +} // namespace mlir::qco diff --git a/mlir/lib/Support/Passes.cpp b/mlir/lib/Support/Passes.cpp index d906d72ff1..e0032aa3b6 100644 --- a/mlir/lib/Support/Passes.cpp +++ b/mlir/lib/Support/Passes.cpp @@ -50,7 +50,6 @@ void registerMQTCompilerPasses() { static const auto REGISTERED = [] { qco::registerDecomposeMultiControlled(); qco::registerFuseSingleQubitUnitaryRuns(); - qco::registerFuseTwoQubitUnitaryRuns(); qco::registerHadamardLifting(); qco::registerMeasurementLifting(); qco::registerMergeSingleQubitRotationGates(); diff --git a/mlir/tools/mqt-cc/mqt-cc.cpp b/mlir/tools/mqt-cc/mqt-cc.cpp index 0f22d57975..69870df47a 100644 --- a/mlir/tools/mqt-cc/mqt-cc.cpp +++ b/mlir/tools/mqt-cc/mqt-cc.cpp @@ -17,7 +17,6 @@ #include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QC/Translation/TranslateQASM3ToQC.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" -#include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" #include "mlir/Support/Passes.h" @@ -85,13 +84,6 @@ static llvm::cl::opt outputFormat( "qir-adaptive, or jeff"), llvm::cl::value_desc("format"), llvm::cl::init("mlir")); -static llvm::cl::opt nativeGates( - "native-gates", - llvm::cl::desc( - "Comma-separated native gate menu for the fuse-two-qubit-unitary-runs " - "pass"), - llvm::cl::value_desc("csv"), llvm::cl::init("")); - namespace { enum class InputFormat : std::uint8_t { MLIR, QASM, Jeff }; enum class InputDialect : std::uint8_t { QC, QCO }; @@ -397,19 +389,6 @@ static int runCompiler(int argc, char** argv) { "QCO optimization.\n"; return 1; } - const llvm::StringRef nativeGateMenu = - llvm::StringRef(nativeGates.getValue()).trim(); - if (nativeGates.getNumOccurrences() > 0 && nativeGateMenu.empty()) { - llvm::errs() << "--native-gates must not be empty.\n"; - return 1; - } - if (nativeGates.getNumOccurrences() > 0 && - (*parsedOutputFormat == OutputFormat::QCImport || - *parsedOutputFormat == OutputFormat::QCO)) { - llvm::errs() << "--native-gates requires an output that passes through " - "QCO optimization.\n"; - return 1; - } if (enableDecomposeMultiControlled && !isDecomposeMultiControlledConfigValid( decomposeMultiControlledMinControls.getValue())) { @@ -459,12 +438,6 @@ static int runCompiler(int argc, char** argv) { populateDefaultQCOOptimizationPipeline(pm); } populateQCOCleanupPipeline(pm); - if (!nativeGateMenu.empty()) { - pm.addPass(qco::createFuseTwoQubitUnitaryRuns( - qco::FuseTwoQubitUnitaryRunsOptions{ - .nativeGates = nativeGateMenu.str(), - })); - } return success(); }))) { return 1; diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 8f5cdec5f7..503bab4098 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -918,9 +918,6 @@ cx q[0], q[2]; EXPECT_TRUE(qco.fuseSingleQubitUnitaryRuns("zyz")); EXPECT_NE(qco.str(), beforeFusion); - const auto beforeTwoQubitFusion = qco.str(); - EXPECT_TRUE(qco.fuseTwoQubitUnitaryRuns("u,cx")); - EXPECT_NE(qco.str(), beforeTwoQubitFusion); const std::vector> coupling = {{0, 1}, {1, 2}}; EXPECT_TRUE(qco.placeAndRoute(coupling)); @@ -1641,106 +1638,4 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(mlir::qir::singleControlledXOnIndividualQubits), false, true, "reuse-qubits,mqt-qco-default"})); -/** - * @brief Test: fuseTwoQubitUnitaryRuns lowers one- and two-qubit operations - * into native menus. - */ -TEST_F(CompilerPipelineTest, FuseTwoQubitUnitaryRunsLowersToNativeMenu) { - const std::string qasm = R"(OPENQASM 3.0; -include "stdgates.inc"; -qubit[2] q; -h q[0]; -swap q[0], q[1]; -)"; - auto qc = QCProgram::fromQASMString(qasm); - ASSERT_TRUE(qc); - auto qcoResult = std::move(*qc).intoQCO(); - ASSERT_TRUE(qcoResult); - auto qco = std::move(*qcoResult); - ASSERT_TRUE(qco.cleanup()); - EXPECT_NE(qco.str().find("qco.h"), std::string::npos); - EXPECT_NE(qco.str().find("qco.swap"), std::string::npos); - - EXPECT_TRUE(qco.fuseTwoQubitUnitaryRuns("x,sx,rz,cx")); - EXPECT_EQ(qco.str().find("qco.h"), std::string::npos); - EXPECT_EQ(qco.str().find("qco.swap"), std::string::npos); - EXPECT_NE(qco.str().find("qco.ctrl"), std::string::npos); - - auto qcU = QCProgram::fromQASMString(qasm); - ASSERT_TRUE(qcU); - auto qcoUResult = std::move(*qcU).intoQCO(); - ASSERT_TRUE(qcoUResult); - auto qcoU = std::move(*qcoUResult); - ASSERT_TRUE(qcoU.cleanup()); - EXPECT_TRUE(qcoU.fuseTwoQubitUnitaryRuns("u,cx")); - EXPECT_EQ(qcoU.str().find("qco.h"), std::string::npos); - EXPECT_EQ(qcoU.str().find("qco.swap"), std::string::npos); - EXPECT_NE(qcoU.str().find("qco.u"), std::string::npos); - EXPECT_NE(qcoU.str().find("qco.ctrl"), std::string::npos); - - auto qcExpanded = QCProgram::fromQASMString(qasm); - ASSERT_TRUE(qcExpanded); - auto qcoExpandedResult = std::move(*qcExpanded).intoQCO(); - ASSERT_TRUE(qcoExpandedResult); - auto qcoExpanded = std::move(*qcoExpandedResult); - ASSERT_TRUE(qcoExpanded.cleanup()); - EXPECT_TRUE(qcoExpanded.fuseTwoQubitUnitaryRuns("u,rx,rz,cx,cz")); - EXPECT_EQ(qcoExpanded.str().find("qco.h"), std::string::npos); - EXPECT_EQ(qcoExpanded.str().find("qco.swap"), std::string::npos); - EXPECT_NE(qcoExpanded.str().find("qco.ctrl"), std::string::npos); -} - -/** - * @brief Test: fuseTwoQubitUnitaryRuns rejects invalid native gate menus. - */ -TEST_F(CompilerPipelineTest, FuseTwoQubitUnitaryRunsRejectsInvalidMenus) { - const std::string qasm = R"(OPENQASM 3.0; -include "stdgates.inc"; -qubit[2] q; -h q[0]; -h q[1]; -)"; - auto qc = QCProgram::fromQASMString(qasm); - ASSERT_TRUE(qc); - auto qcoResult = std::move(*qc).intoQCO(); - ASSERT_TRUE(qcoResult); - auto qco = std::move(*qcoResult); - ASSERT_TRUE(qco.cleanup()); - EXPECT_FALSE(qco.fuseTwoQubitUnitaryRuns("cx,cz")); - - auto qcInvalid = QCProgram::fromQASMString(qasm); - ASSERT_TRUE(qcInvalid); - auto qcoInvalidResult = std::move(*qcInvalid).intoQCO(); - ASSERT_TRUE(qcoInvalidResult); - auto qcoInvalid = std::move(*qcoInvalidResult); - ASSERT_TRUE(qcoInvalid.cleanup()); - EXPECT_FALSE(qcoInvalid.fuseTwoQubitUnitaryRuns("not-a-gate")); -} - -/** - * @brief Test: the public API rejects an empty native gate menu. - */ -TEST_F(CompilerPipelineTest, FuseTwoQubitUnitaryRunsRejectsEmptyMenu) { - const std::string qasm = R"(OPENQASM 3.0; -include "stdgates.inc"; -qubit[2] q; -h q[0]; -h q[1]; -)"; - auto qc = QCProgram::fromQASMString(qasm); - ASSERT_TRUE(qc); - auto qcoResult = std::move(*qc).intoQCO(); - ASSERT_TRUE(qcoResult); - auto qco = std::move(*qcoResult); - ASSERT_TRUE(qco.cleanup()); - const auto before = qco.str(); - EXPECT_NE(before.find("qco.h"), std::string::npos); - - EXPECT_FALSE(qco.fuseTwoQubitUnitaryRuns("")); - EXPECT_EQ(qco.str(), before); - - EXPECT_FALSE(qco.fuseTwoQubitUnitaryRuns(" \t ")); - EXPECT_EQ(qco.str(), before); -} - } // namespace mqt::test::compiler diff --git a/mlir/unittests/Compiler/test_compiler_target.cpp b/mlir/unittests/Compiler/test_compiler_target.cpp index a4f9462a84..90c731eaf3 100644 --- a/mlir/unittests/Compiler/test_compiler_target.cpp +++ b/mlir/unittests/Compiler/test_compiler_target.cpp @@ -43,9 +43,9 @@ using Coupling = Target::Coupling; using DurationUnit = Target::DurationUnit; using GateKind = Target::GateKind; using Operation = Target::Operation; -using OperationLocus = Target::OperationLocus; using Site = Target::Site; using SiteId = Target::SiteId; +using SiteTuple = Target::SiteTuple; TEST(CompilerTargetTest, ConstructsDetailedNamedTargetAndSharesStorage) { std::vector sites; @@ -56,8 +56,7 @@ TEST(CompilerTargetTest, ConstructsDetailedNamedTargetAndSharesStorage) { std::vector operations; operations.emplace_back( " PRX ", 1, 2, - std::vector{OperationLocus{{7}, 0, 0.99}, OperationLocus{{2}, 5, 0.98}}, - 0, 0.97); + std::vector{SiteTuple{{7}, 0, 0.99}, SiteTuple{{2}, 5, 0.98}}, 0, 0.97); const Target target{"device", std::move(sites), std::vector{{11, 2}, {2, 11}, {7, 2}}, @@ -78,15 +77,15 @@ TEST(CompilerTargetTest, ConstructsDetailedNamedTargetAndSharesStorage) { EXPECT_EQ(*target.sites()[0].name(), "left"); EXPECT_EQ(target.sites()[0].t1(), 100); EXPECT_EQ(target.sites()[0].t2(), 80); - EXPECT_EQ(target.operations()[0].providerName(), " PRX "); + EXPECT_EQ(target.operations()[0].name(), " PRX "); EXPECT_EQ(target.operations()[0].canonicalName(), "r"); EXPECT_EQ(target.operations()[0].numQubits(), 1); EXPECT_EQ(target.operations()[0].numParameters(), 2); EXPECT_EQ(target.operations()[0].duration(), 0); EXPECT_EQ(target.operations()[0].fidelity(), 0.97); - ASSERT_EQ(target.operations()[0].loci().size(), 2); - EXPECT_EQ(target.operations()[0].loci()[0].duration(), 0); - EXPECT_EQ(target.operations()[0].loci()[0].fidelity(), 0.99); + ASSERT_EQ(target.operations()[0].siteTuples().size(), 2); + EXPECT_EQ(target.operations()[0].siteTuples()[0].duration(), 0); + EXPECT_EQ(target.operations()[0].siteTuples()[0].fidelity(), 0.99); EXPECT_EQ(copy.sites().data(), target.sites().data()); EXPECT_EQ(copy.couplings().data(), target.couplings().data()); @@ -166,22 +165,20 @@ TEST(CompilerTargetTest, RejectsInvalidMetadata) { static_cast( DurationUnit{"ns", std::numeric_limits::infinity()}); }); - expectInvalid([] { static_cast(OperationLocus{{0, 0}}); }); - expectInvalid( - [] { static_cast(OperationLocus{{0}, std::nullopt, -0.1}); }); + expectInvalid([] { static_cast(SiteTuple{{0, 0}}); }); + expectInvalid([] { static_cast(SiteTuple{{0}, std::nullopt, -0.1}); }); expectInvalid([] { static_cast(Operation{"", 1, 0}); }); expectInvalid([] { static_cast(Operation{"x", 0, 0}); }); expectInvalid([] { - static_cast( - Operation{"x", 1, 0, std::vector{OperationLocus{{0, 1}}}}); + static_cast(Operation{"x", 1, 0, std::vector{SiteTuple{{0, 1}}}}); }); expectInvalid([] { - static_cast(Operation{ - "x", 1, 0, std::vector{OperationLocus{{0}}, OperationLocus{{0}}}}); + static_cast( + Operation{"x", 1, 0, std::vector{SiteTuple{{0}}, SiteTuple{{0}}}}); }); expectInvalid([] { - static_cast(Operation{"x", 1, 0, std::nullopt, std::nullopt, - std::numeric_limits::quiet_NaN()}); + static_cast(Operation{ + "x", 1, 0, {}, std::nullopt, std::numeric_limits::quiet_NaN()}); }); expectInvalid([] { static_cast(Target{std::vector{}}); }); @@ -191,12 +188,12 @@ TEST(CompilerTargetTest, RejectsInvalidMetadata) { static_cast(Target{std::vector{Site{0, std::nullopt, 1}}}); }); expectInvalid([] { - static_cast(Target{ - 1, std::nullopt, std::vector{Operation{"x", 1, 0, std::nullopt, 1}}}); + static_cast( + Target{1, std::nullopt, std::vector{Operation{"x", 1, 0, {}, 1}}}); }); expectInvalid([] { std::vector operations; - operations.emplace_back("x", 1, 0, std::vector{OperationLocus{{0}, 1}}); + operations.emplace_back("x", 1, 0, std::vector{SiteTuple{{0}, 1}}); static_cast(Target{1, std::nullopt, std::move(operations)}); }); expectInvalid( @@ -207,119 +204,80 @@ TEST(CompilerTargetTest, RejectsInvalidMetadata) { [] { static_cast(Target{3, std::vector{{0, 1}}}); }); expectInvalid([] { std::vector operations; - operations.emplace_back("x", 1, 0, std::vector{OperationLocus{{2}}}); + operations.emplace_back("x", 1, 0, std::vector{SiteTuple{{2}}}); static_cast(Target{2, std::nullopt, std::move(operations)}); }); + expectInvalid([] { + static_cast( + Target{1, std::nullopt, std::vector{Operation{"cx", 2, 0}}}); + }); } TEST(CompilerTargetTest, DistinguishesAbsentAndEmptyOperationSets) { const Target permissive{2}; const Target closed{2, std::nullopt, std::vector{}}; - const Operation globalX{"x", 1, 0}; - const Operation globalCX{"cx", 2, 0}; EXPECT_FALSE(permissive.hasExplicitOperations()); EXPECT_TRUE(permissive.operations().empty()); - EXPECT_TRUE(permissive.supportsOperation("provider.operation", {0})); - EXPECT_TRUE(permissive.supports(GateKind::CX, {0, 1})); - EXPECT_FALSE(permissive.supportsOperation("x", {0, 0})); - EXPECT_FALSE(permissive.supportsOperation("x", {2})); - EXPECT_FALSE(permissive.supportsOperation("", {0})); - EXPECT_FALSE(permissive.supportsOperation(" ", {0})); - EXPECT_FALSE(permissive.supportsOperation("x", {})); - EXPECT_FALSE(globalX.supports({-1})); - EXPECT_FALSE(globalCX.supports({0, 0})); + EXPECT_TRUE(permissive.supportsOperation("device.operation", 1)); + EXPECT_TRUE(permissive.supports(GateKind::CX)); + EXPECT_FALSE(permissive.supportsOperation("", 1)); + EXPECT_FALSE(permissive.supportsOperation(" ", 1)); + EXPECT_FALSE(permissive.supportsOperation("x", 0)); + EXPECT_FALSE(permissive.supportsOperation("x", 3)); EXPECT_TRUE(closed.hasExplicitOperations()); EXPECT_TRUE(closed.operations().empty()); - EXPECT_FALSE(closed.supportsOperation("x", {0})); - EXPECT_FALSE(closed.supports(GateKind::CX, {0, 1})); - EXPECT_TRUE(closed.globallySupportedGates().empty()); + EXPECT_FALSE(closed.supportsOperation("x", 1)); + EXPECT_FALSE(closed.supports(GateKind::CX)); + EXPECT_TRUE(closed.supportedGates().empty()); EXPECT_FALSE(closed.synthesisBasis()); } -TEST(CompilerTargetTest, PreservesOrderedLociAndResolvesGlobalBasis) { +TEST(CompilerTargetTest, PreservesCalibrationAndResolvesHomogeneousBasis) { const std::vector chain{{0, 1}, {1, 2}}; const Operation globalU{"U3", 1, 3}; - const Operation symmetricCZ{ - "cz", 2, 0, std::vector{OperationLocus{{1, 0}}, OperationLocus{{1, 2}}}}; - const Target symmetric{3, chain, std::vector{globalU, symmetricCZ}}; - - EXPECT_TRUE(symmetric.supportsOperation("u", {0}, 3)); - EXPECT_TRUE(symmetric.supportsOperation(" U3 ", {2}, 3)); - EXPECT_TRUE(symmetric.supports(GateKind::CZ, {1, 0})); - EXPECT_FALSE(symmetric.supports(GateKind::CZ, {0, 1})); - EXPECT_TRUE( - llvm::is_contained(symmetric.globallySupportedGates(), GateKind::CZ)); - ASSERT_TRUE(symmetric.synthesisBasis()); - EXPECT_EQ(symmetric.synthesisBasis()->singleQubit, - Target::SingleQubitBasis::U); - EXPECT_EQ(symmetric.synthesisBasis()->entangler, GateKind::CZ); - - const Operation oneWayCX{ - "CNOT", 2, 0, - std::vector{OperationLocus{{0, 1}}, OperationLocus{{1, 2}}}}; - const Target oneWay{3, chain, std::vector{globalU, oneWayCX}}; - EXPECT_TRUE(oneWay.supports(GateKind::CX, {0, 1})); - EXPECT_FALSE(oneWay.supports(GateKind::CX, {1, 0})); - EXPECT_FALSE( - llvm::is_contained(oneWay.globallySupportedGates(), GateKind::CX)); - EXPECT_FALSE(oneWay.synthesisBasis()); - - const Operation twoWayCX{ - "cnot", 2, 0, - std::vector{OperationLocus{{0, 1}}, OperationLocus{{1, 0}}, - OperationLocus{{1, 2}}, OperationLocus{{2, 1}}}}; - const Target twoWay{3, chain, std::vector{globalU, twoWayCX}}; - EXPECT_TRUE( - llvm::is_contained(twoWay.globallySupportedGates(), GateKind::CX)); - ASSERT_TRUE(twoWay.synthesisBasis()); - EXPECT_EQ(twoWay.synthesisBasis()->entangler, GateKind::CX); + const Operation cz{"cz", 2, 0, std::vector{SiteTuple{{1, 0}, 5, 0.99}}}; + const Target target{3, chain, std::vector{globalU, cz}, + DurationUnit{"ns", 1.}}; + + EXPECT_TRUE(target.supportsOperation("u", 1, 3)); + EXPECT_TRUE(target.supportsOperation(" U3 ", 1, 3)); + EXPECT_TRUE(target.supports(GateKind::CZ)); + EXPECT_TRUE(llvm::is_contained(target.supportedGates(), GateKind::CZ)); + ASSERT_EQ(target.operations().size(), 2U); + ASSERT_EQ(target.operations()[1].siteTuples().size(), 1U); + EXPECT_EQ(target.operations()[1].siteTuples()[0].sites(), + (llvm::ArrayRef{1, 0})); + EXPECT_EQ(target.operations()[1].siteTuples()[0].duration(), 5); + EXPECT_EQ(target.operations()[1].siteTuples()[0].fidelity(), 0.99); + ASSERT_TRUE(target.synthesisBasis()); + EXPECT_EQ(target.synthesisBasis()->singleQubit, Target::SingleQubitBasis::U); + EXPECT_EQ(target.synthesisBasis()->entangler, GateKind::CZ); } -TEST(CompilerTargetTest, ClassifiesEveryEntanglerOrientation) { +TEST(CompilerTargetTest, ClassifiesEveryEntangler) { using Entangler = std::tuple; - const std::array symmetricEntanglers{ - Entangler{GateKind::CZ, "cz", 0}, Entangler{GateKind::RXX, "rxx", 1}, - Entangler{GateKind::RYY, "ryy", 1}, Entangler{GateKind::RZZ, "rzz", 1}, - Entangler{GateKind::ISWAP, "iswap", 0}}; - const std::array directionalEntanglers{Entangler{GateKind::CX, "cx", 0}, - Entangler{GateKind::ECR, "ecr", 0}, - Entangler{GateKind::RZX, "rzx", 1}}; + const std::array entanglers{Entangler{GateKind::CZ, "cz", 0}, + Entangler{GateKind::RXX, "rxx", 1}, + Entangler{GateKind::RYY, "ryy", 1}, + Entangler{GateKind::RZZ, "rzz", 1}, + Entangler{GateKind::ISWAP, "iswap", 0}, + Entangler{GateKind::CX, "cx", 0}, + Entangler{GateKind::ECR, "ecr", 0}, + Entangler{GateKind::RZX, "rzx", 1}}; const std::vector chain{{0, 1}, {1, 2}}; const Operation globalU{"u", 1, 3}; - for (const auto& [gate, name, numParameters] : symmetricEntanglers) { + for (const auto& [gate, name, numParameters] : entanglers) { SCOPED_TRACE(name); - const Operation oneOrientation{ - std::string{name}, 2, numParameters, - std::vector{OperationLocus{{1, 0}}, OperationLocus{{1, 2}}}}; - const Target target{3, chain, std::vector{globalU, oneOrientation}}; - EXPECT_TRUE(llvm::is_contained(target.globallySupportedGates(), gate)); - EXPECT_TRUE(target.supports(gate, {1, 0})); - EXPECT_FALSE(target.supports(gate, {0, 1})); + const Operation operation{std::string{name}, 2, numParameters}; + const Target target{3, chain, std::vector{globalU, operation}}; + EXPECT_TRUE(llvm::is_contained(target.supportedGates(), gate)); + EXPECT_TRUE(target.supports(gate)); ASSERT_TRUE(target.synthesisBasis()); EXPECT_EQ(target.synthesisBasis()->entangler, gate); } - - for (const auto& [gate, name, numParameters] : directionalEntanglers) { - SCOPED_TRACE(name); - const Operation oneOrientation{ - std::string{name}, 2, numParameters, - std::vector{OperationLocus{{0, 1}}, OperationLocus{{1, 2}}}}; - const Target oneWay{3, chain, std::vector{globalU, oneOrientation}}; - EXPECT_FALSE(llvm::is_contained(oneWay.globallySupportedGates(), gate)); - EXPECT_FALSE(oneWay.synthesisBasis()); - - const Operation bothOrientations{ - std::string{name}, 2, numParameters, - std::vector{OperationLocus{{0, 1}}, OperationLocus{{1, 0}}, - OperationLocus{{1, 2}}, OperationLocus{{2, 1}}}}; - const Target twoWay{3, chain, std::vector{globalU, bothOrientations}}; - EXPECT_TRUE(llvm::is_contained(twoWay.globallySupportedGates(), gate)); - ASSERT_TRUE(twoWay.synthesisBasis()); - EXPECT_EQ(twoWay.synthesisBasis()->entangler, gate); - } } TEST(CompilerTargetTest, SupportsRealQCOOperationsAndStructuralOps) { @@ -379,25 +337,24 @@ TEST(CompilerTargetTest, SupportsRealQCOOperationsAndStructuralOps) { const Target target{ std::vector{Site{10}, Site{20}}, std::nullopt, - std::vector{Operation{"x", 1, 0}, Operation{"measure", 1, 0}, - Operation{"reset", 1, 0}, - Operation{"cnot", 2, 0, - std::vector{OperationLocus{{10, 20}}, - OperationLocus{{20, 10}}}}}}; - EXPECT_TRUE(target.supports(x, {10})); - EXPECT_FALSE(target.supports(x, {10, 20})); - EXPECT_TRUE(target.supports(cx, {10, 20})); - EXPECT_TRUE(target.supports(measure, {20})); - EXPECT_TRUE(target.supports(reset, {10})); - EXPECT_TRUE(target.supports(barrier, {10, 20})); - EXPECT_TRUE(target.supports(gphase, {})); - EXPECT_FALSE(target.supports(nullptr, {10})); + std::vector{ + Operation{"x", 1, 0}, Operation{"measure", 1, 0}, + Operation{"reset", 1, 0}, + Operation{"cnot", 2, 0, + std::vector{SiteTuple{{10, 20}}, SiteTuple{{20, 10}}}}}}; + EXPECT_TRUE(target.supports(x)); + EXPECT_TRUE(target.supports(cx)); + EXPECT_TRUE(target.supports(measure)); + EXPECT_TRUE(target.supports(reset)); + EXPECT_TRUE(target.supports(barrier)); + EXPECT_TRUE(target.supports(gphase)); + EXPECT_FALSE(target.supports(nullptr)); const Target closed{2, std::nullopt, std::vector{}}; - EXPECT_TRUE(closed.supports(barrier, {0, 1})); - EXPECT_TRUE(closed.supports(gphase, {})); - EXPECT_FALSE(closed.supports(x, {0})); - EXPECT_FALSE(closed.supports(measure, {0})); + EXPECT_TRUE(closed.supports(barrier)); + EXPECT_TRUE(closed.supports(gphase)); + EXPECT_FALSE(closed.supports(x)); + EXPECT_FALSE(closed.supports(measure)); } } // namespace diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp index 397b7c7a26..c6e2e1bb7f 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -59,7 +59,7 @@ using namespace mlir; using namespace mlir::qco; using namespace mlir::qco::decomposition; -using enum EulerBasis; +using enum SingleQubitBasis; // File layout: // 1. Fixtures and parametric test types @@ -100,7 +100,7 @@ struct SynthesizedCircuit { class EulerSynthesisExactTest : public testing::TestWithParam< - std::tuple> {}; + std::tuple> {}; } // namespace @@ -256,7 +256,7 @@ template } [[nodiscard]] static std::size_t countBasisGates(func::FuncOp funcOp, - EulerBasis basis) { + SingleQubitBasis basis) { switch (basis) { case ZYZ: return countZYZGates(funcOp); @@ -277,7 +277,8 @@ template } [[nodiscard]] static SynthesizedCircuit -synthesizeMatrix(MLIRContext* ctx, const Matrix2x2& matrix, EulerBasis basis) { +synthesizeMatrix(MLIRContext* ctx, const Matrix2x2& matrix, + SingleQubitBasis basis) { OwningOpRef mlirModule = ModuleOp::create(UnknownLoc::get(ctx)); OpBuilder builder(ctx); builder.setInsertionPointToStart(mlirModule->getBody()); @@ -303,13 +304,13 @@ synthesizeMatrix(MLIRContext* ctx, const Matrix2x2& matrix, EulerBasis basis) { [[nodiscard]] static std::size_t expectedGateCount(MLIRContext* ctx, const Matrix2x2& segment, - EulerBasis basis) { + SingleQubitBasis basis) { return countBasisGates(synthesizeMatrix(ctx, segment, basis).func, basis); } static void checkSynthesizedReferenceExtras(MLIRContext* ctx, func::FuncOp funcOp, - EulerBasis basis, + SingleQubitBasis basis, const Matrix2x2& matrix) { if (basis == U) { EXPECT_EQ(countOps(funcOp), expectedGateCount(ctx, matrix, basis)); @@ -327,7 +328,7 @@ static void checkSynthesizedReferenceExtras(MLIRContext* ctx, template static void expectSynthesizedMatrix(MLIRContext* ctx, const Matrix2x2& matrix, - EulerBasis basis, + SingleQubitBasis basis, ExtraChecksT extraChecks) { const auto circuit = synthesizeMatrix(ctx, matrix, basis); ASSERT_TRUE(succeeded(verify(*circuit.mlirModule))); @@ -457,7 +458,7 @@ TEST(EulerSynthesisTest, RandomReconstructionAllBases) { for (int i = 0; i < 200; ++i) { const auto original = randomUnitaryMatrix(rng); forEachBasis([&fx, &original](StringRef basisStr) { - const auto parsed = parseEulerBasis(basisStr); + const auto parsed = parseSingleQubitBasis(basisStr); ASSERT_TRUE(parsed) << "basis=" << basisStr.str(); const auto circuit = synthesizeMatrix(fx.ctx(), original, *parsed); ASSERT_TRUE(succeeded(verify(*circuit.mlirModule))) @@ -555,7 +556,7 @@ TEST(EulerAnglesCoverageTest, Mod2PiPreservesNonFinitePhase) { //===----------------------------------------------------------------------===// [[nodiscard]] static bool isAllowedBasisGate(const Operation& op, - EulerBasis basis) { + SingleQubitBasis basis) { switch (basis) { case ZYZ: return isa(op); @@ -580,7 +581,7 @@ template [[nodiscard]] static bool inParent(Operation* op) { } static WalkResult visitBasisGateOp(Operation* op, StringRef basis, - EulerBasis parsedBasis) { + SingleQubitBasis parsedBasis) { if (isa(*op)) { return WalkResult::advance(); } @@ -617,7 +618,7 @@ template } static void expectBasisGatesOnly(func::FuncOp funcOp, StringRef basis) { - const auto parsed = parseEulerBasis(basis); + const auto parsed = parseSingleQubitBasis(basis); ASSERT_TRUE(parsed) << basis.str(); funcOp.walk( @@ -655,7 +656,7 @@ template } static void expectSplitFixtureSegments(func::FuncOp funcOp, StringRef basis, MLIRContext* ctx) { - const auto parsed = parseEulerBasis(basis); + const auto parsed = parseSingleQubitBasis(basis); ASSERT_TRUE(parsed) << basis.str(); const std::size_t ht = expectedGateCount(ctx, splitFixtureHTSegmentMatrix(), *parsed); @@ -686,7 +687,7 @@ template static void expectSplitFixtureSegments(func::FuncOp funcOp, StringRef basis, MLIRContext* ctx, BoundaryPred isBoundary) { - const auto parsed = parseEulerBasis(basis); + const auto parsed = parseSingleQubitBasis(basis); ASSERT_TRUE(parsed) << basis.str(); const std::size_t ht = expectedGateCount(ctx, splitFixtureHTSegmentMatrix(), *parsed); diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index e5efedc328..8e82369894 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -9,10 +9,10 @@ */ #include "dd/Package.hpp" +#include "mlir/Compiler/Target.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/NativeGateset.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" #include "mlir/Dialect/QCO/Utils/DDFunctionality.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" @@ -456,7 +456,7 @@ computeTwoQubitUnitaryFromFunc(func::FuncOp funcOp) { [[nodiscard]] static Synthesized2QCircuit synthesize2QMatrix(MLIRContext* ctx, const Matrix4x4& target, - const NativeGateset& spec) { + const CompilerTarget::SynthesisBasis basis) { OwningOpRef mlirModule = ModuleOp::create(UnknownLoc::get(ctx)); OpBuilder builder(ctx); builder.setInsertionPointToStart(mlirModule->getBody()); @@ -469,21 +469,20 @@ synthesize2QMatrix(MLIRContext* ctx, const Matrix4x4& target, auto* entry = func.addEntryBlock(); builder.setInsertionPointToStart(entry); - const auto synthesized = synthesizeUnitary2QWeyl( - builder, loc, entry->getArgument(0), entry->getArgument(1), target, spec); - if (failed(synthesized)) { - ADD_FAILURE() << "synthesizeUnitary2QWeyl failed during test synthesis"; - } else { - emitGPhaseIfNeeded(builder, loc, synthesized->globalPhase); - func::ReturnOp::create( - builder, loc, ValueRange{synthesized->qubit0, synthesized->qubit1}); - } + const auto decomposition = decomposeUnitary2QWeyl(target, basis.entangler); + const auto synthesized = + emitUnitary2QWeyl(builder, loc, entry->getArgument(0), + entry->getArgument(1), decomposition, basis); + emitGPhaseIfNeeded(builder, loc, synthesized.globalPhase); + func::ReturnOp::create(builder, loc, + ValueRange{synthesized.qubit0, synthesized.qubit1}); return {.mlirModule = std::move(mlirModule), .func = func}; } -static void expectSynthesized2QMatrix(MLIRContext* ctx, const Matrix4x4& target, - const NativeGateset& spec) { - const auto circuit = synthesize2QMatrix(ctx, target, spec); +static void +expectSynthesized2QMatrix(MLIRContext* ctx, const Matrix4x4& target, + const CompilerTarget::SynthesisBasis basis) { + const auto circuit = synthesize2QMatrix(ctx, target, basis); ASSERT_TRUE(succeeded(verify(*circuit.mlirModule))); const auto actual = computeTwoQubitUnitaryFromFunc(circuit.func); ASSERT_TRUE(succeeded(actual)); @@ -508,7 +507,7 @@ struct MlirTestContext { struct WeylSynthesisCase { const char* name; - const char* nativeGates; + CompilerTarget::SynthesisBasis basis; Matrix4x4 (*target)(); }; @@ -519,7 +518,7 @@ class WeylSynthesisTest : public testing::TestWithParam { void SetUp() override { mlir.setUp(); } }; -class NativeGatesetMlirTest : public testing::Test { +class WeylSynthesisMlirTest : public testing::Test { protected: MlirTestContext mlir; @@ -529,22 +528,26 @@ class NativeGatesetMlirTest : public testing::Test { } // namespace TEST_P(WeylSynthesisTest, PreservesTargetUnitary) { - const auto spec = NativeGateset::parse(GetParam().nativeGates); - ASSERT_TRUE(spec); - expectSynthesized2QMatrix(mlir.ctx(), GetParam().target(), *spec); + expectSynthesized2QMatrix(mlir.ctx(), GetParam().target(), GetParam().basis); } INSTANTIATE_TEST_SUITE_P( - Gatesets, WeylSynthesisTest, + TargetBases, WeylSynthesisTest, testing::Values( - WeylSynthesisCase{"CxGeneric", "u,cx", - [] { return TWO_QUBIT_CONTROLLED_X01; }}, - WeylSynthesisCase{"ProductGeneric", "u,cx", - [] { - return Matrix4x4::kron(RZOp::unitaryMatrix(1.0), - RYOp::unitaryMatrix(0.3)); - }}, - WeylSynthesisCase{"IbmBasic", "x,sx,rz,cx", + WeylSynthesisCase{ + "CxGeneric", + {CompilerTarget::SingleQubitBasis::U, CompilerTarget::GateKind::CX}, + [] { return TWO_QUBIT_CONTROLLED_X01; }}, + WeylSynthesisCase{ + "ProductGeneric", + {CompilerTarget::SingleQubitBasis::U, CompilerTarget::GateKind::CX}, + [] { + return Matrix4x4::kron(RZOp::unitaryMatrix(1.0), + RYOp::unitaryMatrix(0.3)); + }}, + WeylSynthesisCase{"IbmBasic", + {CompilerTarget::SingleQubitBasis::ZSXX, + CompilerTarget::GateKind::CX}, [] { return Matrix4x4::kron(HOp::getUnitaryMatrix(), Matrix2x2::identity()) * @@ -553,43 +556,54 @@ INSTANTIATE_TEST_SUITE_P( RYOp::unitaryMatrix(0.1)); }}, WeylSynthesisCase{ - "RxxGeneric", "u,rxx", + "RxxGeneric", + {CompilerTarget::SingleQubitBasis::U, + CompilerTarget::GateKind::RXX}, [] { return RXXOp::unitaryMatrix(std::numbers::pi / 2.0); }}, WeylSynthesisCase{ - "RyyGeneric", "u,ryy", + "RyyGeneric", + {CompilerTarget::SingleQubitBasis::U, + CompilerTarget::GateKind::RYY}, [] { return RYYOp::unitaryMatrix(std::numbers::pi / 2.0); }}, WeylSynthesisCase{ - "RzxGeneric", "u,rzx", + "RzxGeneric", + {CompilerTarget::SingleQubitBasis::U, + CompilerTarget::GateKind::RZX}, [] { return RZXOp::unitaryMatrix(std::numbers::pi / 2.0); }}, WeylSynthesisCase{ - "RzzGeneric", "u,rzz", + "RzzGeneric", + {CompilerTarget::SingleQubitBasis::U, + CompilerTarget::GateKind::RZZ}, [] { return RZZOp::unitaryMatrix(std::numbers::pi / 2.0); }}, - WeylSynthesisCase{"IswapGeneric", "u,iswap", + WeylSynthesisCase{"IswapGeneric", + {CompilerTarget::SingleQubitBasis::U, + CompilerTarget::GateKind::ISWAP}, [] { return iSWAPOp::getUnitaryMatrix(); }}, - WeylSynthesisCase{"CzGeneric", "u,cz", - [] { return TWO_QUBIT_CONTROLLED_Z; }}, - WeylSynthesisCase{"EcrGeneric", "u,ecr", + WeylSynthesisCase{ + "CzGeneric", + {CompilerTarget::SingleQubitBasis::U, CompilerTarget::GateKind::CZ}, + [] { return TWO_QUBIT_CONTROLLED_Z; }}, + WeylSynthesisCase{"EcrGeneric", + {CompilerTarget::SingleQubitBasis::U, + CompilerTarget::GateKind::ECR}, [] { return ECROp::getUnitaryMatrix(); }}), [](const testing::TestParamInfo& info) { return info.param.name; }); TEST(WeylSynthesisTest, IdentityRequiresNoEntanglers) { - for (const char* gateset : {"u,rxx", "u,ryy", "u,rzx", "u,rzz", "u,iswap", - "u,cz", "u,cx", "u,ecr"}) { - const auto spec = NativeGateset::parse(gateset); - ASSERT_TRUE(spec) << gateset; - const auto native = spec->decomposeTarget(Matrix4x4::identity()); - ASSERT_TRUE(native.has_value()) << gateset; - EXPECT_EQ(native->numBasisUses, 0U) << gateset; + for (const auto entangler : + {CompilerTarget::GateKind::RXX, CompilerTarget::GateKind::RYY, + CompilerTarget::GateKind::RZX, CompilerTarget::GateKind::RZZ, + CompilerTarget::GateKind::ISWAP, CompilerTarget::GateKind::CZ, + CompilerTarget::GateKind::CX, CompilerTarget::GateKind::ECR}) { + const auto native = + decomposeUnitary2QWeyl(Matrix4x4::identity(), entangler); + EXPECT_EQ(native.numBasisUses, 0U); } } -TEST(WeylSynthesisTest, RejectsGatesetWithoutEntangler) { - EXPECT_FALSE(NativeGateset::parse("u").has_value()); -} - -TEST_F(NativeGatesetMlirTest, ReconstructionRejectsUnhandledOps) { +TEST_F(WeylSynthesisMlirTest, ReconstructionRejectsUnhandledOps) { OpBuilder builder(mlir.ctx()); const Location loc = UnknownLoc::get(mlir.ctx()); const auto qubitTy = QubitType::get(mlir.ctx()); @@ -604,314 +618,3 @@ TEST_F(NativeGatesetMlirTest, ReconstructionRejectsUnhandledOps) { func::ReturnOp::create(builder, loc, ValueRange{meas.getQubitOut(), q1}); EXPECT_TRUE(failed(computeTwoQubitUnitaryFromFunc(func))); } - -TEST_F(NativeGatesetMlirTest, SynthesisFailsWithoutEulerBasis) { - const NativeGateset spec{.gates = {NativeGateKind::CX}}; - OpBuilder builder(mlir.ctx()); - const auto qubitTy = QubitType::get(mlir.ctx()); - const auto funcTy = - builder.getFunctionType({qubitTy, qubitTy}, {qubitTy, qubitTy}); - auto func = func::FuncOp::create(builder, UnknownLoc::get(mlir.ctx()), "main", - funcTy); - auto* entry = func.addEntryBlock(); - builder.setInsertionPointToStart(entry); - EXPECT_TRUE(failed(synthesizeUnitary2QWeyl( - builder, func.getLoc(), entry->getArgument(0), entry->getArgument(1), - TWO_QUBIT_CONTROLLED_X01, spec))); -} - -TEST_F(NativeGatesetMlirTest, SynthesisFailsWithoutEntangler) { - const NativeGateset spec{.gates = {NativeGateKind::U}}; - OpBuilder builder(mlir.ctx()); - const auto qubitTy = QubitType::get(mlir.ctx()); - const auto funcTy = - builder.getFunctionType({qubitTy, qubitTy}, {qubitTy, qubitTy}); - auto func = func::FuncOp::create(builder, UnknownLoc::get(mlir.ctx()), "main", - funcTy); - auto* entry = func.addEntryBlock(); - builder.setInsertionPointToStart(entry); - EXPECT_TRUE(failed(synthesizeUnitary2QWeyl( - builder, func.getLoc(), entry->getArgument(0), entry->getArgument(1), - TWO_QUBIT_CONTROLLED_X01, spec))); -} - -TEST(WeylSynthesisTest, EntanglerCountFailsWithoutEntangler) { - const NativeGateset spec{.gates = {NativeGateKind::U}}; - EXPECT_FALSE(spec.decomposeTarget(Matrix4x4::identity()).has_value()); -} - -TEST(NativeSpecTest, ParsesAndRejectsGatesets) { - const auto ibm = NativeGateset::parse("x,sx,rz,cx"); - ASSERT_TRUE(ibm); - EXPECT_TRUE(ibm->gates.contains(NativeGateKind::CX)); - EXPECT_TRUE(ibm->gates.contains(NativeGateKind::X)); - EXPECT_FALSE(NativeGateset::parse("x,sx,rz,not-a-gate").has_value()); - EXPECT_FALSE(NativeGateset::parse("u").has_value()); - - const auto whitespaceToken = NativeGateset::parse("u, ,cx"); - ASSERT_TRUE(whitespaceToken); - EXPECT_TRUE(whitespaceToken->gates.contains(NativeGateKind::U)); - EXPECT_TRUE(whitespaceToken->gates.contains(NativeGateKind::CX)); - - EXPECT_FALSE(NativeGateset::parse("x,sx,p,cx").has_value()); - EXPECT_FALSE(NativeGateset::parse("ry,p,cz").has_value()); - - const auto cxOnly = NativeGateset::parse("u,cx"); - ASSERT_TRUE(cxOnly); - EXPECT_TRUE(cxOnly->gates.contains(NativeGateKind::U)); - EXPECT_TRUE(cxOnly->gates.contains(NativeGateKind::CX)); - EXPECT_FALSE(cxOnly->gates.contains(NativeGateKind::CZ)); - EXPECT_FALSE(cxOnly->gates.contains(NativeGateKind::X)); - - const auto both = NativeGateset::parse("u,cx,cz"); - ASSERT_TRUE(both); - EXPECT_TRUE(both->gates.contains(NativeGateKind::CX)); - EXPECT_TRUE(both->gates.contains(NativeGateKind::CZ)); - EXPECT_EQ(both->entangler, NativeGateKind::CZ); - - const auto ecrOnly = NativeGateset::parse("u,ecr"); - ASSERT_TRUE(ecrOnly); - EXPECT_TRUE(ecrOnly->gates.contains(NativeGateKind::ECR)); - EXPECT_EQ(ecrOnly->entangler, NativeGateKind::ECR); - - // With CX/CZ also listed, CX/CZ win over ECR. - const auto cxCzOverEcr = NativeGateset::parse("u,cz,cx,ecr"); - ASSERT_TRUE(cxCzOverEcr); - EXPECT_EQ(cxCzOverEcr->entangler, NativeGateKind::CZ); - - const auto iswapOnly = NativeGateset::parse("u,iswap"); - ASSERT_TRUE(iswapOnly); - EXPECT_TRUE(iswapOnly->gates.contains(NativeGateKind::ISWAP)); - EXPECT_EQ(iswapOnly->entangler, NativeGateKind::ISWAP); - - // iSWAP beats CZ/CX/ECR; two-qubit rotations still win when present. - const auto iswapOverCtrlEcr = NativeGateset::parse("u,iswap,cz,cx,ecr"); - ASSERT_TRUE(iswapOverCtrlEcr); - EXPECT_EQ(iswapOverCtrlEcr->entangler, NativeGateKind::ISWAP); - - // DCX is not a supported native-basis token. - EXPECT_FALSE(NativeGateset::parse("u,dcx").has_value()); - EXPECT_FALSE(NativeGateset::parse("u,cx,dcx").has_value()); - - const auto rzxOnly = NativeGateset::parse("u,rzx"); - ASSERT_TRUE(rzxOnly); - EXPECT_EQ(rzxOnly->entangler, NativeGateKind::RZX); - - // Two-qubit rotations: RXX > RYY > RZX > RZZ (alphabetic). - const auto rxxOverRest = - NativeGateset::parse("u,rzx,rzz,ryy,rxx,iswap,cx,cz,ecr"); - ASSERT_TRUE(rxxOverRest); - EXPECT_EQ(rxxOverRest->entangler, NativeGateKind::RXX); - - const auto ryyOverRzxRzz = NativeGateset::parse("u,rzx,ryy,rzz,iswap,cx"); - ASSERT_TRUE(ryyOverRzxRzz); - EXPECT_EQ(ryyOverRzxRzz->entangler, NativeGateKind::RYY); - - const auto rzxOverRzz = NativeGateset::parse("u,rzx,rzz,iswap,cx,cz"); - ASSERT_TRUE(rzxOverRzz); - EXPECT_EQ(rzxOverRzz->entangler, NativeGateKind::RZX); - - const auto rzzOverDiscrete = NativeGateset::parse("u,rzz,iswap,cx,cz,ecr"); - ASSERT_TRUE(rzzOverDiscrete); - EXPECT_EQ(rzzOverDiscrete->entangler, NativeGateKind::RZZ); - - const auto rzzOnly = NativeGateset::parse("u,rzz"); - ASSERT_TRUE(rzzOnly); - EXPECT_EQ(rzzOnly->entangler, NativeGateKind::RZZ); - - const auto ryyOnly = NativeGateset::parse("u,ryy"); - ASSERT_TRUE(ryyOnly); - EXPECT_EQ(ryyOnly->entangler, NativeGateKind::RYY); - - const auto rxxOnly = NativeGateset::parse("u,rxx"); - ASSERT_TRUE(rxxOnly); - EXPECT_EQ(rxxOnly->entangler, NativeGateKind::RXX); - - const auto rotationThenDiscrete = - NativeGateset::parse("u,rzz,rxx,iswap,cz,cx,ecr"); - ASSERT_TRUE(rotationThenDiscrete); - EXPECT_EQ(rotationThenDiscrete->entangler, NativeGateKind::RXX); - - const auto withoutRxx = NativeGateset::parse("u,ryy,iswap,cz,cx,ecr"); - ASSERT_TRUE(withoutRxx); - EXPECT_EQ(withoutRxx->entangler, NativeGateKind::RYY); - - const auto withoutRotations = NativeGateset::parse("u,iswap,cz,cx,ecr"); - ASSERT_TRUE(withoutRotations); - EXPECT_EQ(withoutRotations->entangler, NativeGateKind::ISWAP); -} - -TEST(NativeSpecTest, RejectsGatesetWithoutSingleQubitStrategy) { - EXPECT_FALSE(NativeGateset::parse("cx").has_value()); - EXPECT_FALSE(NativeGateset::parse("cz").has_value()); - EXPECT_FALSE(NativeGateset::parse("rx,cx").has_value()); -} - -TEST(NativeSpecTest, ResolvesEulerBasisFromGateset) { - const auto uGateset = NativeGateset::parse("u,cx"); - ASSERT_TRUE(uGateset); - EXPECT_EQ(*uGateset->eulerBasis, EulerBasis::U); - - const auto zsxx = NativeGateset::parse("x,sx,rz,cx"); - ASSERT_TRUE(zsxx); - EXPECT_EQ(*zsxx->eulerBasis, EulerBasis::ZSXX); - - const auto rGateset = NativeGateset::parse("r,cz"); - ASSERT_TRUE(rGateset); - EXPECT_EQ(*rGateset->eulerBasis, EulerBasis::R); - - const auto xzx = NativeGateset::parse("rx,rz,cz"); - ASSERT_TRUE(xzx); - EXPECT_EQ(*xzx->eulerBasis, EulerBasis::XZX); - - const auto xyx = NativeGateset::parse("rx,ry,cz"); - ASSERT_TRUE(xyx); - EXPECT_EQ(*xyx->eulerBasis, EulerBasis::XYX); - - const auto zyz = NativeGateset::parse("ry,rz,cz"); - ASSERT_TRUE(zyz); - EXPECT_EQ(*zyz->eulerBasis, EulerBasis::ZYZ); -} - -TEST_F(NativeGatesetMlirTest, AllowsOpMatchesGateset) { - const auto spec = NativeGateset::parse("u,cx"); - ASSERT_TRUE(spec); - - OpBuilder builder(mlir.ctx()); - const Location loc = UnknownLoc::get(mlir.ctx()); - const auto qubitTy = QubitType::get(mlir.ctx()); - const auto funcTy = - builder.getFunctionType({qubitTy, qubitTy}, {qubitTy, qubitTy}); - auto func = func::FuncOp::create(builder, loc, "allows_op", funcTy); - auto* entry = func.addEntryBlock(); - builder.setInsertionPointToStart(entry); - Value q0 = entry->getArgument(0); - Value q1 = entry->getArgument(1); - - EXPECT_TRUE(spec->allowsOp( - BarrierOp::create(builder, loc, ValueRange{q0, q1}).getOperation())); - EXPECT_TRUE( - spec->allowsOp(GPhaseOp::create(builder, loc, 0.1).getOperation())); - EXPECT_TRUE(spec->allowsOp( - UOp::create(builder, loc, q0, 0.1, 0.2, 0.3).getOperation())); - - auto cx = CtrlOp::create(builder, loc, q0, q1, [&](Value target) { - return XOp::create(builder, loc, target).getOutputQubit(0); - }); - EXPECT_TRUE(spec->allowsOp(cx.getOperation())); - - auto cxWithInterleavedH = - CtrlOp::create(builder, loc, q0, q1, [&](Value target) { - auto wire = XOp::create(builder, loc, target).getOutputQubit(0); - return HOp::create(builder, loc, wire).getOutputQubit(0); - }); - EXPECT_FALSE(spec->allowsOp(cxWithInterleavedH.getOperation())); - - EXPECT_FALSE(spec->allowsOp(XOp::create(builder, loc, q0).getOperation())); - EXPECT_FALSE( - spec->allowsOp(RXXOp::create(builder, loc, q0, q1, 0.2).getOperation())); - EXPECT_FALSE( - spec->allowsOp(ECROp::create(builder, loc, q0, q1).getOperation())); - - const auto rzSpec = NativeGateset::parse("x,sx,rz,cx"); - ASSERT_TRUE(rzSpec); - EXPECT_TRUE( - rzSpec->allowsOp(RZOp::create(builder, loc, q0, 0.3).getOperation())); - EXPECT_FALSE( - rzSpec->allowsOp(POp::create(builder, loc, q0, 0.3).getOperation())); - - auto hCtrl = CtrlOp::create(builder, loc, q0, q1, [&](Value target) { - return HOp::create(builder, loc, target).getOutputQubit(0); - }); - EXPECT_FALSE(spec->allowsOp(hCtrl.getOperation())); - - const auto funcTy3 = builder.getFunctionType({qubitTy, qubitTy, qubitTy}, - {qubitTy, qubitTy, qubitTy}); - auto func3 = func::FuncOp::create(builder, loc, "allows_op_ccx", funcTy3); - auto* entry3 = func3.addEntryBlock(); - builder.setInsertionPointToStart(entry3); - Value c0 = entry3->getArgument(0); - Value c1 = entry3->getArgument(1); - Value target = entry3->getArgument(2); - auto ccx = - CtrlOp::create(builder, loc, ValueRange{c0, c1}, target, [&](Value t) { - return XOp::create(builder, loc, t).getOutputQubit(0); - }); - EXPECT_FALSE(spec->allowsOp(ccx.getOperation())); - - const auto czSpec = NativeGateset::parse("u,cz"); - ASSERT_TRUE(czSpec); - auto cz = CtrlOp::create(builder, loc, q0, q1, [&](Value t) { - return ZOp::create(builder, loc, t).getOutputQubit(0); - }); - EXPECT_TRUE(czSpec->allowsOp(cz.getOperation())); - EXPECT_FALSE(czSpec->allowsOp(cx.getOperation())); - - const auto rxxSpec = NativeGateset::parse("u,rxx"); - ASSERT_TRUE(rxxSpec); - EXPECT_TRUE(rxxSpec->allowsOp( - RXXOp::create(builder, loc, q0, q1, 0.2).getOperation())); - EXPECT_TRUE(rxxSpec->allowsOp( - RXXOp::create(builder, loc, q0, q1, std::numbers::pi / 2.0) - .getOperation())); - - const auto ryySpec = NativeGateset::parse("u,ryy"); - ASSERT_TRUE(ryySpec); - EXPECT_TRUE(ryySpec->allowsOp( - RYYOp::create(builder, loc, q0, q1, 0.25).getOperation())); - - const auto rzxSpec = NativeGateset::parse("u,rzx"); - ASSERT_TRUE(rzxSpec); - EXPECT_TRUE(rzxSpec->allowsOp( - RZXOp::create(builder, loc, q0, q1, 0.25).getOperation())); - - const auto rzzSpec = NativeGateset::parse("u,rzz"); - ASSERT_TRUE(rzzSpec); - EXPECT_TRUE(rzzSpec->allowsOp( - RZZOp::create(builder, loc, q0, q1, 0.3).getOperation())); - - const auto iswapSpec = NativeGateset::parse("u,iswap"); - ASSERT_TRUE(iswapSpec); - auto iswap = iSWAPOp::create(builder, loc, q0, q1); - EXPECT_TRUE(iswapSpec->allowsOp(iswap.getOperation())); - EXPECT_FALSE(iswapSpec->allowsOp(cx.getOperation())); - - const auto ecrSpec = NativeGateset::parse("u,ecr"); - ASSERT_TRUE(ecrSpec); - auto ecr = ECROp::create(builder, loc, q0, q1); - EXPECT_TRUE(ecrSpec->allowsOp(ecr.getOperation())); - EXPECT_FALSE(ecrSpec->allowsOp(cx.getOperation())); - - const FloatType f64Float = builder.getF64Type(); - const Type f64Ty = Type::getFromOpaquePointer(f64Float.getAsOpaquePointer()); - const auto funcTyTheta = - builder.getFunctionType({f64Ty, qubitTy, qubitTy}, {qubitTy, qubitTy}); - OpBuilder::InsertionGuard guard(builder); - builder.clearInsertionPoint(); - auto funcTheta = func::FuncOp::create( - builder, loc, "allows_op_runtime_two_qubit_rotations", funcTyTheta); - auto* entryTheta = funcTheta.addEntryBlock(); - builder.setInsertionPointToStart(entryTheta); - Value runtimeTheta = entryTheta->getArgument(0); - Value runtimeQ0 = entryTheta->getArgument(1); - Value runtimeQ1 = entryTheta->getArgument(2); - auto runtimeRxx = - RXXOp::create(builder, loc, runtimeQ0, runtimeQ1, runtimeTheta); - EXPECT_TRUE(rxxSpec->allowsOp(runtimeRxx.getOperation())); - EXPECT_FALSE(spec->allowsOp(runtimeRxx.getOperation())); - auto runtimeRyy = RYYOp::create(builder, loc, runtimeRxx.getOutputQubit(0), - runtimeRxx.getOutputQubit(1), runtimeTheta); - EXPECT_TRUE(ryySpec->allowsOp(runtimeRyy.getOperation())); - auto runtimeRzx = RZXOp::create(builder, loc, runtimeRyy.getOutputQubit(0), - runtimeRyy.getOutputQubit(1), runtimeTheta); - EXPECT_TRUE(rzxSpec->allowsOp(runtimeRzx.getOperation())); - auto runtimeRzz = RZZOp::create(builder, loc, runtimeRzx.getOutputQubit(0), - runtimeRzx.getOutputQubit(1), runtimeTheta); - EXPECT_TRUE(rzzSpec->allowsOp(runtimeRzz.getOperation())); - func::ReturnOp::create( - builder, loc, - ValueRange{runtimeRzz.getOutputQubit(0), runtimeRzz.getOutputQubit(1)}); - auto module = ModuleOp::create(loc); - module.getBody()->push_back(funcTheta); - EXPECT_TRUE(succeeded(verify(module))); -} diff --git a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp index ded44fc35d..fa7d68f75d 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp @@ -607,7 +607,7 @@ TEST_P(MappingPassTest, MapTopologyOnlyWithEmptyOperationSet) { EXPECT_GT(numSwaps, 0); } -TEST_P(MappingPassTest, PreserveNoncontiguousProviderSiteIds) { +TEST_P(MappingPassTest, PreserveNoncontiguousTargetSiteIds) { std::vector sites; sites.emplace_back(7); sites.emplace_back(19); diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt index e70a874187..5da4e9db7d 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt @@ -6,22 +6,22 @@ # # Licensed under the MIT License -set(target_name mqt-core-mlir-unittest-fuse-two-qubit-unitary-runs) -add_executable(${target_name} test_fuse_two_qubit_unitary_runs.cpp) +set(target_name mqt-core-mlir-unittest-target-synthesis) +add_executable(${target_name} test_target_synthesis.cpp) target_link_libraries( ${target_name} PRIVATE MLIRParser GTest::gtest_main - MLIRQCPrograms + MQTCompilerTarget MLIRQCOProgramBuilder MLIRQCODDFunctionality MLIRQCOUtils - MLIRQCToQCO MLIRQCOTransforms MLIRPass MLIRFuncDialect MLIRArithDialect + MLIRSCFDialect MLIRIR MLIRSupport LLVMSupport) diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp deleted file mode 100644 index 99d10331fe..0000000000 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp +++ /dev/null @@ -1,758 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "dd/DDDefinitions.hpp" -#include "dd/Package.hpp" -#include "mlir/Conversion/QCToQCO/QCToQCO.h" -#include "mlir/Dialect/QCO/IR/QCODialect.h" -#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" -#include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/NativeGateset.h" -#include "mlir/Dialect/QCO/Transforms/Passes.h" -#include "mlir/Dialect/QCO/Utils/DDFunctionality.h" -#include "mlir/Dialect/QCO/Utils/Matrix.h" -#include "qc_programs.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace mlir; -using namespace mlir::qco; - -using ProgramFn = Value (*)(mlir::qc::QCProgramBuilder&); - -// --- Native-gateset membership check ------------------------------------- // - -/// Returns true when every single- or two-qubit operation in @p moduleOp is -/// native to the gateset parsed from @p nativeGates. Operations nested inside a -/// controlled shell are validated through the shell itself, and gates acting on -/// more than two qubits are out of scope for this pass and thus ignored. -static bool allOpsNative(OwningOpRef& moduleOp, - StringRef nativeGates) { - const auto spec = decomposition::NativeGateset::parse(nativeGates); - if (!spec) { - return false; - } - bool ok = true; - std::ignore = moduleOp->walk([&](UnitaryOpInterface op) { - Operation* raw = op.getOperation(); - if (isa_and_present(raw->getParentOp()) || op.getNumQubits() > 2) { - return WalkResult::advance(); - } - if (!spec->allowsOp(raw)) { - ok = false; - return WalkResult::interrupt(); - } - return WalkResult::advance(); - }); - return ok; -} - -static void expectNormalizedPhaseScopes(Operation* root) { - for (auto& region : root->getRegions()) { - for (auto& block : region) { - const auto phases = llvm::to_vector(block.getOps()); - ASSERT_LE(phases.size(), 1); - if (!phases.empty()) { - EXPECT_EQ(phases.front()->getNextNode(), block.getTerminator()); - } - for (auto& nested : block) { - expectNormalizedPhaseScopes(&nested); - } - } - } -} - -// --- DD-based equivalence ------------------------------------------------ // - -[[nodiscard]] static func::FuncOp mainFunc(ModuleOp module) { - return *module.getBody()->getOps().begin(); -} - -[[nodiscard]] static size_t countStaticQubits(func::FuncOp func) { - size_t numQubits = 0; - for (StaticOp staticOp : func.getOps()) { - numQubits = - std::max(numQubits, static_cast(staticOp.getIndex()) + 1); - } - return numQubits; -} - -[[nodiscard]] static DynamicMatrix matrixFromDD(const dd::CMat& matrix) { - const auto dim = static_cast(matrix.size()); - DynamicMatrix out(dim); - for (int64_t row = 0; row < dim; ++row) { - for (int64_t col = 0; col < dim; ++col) { - out(row, col) = - matrix[static_cast(row)][static_cast(col)]; - } - } - return out; -} - -// --- Expressive circuits -------------------------------------------------- // -// -// A handful of circuits, which are crossed with the gateset table below. - -/// A bare SWAP (three-entangler class), the canonical two-qubit decomposition. -static Value swapTwoQ(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.staticQubit(0); - const auto q1 = b.staticQubit(1); - b.swap(q0, q1); - return b.intConstant(0); -} - -/// Rich single-qubit variety on both wires, followed by a two-qubit entangler. -static Value broadOneQThenCz(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.staticQubit(0); - const auto q1 = b.staticQubit(1); - b.x(q0); - b.y(q1); - b.h(q0); - b.sx(q1); - b.rx(0.13, q0); - b.ry(-0.47, q1); - b.rz(0.29, q0); - b.cz(q0, q1); - return b.intConstant(0); -} - -/// Long single-qubit run on one wire, then an entangler (Euler-run fusion). -static Value hstycxTwoQ(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.staticQubit(0); - const auto q1 = b.staticQubit(1); - b.h(q0); - b.s(q0); - b.t(q0); - b.y(q0); - b.cx(q0, q1); - return b.intConstant(0); -} - -/// Zero-angle rotations that must canonicalize away before the entangler. -static Value zeroAngleThenCz(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.staticQubit(0); - const auto q1 = b.staticQubit(1); - b.rx(0.0, q0); - b.ry(0.0, q1); - b.rz(0.0, q0); - b.p(0.0, q1); - b.cz(q0, q1); - return b.intConstant(0); -} - -/// Single-qubit gates surrounding an entangler on both sides. -static Value hCxSq1(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.staticQubit(0); - const auto q1 = b.staticQubit(1); - b.h(q0); - b.cx(q0, q1); - b.s(q1); - return b.intConstant(0); -} - -/// Three-qubit program with chained entanglers on overlapping pairs. -static Value threeQGhz(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.staticQubit(0); - const auto q1 = b.staticQubit(1); - const auto q2 = b.staticQubit(2); - b.h(q0); - b.cx(q0, q1); - b.cx(q1, q2); - return b.intConstant(0); -} - -/// Single-qubit gates wrapped in an inverse modifier (no entangler). -static Value inverseTwoX(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.staticQubit(0); - b.inv(q0, [&](Value qubit) { - b.x(qubit); - b.x(qubit); - }); - return b.intConstant(0); -} - -/// A controlled two-gate body that must be synthesized as a two-qubit unitary. -static Value controlledXH(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.staticQubit(0); - const auto q1 = b.staticQubit(1); - b.ctrl(q0, q1, [&](Value target) { - b.x(target); - b.h(target); - }); - return b.intConstant(0); -} - -// --- Fusion-window circuits ---------------------------------------------- // -// -// These probe window geometry (where fusion starts/stops), so they run on a -// single fixed gateset rather than the full table. - -static Value fusionCxCx(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.staticQubit(0); - const auto q1 = b.staticQubit(1); - b.cx(q0, q1); - b.cx(q0, q1); - return b.intConstant(0); -} - -static Value fusionHCxInterleavedTCx(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.staticQubit(0); - const auto q1 = b.staticQubit(1); - b.h(q0); - b.cx(q0, q1); - b.t(q1); - b.s(q0); - b.cx(q0, q1); - return b.intConstant(0); -} - -static Value fusionThreeLineCx(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.staticQubit(0); - const auto q1 = b.staticQubit(1); - const auto q2 = b.staticQubit(2); - b.cx(q0, q1); - b.cx(q1, q2); - b.cx(q0, q1); - return b.intConstant(0); -} - -static Value fusionCxRSharedOtherPair(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.staticQubit(0); - const auto q1 = b.staticQubit(1); - const auto q2 = b.staticQubit(2); - b.cx(q0, q1); - b.rz(0.17, q1); - b.cx(q1, q2); - return b.intConstant(0); -} - -static Value fusionCxBarrierCx(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.staticQubit(0); - const auto q1 = b.staticQubit(1); - b.cx(q0, q1); - b.barrier({q0, q1}); - b.cx(q0, q1); - return b.intConstant(0); -} - -/// A single-wire barrier between two entanglers: a non-walkable single-qubit -/// shell terminates the run scan on wire A (and breaks the run-start chain of -/// the second entangler), so neither entangler fuses. -static Value fusionCxSingleWireBarrierCx(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.staticQubit(0); - const auto q1 = b.staticQubit(1); - b.cx(q0, q1); - b.barrier({q0}); - b.cx(q0, q1); - return b.intConstant(0); -} - -/// An entangler followed by a three-qubit gate sharing both run wires: the run -/// scan must stop at the wider gate (it is neither a single- nor a two-qubit -/// run member) and leave it untouched, since gates on more than two qubits are -/// out of scope for this pass rather than a failure. -static Value fusionCxThenMultiControlledX(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.staticQubit(0); - const auto q1 = b.staticQubit(1); - const auto q2 = b.staticQubit(2); - b.cx(q0, q1); - b.mcx({q0, q1}, q2); - return b.intConstant(0); -} - -static Value fusionSwapCxPattern(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.staticQubit(0); - const auto q1 = b.staticQubit(1); - b.cx(q0, q1); - b.cx(q1, q0); - b.cx(q0, q1); - return b.intConstant(0); -} - -static Value fusionOffMenuGateInWindow(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.staticQubit(0); - const auto q1 = b.staticQubit(1); - b.cx(q0, q1); - b.h(q0); - b.cx(q0, q1); - return b.intConstant(0); -} - -static Value fusionDualWireOneQBetweenCx(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.staticQubit(0); - const auto q1 = b.staticQubit(1); - b.cx(q0, q1); - b.rz(0.11, q0); - b.ry(0.22, q1); - b.cx(q0, q1); - return b.intConstant(0); -} - -static Value determinismSwap(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.swap(q0, q1); - b.dealloc(q0); - b.dealloc(q1); - return b.intConstant(0); -} - -namespace { - -/// A named circuit builder, used as a test parameter. -struct NamedProgram { - const char* name; - ProgramFn program; -}; - -/// Native gatesets spanning every supported single-qubit basis and all -/// entangler families (RXX/RYY/RZX/RZZ/iSWAP/CZ/CX/ECR, plus multi-entangler -/// menus). Because the pass re-synthesizes each two-qubit window into the -/// target basis, every circuit is valid input for every gateset. -constexpr std::array GATESETS = { - "u,rxx", - "u,ryy", - "u,rzx", - "u,rzz", - "x,sx,rz,rzz", - // iSWAP - "u,iswap", - "x,sx,rz,iswap", - // CZ - "r,cz", // R - "ry,rz,cz", // ZYZ - "x,sx,rz,cz", // ZSXX - "u,cz", // U - // CX - "x,sx,rz,cx", // ZSXX - "u,cx", // U - "rx,rz,cx", // XZX - "rx,ry,cx", // XYX - // ECR - "u,ecr", - "x,sx,rz,ecr", - // Multiple entanglers (listed in preference order; rxx / iswap / cz win) - "u,rxx,ryy,rzx,rzz,iswap,cz,cx,ecr", - "u,iswap,cz,cx,ecr", - "u,cz,cx", -}; - -/// Gateset used for the fusion-window suite, which asserts on structure rather -/// than on native-basis coverage. -constexpr StringRef FUSION_GATESET = "u,cx"; - -/// Structural expectations for a fusion-window circuit under @ref -/// FUSION_GATESET. -struct FusionCase { - const char* name; - ProgramFn program; - std::optional exactCtrlCount; - std::optional minCtrlCount; - bool checkTwoQUnitary; -}; - -class FuseTwoQubitUnitaryRunsPassTest : public testing::Test { -protected: - void SetUp() override { - DialectRegistry registry; - registry.insert(); - context = std::make_unique(); - context->appendDialectRegistry(registry); - context->loadAllAvailableDialects(); - } - - static void runFusePipeline(OwningOpRef& moduleOp, - StringRef nativeGates) { - PassManager pm(moduleOp->getContext()); - pm.addPass(createQCToQCO()); - pm.addPass(createFuseTwoQubitUnitaryRuns(FuseTwoQubitUnitaryRunsOptions{ - .nativeGates = nativeGates.str(), - })); - ASSERT_TRUE(succeeded(pm.run(*moduleOp))); - } - - static void runQcToQco(OwningOpRef& moduleOp) { - PassManager pm(moduleOp->getContext()); - pm.addPass(createQCToQCO()); - ASSERT_TRUE(succeeded(pm.run(*moduleOp))); - } - - static void runTwoQFuse(OwningOpRef& moduleOp, - StringRef nativeGates) { - PassManager pm(moduleOp->getContext()); - pm.addPass(createFuseTwoQubitUnitaryRuns(FuseTwoQubitUnitaryRunsOptions{ - .nativeGates = nativeGates.str(), - })); - ASSERT_TRUE(succeeded(pm.run(*moduleOp))); - } - - static void expectQcoModulesEquivalent(const OwningOpRef& lhs, - const OwningOpRef& rhs) { - const auto lhsFunc = mainFunc(*lhs); - const auto rhsFunc = mainFunc(*rhs); - const auto numQubits = countStaticQubits(lhsFunc); - ASSERT_EQ(numQubits, countStaticQubits(rhsFunc)); - ASSERT_GT(numQubits, 0U); - - auto dd = std::make_unique(numQubits); - const auto lhsUnitary = buildFunctionality(lhsFunc, *dd); - ASSERT_TRUE(succeeded(lhsUnitary)); - const auto rhsUnitary = buildFunctionality(rhsFunc, *dd); - ASSERT_TRUE(succeeded(rhsUnitary)); - - const auto lhsMatrix = matrixFromDD(lhsUnitary->getMatrix(numQubits)); - const auto rhsMatrix = matrixFromDD(rhsUnitary->getMatrix(numQubits)); - dd->decRef(*lhsUnitary); - dd->decRef(*rhsUnitary); - EXPECT_TRUE(lhsMatrix.isApprox(rhsMatrix)); - } - - void expectEquivalentAndNativeAfterSynthesis(ProgramFn program, - StringRef nativeGates) { - auto expected = mlir::qc::QCProgramBuilder::build(context.get(), program); - runQcToQco(expected); - auto synthesized = - mlir::qc::QCProgramBuilder::build(context.get(), program); - runFusePipeline(synthesized, nativeGates); - EXPECT_TRUE(allOpsNative(synthesized, nativeGates)); - expectNormalizedPhaseScopes(synthesized->getOperation()); - expectQcoModulesEquivalent(expected, synthesized); - } - - void expectSynthesisFailure(ProgramFn program, StringRef nativeGates) { - auto moduleOp = mlir::qc::QCProgramBuilder::build(context.get(), program); - PassManager pm(moduleOp->getContext()); - pm.addPass(createQCToQCO()); - pm.addPass(createFuseTwoQubitUnitaryRuns(FuseTwoQubitUnitaryRunsOptions{ - .nativeGates = nativeGates.str(), - })); - EXPECT_TRUE(failed(pm.run(*moduleOp))); - } - - void expectSynthesisFailure( - SmallVector (*program)(mlir::qc::QCProgramBuilder&), - StringRef nativeGates) { - auto moduleOp = mlir::qc::QCProgramBuilder::build(context.get(), program); - PassManager pm(moduleOp->getContext()); - pm.addPass(createQCToQCO()); - pm.addPass(createFuseTwoQubitUnitaryRuns(FuseTwoQubitUnitaryRunsOptions{ - .nativeGates = nativeGates.str(), - })); - EXPECT_TRUE(failed(pm.run(*moduleOp))); - } - - void expectTwoQFusePreservesUnitary(ProgramFn program, - StringRef nativeGates) { - auto expected = mlir::qc::QCProgramBuilder::build(context.get(), program); - ASSERT_TRUE(expected); - runQcToQco(expected); - auto fused = mlir::qc::QCProgramBuilder::build(context.get(), program); - ASSERT_TRUE(fused); - runQcToQco(fused); - runTwoQFuse(fused, nativeGates); - ASSERT_TRUE(succeeded(verify(*fused))); - expectQcoModulesEquivalent(expected, fused); - } - - static size_t countCtrlOps(const OwningOpRef& moduleOp) { - size_t count = 0; - moduleOp.get()->walk([&](CtrlOp) { ++count; }); - return count; - } - - /// Counts unitaries acting on more than two qubits, i.e. gates left untouched - /// for the dedicated multi-controlled synthesis pass. - static size_t countWideGates(const OwningOpRef& moduleOp) { - size_t count = 0; - moduleOp.get()->walk([&](UnitaryOpInterface op) { - if (op.getNumQubits() > 2) { - ++count; - } - }); - return count; - } - - std::unique_ptr context; -}; - -TEST(FuseTwoQubitUnitaryRunsPassContract, RegistersArithDialectDependency) { - DialectRegistry registry; - const auto pass = createFuseTwoQubitUnitaryRuns(); - pass->getDependentDialects(registry); - - EXPECT_TRUE( - registry.getDialectAllocator(arith::ArithDialect::getDialectNamespace())); -} - -using SynthesisParam = std::tuple; - -class FuseTwoQubitSynthesisTest - : public FuseTwoQubitUnitaryRunsPassTest, - public testing::WithParamInterface {}; - -class FuseTwoQubitFusionTest : public FuseTwoQubitUnitaryRunsPassTest, - public testing::WithParamInterface { -}; - -} // namespace - -// --- Synthesis: every expressive circuit against every gateset ----------- // - -TEST_P(FuseTwoQubitSynthesisTest, IsNativeAndEquivalent) { - const auto& [circuit, gateset] = GetParam(); - expectEquivalentAndNativeAfterSynthesis(circuit.program, gateset); -} - -INSTANTIATE_TEST_SUITE_P( - Circuits, FuseTwoQubitSynthesisTest, - testing::Combine( - testing::Values(NamedProgram{"Swap", swapTwoQ}, - NamedProgram{"BroadOneQThenCz", broadOneQThenCz}, - NamedProgram{"HstyThenCx", hstycxTwoQ}, - NamedProgram{"ZeroAngleThenCz", zeroAngleThenCz}, - NamedProgram{"SurroundedCx", hCxSq1}, - NamedProgram{"ThreeQubitGhz", threeQGhz}, - NamedProgram{"InverseBody", inverseTwoX}, - NamedProgram{"ControlledBody", controlledXH}, - NamedProgram{"SingleWireBarrier", - fusionCxSingleWireBarrierCx}), - testing::ValuesIn(GATESETS)), - [](const testing::TestParamInfo& info) { - std::string gateset = std::get<1>(info.param); - std::ranges::replace(gateset, ',', '_'); - return std::string(std::get<0>(info.param).name) + "__" + gateset; - }); - -// --- Fusion windows: structural behavior on a fixed gateset -------------- // - -TEST_P(FuseTwoQubitFusionTest, WindowFusionBehavior) { - const FusionCase& c = GetParam(); - if (c.checkTwoQUnitary) { - expectTwoQFusePreservesUnitary(c.program, FUSION_GATESET); - } - auto module = mlir::qc::QCProgramBuilder::build(context.get(), c.program); - ASSERT_TRUE(module); - runQcToQco(module); - runTwoQFuse(module, FUSION_GATESET); - if (c.exactCtrlCount) { - EXPECT_EQ(countCtrlOps(module), *c.exactCtrlCount); - } - if (c.minCtrlCount) { - EXPECT_GE(countCtrlOps(module), *c.minCtrlCount); - } -} - -INSTANTIATE_TEST_SUITE_P( - Windows, FuseTwoQubitFusionTest, - testing::Values( - FusionCase{"AdjacentCxCancel", fusionCxCx, 0, std::nullopt, true}, - FusionCase{"InterleavedOneQ", fusionHCxInterleavedTCx, std::nullopt, - std::nullopt, true}, - FusionCase{"DifferentPairBoundary", fusionThreeLineCx, std::nullopt, 1, - false}, - FusionCase{"SharedWireOneQ", fusionCxRSharedOtherPair, std::nullopt, 2, - false}, - FusionCase{"BarrierBoundary", fusionCxBarrierCx, 2, std::nullopt, - false}, - FusionCase{"SingleWireBarrierBoundary", fusionCxSingleWireBarrierCx, 2, - std::nullopt, true}, - FusionCase{"SwappedWireOrder", fusionSwapCxPattern, std::nullopt, - std::nullopt, true}, - FusionCase{"OffMenuGateInWindow", fusionOffMenuGateInWindow, - std::nullopt, std::nullopt, true}, - FusionCase{"DualWireOneQBetweenCx", fusionDualWireOneQBetweenCx, - std::nullopt, std::nullopt, true}), - [](const testing::TestParamInfo& info) { - return info.param.name; - }); - -// --- Pass edge cases ----------------------------------------------------- // - -TEST_F(FuseTwoQubitUnitaryRunsPassTest, EmptyNativeGatesSkipsPass) { - auto module = mlir::qc::QCProgramBuilder::build(context.get(), fusionCxCx); - ASSERT_TRUE(module); - runQcToQco(module); - std::string before; - llvm::raw_string_ostream osBefore(before); - module->print(osBefore); - - PassManager pm(module->getContext()); - pm.addPass(createFuseTwoQubitUnitaryRuns(FuseTwoQubitUnitaryRunsOptions{ - .nativeGates = "", - })); - ASSERT_TRUE(succeeded(pm.run(*module))); - - std::string after; - llvm::raw_string_ostream osAfter(after); - module->print(osAfter); - EXPECT_EQ(before, after); -} - -TEST_F(FuseTwoQubitUnitaryRunsPassTest, FailsForInvalidNativeGateMenu) { - expectSynthesisFailure(mlir::qc::h, "not-a-gate"); -} - -TEST_F(FuseTwoQubitUnitaryRunsPassTest, - FailsForNativeGateMenuWithoutSingleQEmitter) { - expectSynthesisFailure(mlir::qc::singleControlledX, "cx,cz"); -} - -TEST_F(FuseTwoQubitUnitaryRunsPassTest, - PreservesRuntimeParameterizedNativeTwoQubitRotations) { - auto module = parseSourceString(R"mlir( - module { - func.func @main(%theta: f64) -> (!qco.qubit, !qco.qubit) { - %q0 = qco.static 0 : !qco.qubit - %q1 = qco.static 1 : !qco.qubit - %q2, %q3 = qco.rxx(%theta) %q0, %q1 : !qco.qubit, !qco.qubit -> !qco.qubit, !qco.qubit - %q4, %q5 = qco.ryy(%theta) %q2, %q3 : !qco.qubit, !qco.qubit -> !qco.qubit, !qco.qubit - %q6, %q7 = qco.rzx(%theta) %q4, %q5 : !qco.qubit, !qco.qubit -> !qco.qubit, !qco.qubit - %q8, %q9 = qco.rzz(%theta) %q6, %q7 : !qco.qubit, !qco.qubit -> !qco.qubit, !qco.qubit - return %q8, %q9 : !qco.qubit, !qco.qubit - } - } - )mlir", - context.get()); - ASSERT_TRUE(module); - - std::string before; - llvm::raw_string_ostream osBefore(before); - module->print(osBefore); - - PassManager pm(module->getContext()); - pm.addPass(createFuseTwoQubitUnitaryRuns(FuseTwoQubitUnitaryRunsOptions{ - .nativeGates = "u,rxx,ryy,rzx,rzz", - })); - ASSERT_TRUE(succeeded(pm.run(*module))); - - std::string after; - llvm::raw_string_ostream osAfter(after); - module->print(osAfter); - EXPECT_EQ(before, after); -} - -TEST_F(FuseTwoQubitUnitaryRunsPassTest, - FailsLocallyForRuntimeParameterizedResidual) { - // A runtime-angle `qco.rz` cannot be Euler-fused under `u,cx` and is not on - // the menu, so the convergence check must fail on that op (not the module). - auto module = parseSourceString(R"mlir( - module { - func.func @main(%theta: f64) -> !qco.qubit { - %q = qco.static 0 : !qco.qubit - %q1 = qco.rz(%theta) %q : !qco.qubit -> !qco.qubit - return %q1 : !qco.qubit - } - } - )mlir", - context.get()); - ASSERT_TRUE(module); - - Location rzLoc = UnknownLoc::get(context.get()); - module->walk([&](RZOp op) { - rzLoc = op.getLoc(); - return WalkResult::interrupt(); - }); - ASSERT_FALSE(isa(rzLoc)); - - bool sawLocalizedDiagnostic = false; - std::string diagnostics; - ScopedDiagnosticHandler handler(context.get(), [&](Diagnostic& diagnostic) { - diagnostics += diagnostic.str(); - if (StringRef(diagnostic.str()) - .contains("native gate synthesis: operation remains outside the " - "native gateset") && - diagnostic.getLocation() == rzLoc) { - sawLocalizedDiagnostic = true; - } - return success(); - }); - - PassManager pm(module->getContext()); - pm.addPass(createFuseTwoQubitUnitaryRuns(FuseTwoQubitUnitaryRunsOptions{ - .nativeGates = "u,cx", - })); - EXPECT_TRUE(failed(pm.run(*module))); - EXPECT_TRUE(sawLocalizedDiagnostic) << diagnostics; -} - -TEST_F(FuseTwoQubitUnitaryRunsPassTest, LeavesMultiControlledGateUntouched) { - // A multi-controlled gate is out of scope for this pass; it is left untouched - // (for a dedicated multi-controlled synthesis pass) and does not fail the - // run. - auto module = mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::multipleControlledX); - ASSERT_TRUE(module); - runFusePipeline(module, "x,sx,rz,cx"); - EXPECT_TRUE(allOpsNative(module, "x,sx,rz,cx")); - EXPECT_EQ(countWideGates(module), 1U); -} - -TEST_F(FuseTwoQubitUnitaryRunsPassTest, - LowersTwoQubitRunButLeavesWiderGateBoundary) { - // The `cx` is off-menu for this cz-family gateset, so it is Weyl-synthesized - // even though the run scan stops at the three-qubit boundary; the wider gate - // is left untouched, so the pass succeeds with the two-qubit run lowered. - auto module = mlir::qc::QCProgramBuilder::build(context.get(), - fusionCxThenMultiControlledX); - ASSERT_TRUE(module); - runFusePipeline(module, "u,cz"); - EXPECT_TRUE(allOpsNative(module, "u,cz")); - EXPECT_EQ(countWideGates(module), 1U); -} - -TEST_F(FuseTwoQubitUnitaryRunsPassTest, - CandidateSelectionIsDeterministicAcrossRuns) { - auto buildFn = [&] { - return mlir::qc::QCProgramBuilder::build(context.get(), determinismSwap); - }; - auto firstModule = buildFn(); - runFusePipeline(firstModule, "u,cx"); - auto secondModule = buildFn(); - runFusePipeline(secondModule, "u,cx"); - - std::string first; - std::string second; - llvm::raw_string_ostream osFirst(first); - llvm::raw_string_ostream osSecond(second); - firstModule->print(osFirst); - secondModule->print(osSecond); - EXPECT_EQ(first, second); -} diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp new file mode 100644 index 0000000000..bfa2c07d65 --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_target_synthesis.cpp @@ -0,0 +1,653 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "dd/DDDefinitions.hpp" +#include "dd/Package.hpp" +#include "mlir/Compiler/Target.h" +#include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h" +#include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Passes.h" +#include "mlir/Dialect/QCO/Utils/DDFunctionality.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" +#include "mlir/Dialect/QTensor/IR/QTensorDialect.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mqt::test::qco { + +using Target = mlir::CompilerTarget; +using Operation = Target::Operation; +using Site = Target::Site; +using mlir::ModuleOp; +using mlir::OwningOpRef; +using mlir::Value; +using mlir::qco::CtrlOp; +using mlir::qco::HOp; +using mlir::qco::QCOProgramBuilder; +using mlir::qco::RXXOp; +using mlir::qco::SWAPOp; +using mlir::qco::XOp; +using mlir::qco::ZOp; + +[[nodiscard]] static mlir::func::FuncOp mainFunction(ModuleOp module) { + return *module.getBody()->getOps().begin(); +} + +[[nodiscard]] static size_t countStaticQubits(mlir::func::FuncOp function) { + size_t numQubits = 0; + for (auto staticOp : function.getOps()) { + numQubits = + std::max(numQubits, static_cast(staticOp.getIndex()) + 1); + } + return numQubits; +} + +[[nodiscard]] static mlir::qco::DynamicMatrix +matrixFromDD(const dd::CMat& matrix) { + const auto dimension = static_cast(matrix.size()); + mlir::qco::DynamicMatrix result(dimension); + for (int64_t row = 0; row < dimension; ++row) { + for (int64_t column = 0; column < dimension; ++column) { + result(row, column) = + matrix[static_cast(row)][static_cast(column)]; + } + } + return result; +} + +static void expectEquivalent(const OwningOpRef& expected, + const OwningOpRef& actual) { + const auto expectedFunction = mainFunction(*expected); + const auto actualFunction = mainFunction(*actual); + const auto numQubits = countStaticQubits(expectedFunction); + ASSERT_EQ(numQubits, countStaticQubits(actualFunction)); + ASSERT_GT(numQubits, 0U); + + auto package = std::make_unique(numQubits); + const auto expectedUnitary = + mlir::qco::buildFunctionality(expectedFunction, *package); + ASSERT_TRUE(mlir::succeeded(expectedUnitary)); + const auto actualUnitary = + mlir::qco::buildFunctionality(actualFunction, *package); + ASSERT_TRUE(mlir::succeeded(actualUnitary)); + + const auto expectedMatrix = + matrixFromDD(expectedUnitary->getMatrix(numQubits)); + const auto actualMatrix = matrixFromDD(actualUnitary->getMatrix(numQubits)); + package->decRef(*expectedUnitary); + package->decRef(*actualUnitary); + EXPECT_TRUE(expectedMatrix.isApprox(actualMatrix)); +} + +template [[nodiscard]] static size_t countOps(ModuleOp module) { + size_t count = 0; + module.walk([&](Op) { ++count; }); + return count; +} + +[[nodiscard]] static std::string printModule(ModuleOp module) { + std::string result; + llvm::raw_string_ostream stream(result); + module.print(stream); + return result; +} + +[[nodiscard]] static mlir::LogicalResult +runPass(ModuleOp module, std::unique_ptr pass) { + mlir::PassManager manager(module.getContext()); + manager.addPass(std::move(pass)); + return manager.run(module); +} + +[[nodiscard]] static Target makeUCxTarget(std::vector sites = std::vector{ + Site{0}, Site{1}}) { + return Target{std::move(sites), std::nullopt, + std::vector{Operation{"u", 1, 3}, Operation{"cx", 2, 0}}}; +} + +namespace { + +class TargetSynthesisTest : public testing::Test { +protected: + void SetUp() override { + mlir::DialectRegistry registry; + registry.insert(); + context = std::make_unique(); + context->appendDialectRegistry(registry); + context->loadAllAvailableDialects(); + } + + [[nodiscard]] OwningOpRef + build(const mlir::function_ref& builder) const { + return QCOProgramBuilder::build(context.get(), builder); + } + + [[nodiscard]] std::string + expectFailure(ModuleOp module, std::unique_ptr pass) const { + std::string diagnostics; + mlir::ScopedDiagnosticHandler handler(context.get(), + [&](mlir::Diagnostic& diagnostic) { + diagnostics += diagnostic.str(); + diagnostics += '\n'; + return mlir::success(); + }); + EXPECT_TRUE(mlir::failed(runPass(module, std::move(pass)))); + return diagnostics; + } + + std::unique_ptr context; +}; + +} // namespace + +TEST(TargetSynthesisPassContract, FactoriesAreIndependentlyConstructible) { + const Target target{2}; + auto fusion = mlir::qco::createFuseTwoQubitGates(); + auto synthesis = mlir::qco::createTargetNativeSynthesis(target); + auto conformance = mlir::qco::createVerifyTargetConformance(target); + + ASSERT_NE(fusion, nullptr); + ASSERT_NE(synthesis, nullptr); + ASSERT_NE(conformance, nullptr); + + mlir::DialectRegistry fusionDialects; + fusion->getDependentDialects(fusionDialects); + EXPECT_TRUE(fusionDialects.getDialectAllocator( + mlir::arith::ArithDialect::getDialectNamespace())); + + mlir::DialectRegistry synthesisDialects; + synthesis->getDependentDialects(synthesisDialects); + EXPECT_TRUE(synthesisDialects.getDialectAllocator( + mlir::arith::ArithDialect::getDialectNamespace())); +} + +TEST_F(TargetSynthesisTest, TwoQubitGateFusionRequiresStrictImprovement) { + const auto adjacentCx = [](QCOProgramBuilder& builder) { + auto q0 = builder.staticQubit(0); + auto q1 = builder.staticQubit(1); + std::tie(q0, q1) = builder.cx(q0, q1); + std::tie(q0, q1) = builder.cx(q0, q1); + return builder.intConstant(0); + }; + auto expected = build(adjacentCx); + auto optimized = build(adjacentCx); + ASSERT_TRUE(mlir::succeeded( + runPass(*optimized, mlir::qco::createFuseTwoQubitGates()))); + EXPECT_EQ(countOps(*optimized), 0U); + expectEquivalent(expected, optimized); + + auto nonImproving = build([](QCOProgramBuilder& builder) { + auto q0 = builder.staticQubit(0); + auto q1 = builder.staticQubit(1); + std::tie(q0, q1) = builder.cx(q0, q1); + std::tie(q1, q0) = builder.cx(q1, q0); + std::tie(q0, q1) = builder.cx(q0, q1); + return builder.intConstant(0); + }); + ASSERT_TRUE(mlir::succeeded( + runPass(*nonImproving, mlir::qco::createFuseTwoQubitGates()))); + EXPECT_EQ(countOps(*nonImproving), 3U); +} + +TEST_F(TargetSynthesisTest, + TwoQubitGateFusionFusesInterleavedSingleQubitGates) { + const auto interleaved = [](QCOProgramBuilder& builder) { + auto q0 = builder.staticQubit(0); + auto q1 = builder.staticQubit(1); + std::tie(q0, q1) = builder.cx(q0, q1); + q1 = builder.x(q1); + q0 = builder.z(q0); + std::tie(q0, q1) = builder.cx(q0, q1); + return builder.intConstant(0); + }; + auto expected = build(interleaved); + auto optimized = build(interleaved); + + ASSERT_TRUE(mlir::succeeded( + runPass(*optimized, mlir::qco::createFuseTwoQubitGates()))); + EXPECT_EQ(countOps(*optimized), 0U); + expectEquivalent(expected, optimized); +} + +TEST_F(TargetSynthesisTest, TwoQubitGateFusionEmitsSymmetricEntangler) { + const auto reducible = [](QCOProgramBuilder& builder) { + auto q0 = builder.staticQubit(0); + auto q1 = builder.staticQubit(1); + std::tie(q0, q1) = builder.cx(q0, q1); + std::tie(q1, q0) = builder.cx(q1, q0); + std::tie(q1, q0) = builder.cx(q1, q0); + return builder.intConstant(0); + }; + auto expected = build(reducible); + auto optimized = build(reducible); + + ASSERT_TRUE(mlir::succeeded( + runPass(*optimized, mlir::qco::createFuseTwoQubitGates()))); + EXPECT_EQ(countOps(*optimized), 1U); + EXPECT_EQ(countOps(*optimized), 1U); + EXPECT_EQ(countOps(*optimized), 0U); + expectEquivalent(expected, optimized); +} + +TEST_F(TargetSynthesisTest, TwoQubitGateFusionLeavesIndividualOpsAlone) { + auto module = build([](QCOProgramBuilder& builder) { + auto q0 = builder.staticQubit(0); + auto q1 = builder.staticQubit(1); + std::tie(q0, q1) = builder.swap(q0, q1); + return builder.intConstant(0); + }); + const auto before = printModule(*module); + ASSERT_TRUE( + mlir::succeeded(runPass(*module, mlir::qco::createFuseTwoQubitGates()))); + EXPECT_EQ(countOps(*module), 1U); + EXPECT_EQ(countOps(*module), 0U); + EXPECT_EQ(printModule(*module), before); +} + +TEST_F(TargetSynthesisTest, + TwoQubitGateFusionLeavesRuntimeParameterizedRunsAlone) { + auto module = mlir::parseSourceString(R"mlir( + module { + func.func @main(%theta: f64) -> (!qco.qubit, !qco.qubit) { + %q0 = qco.static 0 : !qco.qubit + %q1 = qco.static 1 : !qco.qubit + %q2, %q3 = qco.rxx(%theta) %q0, %q1 : !qco.qubit, !qco.qubit -> !qco.qubit, !qco.qubit + %q4, %q5 = qco.rxx(%theta) %q2, %q3 : !qco.qubit, !qco.qubit -> !qco.qubit, !qco.qubit + return %q4, %q5 : !qco.qubit, !qco.qubit + } + } + )mlir", + context.get()); + ASSERT_TRUE(module); + const auto before = printModule(*module); + ASSERT_TRUE( + mlir::succeeded(runPass(*module, mlir::qco::createFuseTwoQubitGates()))); + EXPECT_EQ(countOps(*module), 2U); + EXPECT_EQ(printModule(*module), before); +} + +TEST_F(TargetSynthesisTest, TargetNativeSynthesisRemovesOrdinarySwap) { + const auto swap = [](QCOProgramBuilder& builder) { + auto q0 = builder.staticQubit(0); + auto q1 = builder.staticQubit(1); + std::tie(q0, q1) = builder.swap(q0, q1); + return builder.intConstant(0); + }; + auto expected = build(swap); + auto synthesized = build(swap); + const auto target = makeUCxTarget(); + + ASSERT_TRUE(mlir::succeeded( + runPass(*synthesized, mlir::qco::createTargetNativeSynthesis(target)))); + EXPECT_EQ(countOps(*synthesized), 0U); + EXPECT_GT(countOps(*synthesized), 0U); + ASSERT_TRUE(mlir::succeeded( + runPass(*synthesized, mlir::qco::createVerifyTargetConformance(target)))); + ASSERT_TRUE(mlir::succeeded(mlir::verify(*synthesized))); + expectEquivalent(expected, synthesized); +} + +TEST_F(TargetSynthesisTest, + TargetNativeSynthesisLowersConstantSingleQubitGate) { + const auto hadamard = [](QCOProgramBuilder& builder) { + auto qubit = builder.staticQubit(0); + qubit = builder.h(qubit); + return builder.intConstant(0); + }; + auto expected = build(hadamard); + auto synthesized = build(hadamard); + const auto target = makeUCxTarget(); + + ASSERT_TRUE(mlir::succeeded( + runPass(*synthesized, mlir::qco::createTargetNativeSynthesis(target)))); + EXPECT_EQ(countOps(*synthesized), 0U); + ASSERT_TRUE(mlir::succeeded( + runPass(*synthesized, mlir::qco::createVerifyTargetConformance(target)))); + expectEquivalent(expected, synthesized); +} + +TEST_F(TargetSynthesisTest, TargetNativeSynthesisPreservesNativeSwap) { + auto module = build([](QCOProgramBuilder& builder) { + auto q0 = builder.staticQubit(0); + auto q1 = builder.staticQubit(1); + std::tie(q0, q1) = builder.swap(q0, q1); + return builder.intConstant(0); + }); + const Target swapTarget{2, std::nullopt, + std::vector{Operation{"swap", 2, 0}}}; + ASSERT_FALSE(swapTarget.synthesisBasis()); + const auto before = printModule(*module); + + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createTargetNativeSynthesis(swapTarget)))); + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createVerifyTargetConformance(swapTarget)))); + EXPECT_EQ(countOps(*module), 1U); + EXPECT_EQ(printModule(*module), before); +} + +TEST_F(TargetSynthesisTest, TargetNativeSynthesisUsesHomogeneousCapability) { + const auto swap = [](QCOProgramBuilder& builder) { + auto q0 = builder.staticQubit(0); + auto q1 = builder.staticQubit(1); + std::tie(q0, q1) = builder.swap(q0, q1); + return builder.intConstant(0); + }; + auto expected = build(swap); + auto synthesized = build(swap); + const Target target{2, std::nullopt, + std::vector{Operation{"u", 1, 3}, Operation{"cz", 2, 0}}}; + ASSERT_TRUE(target.synthesisBasis()); + ASSERT_EQ(target.synthesisBasis()->entangler, Target::GateKind::CZ); + + ASSERT_TRUE(mlir::succeeded( + runPass(*synthesized, mlir::qco::createTargetNativeSynthesis(target)))); + EXPECT_EQ(countOps(*synthesized), 0U); + EXPECT_GT(countOps(*synthesized), 0U); + ASSERT_TRUE(mlir::succeeded( + runPass(*synthesized, mlir::qco::createVerifyTargetConformance(target)))); + ASSERT_TRUE(mlir::succeeded(mlir::verify(*synthesized))); + expectEquivalent(expected, synthesized); +} + +TEST_F(TargetSynthesisTest, AbsentOperationSetTreatsEveryOperationAsNative) { + auto module = build([](QCOProgramBuilder& builder) { + auto qubit = builder.staticQubit(0); + qubit = builder.h(qubit); + return builder.intConstant(0); + }); + const Target permissive{1}; + const auto before = printModule(*module); + + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createTargetNativeSynthesis(permissive)))); + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createVerifyTargetConformance(permissive)))); + EXPECT_EQ(printModule(*module), before); +} + +TEST_F(TargetSynthesisTest, NativePowShellHidesItsImplementationBody) { + auto module = build([](QCOProgramBuilder& builder) { + auto qubit = builder.staticQubit(0); + qubit = builder.pow(2.0, qubit, + [&](Value argument) { return builder.h(argument); }); + return builder.intConstant(0); + }); + const Target powOnly{1, std::nullopt, std::vector{Operation{"pow", 1, 1}}}; + ASSERT_FALSE(powOnly.synthesisBasis()); + const auto before = printModule(*module); + + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createTargetNativeSynthesis(powOnly)))); + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createVerifyTargetConformance(powOnly)))); + EXPECT_EQ(printModule(*module), before); +} + +TEST_F(TargetSynthesisTest, MissingBasisIsDiagnosedOnlyWhenLoweringIsNeeded) { + const Target hOnly{1, std::nullopt, std::vector{Operation{"h", 1, 0}}}; + ASSERT_FALSE(hOnly.synthesisBasis()); + + auto supported = build([](QCOProgramBuilder& builder) { + auto qubit = builder.staticQubit(0); + qubit = builder.h(qubit); + return builder.intConstant(0); + }); + const auto before = printModule(*supported); + ASSERT_TRUE(mlir::succeeded( + runPass(*supported, mlir::qco::createTargetNativeSynthesis(hOnly)))); + ASSERT_TRUE(mlir::succeeded( + runPass(*supported, mlir::qco::createVerifyTargetConformance(hOnly)))); + EXPECT_EQ(printModule(*supported), before); + + auto unsupported = build([](QCOProgramBuilder& builder) { + auto qubit = builder.staticQubit(0); + qubit = builder.x(qubit); + return builder.intConstant(0); + }); + const auto diagnostics = expectFailure( + *unsupported, mlir::qco::createTargetNativeSynthesis(hOnly)); + EXPECT_NE(diagnostics.find("target-native synthesis cannot lower operation " + "'qco.x'"), + std::string::npos) + << diagnostics; + EXPECT_NE(diagnostics.find("no usable synthesis basis"), std::string::npos) + << diagnostics; +} + +TEST_F(TargetSynthesisTest, SupportedRuntimeParameterizedGateStaysUntouched) { + auto module = mlir::parseSourceString(R"mlir( + module { + func.func @main(%theta: f64) -> (!qco.qubit, !qco.qubit) { + %q0 = qco.static 0 : !qco.qubit + %q1 = qco.static 1 : !qco.qubit + %q2, %q3 = qco.rxx(%theta) %q0, %q1 : !qco.qubit, !qco.qubit -> !qco.qubit, !qco.qubit + return %q2, %q3 : !qco.qubit, !qco.qubit + } + } + )mlir", + context.get()); + ASSERT_TRUE(module); + const Target target{ + 2, std::nullopt, + std::vector{Operation{"u", 1, 3}, Operation{"rxx", 2, 1}}}; + const auto before = printModule(*module); + + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createTargetNativeSynthesis(target)))); + ASSERT_TRUE(mlir::succeeded( + runPass(*module, mlir::qco::createVerifyTargetConformance(target)))); + EXPECT_EQ(printModule(*module), before); +} + +TEST_F(TargetSynthesisTest, + UnsupportedRuntimeParameterizedGateHasLocalDiagnostic) { + auto module = mlir::parseSourceString(R"mlir( + module { + func.func @main(%theta: f64) -> (!qco.qubit, !qco.qubit) { + %q0 = qco.static 0 : !qco.qubit + %q1 = qco.static 1 : !qco.qubit + %q2, %q3 = qco.rxx(%theta) %q0, %q1 : !qco.qubit, !qco.qubit -> !qco.qubit, !qco.qubit + return %q2, %q3 : !qco.qubit, !qco.qubit + } + } + )mlir", + context.get()); + ASSERT_TRUE(module); + const auto diagnostics = expectFailure( + *module, mlir::qco::createTargetNativeSynthesis(makeUCxTarget())); + EXPECT_NE(diagnostics.find("target-native synthesis cannot lower operation " + "'qco.rxx'"), + std::string::npos) + << diagnostics; + EXPECT_NE(diagnostics.find("unitary matrix is not available at compile time"), + std::string::npos) + << diagnostics; +} + +TEST_F(TargetSynthesisTest, + UnsupportedRuntimeParameterizedGateDoesNotPartiallyRewrite) { + auto module = mlir::parseSourceString(R"mlir( + module { + func.func @main(%theta: f64) -> (!qco.qubit, !qco.qubit) { + %q0 = qco.static 0 : !qco.qubit + %q1 = qco.static 1 : !qco.qubit + %q2 = qco.h %q0 : !qco.qubit -> !qco.qubit + %q3, %q4 = qco.rxx(%theta) %q2, %q1 : !qco.qubit, !qco.qubit -> !qco.qubit, !qco.qubit + return %q3, %q4 : !qco.qubit, !qco.qubit + } + } + )mlir", + context.get()); + ASSERT_TRUE(module); + const auto before = printModule(*module); + + static_cast(expectFailure( + *module, mlir::qco::createTargetNativeSynthesis(makeUCxTarget()))); + EXPECT_EQ(printModule(*module), before); +} + +TEST_F(TargetSynthesisTest, + ConformanceUsesHomogeneousCapabilitiesAndValidatesSites) { + const Target target{std::vector{Site{10}, Site{20}}, std::nullopt, + std::vector{Operation{"cx", 2, 0}}}; + ASSERT_FALSE(target.synthesisBasis()); + + auto reversed = build([](QCOProgramBuilder& builder) { + auto q10 = builder.staticQubit(10); + auto q20 = builder.staticQubit(20); + std::tie(q20, q10) = builder.cx(q20, q10); + return builder.intConstant(0); + }); + ASSERT_TRUE(mlir::succeeded( + runPass(*reversed, mlir::qco::createTargetNativeSynthesis(target)))); + ASSERT_TRUE(mlir::succeeded( + runPass(*reversed, mlir::qco::createVerifyTargetConformance(target)))); + + auto unknownSite = build([](QCOProgramBuilder& builder) { + auto q30 = builder.staticQubit(30); + auto q20 = builder.staticQubit(20); + std::tie(q30, q20) = builder.cx(q30, q20); + return builder.intConstant(0); + }); + const auto diagnostics = expectFailure( + *unknownSite, mlir::qco::createVerifyTargetConformance(target)); + EXPECT_NE(diagnostics.find("target does not contain static site 30"), + std::string::npos) + << diagnostics; +} + +TEST_F(TargetSynthesisTest, ConformanceRejectsDynamicAllocations) { + const Target target{1, std::nullopt, std::vector{Operation{"x", 1, 0}}}; + const auto expectDynamicAllocationFailure = + [&](OwningOpRef module) { + const auto diagnostics = expectFailure( + *module, mlir::qco::createVerifyTargetConformance(target)); + EXPECT_NE( + diagnostics.find("requires qubits to be assigned to qco.static"), + std::string::npos) + << diagnostics; + }; + + expectDynamicAllocationFailure(build([](QCOProgramBuilder& builder) { + auto qubit = builder.allocQubit(); + qubit = builder.x(qubit); + return builder.intConstant(0); + })); + expectDynamicAllocationFailure(build([](QCOProgramBuilder& builder) { + auto qubits = builder.allocQubitRegister(1); + qubits[0] = builder.x(qubits[0]); + return builder.intConstant(0); + })); +} + +TEST_F(TargetSynthesisTest, ConformanceRejectsQuantumFunctionInputs) { + auto module = mlir::parseSourceString(R"mlir( + module { + func.func @main(%qubit: !qco.qubit) -> !qco.qubit { + %result = qco.x %qubit : !qco.qubit -> !qco.qubit + return %result : !qco.qubit + } + } + )mlir", + context.get()); + ASSERT_TRUE(module); + const Target target{1, std::nullopt, std::vector{Operation{"x", 1, 0}}}; + + const auto diagnostics = + expectFailure(*module, mlir::qco::createVerifyTargetConformance(target)); + EXPECT_NE(diagnostics.find("requires quantum function inputs to be assigned " + "to qco.static target sites"), + std::string::npos) + << diagnostics; +} + +TEST_F(TargetSynthesisTest, ConformanceChecksTypeArityAndParameters) { + const auto expectUnsupported = [&](const Target& target, + OwningOpRef module, + const std::string& operation, + const std::string& details) { + const auto diagnostics = expectFailure( + *module, mlir::qco::createVerifyTargetConformance(target)); + EXPECT_NE(diagnostics.find(operation), std::string::npos) << diagnostics; + EXPECT_NE(diagnostics.find(details), std::string::npos) << diagnostics; + }; + + expectUnsupported(Target{std::vector{Site{10}}, std::nullopt, + std::vector{Operation{"x", 1, 0}}}, + build([](QCOProgramBuilder& builder) { + auto qubit = builder.staticQubit(10); + qubit = builder.h(qubit); + return builder.intConstant(0); + }), + "'qco.h'", "arity 1 and 0 parameter(s)"); + + expectUnsupported(Target{std::vector{Site{10}, Site{20}}, std::nullopt, + std::vector{Operation{"x", 2, 0}}}, + build([](QCOProgramBuilder& builder) { + auto qubit = builder.staticQubit(10); + qubit = builder.x(qubit); + return builder.intConstant(0); + }), + "'qco.x'", "arity 1 and 0 parameter(s)"); + + expectUnsupported(Target{std::vector{Site{10}}, std::nullopt, + std::vector{Operation{"rz", 1, 0}}}, + build([](QCOProgramBuilder& builder) { + auto qubit = builder.staticQubit(10); + qubit = builder.rz(0.25, qubit); + return builder.intConstant(0); + }), + "'qco.rz'", "arity 1 and 1 parameter(s)"); +} + +TEST_F(TargetSynthesisTest, ConformanceChecksNonUnitaryCapabilities) { + auto module = build([](QCOProgramBuilder& builder) { + auto qubit = builder.staticQubit(0); + auto [measured, result] = builder.measure(qubit); + static_cast(result); + measured = builder.reset(measured); + return builder.intConstant(0); + }); + const Target xOnly{1, std::nullopt, std::vector{Operation{"x", 1, 0}}}; + const auto diagnostics = + expectFailure(*module, mlir::qco::createVerifyTargetConformance(xOnly)); + EXPECT_NE(diagnostics.find("'qco.measure' with arity 1 and 0 parameter(s)"), + std::string::npos) + << diagnostics; +} + +} // namespace mqt::test::qco diff --git a/mlir/unittests/Dialect/Utils/test_global_phase_normalization.cpp b/mlir/unittests/Dialect/Utils/test_global_phase_normalization.cpp index 6efa75a811..a31a1ea4ef 100644 --- a/mlir/unittests/Dialect/Utils/test_global_phase_normalization.cpp +++ b/mlir/unittests/Dialect/Utils/test_global_phase_normalization.cpp @@ -806,40 +806,6 @@ TEST_F(GlobalPhaseNormalizationTest, VerifiesPracticalConstantAngleRange) { } } -TEST_F(GlobalPhaseNormalizationTest, ScalesLinearlyAcrossLargePhaseScopes) { - constexpr std::array sizes{1'000, 10'000, 100'000}; - std::vector durations; - durations.reserve(sizes.size()); - - for (const auto size : sizes) { - SCOPED_TRACE(size); - OwningOpRef moduleOp = ModuleOp::create(UnknownLoc::get(context.get())); - OpBuilder builder(context.get()); - builder.setInsertionPointToStart(moduleOp->getBody()); - const auto loc = moduleOp->getLoc(); - auto function = func::FuncOp::create(builder, loc, "test", - builder.getFunctionType({}, {})); - auto* entry = function.addEntryBlock(); - builder.setInsertionPointToStart(entry); - const auto angle = utils::constantFromScalar(builder, loc, 0.001); - for (std::size_t i = 0; i < size; ++i) { - qco::GPhaseOp::create(builder, loc, angle); - } - func::ReturnOp::create(builder, loc); - - const auto start = std::chrono::steady_clock::now(); - ASSERT_TRUE(mlir::mqt::normalizeGlobalPhases(*moduleOp).succeeded()); - durations.emplace_back(std::chrono::steady_clock::now() - start); - EXPECT_EQ(llvm::range_size(function.getBody().getOps()), 1); - } - - RecordProperty("normalize_1000_ns", durations[0].count()); - RecordProperty("normalize_10000_ns", durations[1].count()); - RecordProperty("normalize_100000_ns", durations[2].count()); - EXPECT_LT(durations[1].count(), durations[0].count() * 50); - EXPECT_LT(durations[2].count(), durations[1].count() * 50); -} - TEST_F(GlobalPhaseNormalizationTest, ScalesLinearlyAcrossNestedDynamicIntegralPowers) { constexpr std::array depths{128, 256, 512, 1'024}; diff --git a/python/mqt/core/mlir.pyi b/python/mqt/core/mlir.pyi index 771fda6f26..58e8f53c5c 100644 --- a/python/mqt/core/mlir.pyi +++ b/python/mqt/core/mlir.pyi @@ -150,9 +150,6 @@ class QCOProgram(Program): def fuse_single_qubit_unitary_runs(self, *, basis: str = "zyz") -> None: """Fuse single-qubit unitary runs into the chosen decomposition basis.""" - def fuse_two_qubit_unitary_runs(self, *, native_gates: str) -> None: - """Lower unitaries to a non-empty native gate menu via two-qubit run fusion.""" - def unroll_quantum_loops(self, *, unroll_factor: int = -1) -> None: """Unroll quantum loops, optionally using a maximum unroll factor.""" diff --git a/python/mqt/core/plugins/qiskit/backend.py b/python/mqt/core/plugins/qiskit/backend.py index df131e7911..dc14f94548 100644 --- a/python/mqt/core/plugins/qiskit/backend.py +++ b/python/mqt/core/plugins/qiskit/backend.py @@ -264,7 +264,7 @@ def _add_operation_to_target( Subclasses may override this to customize how an individual device operation is represented in the Target, e.g. substituting fictional - qubit-qubit loci for an operation that natively acts on non-qubit + pairs of qubit sites for an operation that natively acts on non-qubit sites (such as a qubit-resonator gate). Args: diff --git a/test/python/test_mlir.py b/test/python/test_mlir.py index 3dc2dd4c3a..5fffae2715 100644 --- a/test/python/test_mlir.py +++ b/test/python/test_mlir.py @@ -365,20 +365,6 @@ def test_typed_programs_normalize_global_phases() -> None: assert qco.ir == once -def test_qco_program_two_qubit_fusion_requires_native_gates() -> None: - """Require callers to provide a non-empty native gate menu.""" - qco = compile_program(QASM_STRING, output=OutputFormat.QCO) - assert isinstance(qco, QCOProgram) - - with pytest.raises(TypeError, match="incompatible function arguments"): - qco.fuse_two_qubit_unitary_runs() # ty: ignore[missing-argument] - - with pytest.raises(RuntimeError, match="MLIR operation failed"): - qco.fuse_two_qubit_unitary_runs(native_gates="") - - qco.fuse_two_qubit_unitary_runs(native_gates="u,cx") - - def test_qco_program_decomposes_multi_controlled() -> None: """Decompose multi-controlled gates through the typed QCOProgram API.""" qc = QuantumComputation(3) diff --git a/test/qdmi/devices/sc/test_configuration.cpp b/test/qdmi/devices/sc/test_configuration.cpp index 7089657c5d..0b92fdc4de 100644 --- a/test/qdmi/devices/sc/test_configuration.cpp +++ b/test/qdmi/devices/sc/test_configuration.cpp @@ -69,7 +69,7 @@ TEST(ScConfigurationTest, ParsesBundledDeviceStrictly) { TEST(ScConfigurationTest, ParsesIqmDeviceModels) { const auto assertModel = [](const Device& device, const std::string& name, const size_t qubits, const size_t couplings, - const size_t czLoci) { + const size_t czSiteTuples) { EXPECT_EQ(device.name, name); EXPECT_EQ(device.numQubits, qubits); EXPECT_EQ(device.couplings.size(), couplings); @@ -91,16 +91,16 @@ TEST(ScConfigurationTest, ParsesIqmDeviceModels) { EXPECT_EQ(device.operations[1].name, "cz"); EXPECT_EQ(device.operations[1].numParameters, 0); EXPECT_EQ(device.operations[1].numQubits, 2); - EXPECT_EQ(device.operations[1].siteOverrides.size(), czLoci); + EXPECT_EQ(device.operations[1].siteOverrides.size(), czSiteTuples); EXPECT_EQ(device.operations[2].name, "measure"); EXPECT_EQ(device.operations[2].numParameters, 0); EXPECT_EQ(device.operations[2].numQubits, 1); EXPECT_EQ(device.operations[2].siteOverrides.size(), qubits); for (const auto& operation : device.operations) { EXPECT_FALSE(operation.duration); - for (const auto& locus : operation.siteOverrides) { - EXPECT_FALSE(locus.duration); - EXPECT_TRUE(locus.fidelity); + for (const auto& siteOverride : operation.siteOverrides) { + EXPECT_FALSE(siteOverride.duration); + EXPECT_TRUE(siteOverride.fidelity); } } };