diff --git a/CHANGELOG.md b/CHANGELOG.md index ef7be25d24..42bdae1dfe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +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 ([#1810]) ([**@simon1hofmann**]) - ✨ Add a `fuse-single-qubit-unitary-runs` pass for fusing compile-time single-qubit unitary runs via Euler resynthesis ([#1672]) ([**@simon1hofmann**], [**@burgholzer**]) @@ -598,6 +600,7 @@ changelogs._ +[#1810]: https://github.com/munich-quantum-toolkit/core/pull/1810 [#1808]: https://github.com/munich-quantum-toolkit/core/pull/1808 [#1807]: https://github.com/munich-quantum-toolkit/core/pull/1807 [#1806]: https://github.com/munich-quantum-toolkit/core/pull/1806 diff --git a/mlir/include/mlir/Compiler/CompilerPipeline.h b/mlir/include/mlir/Compiler/CompilerPipeline.h index 54d9f039de..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 { @@ -49,6 +50,13 @@ struct QuantumCompilerConfig { /// Enable Hadamard lifting bool enableHadamardLifting = false; + + /// 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 new file mode 100644 index 0000000000..45be0e7247 --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/MultiControlled.h @@ -0,0 +1,77 @@ +/* + * 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. + * (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 + * 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]`. + * + * @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 fafb906a77..c6a57831d3 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -200,4 +200,46 @@ 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` 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. 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 2.">]; +} + #endif // MLIR_DIALECT_QCO_TRANSFORMS_PASSES_TD diff --git a/mlir/lib/Compiler/CompilerPipeline.cpp b/mlir/lib/Compiler/CompilerPipeline.cpp index 821ccda2ee..2d9846ca07 100644 --- a/mlir/lib/Compiler/CompilerPipeline.cpp +++ b/mlir/lib/Compiler/CompilerPipeline.cpp @@ -77,6 +77,13 @@ 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()); @@ -147,6 +154,11 @@ QuantumCompilerPipeline::runPipeline(ModuleOp module, } // Stage 5: Optimization passes if (failed(runStage([&](PassManager& pm) { + if (config_.enableDecomposeMultiControlled) { + 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/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) 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..3634b06e37 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/DecomposeMultiControlled.cpp @@ -0,0 +1,108 @@ +/* + * 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 +#include +#include +#include + +#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; + +protected: + 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/MultiControlled.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/MultiControlled.cpp new file mode 100644 index 0000000000..6b8c962325 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/MultiControlled.cpp @@ -0,0 +1,541 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Dialect/QCO/Transforms/Decomposition/MultiControlled.h" + +#include "mlir/Dialect/QCO/IR/QCOOps.h" + +#include +#include +#include +#include +#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_; +}; + +} // namespace + +static void synthRelativeMcx(GateEmitter& builder, std::size_t numControls); + +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); + builder.tdg(q2); + builder.cx(q1, 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); + builder.tdg(q2); + builder.h(q2); +} + +static 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); + } + } + } +} + +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); +} + +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); +} + +static 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); +} + +static 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); +} + +static void incrementNDirty(GateEmitter& builder, std::size_t n) { + if (n <= 10) { + incrementNDirtySmall(builder, n); + } else { + incrementNDirtyLarge(builder, n); + } +} + +static 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); +} + +static void synthRelativeMcxNDirty(GateEmitter& builder, + std::size_t numControls) { + if (numControls < 11) { + synthRelativeMcx(builder, numControls); + } else { + synthMcxNDirtyI15(builder, numControls); + } +} + +static void incrementDirty(GateEmitter& builder, std::size_t n, + std::size_t numDirtyAncillae, bool flagAdd) { + 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); + } + } +} + +/// 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; + const std::size_t c0 = n - 2; + const std::size_t c1 = n - 1; + + SmallVector incrementQubits(n); + std::iota(incrementQubits.begin(), incrementQubits.end(), 0U); + + 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); + }); + 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); + } +} + +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; +} + +} // namespace mlir::qco::decomposition diff --git a/mlir/tools/mqt-cc/mqt-cc.cpp b/mlir/tools/mqt-cc/mqt-cc.cpp index d8fb75daa4..16657d7c4c 100644 --- a/mlir/tools/mqt-cc/mqt-cc.cpp +++ b/mlir/tools/mqt-cc/mqt-cc.cpp @@ -88,6 +88,18 @@ 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)); + +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 */ @@ -179,6 +191,9 @@ int main(int argc, char** argv) { config.disableMergeSingleQubitRotationGates = disableMergeSingleQubitRotationGates; config.enableHadamardLifting = enableHadamardLifting; + config.enableDecomposeMultiControlled = enableDecomposeMultiControlled; + config.decomposeMultiControlledMinControls = + decomposeMultiControlledMinControls.getValue(); // Run the compilation pipeline CompilationRecord record; diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index ddc3e4ce4d..5380b88e4b 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,12 +241,62 @@ 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, true, false, true, record); + + // The outputs must differ, proving the pass ran and transformed the IR + 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, true, false, true, record); + + // The outputs must differ, proving the MCZ path ran and transformed the IR + 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()); + EXPECT_TRUE(record.afterQCImport.empty()); +} + INSTANTIATE_TEST_SUITE_P( QuantumComputationPipelineProgramsTest, CompilerPipelineTest, testing::Values( diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt index f493bb9e4d..1046fee5c0 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt @@ -7,10 +7,10 @@ # 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..fe3cefa42b --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_multi_controlled_decomposition.cpp @@ -0,0 +1,462 @@ +/* + * 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/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" +#include "mlir/Dialect/QCO/Transforms/Decomposition/MultiControlled.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 +#include +#include +#include +#include +#include +#include + +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; 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, +}; + +namespace { + +enum class ControlledPauli : std::uint8_t { X, Z }; + +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]] static 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]] static OwningOpRef +buildMcxModule(MLIRContext* context, std::size_t numControls) { + return buildControlledPauliModule(context, numControls, ControlledPauli::X); +} + +[[nodiscard]] static OwningOpRef +buildMczModule(MLIRContext* context, std::size_t numControls) { + return buildControlledPauliModule(context, numControls, ControlledPauli::Z); +} + +[[nodiscard]] static 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; +} + +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); + + 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)); + } + 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); + EXPECT_TRUE(dd->multiply(decomposedDD, dd->conjugateTranspose(referenceDD)) + .isIdentity(/*upToGlobalPhase=*/false)); +} + +static void expectImplementsMcx(func::FuncOp funcOp, std::size_t numControls) { + expectImplementsControlledPauli(funcOp, numControls, ControlledPauli::X); +} + +static void expectImplementsMcz(func::FuncOp funcOp, std::size_t numControls) { + expectImplementsControlledPauli(funcOp, numControls, ControlledPauli::Z); +} + +[[nodiscard]] static std::size_t countMultiControlledOps(ModuleOp moduleOp) { + std::size_t count = 0; + moduleOp.walk([&count](CtrlOp op) { + if (op.getNumControls() >= 2) { + ++count; + } + }); + 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 = {}) { + PassManager pm(moduleOp.getContext()); + pm.addPass(createDecomposeMultiControlled(options)); + return pm.run(moduleOp); +} + +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); + }); + +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); + }); + +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_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) { + builder.cx(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, 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) { + 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, 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), + 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()), 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()); + + 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); +} + +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()); + + 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); +} + +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"); +} 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"