diff --git a/CHANGELOG.md b/CHANGELOG.md index 63090a64e2..cdc0b9ccfe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,10 +12,13 @@ releases may include breaking changes. ### Added +- ✨ Add a `fuse-two-qubit-unitary-runs` pass for fusing compile-time two-qubit + unitary windows via Weyl/KAK resynthesis ([#1865]) ([**@simon1hofmann**], + [**@burgholzer**]) - ✨ Add support for IQM's `move` gate in the QDMI Qiskit backend converter ([#1844], [#1848]) ([**@burgholzer**], [**@marcelwa**]) - 🚸 Add `const` version of the `CompoundOperation`'s `getOps()` function - ([#1826]) ([**@ystade]) + ([#1826]) ([**@ystade**]) - 🐳 Add dev container configuration for consistent local development environment ([#1786]) ([**@denialhaag**]) - ✨ Add two-qubit Weyl (KAK) decomposition and native-gateset synthesis support @@ -600,6 +603,7 @@ changelogs._ +[#1865]: https://github.com/munich-quantum-toolkit/core/pull/1865 [#1848]: https://github.com/munich-quantum-toolkit/core/pull/1848 [#1844]: https://github.com/munich-quantum-toolkit/core/pull/1844 [#1842]: https://github.com/munich-quantum-toolkit/core/pull/1842 diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h index 4130967cde..952875d972 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h @@ -20,6 +20,10 @@ #include #include +namespace mlir { +class RewritePatternSet; +} // namespace mlir + namespace mlir::qco::decomposition { /** @@ -99,4 +103,15 @@ synthesizeUnitary1QEuler(OpBuilder& builder, Location loc, Value qubit, */ void emitGPhaseIfNeeded(OpBuilder& builder, Location loc, double phase); +/** + * @brief Populates @p patterns with the single-qubit run fusion rewrite for + * @p basis (the reusable core of `fuse-single-qubit-unitary-runs`). + * + * @param skipControlledBodies When set, single-qubit gates nested in `qco.ctrl` + * bodies are left untouched. + */ +void populateFuseSingleQubitUnitaryRunsPatterns( + RewritePatternSet& patterns, EulerBasis 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 index caa11c7c1e..ac73592bae 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeGateset.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeGateset.h @@ -18,6 +18,10 @@ #include #include +namespace mlir { +class Operation; +} // namespace mlir + namespace mlir::qco::decomposition { /** @@ -63,6 +67,15 @@ struct NativeGateset { */ [[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 + * and single-target `qco.ctrl` shells (with an `X`/`Z` body) are checked + * against @p gates. All other ops are rejected. + */ + [[nodiscard]] bool allowsOp(Operation* op) const; }; } // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index 971dc18f71..ebdd8db428 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -67,6 +67,57 @@ 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"]; + let summary = "Lower QCO unitary gates to a user-specified native gate menu."; + let description = [{ + Rewrites unitaries to match the comma-separated `native-gates` gateset. + `qco.barrier` and `qco.gphase` are preserved. A `qco.ctrl` is treated as a + two-qubit gate only when it has a single control and a single target; 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 the + two-qubit entangler `cx` or `cz`. + + Recognised tokens: `u`, `x`, `sx`, `rz`, `rx`, `ry`, `r`, `cx`, + `cz`. 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 either `cx` + or `cz` as the entangler, or list both if both are native): + - IBM: `x,sx,rz,cx` or `x,sx,rz,cz` + - Generic single-qubit U: `u,cx` or `u,cz` + - 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`. + + 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: `cx` is preferred over `cz`, 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, cx, cz. " + "Examples: x,sx,rz,cx; u,cx; r,cz; rx,rz,cx.">]; +} + +//===----------------------------------------------------------------------===// + def QuantumLoopUnroll : InterfacePass<"quantum-loop-unroll", "FunctionOpInterface"> { let dependentDialects = ["mlir::qco::QCODialect", "mlir::scf::SCFDialect"]; diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeGateset.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeGateset.cpp index 0fdec01791..fe3ae7aece 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeGateset.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeGateset.cpp @@ -10,12 +10,16 @@ #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 @@ -141,6 +145,48 @@ NativeGateset::decomposeTarget(const Matrix4x4& target) const { 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([&](CtrlOp ctrl) { + const auto kind = entanglerKindFor(ctrl); + return kind && gates.contains(*kind); + }) + .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) { diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp index 73fe887dfb..b6fa901035 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp @@ -137,10 +137,13 @@ namespace { struct FuseSingleQubitUnitaryRunsPattern final : OpInterfaceRewritePattern { FuseSingleQubitUnitaryRunsPattern(MLIRContext* context, - const decomposition::EulerBasis basis) - : OpInterfaceRewritePattern(context), basis(basis) {} + const decomposition::EulerBasis basis, + const bool skipControlledBodies) + : OpInterfaceRewritePattern(context), basis(basis), + skipControlledBodies(skipControlledBodies) {} decomposition::EulerBasis basis; + bool skipControlledBodies; /** * @brief Whether `op` starts a run. @@ -165,6 +168,10 @@ struct FuseSingleQubitUnitaryRunsPattern final */ LogicalResult matchAndRewrite(UnitaryOpInterface op, PatternRewriter& rewriter) const override { + if (skipControlledBodies && + (op.getOperation()->getParentOfType() != nullptr)) { + return failure(); + } if (!isRunStart(op)) { return failure(); } @@ -208,8 +215,8 @@ struct FuseSingleQubitUnitaryRunsPass final } RewritePatternSet patterns(&getContext()); - patterns.add(patterns.getContext(), - *parsed); + decomposition::populateFuseSingleQubitUnitaryRunsPatterns( + patterns, *parsed, /*skipControlledBodies=*/false); if (failed(applyPatternsGreedily(module, std::move(patterns)))) { signalPassFailure(); @@ -220,3 +227,14 @@ struct FuseSingleQubitUnitaryRunsPass final } // namespace } // namespace mlir::qco + +namespace mlir::qco::decomposition { + +void populateFuseSingleQubitUnitaryRunsPatterns( + RewritePatternSet& patterns, const EulerBasis basis, + const bool skipControlledBodies) { + patterns.add(patterns.getContext(), basis, + skipControlledBodies); +} + +} // namespace mlir::qco::decomposition diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp new file mode 100644 index 0000000000..f8491d70b8 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp @@ -0,0 +1,457 @@ +/* + * 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 +#include +#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); + } +} + +/// Whether any single- or two-qubit unitary (including `ctrl` shells) remains +/// off the native gateset. 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 bool hasNonNativeOps(Operation* root, const NativeGateset& spec) { + const WalkResult walkResult = root->walk([&](Operation* op) { + auto unitary = dyn_cast(op); + if (!unitary || !isWalkableUnitaryShell(op) || unitary.getNumQubits() > 2) { + return WalkResult::advance(); + } + return spec.allowsOp(op) ? WalkResult::advance() : WalkResult::interrupt(); + }); + return walkResult.wasInterrupted(); +} + +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); + Value newA; + Value newB; + if (failed(synthesizeUnitary2QWeyl( + rewriter, firstOp.getLoc(), firstOp.getInputQubit(0), + firstOp.getInputQubit(1), run.composed, spec, newA, newB))) { + firstOp->emitError("failed to emit synthesized two-qubit gate sequence"); + return failure(); + } + rewriter.replaceAllUsesWith(run.tailA, newA); + rewriter.replaceAllUsesWith(run.tailB, newB); + 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 { + 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); + Value out0; + Value out1; + if (failed(synthesizeUnitary2QWeyl(rewriter, raw->getLoc(), in0, in1, + matrix, spec, out0, out1))) { + return failure(); + } + rewriter.replaceOp(raw, ValueRange{out0, out1}); + 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 module, + const EulerBasis basis) { + RewritePatternSet patterns(module.getContext()); + populateFuseSingleQubitUnitaryRunsPatterns(patterns, basis, + /*skipControlledBodies=*/true); + return applyPatternsGreedily(module, std::move(patterns)); +} + +/// Fuses two-qubit runs, then lowers any remaining off-gateset two-qubit ops. +static LogicalResult fuseAndLowerTwoQubitOps(ModuleOp module, + const NativeGateset& spec) { + MLIRContext* ctx = module.getContext(); + { + RewritePatternSet runPatterns(ctx); + runPatterns.add(ctx, spec); + if (failed(applyPatternsGreedily(module, std::move(runPatterns)))) { + return failure(); + } + } + RewritePatternSet lowerPatterns(ctx); + lowerPatterns.add(ctx, spec); + return applyPatternsGreedily(module, 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 module = 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(module, basis)) || + failed(fuseAndLowerTwoQubitOps(module, *spec)) || + failed(fuseSingleQubitRuns(module, basis))) { + signalPassFailure(); + return; + } + + if (hasNonNativeOps(module, *spec)) { + module.emitError() << "native gate synthesis: operations remain outside " + "the native gateset (native-gates='" + << nativeGates << "')"; + signalPassFailure(); + } + } +}; + +} // namespace + +} // namespace mlir::qco diff --git a/mlir/unittests/Dialect/QCO/Transforms/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/CMakeLists.txt index d59780f461..163412b775 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/CMakeLists.txt +++ b/mlir/unittests/Dialect/QCO/Transforms/CMakeLists.txt @@ -8,4 +8,5 @@ add_subdirectory(Decomposition) add_subdirectory(Mapping) +add_subdirectory(NativeSynthesis) add_subdirectory(Optimizations) 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 c10ea5df1f..37c175edc8 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -18,7 +18,6 @@ #include #include -#include #include #include #include @@ -769,48 +768,6 @@ TEST(NativeSpecTest, ResolvesEulerBasisFromGateset) { EXPECT_EQ(*zyz->eulerBasis, EulerBasis::ZYZ); } -static std::optional gateKindFor(UnitaryOpInterface op) { - return llvm::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 llvm::TypeSwitch>( - ctrl.getBodyUnitary(0).getOperation()) - .Case([](XOp) { return NativeGateKind::CX; }) - .Case([](ZOp) { return NativeGateKind::CZ; }) - .Default([](Operation*) { return std::nullopt; }); -} - -static bool allowsOp(Operation* op, const NativeGateset& spec) { - return llvm::TypeSwitch(op) - .Case([](auto) { return true; }) - .Case([&](CtrlOp ctrl) { - const auto kind = entanglerKindFor(ctrl); - return kind && spec.gates.contains(*kind); - }) - .Case([&](UnitaryOpInterface unitary) { - if (!unitary.isSingleQubit()) { - return false; - } - const auto gate = gateKindFor(unitary); - return gate && spec.gates.contains(*gate); - }) - .Default([](Operation*) { return false; }); -} - TEST_F(NativeGatesetMlirTest, AllowsOpMatchesGateset) { const auto spec = NativeGateset::parse("u,cx"); ASSERT_TRUE(spec); @@ -826,20 +783,19 @@ TEST_F(NativeGatesetMlirTest, AllowsOpMatchesGateset) { Value q0 = entry->getArgument(0); Value q1 = entry->getArgument(1); - EXPECT_TRUE(allowsOp( - BarrierOp::create(builder, loc, ValueRange{q0, q1}).getOperation(), - *spec)); + EXPECT_TRUE(spec->allowsOp( + BarrierOp::create(builder, loc, ValueRange{q0, q1}).getOperation())); EXPECT_TRUE( - allowsOp(GPhaseOp::create(builder, loc, 0.1).getOperation(), *spec)); - EXPECT_TRUE(allowsOp( - UOp::create(builder, loc, q0, 0.1, 0.2, 0.3).getOperation(), *spec)); + 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, ValueRange{q0}, ValueRange{q1}, [&builder, &loc](ValueRange targets) -> SmallVector { return {XOp::create(builder, loc, targets[0]).getOutputQubit(0)}; }); - EXPECT_TRUE(allowsOp(cx.getOperation(), *spec)); + EXPECT_TRUE(spec->allowsOp(cx.getOperation())); auto cxWithInterleavedH = CtrlOp::create( builder, loc, ValueRange{q0}, ValueRange{q1}, @@ -847,25 +803,25 @@ TEST_F(NativeGatesetMlirTest, AllowsOpMatchesGateset) { auto wire = XOp::create(builder, loc, targets[0]).getOutputQubit(0); return {HOp::create(builder, loc, wire).getOutputQubit(0)}; }); - EXPECT_FALSE(allowsOp(cxWithInterleavedH.getOperation(), *spec)); + EXPECT_FALSE(spec->allowsOp(cxWithInterleavedH.getOperation())); - EXPECT_FALSE(allowsOp(XOp::create(builder, loc, q0).getOperation(), *spec)); + EXPECT_FALSE(spec->allowsOp(XOp::create(builder, loc, q0).getOperation())); EXPECT_FALSE( - allowsOp(RXXOp::create(builder, loc, q0, q1, 0.2).getOperation(), *spec)); + spec->allowsOp(RXXOp::create(builder, loc, q0, q1, 0.2).getOperation())); const auto rzSpec = NativeGateset::parse("x,sx,rz,cx"); ASSERT_TRUE(rzSpec); EXPECT_TRUE( - allowsOp(RZOp::create(builder, loc, q0, 0.3).getOperation(), *rzSpec)); + rzSpec->allowsOp(RZOp::create(builder, loc, q0, 0.3).getOperation())); EXPECT_FALSE( - allowsOp(POp::create(builder, loc, q0, 0.3).getOperation(), *rzSpec)); + rzSpec->allowsOp(POp::create(builder, loc, q0, 0.3).getOperation())); auto hCtrl = CtrlOp::create( builder, loc, ValueRange{q0}, ValueRange{q1}, [&builder, &loc](ValueRange targets) -> SmallVector { return {HOp::create(builder, loc, targets[0]).getOutputQubit(0)}; }); - EXPECT_FALSE(allowsOp(hCtrl.getOperation(), *spec)); + EXPECT_FALSE(spec->allowsOp(hCtrl.getOperation())); const auto funcTy3 = builder.getFunctionType({qubitTy, qubitTy, qubitTy}, {qubitTy, qubitTy, qubitTy}); @@ -880,7 +836,7 @@ TEST_F(NativeGatesetMlirTest, AllowsOpMatchesGateset) { [&builder, &loc](ValueRange targets) -> SmallVector { return {XOp::create(builder, loc, targets[0]).getOutputQubit(0)}; }); - EXPECT_FALSE(allowsOp(ccx.getOperation(), *spec)); + EXPECT_FALSE(spec->allowsOp(ccx.getOperation())); const auto czSpec = NativeGateset::parse("u,cz"); ASSERT_TRUE(czSpec); @@ -889,6 +845,6 @@ TEST_F(NativeGatesetMlirTest, AllowsOpMatchesGateset) { [&builder, &loc](ValueRange targets) -> SmallVector { return {ZOp::create(builder, loc, targets[0]).getOutputQubit(0)}; }); - EXPECT_TRUE(allowsOp(cz.getOperation(), *czSpec)); - EXPECT_FALSE(allowsOp(cx.getOperation(), *czSpec)); + EXPECT_TRUE(czSpec->allowsOp(cz.getOperation())); + EXPECT_FALSE(czSpec->allowsOp(cx.getOperation())); } diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt new file mode 100644 index 0000000000..0fa87a5daf --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt @@ -0,0 +1,30 @@ +# 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 + +set(target_name mqt-core-mlir-unittest-fuse-two-qubit-unitary-runs) +add_executable(${target_name} test_fuse_two_qubit_unitary_runs.cpp) + +target_link_libraries( + ${target_name} + PRIVATE MLIRParser + GTest::gtest_main + MLIRQCPrograms + MLIRQCOProgramBuilder + MLIRQCOUtils + MLIRQCToQCO + MLIRQCOTransforms + MLIRPass + MLIRFuncDialect + MLIRArithDialect + MLIRIR + MLIRSupport + LLVMSupport) + +mqt_mlir_configure_unittest_target(${target_name}) + +gtest_discover_tests(${target_name} PROPERTIES LABELS mqt-mlir-unittests DISCOVERY_TIMEOUT 60) 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 new file mode 100644 index 0000000000..207363c98b --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp @@ -0,0 +1,729 @@ +/* + * 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/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/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 + +using namespace mlir; +using namespace mlir::qco; + +using ProgramFn = void (*)(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; +} + +// --- Reference unitary reconstruction ------------------------------------ // + +static std::optional unitaryQubit(Value v, size_t index, + size_t numQubits) { + if (index >= numQubits || !isa(v.getType())) { + return std::nullopt; + } + return v; +} + +static std::optional unitaryQubitOperand(UnitaryOpInterface op, + size_t index) { + return unitaryQubit(op->getOperand(index), index, op.getNumQubits()); +} + +static std::optional unitaryQubitResult(UnitaryOpInterface op, + size_t index) { + return unitaryQubit(op->getResult(index), index, op.getNumQubits()); +} + +static bool extractSingleQubitMatrix(UnitaryOpInterface op, Matrix2x2& out) { + if (op.getUnitaryMatrix2x2(out)) { + return true; + } + DynamicMatrix dynamic; + if (!op.getUnitaryMatrixDynamic(dynamic) || dynamic.rows() != 2 || + dynamic.cols() != 2) { + return false; + } + out = Matrix2x2::fromElements(dynamic(0, 0), dynamic(0, 1), dynamic(1, 0), + dynamic(1, 1)); + return true; +} + +static bool extractTwoQubitMatrix(UnitaryOpInterface op, Matrix4x4& out) { + if (auto ctrl = dyn_cast(op.getOperation()); + ctrl && (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1)) { + return false; + } + return op.getUnitaryMatrix4x4(out); +} + +static std::optional +computeUnitaryFromQcoModule(const OwningOpRef& moduleOp) { + ModuleOp module = moduleOp.get(); + if (!module) { + return std::nullopt; + } + + DenseMap qubitIds; + size_t nextQubitId = 0; + size_t numQubits = 0; + + for (auto func : module.getOps()) { + for (auto& block : func.getBlocks()) { + for (auto& rawOp : block.getOperations()) { + if (auto staticOp = dyn_cast(&rawOp)) { + const auto index = static_cast(staticOp.getIndex()); + qubitIds.try_emplace(staticOp.getQubit(), index); + numQubits = std::max(numQubits, index + 1); + } else if (auto alloc = dyn_cast(&rawOp)) { + qubitIds.try_emplace(alloc.getResult(), nextQubitId++); + numQubits = std::max(numQubits, nextQubitId); + } + } + } + } + + if (numQubits == 0) { + return std::nullopt; + } + + DynamicMatrix unitary = + DynamicMatrix::identity(static_cast(1ULL << numQubits)); + Complex globalPhase{1.0, 0.0}; + + auto getQubitId = [&](Value qubit) -> std::optional { + const auto it = qubitIds.find(qubit); + if (it == qubitIds.end()) { + return std::nullopt; + } + return it->second; + }; + + for (auto func : module.getOps()) { + for (auto& block : func.getBlocks()) { + for (auto& rawOp : block.getOperations()) { + auto op = dyn_cast(&rawOp); + if (!op) { + continue; + } + if (isa(op.getOperation())) { + // A barrier is an identity on its wires, but it still threads qubit + // values, so carry each input's id over to the matching output. + for (size_t i = 0; i < op.getNumQubits(); ++i) { + if (const auto id = getQubitId(op->getOperand(i))) { + qubitIds[op->getResult(i)] = *id; + } + } + continue; + } + if (auto gphase = dyn_cast(op.getOperation())) { + if (const auto matrix = gphase.getUnitaryMatrix()) { + globalPhase *= (*matrix)(0, 0); + } + continue; + } + + if (op.isSingleQubit()) { + const auto qIn = unitaryQubitOperand(op, 0); + if (!qIn) { + return std::nullopt; + } + const auto qid = getQubitId(*qIn); + if (!qid) { + return std::nullopt; + } + Matrix2x2 oneQ; + if (!extractSingleQubitMatrix(op, oneQ)) { + return std::nullopt; + } + unitary = oneQ.embedInNqubit(numQubits, *qid) * unitary; + const auto qOut = unitaryQubitResult(op, 0); + if (!qOut) { + return std::nullopt; + } + qubitIds[*qOut] = *qid; + continue; + } + + if (op.isTwoQubit()) { + const auto q0In = unitaryQubitOperand(op, 0); + const auto q1In = unitaryQubitOperand(op, 1); + if (!q0In || !q1In) { + return std::nullopt; + } + const auto q0id = getQubitId(*q0In); + const auto q1id = getQubitId(*q1In); + if (!q0id || !q1id) { + return std::nullopt; + } + Matrix4x4 twoQ; + if (!extractTwoQubitMatrix(op, twoQ)) { + return std::nullopt; + } + unitary = twoQ.embedInNqubit(numQubits, *q0id, *q1id) * unitary; + const auto q0Out = unitaryQubitResult(op, 0); + const auto q1Out = unitaryQubitResult(op, 1); + if (!q0Out || !q1Out) { + return std::nullopt; + } + qubitIds[*q0Out] = *q0id; + qubitIds[*q1Out] = *q1id; + continue; + } + + return std::nullopt; + } + } + } + + return globalPhase * unitary; +} + +// --- 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 void swapTwoQ(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.swap(q0, q1); +} + +/// Rich single-qubit variety on both wires, followed by a two-qubit entangler. +static void broadOneQThenCz(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + 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); +} + +/// Long single-qubit run on one wire, then an entangler (Euler-run fusion). +static void hstycxTwoQ(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.s(q0); + b.t(q0); + b.y(q0); + b.cx(q0, q1); +} + +/// Zero-angle rotations that must canonicalize away before the entangler. +static void zeroAngleThenCz(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.rx(0.0, q0); + b.ry(0.0, q1); + b.rz(0.0, q0); + b.p(0.0, q1); + b.cz(q0, q1); +} + +/// Single-qubit gates surrounding an entangler on both sides. +static void hCxSq1(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.cx(q0, q1); + b.s(q1); +} + +/// Three-qubit program with chained entanglers on overlapping pairs. +static void threeQGhz(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + const auto q2 = b.allocQubit(); + b.h(q0); + b.cx(q0, q1); + b.cx(q1, q2); +} + +/// Single-qubit gates wrapped in an inverse modifier (no entangler). +static void inverseTwoX(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + b.inv({q0}, [&](mlir::ValueRange qubits) { + b.x(qubits[0]); + b.x(qubits[0]); + }); +} + +/// A controlled two-gate body that must be synthesized as a two-qubit unitary. +static void controlledXH(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.ctrl(q0, {q1}, [&](ValueRange targets) { + b.x(targets[0]); + b.h(targets[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 void fusionCxCx(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.cx(q0, q1); + b.cx(q0, q1); +} + +static void fusionHCxInterleavedTCx(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.cx(q0, q1); + b.t(q1); + b.s(q0); + b.cx(q0, q1); +} + +static void fusionThreeLineCx(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + const auto q2 = b.allocQubit(); + b.cx(q0, q1); + b.cx(q1, q2); + b.cx(q0, q1); +} + +static void fusionCxRSharedOtherPair(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + const auto q2 = b.allocQubit(); + b.cx(q0, q1); + b.rz(0.17, q1); + b.cx(q1, q2); +} + +static void fusionCxBarrierCx(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.cx(q0, q1); + b.barrier({q0, q1}); + b.cx(q0, q1); +} + +/// 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 void fusionCxSingleWireBarrierCx(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.cx(q0, q1); + b.barrier({q0}); + b.cx(q0, q1); +} + +/// 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 void fusionCxThenMultiControlledX(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + const auto q2 = b.allocQubit(); + b.cx(q0, q1); + b.mcx({q0, q1}, q2); +} + +static void fusionSwapCxPattern(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.cx(q0, q1); + b.cx(q1, q0); + b.cx(q0, q1); +} + +static void fusionOffMenuGateInWindow(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.cx(q0, q1); + b.h(q0); + b.cx(q0, q1); +} + +static void fusionDualWireOneQBetweenCx(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.cx(q0, q1); + b.rz(0.11, q0); + b.ry(0.22, q1); + b.cx(q0, q1); +} + +static void 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); +} + +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 both +/// entangler families (plus a multi-entangler menu). 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 = { + // CX entangler family + "x,sx,rz,cx", // ZSXX + "u,cx", // U + "rx,rz,cx", // XZX + "rx,ry,cx", // XYX + // CZ entangler family + "r,cz", // R + "ry,rz,cz", // ZYZ + "x,sx,rz,cz", // ZSXX + "u,cz", // U + // Multiple entanglers (cx preferred) + "u,cx,cz", +}; + +/// 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 lhsUnitary = computeUnitaryFromQcoModule(lhs); + ASSERT_TRUE(lhsUnitary.has_value()); + const auto rhsUnitary = computeUnitaryFromQcoModule(rhs); + ASSERT_TRUE(rhsUnitary.has_value()); + EXPECT_TRUE(lhsUnitary->isApprox(*rhsUnitary)); + } + + 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)); + 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 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; +}; + +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, 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); +}