diff --git a/CHANGELOG.md b/CHANGELOG.md index b1ecfbd17a..f2b8cafd55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ 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 Python bindings for the MQT Compiler Collection ([#1815]) ([**@burgholzer**], [**@denialhaag**]) - ✨ Add support for QDMI child devices to the driver and FoMaC libraries @@ -22,10 +25,12 @@ releases may include breaking changes. ([#1887]) ([**@flowerthrower**], [**@burgholzer**]) - ✨ Add QIR Output Schemas support to the QIR runtime ([#1877]) ([**@rturrado**]) +- ✨ Add a `fuse-two-qubit-unitary-runs` pass for fusing compile-time two-qubit + unitary windows via Weyl/KAK resynthesis ([#1655]) ([**@simon1hofmann**]) - ✨ 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 @@ -646,6 +651,7 @@ changelogs._ [#1872]: https://github.com/munich-quantum-toolkit/core/pull/1872 [#1870]: https://github.com/munich-quantum-toolkit/core/pull/1870 [#1869]: https://github.com/munich-quantum-toolkit/core/pull/1869 +[#1865]: https://github.com/munich-quantum-toolkit/core/pull/1865 [#1850]: https://github.com/munich-quantum-toolkit/core/pull/1850 [#1849]: https://github.com/munich-quantum-toolkit/core/pull/1849 [#1848]: https://github.com/munich-quantum-toolkit/core/pull/1848 @@ -704,6 +710,7 @@ changelogs._ [#1664]: https://github.com/munich-quantum-toolkit/core/pull/1664 [#1662]: https://github.com/munich-quantum-toolkit/core/pull/1662 [#1660]: https://github.com/munich-quantum-toolkit/core/pull/1660 +[#1655]: https://github.com/munich-quantum-toolkit/core/pull/1655 [#1652]: https://github.com/munich-quantum-toolkit/core/pull/1652 [#1638]: https://github.com/munich-quantum-toolkit/core/pull/1638 [#1637]: https://github.com/munich-quantum-toolkit/core/pull/1637 diff --git a/bindings/mlir/register_mlir.cpp b/bindings/mlir/register_mlir.cpp index d30a79c5e2..8c607c5e9f 100644 --- a/bindings/mlir/register_mlir.cpp +++ b/bindings/mlir/register_mlir.cpp @@ -387,6 +387,11 @@ 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 comma-separated native gate menu.") .def("unroll_quantum_loops", &BooleanMemberAdapter<&mlir::QCOProgram::unrollQuantumLoops>::call, nb::kw_only(), "unroll_factor"_a = -1, diff --git a/mlir/include/mlir/Compiler/Programs.h b/mlir/include/mlir/Compiler/Programs.h index 96e7bc5aa1..5b1d087c9a 100644 --- a/mlir/include/mlir/Compiler/Programs.h +++ b/mlir/include/mlir/Compiler/Programs.h @@ -182,6 +182,14 @@ class QCOProgram final : public Program { /** @brief Fuse single-qubit unitary runs into the selected Euler basis. */ [[nodiscard]] bool fuseSingleQubitUnitaryRuns(std::string_view basis = "zyz"); + /** + * @brief Lower unitaries to a comma-separated native gate menu. + * + * @details An empty or whitespace-only menu is a no-op. Unrecognised tokens + * cause the pass to fail. + */ + [[nodiscard]] bool fuseTwoQubitUnitaryRuns(std::string_view nativeGates = ""); + /** @brief Unroll loops containing quantum operations. */ [[nodiscard]] bool unrollQuantumLoops(int64_t factor = -1); 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/include/mlir/Dialect/QCO/Utils/Matrix.h b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h index cb99ce6fce..76fca54a16 100644 --- a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h +++ b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -866,6 +867,32 @@ concept SupportedMatrix = std::same_as || std::same_as || std::same_as || std::same_as; +/** + * @brief Checks whether two unitaries are equal up to a global phase. + * + * Uses `trace(rhs.adjoint() * lhs)` to infer a unit-modulus phase factor, then + * compares `lhs` to `factor * rhs` with @ref isApprox. A near-zero overlap + * (`|trace| <= tol`) is treated as not equivalent to avoid dividing by a tiny + * number. + * + * @tparam Matrix Any supported fixed- or dynamic-size matrix type. + * @param lhs Left-hand unitary. + * @param rhs Right-hand unitary. + * @param tol Absolute tolerance for overlap and entry-wise comparison. + * @return True when @p lhs and @p rhs differ only by a global phase. + */ +template +[[nodiscard]] bool isEquivalentUpToGlobalPhase(const Matrix& lhs, + const Matrix& rhs, + double tol = MATRIX_TOLERANCE) { + const auto overlap = (rhs.adjoint() * lhs).trace(); + if (std::abs(overlap) <= tol) { + return false; + } + const auto factor = overlap / std::abs(overlap); + return lhs.isApprox(factor * rhs, tol); +} + /// Scalar-on-the-left multiply `scalar * matrix` (commutes with the member /// `matrix * scalar`). Provided so generic code can scale a matrix from /// either side. diff --git a/mlir/lib/Compiler/Programs.cpp b/mlir/lib/Compiler/Programs.cpp index 822289abb8..f06fb7bd0d 100644 --- a/mlir/lib/Compiler/Programs.cpp +++ b/mlir/lib/Compiler/Programs.cpp @@ -343,6 +343,17 @@ bool QCOProgram::fuseSingleQubitUnitaryRuns(const std::string_view basis) { "failed to fuse single-qubit unitary runs")); } +bool QCOProgram::fuseTwoQubitUnitaryRuns(const std::string_view nativeGates) { + qco::FuseTwoQubitUnitaryRunsOptions options; + options.nativeGates = nativeGates; + return succeeded(runPasses( + module(), + [&options](OpPassManager& pm) { + pm.addPass(qco::createFuseTwoQubitUnitaryRuns(options)); + }, + "failed to fuse two-qubit unitary runs")); +} + bool QCOProgram::unrollQuantumLoops(const int64_t factor) { qco::QuantumLoopUnrollOptions options; options.unrollFactor = factor; diff --git a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt index 77f594db17..77d847d3ba 100644 --- a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt +++ b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt @@ -24,11 +24,11 @@ add_mlir_library( mqt_mlir_target_use_project_options(MLIRQCOTransforms) -# collect header files -file(GLOB_RECURSE PASSES_HEADERS_SOURCE - ${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Dialect/QCO/Transforms/*.h) -file(GLOB_RECURSE PASSES_HEADERS_BUILD - ${MQT_MLIR_BUILD_INCLUDE_DIR}/mlir/Dialect/QCO/Transforms/*.inc) +# collect header files (subdirs: Decomposition/, …) +file(GLOB_RECURSE PASSES_HEADERS_SOURCE CONFIGURE_DEPENDS + "${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Dialect/QCO/Transforms/*.h") +file(GLOB_RECURSE PASSES_HEADERS_BUILD CONFIGURE_DEPENDS + "${MQT_MLIR_BUILD_INCLUDE_DIR}/mlir/Dialect/QCO/Transforms/*.inc") # add public headers using file sets target_sources( 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/lib/Support/Passes.cpp b/mlir/lib/Support/Passes.cpp index e9f5dd7296..f4a28e241e 100644 --- a/mlir/lib/Support/Passes.cpp +++ b/mlir/lib/Support/Passes.cpp @@ -46,6 +46,7 @@ runWithPassManager(ModuleOp module, void registerMQTCompilerPasses() { static const auto REGISTERED = [] { qco::registerFuseSingleQubitUnitaryRuns(); + qco::registerFuseTwoQubitUnitaryRuns(); qco::registerHadamardLifting(); qco::registerMergeSingleQubitRotationGates(); qco::registerQuantumLoopUnroll(); diff --git a/mlir/tools/mqt-cc/mqt-cc.cpp b/mlir/tools/mqt-cc/mqt-cc.cpp index 2170c32d78..a25a24a8ca 100644 --- a/mlir/tools/mqt-cc/mqt-cc.cpp +++ b/mlir/tools/mqt-cc/mqt-cc.cpp @@ -17,6 +17,7 @@ #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" @@ -83,6 +84,14 @@ 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 (appended after the default or custom QCO optimization " + "pipeline)"), + llvm::cl::value_desc("csv"), llvm::cl::init("")); + namespace { enum class InputFormat : std::uint8_t { MLIR, QCO, QASM, Jeff }; enum class InputDialect : std::uint8_t { QC, QCO }; @@ -389,6 +398,12 @@ int main(int argc, char** argv) { } else { populateDefaultQCOOptimizationPipeline(pm); } + if (!nativeGates.empty()) { + pm.addPass(qco::createFuseTwoQubitUnitaryRuns( + qco::FuseTwoQubitUnitaryRunsOptions{ + .nativeGates = nativeGates.getValue(), + })); + } populateQCOCleanupPipeline(pm); return success(); }))) { diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 3c46089067..3a22aadfbf 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -16,6 +16,9 @@ #include "mlir/Dialect/QC/Translation/TranslateQuantumComputationToQC.h" #include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.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/Utils/Matrix.h" #include "mlir/Dialect/QIR/Builder/QIRProgramBuilder.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" #include "mlir/Support/IRVerification.h" @@ -27,6 +30,8 @@ #include #include +#include +#include #include #include #include @@ -49,7 +54,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -432,6 +439,7 @@ cx q[0], q[2]; EXPECT_TRUE(qco.fuseSingleQubitUnitaryRuns("zyz")); EXPECT_NE(qco.str(), beforeFusion); + EXPECT_TRUE(qco.fuseTwoQubitUnitaryRuns("u,cx")); const std::vector> coupling = { {0, 1}, {1, 0}, {1, 2}, {2, 1}}; EXPECT_TRUE(qco.placeAndRoute(coupling)); @@ -995,4 +1003,201 @@ INSTANTIATE_TEST_SUITE_P( nullptr, MQT_NAMED_BUILDER(mlir::qc::ctrlTwo), MQT_NAMED_BUILDER(mlir::qir::ctrlTwo)})); +namespace { + +class CompilerPipelineNativeSynthesisConfigTest : public testing::Test { +protected: + std::string beforeIR; + std::optional program; + + void SetUp() override { + DialectRegistry registry; + registry.insert(); + auto context = std::make_unique(); + context->appendDialectRegistry(registry); + context->loadAllAvailableDialects(); + + auto module = + QCProgramBuilder::build(context.get(), mlir::qc::staticQubitsWithOps); + ASSERT_TRUE(module); + + std::string source; + llvm::raw_string_ostream stream(source); + module->print(stream); + + auto qc = QCProgram::fromMLIRString(source); + ASSERT_TRUE(qc); + auto qco = std::move(*qc).intoQCO(); + ASSERT_TRUE(qco); + ASSERT_TRUE(qco->cleanup()); + beforeIR = qco->str(); + program = std::move(*qco); + } + + [[nodiscard]] std::string + runNativeSynthesisAndExpectSuccess(const std::string_view nativeGates) { + EXPECT_TRUE(program->fuseTwoQubitUnitaryRuns(nativeGates)); + return program->str(); + } + + void runNativeSynthesisAndExpectFailure(const std::string_view nativeGates) { + EXPECT_FALSE(program->fuseTwoQubitUnitaryRuns(nativeGates)); + } +}; + +[[nodiscard]] std::optional +computeStaticTwoQubitUnitary(ModuleOp module) { + if (module == nullptr) { + return std::nullopt; + } + + Matrix4x4 unitary = Matrix4x4::identity(); + llvm::DenseMap qubitIds; + + const 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()) { + if (auto staticOp = llvm::dyn_cast(&rawOp)) { + const auto index = static_cast(staticOp.getIndex()); + if (index >= 2) { + return std::nullopt; + } + qubitIds.try_emplace(staticOp.getResult(), index); + continue; + } + + if (llvm::isa(&rawOp)) { + continue; + } + + auto op = llvm::dyn_cast(&rawOp); + if (!op) { + continue; + } + + if (op.isSingleQubit()) { + const auto qid = getQubitId(op.getInputQubit(0)); + if (!qid) { + return std::nullopt; + } + Matrix2x2 oneQ; + if (!op.getUnitaryMatrix2x2(oneQ)) { + return std::nullopt; + } + unitary = oneQ.embedInTwoQubit(*qid) * unitary; + qubitIds[op.getOutputQubit(0)] = *qid; + continue; + } + + if (op.isTwoQubit()) { + const auto q0 = getQubitId(op.getInputQubit(0)); + const auto q1 = getQubitId(op.getInputQubit(1)); + if (!q0 || !q1) { + return std::nullopt; + } + Matrix4x4 twoQ; + if (!op.getUnitaryMatrix4x4(twoQ)) { + return std::nullopt; + } + const SmallVector ids{*q0, *q1}; + unitary = twoQ.reorderForQubits(ids[0], ids[1]) * unitary; + qubitIds[op.getOutputQubit(0)] = *q0; + qubitIds[op.getOutputQubit(1)] = *q1; + continue; + } + + return std::nullopt; + } + } + } + + return unitary; +} + +} // namespace + +TEST_F(CompilerPipelineNativeSynthesisConfigTest, + AppliesConfiguredNativeSynthesisProfile) { + const auto afterIR = runNativeSynthesisAndExpectSuccess("x,sx,rz,cx"); + + EXPECT_NE(beforeIR.find("qco.h"), std::string::npos); + EXPECT_EQ(afterIR.find("qco.h"), std::string::npos); +} + +TEST_F(CompilerPipelineNativeSynthesisConfigTest, + AppliesConfiguredU3CxNativeSynthesisProfile) { + const auto afterIR = runNativeSynthesisAndExpectSuccess("u,cx"); + + EXPECT_NE(beforeIR.find("qco.h"), std::string::npos); + EXPECT_EQ(afterIR.find("qco.h"), std::string::npos); + EXPECT_NE(afterIR.find("qco.u"), std::string::npos); +} + +TEST_F(CompilerPipelineNativeSynthesisConfigTest, + AppliesConfiguredExpandedNativeSynthesisProfile) { + const auto afterIR = runNativeSynthesisAndExpectSuccess("u,rx,rz,cx,cz"); + + EXPECT_NE(beforeIR.find("qco.h"), std::string::npos); + EXPECT_EQ(afterIR.find("qco.h"), std::string::npos); +} + +TEST_F(CompilerPipelineNativeSynthesisConfigTest, + RejectsUnderSpecifiedNativeSynthesisMenu) { + runNativeSynthesisAndExpectFailure("cx,cz"); +} + +TEST_F(CompilerPipelineNativeSynthesisConfigTest, + RejectsInvalidNativeGateToken) { + runNativeSynthesisAndExpectFailure("not-a-gate"); +} + +TEST_F(CompilerPipelineNativeSynthesisConfigTest, + LeavesIRUnchangedWhenNoNativeGatesetIsConfigured) { + const auto afterIR = runNativeSynthesisAndExpectSuccess(""); + + EXPECT_NE(beforeIR.find("qco.h"), std::string::npos); + EXPECT_EQ(beforeIR, afterIR); +} + +TEST_F(CompilerPipelineNativeSynthesisConfigTest, + LeavesIRUnchangedWhenNativeGatesIsWhitespaceOnly) { + const auto afterIR = runNativeSynthesisAndExpectSuccess(" \t "); + + EXPECT_NE(beforeIR.find("qco.h"), std::string::npos); + EXPECT_EQ(beforeIR, afterIR); +} + +TEST_F(CompilerPipelineNativeSynthesisConfigTest, + NativeSynthesisPreservesUnitaryOnStaticQubits) { + const auto afterIR = runNativeSynthesisAndExpectSuccess("x,sx,rz,cx"); + + DialectRegistry registry; + registry.insert(); + MLIRContext context(registry); + context.loadAllAvailableDialects(); + + auto preSynth = parseSourceString(beforeIR, &context); + auto postSynth = parseSourceString(afterIR, &context); + ASSERT_TRUE(preSynth); + ASSERT_TRUE(postSynth); + + const auto preU = computeStaticTwoQubitUnitary(preSynth.get()); + const auto postU = computeStaticTwoQubitUnitary(postSynth.get()); + ASSERT_TRUE(preU); + ASSERT_TRUE(postU); + EXPECT_TRUE(isEquivalentUpToGlobalPhase(*preU, *postU)); +} + } // namespace mqt::test::compiler 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 1a616d3d84..2b822c6cba 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,41 +783,40 @@ 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, q0, q1, [&](Value target) { return XOp::create(builder, loc, target).getOutputQubit(0); }); - EXPECT_TRUE(allowsOp(cx.getOperation(), *spec)); + 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(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, q0, q1, [&](Value target) { return HOp::create(builder, loc, target).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}); @@ -874,13 +830,13 @@ TEST_F(NativeGatesetMlirTest, AllowsOpMatchesGateset) { CtrlOp::create(builder, loc, ValueRange{c0, c1}, target, [&](Value t) { return XOp::create(builder, loc, t).getOutputQubit(0); }); - EXPECT_FALSE(allowsOp(ccx.getOperation(), *spec)); + 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(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..14c76e7e71 --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp @@ -0,0 +1,773 @@ +/* + * 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 +#include +#include + +using namespace mlir; +using namespace mlir::qco; + +using ProgramFn = SmallVector (*)(mlir::qc::QCProgramBuilder&); + +static SmallVector measureAndReturn(mlir::qc::QCProgramBuilder& b, + ValueRange qubits) { + return llvm::to_vector( + llvm::map_range(qubits, [&](Value q) { return b.measure(q); })); +} + +// --- 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 SmallVector swapTwoQ(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.swap(q0, q1); + return measureAndReturn(b, {q0, q1}); +} + +/// Rich single-qubit variety on both wires, followed by a two-qubit entangler. +static SmallVector 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); + return measureAndReturn(b, {q0, q1}); +} + +/// Long single-qubit run on one wire, then an entangler (Euler-run fusion). +static SmallVector 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); + return measureAndReturn(b, {q0, q1}); +} + +/// Zero-angle rotations that must canonicalize away before the entangler. +static SmallVector 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); + return measureAndReturn(b, {q0, q1}); +} + +/// Single-qubit gates surrounding an entangler on both sides. +static SmallVector 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); + return measureAndReturn(b, {q0, q1}); +} + +/// Three-qubit program with chained entanglers on overlapping pairs. +static SmallVector 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); + return measureAndReturn(b, {q0, q1, q2}); +} + +/// Single-qubit gates wrapped in an inverse modifier (no entangler). +static SmallVector inverseTwoX(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + b.inv(q0, [&](Value qubit) { + b.x(qubit); + b.x(qubit); + }); + return measureAndReturn(b, {q0}); +} + +/// A controlled two-gate body that must be synthesized as a two-qubit unitary. +static SmallVector controlledXH(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.ctrl(q0, q1, [&](Value target) { + b.x(target); + b.h(target); + }); + return measureAndReturn(b, {q0, q1}); +} + +// --- 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 SmallVector fusionCxCx(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.cx(q0, q1); + b.cx(q0, q1); + return measureAndReturn(b, {q0, q1}); +} + +static SmallVector +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); + return measureAndReturn(b, {q0, q1}); +} + +static SmallVector 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); + return measureAndReturn(b, {q0, q1, q2}); +} + +static SmallVector +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); + return measureAndReturn(b, {q0, q1, q2}); +} + +static SmallVector 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); + return measureAndReturn(b, {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 SmallVector +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); + return measureAndReturn(b, {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 SmallVector +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); + return measureAndReturn(b, {q0, q1, q2}); +} + +static SmallVector 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); + return measureAndReturn(b, {q0, q1}); +} + +static SmallVector +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); + return measureAndReturn(b, {q0, q1}); +} + +static SmallVector +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); + return measureAndReturn(b, {q0, q1}); +} + +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 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 expectSynthesisFailure(Value (*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; +}; + +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); +} diff --git a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp index 4e5e1e4857..5191099c62 100644 --- a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp +++ b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp @@ -846,6 +846,14 @@ TEST(SymmetricEigensolver, HandlesDegenerateSpectrum) { EXPECT_TRUE((v.transpose() * v).isIdentity()); } +TEST(GlobalPhaseEquivalence, IsEquivalentUpToGlobalPhase) { + const Matrix2x2 pauliX = Matrix2x2::fromElements(0, 1, 1, 0); + const Complex phase{0.0, 1.0}; + EXPECT_TRUE(isEquivalentUpToGlobalPhase(pauliX, phase * pauliX)); + EXPECT_TRUE(isEquivalentUpToGlobalPhase(pauliX, pauliX)); + EXPECT_FALSE(isEquivalentUpToGlobalPhase(pauliX, Matrix2x2::identity())); +} + TEST(Eigensolver, EmptyMatrixReturnsNullopt) { EXPECT_FALSE(DynamicMatrix{}.eigenDecomposition().has_value()); } diff --git a/python/mqt/core/mlir.pyi b/python/mqt/core/mlir.pyi index 25a795438d..0c8fe6104c 100644 --- a/python/mqt/core/mlir.pyi +++ b/python/mqt/core/mlir.pyi @@ -138,6 +138,9 @@ 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 comma-separated native gate menu.""" + def unroll_quantum_loops(self, *, unroll_factor: int = -1) -> None: """Unroll quantum loops, optionally using a maximum unroll factor."""