From cd9a2a8735a2bd00732a72162dfcb84ed8601294 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 14 Sep 2026 16:31:41 +0200 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=90=9B=20Preserve=20symbolic=20Euler?= =?UTF-8?q?=20chains=20in=20target=20synthesis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 *AI text below* 🤖 Reuse direct Euler angles and synthesize into the target basis after placement. This avoids inverse trigonometry and cancelling U phase corrections for symbolic circSU2 circuits while retaining classical read sharing and the existing U-target optimizer. Assisted-by: GPT-5 via Codex --- mlir/include/mqt/Compiler/TargetCompilation.h | 2 +- .../mqt/Dialect/QCO/Transforms/Passes.h | 10 ++-- .../mqt/Dialect/QCO/Transforms/Passes.td | 8 ++- mlir/lib/Compiler/TargetCompilation.cpp | 20 +++++-- .../FuseSingleQubitUnitaryRuns.cpp | 33 +++++++++++ .../MergeSingleQubitRotationGates.cpp | 48 +++++++++++++++- .../test_qco_merge_single_qubit_rotation.cpp | 57 +++++++++++++++++++ test/python/test_mlir.py | 40 +++++++++++++ 8 files changed, 205 insertions(+), 13 deletions(-) diff --git a/mlir/include/mqt/Compiler/TargetCompilation.h b/mlir/include/mqt/Compiler/TargetCompilation.h index c0128ec61e..805ebc67d7 100644 --- a/mlir/include/mqt/Compiler/TargetCompilation.h +++ b/mlir/include/mqt/Compiler/TargetCompilation.h @@ -18,7 +18,7 @@ class OpPassManager; /// Populate the canonical compiler-target pipeline. /// /// Inlines reusable functions, decomposes supported multi-controlled gates, -/// performs target-independent optimization, maps to the target topology, +/// maps to the target topology, fuses blocks in the target basis, /// synthesizes native operations, performs a final local cleanup, and verifies /// target conformance. The context that runs this low-level pipeline must /// register inliner extensions for its callable dialects. diff --git a/mlir/include/mqt/Dialect/QCO/Transforms/Passes.h b/mlir/include/mqt/Dialect/QCO/Transforms/Passes.h index 378bf4bfcc..987f5c55ce 100644 --- a/mlir/include/mqt/Dialect/QCO/Transforms/Passes.h +++ b/mlir/include/mqt/Dialect/QCO/Transforms/Passes.h @@ -10,6 +10,8 @@ #pragma once +#include "mqt/Compiler/Target.h" + #include "mlir/Interfaces/FunctionInterfaces.h" #include "mlir/Pass/Pass.h" #include "mlir/Pass/PassRegistry.h" @@ -19,15 +21,15 @@ #include #include -namespace mlir { -class CompilerTarget; -} // namespace mlir - namespace mlir::qco { #define GEN_PASS_DECL #include "mqt/Dialect/QCO/Transforms/Passes.h.inc" // IWYU pragma: export +/// Fuse single-qubit runs directly in the selected compiler-target basis. +[[nodiscard]] std::unique_ptr +createFuseSingleQubitUnitaryRuns(CompilerTarget::SingleQubitBasis basis); + //===----------------------------------------------------------------------===// // Registration //===----------------------------------------------------------------------===// diff --git a/mlir/include/mqt/Dialect/QCO/Transforms/Passes.td b/mlir/include/mqt/Dialect/QCO/Transforms/Passes.td index 70afdb8386..45d6885e86 100644 --- a/mlir/include/mqt/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mqt/Dialect/QCO/Transforms/Passes.td @@ -45,8 +45,10 @@ def MergeSingleQubitRotationGates When every angle in the chain is a compile-time constant, the same algorithm runs on host scalars and emits constant `U` / `gphase` results directly. - Otherwise it emits `arith` / `math` operations that compute the merged - parameters at run time. + Symbolic RZ-RX-RZ and RZ-RY-RZ chains reuse their Euler angles directly; + either outer RZ may be absent. Their angles remain unwrapped, without + inverse trigonometry or conditional expressions. Other dynamic chains emit + `arith` / `math` operations that compute the merged parameters at run time. The emitted `UOp` is defined by $U = \exp [i (\phi + \lambda) / 2] R_z (\phi) R_y (\theta) R_z (\lambda)$. Normalizing either extracted Z angle into $[-\pi, \pi)$ by $\pm 2\pi$ flips @@ -56,7 +58,7 @@ def MergeSingleQubitRotationGates restores exact matrix equality (including global phase). This applies even to chains of purely $\mathrm{SU}(2)$ gates (`rx`, `ry`, `rz`, `r`). On the host (static) path, a near-zero correction may be omitted; on the - SSA (dynamic) path a `GPhaseOp` is always emitted. After greedy merging, + SSA (dynamic) path a `GPhaseOp` carries any remaining correction. After greedy merging, the implementation directly invokes the shared global-phase normalization utility to combine, normalize, and remove trivial corrections in their respective scopes. diff --git a/mlir/lib/Compiler/TargetCompilation.cpp b/mlir/lib/Compiler/TargetCompilation.cpp index eefc12d7b9..106ddc88e6 100644 --- a/mlir/lib/Compiler/TargetCompilation.cpp +++ b/mlir/lib/Compiler/TargetCompilation.cpp @@ -74,13 +74,18 @@ class PrepareTargetCompilationPass } /* namespace */ -static void populatePostPlacementPipeline(OpPassManager& pm) { +static void populatePostPlacementPipeline(OpPassManager& pm, + const CompilerTarget& target) { /// Placement consumes allocations; native synthesis normalizes phases. pm.addPass(createCanonicalizerPass( GreedyRewriteConfig{}.setMaxIterations(GreedyRewriteConfig::kNoLimit))); /// Reuse unchanged classical reads before native synthesis splits their uses. pm.addPass(createCSEPass()); pm.addPass(createRemoveDeadValuesPass()); + if (const auto basis = target.synthesisBasis(); + basis && basis->singleQubit != CompilerTarget::SingleQubitBasis::U) { + pm.addPass(qco::createFuseSingleQubitUnitaryRuns(basis->singleQubit)); + } pm.addPass(qco::createTargetNativeSynthesis()); pm.addPass(createCSEPass()); pm.addPass(qco::createVerifyTargetConformance()); @@ -105,7 +110,14 @@ void populateTargetCompilationPipeline(OpPassManager& pm, pm.addPass(qco::createLegalizeControlFlow()); pm.addPass(qco::createDecomposeMultiControlled(target)); pm.addPass(qco::createFuseTwoQubitGates(target)); - populateDefaultQCOOptimizationPipeline(pm); + // Non-U targets fuse directly in their basis after placement, avoiding an + // intermediate U representation and its symbolic phase correction. + if (const auto basis = target.synthesisBasis(); + !basis || basis->singleQubit == CompilerTarget::SingleQubitBasis::U) { + // Retain the U optimizer's treatment of isolated gates on U-based and + // unrestricted targets. + populateDefaultQCOOptimizationPipeline(pm); + } switch (target.connectivityKind()) { case CompilerTarget::Connectivity::Kind::Explicit: pm.addPass(qco::createMappingPass(qco::MappingPassOptions{})); @@ -114,7 +126,7 @@ void populateTargetCompilationPipeline(OpPassManager& pm, pm.addPass(qco::createPlacementPass(target)); break; } - populatePostPlacementPipeline(pm); + populatePostPlacementPipeline(pm, target); } void populateTargetSynthesisPipeline(OpPassManager& pm, @@ -128,7 +140,7 @@ void populateTargetSynthesisPipeline(OpPassManager& pm, pm.addPass(qco::createDecomposeMultiControlled(target)); pm.addPass(qco::createFuseTwoQubitGates(target)); pm.addPass(qco::createPlacementPass(target)); - populatePostPlacementPipeline(pm); + populatePostPlacementPipeline(pm, target); } } // namespace mlir diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp index 70c96b4362..9451ad17b6 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp @@ -27,6 +27,7 @@ #include "mlir/Transforms/GreedyPatternRewriteDriver.h" #include +#include #include #include @@ -207,6 +208,38 @@ struct FuseSingleQubitUnitaryRunsPass final } // namespace +std::unique_ptr +createFuseSingleQubitUnitaryRuns(CompilerTarget::SingleQubitBasis basis) { + // Populate the existing option so textual pipelines and reproducers retain + // the basis selected by target compilation. + FuseSingleQubitUnitaryRunsOptions options; + using Basis = CompilerTarget::SingleQubitBasis; + switch (basis) { + case Basis::U: + options.basis = "u"; + break; + case Basis::ZSXX: + options.basis = "zsxx"; + break; + case Basis::R: + options.basis = "r"; + break; + case Basis::XZX: + options.basis = "xzx"; + break; + case Basis::XYX: + options.basis = "xyx"; + break; + case Basis::ZYZ: + options.basis = "zyz"; + break; + case Basis::ZXZ: + options.basis = "zxz"; + break; + } + return createFuseSingleQubitUnitaryRuns(options); +} + } // namespace mlir::qco namespace mlir::qco::decomposition { diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp index 077e3aefc7..b48eba6551 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp @@ -1066,6 +1066,49 @@ struct MergeSingleQubitRotationGatesPattern final return success(); } + /// Reuse the Euler angles of RZ-RX-RZ and RZ-RY-RZ chains, with either + /// outer RZ optional. Do not add independent, unbounded dynamic angles. + static LogicalResult + tryMergeDirectChain(MutableArrayRef chain, + RewriterBase& rewriter, + decomposition::SingleQubitBasis basis) { + if (basis == decomposition::SingleQubitBasis::XZX || + basis == decomposition::SingleQubitBasis::XYX || + basis == decomposition::SingleQubitBasis::R) { + return failure(); + } + + const size_t middle = isa(chain.front().getOperation()) ? 1 : 0; + if (chain.size() <= middle || chain.size() > middle + 2 || + !isa(chain[middle].getOperation()) || + (chain.size() == middle + 2 && + !isa(chain.back().getOperation()))) { + return failure(); + } + + // Check the complete run before creating or replacing any operations. + const Location loc = chain.front()->getLoc(); + const auto consts = makeConsts(rewriter, loc); + auto angles = directZYZAnglesFromGate(chain[middle], rewriter, consts); + const auto angle = [&](UnitaryOpInterface op) { + return Val{op.getParameter(0), &rewriter, loc}; + }; + if (middle == 1) { + angles.lambda = sumAngles(angles.lambda, angle(chain.front())); + } + if (chain.size() == middle + 2) { + angles.phi = sumAngles(angles.phi, angle(chain.back())); + } + + for (auto op : llvm::drop_begin(chain)) { + rewriter.replaceOp(op, op.getInputQubit(0)); + } + Value qubit = emitRuntimeEulerAngles( + rewriter, loc, chain.front().getInputQubit(0), angles, basis, consts); + rewriter.replaceOp(chain.front(), qubit); + return success(); + } + // Merges a dynamic or mixed-angle chain through `Val` SSA. // // Fusion mode emits the requested basis directly. Regular merge mode emits @@ -1079,6 +1122,10 @@ struct MergeSingleQubitRotationGatesPattern final RewriterBase& rewriter, std::optional fusionBasis = std::nullopt) { + const auto basis = fusionBasis.value_or(decomposition::SingleQubitBasis::U); + if (succeeded(tryMergeDirectChain(chain, rewriter, basis))) { + return success(); + } const Location loc = chain.front()->getLoc(); const auto consts = makeConsts(rewriter, loc); @@ -1101,7 +1148,6 @@ struct MergeSingleQubitRotationGatesPattern final rewriter.replaceOp(chainOp, chainOp.getInputQubit(0)); } - const auto basis = fusionBasis.value_or(decomposition::SingleQubitBasis::U); const bool transformed = basis == decomposition::SingleQubitBasis::XZX || basis == decomposition::SingleQubitBasis::XYX || basis == decomposition::SingleQubitBasis::R; diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_merge_single_qubit_rotation.cpp b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_merge_single_qubit_rotation.cpp index 0966f9ca1a..608fcad87c 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_merge_single_qubit_rotation.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_merge_single_qubit_rotation.cpp @@ -22,6 +22,7 @@ #include "mlir/Dialect/Arith/IR/Arith.h" #include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/Dialect/Math/IR/Math.h" #include "mlir/Dialect/SCF/IR/SCF.h" #include "mlir/IR/Builders.h" #include "mlir/IR/BuiltinAttributes.h" @@ -1003,6 +1004,62 @@ TEST_F(MergeSingleQubitRotationGatesTest, EXPECT_NEAR(*phase, mlir::mqt::normalizeAngle(*phase), 1e-8); } +TEST_F(MergeSingleQubitRotationGatesTest, + mergesSymbolicEulerChainsWithoutTrigonometry) { + for (const bool useX : {false, true}) { + SCOPED_TRACE(useX); + module = QCOProgramBuilder::build(&context, [&](auto& b) { + auto [control, target] = + b.ctrl(b.staticQubit(0), b.staticQubit(1), [&](Value qubit) { + qubit = b.rz(0.1, qubit); + qubit = useX ? b.rx(0.2, qubit) : b.ry(0.2, qubit); + return b.rz(0.4, qubit); + }); + return SmallVector{control, target}; + }); + auto funcOp = module->lookupSymbol("main"); + module->walk([&](UnitaryOpInterface op) { + if (isa(op.getOperation())) { + const auto index = funcOp.getNumArguments(); + funcOp.insertArgument(index, Float64Type::get(&context), {}, + funcOp.getLoc()); + op.getParameter(0).replaceAllUsesWith(funcOp.getArgument(index)); + } + }); + ASSERT_EQ(funcOp.getNumArguments(), 3U); + ASSERT_TRUE(succeeded(verify(*module))); + ASSERT_TRUE(succeeded(verifyLinearity(*module))); + OwningOpRef original = module->clone(); + ASSERT_TRUE(succeeded(runMergePass(*module))); + ASSERT_TRUE(succeeded(verify(*module))); + ASSERT_TRUE(succeeded(verifyLinearity(*module))); + EXPECT_EQ(countOps(), 1); + EXPECT_EQ(countOps(), 0); + EXPECT_EQ(countOps(), 0); + EXPECT_EQ(countOps(), 0); + EXPECT_EQ(countOps(), 0); + + for (const auto angles : { + std::array{0.0, 0.0, 0.0}, + std::array{PI, PI, PI}, + std::array{2 * PI, -2 * PI, 2 * PI}, + std::array{-3 * PI, 0.37, 4 * PI}, + }) { + SCOPED_TRACE(testing::PrintToString(angles)); + OwningOpRef before = original->clone(); + OwningOpRef after = module->clone(); + bindLeadingArgs(before->lookupSymbol("main"), angles); + bindLeadingArgs(after->lookupSymbol("main"), angles); + PassManager pm(&context); + pm.addPass(createCanonicalizerPass()); + ASSERT_TRUE(succeeded(pm.run(*after))); + ASSERT_TRUE(succeeded(verify(*after))); + ASSERT_TRUE(succeeded(verifyLinearity(*after))); + ::mqt::test::expectFullUnitaryEqual(*before, *after, 2); + } + } +} + TEST_F(MergeSingleQubitRotationGatesTest, mergeDynamicAngleRotationsUsesSsaPath) { // Pure-Z chain with unfoldable angle SSA forces Val merge (not the diff --git a/test/python/test_mlir.py b/test/python/test_mlir.py index ae519ee2a8..af9edf50f0 100644 --- a/test/python/test_mlir.py +++ b/test/python/test_mlir.py @@ -544,6 +544,46 @@ def test_target_compiles_single_qubit_gates_without_entangler(num_sites: int) -> assert np.allclose(Operator(result).data, Operator(source).data) +@requires_qiskit_translation +@pytest.mark.parametrize(("num_qubits", "reps"), [(2, 1), (100, 3)]) +def test_symbolic_su2_compiles_and_binds_after_export(num_qubits: int, reps: int) -> None: + """Compile symbolic SU2 circuits with bindable parameters and exact phase.""" + source = library.efficient_su2(num_qubits, reps=reps, entanglement="circular") + source.global_phase = source.parameters[0] / 5 - 0.3 + program = QCProgram.from_qiskit(source).to_qco() + target = CompilerTarget( + num_qubits, + connectivity=CompilerTarget.Connectivity.all_to_all(), + native_operations=CompilerTarget.NativeOperations([ + CompilerTarget.OperationCapability("sx", 1, 0), + CompilerTarget.OperationCapability("x", 1, 0), + CompilerTarget.OperationCapability("rz", 1, 1), + CompilerTarget.OperationCapability("cz", 2, 0), + CompilerTarget.OperationCapability("gphase", 0, 1), + ]), + ) + program.compile_for_target(_test_target_environment(target)) + result = program.to_qiskit(target=target) + assert set(result.count_ops()) <= {"sx", "x", "rz", "cz"} + assert result.parameters == source.parameters + for angles in ( + np.zeros(source.num_parameters), + np.full(source.num_parameters, np.pi), + np.full(source.num_parameters, 2 * np.pi), + np.linspace(-3 * np.pi, 3 * np.pi, source.num_parameters), + ): + values = dict(zip(source.parameters, angles, strict=True)) + bound = result.assign_parameters(values) + assert bound.num_parameters == 0 + if num_qubits == 2: + assert np.allclose( + Operator(bound).data, + Operator(source.assign_parameters(values)).data, + atol=1e-10, + rtol=0, + ) + + @requires_qiskit_translation def test_target_compilation_exports_canonical_physical_qiskit_circuit() -> None: """Export a mapped program with the complete compiler target.""" From 1a9a1da5dae6ba186d5d90ed4384e7920164403e Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 14 Sep 2026 21:43:14 +0200 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=90=9B=20Support=20symbolic=20X-outer?= =?UTF-8?q?=20Euler=20chains?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 *AI text below* 🤖 Reuse the existing Euler emitters for symbolic XZX and XYX chains in compatible XZX, XYX, and R bases, including shortened chains. Preserve phase and gate-count bounds without inverse trigonometry. Cover controlled matrix equality and late-bound Qiskit export through both target pipelines. Initialize the test fixture in its constructor. Assisted-by: GPT-5 via Codex --- .../mqt/Dialect/QCO/Transforms/Passes.td | 6 ++ .../MergeSingleQubitRotationGates.cpp | 42 ++++++--- .../test_qco_merge_single_qubit_rotation.cpp | 94 ++++++++++++++++++- test/python/test_mlir.py | 36 +++++++ 4 files changed, 163 insertions(+), 15 deletions(-) diff --git a/mlir/include/mqt/Dialect/QCO/Transforms/Passes.td b/mlir/include/mqt/Dialect/QCO/Transforms/Passes.td index 45d6885e86..1527bdef89 100644 --- a/mlir/include/mqt/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mqt/Dialect/QCO/Transforms/Passes.td @@ -88,6 +88,12 @@ def FuseSingleQubitUnitaryRuns The pass also composes supported named gates with dynamic `f64` parameters and emits conservative runtime sequences in the requested basis. Dynamic `pow` and arbitrary dynamic unitaries remain unchanged. + + Symbolic ZXZ/ZYZ chains reuse their angles directly in the `u`, `zyz`, + `zxz`, and `zsxx` bases; XZX/XYX chains do so in `xzx`, `xyx`, and `r`. + Either outer rotation may be absent. These conversions need no inverse + trigonometry or conditional expressions. Other dynamic chains use runtime + Euler-angle extraction. }]; let options = [Option< "basis", "basis", "std::string", "\"zyz\"", diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp index b48eba6551..c6a98ee97c 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp @@ -1066,33 +1066,51 @@ struct MergeSingleQubitRotationGatesPattern final return success(); } - /// Reuse the Euler angles of RZ-RX-RZ and RZ-RY-RZ chains, with either - /// outer RZ optional. Do not add independent, unbounded dynamic angles. + /// Reuse Euler angles when the chain and output share their outer axis. + /// Either outer rotation may be absent. Do not add independent, unbounded + /// dynamic angles. static LogicalResult tryMergeDirectChain(MutableArrayRef chain, RewriterBase& rewriter, decomposition::SingleQubitBasis basis) { - if (basis == decomposition::SingleQubitBasis::XZX || - basis == decomposition::SingleQubitBasis::XYX || - basis == decomposition::SingleQubitBasis::R) { - return failure(); - } + const bool outerX = basis == decomposition::SingleQubitBasis::XZX || + basis == decomposition::SingleQubitBasis::XYX || + basis == decomposition::SingleQubitBasis::R; + const auto isOuter = [outerX](UnitaryOpInterface op) { + return outerX ? isa(op.getOperation()) + : isa(op.getOperation()); + }; - const size_t middle = isa(chain.front().getOperation()) ? 1 : 0; + const size_t middle = chain.size() > 1 && isOuter(chain.front()) ? 1 : 0; if (chain.size() <= middle || chain.size() > middle + 2 || - !isa(chain[middle].getOperation()) || - (chain.size() == middle + 2 && - !isa(chain.back().getOperation()))) { + !isa(chain[middle].getOperation()) || + (chain.size() > 1 && isOuter(chain[middle])) || + (chain.size() == middle + 2 && !isOuter(chain.back()))) { return failure(); } // Check the complete run before creating or replacing any operations. const Location loc = chain.front()->getLoc(); const auto consts = makeConsts(rewriter, loc); - auto angles = directZYZAnglesFromGate(chain[middle], rewriter, consts); const auto angle = [&](UnitaryOpInterface op) { return Val{op.getParameter(0), &rewriter, loc}; }; + RuntimeEulerAngles angles{.theta = angle(chain[middle]), + .phi = consts.zero, + .lambda = consts.zero, + .phase = consts.zero}; + if (!outerX) { + angles = directZYZAnglesFromGate(chain[middle], rewriter, consts); + } else if (isOuter(chain[middle])) { + angles.lambda = angles.theta; + angles.theta = consts.zero; + } else if (const bool middleZ = isa(chain[middle].getOperation()); + middleZ != (basis == decomposition::SingleQubitBasis::XZX)) { + // RX conjugation exchanges Y and Z, with opposite quarter-turns. + const auto halfPi = consts.pi / consts.two; + angles.phi = middleZ ? halfPi : -halfPi; + angles.lambda = -angles.phi; + } if (middle == 1) { angles.lambda = sumAngles(angles.lambda, angle(chain.front())); } diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_merge_single_qubit_rotation.cpp b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_merge_single_qubit_rotation.cpp index 608fcad87c..db1e74a71a 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_merge_single_qubit_rotation.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_merge_single_qubit_rotation.cpp @@ -14,6 +14,7 @@ #include "mqt/Dialect/QCO/IR/QCODialect.h" #include "mqt/Dialect/QCO/IR/QCOOps.h" #include "mqt/Dialect/QCO/QCOUtils.h" +#include "mqt/Dialect/QCO/Transforms/Decomposition/Euler.h" #include "mqt/Dialect/QCO/Transforms/Passes.h" #include "ExactUnitaryTest.h" @@ -70,9 +71,7 @@ class MergeSingleQubitRotationGatesTest : public ::testing::Test { SmallVector angles; }; - MergeSingleQubitRotationGatesTest() : builder(&context) {} - - void SetUp() override { + MergeSingleQubitRotationGatesTest() : builder(&context) { context.loadDialect(); context.loadDialect(); context.loadDialect(); @@ -1060,6 +1059,95 @@ TEST_F(MergeSingleQubitRotationGatesTest, } } +TEST_F(MergeSingleQubitRotationGatesTest, + fusesSymbolicEulerChainsDirectlyInCompatibleBases) { + for (const auto* basisName : {"u", "zyz", "zxz", "zsxx", "xzx", "xyx", "r"}) { + SCOPED_TRACE(basisName); + const auto basis = *decomposition::parseSingleQubitBasis(basisName); + const bool outerX = basis == decomposition::SingleQubitBasis::XZX || + basis == decomposition::SingleQubitBasis::XYX || + basis == decomposition::SingleQubitBasis::R; + for (const bool useY : {false, true}) { + SCOPED_TRACE(useY); + for (const unsigned outerMask : {0U, 1U, 2U, 3U}) { + SCOPED_TRACE(outerMask); + module = QCOProgramBuilder::build(&context, [&](auto& b) { + auto [control, target] = + b.ctrl(b.staticQubit(0), b.staticQubit(1), [&](Value qubit) { + if ((outerMask & 1U) != 0) { + qubit = outerX ? b.rx(0.1, qubit) : b.rz(0.1, qubit); + } + qubit = useY ? b.ry(0.2, qubit) + : outerX ? b.rz(0.2, qubit) + : b.rx(0.2, qubit); + if ((outerMask & 2U) != 0) { + qubit = outerX ? b.rx(0.4, qubit) : b.rz(0.4, qubit); + } + return qubit; + }); + return SmallVector{control, target}; + }); + auto funcOp = module->lookupSymbol("main"); + module->walk([&](UnitaryOpInterface op) { + if (isa(op.getOperation())) { + const auto index = funcOp.getNumArguments(); + funcOp.insertArgument(index, Float64Type::get(&context), {}, + funcOp.getLoc()); + op.getParameter(0).replaceAllUsesWith(funcOp.getArgument(index)); + } + }); + ASSERT_TRUE(succeeded(verify(*module))); + ASSERT_TRUE(succeeded(verifyLinearity(*module))); + OwningOpRef original = module->clone(); + FuseSingleQubitUnitaryRunsOptions options; + options.basis = basisName; + PassManager fusion(&context); + fusion.addPass(createFuseSingleQubitUnitaryRuns(options)); + ASSERT_TRUE(succeeded(fusion.run(*module))); + ASSERT_TRUE(succeeded(verify(*module))); + ASSERT_TRUE(succeeded(verifyLinearity(*module))); + EXPECT_EQ(countOps(), 0); + EXPECT_EQ(countOps(), 0); + EXPECT_EQ(countOps(), 0); + EXPECT_EQ(countOps(), 0); + unsigned gateCount = 0; + module->walk([&](UnitaryOpInterface op) { + // Phase normalization may lift a P gate onto the control wire. + if (op.isSingleQubit() && op->getParentOfType()) { + ++gateCount; + EXPECT_TRUE(decomposition::isSingleQubitBasisGate(op, basis)); + } + }); + EXPECT_GT(gateCount, 0U); + EXPECT_LE(gateCount, basis == decomposition::SingleQubitBasis::U ? 1U + : basis == decomposition::SingleQubitBasis::ZSXX + ? 5U + : 3U); + + for (const auto angles : { + std::array{0.0, 0.0, 0.0}, + std::array{PI, PI, PI}, + std::array{2 * PI, -2 * PI, 2 * PI}, + std::array{-3 * PI, 0.37, 4 * PI}, + }) { + SCOPED_TRACE(testing::PrintToString(angles)); + OwningOpRef before = original->clone(); + OwningOpRef after = module->clone(); + auto values = ArrayRef(angles).take_front(funcOp.getNumArguments()); + bindLeadingArgs(before->lookupSymbol("main"), values); + bindLeadingArgs(after->lookupSymbol("main"), values); + PassManager canonicalizer(&context); + canonicalizer.addPass(createCanonicalizerPass()); + ASSERT_TRUE(succeeded(canonicalizer.run(*after))); + ASSERT_TRUE(succeeded(verify(*after))); + ASSERT_TRUE(succeeded(verifyLinearity(*after))); + ::mqt::test::expectFullUnitaryEqual(*before, *after, 2); + } + } + } + } +} + TEST_F(MergeSingleQubitRotationGatesTest, mergeDynamicAngleRotationsUsesSsaPath) { // Pure-Z chain with unfoldable angle SSA forces Val merge (not the diff --git a/test/python/test_mlir.py b/test/python/test_mlir.py index af9edf50f0..9c3220e515 100644 --- a/test/python/test_mlir.py +++ b/test/python/test_mlir.py @@ -584,6 +584,42 @@ def test_symbolic_su2_compiles_and_binds_after_export(num_qubits: int, reps: int ) +@requires_qiskit_translation +@pytest.mark.parametrize( + ("native_gates", "middle_gate"), + [("rx rz", "ry"), ("rx ry", "rz"), ("r", "rz")], +) +@pytest.mark.parametrize("method", ["compile_for_target", "synthesize_for_target"]) +def test_symbolic_x_euler_chain_exports_for_target(native_gates: str, middle_gate: str, method: str) -> None: + """Keep X-outer Euler chains bindable through both target pipelines.""" + angles = qiskit.circuit.ParameterVector("theta", 3) + source = QuantumCircuit(1) + source.rx(angles[0], 0) + getattr(source, middle_gate)(angles[1], 0) + source.rx(angles[2], 0) + source.global_phase = angles[0] / 5 - 0.3 + target = CompilerTarget( + 1, + connectivity=CompilerTarget.Connectivity.all_to_all(), + native_operations=CompilerTarget.NativeOperations([ + *(CompilerTarget.OperationCapability(gate, 1, 2 if gate == "r" else 1) for gate in native_gates.split()), + CompilerTarget.OperationCapability("gphase", 0, 1), + ]), + ) + program = QCProgram.from_qiskit(source).to_qco() + getattr(program, method)(_test_target_environment(target)) + result = program.to_qiskit(target=target) + assert result.parameters == source.parameters + assert set(result.count_ops()) <= set(native_gates.split()) + values = dict(zip(angles, [-3 * np.pi, 0.37, 4 * np.pi], strict=True)) + assert np.allclose( + Operator(result.assign_parameters(values)).data, + Operator(source.assign_parameters(values)).data, + atol=1e-10, + rtol=0, + ) + + @requires_qiskit_translation def test_target_compilation_exports_canonical_physical_qiskit_circuit() -> None: """Export a mapped program with the complete compiler target.""" From a54dbdb3966b411ccc7f575cd8644ce3bc29e020 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 14 Sep 2026 21:55:33 +0200 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=A7=B9=20Fix=20Euler=20synthesis=20li?= =?UTF-8?q?nt=20and=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 *AI text below* 🤖 Use designated fields for the symbolic angle initializer. Exercise the compiler-target basis overload in the existing seven-basis regression, covering the previously missed U and ZXZ switch cases. Assisted-by: GPT-5 via Codex --- .../Optimizations/MergeSingleQubitRotationGates.cpp | 3 ++- .../Optimizations/test_qco_merge_single_qubit_rotation.cpp | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp index c6a98ee97c..fd1e69a4e4 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp @@ -1093,7 +1093,8 @@ struct MergeSingleQubitRotationGatesPattern final const Location loc = chain.front()->getLoc(); const auto consts = makeConsts(rewriter, loc); const auto angle = [&](UnitaryOpInterface op) { - return Val{op.getParameter(0), &rewriter, loc}; + return Val{ + .v = op.getParameter(0), .rewriter = &rewriter, .loc = loc}; }; RuntimeEulerAngles angles{.theta = angle(chain[middle]), .phi = consts.zero, diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_merge_single_qubit_rotation.cpp b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_merge_single_qubit_rotation.cpp index db1e74a71a..af90d92134 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_merge_single_qubit_rotation.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_merge_single_qubit_rotation.cpp @@ -1099,10 +1099,8 @@ TEST_F(MergeSingleQubitRotationGatesTest, ASSERT_TRUE(succeeded(verify(*module))); ASSERT_TRUE(succeeded(verifyLinearity(*module))); OwningOpRef original = module->clone(); - FuseSingleQubitUnitaryRunsOptions options; - options.basis = basisName; PassManager fusion(&context); - fusion.addPass(createFuseSingleQubitUnitaryRuns(options)); + fusion.addPass(createFuseSingleQubitUnitaryRuns(basis)); ASSERT_TRUE(succeeded(fusion.run(*module))); ASSERT_TRUE(succeeded(verify(*module))); ASSERT_TRUE(succeeded(verifyLinearity(*module))); From a24279785a70083b4e1f76e9704d0ce3da5f325d Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 14 Sep 2026 22:32:34 +0200 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=A7=B9=20Fix=20trailing=20commas=20an?= =?UTF-8?q?d=20isolated=20rotation=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 *AI text below* 🤖 Add the trailing commas required by clang-tidy. Extend the existing symbolic Euler regression with isolated rotations to exercise the RX-to-R conversion. Assisted-by: GPT-5 via Codex --- .../MergeSingleQubitRotationGates.cpp | 15 ++++++++++----- .../test_qco_merge_single_qubit_rotation.cpp | 14 +++++++++----- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp index fd1e69a4e4..5cd729e746 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Optimizations/MergeSingleQubitRotationGates.cpp @@ -1094,12 +1094,17 @@ struct MergeSingleQubitRotationGatesPattern final const auto consts = makeConsts(rewriter, loc); const auto angle = [&](UnitaryOpInterface op) { return Val{ - .v = op.getParameter(0), .rewriter = &rewriter, .loc = loc}; + .v = op.getParameter(0), + .rewriter = &rewriter, + .loc = loc, + }; + }; + RuntimeEulerAngles angles{ + .theta = angle(chain[middle]), + .phi = consts.zero, + .lambda = consts.zero, + .phase = consts.zero, }; - RuntimeEulerAngles angles{.theta = angle(chain[middle]), - .phi = consts.zero, - .lambda = consts.zero, - .phase = consts.zero}; if (!outerX) { angles = directZYZAnglesFromGate(chain[middle], rewriter, consts); } else if (isOuter(chain[middle])) { diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_merge_single_qubit_rotation.cpp b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_merge_single_qubit_rotation.cpp index af90d92134..83c3eb63f3 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_merge_single_qubit_rotation.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_merge_single_qubit_rotation.cpp @@ -1067,9 +1067,13 @@ TEST_F(MergeSingleQubitRotationGatesTest, const bool outerX = basis == decomposition::SingleQubitBasis::XZX || basis == decomposition::SingleQubitBasis::XYX || basis == decomposition::SingleQubitBasis::R; - for (const bool useY : {false, true}) { - SCOPED_TRACE(useY); + for (const auto middleGate : {GateType::RX, GateType::RY, GateType::RZ}) { + SCOPED_TRACE(static_cast(middleGate)); for (const unsigned outerMask : {0U, 1U, 2U, 3U}) { + if (middleGate == (outerX ? GateType::RX : GateType::RZ) && + outerMask != 0) { + continue; + } SCOPED_TRACE(outerMask); module = QCOProgramBuilder::build(&context, [&](auto& b) { auto [control, target] = @@ -1077,9 +1081,9 @@ TEST_F(MergeSingleQubitRotationGatesTest, if ((outerMask & 1U) != 0) { qubit = outerX ? b.rx(0.1, qubit) : b.rz(0.1, qubit); } - qubit = useY ? b.ry(0.2, qubit) - : outerX ? b.rz(0.2, qubit) - : b.rx(0.2, qubit); + qubit = middleGate == GateType::RX ? b.rx(0.2, qubit) + : middleGate == GateType::RY ? b.ry(0.2, qubit) + : b.rz(0.2, qubit); if ((outerMask & 2U) != 0) { qubit = outerX ? b.rx(0.4, qubit) : b.rz(0.4, qubit); }