From bec1fdfd6f4bb1d796bc34e002787b34394cf7c8 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 17 Jun 2026 16:41:56 +0200 Subject: [PATCH 01/13] =?UTF-8?q?=E2=9C=A8=20Implement=20`decompose-multi-?= =?UTF-8?q?controlled`=20pass=20for=20decomposing=20multi-controlled=20X?= =?UTF-8?q?=20gates=20into=20one-=20and=20two-qubit=20gates.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 3 + mlir/include/mlir/Compiler/CompilerPipeline.h | 3 + .../Decomposition/MultiControlled.h | 54 ++ .../mlir/Dialect/QCO/Transforms/Passes.td | 38 ++ mlir/lib/Compiler/CompilerPipeline.cpp | 3 + .../Transforms/Decomposition/McxSynthesis.cpp | 532 ++++++++++++++++++ .../DecomposeMultiControlled.cpp | 76 +++ mlir/tools/mqt-cc/mqt-cc.cpp | 7 + .../Transforms/Decomposition/CMakeLists.txt | 5 +- .../test_multi_controlled_decomposition.cpp | 258 +++++++++ pyproject.toml | 1 + 11 files changed, 978 insertions(+), 2 deletions(-) create mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/MultiControlled.h create mode 100644 mlir/lib/Dialect/QCO/Transforms/Decomposition/McxSynthesis.cpp create mode 100644 mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/DecomposeMultiControlled.cpp create mode 100644 mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_multi_controlled_decomposition.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 614fe3c174..330e0bfd2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ with the exception that minor releases may include breaking changes. ### Added +- ✨ Add a `decompose-multi-controlled` pass + for decomposing multi-controlled gates into one- and two-qubit gates + ([**@simon1hofmann**]) - ✨ Add a `fuse-single-qubit-unitary-runs` pass for fusing compile-time single-qubit unitary runs via Euler resynthesis ([#1672]) ([**@simon1hofmann**], [**@burgholzer**]) diff --git a/mlir/include/mlir/Compiler/CompilerPipeline.h b/mlir/include/mlir/Compiler/CompilerPipeline.h index 54d9f039de..cc9681a8c7 100644 --- a/mlir/include/mlir/Compiler/CompilerPipeline.h +++ b/mlir/include/mlir/Compiler/CompilerPipeline.h @@ -49,6 +49,9 @@ struct QuantumCompilerConfig { /// Enable Hadamard lifting bool enableHadamardLifting = false; + + /// Decompose multi-controlled gates into one- and two-qubit gates + bool enableDecomposeMultiControlled = false; }; /** diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/MultiControlled.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/MultiControlled.h new file mode 100644 index 0000000000..d96bcc7c52 --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/MultiControlled.h @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include +#include +#include +#include + +namespace mlir::qco::decomposition { + +/** + * @brief Emits a decomposition of a multi-controlled X gate. + * + * @details Emits a sequence of one- and two-qubit gates that, taken together, + * implement the multi-controlled X gate that flips @p target whenever all @p + * controls are in the @f$|1\rangle@f$ state. The decomposition follows Huang + * and Palsberg (PLDI 2024), composing Iten et al. (Phys. Rev. A 93, 032318, + * 2016) dirty-ancilla subcircuits for large control counts. The emitted gates + * are `h`, `t`, `tdg`, `p`, and `cx`. + * + * @note Adapted from ``synth_mcx_noaux_hp24`` and ``synth_mcx_n_dirty_i15`` in + * the IBM Qiskit framework + * (``crates/synthesis/src/multi_controlled/mcx.rs``). + * (C) Copyright IBM 2024 + * + * This code is licensed under the Apache License, Version 2.0. You may + * obtain a copy of this license in the LICENSE.txt file in the root + * directory of this source tree or at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Any modifications or derivative works of this code must retain this + * copyright notice, and modified files need to carry a notice + * indicating that they have been altered from the originals. + * + * @param builder Builder positioned at the desired insertion point. + * @param loc Location attached to the emitted operations. + * @param controls Current SSA values of the control qubits. + * @param target Current SSA value of the target qubit. + * @return The updated SSA values, ordered as `[controls..., target]`. + */ +[[nodiscard]] SmallVector synthesizeMcx(OpBuilder& builder, Location loc, + ValueRange controls, + Value target); + +} // 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 fafb906a77..6ac9702f09 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -200,4 +200,42 @@ def HadamardLifting : Pass<"hadamard-lifting", "mlir::ModuleOp"> { }]; } +//===----------------------------------------------------------------------===// +// Decomposition Passes +//===----------------------------------------------------------------------===// + +def DecomposeMultiControlled + : Pass<"decompose-multi-controlled", "mlir::ModuleOp"> { + let dependentDialects = ["mlir::qco::QCODialect", + "::mlir::arith::ArithDialect"]; + let summary = + "Decompose multi-controlled gates into one- and two-qubit gates"; + let description = [{ + Decomposes multi-controlled gates (`qco.ctrl`) into a sequence of one- and + two-qubit gates that can subsequently be handled by one- and two-qubit gate + synthesis methods. + + The pass matches `qco.ctrl` operations with at least `min-controls` control + qubits whose body is a single `qco.x` on one target (multi-controlled Pauli-X). + Other multi-controlled gates are left unchanged. Support for further base + gates will be added gradually. + + Multi-controlled X gates are decomposed using the Huang-Palsberg (PLDI 2024) + no-ancilla synthesis algorithm with a linear number of CX gates in the + number of controls, composing Iten et al. (Phys. Rev. A 93, 032318, 2016) + dirty-ancilla subcircuits for large control counts. The pass emits `h`, `t`, + `tdg`, `p`, and `cx` gates. + + By default (`min-controls=2`), Toffoli (`CCX`) gates are decomposed as well, + so that the output contains only genuine one- and two-qubit gates. Increase + `min-controls` to keep smaller controlled gates intact (e.g., when `CCX` is a + native gate of the target). + }]; + let options = [Option< + "minControls", "min-controls", "uint64_t", "2", + "Minimum number of controls for which a controlled gate is " + "decomposed. Controlled gates with fewer controls are left " + "unchanged. Must be at least 1.">]; +} + #endif // MLIR_DIALECT_QCO_TRANSFORMS_PASSES_TD diff --git a/mlir/lib/Compiler/CompilerPipeline.cpp b/mlir/lib/Compiler/CompilerPipeline.cpp index 821ccda2ee..7fd843ac14 100644 --- a/mlir/lib/Compiler/CompilerPipeline.cpp +++ b/mlir/lib/Compiler/CompilerPipeline.cpp @@ -147,6 +147,9 @@ QuantumCompilerPipeline::runPipeline(ModuleOp module, } // Stage 5: Optimization passes if (failed(runStage([&](PassManager& pm) { + if (config_.enableDecomposeMultiControlled) { + pm.addPass(qco::createDecomposeMultiControlled()); + } if (!config_.disableMergeSingleQubitRotationGates) { pm.addPass(qco::createMergeSingleQubitRotationGates()); } diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/McxSynthesis.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/McxSynthesis.cpp new file mode 100644 index 0000000000..9162259a7b --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/McxSynthesis.cpp @@ -0,0 +1,532 @@ +/* + * 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 + */ + +// Portions adapted from Qiskit (crates/synthesis/src/multi_controlled/mcx.rs). +// +// (C) Copyright IBM 2024 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at https://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/MultiControlled.h" + +#include +#include + +#include +#include +#include +#include + +namespace mlir::qco::decomposition { + +namespace { + +constexpr double K_PI = std::numbers::pi; +constexpr double K_PI8 = K_PI / 8.0; + +/// Emits QCO gates for multi-controlled X decomposition. +class GateEmitter { +public: + GateEmitter(OpBuilder& builder, Location loc, SmallVector& wires, + ArrayRef remap = {}) + : builder_(&builder), loc_(loc), wires_(&wires), remap_(remap) {} + + template void compose(ArrayRef qubitMap, Fn&& fn) { + SmallVector composeRemap; + composeRemap.reserve(qubitMap.size()); + for (std::size_t local : qubitMap) { + composeRemap.push_back(wireIndex(local)); + } + GateEmitter nested(*builder_, loc_, *wires_, composeRemap); + std::forward(fn)(nested); + } + + void h(std::size_t q) { + setWire(q, HOp::create(*builder_, loc_, wire(q)).getOutputQubit(0)); + } + + void x(std::size_t q) { + setWire(q, XOp::create(*builder_, loc_, wire(q)).getOutputQubit(0)); + } + + void p(std::size_t q, double theta) { + setWire(q, POp::create(*builder_, loc_, wire(q), theta).getOutputQubit(0)); + } + + void cx(std::size_t control, std::size_t target) { + auto ctrlOp = CtrlOp::create( + *builder_, loc_, ValueRange{wire(control)}, ValueRange{wire(target)}, + [&](ValueRange args) -> SmallVector { + return {XOp::create(*builder_, loc_, args[0]).getOutputQubit(0)}; + }); + setWire(control, ctrlOp.getControlsOut()[0]); + setWire(target, ctrlOp.getTargetsOut()[0]); + } + + void cp(std::size_t control, std::size_t target, double theta) { + p(control, theta / 2.0); + cx(control, target); + p(target, -theta / 2.0); + cx(control, target); + p(target, theta / 2.0); + } + + void t(std::size_t q) { + setWire(q, TOp::create(*builder_, loc_, wire(q)).getOutputQubit(0)); + } + + void tdg(std::size_t q) { + setWire(q, TdgOp::create(*builder_, loc_, wire(q)).getOutputQubit(0)); + } + + void ccp(double theta, std::size_t c0, std::size_t c1, std::size_t target) { + cx(c0, target); + p(target, -theta / 4.0); + cx(c1, target); + p(target, theta / 4.0); + cx(c0, target); + p(target, -theta / 4.0); + cx(c1, target); + p(target, theta / 4.0); + p(c0, theta / 4.0); + p(c1, theta / 4.0); + cx(c0, c1); + p(c1, -theta / 4.0); + cx(c0, c1); + } + + /// Standard CCX decomposition. + void emitCcx(std::size_t c0, std::size_t c1, std::size_t target) { + h(target); + cx(c1, target); + tdg(target); + cx(c0, target); + t(target); + cx(c1, target); + tdg(target); + cx(c0, target); + t(c1); + t(target); + h(target); + cx(c0, c1); + t(c0); + tdg(c1); + cx(c0, c1); + } + + /// Relative-phase CCX. + void emitRelativeCcx(std::size_t c0, std::size_t c1, std::size_t target) { + h(target); + t(target); + cx(c1, target); + tdg(target); + cx(c0, target); + t(target); + cx(c1, target); + tdg(target); + h(target); + } + + void c3x() { + h(3); + p(0, K_PI8); + p(1, K_PI8); + p(2, K_PI8); + p(3, K_PI8); + cx(0, 1); + p(1, -K_PI8); + cx(0, 1); + cx(1, 2); + p(2, -K_PI8); + cx(0, 2); + p(2, K_PI8); + cx(1, 2); + p(2, -K_PI8); + cx(0, 2); + cx(2, 3); + p(3, -K_PI8); + cx(1, 3); + p(3, K_PI8); + cx(2, 3); + p(3, -K_PI8); + cx(0, 3); + p(3, K_PI8); + cx(2, 3); + p(3, -K_PI8); + cx(1, 3); + p(3, K_PI8); + cx(2, 3); + p(3, -K_PI8); + cx(0, 3); + h(3); + } + +private: + [[nodiscard]] std::size_t wireIndex(std::size_t local) const { + return remap_.empty() ? local : remap_[local]; + } + + [[nodiscard]] Value wire(std::size_t local) const { + return (*wires_)[wireIndex(local)]; + } + + void setWire(std::size_t local, Value value) { + (*wires_)[wireIndex(local)] = value; + } + + OpBuilder* builder_; + Location loc_; + SmallVector* wires_; + ArrayRef remap_; +}; + +void synthRelativeMcx(GateEmitter& builder, std::size_t numControls); + +// Dirty-ancilla subcircuit after Iten et al., Phys. Rev. A 93, 032318 (2016). + +void addActionGadget(GateEmitter& builder, std::size_t q0, std::size_t q1, + std::size_t q2) { + builder.h(q2); + builder.t(q2); + builder.cx(q0, q2); + builder.tdg(q2); + builder.cx(q1, q2); +} + +void addResetGadget(GateEmitter& builder, std::size_t q0, std::size_t q1, + std::size_t q2) { + builder.cx(q1, q2); + builder.t(q2); + builder.cx(q0, q2); + builder.tdg(q2); + builder.h(q2); +} + +void synthMcxNDirtyI15(GateEmitter& builder, std::size_t numControls) { + if (numControls == 1) { + builder.cx(0, 1); + } else if (numControls == 2) { + builder.emitCcx(0, 1, 2); + } else if (numControls == 3) { + builder.c3x(); + } else { + const std::size_t controlsEnd = numControls; + const std::size_t target = numControls; + const std::size_t firstAncilla = numControls + 1; + const std::size_t lastAncilla = firstAncilla + numControls - 3; + for (std::size_t pass = 0; pass < 2; ++pass) { + builder.emitCcx(controlsEnd - 1, lastAncilla, target); + for (std::size_t i = numControls - 3; i-- > 0;) { + addActionGadget(builder, i + 2, firstAncilla + i, firstAncilla + i + 1); + } + builder.emitRelativeCcx(0, 1, firstAncilla); + for (std::size_t i = 0; i < numControls - 3; ++i) { + addResetGadget(builder, i + 2, firstAncilla + i, firstAncilla + i + 1); + } + } + } +} + +// No-auxiliary decomposition after Huang & Palsberg, PLDI 2024. + +void ux(GateEmitter& builder, std::size_t q1, std::size_t q2, std::size_t q3) { + builder.cx(q1, q3); + builder.cx(q1, q2); + builder.emitCcx(q2, q3, q1); +} + +void uz(GateEmitter& builder, std::size_t q1, std::size_t q2, std::size_t q3) { + builder.emitCcx(q2, q3, q1); + builder.cx(q1, q2); + builder.cx(q2, q3); +} + +void incrementNDirtyLarge(GateEmitter& builder, std::size_t n) { + const std::size_t lastQubit = n - 1; + + builder.x(n); + for (std::size_t q = 0; q < n; ++q) { + builder.cx(n, q); + } + builder.x(n); + + for (std::size_t i = 0; i < n - 1; ++i) { + ux(builder, n, n + 1 + i, i); + } + + builder.cx(n, lastQubit); + for (std::size_t i = n - 1; i-- > 0;) { + uz(builder, n, n + 1 + i, i); + } + + for (std::size_t i = 0; i < n - 1; ++i) { + builder.x(n + 1 + i); + } + + for (std::size_t i = 0; i < n - 1; ++i) { + ux(builder, n, n + 1 + i, i); + } + builder.cx(n, lastQubit); + for (std::size_t i = n - 1; i-- > 0;) { + uz(builder, n, n + 1 + i, i); + } + for (std::size_t i = 0; i < n - 1; ++i) { + builder.x(n + 1 + i); + } + + builder.x(lastQubit); + builder.x(n); + for (std::size_t q = 0; q < n; ++q) { + builder.cx(n, q); + } + builder.x(n); +} + +void incrementNDirtySmall(GateEmitter& builder, std::size_t n) { + SmallVector qubits; + for (std::size_t k = n - 1; k >= 1; --k) { + qubits.clear(); + for (std::size_t q = 0; q <= k; ++q) { + qubits.push_back(q); + } + for (std::size_t q = n + 1; q < 2 * n; ++q) { + qubits.push_back(q); + } + builder.compose(qubits, + [&](GateEmitter& sub) { synthMcxNDirtyI15(sub, k); }); + } + builder.x(0); +} + +void incrementNDirty(GateEmitter& builder, std::size_t n) { + if (n <= 10) { + incrementNDirtySmall(builder, n); + } else { + incrementNDirtyLarge(builder, n); + } +} + +void synthRelativeMcx(GateEmitter& builder, std::size_t numControls) { + const std::size_t target = numControls; + + if (numControls == 0) { + return; + } + if (numControls == 1) { + builder.cx(0, 1); + return; + } + if (numControls == 2) { + builder.emitRelativeCcx(0, 1, 2); + return; + } + + const std::size_t num3 = numControls / 3; + const std::size_t num2 = (numControls - num3) / 2; + const std::size_t num1 = numControls - num3 - num2; + const std::size_t block2Begin = num1; + const std::size_t block3Begin = num1 + num2; + const std::size_t controlsEnd = numControls; + + SmallVector map; + const auto relativeStep = [&](const double sign, const std::size_t begin, + const std::size_t end, const std::size_t k) { + builder.p(target, sign * K_PI8); + map.clear(); + for (std::size_t q = begin; q < end; ++q) { + map.push_back(q); + } + map.push_back(target); + builder.compose(map, [&](GateEmitter& sub) { synthRelativeMcx(sub, k); }); + }; + + builder.h(target); + relativeStep(+1, block3Begin, controlsEnd, num3); + relativeStep(-1, block2Begin, block3Begin, num2); + relativeStep(+1, block3Begin, controlsEnd, num3); + relativeStep(-1, 0, block2Begin, num1); + relativeStep(+1, block3Begin, controlsEnd, num3); + relativeStep(-1, block2Begin, block3Begin, num2); + relativeStep(+1, block3Begin, controlsEnd, num3); + relativeStep(-1, 0, block2Begin, num1); + builder.h(target); +} + +void synthRelativeMcxNDirty(GateEmitter& builder, std::size_t numControls) { + if (numControls < 11) { + synthRelativeMcx(builder, numControls); + } else { + synthMcxNDirtyI15(builder, numControls); + } +} + +/// Increment gadget from Huang & Palsberg Fig. 6 (`numDirtyAncillae == 1`) or +/// Fig. 8 (`numDirtyAncillae == 2`). +void incrementDirty(GateEmitter& builder, std::size_t n, + std::size_t numDirtyAncillae, bool flagAdd) { + if (numDirtyAncillae == 1 && n % 2 == 0) { + return; + } + + const std::size_t k = numDirtyAncillae == 1 ? (n + 1) / 2 : (n + 2) / 2; + const std::size_t ancilla1 = n; + const std::size_t ancilla2 = n + 1; + const std::size_t incrementWidth = numDirtyAncillae == 1 ? k : (1 + n - k); + + if (!flagAdd) { + for (std::size_t i = 0; i < n; ++i) { + builder.x(i); + } + } + + SmallVector k12Qubits; + k12Qubits.push_back(ancilla1); + for (std::size_t q = k; q < n; ++q) { + k12Qubits.push_back(q); + } + for (std::size_t q = 0; q < k; ++q) { + k12Qubits.push_back(q); + } + if (numDirtyAncillae == 2) { + k12Qubits.push_back(ancilla2); + } + + builder.compose(k12Qubits, [&](GateEmitter& sub) { + incrementNDirty(sub, incrementWidth); + }); + builder.x(ancilla1); + for (std::size_t q = k; q < n; ++q) { + builder.cx(ancilla1, q); + } + + SmallVector kMcxQubits; + for (std::size_t q = 0; q < k; ++q) { + kMcxQubits.push_back(q); + } + kMcxQubits.push_back(ancilla1); + for (std::size_t q = k; q < n; ++q) { + kMcxQubits.push_back(q); + } + if (numDirtyAncillae == 2) { + kMcxQubits.push_back(ancilla2); + } + + builder.compose(kMcxQubits, + [&](GateEmitter& sub) { synthRelativeMcxNDirty(sub, k); }); + builder.compose(k12Qubits, [&](GateEmitter& sub) { + incrementNDirty(sub, incrementWidth); + }); + builder.x(ancilla1); + builder.compose(kMcxQubits, + [&](GateEmitter& sub) { synthRelativeMcxNDirty(sub, k); }); + for (std::size_t q = k; q < n; ++q) { + builder.cx(ancilla1, q); + } + + SmallVector k3Qubits; + for (std::size_t q = 0; q < k; ++q) { + k3Qubits.push_back(q); + } + for (std::size_t q = k; q < n; ++q) { + k3Qubits.push_back(q); + } + k3Qubits.push_back(ancilla1); + if (numDirtyAncillae == 2) { + k3Qubits.push_back(ancilla2); + } + builder.compose(k3Qubits, [&](GateEmitter& sub) { incrementNDirty(sub, k); }); + + if (!flagAdd) { + for (std::size_t i = 0; i < n; ++i) { + builder.x(i); + } + } +} + +} // namespace + +SmallVector synthesizeMcx(OpBuilder& builder, Location loc, + ValueRange controls, Value target) { + SmallVector wires(controls.begin(), controls.end()); + wires.push_back(target); + + const std::size_t numControls = controls.size(); + const std::size_t n = numControls + 1; + const std::size_t targetIdx = numControls; + const std::size_t lastControl = n - 1; + const std::size_t c0 = n - 2; + const std::size_t c1 = n - 1; + + GateEmitter emitter(builder, loc, wires); + if (n == 1) { + emitter.x(0); + return wires; + } + if (n == 2) { + emitter.cx(0, 1); + return wires; + } + + emitter.h(targetIdx); + + SmallVector incrementQubits(n); + std::iota(incrementQubits.begin(), incrementQubits.end(), 0U); + + // Fig. 6 path for very large even widths (22+ controls); otherwise Fig. 8. + if ((n % 2 == 0) && (n >= 23)) { + emitter.compose(incrementQubits, [&](GateEmitter& sub) { + incrementDirty(sub, n - 1, 1, true); + }); + double phi = -K_PI; + for (std::size_t q = n - 2; q > 0; --q) { + phi /= 2.0; + emitter.cp(q, lastControl, phi); + } + emitter.compose(incrementQubits, [&](GateEmitter& sub) { + incrementDirty(sub, n - 1, 1, false); + }); + phi = K_PI; + for (std::size_t q = n - 2; q > 0; --q) { + phi /= 2.0; + emitter.cp(q, lastControl, phi); + } + emitter.cp(0, lastControl, phi); + } else { + emitter.compose(incrementQubits, [&](GateEmitter& sub) { + incrementDirty(sub, n - 2, 2, true); + }); + double phi = -K_PI; + for (std::size_t q = n - 3; q > 0; --q) { + phi /= 2.0; + emitter.ccp(phi, q, c0, c1); + } + emitter.compose(incrementQubits, [&](GateEmitter& sub) { + incrementDirty(sub, n - 2, 2, false); + }); + phi = K_PI; + for (std::size_t q = n - 3; q > 0; --q) { + phi /= 2.0; + emitter.ccp(phi, q, c0, c1); + } + emitter.ccp(phi, 0, c0, c1); + } + + emitter.h(targetIdx); + return wires; +} + +} // namespace mlir::qco::decomposition diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/DecomposeMultiControlled.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/DecomposeMultiControlled.cpp new file mode 100644 index 0000000000..4db4e07001 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/DecomposeMultiControlled.cpp @@ -0,0 +1,76 @@ +/* + * 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/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/MultiControlled.h" +#include "mlir/Dialect/QCO/Transforms/Passes.h" +#include "mlir/Dialect/Utils/Utils.h" + +#include // IWYU pragma: keep (Passes.h.inc) +#include +#include + +namespace mlir::qco { + +#define GEN_PASS_DEF_DECOMPOSEMULTICONTROLLED +#include "mlir/Dialect/QCO/Transforms/Passes.h.inc" + +namespace { + +/** + * @brief Decomposes a multi-controlled X gate into elementary one- and + * two-qubit gates. + */ +struct DecomposeMultiControlledXPattern final : OpRewritePattern { + explicit DecomposeMultiControlledXPattern(MLIRContext* context, + uint64_t minControls) + : OpRewritePattern(context), minControls_(minControls) {} + + LogicalResult matchAndRewrite(CtrlOp op, + PatternRewriter& rewriter) const override { + if (op.getNumControls() < minControls_) { + return failure(); + } + auto inner = utils::getSoleBodyUnitary(*op.getBody()); + if (!inner || !isa(inner.getOperation()) || op.getNumTargets() != 1) { + return failure(); + } + + rewriter.setInsertionPoint(op); + const auto results = decomposition::synthesizeMcx( + rewriter, op.getLoc(), op.getControlsIn(), op.getInputTarget(0)); + rewriter.replaceOp(op, results); + return success(); + } + +private: + uint64_t minControls_; +}; + +/** + * @brief Pass that decomposes multi-controlled X gates into elementary gates. + */ +struct DecomposeMultiControlled final + : impl::DecomposeMultiControlledBase { + using DecomposeMultiControlledBase::DecomposeMultiControlledBase; + + void runOnOperation() override { + RewritePatternSet patterns(&getContext()); + patterns.add(&getContext(), minControls); + + if (failed(applyPatternsGreedily(getOperation(), std::move(patterns)))) { + signalPassFailure(); + } + } +}; + +} // namespace + +} // namespace mlir::qco diff --git a/mlir/tools/mqt-cc/mqt-cc.cpp b/mlir/tools/mqt-cc/mqt-cc.cpp index d8fb75daa4..b83721f0c8 100644 --- a/mlir/tools/mqt-cc/mqt-cc.cpp +++ b/mlir/tools/mqt-cc/mqt-cc.cpp @@ -88,6 +88,12 @@ static llvm::cl::opt enableHadamardLifting( llvm::cl::desc("Apply Hadamard lifting during optimization"), llvm::cl::init(false)); +static llvm::cl::opt enableDecomposeMultiControlled( + "decompose-multi-controlled", + llvm::cl::desc("Decompose multi-controlled gates into one- and two-qubit " + "gates during optimization"), + llvm::cl::init(false)); + /** * @brief Load and parse a .qasm file */ @@ -179,6 +185,7 @@ int main(int argc, char** argv) { config.disableMergeSingleQubitRotationGates = disableMergeSingleQubitRotationGates; config.enableHadamardLifting = enableHadamardLifting; + config.enableDecomposeMultiControlled = enableDecomposeMultiControlled; // Run the compilation pipeline CompilationRecord record; diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt index f493bb9e4d..662f3668e2 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt @@ -7,10 +7,11 @@ # Licensed under the MIT License set(target_name mqt-core-mlir-unittest-decomposition) -add_executable(${target_name} test_euler_decomposition.cpp) +add_executable(${target_name} test_euler_decomposition.cpp + test_multi_controlled_decomposition.cpp) target_link_libraries(${target_name} PRIVATE GTest::gtest_main MLIRQCOProgramBuilder - MLIRQCOTransforms) + MLIRQCOTransforms MQT::CoreDD) target_link_libraries(${target_name} PRIVATE MLIRPass MLIRFuncDialect MLIRArithDialect MLIRIR MLIRSupport MLIRQTensorDialect) diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_multi_controlled_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_multi_controlled_decomposition.cpp new file mode 100644 index 0000000000..25c6dfeb45 --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_multi_controlled_decomposition.cpp @@ -0,0 +1,258 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "dd/FunctionalityConstruction.hpp" +#include "dd/Package.hpp" +#include "ir/QuantumComputation.hpp" +#include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h" +#include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Passes.h" +#include "mlir/Dialect/Utils/Utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace { + +using namespace mlir; +using namespace mlir::qco; + +/// Full unitary verification via DD (k <= 8 only; cost grows quickly above +/// that). +constexpr std::array K_DD_CONTROL_COUNTS = {2, 3, 4, 5, + 6, 7, 8}; +/// Pass-only checks above k = 8; spaced toward 30. Includes 22–24 for the +/// Fig. 6 / Fig. 8 boundary at n = k + 1 >= 23. +constexpr std::array K_SMOKE_CONTROL_COUNTS = { + 10, 12, 15, 18, 20, 22, 23, 24, 25, 28, 30, +}; + +[[nodiscard]] OwningOpRef buildMcxModule(MLIRContext* context, + std::size_t numControls) { + return QCOProgramBuilder::build(context, [numControls](QCOProgramBuilder& b) { + SmallVector wires; + wires.reserve(numControls + 1); + for (std::size_t i = 0; i <= numControls; ++i) { + wires.push_back(b.staticQubit(i)); + } + b.mcx(ValueRange(wires).drop_back(), wires.back()); + }); +} + +/// Converts a decomposed QCO function into a `QuantumComputation`. +[[nodiscard]] qc::QuantumComputation +funcOpToQuantumComputation(func::FuncOp funcOp, std::size_t& numQubits) { + DenseMap qubitIndex; + numQubits = 0; + for (StaticOp staticOp : funcOp.getOps()) { + const auto index = static_cast(staticOp.getIndex()); + qubitIndex[staticOp.getQubit()] = index; + numQubits = std::max(numQubits, index + 1); + } + + const auto mapQubit = [&qubitIndex](Value in, Value out) { + const std::size_t q = qubitIndex.at(in); + qubitIndex[out] = q; + return static_cast(q); + }; + + qc::QuantumComputation qc(numQubits); + for (Operation& op : funcOp.getBody().front()) { + if (isa(op)) { + continue; + } + if (auto hOp = dyn_cast(op)) { + qc.h(mapQubit(hOp.getInputQubit(0), hOp.getOutputQubit(0))); + continue; + } + if (auto xOp = dyn_cast(op)) { + qc.x(mapQubit(xOp.getInputQubit(0), xOp.getOutputQubit(0))); + continue; + } + if (auto tOp = dyn_cast(op)) { + qc.t(mapQubit(tOp.getInputQubit(0), tOp.getOutputQubit(0))); + continue; + } + if (auto tdgOp = dyn_cast(op)) { + qc.tdg(mapQubit(tdgOp.getInputQubit(0), tdgOp.getOutputQubit(0))); + continue; + } + if (auto pOp = dyn_cast(op)) { + const qc::Qubit q = mapQubit(pOp.getInputQubit(0), pOp.getOutputQubit(0)); + const auto theta = mlir::utils::valueToDouble(pOp.getTheta()); + EXPECT_TRUE(theta.has_value()); + qc.p(theta.value_or(0.0), q); + continue; + } + if (auto ctrlOp = dyn_cast(op)) { + EXPECT_EQ(ctrlOp.getNumControls(), 1U) + << "decomposition must not leave multi-controlled gates"; + EXPECT_EQ(ctrlOp.getNumTargets(), 1U); + const qc::Qubit control = + mapQubit(ctrlOp.getControlsIn()[0], ctrlOp.getControlsOut()[0]); + const qc::Qubit target = + mapQubit(ctrlOp.getInputTarget(0), ctrlOp.getTargetsOut()[0]); + qc.cx(control, target); + continue; + } + ADD_FAILURE() << "unexpected op in decomposed circuit: " + << op.getName().getStringRef().str(); + } + + return qc; +} + +void expectImplementsMcx(func::FuncOp funcOp, std::size_t numControls) { + std::size_t numQubits = 0; + auto decomposedQc = funcOpToQuantumComputation(funcOp, numQubits); + ASSERT_EQ(numQubits, numControls + 1); + + const auto dd = std::make_unique(numQubits); + + qc::QuantumComputation referenceQc(numQubits); + qc::Controls controls; + for (std::size_t i = 0; i < numControls; ++i) { + controls.emplace(static_cast(i)); + } + referenceQc.mcx(controls, static_cast(numControls)); + + const dd::MatrixDD decomposedDD = dd::buildFunctionality(decomposedQc, *dd); + const dd::MatrixDD referenceDD = dd::buildFunctionality(referenceQc, *dd); + EXPECT_TRUE(dd->multiply(decomposedDD, dd->conjugateTranspose(referenceDD)) + .isIdentity(/*upToGlobalPhase=*/false)); +} + +[[nodiscard]] std::size_t countMultiControlledOps(ModuleOp moduleOp) { + std::size_t count = 0; + moduleOp.walk([&count](CtrlOp op) { + if (op.getNumControls() >= 2) { + ++count; + } + }); + return count; +} + +LogicalResult +runDecomposePass(ModuleOp moduleOp, + const DecomposeMultiControlledOptions& options = {}) { + PassManager pm(moduleOp.getContext()); + pm.addPass(createDecomposeMultiControlled(options)); + return pm.run(moduleOp); +} + +class McxDecompositionTest : public testing::Test { +protected: + void SetUp() override { + DialectRegistry registry; + registry.insert(); + context_ = std::make_unique(); + context_->appendDialectRegistry(registry); + context_->loadAllAvailableDialects(); + } + +public: + [[nodiscard]] MLIRContext* context() const { return context_.get(); } + +private: + std::unique_ptr context_; +}; + +class McxDdTest : public McxDecompositionTest, + public testing::WithParamInterface {}; + +TEST_P(McxDdTest, ImplementsMcx) { + const std::size_t numControls = GetParam(); + auto moduleOp = buildMcxModule(context(), numControls); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(runDecomposePass(moduleOp.get()).succeeded()); + + auto funcOp = *moduleOp->getBody()->getOps().begin(); + expectImplementsMcx(funcOp, numControls); +} + +INSTANTIATE_TEST_SUITE_P(McxDd, McxDdTest, + testing::ValuesIn(K_DD_CONTROL_COUNTS), + [](const testing::TestParamInfo& info) { + return "controls" + std::to_string(info.param); + }); + +class LargeMcxTest : public McxDecompositionTest, + public testing::WithParamInterface {}; + +TEST_P(LargeMcxTest, DecomposesWithoutMultiControlledGates) { + const std::size_t numControls = GetParam(); + auto moduleOp = buildMcxModule(context(), numControls); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(runDecomposePass(moduleOp.get()).succeeded()); + EXPECT_EQ(countMultiControlledOps(moduleOp.get()), 0U); +} + +INSTANTIATE_TEST_SUITE_P(LargeMcx, LargeMcxTest, + testing::ValuesIn(K_SMOKE_CONTROL_COUNTS), + [](const testing::TestParamInfo& info) { + return "controls" + std::to_string(info.param); + }); + +TEST_F(McxDecompositionTest, LeavesSingleControlledXUntouched) { + auto moduleOp = + QCOProgramBuilder::build(context(), [](QCOProgramBuilder& builder) { + builder.cx(builder.staticQubit(0), builder.staticQubit(1)); + }); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(runDecomposePass(moduleOp.get()).succeeded()); + + std::size_t hCount = 0; + moduleOp->walk([&hCount](HOp /*op*/) { ++hCount; }); + EXPECT_EQ(hCount, 0U); +} + +TEST_F(McxDecompositionTest, MinControlsKeepsToffoli) { + auto moduleOp = buildMcxModule(context(), 2); + ASSERT_TRUE(moduleOp); + + DecomposeMultiControlledOptions options; + options.minControls = 3; + ASSERT_TRUE(runDecomposePass(moduleOp.get(), options).succeeded()); + EXPECT_EQ(countMultiControlledOps(moduleOp.get()), 1U); +} + +TEST_F(McxDecompositionTest, DecomposesMcxAndLeavesUnsupportedGates) { + auto moduleOp = + QCOProgramBuilder::build(context(), [](QCOProgramBuilder& builder) { + builder.mcx({builder.staticQubit(0), builder.staticQubit(1), + builder.staticQubit(2)}, + builder.staticQubit(3)); + builder.mcz({builder.staticQubit(4), builder.staticQubit(5)}, + builder.staticQubit(6)); + }); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(runDecomposePass(moduleOp.get()).succeeded()); + + EXPECT_EQ(countMultiControlledOps(moduleOp.get()), 1U); + + std::size_t hCount = 0; + moduleOp->walk([&hCount](HOp /*op*/) { ++hCount; }); + EXPECT_GT(hCount, 0U); +} + +} // namespace diff --git a/pyproject.toml b/pyproject.toml index f1ea00d081..e25ee071e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -254,6 +254,7 @@ default.extend-ignore-re = [ ] [tool.typos.default.extend-words] +Iten = "Iten" wille = "wille" anc = "anc" mch = "mch" From d48be9c9f2b3401ea3c9e5ad1f3cafda87c5f6bb Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 24 Jun 2026 16:10:30 +0200 Subject: [PATCH 02/13] =?UTF-8?q?=E2=9C=A8=20Extend=20`decompose-multi-con?= =?UTF-8?q?trolled`=20pass=20to=20support=20decomposition=20of=20multi-con?= =?UTF-8?q?trolled=20Z=20gates.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 +- mlir/include/mlir/Compiler/CompilerPipeline.h | 7 +- .../Decomposition/MultiControlled.h | 24 +++ .../mlir/Dialect/QCO/Transforms/Passes.td | 26 ++-- mlir/lib/Compiler/CompilerPipeline.cpp | 4 +- .../DecomposeMultiControlled.cpp | 101 ++++++++++++ .../Transforms/Decomposition/McxSynthesis.cpp | 60 +++++--- .../DecomposeMultiControlled.cpp | 76 --------- mlir/tools/mqt-cc/mqt-cc.cpp | 8 + .../Transforms/Decomposition/CMakeLists.txt | 3 +- .../test_multi_controlled_decomposition.cpp | 144 +++++++++++++++--- 11 files changed, 322 insertions(+), 133 deletions(-) create mode 100644 mlir/lib/Dialect/QCO/Transforms/Decomposition/DecomposeMultiControlled.cpp delete mode 100644 mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/DecomposeMultiControlled.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 330e0bfd2b..501b51264a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ with the exception that minor releases may include breaking changes. ### Added - ✨ Add a `decompose-multi-controlled` pass - for decomposing multi-controlled gates into one- and two-qubit gates + for decomposing multi-controlled X and Z gates into one- and two-qubit gates ([**@simon1hofmann**]) - ✨ Add a `fuse-single-qubit-unitary-runs` pass for fusing compile-time single-qubit unitary runs via Euler resynthesis diff --git a/mlir/include/mlir/Compiler/CompilerPipeline.h b/mlir/include/mlir/Compiler/CompilerPipeline.h index cc9681a8c7..79eee5a100 100644 --- a/mlir/include/mlir/Compiler/CompilerPipeline.h +++ b/mlir/include/mlir/Compiler/CompilerPipeline.h @@ -13,6 +13,7 @@ #include #include +#include #include namespace mlir { @@ -50,8 +51,12 @@ struct QuantumCompilerConfig { /// Enable Hadamard lifting bool enableHadamardLifting = false; - /// Decompose multi-controlled gates into one- and two-qubit gates + /// Decompose multi-controlled X/Z gates into elementary one- and two-qubit + /// gates. bool enableDecomposeMultiControlled = false; + + /// Minimum control count for @ref enableDecomposeMultiControlled (default 2). + std::uint64_t decomposeMultiControlledMinControls = 2; }; /** diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/MultiControlled.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/MultiControlled.h index d96bcc7c52..2d0364a0e4 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/MultiControlled.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/MultiControlled.h @@ -46,9 +46,33 @@ namespace mlir::qco::decomposition { * @param controls Current SSA values of the control qubits. * @param target Current SSA value of the target qubit. * @return The updated SSA values, ordered as `[controls..., target]`. + * + * @pre @p controls must contain at least two qubits. */ [[nodiscard]] SmallVector synthesizeMcx(OpBuilder& builder, Location loc, ValueRange controls, Value target); +/** + * @brief Emits a decomposition of a multi-controlled Z gate. + * + * @details Emits the same gate set as @ref synthesizeMcx (`h`, `t`, `tdg`, `p`, + * and `cx`). For @f$k \ge 2@f$ controls, emits the HP24 MCX core directly, + * since @f$\mathrm{MCZ} = + * H_{\text{target}}\,(H_{\text{target}}\,\mathrm{CORE}\, + * H_{\text{target}})\,H_{\text{target}} = \mathrm{CORE}@f$ and the MCX + * Hadamard bookends cancel under this conjugation. + * + * @param builder Builder positioned at the desired insertion point. + * @param loc Location attached to the emitted operations. + * @param controls Current SSA values of the control qubits. + * @param target Current SSA value of the target qubit. + * @return The updated SSA values, ordered as `[controls..., target]`. + * + * @pre @p controls must contain at least two qubits. + */ +[[nodiscard]] SmallVector synthesizeMcz(OpBuilder& builder, Location loc, + ValueRange controls, + Value target); + } // 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 6ac9702f09..c6a57831d3 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -216,26 +216,30 @@ def DecomposeMultiControlled synthesis methods. The pass matches `qco.ctrl` operations with at least `min-controls` control - qubits whose body is a single `qco.x` on one target (multi-controlled Pauli-X). - Other multi-controlled gates are left unchanged. Support for further base - gates will be added gradually. + qubits whose body is a single `qco.x` or `qco.z` on one target + (multi-controlled Pauli-X or Pauli-Z). Other multi-controlled gates are left + unchanged. Support for further base gates will be added gradually. Multi-controlled X gates are decomposed using the Huang-Palsberg (PLDI 2024) no-ancilla synthesis algorithm with a linear number of CX gates in the number of controls, composing Iten et al. (Phys. Rev. A 93, 032318, 2016) - dirty-ancilla subcircuits for large control counts. The pass emits `h`, `t`, - `tdg`, `p`, and `cx` gates. - - By default (`min-controls=2`), Toffoli (`CCX`) gates are decomposed as well, - so that the output contains only genuine one- and two-qubit gates. Increase - `min-controls` to keep smaller controlled gates intact (e.g., when `CCX` is a - native gate of the target). + dirty-ancilla subcircuits for large control counts. Multi-controlled Z gates + use the same HP24 core (algebraically equivalent to + @f$H_{\text{target}}\,\mathrm{MCX}\,H_{\text{target}}@f$, without redundant + Hadamard bookends for @f$k \ge 2@f$). The pass emits `h`, `t`, `tdg`, `p`, + and `cx` gates. + + By default (`min-controls=2`), gates with at least two controls (e.g. `CCX`, + `CCZ`, and larger `MCX`/`MCZ`) are decomposed, leaving single-control gates + (e.g. `CX`, `CZ`) and bare one-qubit gates intact. Increase `min-controls` + to keep smaller multi-controlled gates intact (e.g., when `CCX` is a native + gate of the target). }]; let options = [Option< "minControls", "min-controls", "uint64_t", "2", "Minimum number of controls for which a controlled gate is " "decomposed. Controlled gates with fewer controls are left " - "unchanged. Must be at least 1.">]; + "unchanged. Must be at least 2.">]; } #endif // MLIR_DIALECT_QCO_TRANSFORMS_PASSES_TD diff --git a/mlir/lib/Compiler/CompilerPipeline.cpp b/mlir/lib/Compiler/CompilerPipeline.cpp index 7fd843ac14..a9000c6d0d 100644 --- a/mlir/lib/Compiler/CompilerPipeline.cpp +++ b/mlir/lib/Compiler/CompilerPipeline.cpp @@ -148,7 +148,9 @@ QuantumCompilerPipeline::runPipeline(ModuleOp module, // Stage 5: Optimization passes if (failed(runStage([&](PassManager& pm) { if (config_.enableDecomposeMultiControlled) { - pm.addPass(qco::createDecomposeMultiControlled()); + qco::DecomposeMultiControlledOptions options; + options.minControls = config_.decomposeMultiControlledMinControls; + pm.addPass(qco::createDecomposeMultiControlled(options)); } if (!config_.disableMergeSingleQubitRotationGates) { pm.addPass(qco::createMergeSingleQubitRotationGates()); diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/DecomposeMultiControlled.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/DecomposeMultiControlled.cpp new file mode 100644 index 0000000000..1ee3269b42 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/DecomposeMultiControlled.cpp @@ -0,0 +1,101 @@ +/* + * 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/MultiControlled.h" +#include "mlir/Dialect/QCO/Transforms/Passes.h" +#include "mlir/Dialect/Utils/Utils.h" + +#include // IWYU pragma: keep (Passes.h.inc) +#include +#include + +namespace mlir::qco { + +#define GEN_PASS_DEF_DECOMPOSEMULTICONTROLLED +#include "mlir/Dialect/QCO/Transforms/Passes.h.inc" + +namespace { + +/** + * @brief Decomposes a multi-controlled Pauli-X or Pauli-Z gate into elementary + * one- and two-qubit gates. + * + * @details Matches `qco.ctrl` with a single `qco.x` or `qco.z` body when the + * control count is at least `minControls_` (and at least two, as enforced by + * the pass). Single-control `CX`/`CZ` and other gates are left unchanged. + */ +struct DecomposeMultiControlledPauliPattern final : OpRewritePattern { + explicit DecomposeMultiControlledPauliPattern(MLIRContext* context, + uint64_t minControls) + : OpRewritePattern(context), minControls_(minControls) {} + + LogicalResult matchAndRewrite(CtrlOp op, + PatternRewriter& rewriter) const override { + if (op.getNumControls() < minControls_ || op.getNumTargets() != 1) { + return failure(); + } + auto inner = utils::getSoleBodyUnitary(*op.getBody()); + if (!inner) { + return failure(); + } + + rewriter.setInsertionPoint(op); + const auto controls = op.getControlsIn(); + const auto target = op.getInputTarget(0); + const auto loc = op.getLoc(); + + if (isa(inner.getOperation())) { + rewriter.replaceOp( + op, decomposition::synthesizeMcx(rewriter, loc, controls, target)); + return success(); + } + if (isa(inner.getOperation())) { + rewriter.replaceOp( + op, decomposition::synthesizeMcz(rewriter, loc, controls, target)); + return success(); + } + return failure(); + } + +private: + uint64_t minControls_; +}; + +/** + * @brief Pass that decomposes multi-controlled X and Z gates into elementary + * gates. + */ +struct DecomposeMultiControlled final + : impl::DecomposeMultiControlledBase { + using DecomposeMultiControlledBase::DecomposeMultiControlledBase; + + void runOnOperation() override { + if (minControls < 2) { + getOperation().emitError() + << "decompose-multi-controlled requires min-controls >= 2"; + signalPassFailure(); + return; + } + + RewritePatternSet patterns(&getContext()); + patterns.add(&getContext(), + minControls); + + if (failed(applyPatternsGreedily(getOperation(), std::move(patterns)))) { + signalPassFailure(); + } + } +}; + +} // namespace + +} // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/McxSynthesis.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/McxSynthesis.cpp index 9162259a7b..4b7e2ddbb8 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/McxSynthesis.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/McxSynthesis.cpp @@ -24,6 +24,7 @@ #include "mlir/Dialect/QCO/Transforms/Decomposition/MultiControlled.h" #include +#include #include #include @@ -459,30 +460,14 @@ void incrementDirty(GateEmitter& builder, std::size_t n, } // namespace -SmallVector synthesizeMcx(OpBuilder& builder, Location loc, - ValueRange controls, Value target) { - SmallVector wires(controls.begin(), controls.end()); - wires.push_back(target); - - const std::size_t numControls = controls.size(); - const std::size_t n = numControls + 1; - const std::size_t targetIdx = numControls; +/// HP24 no-auxiliary MCX core (Fig. 6 / Fig. 8), without target Hadamard +/// bookends. +/// @param n Total wire count (controls plus target). +static void emitMcxHp24Core(GateEmitter& emitter, std::size_t n) { const std::size_t lastControl = n - 1; const std::size_t c0 = n - 2; const std::size_t c1 = n - 1; - GateEmitter emitter(builder, loc, wires); - if (n == 1) { - emitter.x(0); - return wires; - } - if (n == 2) { - emitter.cx(0, 1); - return wires; - } - - emitter.h(targetIdx); - SmallVector incrementQubits(n); std::iota(incrementQubits.begin(), incrementQubits.end(), 0U); @@ -524,8 +509,43 @@ SmallVector synthesizeMcx(OpBuilder& builder, Location loc, } emitter.ccp(phi, 0, c0, c1); } +} + +SmallVector synthesizeMcx(OpBuilder& builder, Location loc, + ValueRange controls, Value target) { + if (controls.size() < 2) { + llvm::reportFatalUsageError( + "synthesizeMcx requires at least 2 control qubits"); + } + SmallVector wires(controls.begin(), controls.end()); + wires.push_back(target); + + const std::size_t n = controls.size() + 1; + const std::size_t targetIdx = controls.size(); + + GateEmitter emitter(builder, loc, wires); emitter.h(targetIdx); + emitMcxHp24Core(emitter, n); + emitter.h(targetIdx); + return wires; +} + +SmallVector synthesizeMcz(OpBuilder& builder, Location loc, + ValueRange controls, Value target) { + if (controls.size() < 2) { + llvm::reportFatalUsageError( + "synthesizeMcz requires at least 2 control qubits"); + } + + SmallVector wires(controls.begin(), controls.end()); + wires.push_back(target); + + const std::size_t n = controls.size() + 1; + + GateEmitter emitter(builder, loc, wires); + // Algebraically MCZ = H·(H·CORE·H)·H = CORE for k >= 2. + emitMcxHp24Core(emitter, n); return wires; } diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/DecomposeMultiControlled.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/DecomposeMultiControlled.cpp deleted file mode 100644 index 4db4e07001..0000000000 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/DecomposeMultiControlled.cpp +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/MultiControlled.h" -#include "mlir/Dialect/QCO/Transforms/Passes.h" -#include "mlir/Dialect/Utils/Utils.h" - -#include // IWYU pragma: keep (Passes.h.inc) -#include -#include - -namespace mlir::qco { - -#define GEN_PASS_DEF_DECOMPOSEMULTICONTROLLED -#include "mlir/Dialect/QCO/Transforms/Passes.h.inc" - -namespace { - -/** - * @brief Decomposes a multi-controlled X gate into elementary one- and - * two-qubit gates. - */ -struct DecomposeMultiControlledXPattern final : OpRewritePattern { - explicit DecomposeMultiControlledXPattern(MLIRContext* context, - uint64_t minControls) - : OpRewritePattern(context), minControls_(minControls) {} - - LogicalResult matchAndRewrite(CtrlOp op, - PatternRewriter& rewriter) const override { - if (op.getNumControls() < minControls_) { - return failure(); - } - auto inner = utils::getSoleBodyUnitary(*op.getBody()); - if (!inner || !isa(inner.getOperation()) || op.getNumTargets() != 1) { - return failure(); - } - - rewriter.setInsertionPoint(op); - const auto results = decomposition::synthesizeMcx( - rewriter, op.getLoc(), op.getControlsIn(), op.getInputTarget(0)); - rewriter.replaceOp(op, results); - return success(); - } - -private: - uint64_t minControls_; -}; - -/** - * @brief Pass that decomposes multi-controlled X gates into elementary gates. - */ -struct DecomposeMultiControlled final - : impl::DecomposeMultiControlledBase { - using DecomposeMultiControlledBase::DecomposeMultiControlledBase; - - void runOnOperation() override { - RewritePatternSet patterns(&getContext()); - patterns.add(&getContext(), minControls); - - if (failed(applyPatternsGreedily(getOperation(), std::move(patterns)))) { - signalPassFailure(); - } - } -}; - -} // namespace - -} // namespace mlir::qco diff --git a/mlir/tools/mqt-cc/mqt-cc.cpp b/mlir/tools/mqt-cc/mqt-cc.cpp index b83721f0c8..16657d7c4c 100644 --- a/mlir/tools/mqt-cc/mqt-cc.cpp +++ b/mlir/tools/mqt-cc/mqt-cc.cpp @@ -94,6 +94,12 @@ static llvm::cl::opt enableDecomposeMultiControlled( "gates during optimization"), llvm::cl::init(false)); +static llvm::cl::opt decomposeMultiControlledMinControls( + "decompose-multi-controlled-min-controls", + llvm::cl::desc("Minimum number of controls to decompose when " + "--decompose-multi-controlled is enabled"), + llvm::cl::init(2)); + /** * @brief Load and parse a .qasm file */ @@ -186,6 +192,8 @@ int main(int argc, char** argv) { disableMergeSingleQubitRotationGates; config.enableHadamardLifting = enableHadamardLifting; config.enableDecomposeMultiControlled = enableDecomposeMultiControlled; + config.decomposeMultiControlledMinControls = + decomposeMultiControlledMinControls.getValue(); // Run the compilation pipeline CompilationRecord record; diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt index 662f3668e2..1046fee5c0 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt @@ -7,8 +7,7 @@ # Licensed under the MIT License set(target_name mqt-core-mlir-unittest-decomposition) -add_executable(${target_name} test_euler_decomposition.cpp - test_multi_controlled_decomposition.cpp) +add_executable(${target_name} test_euler_decomposition.cpp test_multi_controlled_decomposition.cpp) target_link_libraries(${target_name} PRIVATE GTest::gtest_main MLIRQCOProgramBuilder MLIRQCOTransforms MQT::CoreDD) diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_multi_controlled_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_multi_controlled_decomposition.cpp index 25c6dfeb45..af0f0a7ae1 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_multi_controlled_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_multi_controlled_decomposition.cpp @@ -41,22 +41,42 @@ using namespace mlir::qco; /// that). constexpr std::array K_DD_CONTROL_COUNTS = {2, 3, 4, 5, 6, 7, 8}; -/// Pass-only checks above k = 8; spaced toward 30. Includes 22–24 for the -/// Fig. 6 / Fig. 8 boundary at n = k + 1 >= 23. +/// Pass-only checks above k = 8; spaced toward 30. Includes k = 22–24 for the +/// Fig. 6 / Fig. 8 boundary at wire count n = k + 1 >= 23. constexpr std::array K_SMOKE_CONTROL_COUNTS = { 10, 12, 15, 18, 20, 22, 23, 24, 25, 28, 30, }; +enum class ControlledPauli { X, Z }; + +[[nodiscard]] OwningOpRef +buildControlledPauliModule(MLIRContext* context, std::size_t numControls, + ControlledPauli pauli) { + return QCOProgramBuilder::build( + context, [numControls, pauli](QCOProgramBuilder& b) { + SmallVector wires; + wires.reserve(numControls + 1); + for (std::size_t i = 0; i <= numControls; ++i) { + wires.push_back(b.staticQubit(i)); + } + const auto controls = ValueRange(wires).drop_back(); + const auto target = wires.back(); + if (pauli == ControlledPauli::X) { + b.mcx(controls, target); + } else { + b.mcz(controls, target); + } + }); +} + [[nodiscard]] OwningOpRef buildMcxModule(MLIRContext* context, std::size_t numControls) { - return QCOProgramBuilder::build(context, [numControls](QCOProgramBuilder& b) { - SmallVector wires; - wires.reserve(numControls + 1); - for (std::size_t i = 0; i <= numControls; ++i) { - wires.push_back(b.staticQubit(i)); - } - b.mcx(ValueRange(wires).drop_back(), wires.back()); - }); + return buildControlledPauliModule(context, numControls, ControlledPauli::X); +} + +[[nodiscard]] OwningOpRef buildMczModule(MLIRContext* context, + std::size_t numControls) { + return buildControlledPauliModule(context, numControls, ControlledPauli::Z); } /// Converts a decomposed QCO function into a `QuantumComputation`. @@ -122,7 +142,9 @@ funcOpToQuantumComputation(func::FuncOp funcOp, std::size_t& numQubits) { return qc; } -void expectImplementsMcx(func::FuncOp funcOp, std::size_t numControls) { +void expectImplementsControlledPauli(func::FuncOp funcOp, + std::size_t numControls, + ControlledPauli pauli) { std::size_t numQubits = 0; auto decomposedQc = funcOpToQuantumComputation(funcOp, numQubits); ASSERT_EQ(numQubits, numControls + 1); @@ -134,7 +156,12 @@ void expectImplementsMcx(func::FuncOp funcOp, std::size_t numControls) { for (std::size_t i = 0; i < numControls; ++i) { controls.emplace(static_cast(i)); } - referenceQc.mcx(controls, static_cast(numControls)); + const auto target = static_cast(numControls); + if (pauli == ControlledPauli::X) { + referenceQc.mcx(controls, target); + } else { + referenceQc.mcz(controls, target); + } const dd::MatrixDD decomposedDD = dd::buildFunctionality(decomposedQc, *dd); const dd::MatrixDD referenceDD = dd::buildFunctionality(referenceQc, *dd); @@ -142,6 +169,14 @@ void expectImplementsMcx(func::FuncOp funcOp, std::size_t numControls) { .isIdentity(/*upToGlobalPhase=*/false)); } +void expectImplementsMcx(func::FuncOp funcOp, std::size_t numControls) { + expectImplementsControlledPauli(funcOp, numControls, ControlledPauli::X); +} + +void expectImplementsMcz(func::FuncOp funcOp, std::size_t numControls) { + expectImplementsControlledPauli(funcOp, numControls, ControlledPauli::Z); +} + [[nodiscard]] std::size_t countMultiControlledOps(ModuleOp moduleOp) { std::size_t count = 0; moduleOp.walk([&count](CtrlOp op) { @@ -196,6 +231,25 @@ INSTANTIATE_TEST_SUITE_P(McxDd, McxDdTest, return "controls" + std::to_string(info.param); }); +class MczDdTest : public McxDecompositionTest, + public testing::WithParamInterface {}; + +TEST_P(MczDdTest, ImplementsMcz) { + const std::size_t numControls = GetParam(); + auto moduleOp = buildMczModule(context(), numControls); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(runDecomposePass(moduleOp.get()).succeeded()); + + auto funcOp = *moduleOp->getBody()->getOps().begin(); + expectImplementsMcz(funcOp, numControls); +} + +INSTANTIATE_TEST_SUITE_P(MczDd, MczDdTest, + testing::ValuesIn(K_DD_CONTROL_COUNTS), + [](const testing::TestParamInfo& info) { + return "controls" + std::to_string(info.param); + }); + class LargeMcxTest : public McxDecompositionTest, public testing::WithParamInterface {}; @@ -213,6 +267,23 @@ INSTANTIATE_TEST_SUITE_P(LargeMcx, LargeMcxTest, return "controls" + std::to_string(info.param); }); +class LargeMczTest : public McxDecompositionTest, + public testing::WithParamInterface {}; + +TEST_P(LargeMczTest, DecomposesWithoutMultiControlledGates) { + const std::size_t numControls = GetParam(); + auto moduleOp = buildMczModule(context(), numControls); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(runDecomposePass(moduleOp.get()).succeeded()); + EXPECT_EQ(countMultiControlledOps(moduleOp.get()), 0U); +} + +INSTANTIATE_TEST_SUITE_P(LargeMcz, LargeMczTest, + testing::ValuesIn(K_SMOKE_CONTROL_COUNTS), + [](const testing::TestParamInfo& info) { + return "controls" + std::to_string(info.param); + }); + TEST_F(McxDecompositionTest, LeavesSingleControlledXUntouched) { auto moduleOp = QCOProgramBuilder::build(context(), [](QCOProgramBuilder& builder) { @@ -221,9 +292,34 @@ TEST_F(McxDecompositionTest, LeavesSingleControlledXUntouched) { ASSERT_TRUE(moduleOp); ASSERT_TRUE(runDecomposePass(moduleOp.get()).succeeded()); - std::size_t hCount = 0; - moduleOp->walk([&hCount](HOp /*op*/) { ++hCount; }); - EXPECT_EQ(hCount, 0U); + EXPECT_EQ(countMultiControlledOps(moduleOp.get()), 0U); + + std::size_t singleControlledCount = 0; + moduleOp->walk([&singleControlledCount](CtrlOp op) { + if (op.getNumControls() == 1) { + ++singleControlledCount; + } + }); + EXPECT_EQ(singleControlledCount, 1U); +} + +TEST_F(McxDecompositionTest, LeavesSingleControlledZUntouched) { + auto moduleOp = + QCOProgramBuilder::build(context(), [](QCOProgramBuilder& builder) { + builder.cz(builder.staticQubit(0), builder.staticQubit(1)); + }); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(runDecomposePass(moduleOp.get()).succeeded()); + + EXPECT_EQ(countMultiControlledOps(moduleOp.get()), 0U); + + std::size_t singleControlledCount = 0; + moduleOp->walk([&singleControlledCount](CtrlOp op) { + if (op.getNumControls() == 1) { + ++singleControlledCount; + } + }); + EXPECT_EQ(singleControlledCount, 1U); } TEST_F(McxDecompositionTest, MinControlsKeepsToffoli) { @@ -236,7 +332,17 @@ TEST_F(McxDecompositionTest, MinControlsKeepsToffoli) { EXPECT_EQ(countMultiControlledOps(moduleOp.get()), 1U); } -TEST_F(McxDecompositionTest, DecomposesMcxAndLeavesUnsupportedGates) { +TEST_F(McxDecompositionTest, MinControlsKeepsCcZ) { + auto moduleOp = buildMczModule(context(), 2); + ASSERT_TRUE(moduleOp); + + DecomposeMultiControlledOptions options; + options.minControls = 3; + ASSERT_TRUE(runDecomposePass(moduleOp.get(), options).succeeded()); + EXPECT_EQ(countMultiControlledOps(moduleOp.get()), 1U); +} + +TEST_F(McxDecompositionTest, DecomposesMcxAndMcz) { auto moduleOp = QCOProgramBuilder::build(context(), [](QCOProgramBuilder& builder) { builder.mcx({builder.staticQubit(0), builder.staticQubit(1), @@ -248,11 +354,7 @@ TEST_F(McxDecompositionTest, DecomposesMcxAndLeavesUnsupportedGates) { ASSERT_TRUE(moduleOp); ASSERT_TRUE(runDecomposePass(moduleOp.get()).succeeded()); - EXPECT_EQ(countMultiControlledOps(moduleOp.get()), 1U); - - std::size_t hCount = 0; - moduleOp->walk([&hCount](HOp /*op*/) { ++hCount; }); - EXPECT_GT(hCount, 0U); + EXPECT_EQ(countMultiControlledOps(moduleOp.get()), 0U); } } // namespace From f0de507acf2ce7204c062d2329a72e45606253fe Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 24 Jun 2026 16:23:00 +0200 Subject: [PATCH 03/13] =?UTF-8?q?=F0=9F=93=9D=20Update=20copyright=20year?= =?UTF-8?q?=20in=20QCO=20dialect=20files=20to=202025=20and=20remove=20outd?= =?UTF-8?q?ated=20comments=20in=20McxSynthesis.cpp.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/Decomposition/MultiControlled.h | 5 ++--- .../QCO/Transforms/Decomposition/McxSynthesis.cpp | 12 ------------ 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/MultiControlled.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/MultiControlled.h index 2d0364a0e4..45be0e7247 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/MultiControlled.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/MultiControlled.h @@ -28,9 +28,8 @@ namespace mlir::qco::decomposition { * are `h`, `t`, `tdg`, `p`, and `cx`. * * @note Adapted from ``synth_mcx_noaux_hp24`` and ``synth_mcx_n_dirty_i15`` in - * the IBM Qiskit framework - * (``crates/synthesis/src/multi_controlled/mcx.rs``). - * (C) Copyright IBM 2024 + * the IBM Qiskit framework. + * (C) Copyright IBM 2025 * * This code is licensed under the Apache License, Version 2.0. You may * obtain a copy of this license in the LICENSE.txt file in the root diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/McxSynthesis.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/McxSynthesis.cpp index 4b7e2ddbb8..a3095c8575 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/McxSynthesis.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/McxSynthesis.cpp @@ -8,18 +8,6 @@ * Licensed under the MIT License */ -// Portions adapted from Qiskit (crates/synthesis/src/multi_controlled/mcx.rs). -// -// (C) Copyright IBM 2024 -// -// This code is licensed under the Apache License, Version 2.0. You may -// obtain a copy of this license in the LICENSE.txt file in the root directory -// of this source tree or at https://www.apache.org/licenses/LICENSE-2.0. -// -// Any modifications or derivative works of this code must retain this -// copyright notice, and modified files need to carry a notice indicating -// that they have been altered from the originals. - #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/MultiControlled.h" From 08a9dfa3575cbd5fd44afbafe36952f0aad3f86c Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 24 Jun 2026 16:24:54 +0200 Subject: [PATCH 04/13] =?UTF-8?q?=F0=9F=93=9D=20Refine=20comments=20in=20M?= =?UTF-8?q?cxSynthesis.cpp=20for=20clarity=20and=20update=20test=20descrip?= =?UTF-8?q?tions=20in=20test=5Fmulti=5Fcontrolled=5Fdecomposition.cpp=20to?= =?UTF-8?q?=20reflect=20the=20one-=20vs=20two-ancilla=20boundary.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Dialect/QCO/Transforms/Decomposition/McxSynthesis.cpp | 8 +++----- .../Decomposition/test_multi_controlled_decomposition.cpp | 4 ++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/McxSynthesis.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/McxSynthesis.cpp index a3095c8575..e09588a6a4 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/McxSynthesis.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/McxSynthesis.cpp @@ -363,8 +363,7 @@ void synthRelativeMcxNDirty(GateEmitter& builder, std::size_t numControls) { } } -/// Increment gadget from Huang & Palsberg Fig. 6 (`numDirtyAncillae == 1`) or -/// Fig. 8 (`numDirtyAncillae == 2`). +/// Increment gadget with @p numDirtyAncillae dirty ancilla(e). void incrementDirty(GateEmitter& builder, std::size_t n, std::size_t numDirtyAncillae, bool flagAdd) { if (numDirtyAncillae == 1 && n % 2 == 0) { @@ -448,8 +447,7 @@ void incrementDirty(GateEmitter& builder, std::size_t n, } // namespace -/// HP24 no-auxiliary MCX core (Fig. 6 / Fig. 8), without target Hadamard -/// bookends. +/// HP24 no-auxiliary MCX core, without target Hadamard bookends. /// @param n Total wire count (controls plus target). static void emitMcxHp24Core(GateEmitter& emitter, std::size_t n) { const std::size_t lastControl = n - 1; @@ -459,7 +457,7 @@ static void emitMcxHp24Core(GateEmitter& emitter, std::size_t n) { SmallVector incrementQubits(n); std::iota(incrementQubits.begin(), incrementQubits.end(), 0U); - // Fig. 6 path for very large even widths (22+ controls); otherwise Fig. 8. + // One dirty ancilla for very large even widths (22+ controls); two otherwise. if ((n % 2 == 0) && (n >= 23)) { emitter.compose(incrementQubits, [&](GateEmitter& sub) { incrementDirty(sub, n - 1, 1, true); diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_multi_controlled_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_multi_controlled_decomposition.cpp index af0f0a7ae1..c48d07c0d3 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_multi_controlled_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_multi_controlled_decomposition.cpp @@ -41,8 +41,8 @@ using namespace mlir::qco; /// that). constexpr std::array K_DD_CONTROL_COUNTS = {2, 3, 4, 5, 6, 7, 8}; -/// Pass-only checks above k = 8; spaced toward 30. Includes k = 22–24 for the -/// Fig. 6 / Fig. 8 boundary at wire count n = k + 1 >= 23. +/// Pass-only checks above k = 8; includes k = 22–24 for the one- vs +/// two-ancilla boundary at wire count n = k + 1 >= 23. constexpr std::array K_SMOKE_CONTROL_COUNTS = { 10, 12, 15, 18, 20, 22, 23, 24, 25, 28, 30, }; From d47641d4db462e5cf6bcd87a8f21d6048779686e Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 24 Jun 2026 16:31:07 +0200 Subject: [PATCH 05/13] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20linter=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DecomposeMultiControlled.cpp | 7 ++ .../Transforms/Decomposition/McxSynthesis.cpp | 42 ++++---- .../test_multi_controlled_decomposition.cpp | 102 +++++++++--------- 3 files changed, 82 insertions(+), 69 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/DecomposeMultiControlled.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/DecomposeMultiControlled.cpp index 1ee3269b42..3634b06e37 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/DecomposeMultiControlled.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/DecomposeMultiControlled.cpp @@ -15,9 +15,15 @@ #include "mlir/Dialect/Utils/Utils.h" #include // IWYU pragma: keep (Passes.h.inc) +#include #include +#include +#include #include +#include +#include + namespace mlir::qco { #define GEN_PASS_DEF_DECOMPOSEMULTICONTROLLED @@ -78,6 +84,7 @@ struct DecomposeMultiControlled final : impl::DecomposeMultiControlledBase { using DecomposeMultiControlledBase::DecomposeMultiControlledBase; +protected: void runOnOperation() override { if (minControls < 2) { getOperation().emitError() diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/McxSynthesis.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/McxSynthesis.cpp index e09588a6a4..3531b898a5 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/McxSynthesis.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/McxSynthesis.cpp @@ -13,6 +13,8 @@ #include #include +#include +#include #include #include @@ -183,12 +185,12 @@ class GateEmitter { ArrayRef remap_; }; -void synthRelativeMcx(GateEmitter& builder, std::size_t numControls); +} // namespace -// Dirty-ancilla subcircuit after Iten et al., Phys. Rev. A 93, 032318 (2016). +static void synthRelativeMcx(GateEmitter& builder, std::size_t numControls); -void addActionGadget(GateEmitter& builder, std::size_t q0, std::size_t q1, - std::size_t q2) { +static void addActionGadget(GateEmitter& builder, std::size_t q0, + std::size_t q1, std::size_t q2) { builder.h(q2); builder.t(q2); builder.cx(q0, q2); @@ -196,8 +198,8 @@ void addActionGadget(GateEmitter& builder, std::size_t q0, std::size_t q1, builder.cx(q1, q2); } -void addResetGadget(GateEmitter& builder, std::size_t q0, std::size_t q1, - std::size_t q2) { +static void addResetGadget(GateEmitter& builder, std::size_t q0, std::size_t q1, + std::size_t q2) { builder.cx(q1, q2); builder.t(q2); builder.cx(q0, q2); @@ -205,7 +207,7 @@ void addResetGadget(GateEmitter& builder, std::size_t q0, std::size_t q1, builder.h(q2); } -void synthMcxNDirtyI15(GateEmitter& builder, std::size_t numControls) { +static void synthMcxNDirtyI15(GateEmitter& builder, std::size_t numControls) { if (numControls == 1) { builder.cx(0, 1); } else if (numControls == 2) { @@ -230,21 +232,21 @@ void synthMcxNDirtyI15(GateEmitter& builder, std::size_t numControls) { } } -// No-auxiliary decomposition after Huang & Palsberg, PLDI 2024. - -void ux(GateEmitter& builder, std::size_t q1, std::size_t q2, std::size_t q3) { +static void ux(GateEmitter& builder, std::size_t q1, std::size_t q2, + std::size_t q3) { builder.cx(q1, q3); builder.cx(q1, q2); builder.emitCcx(q2, q3, q1); } -void uz(GateEmitter& builder, std::size_t q1, std::size_t q2, std::size_t q3) { +static void uz(GateEmitter& builder, std::size_t q1, std::size_t q2, + std::size_t q3) { builder.emitCcx(q2, q3, q1); builder.cx(q1, q2); builder.cx(q2, q3); } -void incrementNDirtyLarge(GateEmitter& builder, std::size_t n) { +static void incrementNDirtyLarge(GateEmitter& builder, std::size_t n) { const std::size_t lastQubit = n - 1; builder.x(n); @@ -285,7 +287,7 @@ void incrementNDirtyLarge(GateEmitter& builder, std::size_t n) { builder.x(n); } -void incrementNDirtySmall(GateEmitter& builder, std::size_t n) { +static void incrementNDirtySmall(GateEmitter& builder, std::size_t n) { SmallVector qubits; for (std::size_t k = n - 1; k >= 1; --k) { qubits.clear(); @@ -301,7 +303,7 @@ void incrementNDirtySmall(GateEmitter& builder, std::size_t n) { builder.x(0); } -void incrementNDirty(GateEmitter& builder, std::size_t n) { +static void incrementNDirty(GateEmitter& builder, std::size_t n) { if (n <= 10) { incrementNDirtySmall(builder, n); } else { @@ -309,7 +311,7 @@ void incrementNDirty(GateEmitter& builder, std::size_t n) { } } -void synthRelativeMcx(GateEmitter& builder, std::size_t numControls) { +static void synthRelativeMcx(GateEmitter& builder, std::size_t numControls) { const std::size_t target = numControls; if (numControls == 0) { @@ -355,7 +357,8 @@ void synthRelativeMcx(GateEmitter& builder, std::size_t numControls) { builder.h(target); } -void synthRelativeMcxNDirty(GateEmitter& builder, std::size_t numControls) { +static void synthRelativeMcxNDirty(GateEmitter& builder, + std::size_t numControls) { if (numControls < 11) { synthRelativeMcx(builder, numControls); } else { @@ -363,9 +366,8 @@ void synthRelativeMcxNDirty(GateEmitter& builder, std::size_t numControls) { } } -/// Increment gadget with @p numDirtyAncillae dirty ancilla(e). -void incrementDirty(GateEmitter& builder, std::size_t n, - std::size_t numDirtyAncillae, bool flagAdd) { +static void incrementDirty(GateEmitter& builder, std::size_t n, + std::size_t numDirtyAncillae, bool flagAdd) { if (numDirtyAncillae == 1 && n % 2 == 0) { return; } @@ -445,8 +447,6 @@ void incrementDirty(GateEmitter& builder, std::size_t n, } } -} // namespace - /// HP24 no-auxiliary MCX core, without target Hadamard bookends. /// @param n Total wire count (controls plus target). static void emitMcxHp24Core(GateEmitter& emitter, std::size_t n) { diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_multi_controlled_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_multi_controlled_decomposition.cpp index c48d07c0d3..0874f5bb1e 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_multi_controlled_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_multi_controlled_decomposition.cpp @@ -10,7 +10,9 @@ #include "dd/FunctionalityConstruction.hpp" #include "dd/Package.hpp" +#include "ir/Definitions.hpp" #include "ir/QuantumComputation.hpp" +#include "ir/operations/Control.hpp" #include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" @@ -25,14 +27,17 @@ #include #include #include +#include #include #include +#include +#include #include #include +#include #include - -namespace { +#include using namespace mlir; using namespace mlir::qco; @@ -47,9 +52,42 @@ constexpr std::array K_SMOKE_CONTROL_COUNTS = { 10, 12, 15, 18, 20, 22, 23, 24, 25, 28, 30, }; -enum class ControlledPauli { X, Z }; +enum class ControlledPauli : std::uint8_t { X, Z }; + +namespace { + +class McxDecompositionTest : public testing::Test { +protected: + void SetUp() override { + DialectRegistry registry; + registry.insert(); + context_ = std::make_unique(); + context_->appendDialectRegistry(registry); + context_->loadAllAvailableDialects(); + } + +public: + [[nodiscard]] MLIRContext* context() const { return context_.get(); } + +private: + std::unique_ptr context_; +}; + +class McxDdTest : public McxDecompositionTest, + public testing::WithParamInterface {}; + +class MczDdTest : public McxDecompositionTest, + public testing::WithParamInterface {}; + +class LargeMcxTest : public McxDecompositionTest, + public testing::WithParamInterface {}; + +class LargeMczTest : public McxDecompositionTest, + public testing::WithParamInterface {}; + +} // namespace -[[nodiscard]] OwningOpRef +[[nodiscard]] static OwningOpRef buildControlledPauliModule(MLIRContext* context, std::size_t numControls, ControlledPauli pauli) { return QCOProgramBuilder::build( @@ -69,18 +107,17 @@ buildControlledPauliModule(MLIRContext* context, std::size_t numControls, }); } -[[nodiscard]] OwningOpRef buildMcxModule(MLIRContext* context, - std::size_t numControls) { +[[nodiscard]] static OwningOpRef +buildMcxModule(MLIRContext* context, std::size_t numControls) { return buildControlledPauliModule(context, numControls, ControlledPauli::X); } -[[nodiscard]] OwningOpRef buildMczModule(MLIRContext* context, - std::size_t numControls) { +[[nodiscard]] static OwningOpRef +buildMczModule(MLIRContext* context, std::size_t numControls) { return buildControlledPauliModule(context, numControls, ControlledPauli::Z); } -/// Converts a decomposed QCO function into a `QuantumComputation`. -[[nodiscard]] qc::QuantumComputation +[[nodiscard]] static qc::QuantumComputation funcOpToQuantumComputation(func::FuncOp funcOp, std::size_t& numQubits) { DenseMap qubitIndex; numQubits = 0; @@ -142,9 +179,9 @@ funcOpToQuantumComputation(func::FuncOp funcOp, std::size_t& numQubits) { return qc; } -void expectImplementsControlledPauli(func::FuncOp funcOp, - std::size_t numControls, - ControlledPauli pauli) { +static void expectImplementsControlledPauli(func::FuncOp funcOp, + std::size_t numControls, + ControlledPauli pauli) { std::size_t numQubits = 0; auto decomposedQc = funcOpToQuantumComputation(funcOp, numQubits); ASSERT_EQ(numQubits, numControls + 1); @@ -169,15 +206,15 @@ void expectImplementsControlledPauli(func::FuncOp funcOp, .isIdentity(/*upToGlobalPhase=*/false)); } -void expectImplementsMcx(func::FuncOp funcOp, std::size_t numControls) { +static void expectImplementsMcx(func::FuncOp funcOp, std::size_t numControls) { expectImplementsControlledPauli(funcOp, numControls, ControlledPauli::X); } -void expectImplementsMcz(func::FuncOp funcOp, std::size_t numControls) { +static void expectImplementsMcz(func::FuncOp funcOp, std::size_t numControls) { expectImplementsControlledPauli(funcOp, numControls, ControlledPauli::Z); } -[[nodiscard]] std::size_t countMultiControlledOps(ModuleOp moduleOp) { +[[nodiscard]] static std::size_t countMultiControlledOps(ModuleOp moduleOp) { std::size_t count = 0; moduleOp.walk([&count](CtrlOp op) { if (op.getNumControls() >= 2) { @@ -187,7 +224,7 @@ void expectImplementsMcz(func::FuncOp funcOp, std::size_t numControls) { return count; } -LogicalResult +static LogicalResult runDecomposePass(ModuleOp moduleOp, const DecomposeMultiControlledOptions& options = {}) { PassManager pm(moduleOp.getContext()); @@ -195,26 +232,6 @@ runDecomposePass(ModuleOp moduleOp, return pm.run(moduleOp); } -class McxDecompositionTest : public testing::Test { -protected: - void SetUp() override { - DialectRegistry registry; - registry.insert(); - context_ = std::make_unique(); - context_->appendDialectRegistry(registry); - context_->loadAllAvailableDialects(); - } - -public: - [[nodiscard]] MLIRContext* context() const { return context_.get(); } - -private: - std::unique_ptr context_; -}; - -class McxDdTest : public McxDecompositionTest, - public testing::WithParamInterface {}; - TEST_P(McxDdTest, ImplementsMcx) { const std::size_t numControls = GetParam(); auto moduleOp = buildMcxModule(context(), numControls); @@ -231,9 +248,6 @@ INSTANTIATE_TEST_SUITE_P(McxDd, McxDdTest, return "controls" + std::to_string(info.param); }); -class MczDdTest : public McxDecompositionTest, - public testing::WithParamInterface {}; - TEST_P(MczDdTest, ImplementsMcz) { const std::size_t numControls = GetParam(); auto moduleOp = buildMczModule(context(), numControls); @@ -250,9 +264,6 @@ INSTANTIATE_TEST_SUITE_P(MczDd, MczDdTest, return "controls" + std::to_string(info.param); }); -class LargeMcxTest : public McxDecompositionTest, - public testing::WithParamInterface {}; - TEST_P(LargeMcxTest, DecomposesWithoutMultiControlledGates) { const std::size_t numControls = GetParam(); auto moduleOp = buildMcxModule(context(), numControls); @@ -267,9 +278,6 @@ INSTANTIATE_TEST_SUITE_P(LargeMcx, LargeMcxTest, return "controls" + std::to_string(info.param); }); -class LargeMczTest : public McxDecompositionTest, - public testing::WithParamInterface {}; - TEST_P(LargeMczTest, DecomposesWithoutMultiControlledGates) { const std::size_t numControls = GetParam(); auto moduleOp = buildMczModule(context(), numControls); @@ -356,5 +364,3 @@ TEST_F(McxDecompositionTest, DecomposesMcxAndMcz) { EXPECT_EQ(countMultiControlledOps(moduleOp.get()), 0U); } - -} // namespace From e0b53a4c694d1c8c0808752f6c0783dc2615e4e7 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 24 Jun 2026 16:31:24 +0200 Subject: [PATCH 06/13] =?UTF-8?q?=F0=9F=93=9D=20Update=20CHANGELOG.md=20to?= =?UTF-8?q?=20include=20pull=20request=20link=20for=20`decompose-multi-con?= =?UTF-8?q?trolled`=20pass=20addition.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 501b51264a..b93deeb7c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,9 +12,8 @@ with the exception that minor releases may include breaking changes. ### Added -- ✨ Add a `decompose-multi-controlled` pass - for decomposing multi-controlled X and Z gates into one- and two-qubit gates - ([**@simon1hofmann**]) +- ✨ Add a `decompose-multi-controlled` pass for decomposing multi-controlled X + and Z gates into one- and two-qubit gates ([#1810]) ([**@simon1hofmann**]) - ✨ Add a `fuse-single-qubit-unitary-runs` pass for fusing compile-time single-qubit unitary runs via Euler resynthesis ([#1672]) ([**@simon1hofmann**], [**@burgholzer**]) @@ -601,6 +600,7 @@ changelogs._ +[#1810]: https://github.com/munich-quantum-toolkit/core/pull/1810 [#1802]: https://github.com/munich-quantum-toolkit/core/pull/1802 [#1787]: https://github.com/munich-quantum-toolkit/core/pull/1787 [#1782]: https://github.com/munich-quantum-toolkit/core/pull/1782 From 0ef3f714801b2e9eac7023dd671c82d84c23e6c8 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 24 Jun 2026 16:34:08 +0200 Subject: [PATCH 07/13] =?UTF-8?q?=E2=98=82=EF=B8=8F=20Increase=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Compiler/test_compiler_pipeline.cpp | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index ddc3e4ce4d..0670c3c240 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -120,12 +120,14 @@ class CompilerPipelineTest static void runPipeline(const mlir::ModuleOp module, const bool convertToQIR, const bool disableMergeSingleQubitRotationGates, const bool enableHadamardLifting, + const bool enableDecomposeMultiControlled, mlir::CompilationRecord& record) { mlir::QuantumCompilerConfig config; config.convertToQIRAdaptive = convertToQIR; config.disableMergeSingleQubitRotationGates = disableMergeSingleQubitRotationGates; config.enableHadamardLifting = enableHadamardLifting; + config.enableDecomposeMultiControlled = enableDecomposeMultiControlled; config.recordIntermediates = true; config.printIRAfterAllStages = true; @@ -169,7 +171,7 @@ TEST_P(CompilerPipelineTest, EndToEndPipeline) { EXPECT_TRUE(mlir::verify(*module).succeeded()); mlir::CompilationRecord record; - runPipeline(module.get(), testCase.convertToQIR, false, false, record); + runPipeline(module.get(), testCase.convertToQIR, false, false, false, record); ASSERT_TRUE(testCase.qcReferenceBuilder); auto qcReference = buildQCReference(testCase.qcReferenceBuilder); @@ -216,7 +218,7 @@ TEST_F(CompilerPipelineTest, RotationGateMergingPass) { ASSERT_TRUE(module); mlir::CompilationRecord record; - runPipeline(module.get(), false, false, false, record); + runPipeline(module.get(), false, false, false, false, record); // The outputs must differ, proving the pass ran and transformed the IR EXPECT_NE(record.afterQCOCanon, record.afterOptimization); @@ -239,7 +241,28 @@ TEST_F(CompilerPipelineTest, HadamardLiftingPass) { ASSERT_TRUE(module); mlir::CompilationRecord record; - runPipeline(module.get(), false, true, true, record); + runPipeline(module.get(), false, true, true, false, record); + + // The outputs must differ, proving the pass ran and transformed the IR + EXPECT_NE(record.afterQCOCanon, record.afterOptimization); +} + +/** + * @brief Test: Multi-controlled decomposition pass is invoked during + * optimization + * + * @details + * We run the pipeline with multi-controlled decomposition enabled and check + * whether the QCO IR changes between initial and post-optimization cleanup. + * Correctness of the pass is tested in a dedicated test. + */ +TEST_F(CompilerPipelineTest, DecomposeMultiControlledPass) { + auto module = mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::multipleControlledX); + ASSERT_TRUE(module); + + mlir::CompilationRecord record; + runPipeline(module.get(), false, false, false, true, record); // The outputs must differ, proving the pass ran and transformed the IR EXPECT_NE(record.afterQCOCanon, record.afterOptimization); From dff6eebec23e4376d59529090009f1bbb1afd692 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 24 Jun 2026 16:48:30 +0200 Subject: [PATCH 08/13] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20linter=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/Dialect/QCO/Transforms/Decomposition/McxSynthesis.cpp | 1 + .../Decomposition/test_multi_controlled_decomposition.cpp | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/McxSynthesis.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/McxSynthesis.cpp index 3531b898a5..a59a470fc3 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/McxSynthesis.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/McxSynthesis.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_multi_controlled_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_multi_controlled_decomposition.cpp index 0874f5bb1e..517d6e7670 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_multi_controlled_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_multi_controlled_decomposition.cpp @@ -52,10 +52,10 @@ constexpr std::array K_SMOKE_CONTROL_COUNTS = { 10, 12, 15, 18, 20, 22, 23, 24, 25, 28, 30, }; -enum class ControlledPauli : std::uint8_t { X, Z }; - namespace { +enum class ControlledPauli : std::uint8_t { X, Z }; + class McxDecompositionTest : public testing::Test { protected: void SetUp() override { From 9ec9e6f64508e813e5853a8680a0521a69e61aeb Mon Sep 17 00:00:00 2001 From: Simon Date: Wed, 24 Jun 2026 21:08:49 +0200 Subject: [PATCH 09/13] =?UTF-8?q?=F0=9F=90=87=20Address=20Rabbit's=20comme?= =?UTF-8?q?nts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/lib/Compiler/CompilerPipeline.cpp | 6 ++++++ .../QCO/Transforms/Decomposition/McxSynthesis.cpp | 9 +++++++-- mlir/unittests/Compiler/test_compiler_pipeline.cpp | 12 ++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/mlir/lib/Compiler/CompilerPipeline.cpp b/mlir/lib/Compiler/CompilerPipeline.cpp index a9000c6d0d..5771c7db17 100644 --- a/mlir/lib/Compiler/CompilerPipeline.cpp +++ b/mlir/lib/Compiler/CompilerPipeline.cpp @@ -77,6 +77,12 @@ QuantumCompilerPipeline::runPipeline(ModuleOp module, "enabled and the record pointer to be non-null.\n"; return failure(); } + if (config_.enableDecomposeMultiControlled && + config_.decomposeMultiControlledMinControls < 2) { + llvm::errs() << "decomposeMultiControlledMinControls must be at least 2 when " + "enableDecomposeMultiControlled is enabled.\n"; + return failure(); + } auto runStage = [&](auto&& populatePasses) -> LogicalResult { PassManager pm(module.getContext()); diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/McxSynthesis.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/McxSynthesis.cpp index a59a470fc3..b6ef17a8fe 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/McxSynthesis.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/McxSynthesis.cpp @@ -458,8 +458,13 @@ static void emitMcxHp24Core(GateEmitter& emitter, std::size_t n) { SmallVector incrementQubits(n); std::iota(incrementQubits.begin(), incrementQubits.end(), 0U); - // One dirty ancilla for very large even widths (22+ controls); two otherwise. - if ((n % 2 == 0) && (n >= 23)) { + const std::size_t numControls = n - 1; + // One dirty ancilla for odd control counts >= 23 (even total wire count n); + // two dirty ancillae otherwise. + constexpr std::size_t kOneDirtyAncillaMinControls = 23; + const bool useOneDirtyAncilla = + numControls >= kOneDirtyAncillaMinControls && (numControls % 2 == 1); + if (useOneDirtyAncilla) { emitter.compose(incrementQubits, [&](GateEmitter& sub) { incrementDirty(sub, n - 1, 1, true); }); diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 0670c3c240..00a890e96b 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -268,6 +268,18 @@ TEST_F(CompilerPipelineTest, DecomposeMultiControlledPass) { EXPECT_NE(record.afterQCOCanon, record.afterOptimization); } +TEST_F(CompilerPipelineTest, DecomposeMultiControlledPassMcz) { + auto module = mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::multipleControlledZ); + ASSERT_TRUE(module); + + mlir::CompilationRecord record; + runPipeline(module.get(), false, false, false, true, record); + + // The outputs must differ, proving the MCZ path ran and transformed the IR + EXPECT_NE(record.afterQCOCanon, record.afterOptimization); +} + INSTANTIATE_TEST_SUITE_P( QuantumComputationPipelineProgramsTest, CompilerPipelineTest, testing::Values( From cf57d474b2ad5ce12b611e77adb461544ba46ceb Mon Sep 17 00:00:00 2001 From: Simon Date: Wed, 24 Jun 2026 21:09:58 +0200 Subject: [PATCH 10/13] =?UTF-8?q?=E2=9C=A8=20Add=20project=20options=20for?= =?UTF-8?q?=20MLIRQCOTransforms=20in=20CMakeLists.txt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt index 3564b55dc5..77f594db17 100644 --- a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt +++ b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt @@ -22,6 +22,8 @@ add_mlir_library( DEPENDS MLIRQCOTransformsIncGen) +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) From 0292b3eb863ae1bdbb34b9519b90fb24d0c24ce1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 19:10:32 +0000 Subject: [PATCH 11/13] =?UTF-8?q?=F0=9F=8E=A8=20pre-commit=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/lib/Compiler/CompilerPipeline.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mlir/lib/Compiler/CompilerPipeline.cpp b/mlir/lib/Compiler/CompilerPipeline.cpp index 5771c7db17..2d9846ca07 100644 --- a/mlir/lib/Compiler/CompilerPipeline.cpp +++ b/mlir/lib/Compiler/CompilerPipeline.cpp @@ -79,8 +79,9 @@ QuantumCompilerPipeline::runPipeline(ModuleOp module, } if (config_.enableDecomposeMultiControlled && config_.decomposeMultiControlledMinControls < 2) { - llvm::errs() << "decomposeMultiControlledMinControls must be at least 2 when " - "enableDecomposeMultiControlled is enabled.\n"; + llvm::errs() + << "decomposeMultiControlledMinControls must be at least 2 when " + "enableDecomposeMultiControlled is enabled.\n"; return failure(); } From b2f30eb28e51f4439c12ea8e131ecdc9952864a9 Mon Sep 17 00:00:00 2001 From: Simon Date: Wed, 24 Jun 2026 21:59:46 +0200 Subject: [PATCH 12/13] =?UTF-8?q?=E2=98=82=EF=B8=8F=20Increase=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{McxSynthesis.cpp => MultiControlled.cpp} | 7 +-- .../Compiler/test_compiler_pipeline.cpp | 16 +++++ .../test_multi_controlled_decomposition.cpp | 62 +++++++++++++++++++ 3 files changed, 80 insertions(+), 5 deletions(-) rename mlir/lib/Dialect/QCO/Transforms/Decomposition/{McxSynthesis.cpp => MultiControlled.cpp} (99%) diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/McxSynthesis.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/MultiControlled.cpp similarity index 99% rename from mlir/lib/Dialect/QCO/Transforms/Decomposition/McxSynthesis.cpp rename to mlir/lib/Dialect/QCO/Transforms/Decomposition/MultiControlled.cpp index b6ef17a8fe..6b8c962325 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/McxSynthesis.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/MultiControlled.cpp @@ -8,9 +8,10 @@ * Licensed under the MIT License */ -#include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/MultiControlled.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" + #include #include #include @@ -369,10 +370,6 @@ static void synthRelativeMcxNDirty(GateEmitter& builder, static void incrementDirty(GateEmitter& builder, std::size_t n, std::size_t numDirtyAncillae, bool flagAdd) { - if (numDirtyAncillae == 1 && n % 2 == 0) { - return; - } - const std::size_t k = numDirtyAncillae == 1 ? (n + 1) / 2 : (n + 2) / 2; const std::size_t ancilla1 = n; const std::size_t ancilla2 = n + 1; diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 00a890e96b..9860abb478 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -280,6 +280,22 @@ TEST_F(CompilerPipelineTest, DecomposeMultiControlledPassMcz) { EXPECT_NE(record.afterQCOCanon, record.afterOptimization); } +TEST_F(CompilerPipelineTest, + RejectsDecomposeMultiControlledMinControlsBelowTwo) { + auto module = mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::multipleControlledX); + ASSERT_TRUE(module); + + mlir::QuantumCompilerConfig config; + config.enableDecomposeMultiControlled = true; + config.decomposeMultiControlledMinControls = 1; + config.recordIntermediates = true; + + mlir::QuantumCompilerPipeline pipeline(config); + mlir::CompilationRecord record; + EXPECT_FALSE(pipeline.runPipeline(module.get(), &record).succeeded()); +} + INSTANTIATE_TEST_SUITE_P( QuantumComputationPipelineProgramsTest, CompilerPipelineTest, testing::Values( diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_multi_controlled_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_multi_controlled_decomposition.cpp index 517d6e7670..2713124495 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_multi_controlled_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_multi_controlled_decomposition.cpp @@ -16,6 +16,7 @@ #include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/MultiControlled.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/Utils/Utils.h" @@ -364,3 +365,64 @@ TEST_F(McxDecompositionTest, DecomposesMcxAndMcz) { EXPECT_EQ(countMultiControlledOps(moduleOp.get()), 0U); } + +TEST_F(McxDecompositionTest, PassFailsWhenMinControlsBelowTwo) { + auto moduleOp = buildMcxModule(context(), 3); + ASSERT_TRUE(moduleOp); + + DecomposeMultiControlledOptions options; + options.minControls = 1; + EXPECT_FALSE(runDecomposePass(moduleOp.get(), options).succeeded()); +} + +TEST_F(McxDecompositionTest, LeavesMultiOpCtrlUntouched) { + auto moduleOp = + QCOProgramBuilder::build(context(), [](QCOProgramBuilder& builder) { + const Value c0 = builder.staticQubit(0); + const Value c1 = builder.staticQubit(1); + const Value target = builder.staticQubit(2); + builder.ctrl({c0, c1}, {target}, + [&](ValueRange targets) -> SmallVector { + const Value afterX = builder.x(targets[0]); + return {builder.y(afterX)}; + }); + }); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(runDecomposePass(moduleOp.get()).succeeded()); + EXPECT_EQ(countMultiControlledOps(moduleOp.get()), 1U); +} + +TEST_F(McxDecompositionTest, LeavesMultiControlledHUntouched) { + auto moduleOp = + QCOProgramBuilder::build(context(), [](QCOProgramBuilder& builder) { + builder.mch({builder.staticQubit(0), builder.staticQubit(1)}, + builder.staticQubit(2)); + }); + ASSERT_TRUE(moduleOp); + ASSERT_TRUE(runDecomposePass(moduleOp.get()).succeeded()); + EXPECT_EQ(countMultiControlledOps(moduleOp.get()), 1U); +} + +TEST_F(McxDecompositionTest, SynthesizeMcxRequiresTwoControls) { + EXPECT_DEATH( + { + QCOProgramBuilder builder(context()); + builder.initialize(); + const Value q0 = builder.staticQubit(0); + decomposition::synthesizeMcx(builder, builder.getLoc(), ValueRange{q0}, + q0); + }, + "synthesizeMcx requires at least 2 control qubits"); +} + +TEST_F(McxDecompositionTest, SynthesizeMczRequiresTwoControls) { + EXPECT_DEATH( + { + QCOProgramBuilder builder(context()); + builder.initialize(); + const Value q0 = builder.staticQubit(0); + decomposition::synthesizeMcz(builder, builder.getLoc(), ValueRange{q0}, + q0); + }, + "synthesizeMcz requires at least 2 control qubits"); +} From 55176d1402a21c6f928f265d3536d4c33ca1ad4a Mon Sep 17 00:00:00 2001 From: Simon Date: Wed, 24 Jun 2026 22:47:49 +0200 Subject: [PATCH 13/13] =?UTF-8?q?=F0=9F=90=87=20Address=20Rabbit's=20comme?= =?UTF-8?q?nts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Compiler/test_compiler_pipeline.cpp | 5 +-- .../test_multi_controlled_decomposition.cpp | 34 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 9860abb478..5380b88e4b 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -262,7 +262,7 @@ TEST_F(CompilerPipelineTest, DecomposeMultiControlledPass) { ASSERT_TRUE(module); mlir::CompilationRecord record; - runPipeline(module.get(), false, false, false, true, record); + runPipeline(module.get(), false, true, false, true, record); // The outputs must differ, proving the pass ran and transformed the IR EXPECT_NE(record.afterQCOCanon, record.afterOptimization); @@ -274,7 +274,7 @@ TEST_F(CompilerPipelineTest, DecomposeMultiControlledPassMcz) { ASSERT_TRUE(module); mlir::CompilationRecord record; - runPipeline(module.get(), false, false, false, true, record); + runPipeline(module.get(), false, true, false, true, record); // The outputs must differ, proving the MCZ path ran and transformed the IR EXPECT_NE(record.afterQCOCanon, record.afterOptimization); @@ -294,6 +294,7 @@ TEST_F(CompilerPipelineTest, mlir::QuantumCompilerPipeline pipeline(config); mlir::CompilationRecord record; EXPECT_FALSE(pipeline.runPipeline(module.get(), &record).succeeded()); + EXPECT_TRUE(record.afterQCImport.empty()); } INSTANTIATE_TEST_SUITE_P( diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_multi_controlled_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_multi_controlled_decomposition.cpp index 2713124495..fe3cefa42b 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_multi_controlled_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_multi_controlled_decomposition.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include using namespace mlir; @@ -225,6 +226,22 @@ static void expectImplementsMcz(func::FuncOp funcOp, std::size_t numControls) { return count; } +[[nodiscard]] static std::optional +findSoleMultiControlledCtrlOp(ModuleOp moduleOp) { + CtrlOp found; + std::size_t count = 0; + moduleOp.walk([&](CtrlOp op) { + if (op.getNumControls() >= 2) { + found = op; + ++count; + } + }); + if (count != 1) { + return std::nullopt; + } + return found; +} + static LogicalResult runDecomposePass(ModuleOp moduleOp, const DecomposeMultiControlledOptions& options = {}) { @@ -389,6 +406,15 @@ TEST_F(McxDecompositionTest, LeavesMultiOpCtrlUntouched) { }); ASSERT_TRUE(moduleOp); ASSERT_TRUE(runDecomposePass(moduleOp.get()).succeeded()); + + const auto ctrlOpOpt = findSoleMultiControlledCtrlOp(moduleOp.get()); + ASSERT_TRUE(ctrlOpOpt.has_value()); + CtrlOp ctrlOp = *ctrlOpOpt; + EXPECT_EQ(ctrlOp.getNumControls(), 2U); + EXPECT_EQ(ctrlOp.getNumTargets(), 1U); + EXPECT_EQ(ctrlOp.getNumBodyUnitaries(), 2U); + EXPECT_TRUE(isa(ctrlOp.getBodyUnitary(0).getOperation())); + EXPECT_TRUE(isa(ctrlOp.getBodyUnitary(1).getOperation())); EXPECT_EQ(countMultiControlledOps(moduleOp.get()), 1U); } @@ -400,6 +426,14 @@ TEST_F(McxDecompositionTest, LeavesMultiControlledHUntouched) { }); ASSERT_TRUE(moduleOp); ASSERT_TRUE(runDecomposePass(moduleOp.get()).succeeded()); + + const auto ctrlOpOpt = findSoleMultiControlledCtrlOp(moduleOp.get()); + ASSERT_TRUE(ctrlOpOpt.has_value()); + CtrlOp ctrlOp = *ctrlOpOpt; + EXPECT_EQ(ctrlOp.getNumControls(), 2U); + EXPECT_EQ(ctrlOp.getNumTargets(), 1U); + ASSERT_EQ(ctrlOp.getNumBodyUnitaries(), 1U); + EXPECT_TRUE(isa(ctrlOp.getBodyUnitary(0).getOperation())); EXPECT_EQ(countMultiControlledOps(moduleOp.get()), 1U); }