diff --git a/CHANGELOG.md b/CHANGELOG.md index 03500c28a9..39a3239157 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ This project adheres to [Semantic Versioning], with the exception that minor rel ### Added -- ✨ Add initial infrastructure for new QC and QCO MLIR dialects ([#1264], [#1402], [#1428], [#1430], [#1436], [#1443], [#1446], [#1464], [#1465], [#1470], [#1471], [#1472], [#1475]) ([**@burgholzer**], [**@denialhaag**], [**@taminob**], [**@DRovara**], [**@li-mingbao**]) +- ✨ Add initial infrastructure for new QC and QCO MLIR dialects ([#1264], [#1402], [#1426], [#1428], [#1430], [#1436], [#1443], [#1446], [#1464], [#1465], [#1470], [#1471], [#1472], [#1475]) ([**@burgholzer**], [**@denialhaag**], [**@taminob**], [**@DRovara**], [**@li-mingbao**]) ### Changed @@ -332,6 +332,7 @@ _📚 Refer to the [GitHub Release Notes](https://github.com/munich-quantum-tool [#1436]: https://github.com/munich-quantum-toolkit/core/pull/1436 [#1430]: https://github.com/munich-quantum-toolkit/core/pull/1430 [#1428]: https://github.com/munich-quantum-toolkit/core/pull/1428 +[#1426]: https://github.com/munich-quantum-toolkit/core/pull/1426 [#1415]: https://github.com/munich-quantum-toolkit/core/pull/1415 [#1414]: https://github.com/munich-quantum-toolkit/core/pull/1414 [#1413]: https://github.com/munich-quantum-toolkit/core/pull/1413 diff --git a/mlir/include/mlir/CMakeLists.txt b/mlir/include/mlir/CMakeLists.txt index 272dfcf570..7d688914a2 100644 --- a/mlir/include/mlir/CMakeLists.txt +++ b/mlir/include/mlir/CMakeLists.txt @@ -6,5 +6,6 @@ # # Licensed under the MIT License -add_subdirectory(Dialect) add_subdirectory(Conversion) +add_subdirectory(Dialect) +add_subdirectory(Passes) diff --git a/mlir/include/mlir/Compiler/CompilerPipeline.h b/mlir/include/mlir/Compiler/CompilerPipeline.h index e831ad907c..14bcdfe1fb 100644 --- a/mlir/include/mlir/Compiler/CompilerPipeline.h +++ b/mlir/include/mlir/Compiler/CompilerPipeline.h @@ -119,6 +119,11 @@ class QuantumCompilerPipeline { */ static void addCleanupPasses(PassManager& pm); + /** + * @brief Add all available optimization passes + */ + static void addOptimizationPasses(PassManager& pm); + /** * @brief Configure PassManager with diagnostic options * diff --git a/mlir/include/mlir/Passes/CMakeLists.txt b/mlir/include/mlir/Passes/CMakeLists.txt new file mode 100644 index 0000000000..9d11c5ca00 --- /dev/null +++ b/mlir/include/mlir/Passes/CMakeLists.txt @@ -0,0 +1,12 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +set(LLVM_TARGET_DEFINITIONS Passes.td) +mlir_tablegen(Passes.h.inc -gen-pass-decls -name QCO) +add_public_tablegen_target(QcoPassesIncGen) +add_mlir_doc(Passes QcoPasses Passes/ -gen-pass-doc) diff --git a/mlir/include/mlir/Passes/Decomposition/BasisDecomposer.h b/mlir/include/mlir/Passes/Decomposition/BasisDecomposer.h new file mode 100644 index 0000000000..b6500d9365 --- /dev/null +++ b/mlir/include/mlir/Passes/Decomposition/BasisDecomposer.h @@ -0,0 +1,547 @@ +/* + * 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 "EulerBasis.h" +#include "EulerDecomposition.h" +#include "GateSequence.h" +#include "Helpers.h" +#include "UnitaryMatrices.h" +#include "WeylDecomposition.h" +#include "ir/Definitions.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mlir::qco::decomposition { + +/** + * Decomposer that must be initialized with a two-qubit basis gate that will + * be used to generate a circuit equivalent to a canonical gate (RXX+RYY+RZZ). + */ +class TwoQubitBasisDecomposer { +public: + /** + * Create decomposer that allows two-qubit decompositions based on the + * specified basis gate. + * This basis gate will appear between 0 and 3 times in each decompositions. + * The order of qubits is relevant and will change the results accordingly. + * The decomposer cannot handle different basis gates in the same + * decomposition (different order of the qubits also counts as a different + * basis gate). + */ + static TwoQubitBasisDecomposer create(const Gate& basisGate, + fp basisFidelity) { + auto relativeEq = [](auto&& lhs, auto&& rhs, auto&& epsilon, + auto&& maxRelative) { + // Handle same infinities + if (lhs == rhs) { + return true; + } + + // Handle remaining infinities + if (std::isinf(lhs) || std::isinf(rhs)) { + return false; + } + + auto absDiff = std::abs(lhs - rhs); + + // For when the numbers are really close together + if (absDiff <= epsilon) { + return true; + } + + auto absLhs = std::abs(lhs); + auto absRhs = std::abs(rhs); + if (absRhs > absLhs) { + return absDiff <= absRhs * maxRelative; + } + return absDiff <= absLhs * maxRelative; + }; + const matrix2x2 k12RArr{ + {qfp(0., FRAC1_SQRT2), qfp(FRAC1_SQRT2, 0.)}, + {qfp(-FRAC1_SQRT2, 0.), qfp(0., -FRAC1_SQRT2)}, + }; + const matrix2x2 k12LArr{ + {qfp(0.5, 0.5), qfp(0.5, 0.5)}, + {qfp(-0.5, 0.5), qfp(0.5, -0.5)}, + }; + + const auto basisDecomposer = + decomposition::TwoQubitWeylDecomposition::create( + getTwoQubitMatrix(basisGate), basisFidelity); + const auto isSuperControlled = + relativeEq(basisDecomposer.a, qc::PI_4, 1e-13, 1e-09) && + relativeEq(basisDecomposer.c, 0.0, 1e-13, 1e-09); + + // Create some useful matrices U1, U2, U3 are equivalent to the basis, + // expand as Ui = Ki1.Ubasis.Ki2 + auto b = basisDecomposer.b; + auto temp = qfp(0.5, -0.5); + const matrix2x2 k11l{ + {temp * (M_IM * std::exp(qfp(0., -b))), temp * std::exp(qfp(0., -b))}, + {temp * (M_IM * std::exp(qfp(0., b))), temp * -std::exp(qfp(0., b))}}; + const matrix2x2 k11r{{FRAC1_SQRT2 * (IM * std::exp(qfp(0., -b))), + FRAC1_SQRT2 * -std::exp(qfp(0., -b))}, + {FRAC1_SQRT2 * std::exp(qfp(0., b)), + FRAC1_SQRT2 * (M_IM * std::exp(qfp(0., b)))}}; + const matrix2x2 k32lK21l{{FRAC1_SQRT2 * qfp(1., std::cos(2. * b)), + FRAC1_SQRT2 * (IM * std::sin(2. * b))}, + {FRAC1_SQRT2 * (IM * std::sin(2. * b)), + FRAC1_SQRT2 * qfp(1., -std::cos(2. * b))}}; + temp = qfp(0.5, 0.5); + const matrix2x2 k21r{ + {temp * (M_IM * std::exp(qfp(0., -2. * b))), + temp * std::exp(qfp(0., -2. * b))}, + {temp * (IM * std::exp(qfp(0., 2. * b))), + temp * std::exp(qfp(0., 2. * b))}, + }; + const matrix2x2 k22l{ + {qfp(FRAC1_SQRT2, 0.), qfp(-FRAC1_SQRT2, 0.)}, + {qfp(FRAC1_SQRT2, 0.), qfp(FRAC1_SQRT2, 0.)}, + }; + const matrix2x2 k22r{{C_ZERO, C_ONE}, {C_M_ONE, C_ZERO}}; + const matrix2x2 k31l{ + {FRAC1_SQRT2 * std::exp(qfp(0., -b)), + FRAC1_SQRT2 * std::exp(qfp(0., -b))}, + {FRAC1_SQRT2 * -std::exp(qfp(0., b)), + FRAC1_SQRT2 * std::exp(qfp(0., b))}, + }; + const matrix2x2 k31r{ + {IM * std::exp(qfp(0., b)), C_ZERO}, + {C_ZERO, M_IM * std::exp(qfp(0., -b))}, + }; + temp = qfp(0.5, 0.5); + const matrix2x2 k32r{ + {temp * std::exp(qfp(0., b)), temp * -std::exp(qfp(0., -b))}, + {temp * (M_IM * std::exp(qfp(0., b))), + temp * (M_IM * std::exp(qfp(0., -b)))}, + }; + auto k1lDagger = basisDecomposer.k1l.transpose().conjugate(); + auto k1rDagger = basisDecomposer.k1r.transpose().conjugate(); + auto k2lDagger = basisDecomposer.k2l.transpose().conjugate(); + auto k2rDagger = basisDecomposer.k2r.transpose().conjugate(); + // Pre-build the fixed parts of the matrices used in 3-part + // decomposition + auto u0l = k31l * k1lDagger; + auto u0r = k31r * k1rDagger; + auto u1l = k2lDagger * k32lK21l * k1lDagger; + auto u1ra = k2rDagger * k32r; + auto u1rb = k21r * k1rDagger; + auto u2la = k2lDagger * k22l; + auto u2lb = k11l * k1lDagger; + auto u2ra = k2rDagger * k22r; + auto u2rb = k11r * k1rDagger; + auto u3l = k2lDagger * k12LArr; + auto u3r = k2rDagger * k12RArr; + // Pre-build the fixed parts of the matrices used in the 2-part + // decomposition + auto q0l = k12LArr.transpose().conjugate() * k1lDagger; + auto q0r = k12RArr.transpose().conjugate() * IPZ * k1rDagger; + auto q1la = k2lDagger * k11l.transpose().conjugate(); + auto q1lb = k11l * k1lDagger; + auto q1ra = k2rDagger * IPZ * k11r.transpose().conjugate(); + auto q1rb = k11r * k1rDagger; + auto q2l = k2lDagger * k12LArr; + auto q2r = k2rDagger * k12RArr; + + return TwoQubitBasisDecomposer{ + basisGate, + basisFidelity, + basisDecomposer, + isSuperControlled, + u0l, + u0r, + u1l, + u1ra, + u1rb, + u2la, + u2lb, + u2ra, + u2rb, + u3l, + u3r, + q0l, + q0r, + q1la, + q1lb, + q1ra, + q1rb, + q2l, + q2r, + }; + } + + /** + * Perform decomposition using the basis gate of this decomposer. + * + * @param targetDecomposition Prepared Weyl decomposition of unitary matrix + * to be decomposed. + * @param target1qEulerBases List of euler bases that should be tried out to + * find the best one for each euler decomposition. + * All bases will be mixed to get the best overall + * result. + * @param basisFidelity Fidelity for lowering the number of basis gates + * required + * @param approximate If true, use basisFidelity or, if std::nullopt, use + * basisFidelity of this decomposer. If false, fidelity + * of 1.0 will be assumed. + * @param numBasisGateUses Force use of given number of basis gates. + */ + [[nodiscard]] std::optional twoQubitDecompose( + const decomposition::TwoQubitWeylDecomposition& targetDecomposition, + const llvm::SmallVector& target1qEulerBases, + std::optional basisFidelity, bool approximate, + std::optional numBasisGateUses) const { + auto getBasisFidelity = [&]() { + if (approximate) { + return basisFidelity.value_or(this->basisFidelity); + } + return static_cast(1.0); + }; + fp actualBasisFidelity = getBasisFidelity(); + auto traces = this->traces(targetDecomposition); + auto getDefaultNbasis = [&]() { + auto minValue = std::numeric_limits::min(); + auto minIndex = -1; + for (int i = 0; std::cmp_less(i, traces.size()); ++i) { + // lower fidelity means it becomes easier to choose a lower number of + // basis gates + auto value = helpers::traceToFidelity(traces[i]) * + std::pow(actualBasisFidelity, i); + if (value > minValue) { + minIndex = i; + minValue = value; + } + } + return minIndex; + }; + // number of basis gates that need to be used in the decomposition + auto bestNbasis = numBasisGateUses.value_or(getDefaultNbasis()); + auto chooseDecomposition = [&]() { + if (bestNbasis == 0) { + return decomp0(targetDecomposition); + } + if (bestNbasis == 1) { + return decomp1(targetDecomposition); + } + if (bestNbasis == 2) { + return decomp2Supercontrolled(targetDecomposition); + } + if (bestNbasis == 3) { + return decomp3Supercontrolled(targetDecomposition); + } + throw std::logic_error{"Invalid basis to use"}; + }; + auto decomposition = chooseDecomposition(); + llvm::SmallVector, 8> + eulerDecompositions; + for (auto&& decomp : decomposition) { + assert(helpers::isUnitaryMatrix(decomp)); + auto eulerDecomp = unitaryToGateSequenceInner(decomp, target1qEulerBases, + 0, true, std::nullopt); + eulerDecompositions.push_back(eulerDecomp); + } + TwoQubitGateSequence gates{ + .gates = {}, + .globalPhase = targetDecomposition.globalPhase, + }; + // Worst case length is 5x 1q gates for each 1q decomposition + 1x 2q + // gate We might overallocate a bit if the euler basis is different but + // the worst case is just 16 extra elements with just a String and 2 + // smallvecs each. This is only transient though as the circuit + // sequences aren't long lived and are just used to create a + // QuantumCircuit or DAGCircuit when we return to Python space. + constexpr auto twoQubitSequenceDefaultCapacity = 21; + gates.gates.reserve(twoQubitSequenceDefaultCapacity); + gates.globalPhase -= bestNbasis * basisDecomposer.globalPhase; + if (bestNbasis == 2) { + gates.globalPhase += qc::PI; + } + + auto addEulerDecomposition = [&](std::size_t index, QubitId qubitId) { + if (auto&& eulerDecomp = eulerDecompositions[index]) { + for (auto&& gate : eulerDecomp->gates) { + gates.gates.push_back({.type = gate.type, + .parameter = gate.parameter, + .qubitId = {qubitId}}); + } + gates.globalPhase += eulerDecomp->globalPhase; + } + }; + + for (std::size_t i = 0; i < bestNbasis; ++i) { + // add single-qubit decompositions before basis gate + addEulerDecomposition(2 * i, 0); + addEulerDecomposition((2 * i) + 1, 1); + + // add basis gate + gates.gates.push_back(basisGate); + } + + // add single-qubit decompositions after basis gate + addEulerDecomposition(2UL * bestNbasis, 0); + addEulerDecomposition((2UL * bestNbasis) + 1, 1); + + // large global phases can be generated by the decomposition, thus limit + // it to [0, +2*pi); TODO: can be removed, should be done by something + // like constant folding + gates.globalPhase = helpers::remEuclid(gates.globalPhase, qc::TAU); + + return gates; + } + +protected: + // NOLINTBEGIN(modernize-pass-by-value) + /** + * Constructs decomposer instance. + */ + TwoQubitBasisDecomposer( + Gate basisGate, fp basisFidelity, + const decomposition::TwoQubitWeylDecomposition& basisDecomposer, + bool isSuperControlled, const matrix2x2& u0l, const matrix2x2& u0r, + const matrix2x2& u1l, const matrix2x2& u1ra, const matrix2x2& u1rb, + const matrix2x2& u2la, const matrix2x2& u2lb, const matrix2x2& u2ra, + const matrix2x2& u2rb, const matrix2x2& u3l, const matrix2x2& u3r, + const matrix2x2& q0l, const matrix2x2& q0r, const matrix2x2& q1la, + const matrix2x2& q1lb, const matrix2x2& q1ra, const matrix2x2& q1rb, + const matrix2x2& q2l, const matrix2x2& q2r) + : basisGate{std::move(basisGate)}, basisFidelity{basisFidelity}, + basisDecomposer{basisDecomposer}, isSuperControlled{isSuperControlled}, + u0l{u0l}, u0r{u0r}, u1l{u1l}, u1ra{u1ra}, u1rb{u1rb}, u2la{u2la}, + u2lb{u2lb}, u2ra{u2ra}, u2rb{u2rb}, u3l{u3l}, u3r{u3r}, q0l{q0l}, + q0r{q0r}, q1la{q1la}, q1lb{q1lb}, q1ra{q1ra}, q1rb{q1rb}, q2l{q2l}, + q2r{q2r} {} + // NOLINTEND(modernize-pass-by-value) + + /** + * Calculate decompositions when no basis gate is required. + * + * Decompose target :math:`\sim U_d(x, y, z)` with 0 uses of the + * basis gate. Result :math:`U_r` has trace: + * + * .. math:: + * + * \Big\vert\text{Tr}(U_r\cdot U_\text{target}^{\dag})\Big\vert = + * 4\Big\vert (\cos(x)\cos(y)\cos(z)+ j \sin(x)\sin(y)\sin(z)\Big\vert + * + * which is optimal for all targets and bases + */ + [[nodiscard]] static llvm::SmallVector + decomp0(const decomposition::TwoQubitWeylDecomposition& target) { + return { + target.k1r * target.k2r, + target.k1l * target.k2l, + }; + } + + /** + * Calculate decompositions when one basis gate is required. + * + * Decompose target :math:`\sim U_d(x, y, z)` with 1 use of the + * basis gate math:`\sim U_d(a, b, c)`. Result :math:`U_r` has trace: + * + * .. math:: + * + * \Big\vert\text{Tr}(U_r \cdot U_\text{target}^{\dag})\Big\vert = + * 4\Big\vert \cos(x-a)\cos(y-b)\cos(z-c) + j + * \sin(x-a)\sin(y-b)\sin(z-c)\Big\vert + * + * which is optimal for all targets and bases with ``z==0`` or ``c==0``. + */ + [[nodiscard]] llvm::SmallVector + decomp1(const decomposition::TwoQubitWeylDecomposition& target) const { + // may not work for z != 0 and c != 0 (not always in Weyl chamber) + return { + basisDecomposer.k2r.transpose().conjugate() * target.k2r, + basisDecomposer.k2l.transpose().conjugate() * target.k2l, + target.k1r * basisDecomposer.k1r.transpose().conjugate(), + target.k1l * basisDecomposer.k1l.transpose().conjugate(), + }; + } + + /** + * Calculate decompositions when two basis gates are required. + * + * Decompose target :math:`\sim U_d(x, y, z)` with 2 uses of the + * basis gate. + * + * For supercontrolled basis :math:`\sim U_d(\pi/4, b, 0)`, all b, result + * :math:`U_r` has trace + * + * .. math:: + * + * \Big\vert\text{Tr}(U_r \cdot U_\text{target}^\dag) \Big\vert = + * 4\cos(z) + * + * which is the optimal approximation for basis of CNOT-class + * :math:`\sim U_d(\pi/4, 0, 0)` or DCNOT-class + * :math:`\sim U_d(\pi/4, \pi/4, 0)` and any target. It may be sub-optimal + * for :math:`b \neq 0` (i.e. there exists an exact decomposition for any + * target using :math:`B \sim U_d(\pi/4, \pi/8, 0)`, but it may not be this + * decomposition). This is an exact decomposition for supercontrolled basis + * and target :math:`\sim U_d(x, y, 0)`. No guarantees for + * non-supercontrolled basis. + */ + [[nodiscard]] llvm::SmallVector decomp2Supercontrolled( + const decomposition::TwoQubitWeylDecomposition& target) const { + if (!isSuperControlled) { + // TODO: make fatal error? check in constructor? + llvm::errs() + << "Basis gate of TwoQubitBasisDecomposer is not super-controlled " + "- no guarantee for exact decomposition with two basis gates\n"; + } + return { + q2r * target.k2r, + q2l * target.k2l, + q1ra * rzMatrix(2. * target.b) * q1rb, + q1la * rzMatrix(-2. * target.a) * q1lb, + target.k1r * q0r, + target.k1l * q0l, + }; + } + + /** + * Calculate decompositions when three basis gates are required. + * + * Decompose target with 3 uses of the basis. + * + * This is an exact decomposition for supercontrolled basis + * :math:`\sim U_d(\pi/4, b, 0)`, all b, and any target. No guarantees for + * non-supercontrolled basis. + */ + [[nodiscard]] llvm::SmallVector decomp3Supercontrolled( + const decomposition::TwoQubitWeylDecomposition& target) const { + if (!isSuperControlled) { + llvm::errs() + << "Basis gate of TwoQubitBasisDecomposer is not super-controlled " + "- no guarantee for exact decomposition with " + "three basis gates\n"; + } + return { + u3r * target.k2r, + u3l * target.k2l, + u2ra * rzMatrix(2. * target.b) * u2rb, + u2la * rzMatrix(-2. * target.a) * u2lb, + u1ra * rzMatrix(-2. * target.c) * u1rb, + u1l, + target.k1r * u0r, + target.k1l * u0l, + }; + } + + /** + * Calculate traces for a combination of the parameters of the canonical + * gates of the target and basis decompositions. + * This can be used to determine the smallest number of basis gates that are + * necessary to construct an equivalent to the canonical gate. + */ + [[nodiscard]] std::array + traces(const decomposition::TwoQubitWeylDecomposition& target) const { + return { + static_cast(4.) * + qfp(std::cos(target.a) * std::cos(target.b) * std::cos(target.c), + std::sin(target.a) * std::sin(target.b) * std::sin(target.c)), + static_cast(4.) * + qfp(std::cos(qc::PI_4 - target.a) * + std::cos(basisDecomposer.b - target.b) * std::cos(target.c), + std::sin(qc::PI_4 - target.a) * + std::sin(basisDecomposer.b - target.b) * + std::sin(target.c)), + qfp(4. * std::cos(target.c), 0.), + qfp(4., 0.), + }; + } + + /** + * Decompose a single-qubit unitary matrix into a single-qubit gate + * sequence. Multiple euler bases may be specified and the one with the + * least complexity will be chosen. + */ + [[nodiscard]] static OneQubitGateSequence unitaryToGateSequenceInner( + const matrix2x2& unitaryMat, + const llvm::SmallVector& targetBasisList, QubitId /*qubit*/, + // TODO: add error map here: per qubit a mapping of operation to error + // value for better calculateError() + bool simplify, std::optional atol) { + auto calculateError = [](const OneQubitGateSequence& sequence) -> fp { + return static_cast(sequence.complexity()); + }; + + auto minError = std::numeric_limits::max(); + OneQubitGateSequence bestCircuit; + for (auto targetBasis : targetBasisList) { + auto circuit = EulerDecomposition::generateCircuit( + targetBasis, unitaryMat, simplify, atol); + assert(circuit.getUnitaryMatrix().isApprox( + helpers::kroneckerProduct(IDENTITY_GATE, unitaryMat), + SANITY_CHECK_PRECISION)); + auto error = calculateError(circuit); + if (error < minError) { + bestCircuit = circuit; + minError = error; + } + } + return bestCircuit; + } + +private: + // basis gate of this decomposer instance + Gate basisGate{}; + // fidelity with which the basis gate decomposition has been calculated + fp basisFidelity; + // cached decomposition for basis gate + decomposition::TwoQubitWeylDecomposition basisDecomposer; + // true if basis gate is super-controlled + bool isSuperControlled; + + // pre-built components for decomposition with 3 basis gates + matrix2x2 u0l; + matrix2x2 u0r; + matrix2x2 u1l; + matrix2x2 u1ra; + matrix2x2 u1rb; + matrix2x2 u2la; + matrix2x2 u2lb; + matrix2x2 u2ra; + matrix2x2 u2rb; + matrix2x2 u3l; + matrix2x2 u3r; + + // pre-built components for decomposition with 2 basis gates + matrix2x2 q0l; + matrix2x2 q0r; + matrix2x2 q1la; + matrix2x2 q1lb; + matrix2x2 q1ra; + matrix2x2 q1rb; + matrix2x2 q2l; + matrix2x2 q2r; +}; + +} // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Passes/Decomposition/EulerBasis.h b/mlir/include/mlir/Passes/Decomposition/EulerBasis.h new file mode 100644 index 0000000000..63f3aa9c3a --- /dev/null +++ b/mlir/include/mlir/Passes/Decomposition/EulerBasis.h @@ -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 + */ + +#pragma once + +#include "ir/operations/OpType.hpp" + +#include +#include + +namespace mlir::qco::decomposition { +/** + * Largest number that will be assumed as zero for the euler decompositions. + */ +static constexpr auto DEFAULT_ATOL = 1e-12; + +/** + * EulerBasis for a euler decomposition. + * + * @note only the following bases are supported for now: ZYZ, ZXZ and XZX + */ +enum class EulerBasis : std::uint8_t { + U3 = 0, + U321 = 1, + U = 2, + PSX = 3, + U1X = 4, + RR = 5, + ZYZ = 6, + ZXZ = 7, + XZX = 8, + XYX = 9, + ZSXX = 10, + ZSX = 11, +}; + +[[nodiscard]] inline llvm::SmallVector +getGateTypesForEulerBasis(EulerBasis eulerBasis) { + switch (eulerBasis) { + case EulerBasis::ZYZ: + return {qc::RZ, qc::RY}; + case EulerBasis::ZXZ: + return {qc::RZ, qc::RX}; + case EulerBasis::XZX: + return {qc::RX, qc::RZ}; + case EulerBasis::XYX: + return {qc::RX, qc::RY}; + case EulerBasis::U3: + [[fallthrough]]; + case EulerBasis::U321: + [[fallthrough]]; + case EulerBasis::U: + return {qc::U}; + case EulerBasis::RR: + return {qc::R}; + case EulerBasis::ZSXX: + [[fallthrough]]; + case EulerBasis::ZSX: + return {qc::RZ, qc::X}; + case EulerBasis::PSX: + [[fallthrough]]; + case EulerBasis::U1X: + break; + } + throw std::invalid_argument{ + "Unsupported euler basis for translation to gate types"}; +} + +} // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Passes/Decomposition/EulerDecomposition.h b/mlir/include/mlir/Passes/Decomposition/EulerDecomposition.h new file mode 100644 index 0000000000..8db67d6571 --- /dev/null +++ b/mlir/include/mlir/Passes/Decomposition/EulerDecomposition.h @@ -0,0 +1,213 @@ +/* + * 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 "EulerBasis.h" +#include "GateSequence.h" +#include "Helpers.h" +#include "ir/Definitions.hpp" +#include "ir/operations/OpType.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mlir::qco::decomposition { + +/** + * Decomposition of single-qubit matrices into rotation gates using a KAK + * decomposition. + */ +class EulerDecomposition { +public: + /** + * Perform single-qubit decomposition of a 2x2 unitary matrix based on a + * given euler basis. + */ + [[nodiscard]] static OneQubitGateSequence + generateCircuit(EulerBasis targetBasis, const matrix2x2& unitaryMatrix, + bool simplify, std::optional atol) { + auto [theta, phi, lambda, phase] = + anglesFromUnitary(unitaryMatrix, targetBasis); + + switch (targetBasis) { + case EulerBasis::ZYZ: + return decomposeKAK(theta, phi, lambda, phase, qc::RZ, qc::RY, simplify, + atol); + case EulerBasis::ZXZ: + return decomposeKAK(theta, phi, lambda, phase, qc::RZ, qc::RX, simplify, + atol); + case EulerBasis::XZX: + return decomposeKAK(theta, phi, lambda, phase, qc::RX, qc::RZ, simplify, + atol); + case EulerBasis::XYX: + return decomposeKAK(theta, phi, lambda, phase, qc::RX, qc::RY, simplify, + atol); + default: + // TODO: allow other bases + throw std::invalid_argument{"Unsupported base for circuit generation!"}; + } + } + + /** + * Calculate angles of a single-qubit matrix according to the given + * EulerBasis. + * + * @return array containing (theta, phi, lambda, phase) in this order + */ + static std::array anglesFromUnitary(const matrix2x2& matrix, + EulerBasis basis) { + if (basis == EulerBasis::XYX) { + return paramsXyxInner(matrix); + } + if (basis == EulerBasis::XZX) { + return paramsXzxInner(matrix); + } + if (basis == EulerBasis::ZYZ) { + return paramsZyzInner(matrix); + } + if (basis == EulerBasis::ZXZ) { + return paramsZxzInner(matrix); + } + throw std::invalid_argument{"Unknown EulerBasis for angles_from_unitary"}; + } + +private: + static std::array paramsZyzInner(const matrix2x2& matrix) { + const auto detArg = std::arg(matrix.determinant()); + const auto phase = 0.5 * detArg; + const auto theta = + 2. * std::atan2(std::abs(matrix(1, 0)), std::abs(matrix(0, 0))); + const auto ang1 = std::arg(matrix(1, 1)); + const auto ang2 = std::arg(matrix(1, 0)); + const auto phi = ang1 + ang2 - detArg; + const auto lam = ang1 - ang2; + return {theta, phi, lam, phase}; + } + + static std::array paramsZxzInner(const matrix2x2& matrix) { + const auto [theta, phi, lam, phase] = paramsZyzInner(matrix); + return {theta, phi + (qc::PI / 2.), lam - (qc::PI / 2.), phase}; + } + + static std::array paramsXyxInner(const matrix2x2& matrix) { + const matrix2x2 matZyz{ + {static_cast(0.5) * + (matrix(0, 0) + matrix(0, 1) + matrix(1, 0) + matrix(1, 1)), + static_cast(0.5) * + (matrix(0, 0) - matrix(0, 1) + matrix(1, 0) - matrix(1, 1))}, + {static_cast(0.5) * + (matrix(0, 0) + matrix(0, 1) - matrix(1, 0) - matrix(1, 1)), + static_cast(0.5) * + (matrix(0, 0) - matrix(0, 1) - matrix(1, 0) + matrix(1, 1))}, + }; + auto [theta, phi, lam, phase] = paramsZyzInner(matZyz); + auto newPhi = helpers::mod2pi(phi + qc::PI, 0.); + auto newLam = helpers::mod2pi(lam + qc::PI, 0.); + return { + theta, + newPhi, + newLam, + phase + ((newPhi + newLam - phi - lam) / 2.), + }; + } + + static std::array paramsXzxInner(const matrix2x2& matrix) { + auto det = matrix.determinant(); + auto phase = std::imag(std::log(det)) / 2.0; + auto sqrtDet = std::sqrt(det); + const matrix2x2 matZyz{ + { + {(matrix(0, 0) / sqrtDet).real(), (matrix(1, 0) / sqrtDet).imag()}, + {(matrix(1, 0) / sqrtDet).real(), (matrix(0, 0) / sqrtDet).imag()}, + }, + { + {-(matrix(1, 0) / sqrtDet).real(), (matrix(0, 0) / sqrtDet).imag()}, + {(matrix(0, 0) / sqrtDet).real(), -(matrix(1, 0) / sqrtDet).imag()}, + }, + }; + auto [theta, phi, lam, phase_zxz] = paramsZxzInner(matZyz); + return {theta, phi, lam, phase + phase_zxz}; + } + + /** + * @note Adapted from circuit_kak() in the IBM Qiskit framework. + * (C) Copyright IBM 2022 + * + * 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 + * http://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. + */ + [[nodiscard]] static OneQubitGateSequence + decomposeKAK(fp theta, fp phi, fp lambda, fp phase, qc::OpType kGate, + qc::OpType aGate, bool simplify, std::optional atol) { + fp angleZeroEpsilon = atol.value_or(DEFAULT_ATOL); + if (!simplify) { + angleZeroEpsilon = -1.0; + } + + OneQubitGateSequence sequence{ + .gates = {}, + .globalPhase = phase - ((phi + lambda) / 2.), + }; + if (std::abs(theta) <= angleZeroEpsilon) { + lambda += phi; + lambda = helpers::mod2pi(lambda); + if (std::abs(lambda) > angleZeroEpsilon) { + sequence.gates.push_back({.type = kGate, .parameter = {lambda}}); + sequence.globalPhase += lambda / 2.0; + } + return sequence; + } + + if (std::abs(theta - qc::PI) <= angleZeroEpsilon) { + sequence.globalPhase += phi; + lambda -= phi; + phi = 0.0; + } + if (std::abs(helpers::mod2pi(lambda + qc::PI)) <= angleZeroEpsilon || + std::abs(helpers::mod2pi(phi + qc::PI)) <= angleZeroEpsilon) { + lambda += qc::PI; + theta = -theta; + phi += qc::PI; + } + lambda = helpers::mod2pi(lambda); + if (std::abs(lambda) > angleZeroEpsilon) { + sequence.globalPhase += lambda / 2.0; + sequence.gates.push_back({.type = kGate, .parameter = {lambda}}); + } + sequence.gates.push_back({.type = aGate, .parameter = {theta}}); + phi = helpers::mod2pi(phi); + if (std::abs(phi) > angleZeroEpsilon) { + sequence.globalPhase += phi / 2.0; + sequence.gates.push_back({.type = kGate, .parameter = {phi}}); + } + return sequence; + } +}; +} // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Passes/Decomposition/Gate.h b/mlir/include/mlir/Passes/Decomposition/Gate.h new file mode 100644 index 0000000000..df0fabde3d --- /dev/null +++ b/mlir/include/mlir/Passes/Decomposition/Gate.h @@ -0,0 +1,32 @@ +/* + * 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 "Helpers.h" +#include "ir/operations/OpType.hpp" + +#include + +namespace mlir::qco::decomposition { + +using QubitId = std::size_t; + +/** + * Gate description which should be able to represent every possible + * one-qubit or two-qubit operation. + */ +struct Gate { + qc::OpType type{qc::I}; + llvm::SmallVector parameter; + llvm::SmallVector qubitId = {0}; +}; + +} // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Passes/Decomposition/GateSequence.h b/mlir/include/mlir/Passes/Decomposition/GateSequence.h new file mode 100644 index 0000000000..f7cd6458b7 --- /dev/null +++ b/mlir/include/mlir/Passes/Decomposition/GateSequence.h @@ -0,0 +1,98 @@ +/* + * 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 "EulerBasis.h" +#include "Gate.h" +#include "Helpers.h" +#include "UnitaryMatrices.h" +#include "ir/operations/OpType.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mlir::qco::decomposition { +/** + * Gate sequence of single-qubit and/or two-qubit gates. + */ +struct QubitGateSequence { + /** + * Container sorting the gate sequence in order. + */ + llvm::SmallVector gates; + + /** + * Global phase adjustment required for the sequence. + */ + fp globalPhase{}; + /** + * @return true if the global phase adjustment is not zero. + */ + [[nodiscard]] bool hasGlobalPhase() const { + return std::abs(globalPhase) > DEFAULT_ATOL; + } + + /** + * Calculate complexity of sequence according to getComplexity(). + */ + [[nodiscard]] std::size_t complexity() const { + // TODO: add more sophisticated metric to determine complexity of + // series/sequence + // TODO: caching mechanism + std::size_t c{}; + for (auto&& gate : gates) { + c += helpers::getComplexity(gate.type, gate.qubitId.size()); + } + if (hasGlobalPhase()) { + // need to add a global phase gate if a global phase needs to be applied + c += helpers::getComplexity(qc::GPhase, 0); + } + return c; + } + + /** + * Calculate overall unitary matrix of the sequence. + */ + [[nodiscard]] matrix4x4 getUnitaryMatrix() const { + matrix4x4 unitaryMatrix = + helpers::kroneckerProduct(IDENTITY_GATE, IDENTITY_GATE); + for (auto&& gate : gates) { + auto gateMatrix = getTwoQubitMatrix(gate); + unitaryMatrix = gateMatrix * unitaryMatrix; + } + unitaryMatrix *= std::exp(IM * globalPhase); + assert(helpers::isUnitaryMatrix(unitaryMatrix)); + return unitaryMatrix; + } +}; +/** + * Helper type to show that a gate sequence is supposed to only contain + * single-qubit gates. + */ +using OneQubitGateSequence = QubitGateSequence; +/** + * Helper type to show that the gate sequence may contain two-qubit gates. + */ +using TwoQubitGateSequence = QubitGateSequence; + +} // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Passes/Decomposition/Helpers.h b/mlir/include/mlir/Passes/Decomposition/Helpers.h new file mode 100644 index 0000000000..eecea033da --- /dev/null +++ b/mlir/include/mlir/Passes/Decomposition/Helpers.h @@ -0,0 +1,225 @@ +/* + * 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 "ir/Definitions.hpp" +#include "ir/operations/OpType.hpp" +#include "mlir/Dialect/QCO/IR/QCODialect.h" + +#include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include // TODO: unstable, NOLINT(misc-include-cleaner) + +namespace mlir::qco { +using fp = qc::fp; +using qfp = std::complex; +// NOLINTBEGIN(misc-include-cleaner) +using matrix2x2 = Eigen::Matrix2; +using matrix4x4 = Eigen::Matrix4; +using rmatrix4x4 = Eigen::Matrix4; +using diagonal4x4 = Eigen::Vector; +using rdiagonal4x4 = Eigen::Vector; +// NOLINTEND(misc-include-cleaner) + +constexpr qfp C_ZERO{0., 0.}; +constexpr qfp C_ONE{1., 0.}; +constexpr qfp C_M_ONE{-1., 0.}; +constexpr qfp IM{0., 1.}; +constexpr qfp M_IM{0., -1.}; + +} // namespace mlir::qco + +namespace mlir::qco::helpers { + +std::optional mlirValueToFp(mlir::Value value); + +template +[[nodiscard]] std::optional performMlirFloatBinaryOp(mlir::Value value, + Func&& func) { + if (auto op = value.getDefiningOp()) { + auto lhs = mlirValueToFp(op.getLhs()); + auto rhs = mlirValueToFp(op.getRhs()); + if (lhs && rhs) { + return std::invoke(std::forward(func), *lhs, *rhs); + } + } + return std::nullopt; +} + +template +[[nodiscard]] std::optional performMlirFloatUnaryOp(mlir::Value value, + Func&& func) { + if (auto op = value.getDefiningOp()) { + if (auto operand = mlirValueToFp(op.getOperand())) { + return std::invoke(std::forward(func), *operand); + } + } + return std::nullopt; +} + +[[nodiscard]] inline std::optional mlirValueToFp(mlir::Value value) { + if (auto op = value.getDefiningOp()) { + if (auto attr = llvm::dyn_cast(op.getValue())) { + return attr.getValueAsDouble(); + } + return std::nullopt; + } + if (auto result = performMlirFloatUnaryOp( + value, [](fp a) { return -a; })) { + return result; + } + if (auto result = performMlirFloatUnaryOp( + value, [](fp a) { return a; })) { + return result; + } + if (auto result = performMlirFloatUnaryOp( + value, [](fp a) { return a; })) { + return result; + } + if (auto result = performMlirFloatBinaryOp( + value, [](fp a, fp b) { return std::max(a, b); })) { + return result; + } + if (auto result = performMlirFloatBinaryOp( + value, [](fp a, fp b) { return std::max(a, b); })) { + return result; + } + if (auto result = performMlirFloatBinaryOp( + value, [](fp a, fp b) { return std::min(a, b); })) { + return result; + } + if (auto result = performMlirFloatBinaryOp( + value, [](fp a, fp b) { return std::min(a, b); })) { + return result; + } + if (auto result = performMlirFloatBinaryOp( + value, [](fp a, fp b) { return std::fmod(a, b); })) { + return result; + } + if (auto result = performMlirFloatBinaryOp( + value, [](fp a, fp b) { return a + b; })) { + return result; + } + if (auto result = performMlirFloatBinaryOp( + value, [](fp a, fp b) { return a * b; })) { + return result; + } + if (auto result = performMlirFloatBinaryOp( + value, [](fp a, fp b) { return a / b; })) { + return result; + } + if (auto result = performMlirFloatBinaryOp( + value, [](fp a, fp b) { return a - b; })) { + return result; + } + return std::nullopt; +} + +[[nodiscard]] inline llvm::SmallVector +getParameters(UnitaryOpInterface op) { + llvm::SmallVector parameters; + for (std::size_t i = 0; i < op.getNumParams(); ++i) { + if (auto value = helpers::mlirValueToFp(op.getParameter(i))) { + parameters.push_back(*value); + } + } + return parameters; +} + +[[nodiscard]] inline qc::OpType getQcType(UnitaryOpInterface op) { + try { + auto type = op.getBaseSymbol(); + if (type == "ctrl") { + type = llvm::cast(op).getBodyUnitary().getBaseSymbol(); + } + return qc::opTypeFromString(type.str()); + } catch (const std::invalid_argument& /*exception*/) { + return qc::OpType::None; + } +} + +[[nodiscard]] inline bool isSingleQubitOperation(UnitaryOpInterface op) { + return op.isSingleQubit(); +} + +[[nodiscard]] inline bool isTwoQubitOperation(UnitaryOpInterface op) { + return op.isTwoQubit(); +} + +// NOLINTBEGIN(misc-include-cleaner) +template +[[nodiscard]] inline Eigen::Matrix4 +kroneckerProduct(const Eigen::Matrix2& lhs, const Eigen::Matrix2& rhs) { + return Eigen::kroneckerProduct(lhs, rhs); +} + +template +[[nodiscard]] inline auto selfAdjointEvd(Eigen::Matrix a) { + Eigen::SelfAdjointEigenSolver s; + s.compute(a); // TODO: computeDirect is faster + auto vecs = s.eigenvectors().eval(); + auto vals = s.eigenvalues(); + return std::make_pair(vecs, vals); +} + +template +[[nodiscard]] bool isUnitaryMatrix(const Eigen::Matrix& matrix) { + return (matrix.transpose().conjugate() * matrix).isIdentity(); +} +// NOLINTEND(misc-include-cleaner) + +[[nodiscard]] inline fp remEuclid(fp a, fp b) { + auto r = std::fmod(a, b); + return (r < 0.0) ? r + std::abs(b) : r; +} + +// Wrap angle into interval [-π,π). If within atol of the endpoint, clamp +// to -π +[[nodiscard]] inline fp mod2pi(fp angle, fp angleZeroEpsilon = 1e-13) { + // remEuclid() isn't exactly the same as Python's % operator, but + // because the RHS here is a constant and positive it is effectively + // equivalent for this case + auto wrapped = remEuclid(angle + qc::PI, qc::TAU) - qc::PI; + if (std::abs(wrapped - qc::PI) < angleZeroEpsilon) { + return -qc::PI; + } + return wrapped; +} + +[[nodiscard]] inline fp traceToFidelity(const qfp& x) { + auto xAbs = std::abs(x); + return (4.0 + xAbs * xAbs) / 20.0; +} + +[[nodiscard]] inline std::size_t getComplexity(qc::OpType type, + std::size_t numOfQubits) { + if (numOfQubits > 1) { + constexpr std::size_t multiQubitFactor = 10; + return (numOfQubits - 1) * multiQubitFactor; + } + if (type == qc::GPhase) { + return 0; + } + return 1; +} + +} // namespace mlir::qco::helpers diff --git a/mlir/include/mlir/Passes/Decomposition/UnitaryMatrices.h b/mlir/include/mlir/Passes/Decomposition/UnitaryMatrices.h new file mode 100644 index 0000000000..2d829f4706 --- /dev/null +++ b/mlir/include/mlir/Passes/Decomposition/UnitaryMatrices.h @@ -0,0 +1,211 @@ +/* + * 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 "Gate.h" +#include "Helpers.h" +#include "ir/operations/OpType.hpp" + +namespace mlir::qco::decomposition { + +inline constexpr auto SQRT2 = + static_cast(1.414213562373095048801688724209698079L); +inline constexpr auto FRAC1_SQRT2 = static_cast( + 0.707106781186547524400844362104849039284835937688474036588L); + +[[nodiscard]] inline matrix2x2 uMatrix(const fp lambda, const fp phi, + const fp theta) { + return matrix2x2{{{{std::cos(theta / 2.), 0.}, + {-std::cos(lambda) * std::sin(theta / 2.), + -std::sin(lambda) * std::sin(theta / 2.)}}, + {{std::cos(phi) * std::sin(theta / 2.), + std::sin(phi) * std::sin(theta / 2.)}, + {std::cos(lambda + phi) * std::cos(theta / 2.), + std::sin(lambda + phi) * std::cos(theta / 2.)}}}}; +} + +[[nodiscard]] inline matrix2x2 u2Matrix(const fp lambda, const fp phi) { + return matrix2x2{ + {FRAC1_SQRT2, + {-std::cos(lambda) * FRAC1_SQRT2, -std::sin(lambda) * FRAC1_SQRT2}}, + {{std::cos(phi) * FRAC1_SQRT2, std::sin(phi) * FRAC1_SQRT2}, + {std::cos(lambda + phi) * FRAC1_SQRT2, + std::sin(lambda + phi) * FRAC1_SQRT2}}}; +} + +inline matrix2x2 rxMatrix(fp theta) { + auto halfTheta = theta / 2.; + auto cos = qfp(std::cos(halfTheta), 0.); + auto isin = qfp(0., -std::sin(halfTheta)); + return matrix2x2{{cos, isin}, {isin, cos}}; +} + +inline matrix2x2 ryMatrix(fp theta) { + auto halfTheta = theta / 2.; + auto cos = qfp(std::cos(halfTheta), 0.); + auto sin = qfp(std::sin(halfTheta), 0.); + return matrix2x2{{cos, -sin}, {sin, cos}}; +} + +inline matrix2x2 rzMatrix(fp theta) { + return matrix2x2{{qfp{std::cos(theta / 2.), -std::sin(theta / 2.)}, 0}, + {0, qfp{std::cos(theta / 2.), std::sin(theta / 2.)}}}; +} + +inline matrix4x4 rxxMatrix(const fp theta) { + const auto cosTheta = std::cos(theta / 2.); + const auto sinTheta = std::sin(theta / 2.); + + return matrix4x4{{cosTheta, C_ZERO, C_ZERO, {0., -sinTheta}}, + {C_ZERO, cosTheta, {0., -sinTheta}, C_ZERO}, + {C_ZERO, {0., -sinTheta}, cosTheta, C_ZERO}, + {{0., -sinTheta}, C_ZERO, C_ZERO, cosTheta}}; +} + +inline matrix4x4 ryyMatrix(const fp theta) { + const auto cosTheta = std::cos(theta / 2.); + const auto sinTheta = std::sin(theta / 2.); + + return matrix4x4{{{cosTheta, 0, 0, {0., sinTheta}}, + {0, cosTheta, {0., -sinTheta}, 0}, + {0, {0., -sinTheta}, cosTheta, 0}, + {{0., sinTheta}, 0, 0, cosTheta}}}; +} + +inline matrix4x4 rzzMatrix(const fp theta) { + const auto cosTheta = std::cos(theta / 2.); + const auto sinTheta = std::sin(theta / 2.); + + return matrix4x4{{qfp{cosTheta, -sinTheta}, C_ZERO, C_ZERO, C_ZERO}, + {C_ZERO, {cosTheta, sinTheta}, C_ZERO, C_ZERO}, + {C_ZERO, C_ZERO, {cosTheta, sinTheta}, C_ZERO}, + {C_ZERO, C_ZERO, C_ZERO, {cosTheta, -sinTheta}}}; +} + +inline matrix2x2 pMatrix(const fp lambda) { + return matrix2x2{{1, 0}, {0, {std::cos(lambda), std::sin(lambda)}}}; +} +const matrix2x2 IDENTITY_GATE = matrix2x2::Identity(); +const matrix4x4 SWAP_GATE{ + {1, 0, 0, 0}, {0, 0, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 1}}; +const matrix2x2 H_GATE{{1.0 / SQRT2, 1.0 / SQRT2}, {1.0 / SQRT2, -1.0 / SQRT2}}; +const matrix2x2 IPZ{{IM, C_ZERO}, {C_ZERO, M_IM}}; +const matrix2x2 IPY{{C_ZERO, C_ONE}, {C_M_ONE, C_ZERO}}; +const matrix2x2 IPX{{C_ZERO, IM}, {IM, C_ZERO}}; + +[[nodiscard]] inline matrix4x4 +expandToTwoQubits(const matrix2x2& singleQubitMatrix, QubitId qubitId) { + if (qubitId == 0) { + return helpers::kroneckerProduct(decomposition::IDENTITY_GATE, + singleQubitMatrix); + } + if (qubitId == 1) { + return helpers::kroneckerProduct(singleQubitMatrix, + decomposition::IDENTITY_GATE); + } + throw std::invalid_argument{"Invalid qubit id for single-qubit expansion"}; +} + +[[nodiscard]] inline matrix4x4 +fixTwoQubitMatrixQubitOrder(const matrix4x4& twoQubitMatrix, + const llvm::SmallVector& qubitIds) { + if (qubitIds == llvm::SmallVector{1, 0}) { + // since UnitaryOpInterface::getUnitaryMatrix() does have a static + // qubit order, adjust if we need the other direction of the gate + return decomposition::SWAP_GATE * twoQubitMatrix * decomposition::SWAP_GATE; + } + if (qubitIds == llvm::SmallVector{0, 1}) { + return twoQubitMatrix; + } + throw std::invalid_argument{"Invalid qubit IDs for fixing two-qubit matrix"}; +} + +inline matrix2x2 getSingleQubitMatrix(const Gate& gate) { + if (gate.type == qc::SX) { + return matrix2x2{{qfp{0.5, 0.5}, qfp{0.5, -0.5}}, + {qfp{0.5, -0.5}, qfp{0.5, 0.5}}}; + } + if (gate.type == qc::RX) { + return rxMatrix(gate.parameter[0]); + } + if (gate.type == qc::RY) { + return ryMatrix(gate.parameter[0]); + } + if (gate.type == qc::RZ) { + return rzMatrix(gate.parameter[0]); + } + if (gate.type == qc::X) { + return matrix2x2{{0, 1}, {1, 0}}; + } + if (gate.type == qc::I) { + return IDENTITY_GATE; + } + if (gate.type == qc::P) { + return pMatrix(gate.parameter[0]); + } + if (gate.type == qc::U) { + return uMatrix(gate.parameter[0], gate.parameter[1], gate.parameter[2]); + } + if (gate.type == qc::U2) { + return u2Matrix(gate.parameter[0], gate.parameter[1]); + } + if (gate.type == qc::H) { + return matrix2x2{{FRAC1_SQRT2, FRAC1_SQRT2}, {FRAC1_SQRT2, -FRAC1_SQRT2}}; + } + throw std::invalid_argument{ + "unsupported gate type for single qubit matrix (" + + qc::toString(gate.type) + ")"}; +} + +// TODO: remove? only used for verification of circuit +inline matrix4x4 getTwoQubitMatrix(const Gate& gate) { + using helpers::kroneckerProduct; + + if (gate.qubitId.empty()) { + return kroneckerProduct(IDENTITY_GATE, IDENTITY_GATE); + } + if (gate.qubitId.size() == 1) { + return expandToTwoQubits(getSingleQubitMatrix(gate), gate.qubitId[0]); + } + if (gate.qubitId.size() == 2) { + if (gate.type == qc::X) { + // controlled X (CX) + if (gate.qubitId == llvm::SmallVector{0, 1}) { + return matrix4x4{ + {1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 1}, {0, 0, 1, 0}}; + } + if (gate.qubitId == llvm::SmallVector{1, 0}) { + return matrix4x4{ + {1, 0, 0, 0}, {0, 0, 0, 1}, {0, 0, 1, 0}, {0, 1, 0, 0}}; + } + } + if (gate.type == qc::RXX) { + // TODO: check qubit order? + return rxxMatrix(gate.parameter[0]); + } + if (gate.type == qc::RYY) { + // TODO: check qubit order? + return ryyMatrix(gate.parameter[0]); + } + if (gate.type == qc::RZZ) { + // TODO: check qubit order? + return rzzMatrix(gate.parameter[0]); + } + if (gate.type == qc::I) { + return kroneckerProduct(IDENTITY_GATE, IDENTITY_GATE); + } + throw std::invalid_argument{"unsupported gate type for two qubit matrix (" + + qc::toString(gate.type) + ")"}; + } + throw std::logic_error{"Invalid number of qubit IDs in compute_unitary"}; +} + +} // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Passes/Decomposition/WeylDecomposition.h b/mlir/include/mlir/Passes/Decomposition/WeylDecomposition.h new file mode 100644 index 0000000000..5350868015 --- /dev/null +++ b/mlir/include/mlir/Passes/Decomposition/WeylDecomposition.h @@ -0,0 +1,784 @@ +/* + * 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 "EulerBasis.h" +#include "EulerDecomposition.h" +#include "Helpers.h" +#include "UnitaryMatrices.h" +#include "ir/Definitions.hpp" +#include "ir/operations/OpType.hpp" + +#include // NOLINT(misc-include-cleaner) +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include // TODO: unstable, NOLINT(misc-include-cleaner) +#include + +namespace mlir::qco::decomposition { +/** + * Allowed deviation for internal assert statements which ensure the correctness + * of the decompositions. + */ +constexpr fp SANITY_CHECK_PRECISION = 1e-12; + +/** + * Weyl decomposition of a 2-qubit unitary matrix (4x4). + * The result consists of four 2x2 1-qubit matrices (k1l, k2l, k1r, k2r) and + * three parameters for a canonical gate (a, b, c). The matrices can then be + * decomposed using a single-qubit decomposition into e.g. rotation gates and + * the canonical gate is RXX(-2 * a), RYY(-2 * b), RZZ (-2 * c). + */ +struct TwoQubitWeylDecomposition { + enum class Specialization : std::uint8_t { + General, // canonical gate has no special symmetry. + IdEquiv, // canonical gate is identity. + SWAPEquiv, // canonical gate is SWAP. + PartialSWAPEquiv, // canonical gate is partial SWAP. + PartialSWAPFlipEquiv, // canonical gate is flipped partial SWAP. + ControlledEquiv, // canonical gate is a controlled gate. + MirrorControlledEquiv, // canonical gate is swap + controlled gate. + + // These next 3 gates use the definition of fSim from eq (1) in: + // https://arxiv.org/pdf/2001.08343.pdf + FSimaabEquiv, // parameters a=b & a!=c + FSimabbEquiv, // parameters a!=b & b=c + FSimabmbEquiv, // parameters a!=b!=c & -b=c + }; + + // a, b, c are the parameters of the canonical gate (CAN) + fp a; // rotation of RXX gate in CAN (must be taken times -2.0) + fp b; // rotation of RYY gate in CAN (must be taken times -2.0) + fp c; // rotation of RZZ gate in CAN (must be taken times -2.0) + fp globalPhase; // global phase adjustment + /** + * q1 - k2r - C - k1r - + * A + * q0 - k2l - N - k1l - + */ + matrix2x2 k1l; // "left" qubit after canonical gate + matrix2x2 k2l; // "left" qubit before canonical gate + matrix2x2 k1r; // "right" qubit after canonical gate + matrix2x2 k2r; // "right" qubit before canonical gate + Specialization specialization; // detected symmetries in the matrix + EulerBasis defaultEulerBasis; // recommended euler basis for k1l/k2l/k1r/k2r + std::optional requestedFidelity; // desired fidelity; + // if set to std::nullopt, no automatic + // specialization will be applied + fp calculatedFidelity; // actual fidelity of decomposition + matrix4x4 unitaryMatrix; // original matrix for this decomposition + + /** + * Create Weyl decomposition. + * + * @param unitaryMatrix Matrix of the two-qubit operation/series to be + * decomposed. + * @param fidelity Tolerance to assume a specialization which is used to + * reduce the number of parameters required by the canonical + * gate and thus potentially decreasing the number of basis + * gates. + */ + static TwoQubitWeylDecomposition create(const matrix4x4& unitaryMatrix, + std::optional fidelity) { + auto u = unitaryMatrix; + auto detU = u.determinant(); + auto detPow = std::pow(detU, static_cast(-0.25)); + u *= detPow; // remove global phase from unitary matrix + auto globalPhase = std::arg(detU) / 4.; + + // this should have normalized determinant of u, so that u ∈ SU(4) + assert(std::abs(u.determinant() - 1.0) < SANITY_CHECK_PRECISION); + + // transform unitary matrix to magic basis; this enables two properties: + // 1. if uP ∈ SO(4), V = A ⊗ B (SO(4) → SU(2) ⊗ SU(2)) + // 2. magic basis diagonalizes canonical gate, allowing calculation of + // canonical gate parameters later on + auto uP = magicBasisTransform(u, MagicBasisTransform::OutOf); + const matrix4x4 m2 = uP.transpose() * uP; + + // diagonalization yields eigenvectors (p) and eigenvalues (d); + // p is used to calculate K1/K2 (and thus the single-qubit gates + // surrounding the canonical gate); d is is used to determine the weyl + // coordinates and thus the parameters of the canonical gate + // TODO: it may be possible to lower the precision + auto [p, d] = diagonalizeComplexSymmetric(m2, 1e-13); + + // extract Weyl coordinates from eigenvalues, map to [0, 2*pi) + // NOLINTNEXTLINE(misc-include-cleaner) + Eigen::Vector cs; + rdiagonal4x4 dReal = -1.0 * d.cwiseArg() / 2.0; + dReal(3) = -dReal(0) - dReal(1) - dReal(2); + for (int i = 0; i < static_cast(cs.size()); ++i) { + assert(i < dReal.size()); + cs[i] = helpers::remEuclid((dReal(i) + dReal(3)) / 2.0, qc::TAU); + } + + // re-order coordinates and according to min(a, pi/2 - a) with + // a = x mod pi/2 for each weyl coordinate x + decltype(cs) cstemp; + llvm::transform(cs, cstemp.begin(), [](auto&& x) { + auto tmp = helpers::remEuclid(x, qc::PI_2); + return std::min(tmp, qc::PI_2 - tmp); + }); + std::array order{0, 1, 2}; + llvm::stable_sort(order, + [&](auto a, auto b) { return cstemp[a] < cstemp[b]; }); + std::tie(order[0], order[1], order[2]) = + std::tuple{order[1], order[2], order[0]}; + std::tie(cs[0], cs[1], cs[2]) = + std::tuple{cs[order[0]], cs[order[1]], cs[order[2]]}; + std::tie(dReal(0), dReal(1), dReal(2)) = + std::tuple{dReal(order[0]), dReal(order[1]), dReal(order[2])}; + + // update eigenvectors (columns of p) according to new order of + // weyl coordinates + matrix4x4 pOrig = p; + for (int i = 0; std::cmp_less(i, order.size()); ++i) { + p.col(i) = pOrig.col(order[i]); + } + // apply correction for determinant if necessary + if (p.determinant().real() < 0.0) { + auto lastColumnIndex = p.cols() - 1; + p.col(lastColumnIndex) *= -1.0; + } + assert(std::abs(p.determinant() - 1.0) < SANITY_CHECK_PRECISION); + + // re-create complex eigenvalue matrix; this matrix contains the + // parameters of the canonical gate which is later used in the + // verification + matrix4x4 temp = dReal.asDiagonal(); + temp *= IM; + temp = temp.exp(); + + // combined matrix k1 of 1q gates after canonical gate + matrix4x4 k1 = uP * p * temp; + assert((k1.transpose() * k1).isIdentity()); // k1 must be orthogonal + assert(k1.determinant().real() > 0.0); + k1 = magicBasisTransform(k1, MagicBasisTransform::Into); + + // combined matrix k2 of 1q gates before canonical gate + matrix4x4 k2 = p.transpose().conjugate(); + assert((k2.transpose() * k2).isIdentity()); // k2 must be orthogonal + assert(k2.determinant().real() > 0.0); + k2 = magicBasisTransform(k2, MagicBasisTransform::Into); + + // ensure k1 and k2 are correct (when combined with the canonical gate + // parameters in-between, they are equivalent to u) + assert( + (k1 * magicBasisTransform(temp.conjugate(), MagicBasisTransform::Into) * + k2) + .isApprox(u, SANITY_CHECK_PRECISION)); + + // calculate k1 = K1l ⊗ K1r + auto [K1l, K1r, phase_l] = decomposeTwoQubitProductGate(k1); + // decompose k2 = K2l ⊗ K2r + auto [K2l, K2r, phase_r] = decomposeTwoQubitProductGate(k2); + assert(helpers::kroneckerProduct(K1l, K1r).isApprox( + k1, SANITY_CHECK_PRECISION)); + assert(helpers::kroneckerProduct(K2l, K2r).isApprox( + k2, SANITY_CHECK_PRECISION)); + // accumulate global phase + globalPhase += phase_l + phase_r; + + // Flip into Weyl chamber + if (cs[0] > qc::PI_2) { + cs[0] -= 3.0 * qc::PI_2; + K1l = K1l * IPY; + K1r = K1r * IPY; + globalPhase += qc::PI_2; + } + if (cs[1] > qc::PI_2) { + cs[1] -= 3.0 * qc::PI_2; + K1l = K1l * IPX; + K1r = K1r * IPX; + globalPhase += qc::PI_2; + } + auto conjs = 0; + if (cs[0] > qc::PI_4) { + cs[0] = qc::PI_2 - cs[0]; + K1l = K1l * IPY; + K2r = IPY * K2r; + conjs += 1; + globalPhase -= qc::PI_2; + } + if (cs[1] > qc::PI_4) { + cs[1] = qc::PI_2 - cs[1]; + K1l = K1l * IPX; + K2r = IPX * K2r; + conjs += 1; + globalPhase += qc::PI_2; + if (conjs == 1) { + globalPhase -= qc::PI; + } + } + if (cs[2] > qc::PI_2) { + cs[2] -= 3.0 * qc::PI_2; + K1l = K1l * IPZ; + K1r = K1r * IPZ; + globalPhase += qc::PI_2; + if (conjs == 1) { + globalPhase -= qc::PI; + } + } + if (conjs == 1) { + cs[2] = qc::PI_2 - cs[2]; + K1l = K1l * IPZ; + K2r = IPZ * K2r; + globalPhase += qc::PI_2; + } + if (cs[2] > qc::PI_4) { + cs[2] -= qc::PI_2; + K1l = K1l * IPZ; + K1r = K1r * IPZ; + globalPhase -= qc::PI_2; + } + + // bind weyl coordinates as parameters of canonical gate + auto [a, b, c] = std::tie(cs[1], cs[0], cs[2]); + + TwoQubitWeylDecomposition decomposition{ + .a = a, + .b = b, + .c = c, + .globalPhase = globalPhase, + .k1l = K1l, + .k2l = K2l, + .k1r = K1r, + .k2r = K2r, + .specialization = Specialization::General, + .defaultEulerBasis = EulerBasis::ZYZ, + .requestedFidelity = fidelity, + // will be calculated if a specialization is used, set to -1 for now + .calculatedFidelity = -1.0, + .unitaryMatrix = unitaryMatrix, + }; + // make sure decomposition is equal to input + assert((helpers::kroneckerProduct(K1l, K1r) * + decomposition.getCanonicalMatrix() * + helpers::kroneckerProduct(K2l, K2r) * std::exp(IM * globalPhase)) + .isApprox(unitaryMatrix, SANITY_CHECK_PRECISION)); + + // determine actual specialization of canonical gate so that the 1q + // matrices can potentially be simplified + auto flippedFromOriginal = decomposition.applySpecialization(); + + auto getTrace = [&]() { + if (flippedFromOriginal) { + return TwoQubitWeylDecomposition::getTrace( + qc::PI_2 - a, b, -c, decomposition.a, decomposition.b, + decomposition.c); + } + return TwoQubitWeylDecomposition::getTrace( + a, b, c, decomposition.a, decomposition.b, decomposition.c); + }; + // use trace to calculate fidelity of applied specialization and + // adjust global phase + auto trace = getTrace(); + decomposition.calculatedFidelity = helpers::traceToFidelity(trace); + // final check if specialization is close enough to the original matrix to + // satisfy the requested fidelity; since no forced specialization is + // allowed, this should never fail + if (decomposition.requestedFidelity) { + if (decomposition.calculatedFidelity + 1.0e-13 < + *decomposition.requestedFidelity) { + throw std::runtime_error{ + "TwoQubitWeylDecomposition: Calculated fidelity of " + "specialization is worse than requested fidelity!"}; + } + } + decomposition.globalPhase += std::arg(trace); + + // final check if decomposition is still valid after specialization + assert((helpers::kroneckerProduct(decomposition.k1l, decomposition.k1r) * + decomposition.getCanonicalMatrix() * + helpers::kroneckerProduct(decomposition.k2l, decomposition.k2r) * + std::exp(IM * decomposition.globalPhase)) + .isApprox(unitaryMatrix, SANITY_CHECK_PRECISION)); + + return decomposition; + } + + /** + * Calculate matrix of canonical gate based on its parameters a, b, c. + */ + [[nodiscard]] matrix4x4 getCanonicalMatrix() const { + auto xx = getTwoQubitMatrix({ + .type = qc::RXX, + .parameter = {-2.0 * a}, + .qubitId = {0, 1}, + }); + auto yy = getTwoQubitMatrix({ + .type = qc::RYY, + .parameter = {-2.0 * b}, + .qubitId = {0, 1}, + }); + auto zz = getTwoQubitMatrix({ + .type = qc::RZZ, + .parameter = {-2.0 * c}, + .qubitId = {0, 1}, + }); + return zz * yy * xx; + } + +protected: + static constexpr fp SANITY_CHECK_PRECISION = 1e-12; + + // https://docs.rs/faer/latest/faer/mat/generic/struct.Mat.html#method.self_adjoint_eigen + template static auto selfAdjointEigenLower(T&& a) { + auto [U, S] = helpers::selfAdjointEvd(std::forward(a)); + + return std::make_pair(U, S); + } + + enum class MagicBasisTransform : std::uint8_t { + Into, + OutOf, + }; + + static matrix4x4 magicBasisTransform(const matrix4x4& unitary, + MagicBasisTransform direction) { + const matrix4x4 bNonNormalized{ + {C_ONE, IM, C_ZERO, C_ZERO}, + {C_ZERO, C_ZERO, IM, C_ONE}, + {C_ZERO, C_ZERO, IM, C_M_ONE}, + {C_ONE, M_IM, C_ZERO, C_ZERO}, + }; + + const matrix4x4 bNonNormalizedDagger{ + {qfp(0.5, 0.), C_ZERO, C_ZERO, qfp(0.5, 0.)}, + {qfp(0., -0.5), C_ZERO, C_ZERO, qfp(0., 0.5)}, + {C_ZERO, qfp(0., -0.5), qfp(0., -0.5), C_ZERO}, + {C_ZERO, qfp(0.5, 0.), qfp(-0.5, 0.), C_ZERO}, + }; + if (direction == MagicBasisTransform::OutOf) { + return bNonNormalizedDagger * unitary * bNonNormalized; + } + if (direction == MagicBasisTransform::Into) { + return bNonNormalized * unitary * bNonNormalizedDagger; + } + throw std::logic_error{"Unknown MagicBasisTransform direction!"}; + } + + static fp closestPartialSwap(fp a, fp b, fp c) { + auto m = (a + b + c) / 3.; + auto [am, bm, cm] = std::array{a - m, b - m, c - m}; + auto [ab, bc, ca] = std::array{a - b, b - c, c - a}; + return m + (am * bm * cm * (6. + ab * ab + bc * bc + ca * ca) / 18.); + } + + /** + * Diagonalize given complex symmetric matrix M into (P, d) using a + * randomized algorithm. + * This approach is used in both qiskit and quantumflow. + * + * P is the matrix of real or orthogonal eigenvectors of M with P ∈ SO(4) + * d is a vector containing sqrt(eigenvalues) of M with unit-magnitude + * elements (for each element, complex magnitude is 1.0). + * D is d as a diagonal matrix. + * + * M = P * D * P^T + * + * @return pair of (P, D.diagonal()) + */ + [[nodiscard]] static std::pair + diagonalizeComplexSymmetric(const matrix4x4& m, fp precision) { + // We can't use raw `eig` directly because it isn't guaranteed to give + // us real or orthogonal eigenvectors. Instead, since `M` is + // complex-symmetric, + // M = A + iB + // for real-symmetric `A` and `B`, and as + // M^+ @ M2 = A^2 + B^2 + i [A, B] = 1 + // we must have `A` and `B` commute, and consequently they are + // simultaneously diagonalizable. Mixing them together _should_ account + // for any degeneracy problems, but it's not guaranteed, so we repeat it + // a little bit. The fixed seed is to make failures deterministic; the + // value is not important. + auto state = std::mt19937{2023}; + std::normal_distribution dist; + + for (int i = 0; i < 100; ++i) { + fp randA{}; + fp randB{}; + // For debugging the algorithm use the same RNG values as the + // Qiskit implementation for the first random trial. + // In most cases this loop only executes a single iteration and + // using the same rng values rules out possible RNG differences + // as the root cause of a test failure + if (i == 0) { + randA = 1.2602066112249388; + randB = 0.22317849046722027; + } else { + randA = dist(state); + randB = dist(state); + } + const rmatrix4x4 m2Real = randA * m.real() + randB * m.imag(); + const rmatrix4x4 pReal = selfAdjointEigenLower(m2Real).first; + const matrix4x4 p = pReal; + const diagonal4x4 d = (p.transpose() * m * p).diagonal(); + + const matrix4x4 diagD = d.asDiagonal(); + + const matrix4x4 compare = p * diagD * p.transpose(); + if (compare.isApprox(m, precision)) { + // p are the eigenvectors which are decomposed into the + // single-qubit gates surrounding the canonical gate + // d is the sqrt of the eigenvalues that are used to determine the + // weyl coordinates and thus the parameters of the canonical gate + // check that p is in SO(4) + assert((p.transpose() * p).isIdentity(SANITY_CHECK_PRECISION)); + // make sure determinant of eigenvalues is 1.0 + assert(std::abs(matrix4x4{d.asDiagonal()}.determinant() - 1.0) < + SANITY_CHECK_PRECISION); + return std::make_pair(p, d); + } + } + throw std::runtime_error{ + "TwoQubitWeylDecomposition: failed to diagonalize M2."}; + } + + /** + * Decompose a special unitary matrix C that is the combination of two + * single-qubit gates A and B into its single-qubit matrices. + * + * C = A ⊗ B + * + * @param specialUnitary Special unitary matrix C + * + * @return single-qubit matrices A and B and the required + * global phase adjustment + */ + static std::tuple + decomposeTwoQubitProductGate(const matrix4x4& specialUnitary) { + // for alternative approaches, see + // pennylane's math.decomposition.su2su2_to_tensor_products + // or quantumflow.kronecker_decomposition + + // first quadrant + matrix2x2 r{{specialUnitary(0, 0), specialUnitary(0, 1)}, + {specialUnitary(1, 0), specialUnitary(1, 1)}}; + auto detR = r.determinant(); + if (std::abs(detR) < 0.1) { + // third quadrant + r = matrix2x2{{specialUnitary(2, 0), specialUnitary(2, 1)}, + {specialUnitary(3, 0), specialUnitary(3, 1)}}; + detR = r.determinant(); + } + if (std::abs(detR) < 0.1) { + throw std::runtime_error{ + "decompose_two_qubit_product_gate: unable to decompose: det_r < 0.1"}; + } + r /= std::sqrt(detR); + // transpose with complex conjugate of each element + const matrix2x2 rTConj = r.transpose().conjugate(); + + auto temp = helpers::kroneckerProduct(IDENTITY_GATE, rTConj); + temp = specialUnitary * temp; + + // [[a, b, c, d], + // [e, f, g, h], => [[a, c], + // [i, j, k, l], [i, k]] + // [m, n, o, p]] + matrix2x2 l{{temp(0, 0), temp(0, 2)}, {temp(2, 0), temp(2, 2)}}; + auto detL = l.determinant(); + if (std::abs(detL) < 0.9) { + throw std::runtime_error{ + "decompose_two_qubit_product_gate: unable to decompose: detL < 0.9"}; + } + l /= std::sqrt(detL); + auto phase = std::arg(detL) / 2.; + + return {l, r, phase}; + } + + /** + * Calculate trace of two sets of parameters for the canonical gate. + * The trace has been defined in: https://arxiv.org/abs/1811.12926 + */ + [[nodiscard]] static qfp getTrace(fp a, fp b, fp c, fp ap, fp bp, fp cp) { + auto da = a - ap; + auto db = b - bp; + auto dc = c - cp; + return static_cast(4.) * + qfp(std::cos(da) * std::cos(db) * std::cos(dc), + std::sin(da) * std::sin(db) * std::sin(dc)); + } + + /** + * Choose the best specialization for the for the canonical gate. + * This will use the requestedFidelity to determine if a specialization is + * close enough to the actual canonical gate matrix. + */ + [[nodiscard]] Specialization bestSpecialization() const { + auto isClose = [this](fp ap, fp bp, fp cp) -> bool { + auto tr = getTrace(a, b, c, ap, bp, cp); + if (requestedFidelity) { + return helpers::traceToFidelity(tr) >= *requestedFidelity; + } + return false; + }; + + auto closestAbc = closestPartialSwap(a, b, c); + auto closestAbMinusC = closestPartialSwap(a, b, -c); + + if (isClose(0., 0., 0.)) { + return Specialization::IdEquiv; + } + if (isClose(qc::PI_4, qc::PI_4, qc::PI_4) || + isClose(qc::PI_4, qc::PI_4, -qc::PI_4)) { + return Specialization::SWAPEquiv; + } + if (isClose(closestAbc, closestAbc, closestAbc)) { + return Specialization::PartialSWAPEquiv; + } + if (isClose(closestAbMinusC, closestAbMinusC, -closestAbMinusC)) { + return Specialization::PartialSWAPFlipEquiv; + } + if (isClose(a, 0., 0.)) { + return Specialization::ControlledEquiv; + } + if (isClose(qc::PI_4, qc::PI_4, c)) { + return Specialization::MirrorControlledEquiv; + } + if (isClose((a + b) / 2., (a + b) / 2., c)) { + return Specialization::FSimaabEquiv; + } + if (isClose(a, (b + c) / 2., (b + c) / 2.)) { + return Specialization::FSimabbEquiv; + } + if (isClose(a, (b - c) / 2., (c - b) / 2.)) { + return Specialization::FSimabmbEquiv; + } + return Specialization::General; + } + + /** + * @return true if the specialization flipped the original decomposition + */ + bool applySpecialization() { + if (specialization != Specialization::General) { + throw std::logic_error{"Application of specialization only works on " + "general decomposition!"}; + } + bool flippedFromOriginal = false; + auto newSpecialization = bestSpecialization(); + if (newSpecialization == Specialization::General) { + // U has no special symmetry. + // + // This gate binds all 6 possible parameters, so there is no need to + // make the single-qubit pre-/post-gates canonical. + return flippedFromOriginal; + } + specialization = newSpecialization; + + if (newSpecialization == Specialization::IdEquiv) { + // :math:`U \sim U_d(0,0,0)` + // Thus, :math:`\sim Id` + // + // This gate binds 0 parameters, we make it canonical by setting: + // + // :math:`K2_l = Id` , :math:`K2_r = Id`. + a = 0.; + b = 0.; + c = 0.; + // unmodified global phase + k1l = k1l * k2l; + k2l = IDENTITY_GATE; + k1r = k1r * k2r; + k2r = IDENTITY_GATE; + } else if (newSpecialization == Specialization::SWAPEquiv) { + // :math:`U \sim U_d(\pi/4, \pi/4, \pi/4) \sim U(\pi/4, \pi/4, -\pi/4)` + // Thus, :math:`U \sim \text{SWAP}` + // + // This gate binds 0 parameters, we make it canonical by setting: + // + // :math:`K2_l = Id` , :math:`K2_r = Id`. + a = qc::PI_4; + b = qc::PI_4; + c = qc::PI_4; + if (c > 0.) { + // unmodified global phase + k1l = k1l * k2r; + k1r = k1r * k2l; + k2l = IDENTITY_GATE; + k2r = IDENTITY_GATE; + } else { + flippedFromOriginal = true; + + globalPhase += qc::PI_2; + k1l = k1l * IPZ * k2r; + k1r = k1r * IPZ * k2l; + k2l = IDENTITY_GATE; + k2r = IDENTITY_GATE; + } + } else if (newSpecialization == Specialization::PartialSWAPEquiv) { + // :math:`U \sim U_d(\alpha\pi/4, \alpha\pi/4, \alpha\pi/4)` + // Thus, :math:`U \sim \text{SWAP}^\alpha` + // + // This gate binds 3 parameters, we make it canonical by setting: + // + // :math:`K2_l = Id`. + auto closest = closestPartialSwap(a, b, c); + auto k2lDagger = k2l.transpose().conjugate(); + + a = closest; + b = closest; + c = closest; + // unmodified global phase + k1l = k1l * k2l; + k1r = k1r * k2l; + k2r = k2lDagger * k2r; + k2l = IDENTITY_GATE; + } else if (newSpecialization == Specialization::PartialSWAPFlipEquiv) { + // :math:`U \sim U_d(\alpha\pi/4, \alpha\pi/4, -\alpha\pi/4)` + // Thus, :math:`U \sim \text{SWAP}^\alpha` + // + // (a non-equivalent root of SWAP from the TwoQubitWeylPartialSWAPEquiv + // similar to how :math:`x = (\pm \sqrt(x))^2`) + // + // This gate binds 3 parameters, we make it canonical by setting: + // + // :math:`K2_l = Id` + auto closest = closestPartialSwap(a, b, -c); + auto k2lDagger = k2l.transpose().conjugate(); + + a = closest; + b = closest; + c = -closest; + // unmodified global phase + k1l = k1l * k2l; + k1r = k1r * IPZ * k2l * IPZ; + k2r = IPZ * k2lDagger * IPZ * k2r; + k2l = IDENTITY_GATE; + } else if (newSpecialization == Specialization::ControlledEquiv) { + // :math:`U \sim U_d(\alpha, 0, 0)` + // Thus, :math:`U \sim \text{Ctrl-U}` + // + // This gate binds 4 parameters, we make it canonical by setting: + // + // :math:`K2_l = Ry(\theta_l) Rx(\lambda_l)` + // :math:`K2_r = Ry(\theta_r) Rx(\lambda_r)` + auto eulerBasis = EulerBasis::XYX; + auto [k2ltheta, k2lphi, k2llambda, k2lphase] = + EulerDecomposition::anglesFromUnitary(k2l, eulerBasis); + auto [k2rtheta, k2rphi, k2rlambda, k2rphase] = + EulerDecomposition::anglesFromUnitary(k2r, eulerBasis); + + // unmodified parameter a + b = 0.; + c = 0.; + globalPhase = globalPhase + k2lphase + k2rphase; + k1l = k1l * rxMatrix(k2lphi); + k2l = ryMatrix(k2ltheta) * rxMatrix(k2llambda); + k1r = k1r * rxMatrix(k2rphi); + k2r = ryMatrix(k2rtheta) * rxMatrix(k2rlambda); + defaultEulerBasis = eulerBasis; + } else if (newSpecialization == Specialization::MirrorControlledEquiv) { + // :math:`U \sim U_d(\pi/4, \pi/4, \alpha)` + // Thus, :math:`U \sim \text{SWAP} \cdot \text{Ctrl-U}` + // + // This gate binds 4 parameters, we make it canonical by setting: + // + // :math:`K2_l = Ry(\theta_l)\cdot Rz(\lambda_l)` + // :math:`K2_r = Ry(\theta_r)\cdot Rz(\lambda_r)` + auto [k2ltheta, k2lphi, k2llambda, k2lphase] = + EulerDecomposition::anglesFromUnitary(k2l, EulerBasis::ZYZ); + auto [k2rtheta, k2rphi, k2rlambda, k2rphase] = + EulerDecomposition::anglesFromUnitary(k2r, EulerBasis::ZYZ); + + a = qc::PI_4; + b = qc::PI_4; + // unmodified parameter c + globalPhase = globalPhase + k2lphase + k2rphase; + k1l = k1l * rzMatrix(k2rphi); + k2l = ryMatrix(k2ltheta) * rzMatrix(k2llambda); + k1r = k1r * rzMatrix(k2lphi); + k2r = ryMatrix(k2rtheta) * rzMatrix(k2rlambda); + } else if (newSpecialization == Specialization::FSimaabEquiv) { + // :math:`U \sim U_d(\alpha, \alpha, \beta), \alpha \geq |\beta|` + // + // This gate binds 5 parameters, we make it canonical by setting: + // + // :math:`K2_l = Ry(\theta_l)\cdot Rz(\lambda_l)`. + auto [k2ltheta, k2lphi, k2llambda, k2lphase] = + EulerDecomposition::anglesFromUnitary(k2l, EulerBasis::ZYZ); + auto ab = (a + b) / 2.; + + a = ab; + b = ab; + // unmodified parameter c + globalPhase = globalPhase + k2lphase; + k1l = k1l * rzMatrix(k2lphi); + k2l = ryMatrix(k2ltheta) * rzMatrix(k2llambda); + k1r = k1r * rzMatrix(k2lphi); + k2r = rzMatrix(-k2lphi) * k2r; + } else if (newSpecialization == Specialization::FSimabbEquiv) { + // :math:`U \sim U_d(\alpha, \beta, -\beta), \alpha \geq \beta \geq 0` + // + // This gate binds 5 parameters, we make it canonical by setting: + // + // :math:`K2_l = Ry(\theta_l)Rx(\lambda_l)` + auto eulerBasis = EulerBasis::XYX; + auto [k2ltheta, k2lphi, k2llambda, k2lphase] = + EulerDecomposition::anglesFromUnitary(k2l, eulerBasis); + auto bc = (b + c) / 2.; + + // unmodified parameter a + b = bc; + c = bc; + globalPhase = globalPhase + k2lphase; + k1l = k1l * rxMatrix(k2lphi); + k2l = ryMatrix(k2ltheta) * rxMatrix(k2llambda); + k1r = k1r * rxMatrix(k2lphi); + k2r = rxMatrix(-k2lphi) * k2r; + defaultEulerBasis = eulerBasis; + } else if (newSpecialization == Specialization::FSimabmbEquiv) { + // :math:`U \sim U_d(\alpha, \beta, -\beta), \alpha \geq \beta \geq 0` + // + // This gate binds 5 parameters, we make it canonical by setting: + // + // :math:`K2_l = Ry(\theta_l)Rx(\lambda_l)` + auto eulerBasis = EulerBasis::XYX; + auto [k2ltheta, k2lphi, k2llambda, k2lphase] = + EulerDecomposition::anglesFromUnitary(k2l, eulerBasis); + auto bc = (b - c) / 2.; + + // unmodified parameter a + b = bc; + c = -bc; + globalPhase = globalPhase + k2lphase; + k1l = k1l * rxMatrix(k2lphi); + k2l = ryMatrix(k2ltheta) * rxMatrix(k2llambda); + k1r = k1r * IPZ * rxMatrix(k2lphi) * IPZ; + k2r = IPZ * rxMatrix(-k2lphi) * IPZ * k2r; + defaultEulerBasis = eulerBasis; + } else { + throw std::logic_error{"Unknown specialization"}; + } + return flippedFromOriginal; + } +}; +} // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Passes/Passes.h b/mlir/include/mlir/Passes/Passes.h new file mode 100644 index 0000000000..5deeb4d3ef --- /dev/null +++ b/mlir/include/mlir/Passes/Passes.h @@ -0,0 +1,52 @@ +/* + * 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 "mlir/Dialect/QCO/IR/QCODialect.h" + +#include +#include +#include + +namespace mlir { + +class RewritePatternSet; + +} // namespace mlir + +namespace mlir::qco { + +#define GEN_PASS_DECL +#include "mlir/Passes/Passes.h.inc" // IWYU pragma: export + +void populateGateDecompositionPatterns( + mlir::RewritePatternSet& patterns, llvm::Statistic& twoQubitCreationTime, + llvm::Statistic& numberOfTwoQubitCreations, + llvm::Statistic& successfulSingleQubitDecompositions, + llvm::Statistic& totalSingleQubitDecompositions, + llvm::Statistic& successfulTwoQubitDecompositions, + llvm::Statistic& totalTwoQubitDecompositions, + llvm::Statistic& totalCircuitCollections, + llvm::Statistic& totalTouchedGates, + llvm::Statistic& subCircuitComplexityChange, + llvm::Statistic& timeInCircuitCollection, + llvm::Statistic& timeInSingleQubitDecomposition, + llvm::Statistic& timeInTwoQubitDecomposition); + +//===----------------------------------------------------------------------===// +// Registration +//===----------------------------------------------------------------------===// + +/// Generate the code for registering passes. +#define GEN_PASS_REGISTRATION +#include "mlir/Passes/Passes.h.inc" // IWYU pragma: export + +} // namespace mlir::qco diff --git a/mlir/include/mlir/Passes/Passes.td b/mlir/include/mlir/Passes/Passes.td new file mode 100644 index 0000000000..6c238bcfa2 --- /dev/null +++ b/mlir/include/mlir/Passes/Passes.td @@ -0,0 +1,27 @@ +// 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 + +#ifndef QCO_PASSES +#define QCO_PASSES + +include "mlir/Pass/PassBase.td" + +//===----------------------------------------------------------------------===// +// Optimization Passes +//===----------------------------------------------------------------------===// + +def GateDecompositionPass : Pass<"gate-decomposition", "mlir::ModuleOp"> { + let dependentDialects = [ "mlir::arith::ArithDialect", "mlir::qco::QCODialect" ]; + let summary = "This pass performs various gate decompositions to translate quantum gates being used."; + let description = [{ + Decomposes series of operations that operate on up to two qubits into a sequence of up to three + two-qubit basis gates and single-qubit operations. + }]; +} + +#endif // QCO_PASSES diff --git a/mlir/lib/CMakeLists.txt b/mlir/lib/CMakeLists.txt index afc74137bd..7f46084b8c 100644 --- a/mlir/lib/CMakeLists.txt +++ b/mlir/lib/CMakeLists.txt @@ -6,7 +6,8 @@ # # Licensed under the MIT License -add_subdirectory(Dialect) add_subdirectory(Conversion) add_subdirectory(Compiler) +add_subdirectory(Dialect) +add_subdirectory(Passes) add_subdirectory(Support) diff --git a/mlir/lib/Compiler/CMakeLists.txt b/mlir/lib/Compiler/CMakeLists.txt index 97b700b5c5..df7033e208 100644 --- a/mlir/lib/Compiler/CMakeLists.txt +++ b/mlir/lib/Compiler/CMakeLists.txt @@ -22,6 +22,7 @@ add_mlir_library( QCToQIR MQT::MLIRSupport MQT::ProjectOptions + QcoPasses DISABLE_INSTALL) # collect header files diff --git a/mlir/lib/Compiler/CompilerPipeline.cpp b/mlir/lib/Compiler/CompilerPipeline.cpp index 02183790f4..bac28a9537 100644 --- a/mlir/lib/Compiler/CompilerPipeline.cpp +++ b/mlir/lib/Compiler/CompilerPipeline.cpp @@ -13,6 +13,7 @@ #include "mlir/Conversion/QCOToQC/QCOToQC.h" #include "mlir/Conversion/QCToQCO/QCToQCO.h" #include "mlir/Conversion/QCToQIR/QCToQIR.h" +#include "mlir/Passes/Passes.h" #include "mlir/Support/PrettyPrinting.h" #include @@ -64,6 +65,11 @@ void QuantumCompilerPipeline::addCleanupPasses(PassManager& pm) { pm.addPass(createRemoveDeadValuesPass()); } +void QuantumCompilerPipeline::addOptimizationPasses(PassManager& pm) { + // Always run all optimization passes for now + pm.addPass(qco::createGateDecompositionPass()); +} + void QuantumCompilerPipeline::configurePassManager(PassManager& pm) const { // Enable timing statistics if requested if (config_.enableTiming) { @@ -118,17 +124,17 @@ QuantumCompilerPipeline::runPipeline(ModuleOp module, } // Stage 2: QC canonicalization - addCleanupPasses(pm); - if (pm.run(module).failed()) { - return failure(); - } - if (record != nullptr && config_.recordIntermediates) { - record->afterInitialCanon = captureIR(module); - if (config_.printIRAfterAllStages) { - prettyPrintStage(module, "Initial QC Canonicalization", ++currentStage, - totalStages); - } - } + // addCleanupPasses(pm); + // if (pm.run(module).failed()) { + // return failure(); + // } + // if (record != nullptr && config_.recordIntermediates) { + // record->afterInitialCanon = captureIR(module); + // if (config_.printIRAfterAllStages) { + // prettyPrintStage(module, "Initial QC Canonicalization", ++currentStage, + // totalStages); + // } + // } pm.clear(); // Stage 3: QC-to-QCO conversion @@ -146,21 +152,22 @@ QuantumCompilerPipeline::runPipeline(ModuleOp module, pm.clear(); // Stage 4: QCO canonicalization - addCleanupPasses(pm); - if (failed(pm.run(module))) { - return failure(); - } - if (record != nullptr && config_.recordIntermediates) { - record->afterQCOCanon = captureIR(module); - if (config_.printIRAfterAllStages) { - prettyPrintStage(module, "Initial QCO Canonicalization", ++currentStage, - totalStages); - } - } + // addCleanupPasses(pm); + // if (failed(pm.run(module))) { + // return failure(); + // } + // if (record != nullptr && config_.recordIntermediates) { + // record->afterQCOCanon = captureIR(module); + // if (config_.printIRAfterAllStages) { + // prettyPrintStage(module, "Initial QCO Canonicalization", + // ++currentStage, + // totalStages); + // } + // } pm.clear(); // Stage 5: Optimization passes - // TODO: Add optimization passes + addOptimizationPasses(pm); addCleanupPasses(pm); if (failed(pm.run(module))) { return failure(); @@ -175,17 +182,17 @@ QuantumCompilerPipeline::runPipeline(ModuleOp module, pm.clear(); // Stage 6: QCO canonicalization - addCleanupPasses(pm); - if (failed(pm.run(module))) { - return failure(); - } - if (record != nullptr && config_.recordIntermediates) { - record->afterOptimizationCanon = captureIR(module); - if (config_.printIRAfterAllStages) { - prettyPrintStage(module, "Final QCO Canonicalization", ++currentStage, - totalStages); - } - } + // addCleanupPasses(pm); + // if (failed(pm.run(module))) { + // return failure(); + // } + // if (record != nullptr && config_.recordIntermediates) { + // record->afterOptimizationCanon = captureIR(module); + // if (config_.printIRAfterAllStages) { + // prettyPrintStage(module, "Final QCO Canonicalization", ++currentStage, + // totalStages); + // } + // } pm.clear(); // Stage 7: QCO-to-QC conversion @@ -203,17 +210,17 @@ QuantumCompilerPipeline::runPipeline(ModuleOp module, pm.clear(); // Stage 8: QC canonicalization - addCleanupPasses(pm); - if (failed(pm.run(module))) { - return failure(); - } - if (record != nullptr && config_.recordIntermediates) { - record->afterQCCanon = captureIR(module); - if (config_.printIRAfterAllStages) { - prettyPrintStage(module, "Final QC Canonicalization", ++currentStage, - totalStages); - } - } + // addCleanupPasses(pm); + // if (failed(pm.run(module))) { + // return failure(); + // } + // if (record != nullptr && config_.recordIntermediates) { + // record->afterQCCanon = captureIR(module); + // if (config_.printIRAfterAllStages) { + // prettyPrintStage(module, "Final QC Canonicalization", ++currentStage, + // totalStages); + // } + // } pm.clear(); // Stage 9: QC-to-QIR conversion (optional) diff --git a/mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp b/mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp index caa07a6e7c..2bf1d5b205 100644 --- a/mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp @@ -145,7 +145,9 @@ struct CtrlInlineId final : OpRewritePattern { } // namespace UnitaryOpInterface CtrlOp::getBodyUnitary() { - return llvm::dyn_cast(&getBody()->front()); + auto bodyUnitary = llvm::dyn_cast(&getBody()->front()); + assert(bodyUnitary); + return bodyUnitary; } size_t CtrlOp::getNumQubits() { return getNumTargets() + getNumControls(); } @@ -160,7 +162,7 @@ Value CtrlOp::getInputQubit(const size_t i) { return getControlsIn()[i]; } if (numControls <= i && i < getNumQubits()) { - return getBodyUnitary().getInputQubit(i - numControls); + return getTargetsIn()[i - numControls]; } llvm::reportFatalUsageError("Invalid qubit index"); } @@ -171,7 +173,7 @@ Value CtrlOp::getOutputQubit(const size_t i) { return getControlsOut()[i]; } if (numControls <= i && i < getNumQubits()) { - return getBodyUnitary().getOutputQubit(i - numControls); + return getTargetsOut()[i - numControls]; } llvm::reportFatalUsageError("Invalid qubit index"); } diff --git a/mlir/lib/Passes/CMakeLists.txt b/mlir/lib/Passes/CMakeLists.txt new file mode 100644 index 0000000000..cfa3558868 --- /dev/null +++ b/mlir/lib/Passes/CMakeLists.txt @@ -0,0 +1,42 @@ +# 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 + +get_property(dialect_libs GLOBAL PROPERTY MLIR_DIALECT_LIBS) +set(LIBRARIES ${dialect_libs} MQT::CoreIR Eigen3::Eigen) +add_compile_options(-fexceptions) + +file(GLOB_RECURSE PASSES_SOURCES *.cpp) + +add_mlir_library( + QcoPasses + ${PASSES_SOURCES} + LINK_LIBS + PUBLIC + ${LIBRARIES} + DEPENDS + QcoPassesIncGen) + +# collect header files +file(GLOB_RECURSE PASSES_HEADERS_SOURCE ${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Passes/*.h) +file(GLOB_RECURSE PASSES_HEADERS_BUILD ${MQT_MLIR_BUILD_INCLUDE_DIR}/mlir/Passes/*.inc) + +# add public headers using file sets +target_sources( + QcoPasses + PUBLIC FILE_SET + HEADERS + BASE_DIRS + ${MQT_MLIR_SOURCE_INCLUDE_DIR} + FILES + ${PASSES_HEADERS_SOURCE} + FILE_SET + HEADERS + BASE_DIRS + ${MQT_MLIR_BUILD_INCLUDE_DIR} + FILES + ${PASSES_HEADERS_BUILD}) diff --git a/mlir/lib/Passes/GateDecompositionPass.cpp b/mlir/lib/Passes/GateDecompositionPass.cpp new file mode 100644 index 0000000000..5ac5ded2d4 --- /dev/null +++ b/mlir/lib/Passes/GateDecompositionPass.cpp @@ -0,0 +1,137 @@ +/* + * 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/Passes/Passes.h" + +#include +#include +#include +#include + +namespace mlir::qco { + +#define GEN_PASS_DEF_GATEDECOMPOSITIONPASS +#include "mlir/Passes/Passes.h.inc" + +/** + * @brief This pass attempts to collect as many operations as possible into a + * 4x4 unitary matrix and then decompose it into 1q rotations and 2q + * basis gates. + */ +struct GateDecompositionPass final + : impl::GateDecompositionPassBase { + + GateDecompositionPass() = default; + GateDecompositionPass(const GateDecompositionPass& other) + : impl::GateDecompositionPassBase{other}, + twoQubitCreationTime{this, "twoQubitCreationTime", + "Creation time of basis decomposers"}, + numberOfTwoQubitCreations{ + this, "numberOfTwoQubitCreations", + "Number of times basis decomposers are created"}, + successfulSingleQubitDecompositions{ + this, "successfulSingleQubitDecompositions", + "Number of times a single-qubit decomposition was applied"}, + totalSingleQubitDecompositions{this, "totalSingleQubitDecompositions", + "Number of times (only) a single-qubit " + "decomposition was calculated"}, + successfulTwoQubitDecompositions{ + this, "successfulTwoQubitDecompositions", + "Number of times a two-qubit decomposition was applied"}, + totalTwoQubitDecompositions{ + this, "totalTwoQubitDecompositions", + "Number of times a two-qubit decomposition was calculated"}, + totalCircuitCollections{this, "totalCircuitCollections", + "Number of times a sub-circuit was collected"}, + totalTouchedGates{ + this, "totalTouchedGates", + "Number of gates that were looked at (in sub-circuit collection)"}, + subCircuitComplexityChange{ + this, "subCircuitComplexityChange", + "Increase or decrease of complexity in sub-circuit"}, + timeInCircuitCollection{this, "timeInCircuitCollection", + "Time spent in circuit collection (µs)"}, + timeInSingleQubitDecomposition{ + this, "timeInSingleQubitDecomposition", + "Time spent in single-qubit decomposition (µs)"}, + timeInTwoQubitDecomposition{ + this, "timeInTwoQubitDecomposition", + "Time spent in single-qubit decomposition (µs)"} {} + GateDecompositionPass(GateDecompositionPass&& other) = delete; + GateDecompositionPass& operator=(const GateDecompositionPass& other) = delete; + GateDecompositionPass& operator=(GateDecompositionPass&& other) = delete; + + void runOnOperation() override { + // Get the current operation being operated on. + auto op = getOperation(); + auto* ctx = &getContext(); + + // Define the set of patterns to use. + mlir::RewritePatternSet patterns(ctx); + populateGateDecompositionPatterns( + patterns, twoQubitCreationTime, numberOfTwoQubitCreations, + successfulSingleQubitDecompositions, totalSingleQubitDecompositions, + successfulTwoQubitDecompositions, totalTwoQubitDecompositions, + totalCircuitCollections, totalTouchedGates, subCircuitComplexityChange, + timeInCircuitCollection, timeInSingleQubitDecomposition, + timeInTwoQubitDecomposition); + + // Configure greedy driver + mlir::GreedyRewriteConfig config; + // start at top of program to maximize collected sub-circuits + config.setUseTopDownTraversal(true); + // only optimize existing operations to avoid unnecessary sub-circuit + // collections of already decomposed gates + config.setStrictness(GreedyRewriteStrictness::ExistingOps); + + // Apply patterns in an iterative and greedy manner. + if (mlir::failed( + mlir::applyPatternsGreedily(op, std::move(patterns), config))) { + signalPassFailure(); + } + } + + Statistic twoQubitCreationTime{this, "twoQubitCreationTime", + "Creation time of basis decomposers"}; + Statistic numberOfTwoQubitCreations{ + this, "numberOfTwoQubitCreations", + "Number of times basis decomposers are created"}; + Statistic successfulSingleQubitDecompositions{ + this, "successfulSingleQubitDecompositions", + "Number of times a single-qubit decomposition was applied"}; + Statistic totalSingleQubitDecompositions{ + this, "totalSingleQubitDecompositions", + "Number of times (only) a single-qubit decomposition was calculated"}; + Statistic successfulTwoQubitDecompositions{ + this, "successfulTwoQubitDecompositions", + "Number of times a two-qubit decomposition was applied"}; + Statistic totalTwoQubitDecompositions{ + this, "totalTwoQubitDecompositions", + "Number of times a two-qubit decomposition was calculated"}; + Statistic totalCircuitCollections{ + this, "totalCircuitCollections", + "Number of times a sub-circuit was collected"}; + Statistic totalTouchedGates{ + this, "totalTouchedGates", + "Number of gates that were looked at (in sub-circuit collection)"}; + Statistic subCircuitComplexityChange{ + this, "subCircuitComplexityChange", + "Increase or decrease of complexity in sub-circuit"}; + Statistic timeInCircuitCollection{this, "timeInCircuitCollection", + "Time spent in circuit collection (µs)"}; + Statistic timeInSingleQubitDecomposition{ + this, "timeInSingleQubitDecomposition", + "Time spent in single-qubit decomposition (µs)"}; + Statistic timeInTwoQubitDecomposition{ + this, "timeInTwoQubitDecomposition", + "Time spent in single-qubit decomposition (µs)"}; +}; + +} // namespace mlir::qco diff --git a/mlir/lib/Passes/Patterns/GateDecompositionPattern.cpp b/mlir/lib/Passes/Patterns/GateDecompositionPattern.cpp new file mode 100644 index 0000000000..35da3c26b1 --- /dev/null +++ b/mlir/lib/Passes/Patterns/GateDecompositionPattern.cpp @@ -0,0 +1,748 @@ +/* + * 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 "ir/operations/OpType.hpp" +#include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Passes/Decomposition/BasisDecomposer.h" +#include "mlir/Passes/Decomposition/EulerBasis.h" +#include "mlir/Passes/Decomposition/EulerDecomposition.h" +#include "mlir/Passes/Decomposition/Gate.h" +#include "mlir/Passes/Decomposition/GateSequence.h" +#include "mlir/Passes/Decomposition/Helpers.h" +#include "mlir/Passes/Decomposition/UnitaryMatrices.h" +#include "mlir/Passes/Decomposition/WeylDecomposition.h" +#include "mlir/Passes/Passes.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mlir::qco { + +/** + * @brief This pattern attempts to collect as many operations as possible into a + * 4x4 unitary matrix and then decompose it into rotation and given basis + * gates. + */ +struct GateDecompositionPattern final + : mlir::OpInterfaceRewritePattern { + using EulerBasis = decomposition::EulerBasis; + using Gate = decomposition::Gate; + + /** + * Initialize pattern with a set of basis gates and euler bases. + * The best combination of (basis gate, euler basis) will be evaluated for + * each decomposition. + * + * @param context MLIR context in which the pattern is applied + * @param basisGate Set of two-qubit gates that should be used in the + * decomposition. All two-qubit interactions will be + * represented by one of the gates in this set + * @param eulerBasis Set of euler bases that should be used for the + * decomposition of local single-qubit modifications. For + * each necessary single-qubit operation, the optimal basis + * will be chosen from this set + * @param singleQubitOnly If true, only perform single-qubit decompositions + * and no two-qubit decompositions + * @param forceApplication If true, always apply best decomposition, even if + * it is longer/more complex than the previous + * circuit. To prevent recursion, this will not apply + * a decomposition if the (sub)circuit only contains + * gates available as basis gates or euler bases + */ + explicit GateDecompositionPattern( + mlir::MLIRContext* context, llvm::SmallVector basisGate, + llvm::SmallVector eulerBasis, bool singleQubitOnly, + bool forceApplication, llvm::Statistic& twoQubitCreationTime, + llvm::Statistic& numberOfTwoQubitCreations, + llvm::Statistic& successfulSingleQubitDecompositions, + llvm::Statistic& totalSingleQubitDecompositions, + llvm::Statistic& successfulTwoQubitDecompositions, + llvm::Statistic& totalTwoQubitDecompositions, + llvm::Statistic& totalCircuitCollections, + llvm::Statistic& totalTouchedGates, + llvm::Statistic& subCircuitComplexityChange, + llvm::Statistic& timeInCircuitCollection, + llvm::Statistic& timeInSingleQubitDecomposition, + llvm::Statistic& timeInTwoQubitDecomposition) + : OpInterfaceRewritePattern(context), + decomposerBasisGates{std::move(basisGate)}, + decomposerEulerBases{std::move(eulerBasis)}, + singleQubitOnly{singleQubitOnly}, forceApplication{forceApplication}, + twoQubitCreationTime{twoQubitCreationTime}, + numberOfTwoQubitCreations{numberOfTwoQubitCreations}, + successfulSingleQubitDecompositions{ + successfulSingleQubitDecompositions}, + totalSingleQubitDecompositions{totalSingleQubitDecompositions}, + successfulTwoQubitDecompositions{successfulTwoQubitDecompositions}, + totalTwoQubitDecompositions{totalTwoQubitDecompositions}, + totalCircuitCollections{totalCircuitCollections}, + totalTouchedGates{totalTouchedGates}, + subCircuitComplexityChange{subCircuitComplexityChange}, + timeInCircuitCollection{timeInCircuitCollection}, + timeInSingleQubitDecomposition{timeInSingleQubitDecomposition}, + timeInTwoQubitDecomposition{timeInTwoQubitDecomposition} { + ++numberOfTwoQubitCreations; + auto startTime = std::chrono::steady_clock::now(); + for (auto&& basisGate : decomposerBasisGates) { + basisDecomposers.push_back(decomposition::TwoQubitBasisDecomposer::create( + basisGate, DEFAULT_FIDELITY)); + } + auto endTime = std::chrono::steady_clock::now(); + twoQubitCreationTime += + std::chrono::duration_cast(endTime - + startTime) + .count(); + } + + mlir::LogicalResult + matchAndRewrite(UnitaryOpInterface op, + mlir::PatternRewriter& rewriter) const override { + if (op->getParentOfType()) { + // application of pattern might not work on gates inside a control + // modifier because rotation gates need to create new constants which are + // not allowed inside a control body; also, the foreign gate dection does + // not work and e.g. a CNOT will not be recognized as such and thus will + // be further decomposed into a RX gate inside the control body which is + // most likely undesired + return mlir::failure(); + } + + auto collectSeries = [this](UnitaryOpInterface op, bool singleQubitOnly) { + ++totalCircuitCollections; + if (singleQubitOnly) { + return TwoQubitSeries::getSingleQubitSeries(op); + } + return TwoQubitSeries::getTwoQubitSeries(op); + }; + auto startTime = std::chrono::steady_clock::now(); + auto series = collectSeries(op, singleQubitOnly); + auto endTime = std::chrono::steady_clock::now(); + timeInCircuitCollection += + std::chrono::duration_cast(endTime - + startTime) + .count(); + // not really accurate since it neglects the "past the series" gates that + // terminated the series + totalTouchedGates += series.gates.size(); + + auto&& [singleQubitGates, twoQubitGates] = getDecompositionGates(); + auto containsForeignGates = + !series.containsOnlyGates(singleQubitGates, twoQubitGates); + + if (series.gates.empty() || (series.gates.size() < 3 && + !(forceApplication && containsForeignGates))) { + // empty or too short and only contains valid gates anyway + return mlir::failure(); + } + + std::optional bestSequence; + + if (series.isSingleQubitSeries()) { + // only a single-qubit series; + // single-qubit euler decomposition is more efficient + const auto unitaryMatrix = series.getSingleQubitUnitaryMatrix(); + if (!unitaryMatrix) { + // cannot process decomposition without the matrix of the series + return mlir::failure(); + } + // only count the multiple decompositions as "one" since the number of + // euler bases is constant + ++totalSingleQubitDecompositions; + startTime = std::chrono::steady_clock::now(); + for (auto&& eulerBasis : decomposerEulerBases) { + auto sequence = decomposition::EulerDecomposition::generateCircuit( + eulerBasis, *unitaryMatrix, true, std::nullopt); + if (!bestSequence || + sequence.complexity() < bestSequence->complexity()) { + bestSequence = sequence; + } + } + endTime = std::chrono::steady_clock::now(); + timeInSingleQubitDecomposition += + std::chrono::duration_cast(endTime - + startTime) + .count(); + } else { + // two-qubit series; perform two-qubit basis decomposition + const auto unitaryMatrix = series.getUnitaryMatrix(); + if (!unitaryMatrix) { + // cannot process decomposition without the matrix of the series + return mlir::failure(); + } + const auto targetDecomposition = + decomposition::TwoQubitWeylDecomposition::create(*unitaryMatrix, + DEFAULT_FIDELITY); + + // only count the multiple decompositions as "one" since the number of + // euler bases is constant + ++totalTwoQubitDecompositions; + startTime = std::chrono::steady_clock::now(); + for (const auto& decomposer : basisDecomposers) { + auto sequence = decomposer.twoQubitDecompose( + targetDecomposition, decomposerEulerBases, DEFAULT_FIDELITY, true, + std::nullopt); + if (sequence) { + // decomposition successful + if (!bestSequence || + sequence->complexity() < bestSequence->complexity()) { + // this decomposition is better than any successful decomposition + // before + bestSequence = sequence; + } + } + } + endTime = std::chrono::steady_clock::now(); + timeInTwoQubitDecomposition += + std::chrono::duration_cast(endTime - + startTime) + .count(); + } + + if (!bestSequence) { + return mlir::failure(); + } + llvm::errs() << "\nDecomposition (" << bestSequence->complexity() << "): "; + for (auto&& gate : bestSequence->gates) { + llvm::errs() << qc::toString(gate.type) << ", "; + } + llvm::errs() << "\n"; + // only accept new sequence if it shortens existing series by more than two + // gates; this prevents an oscillation with phase gates + if (bestSequence->complexity() + 2 >= series.complexity && + !(forceApplication && containsForeignGates)) { + return mlir::failure(); + } + + if (series.isSingleQubitSeries()) { + ++successfulSingleQubitDecompositions; + } else { + ++successfulTwoQubitDecompositions; + } + subCircuitComplexityChange += + series.complexity - bestSequence->complexity(); + + applySeries(rewriter, series, *bestSequence); + + return mlir::success(); + } + +protected: + /** + * Factor by which two matrices are considered to be the same when simplifying + * during a decomposition. + */ + static constexpr auto DEFAULT_FIDELITY = 1.0 - 1e-15; + static constexpr auto SANITY_CHECK_PRECISION = + decomposition::SANITY_CHECK_PRECISION; + + using QubitId = decomposition::QubitId; + struct TwoQubitSeries { + /** + * Complexity of series using getComplexity() for each gate. + */ + std::size_t complexity{0}; + /** + * Qubits that are the input for the series. + * First qubit will always be set, second qubit may be equal to + * mlir::Value{} if the series consists of only single-qubit gates. + */ + std::array inQubits{}; + /** + * Qubits that are the output for the series. + * First qubit will always be set, second qubit may be equal to + * mlir::Value{} if the series consists of only single-qubit gates. + */ + std::array outQubits{}; + + struct MlirGate { + UnitaryOpInterface op; + llvm::SmallVector qubitIds; + }; + llvm::SmallVector gates; + + [[nodiscard]] static TwoQubitSeries + getSingleQubitSeries(UnitaryOpInterface op) { + if (isBarrier(op) || !op.isSingleQubit()) { + return {}; + } + TwoQubitSeries result(op); + + while (auto user = getUser(result.outQubits[0], + &helpers::isSingleQubitOperation)) { + if (!result.appendSingleQubitGate(*user)) { + break; + } + } + + assert(result.isSingleQubitSeries()); + return result; + } + + [[nodiscard]] static TwoQubitSeries + getTwoQubitSeries(UnitaryOpInterface op) { + if (isBarrier(op)) { + return {}; + } + TwoQubitSeries result(op); + + bool foundGate = true; + while (foundGate) { + foundGate = false; + // collect all available single-qubit operations + for (std::size_t i = 0; i < result.outQubits.size(); ++i) { + while (auto user = getUser(result.outQubits[i], + &helpers::isSingleQubitOperation)) { + foundGate = result.appendSingleQubitGate(*user); + if (!foundGate) { + // result.outQubits was not updated, prevent endless loop + break; + } + } + } + + for (std::size_t i = 0; i < result.outQubits.size(); ++i) { + if (auto user = + getUser(result.outQubits[i], &helpers::isTwoQubitOperation)) { + foundGate = result.appendTwoQubitGate(*user); + break; // go back to single-qubit collection + } + } + } + return result; + } + + [[nodiscard]] std::optional getSingleQubitUnitaryMatrix() { + auto unitaryMatrix = decomposition::IDENTITY_GATE; + for (auto&& gate : gates) { + if (auto gateMatrix = gate.op.getUnitaryMatrix()) { + unitaryMatrix = *gateMatrix * unitaryMatrix; + } else { + return std::nullopt; + } + } + + assert(helpers::isUnitaryMatrix(unitaryMatrix)); + return unitaryMatrix; + } + + [[nodiscard]] std::optional getUnitaryMatrix() { + matrix4x4 unitaryMatrix = helpers::kroneckerProduct( + decomposition::IDENTITY_GATE, decomposition::IDENTITY_GATE); + matrix4x4 gateMatrix; + for (auto&& gate : gates) { + if (gate.op.isSingleQubit()) { + assert(gate.qubitIds.size() == 1); + auto matrix = gate.op.getUnitaryMatrix(); + if (!matrix) { + return std::nullopt; + } + gateMatrix = + decomposition::expandToTwoQubits(*matrix, gate.qubitIds[0]); + } else if (gate.op.isTwoQubit()) { + if (auto matrix = gate.op.getUnitaryMatrix()) { + gateMatrix = decomposition::fixTwoQubitMatrixQubitOrder( + *matrix, gate.qubitIds); + } else { + return std::nullopt; + } + } + unitaryMatrix = gateMatrix * unitaryMatrix; + } + + assert(helpers::isUnitaryMatrix(unitaryMatrix)); + return unitaryMatrix; + } + + [[nodiscard]] bool isSingleQubitSeries() const { + return llvm::is_contained(inQubits, mlir::Value{}) || + llvm::is_contained(outQubits, mlir::Value{}); + } + + [[nodiscard]] bool + containsOnlyGates(const llvm::SetVector& singleQubitGates, + const llvm::SetVector& twoQubitGates) { + return llvm::all_of(gates, [&](auto&& gate) { + auto&& gateType = helpers::getQcType(gate.op); + return (gate.qubitIds.size() == 1 && + singleQubitGates.contains(gateType)) || + (gate.qubitIds.size() == 2 && twoQubitGates.contains(gateType)); + }); + } + + private: + /** + * Initialize empty TwoQubitSeries instance. + * New operations can *NOT* be added when calling this constructor overload. + */ + TwoQubitSeries() = default; + /** + * Initialize TwoQubitSeries instance with given first operation. + */ + explicit TwoQubitSeries(UnitaryOpInterface initialOperation) { + if (helpers::isSingleQubitOperation(initialOperation)) { + inQubits = {initialOperation.getInputQubit(0), mlir::Value{}}; + outQubits = {initialOperation.getOutputQubit(0), mlir::Value{}}; + gates.push_back({.op = initialOperation, .qubitIds = {0}}); + } else if (helpers::isTwoQubitOperation(initialOperation)) { + inQubits = {initialOperation.getInputQubit(0), + initialOperation.getInputQubit(1)}; + outQubits = {initialOperation.getOutputQubit(0), + initialOperation.getOutputQubit(1)}; + gates.push_back({.op = initialOperation, .qubitIds = {0, 1}}); + } + complexity += helpers::getComplexity(helpers::getQcType(initialOperation), + initialOperation.getNumQubits()); + } + + /** + * @return true if series continues, otherwise false + * (will always return true) + */ + bool appendSingleQubitGate(UnitaryOpInterface nextGate) { + if (isBarrier(nextGate)) { + return false; + } + auto operand = nextGate.getInputQubit(0); + // NOLINTNEXTLINE(readability-qualified-auto) + auto it = llvm::find(outQubits, operand); + if (it == outQubits.end()) { + throw std::logic_error{"Operand of single-qubit op and user of " + "qubit is not in current outQubits"}; + } + QubitId qubitId = std::distance(outQubits.begin(), it); + *it = nextGate->getResult(0); + + gates.push_back({.op = nextGate, .qubitIds = {qubitId}}); + complexity += helpers::getComplexity(helpers::getQcType(nextGate), 1); + return true; + } + + /** + * @return true if series continues, otherwise false + */ + bool appendTwoQubitGate(UnitaryOpInterface nextGate) { + if (isBarrier(nextGate)) { + // a barrier operation should not be crossed for a decomposition; + // ignore possitility to backtrack (if this is the first two-qubit gate) + // since two single-qubit decompositions are less expensive than one + // two-qubit decomposition + return false; + } + auto&& firstOperand = nextGate.getInputQubit(0); + auto&& secondOperand = nextGate.getInputQubit(1); + assert(firstOperand != secondOperand); + auto firstQubitIt = // NOLINT(readability-qualified-auto) + llvm::find(outQubits, firstOperand); + auto secondQubitIt = // NOLINT(readability-qualified-auto) + llvm::find(outQubits, secondOperand); + assert(firstQubitIt != secondQubitIt); + if (firstQubitIt == outQubits.end() || secondQubitIt == outQubits.end()) { + // another qubit is involved, series is finished (except there only + // has been one qubit so far) + auto it = // NOLINT(readability-qualified-auto) + llvm::find(outQubits, mlir::Value{}); + if (it == outQubits.end()) { + return false; + } + // TODO: this only works because parameters are at end of operands; + // use to-be-implemented getInputQubits() instead + auto getInputQubits = + [](UnitaryOpInterface op) -> llvm::SmallVector { + if (auto&& ctrlOp = llvm::dyn_cast(*op)) { + auto&& range = llvm::concat(ctrlOp.getTargetsIn(), + ctrlOp.getControlsIn()); + return {range.begin(), range.end()}; + } + return op->getOperands(); + }; + auto&& opInQubits = getInputQubits(nextGate); + // iterator in the operation input of "old" qubit that already has + // previous single-qubit gates in this series + auto it2 = llvm::find(opInQubits, firstQubitIt != outQubits.end() + ? *firstQubitIt + : *secondQubitIt); + assert(it2 != opInQubits.end()); + // new qubit ID based on position in outQubits + const QubitId newInQubitId = std::distance(outQubits.begin(), it); + // position in operation input; since there are only two qubits, it must + // be the "not old" one + const QubitId newOpInQubitId = + 1 - std::distance(opInQubits.begin(), it2); + + // update inQubit and update dangling iterator, then proceed as usual + inQubits[newInQubitId] = opInQubits[newOpInQubitId]; + firstQubitIt = (firstQubitIt != outQubits.end()) ? firstQubitIt : it; + secondQubitIt = (secondQubitIt != outQubits.end()) ? secondQubitIt : it; + + // before proceeding as usual, see if backtracking on the "new" qubit is + // possible to collect other single-qubit operations + backtrackSingleQubitSeries(newInQubitId); + } + const QubitId firstQubitId = + std::distance(outQubits.begin(), firstQubitIt); + const QubitId secondQubitId = + std::distance(outQubits.begin(), secondQubitIt); + *firstQubitIt = nextGate->getResult(0); + *secondQubitIt = nextGate->getResult(1); + + gates.push_back( + {.op = nextGate, .qubitIds = {firstQubitId, secondQubitId}}); + complexity += helpers::getComplexity(helpers::getQcType(nextGate), 2); + return true; + } + + /** + * Traverse single-qubit series back from a given qubit. + * This is used when a series starts with single-qubit gates and then + * encounters a two-qubit gate. The second qubit involved in the two-qubit + * gate could have previous single-qubit operations that can be incorporated + * in the series. + */ + void backtrackSingleQubitSeries(QubitId qubitId) { + auto prependSingleQubitGate = [&](UnitaryOpInterface op) { + inQubits[qubitId] = op.getInputQubit(0); + gates.insert(gates.begin(), {.op = op, .qubitIds = {qubitId}}); + complexity += helpers::getComplexity(helpers::getQcType(op), 1); + // outQubits do not need to be updated because the final out qubit is + // already fixed + }; + while (auto* op = inQubits[qubitId].getDefiningOp()) { + auto unitaryOp = mlir::dyn_cast(op); + if (unitaryOp && helpers::isSingleQubitOperation(unitaryOp) && + !isBarrier(unitaryOp)) { + prependSingleQubitGate(unitaryOp); + } else { + break; + } + } + } + + [[nodiscard]] static bool isBarrier(UnitaryOpInterface op) { + return llvm::isa_and_nonnull(*op); + } + + /** + * + */ + template + static std::optional getUser(mlir::Value qubit, + Func&& filter) { + if (qubit) { + auto users = qubit.getUsers(); + auto userIt = users.begin(); + assert(qubit.hasOneUse()); + auto user = mlir::dyn_cast(*userIt); + if (user && std::invoke(std::forward(filter), user)) { + return user; + } + } + return std::nullopt; + }; + }; + + template + static OpType createGate(mlir::PatternRewriter& rewriter, + mlir::Location location, + Args&&... inQubitsAndParams) { + return rewriter.create(location, + std::forward(inQubitsAndParams)...); + } + + template + static CtrlOp createControlledGate(mlir::PatternRewriter& rewriter, + mlir::Location location, + mlir::ValueRange ctrlQubits, + Args&&... inQubitsAndParams) { + llvm::SmallVector inQubits; + auto collectInQubits = [&inQubits](auto&& x) { + if constexpr (std::is_same_v, + mlir::Value>) { + // if argument is a qubit, add it to list; otherwise, do nothing + inQubits.push_back(std::forward(x)); + } + }; + (collectInQubits(inQubitsAndParams), ...); + return rewriter.create( + location, ctrlQubits, mlir::ValueRange{inQubits}, + createGate(rewriter, location, + std::forward(inQubitsAndParams)...)); + } + + static void applySeries(mlir::PatternRewriter& rewriter, + TwoQubitSeries& series, + const decomposition::TwoQubitGateSequence& sequence) { + auto& lastSeriesOp = series.gates.back().op; + auto location = lastSeriesOp->getLoc(); + rewriter.setInsertionPointAfter(lastSeriesOp); + + auto inQubits = series.inQubits; + auto updateInQubits = + [&inQubits](const llvm::SmallVector& qubitIds, + auto&& newGate) { + if (qubitIds.size() == 2) { + inQubits[qubitIds[0]] = newGate.getOutputQubit(0); + inQubits[qubitIds[1]] = newGate.getOutputQubit(1); + } else if (qubitIds.size() == 1) { + inQubits[qubitIds[0]] = newGate.getOutputQubit(0); + } else { + throw std::logic_error{"Invalid number of qubit IDs!"}; + } + }; + + if (sequence.hasGlobalPhase()) { + createGate(rewriter, location, sequence.globalPhase); + } + + matrix4x4 unitaryMatrix = helpers::kroneckerProduct( + decomposition::IDENTITY_GATE, decomposition::IDENTITY_GATE); + for (auto&& gate : sequence.gates) { + // TODO: need to add each basis gate we want to use + if (gate.type == qc::X && gate.qubitId.size() > 1) { + // X gate involving more than one qubit is a CX gate: + // qubit position 0 is target, 1 is control + auto newGate = createControlledGate(rewriter, location, + {inQubits[gate.qubitId[1]]}, + inQubits[gate.qubitId[0]]); + unitaryMatrix = decomposition::fixTwoQubitMatrixQubitOrder( + newGate.getUnitaryMatrix().value(), gate.qubitId) * + unitaryMatrix; + updateInQubits(gate.qubitId, newGate); + } else if (gate.type == qc::RX) { + assert(gate.qubitId.size() == 1); + auto newGate = createGate( + rewriter, location, inQubits[gate.qubitId[0]], gate.parameter[0]); + unitaryMatrix = + decomposition::expandToTwoQubits(newGate.getUnitaryMatrix().value(), + gate.qubitId[0]) * + unitaryMatrix; + updateInQubits(gate.qubitId, newGate); + } else if (gate.type == qc::RY) { + assert(gate.qubitId.size() == 1); + auto newGate = createGate( + rewriter, location, inQubits[gate.qubitId[0]], gate.parameter[0]); + unitaryMatrix = + decomposition::expandToTwoQubits(newGate.getUnitaryMatrix().value(), + gate.qubitId[0]) * + unitaryMatrix; + updateInQubits(gate.qubitId, newGate); + } else if (gate.type == qc::RZ) { + assert(gate.qubitId.size() == 1); + auto newGate = createGate( + rewriter, location, inQubits[gate.qubitId[0]], gate.parameter[0]); + unitaryMatrix = + decomposition::expandToTwoQubits(newGate.getUnitaryMatrix().value(), + gate.qubitId[0]) * + unitaryMatrix; + updateInQubits(gate.qubitId, newGate); + } else { + throw std::runtime_error{"Unknown gate type!"}; + } + } + assert((unitaryMatrix * std::exp(IM * sequence.globalPhase)) + .isApprox(series.getUnitaryMatrix().value_or(matrix4x4::Zero()), + SANITY_CHECK_PRECISION)); + + if (series.isSingleQubitSeries()) { + rewriter.replaceAllUsesWith(series.outQubits[0], inQubits[0]); + } else { + rewriter.replaceAllUsesWith(series.outQubits, inQubits); + } + for (auto&& gate : llvm::reverse(series.gates)) { + rewriter.eraseOp(gate.op); + } + } + + [[nodiscard]] std::array, 2> + getDecompositionGates() const { + llvm::SetVector eulerBasesGates; + llvm::SetVector basisGates; + for (auto&& eulerBasis : decomposerEulerBases) { + eulerBasesGates.insert_range( + decomposition::getGateTypesForEulerBasis(eulerBasis)); + } + for (auto&& basisGate : decomposerBasisGates) { + basisGates.insert(basisGate.type); + } + return {eulerBasesGates, basisGates}; + } + +private: + llvm::SmallVector decomposerBasisGates; + llvm::SmallVector basisDecomposers; + llvm::SmallVector decomposerEulerBases; + bool singleQubitOnly; + bool forceApplication; + + llvm::Statistic& twoQubitCreationTime; + llvm::Statistic& numberOfTwoQubitCreations; + llvm::Statistic& successfulSingleQubitDecompositions; + llvm::Statistic& totalSingleQubitDecompositions; + llvm::Statistic& successfulTwoQubitDecompositions; + llvm::Statistic& totalTwoQubitDecompositions; + llvm::Statistic& totalCircuitCollections; + llvm::Statistic& totalTouchedGates; + llvm::Statistic& subCircuitComplexityChange; + llvm::Statistic& timeInCircuitCollection; + llvm::Statistic& timeInSingleQubitDecomposition; + llvm::Statistic& timeInTwoQubitDecomposition; +}; + +/** + * @brief Populates the given pattern set with patterns for gate + * decomposition. + */ +void populateGateDecompositionPatterns( + mlir::RewritePatternSet& patterns, llvm::Statistic& twoQubitCreationTime, + llvm::Statistic& numberOfTwoQubitCreations, + llvm::Statistic& successfulSingleQubitDecompositions, + llvm::Statistic& totalSingleQubitDecompositions, + llvm::Statistic& successfulTwoQubitDecompositions, + llvm::Statistic& totalTwoQubitDecompositions, + llvm::Statistic& totalCircuitCollections, + llvm::Statistic& totalTouchedGates, + llvm::Statistic& subCircuitComplexityChange, + llvm::Statistic& timeInCircuitCollection, + llvm::Statistic& timeInSingleQubitDecomposition, + llvm::Statistic& timeInTwoQubitDecomposition) { + llvm::SmallVector basisGates; + llvm::SmallVector eulerBases; + basisGates.push_back({.type = qc::X, .parameter = {}, .qubitId = {0, 1}}); + basisGates.push_back({.type = qc::X, .parameter = {}, .qubitId = {1, 0}}); + eulerBases.push_back(GateDecompositionPattern::EulerBasis::ZYZ); + eulerBases.push_back(GateDecompositionPattern::EulerBasis::XYX); + eulerBases.push_back(GateDecompositionPattern::EulerBasis::ZXZ); + patterns.add( + patterns.getContext(), basisGates, eulerBases, false, true, + twoQubitCreationTime, numberOfTwoQubitCreations, + successfulSingleQubitDecompositions, totalSingleQubitDecompositions, + successfulTwoQubitDecompositions, totalTwoQubitDecompositions, + totalCircuitCollections, totalTouchedGates, subCircuitComplexityChange, + timeInCircuitCollection, timeInSingleQubitDecomposition, + timeInTwoQubitDecomposition); +} + +} // namespace mlir::qco diff --git a/mlir/unittests/CMakeLists.txt b/mlir/unittests/CMakeLists.txt index 86b0e59167..46a46bd865 100644 --- a/mlir/unittests/CMakeLists.txt +++ b/mlir/unittests/CMakeLists.txt @@ -6,11 +6,13 @@ # # Licensed under the MIT License +add_subdirectory(decomposition) add_subdirectory(Compiler) add_subdirectory(Dialect) add_custom_target(mqt-core-mlir-unittests) add_dependencies( - mqt-core-mlir-unittests mqt-core-mlir-compiler-pipeline-test mqt-core-mlir-qco-dialect-test - mqt-core-mlir-dialect-qco-ir-modifiers-test mqt-core-mlir-dialect-utils-test) + mqt-core-mlir-unittests mqt-core-mlir-decomposition-test mqt-core-mlir-compiler-pipeline-test + mqt-core-mlir-qco-dialect-test mqt-core-mlir-dialect-qco-ir-modifiers-test + mqt-core-mlir-dialect-utils-test) diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 65b3510195..9f2ff99466 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -3736,4 +3736,57 @@ TEST_F(CompilerPipelineTest, Bell) { }); } +TEST_F(CompilerPipelineTest, TwoQubitDecomposition) { + ::qc::QuantumComputation comp; + comp.addQubitRegister(2, "q"); + + auto buildCircuit = [](auto& circuit, auto&& qubitGetter) { + auto constant0 = 2.5; + auto constant1 = 1.2; + auto constant2 = 0.5; + circuit.h(qubitGetter(0)); + circuit.cx(qubitGetter(0), qubitGetter(1)); + circuit.rzz(constant0, qubitGetter(0), qubitGetter(1)); + circuit.ry(constant1, qubitGetter(1)); + circuit.ry(constant1, qubitGetter(0)); + circuit.cx(qubitGetter(1), qubitGetter(0)); + circuit.rz(constant2, qubitGetter(0)); + circuit.rxx(constant0, qubitGetter(0), qubitGetter(1)); + circuit.ryy(constant2, qubitGetter(0), qubitGetter(1)); + // make series longer to enforce decomposition + circuit.rxx(0.1, qubitGetter(1), qubitGetter(0)); + circuit.rzz(0.1, qubitGetter(1), qubitGetter(0)); + circuit.rxx(0.1, qubitGetter(1), qubitGetter(0)); + circuit.rzz(0.1, qubitGetter(1), qubitGetter(0)); + circuit.rxx(0.1, qubitGetter(1), qubitGetter(0)); + circuit.rzz(0.1, qubitGetter(1), qubitGetter(0)); + circuit.rxx(0.1, qubitGetter(1), qubitGetter(0)); + circuit.rzz(0.1, qubitGetter(1), qubitGetter(0)); + }; + + buildCircuit(comp, [](auto&& index) { return index; }); + + const auto module = importQuantumCircuit(comp); + ASSERT_TRUE(module); + ASSERT_TRUE(runPipeline(module.get()).succeeded()); + + const auto qco = buildQCOIR([&](qco::QCOProgramBuilder& b) { + auto reg = b.allocQubitRegister(2, "q"); + buildCircuit(b, [®](auto&& index) { return reg[index]; }); + }); + + const auto optimizedQco = buildQCOIR([&](qco::QCOProgramBuilder& b) { + auto reg = b.allocQubitRegister(2, "q"); + buildCircuit(b, [®](auto&& index) { return reg[index]; }); + }); + + verifyAllStages({ + .qcImport = nullptr, + .qcoConversion = qco.get(), + .optimization = optimizedQco.get(), + .qcConversion = nullptr, + .qirConversion = nullptr, + }); +} + } // namespace diff --git a/mlir/unittests/decomposition/CMakeLists.txt b/mlir/unittests/decomposition/CMakeLists.txt new file mode 100644 index 0000000000..ca2cb4cd9d --- /dev/null +++ b/mlir/unittests/decomposition/CMakeLists.txt @@ -0,0 +1,29 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +set(testname "mqt-core-mlir-decomposition-test") +file(GLOB_RECURSE DECOMPOSITION_TEST_SOURCES *.cpp) + +if(NOT TARGET ${testname}) + # create an executable in which the tests will be stored + add_executable(${testname} ${DECOMPOSITION_TEST_SOURCES}) + # link the Google test infrastructure and a default main function to the test executable. + target_link_libraries( + ${testname} + PRIVATE GTest::gmock + GTest::gtest_main + LLVMFileCheck + MLIRPass + MLIRTransforms + MQTCompilerPipeline + MQT::CoreIR + Eigen3::Eigen) + # discover tests + gtest_discover_tests(${testname} DISCOVERY_TIMEOUT 60) + set_target_properties(${testname} PROPERTIES FOLDER unittests) +endif() diff --git a/mlir/unittests/decomposition/test_basis_decomposer.cpp b/mlir/unittests/decomposition/test_basis_decomposer.cpp new file mode 100644 index 0000000000..7d92cee7e4 --- /dev/null +++ b/mlir/unittests/decomposition/test_basis_decomposer.cpp @@ -0,0 +1,193 @@ +/* + * 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 "ir/operations/OpType.hpp" +#include "mlir/Passes/Decomposition/BasisDecomposer.h" +#include "mlir/Passes/Decomposition/EulerBasis.h" +#include "mlir/Passes/Decomposition/Gate.h" +#include "mlir/Passes/Decomposition/GateSequence.h" +#include "mlir/Passes/Decomposition/Helpers.h" +#include "mlir/Passes/Decomposition/UnitaryMatrices.h" +#include "mlir/Passes/Decomposition/WeylDecomposition.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace mlir::qco; +using namespace mlir::qco::decomposition; + +namespace { +[[nodiscard]] matrix4x4 randomUnitaryMatrix() { + [[maybe_unused]] static auto initializeRandom = []() { + // Eigen uses std::rand() internally, use fixed seed for deterministic + // testing behavior + std::srand(123456UL); + return true; + }(); + const matrix4x4 randomMatrix = matrix4x4::Random(); + Eigen::HouseholderQR qr{}; // NOLINT(misc-include-cleaner) + qr.compute(randomMatrix); + const matrix4x4 unitaryMatrix = qr.householderQ(); + assert(helpers::isUnitaryMatrix(unitaryMatrix)); + return unitaryMatrix; +} + +[[nodiscard]] matrix4x4 canonicalGate(fp a, fp b, fp c) { + TwoQubitWeylDecomposition tmp{}; + tmp.a = a; + tmp.b = b; + tmp.c = c; + return tmp.getCanonicalMatrix(); +} +} // namespace + +class BasisDecomposerTest + : public testing::TestWithParam< + std::tuple, matrix4x4>> { +public: + void SetUp() override { + basisGate = std::get<0>(GetParam()); + eulerBases = std::get<1>(GetParam()); + target = std::get<2>(GetParam()); + targetDecomposition = TwoQubitWeylDecomposition::create(target, 1.0); + } + + [[nodiscard]] static matrix4x4 restore(const TwoQubitGateSequence& sequence) { + matrix4x4 matrix = matrix4x4::Identity(); + for (auto&& gate : sequence.gates) { + matrix = getTwoQubitMatrix(gate) * matrix; + } + + matrix *= std::exp(IM * sequence.globalPhase); + return matrix; + } + +protected: + matrix4x4 target; + Gate basisGate; + llvm::SmallVector eulerBases; + TwoQubitWeylDecomposition targetDecomposition; +}; + +TEST_P(BasisDecomposerTest, TestExact) { + const auto& originalMatrix = target; + auto decomposer = TwoQubitBasisDecomposer::create(basisGate, 1.0); + auto decomposedSequence = decomposer.twoQubitDecompose( + targetDecomposition, eulerBases, 1.0, false, std::nullopt); + + ASSERT_TRUE(decomposedSequence.has_value()); + + auto restoredMatrix = restore(*decomposedSequence); + + EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix)) + << "RESULT:\n" + << restoredMatrix << '\n'; +} + +TEST_P(BasisDecomposerTest, TestApproximation) { + const auto& originalMatrix = target; + auto decomposer = TwoQubitBasisDecomposer::create(basisGate, 1.0 - 1e-12); + auto decomposedSequence = decomposer.twoQubitDecompose( + targetDecomposition, eulerBases, 1.0 - 1e-12, true, std::nullopt); + + ASSERT_TRUE(decomposedSequence.has_value()); + + auto restoredMatrix = restore(*decomposedSequence); + + EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix)) + << "RESULT:\n" + << restoredMatrix << '\n'; +} + +TEST(BasisDecomposerTest, Random) { + auto stopTime = std::chrono::steady_clock::now() + std::chrono::seconds{10}; + auto iterations = 0; + + const Gate basisGate{.type = qc::X, .parameter = {}, .qubitId = {0, 1}}; + const llvm::SmallVector eulerBases = {EulerBasis::XYX, + EulerBasis::ZXZ}; + + while (std::chrono::steady_clock::now() < stopTime) { + auto originalMatrix = randomUnitaryMatrix(); + + auto targetDecomposition = + TwoQubitWeylDecomposition::create(originalMatrix, 1.0); + auto decomposer = TwoQubitBasisDecomposer::create(basisGate, 1.0); + auto decomposedSequence = decomposer.twoQubitDecompose( + targetDecomposition, eulerBases, 1.0, true, std::nullopt); + + ASSERT_TRUE(decomposedSequence.has_value()); + + auto restoredMatrix = BasisDecomposerTest::restore(*decomposedSequence); + + EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix)) + << "ORIGINAL:\n" + << originalMatrix << '\n' + << "RESULT:\n" + << restoredMatrix << '\n'; + ++iterations; + } + + RecordProperty("iterations", iterations); + std::cerr << "Iterations: " << iterations << '\n'; +} + +INSTANTIATE_TEST_CASE_P( + SingleQubitMatrices, BasisDecomposerTest, + testing::Combine( + // basis gates + testing::Values(Gate{.type = qc::X, .parameter = {}, .qubitId = {0, 1}}, + Gate{ + .type = qc::X, .parameter = {}, .qubitId = {1, 0}}), + // sets of euler bases + testing::Values(llvm::SmallVector{EulerBasis::ZYZ}, + llvm::SmallVector{ + EulerBasis::ZYZ, EulerBasis::ZXZ, EulerBasis::XYX, + EulerBasis::XZX}, + llvm::SmallVector{EulerBasis::XZX}), + // targets to be decomposed + testing::Values(helpers::kroneckerProduct(IDENTITY_GATE, IDENTITY_GATE), + helpers::kroneckerProduct(rzMatrix(1.0), ryMatrix(3.1)), + helpers::kroneckerProduct(IDENTITY_GATE, + rxMatrix(0.1))))); + +INSTANTIATE_TEST_CASE_P( + TwoQubitMatrices, BasisDecomposerTest, + testing::Combine( + // basis gates + testing::Values(Gate{.type = qc::X, .parameter = {}, .qubitId = {0, 1}}, + Gate{ + .type = qc::X, .parameter = {}, .qubitId = {1, 0}}), + // sets of euler bases + testing::Values(llvm::SmallVector{EulerBasis::ZYZ}, + llvm::SmallVector{ + EulerBasis::ZYZ, EulerBasis::ZXZ, EulerBasis::XYX, + EulerBasis::XZX}, + llvm::SmallVector{EulerBasis::XZX}), + // targets to be decomposed + ::testing::Values( + rzzMatrix(2.0), ryyMatrix(1.0) * rzzMatrix(3.0) * rxxMatrix(2.0), + canonicalGate(1.5, -0.2, 0.0) * + helpers::kroneckerProduct(rxMatrix(1.0), IDENTITY_GATE), + helpers::kroneckerProduct(rxMatrix(1.0), ryMatrix(1.0)) * + canonicalGate(1.1, 0.2, 3.0) * + helpers::kroneckerProduct(rxMatrix(1.0), IDENTITY_GATE), + helpers::kroneckerProduct(H_GATE, IPZ) * + getTwoQubitMatrix( + {.type = qc::X, .parameter = {}, .qubitId = {0, 1}}) * + helpers::kroneckerProduct(IPX, IPY)))); diff --git a/mlir/unittests/decomposition/test_euler_decomposition.cpp b/mlir/unittests/decomposition/test_euler_decomposition.cpp new file mode 100644 index 0000000000..ee33bf1d6e --- /dev/null +++ b/mlir/unittests/decomposition/test_euler_decomposition.cpp @@ -0,0 +1,111 @@ +/* + * 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/Passes/Decomposition/EulerBasis.h" +#include "mlir/Passes/Decomposition/EulerDecomposition.h" +#include "mlir/Passes/Decomposition/GateSequence.h" +#include "mlir/Passes/Decomposition/Helpers.h" +#include "mlir/Passes/Decomposition/UnitaryMatrices.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace mlir::qco; +using namespace mlir::qco::decomposition; + +namespace { +[[nodiscard]] matrix2x2 randomUnitaryMatrix() { + [[maybe_unused]] static auto initializeRandom = []() { + // Eigen uses std::rand() internally, use fixed seed for deterministic + // testing behavior + std::srand(123456UL); + return true; + }(); + const matrix2x2 randomMatrix = matrix2x2::Random(); + Eigen::HouseholderQR qr{}; // NOLINT(misc-include-cleaner) + qr.compute(randomMatrix); + const matrix2x2 unitaryMatrix = qr.householderQ(); + assert(helpers::isUnitaryMatrix(unitaryMatrix)); + return unitaryMatrix; +} +} // namespace + +class EulerDecompositionTest + : public testing::TestWithParam> { +public: + [[nodiscard]] static matrix2x2 restore(const TwoQubitGateSequence& sequence) { + matrix2x2 matrix = matrix2x2::Identity(); + for (auto&& gate : sequence.gates) { + matrix = getSingleQubitMatrix(gate) * matrix; + } + + matrix *= std::exp(IM * sequence.globalPhase); + return matrix; + } + + void SetUp() override { + eulerBasis = std::get<0>(GetParam()); + originalMatrix = std::get<1>(GetParam()); + } + +protected: + matrix2x2 originalMatrix; + EulerBasis eulerBasis{}; +}; + +TEST_P(EulerDecompositionTest, TestExact) { + auto decomposition = EulerDecomposition::generateCircuit( + eulerBasis, originalMatrix, false, 0.0); + auto restoredMatrix = restore(decomposition); + + EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix)) + << "RESULT:\n" + << restoredMatrix << '\n'; +} + +TEST(EulerDecompositionTest, Random) { + auto stopTime = std::chrono::steady_clock::now() + std::chrono::seconds{10}; + auto iterations = 0; + auto eulerBases = std::array{EulerBasis::XYX, EulerBasis::XZX, + EulerBasis::ZYZ, EulerBasis::ZXZ}; + std::size_t currentEulerBase = 0; + while (std::chrono::steady_clock::now() < stopTime) { + auto originalMatrix = randomUnitaryMatrix(); + auto eulerBasis = eulerBases[currentEulerBase++ % eulerBases.size()]; + auto decomposition = EulerDecomposition::generateCircuit( + eulerBasis, originalMatrix, true, std::nullopt); + auto restoredMatrix = EulerDecompositionTest::restore(decomposition); + + EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix)) + << "ORIGINAL:\n" + << originalMatrix << '\n' + << "RESULT:\n" + << restoredMatrix << '\n'; + ++iterations; + } + + RecordProperty("iterations", iterations); + std::cerr << "Iterations: " << iterations << '\n'; +} + +INSTANTIATE_TEST_CASE_P( + SingleQubitMatrices, EulerDecompositionTest, + testing::Combine(testing::Values(EulerBasis::XYX, EulerBasis::XZX, + EulerBasis::ZYZ, EulerBasis::ZXZ), + testing::Values(IDENTITY_GATE, ryMatrix(2.0), + rxMatrix(0.5), rzMatrix(3.14), H_GATE))); diff --git a/mlir/unittests/decomposition/test_weyl_decomposition.cpp b/mlir/unittests/decomposition/test_weyl_decomposition.cpp new file mode 100644 index 0000000000..2978bca2f8 --- /dev/null +++ b/mlir/unittests/decomposition/test_weyl_decomposition.cpp @@ -0,0 +1,163 @@ +/* + * 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 "ir/operations/OpType.hpp" +#include "mlir/Passes/Decomposition/Helpers.h" +#include "mlir/Passes/Decomposition/UnitaryMatrices.h" +#include "mlir/Passes/Decomposition/WeylDecomposition.h" + +#include +#include +#include +#include +#include +#include +#include + +using namespace mlir::qco; +using namespace mlir::qco::decomposition; + +namespace { +[[nodiscard]] matrix4x4 randomUnitaryMatrix() { + [[maybe_unused]] static auto initializeRandom = []() { + // Eigen uses std::rand() internally, use fixed seed for deterministic + // testing behavior + std::srand(123456UL); + return true; + }(); + const matrix4x4 randomMatrix = matrix4x4::Random(); + Eigen::HouseholderQR qr{}; // NOLINT(misc-include-cleaner) + qr.compute(randomMatrix); + const matrix4x4 unitaryMatrix = qr.householderQ(); + assert(helpers::isUnitaryMatrix(unitaryMatrix)); + return unitaryMatrix; +} + +[[nodiscard]] matrix4x4 canonicalGate(fp a, fp b, fp c) { + TwoQubitWeylDecomposition tmp{}; + tmp.a = a; + tmp.b = b; + tmp.c = c; + return tmp.getCanonicalMatrix(); +} +} // namespace + +class WeylDecompositionTest : public testing::TestWithParam { +public: + [[nodiscard]] static matrix4x4 + restore(const TwoQubitWeylDecomposition& decomposition) { + return k1(decomposition) * can(decomposition) * k2(decomposition) * + globalPhaseFactor(decomposition); + } + + [[nodiscard]] static qfp + globalPhaseFactor(const TwoQubitWeylDecomposition& decomposition) { + return std::exp(IM * decomposition.globalPhase); + } + [[nodiscard]] static matrix4x4 + can(const TwoQubitWeylDecomposition& decomposition) { + return decomposition.getCanonicalMatrix(); + } + [[nodiscard]] static matrix4x4 + k1(const TwoQubitWeylDecomposition& decomposition) { + return helpers::kroneckerProduct(decomposition.k1l, decomposition.k1r); + } + [[nodiscard]] static matrix4x4 + k2(const TwoQubitWeylDecomposition& decomposition) { + return helpers::kroneckerProduct(decomposition.k2l, decomposition.k2r); + } +}; + +TEST_P(WeylDecompositionTest, TestExact) { + const auto& originalMatrix = GetParam(); + auto decomposition = TwoQubitWeylDecomposition::create(originalMatrix, 1.0); + auto restoredMatrix = restore(decomposition); + + EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix)) + << "RESULT:\n" + << restoredMatrix << '\n'; +} + +TEST_P(WeylDecompositionTest, TestApproximation) { + const auto& originalMatrix = GetParam(); + auto decomposition = + TwoQubitWeylDecomposition::create(originalMatrix, 1.0 - 1e-12); + auto restoredMatrix = restore(decomposition); + + EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix)) + << "RESULT:\n" + << restoredMatrix << '\n'; +} + +TEST(WeylDecompositionTest, Random) { + auto stopTime = std::chrono::steady_clock::now() + std::chrono::seconds{10}; + auto iterations = 0; + while (std::chrono::steady_clock::now() < stopTime) { + auto originalMatrix = randomUnitaryMatrix(); + auto decomposition = + TwoQubitWeylDecomposition::create(originalMatrix, 1.0 - 1e-12); + auto restoredMatrix = WeylDecompositionTest::restore(decomposition); + + EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix)) + << "ORIGINAL:\n" + << originalMatrix << '\n' + << "RESULT:\n" + << restoredMatrix << '\n'; + ++iterations; + } + + RecordProperty("iterations", iterations); + std::cerr << "Iterations: " << iterations << '\n'; +} + +INSTANTIATE_TEST_CASE_P( + SingleQubitMatrices, WeylDecompositionTest, + ::testing::Values(helpers::kroneckerProduct(IDENTITY_GATE, IDENTITY_GATE), + helpers::kroneckerProduct(rzMatrix(1.0), ryMatrix(3.1)), + helpers::kroneckerProduct(IDENTITY_GATE, rxMatrix(0.1)))); + +INSTANTIATE_TEST_CASE_P( + TwoQubitMatrices, WeylDecompositionTest, + ::testing::Values( + rzzMatrix(2.0), ryyMatrix(1.0) * rzzMatrix(3.0) * rxxMatrix(2.0), + canonicalGate(1.5, -0.2, 0.0) * + helpers::kroneckerProduct(rxMatrix(1.0), IDENTITY_GATE), + helpers::kroneckerProduct(rxMatrix(1.0), ryMatrix(1.0)) * + canonicalGate(1.1, 0.2, 3.0) * + helpers::kroneckerProduct(rxMatrix(1.0), IDENTITY_GATE), + helpers::kroneckerProduct(H_GATE, IPZ) * + getTwoQubitMatrix( + {.type = qc::X, .parameter = {}, .qubitId = {0, 1}}) * + helpers::kroneckerProduct(IPX, IPY))); + +INSTANTIATE_TEST_CASE_P( + SpecializedMatrices, WeylDecompositionTest, + ::testing::Values( + // id + controlled + general already covered by other parametrizations + // swap equiv + getTwoQubitMatrix({.type = qc::X, .parameter = {}, .qubitId = {0, 1}}) * + getTwoQubitMatrix( + {.type = qc::X, .parameter = {}, .qubitId = {1, 0}}) * + getTwoQubitMatrix( + {.type = qc::X, .parameter = {}, .qubitId = {0, 1}}), + // partial swap equiv + canonicalGate(0.5, 0.5, 0.5), + // partial swap equiv (flipped) + canonicalGate(0.5, 0.5, -0.5), + // mirror controlled equiv + getTwoQubitMatrix({.type = qc::X, .parameter = {}, .qubitId = {0, 1}}) * + getTwoQubitMatrix( + {.type = qc::X, .parameter = {}, .qubitId = {1, 0}}), + // sim aab equiv + canonicalGate(0.5, 0.5, 0.1), + // sim abb equiv + canonicalGate(0.5, 0.1, 0.1), + // sim ab-b equiv + canonicalGate(0.5, 0.1, -0.1))); diff --git a/thesis-evaluation/__init__.py b/thesis-evaluation/__init__.py new file mode 100644 index 0000000000..38212b5a38 --- /dev/null +++ b/thesis-evaluation/__init__.py @@ -0,0 +1,7 @@ +# 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 diff --git a/thesis-evaluation/evaluate.py b/thesis-evaluation/evaluate.py new file mode 100644 index 0000000000..cbe95a3de9 --- /dev/null +++ b/thesis-evaluation/evaluate.py @@ -0,0 +1,41 @@ +# 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 + +import glob +import pathlib + +INPUT_DIR = "./tmp-output" + + +def read_stats(file_name) -> dict[str, int]: + result = {} + + with pathlib.Path(file_name).open("r", encoding="utf-8") as f: + for line in f: + line = line.lstrip() + line.removeprefix("(S)") + line = line.lstrip() + elements = line.split(" ") + elements = list(filter(lambda x: x != " " and len(x) > 0, elements)) + + metric = elements[2] + value = int(elements[1]) + result[metric] = value if value < 2**31 else value - 2**32 + + return result + + +def evaluate(): + all_stats = {} + for file in glob.glob(f"{INPUT_DIR}/*.statistic"): + name = pathlib.Path(file).name.removesuffix(".statistic") + name = name.removesuffix(".qasm") + name = name.removesuffix(".mlir") + all_stats[name] = read_stats(file) + + return all_stats diff --git a/thesis-evaluation/evaluate_cache.json b/thesis-evaluation/evaluate_cache.json new file mode 100644 index 0000000000..8b19e09283 --- /dev/null +++ b/thesis-evaluation/evaluate_cache.json @@ -0,0 +1,608 @@ +{ + "mqt": { + "modular_adder_indep_2_none_O0": { + "numberOfTwoQubitCreations": 1.0000000000000002, + "subCircuitComplexityChange": 0.0, + "successfulSingleQubitDecompositions": 0.0, + "successfulTwoQubitDecompositions": 0.0, + "timeInCircuitCollection": 14.749999999999998, + "timeInSingleQubitDecomposition": 0.0, + "timeInTwoQubitDecomposition": 0.0, + "totalCircuitCollections": 2.0000000000000004, + "totalSingleQubitDecompositions": 0.0, + "totalTouchedGates": 1.0000000000000002, + "totalTwoQubitDecompositions": 0.0, + "twoQubitCreationTime": 824.45 + }, + "bmw_quark_cardinality_indep_1_none_O0": { + "numberOfTwoQubitCreations": 1.0000000000000002, + "subCircuitComplexityChange": 0.0, + "successfulSingleQubitDecompositions": 0.0, + "successfulTwoQubitDecompositions": 0.0, + "timeInCircuitCollection": 39.24999999999999, + "timeInSingleQubitDecomposition": 8.300000000000002, + "timeInTwoQubitDecomposition": 0.0, + "totalCircuitCollections": 13.999999999999995, + "totalSingleQubitDecompositions": 1.0000000000000002, + "totalTouchedGates": 11.999999999999996, + "totalTwoQubitDecompositions": 0.0, + "twoQubitCreationTime": 826.35 + }, + "draper_qft_adder_indep_2_none_O0": { + "numberOfTwoQubitCreations": 1.0000000000000002, + "subCircuitComplexityChange": 2.0000000000000004, + "successfulSingleQubitDecompositions": 0.0, + "successfulTwoQubitDecompositions": 1.0000000000000002, + "timeInCircuitCollection": 22.65, + "timeInSingleQubitDecomposition": 0.0, + "timeInTwoQubitDecomposition": 743.4500000000002, + "totalCircuitCollections": 2.999999999999999, + "totalSingleQubitDecompositions": 0.0, + "totalTouchedGates": 2.999999999999999, + "totalTwoQubitDecompositions": 1.0000000000000002, + "twoQubitCreationTime": 830.4000000000001 + }, + "vqe_two_local_indep_2_none_O0": { + "numberOfTwoQubitCreations": 1.0000000000000002, + "subCircuitComplexityChange": 0.0, + "successfulSingleQubitDecompositions": 0.0, + "successfulTwoQubitDecompositions": 0.0, + "timeInCircuitCollection": 285.1, + "timeInSingleQubitDecomposition": 0.0, + "timeInTwoQubitDecomposition": 12939.300000000001, + "totalCircuitCollections": 11.999999999999996, + "totalSingleQubitDecompositions": 0.0, + "totalTouchedGates": 67.99999999999999, + "totalTwoQubitDecompositions": 9.0, + "twoQubitCreationTime": 818.9999999999999 + }, + "wstate_indep_2_none_O0": { + "numberOfTwoQubitCreations": 1.0000000000000002, + "subCircuitComplexityChange": -8.000000000000002, + "successfulSingleQubitDecompositions": 0.0, + "successfulTwoQubitDecompositions": 1.0000000000000002, + "timeInCircuitCollection": 30.35, + "timeInSingleQubitDecomposition": 0.0, + "timeInTwoQubitDecomposition": 1399.1, + "totalCircuitCollections": 2.999999999999999, + "totalSingleQubitDecompositions": 0.0, + "totalTouchedGates": 5.0, + "totalTwoQubitDecompositions": 1.0000000000000002, + "twoQubitCreationTime": 817.8500000000001 + }, + "bmw_quark_copula_indep_2_none_O0": { + "numberOfTwoQubitCreations": 1.0000000000000002, + "subCircuitComplexityChange": -4.000000000000001, + "successfulSingleQubitDecompositions": 0.0, + "successfulTwoQubitDecompositions": 1.0000000000000002, + "timeInCircuitCollection": 147.5, + "timeInSingleQubitDecomposition": 50.04999999999999, + "timeInTwoQubitDecomposition": 853.7, + "totalCircuitCollections": 32.999999999999986, + "totalSingleQubitDecompositions": 8.000000000000002, + "totalTouchedGates": 50.0, + "totalTwoQubitDecompositions": 1.0000000000000002, + "twoQubitCreationTime": 832.75 + }, + "qaoa_indep_1_none_O0": { + "numberOfTwoQubitCreations": 1.0000000000000002, + "subCircuitComplexityChange": 1.0000000000000002, + "successfulSingleQubitDecompositions": 1.0000000000000002, + "successfulTwoQubitDecompositions": 0.0, + "timeInCircuitCollection": 12.249999999999998, + "timeInSingleQubitDecomposition": 7.049999999999998, + "timeInTwoQubitDecomposition": 0.0, + "totalCircuitCollections": 1.0000000000000002, + "totalSingleQubitDecompositions": 1.0000000000000002, + "totalTouchedGates": 2.999999999999999, + "totalTwoQubitDecompositions": 0.0, + "twoQubitCreationTime": 800.1499999999999 + }, + "qftentangled_indep_2_none_O0": { + "numberOfTwoQubitCreations": 1.0000000000000002, + "subCircuitComplexityChange": -13.000000000000004, + "successfulSingleQubitDecompositions": 0.0, + "successfulTwoQubitDecompositions": 1.0000000000000002, + "timeInCircuitCollection": 33.75, + "timeInSingleQubitDecomposition": 0.0, + "timeInTwoQubitDecomposition": 1916.6499999999999, + "totalCircuitCollections": 2.999999999999999, + "totalSingleQubitDecompositions": 0.0, + "totalTouchedGates": 5.999999999999998, + "totalTwoQubitDecompositions": 1.0000000000000002, + "twoQubitCreationTime": 814.8 + }, + "wstate_indep_1_none_O0": { + "numberOfTwoQubitCreations": 1.0000000000000002, + "subCircuitComplexityChange": 0.0, + "successfulSingleQubitDecompositions": 1.0000000000000002, + "successfulTwoQubitDecompositions": 0.0, + "timeInCircuitCollection": 8.450000000000001, + "timeInSingleQubitDecomposition": 6.349999999999998, + "timeInTwoQubitDecomposition": 0.0, + "totalCircuitCollections": 2.999999999999999, + "totalSingleQubitDecompositions": 1.0000000000000002, + "totalTouchedGates": 1.0000000000000002, + "totalTwoQubitDecompositions": 0.0, + "twoQubitCreationTime": 802.3000000000001 + }, + "ghz_indep_2_none_O0": { + "numberOfTwoQubitCreations": 1.0000000000000002, + "subCircuitComplexityChange": -4.000000000000001, + "successfulSingleQubitDecompositions": 0.0, + "successfulTwoQubitDecompositions": 1.0000000000000002, + "timeInCircuitCollection": 19.449999999999996, + "timeInSingleQubitDecomposition": 0.0, + "timeInTwoQubitDecomposition": 833.8499999999999, + "totalCircuitCollections": 2.999999999999999, + "totalSingleQubitDecompositions": 0.0, + "totalTouchedGates": 2.0000000000000004, + "totalTwoQubitDecompositions": 1.0000000000000002, + "twoQubitCreationTime": 813.5499999999998 + }, + "qpeinexact_indep_2_none_O0": { + "numberOfTwoQubitCreations": 1.0000000000000002, + "subCircuitComplexityChange": -16.000000000000004, + "successfulSingleQubitDecompositions": 0.0, + "successfulTwoQubitDecompositions": 1.0000000000000002, + "timeInCircuitCollection": 23.65, + "timeInSingleQubitDecomposition": 0.0, + "timeInTwoQubitDecomposition": 1366.4500000000003, + "totalCircuitCollections": 2.999999999999999, + "totalSingleQubitDecompositions": 0.0, + "totalTouchedGates": 4.000000000000001, + "totalTwoQubitDecompositions": 1.0000000000000002, + "twoQubitCreationTime": 819.9 + }, + "vqe_real_amp_indep_1_none_O0": { + "numberOfTwoQubitCreations": 1.0000000000000002, + "subCircuitComplexityChange": 2.999999999999999, + "successfulSingleQubitDecompositions": 1.0000000000000002, + "successfulTwoQubitDecompositions": 0.0, + "timeInCircuitCollection": 14.899999999999999, + "timeInSingleQubitDecomposition": 5.999999999999998, + "timeInTwoQubitDecomposition": 0.0, + "totalCircuitCollections": 2.999999999999999, + "totalSingleQubitDecompositions": 1.0000000000000002, + "totalTouchedGates": 4.000000000000001, + "totalTwoQubitDecompositions": 0.0, + "twoQubitCreationTime": 836.9499999999999 + }, + "vqe_su2_indep_1_none_O0": { + "numberOfTwoQubitCreations": 1.0000000000000002, + "subCircuitComplexityChange": 5.0, + "successfulSingleQubitDecompositions": 1.0000000000000002, + "successfulTwoQubitDecompositions": 0.0, + "timeInCircuitCollection": 21.650000000000006, + "timeInSingleQubitDecomposition": 8.1, + "timeInTwoQubitDecomposition": 0.0, + "totalCircuitCollections": 2.999999999999999, + "totalSingleQubitDecompositions": 1.0000000000000002, + "totalTouchedGates": 8.000000000000002, + "totalTwoQubitDecompositions": 0.0, + "twoQubitCreationTime": 808.6999999999999 + }, + "qft_indep_1_none_O0": { + "numberOfTwoQubitCreations": 1.0000000000000002, + "subCircuitComplexityChange": -1.0000000000000002, + "successfulSingleQubitDecompositions": 1.0000000000000002, + "successfulTwoQubitDecompositions": 0.0, + "timeInCircuitCollection": 8.200000000000003, + "timeInSingleQubitDecomposition": 6.249999999999998, + "timeInTwoQubitDecomposition": 0.0, + "totalCircuitCollections": 2.999999999999999, + "totalSingleQubitDecompositions": 1.0000000000000002, + "totalTouchedGates": 1.0000000000000002, + "totalTwoQubitDecompositions": 0.0, + "twoQubitCreationTime": 820.2 + }, + "bmw_quark_cardinality_indep_2_none_O0": { + "numberOfTwoQubitCreations": 1.0000000000000002, + "subCircuitComplexityChange": -55.99999999999998, + "successfulSingleQubitDecompositions": 0.0, + "successfulTwoQubitDecompositions": 2.999999999999999, + "timeInCircuitCollection": 155.45, + "timeInSingleQubitDecomposition": 26.850000000000005, + "timeInTwoQubitDecomposition": 4141.299999999999, + "totalCircuitCollections": 47.999999999999986, + "totalSingleQubitDecompositions": 4.000000000000001, + "totalTouchedGates": 50.999999999999986, + "totalTwoQubitDecompositions": 2.999999999999999, + "twoQubitCreationTime": 812.3000000000001 + }, + "bv_indep_1_none_O0": { + "numberOfTwoQubitCreations": 1.0000000000000002, + "subCircuitComplexityChange": 0.0, + "successfulSingleQubitDecompositions": 1.0000000000000002, + "successfulTwoQubitDecompositions": 0.0, + "timeInCircuitCollection": 6.549999999999999, + "timeInSingleQubitDecomposition": 6.149999999999998, + "timeInTwoQubitDecomposition": 0.0, + "totalCircuitCollections": 1.0000000000000002, + "totalSingleQubitDecompositions": 1.0000000000000002, + "totalTouchedGates": 1.0000000000000002, + "totalTwoQubitDecompositions": 0.0, + "twoQubitCreationTime": 809.5 + }, + "ghz_indep_1_none_O0": { + "numberOfTwoQubitCreations": 1.0000000000000002, + "subCircuitComplexityChange": -1.0000000000000002, + "successfulSingleQubitDecompositions": 1.0000000000000002, + "successfulTwoQubitDecompositions": 0.0, + "timeInCircuitCollection": 8.400000000000004, + "timeInSingleQubitDecomposition": 6.599999999999999, + "timeInTwoQubitDecomposition": 0.0, + "totalCircuitCollections": 2.999999999999999, + "totalSingleQubitDecompositions": 1.0000000000000002, + "totalTouchedGates": 1.0000000000000002, + "totalTwoQubitDecompositions": 0.0, + "twoQubitCreationTime": 838.4499999999999 + }, + "vqe_su2_indep_2_none_O0": { + "numberOfTwoQubitCreations": 1.0000000000000002, + "subCircuitComplexityChange": 0.0, + "successfulSingleQubitDecompositions": 0.0, + "successfulTwoQubitDecompositions": 0.0, + "timeInCircuitCollection": 635.25, + "timeInSingleQubitDecomposition": 0.0, + "timeInTwoQubitDecomposition": 22975.800000000003, + "totalCircuitCollections": 20.0, + "totalSingleQubitDecompositions": 0.0, + "totalTouchedGates": 198.00000000000006, + "totalTwoQubitDecompositions": 15.0, + "twoQubitCreationTime": 813.6 + }, + "qnn_indep_2_none_O0": { + "numberOfTwoQubitCreations": 1.0000000000000002, + "subCircuitComplexityChange": 2.999999999999999, + "successfulSingleQubitDecompositions": 0.0, + "successfulTwoQubitDecompositions": 1.0000000000000002, + "timeInCircuitCollection": 41.550000000000004, + "timeInSingleQubitDecomposition": 0.0, + "timeInTwoQubitDecomposition": 1040.3999999999999, + "totalCircuitCollections": 2.999999999999999, + "totalSingleQubitDecompositions": 0.0, + "totalTouchedGates": 13.000000000000004, + "totalTwoQubitDecompositions": 1.0000000000000002, + "twoQubitCreationTime": 817.3499999999999 + }, + "qpeexact_indep_2_none_O0": { + "numberOfTwoQubitCreations": 1.0000000000000002, + "subCircuitComplexityChange": 1.0000000000000002, + "successfulSingleQubitDecompositions": 0.0, + "successfulTwoQubitDecompositions": 1.0000000000000002, + "timeInCircuitCollection": 24.65, + "timeInSingleQubitDecomposition": 0.0, + "timeInTwoQubitDecomposition": 798.7, + "totalCircuitCollections": 2.999999999999999, + "totalSingleQubitDecompositions": 0.0, + "totalTouchedGates": 4.000000000000001, + "totalTwoQubitDecompositions": 1.0000000000000002, + "twoQubitCreationTime": 815.2500000000001 + }, + "qnn_indep_1_none_O0": { + "numberOfTwoQubitCreations": 1.0000000000000002, + "subCircuitComplexityChange": 2.999999999999999, + "successfulSingleQubitDecompositions": 1.0000000000000002, + "successfulTwoQubitDecompositions": 0.0, + "timeInCircuitCollection": 18.5, + "timeInSingleQubitDecomposition": 8.300000000000002, + "timeInTwoQubitDecomposition": 0.0, + "totalCircuitCollections": 2.999999999999999, + "totalSingleQubitDecompositions": 1.0000000000000002, + "totalTouchedGates": 5.999999999999998, + "totalTwoQubitDecompositions": 0.0, + "twoQubitCreationTime": 817.25 + }, + "vqe_real_amp_indep_2_none_O0": { + "numberOfTwoQubitCreations": 1.0000000000000002, + "subCircuitComplexityChange": 0.0, + "successfulSingleQubitDecompositions": 0.0, + "successfulTwoQubitDecompositions": 0.0, + "timeInCircuitCollection": 289.8, + "timeInSingleQubitDecomposition": 0.0, + "timeInTwoQubitDecomposition": 12900.799999999997, + "totalCircuitCollections": 11.999999999999996, + "totalSingleQubitDecompositions": 0.0, + "totalTouchedGates": 67.99999999999999, + "totalTwoQubitDecompositions": 9.0, + "twoQubitCreationTime": 805.2500000000001 + }, + "qftentangled_indep_1_none_O0": { + "numberOfTwoQubitCreations": 1.0000000000000002, + "subCircuitComplexityChange": 2.0000000000000004, + "successfulSingleQubitDecompositions": 1.0000000000000002, + "successfulTwoQubitDecompositions": 0.0, + "timeInCircuitCollection": 11.3, + "timeInSingleQubitDecomposition": 3.199999999999999, + "timeInTwoQubitDecomposition": 0.0, + "totalCircuitCollections": 2.999999999999999, + "totalSingleQubitDecompositions": 1.0000000000000002, + "totalTouchedGates": 2.0000000000000004, + "totalTwoQubitDecompositions": 0.0, + "twoQubitCreationTime": 823.65 + }, + "bv_indep_2_none_O0": { + "numberOfTwoQubitCreations": 1.0000000000000002, + "subCircuitComplexityChange": 2.0000000000000004, + "successfulSingleQubitDecompositions": 2.0000000000000004, + "successfulTwoQubitDecompositions": 0.0, + "timeInCircuitCollection": 13.700000000000001, + "timeInSingleQubitDecomposition": 8.500000000000002, + "timeInTwoQubitDecomposition": 0.0, + "totalCircuitCollections": 2.0000000000000004, + "totalSingleQubitDecompositions": 2.0000000000000004, + "totalTouchedGates": 2.999999999999999, + "totalTwoQubitDecompositions": 0.0, + "twoQubitCreationTime": 822.6999999999999 + }, + "randomcircuit_indep_1_none_O0": { + "numberOfTwoQubitCreations": 1.0000000000000002, + "subCircuitComplexityChange": -1.0000000000000002, + "successfulSingleQubitDecompositions": 1.0000000000000002, + "successfulTwoQubitDecompositions": 0.0, + "timeInCircuitCollection": 11.650000000000002, + "timeInSingleQubitDecomposition": 8.150000000000002, + "timeInTwoQubitDecomposition": 0.0, + "totalCircuitCollections": 2.999999999999999, + "totalSingleQubitDecompositions": 1.0000000000000002, + "totalTouchedGates": 2.0000000000000004, + "totalTwoQubitDecompositions": 0.0, + "twoQubitCreationTime": 814.4499999999999 + }, + "qft_indep_2_none_O0": { + "numberOfTwoQubitCreations": 1.0000000000000002, + "subCircuitComplexityChange": -23.999999999999993, + "successfulSingleQubitDecompositions": 0.0, + "successfulTwoQubitDecompositions": 1.0000000000000002, + "timeInCircuitCollection": 25.000000000000004, + "timeInSingleQubitDecomposition": 0.0, + "timeInTwoQubitDecomposition": 1917.25, + "totalCircuitCollections": 2.999999999999999, + "totalSingleQubitDecompositions": 0.0, + "totalTouchedGates": 4.000000000000001, + "totalTwoQubitDecompositions": 1.0000000000000002, + "twoQubitCreationTime": 803.8499999999999 + }, + "qaoa_indep_2_none_O0": { + "numberOfTwoQubitCreations": 1.0000000000000002, + "subCircuitComplexityChange": -6.999999999999997, + "successfulSingleQubitDecompositions": 0.0, + "successfulTwoQubitDecompositions": 1.0000000000000002, + "timeInCircuitCollection": 26.550000000000004, + "timeInSingleQubitDecomposition": 0.0, + "timeInTwoQubitDecomposition": 1506.6999999999996, + "totalCircuitCollections": 1.0000000000000002, + "totalSingleQubitDecompositions": 0.0, + "totalTouchedGates": 8.000000000000002, + "totalTwoQubitDecompositions": 1.0000000000000002, + "twoQubitCreationTime": 840.45 + } + }, + "qiskit": { + "vqe_real_amp_indep_1_none_O0": { + "timeInSingleQubitDecomposition": 297.44634999999994, + "timeInTwoQubitDecomposition": 0.0, + "subCircuitComplexityChange": 2.999999999999999, + "totalTwoQubitDecompositions": 0.0, + "totalSingleQubitDecompositions": 1.0000000000000002 + }, + "randomcircuit_indep_2_none_O0": { + "timeInSingleQubitDecomposition": 0.0, + "timeInTwoQubitDecomposition": 1533.8495500000001, + "subCircuitComplexityChange": -2.999999999999999, + "totalTwoQubitDecompositions": 1.0000000000000002, + "totalSingleQubitDecompositions": 0.0 + }, + "qaoa_indep_2_none_O0": { + "timeInSingleQubitDecomposition": 0.0, + "timeInTwoQubitDecomposition": 1417.1694999999997, + "subCircuitComplexityChange": -11.000000000000002, + "totalTwoQubitDecompositions": 1.0000000000000002, + "totalSingleQubitDecompositions": 0.0 + }, + "modular_adder_indep_2_none_O0": { + "timeInSingleQubitDecomposition": 0.0, + "timeInTwoQubitDecomposition": 1409.6549000000002, + "subCircuitComplexityChange": 0.0, + "totalTwoQubitDecompositions": 1.0000000000000002, + "totalSingleQubitDecompositions": 0.0 + }, + "bmw_quark_cardinality_indep_2_none_O0": { + "timeInSingleQubitDecomposition": 0.0, + "timeInTwoQubitDecomposition": 7895.523400000001, + "subCircuitComplexityChange": -75.0, + "totalTwoQubitDecompositions": 5.999999999999998, + "totalSingleQubitDecompositions": 0.0 + }, + "bmw_quark_copula_indep_2_none_O0": { + "timeInSingleQubitDecomposition": 0.0, + "timeInTwoQubitDecomposition": 3926.8486, + "subCircuitComplexityChange": -8.000000000000002, + "totalTwoQubitDecompositions": 2.999999999999999, + "totalSingleQubitDecompositions": 0.0 + }, + "vqe_two_local_indep_2_none_O0": { + "timeInSingleQubitDecomposition": 0.0, + "timeInTwoQubitDecomposition": 1489.9131000000002, + "subCircuitComplexityChange": -10.0, + "totalTwoQubitDecompositions": 1.0000000000000002, + "totalSingleQubitDecompositions": 0.0 + }, + "dj_indep_1_none_O0": { + "timeInSingleQubitDecomposition": 184.4495, + "timeInTwoQubitDecomposition": 0.0, + "subCircuitComplexityChange": 2.0000000000000004, + "totalTwoQubitDecompositions": 0.0, + "totalSingleQubitDecompositions": 1.0000000000000002 + }, + "vqe_su2_indep_2_none_O0": { + "timeInSingleQubitDecomposition": 0.0, + "timeInTwoQubitDecomposition": 1618.4104499999999, + "subCircuitComplexityChange": -5.999999999999998, + "totalTwoQubitDecompositions": 1.0000000000000002, + "totalSingleQubitDecompositions": 0.0 + }, + "qft_indep_2_none_O0": { + "timeInSingleQubitDecomposition": 0.0, + "timeInTwoQubitDecomposition": 1447.7477500000002, + "subCircuitComplexityChange": -26.000000000000007, + "totalTwoQubitDecompositions": 1.0000000000000002, + "totalSingleQubitDecompositions": 0.0 + }, + "qpeexact_indep_2_none_O0": { + "timeInSingleQubitDecomposition": 0.0, + "timeInTwoQubitDecomposition": 1390.3970000000002, + "subCircuitComplexityChange": -4.000000000000001, + "totalTwoQubitDecompositions": 1.0000000000000002, + "totalSingleQubitDecompositions": 0.0 + }, + "ghz_indep_2_none_O0": { + "timeInSingleQubitDecomposition": 0.0, + "timeInTwoQubitDecomposition": 1328.5080999999998, + "subCircuitComplexityChange": -8.000000000000002, + "totalTwoQubitDecompositions": 1.0000000000000002, + "totalSingleQubitDecompositions": 0.0 + }, + "draper_qft_adder_indep_2_none_O0": { + "timeInSingleQubitDecomposition": 0.0, + "timeInTwoQubitDecomposition": 1378.2878, + "subCircuitComplexityChange": -5.999999999999998, + "totalTwoQubitDecompositions": 1.0000000000000002, + "totalSingleQubitDecompositions": 0.0 + }, + "qnn_indep_2_none_O0": { + "timeInSingleQubitDecomposition": 0.0, + "timeInTwoQubitDecomposition": 1421.9007000000001, + "subCircuitComplexityChange": 1.0000000000000002, + "totalTwoQubitDecompositions": 1.0000000000000002, + "totalSingleQubitDecompositions": 0.0 + }, + "qaoa_indep_1_none_O0": { + "timeInSingleQubitDecomposition": 180.74625, + "timeInTwoQubitDecomposition": 0.0, + "subCircuitComplexityChange": 1.0000000000000002, + "totalTwoQubitDecompositions": 0.0, + "totalSingleQubitDecompositions": 1.0000000000000002 + }, + "vqe_real_amp_indep_2_none_O0": { + "timeInSingleQubitDecomposition": 0.0, + "timeInTwoQubitDecomposition": 1491.0009499999999, + "subCircuitComplexityChange": -10.0, + "totalTwoQubitDecompositions": 1.0000000000000002, + "totalSingleQubitDecompositions": 0.0 + }, + "wstate_indep_1_none_O0": { + "timeInSingleQubitDecomposition": 179.93175, + "timeInTwoQubitDecomposition": 0.0, + "subCircuitComplexityChange": -1.0000000000000002, + "totalTwoQubitDecompositions": 0.0, + "totalSingleQubitDecompositions": 1.0000000000000002 + }, + "vqe_su2_indep_1_none_O0": { + "timeInSingleQubitDecomposition": 173.43564999999998, + "timeInTwoQubitDecomposition": 0.0, + "subCircuitComplexityChange": 5.0, + "totalTwoQubitDecompositions": 0.0, + "totalSingleQubitDecompositions": 1.0000000000000002 + }, + "bv_indep_2_none_O0": { + "timeInSingleQubitDecomposition": 0.0, + "timeInTwoQubitDecomposition": 1344.1407499999998, + "subCircuitComplexityChange": 1.0000000000000002, + "totalTwoQubitDecompositions": 1.0000000000000002, + "totalSingleQubitDecompositions": 0.0 + }, + "qftentangled_indep_2_none_O0": { + "timeInSingleQubitDecomposition": 0.0, + "timeInTwoQubitDecomposition": 1351.71685, + "subCircuitComplexityChange": -1.0000000000000002, + "totalTwoQubitDecompositions": 1.0000000000000002, + "totalSingleQubitDecompositions": 0.0 + }, + "ae_indep_2_none_O0": { + "timeInSingleQubitDecomposition": 0.0, + "timeInTwoQubitDecomposition": 1467.64165, + "subCircuitComplexityChange": -20.0, + "totalTwoQubitDecompositions": 1.0000000000000002, + "totalSingleQubitDecompositions": 0.0 + }, + "wstate_indep_2_none_O0": { + "timeInSingleQubitDecomposition": 0.0, + "timeInTwoQubitDecomposition": 1486.3598000000002, + "subCircuitComplexityChange": -9.0, + "totalTwoQubitDecompositions": 1.0000000000000002, + "totalSingleQubitDecompositions": 0.0 + }, + "qnn_indep_1_none_O0": { + "timeInSingleQubitDecomposition": 184.27100000000002, + "timeInTwoQubitDecomposition": 0.0, + "subCircuitComplexityChange": 2.999999999999999, + "totalTwoQubitDecompositions": 0.0, + "totalSingleQubitDecompositions": 1.0000000000000002 + }, + "bmw_quark_cardinality_indep_1_none_O0": { + "timeInSingleQubitDecomposition": 496.8003, + "timeInTwoQubitDecomposition": 0.0, + "subCircuitComplexityChange": -2.0000000000000004, + "totalTwoQubitDecompositions": 0.0, + "totalSingleQubitDecompositions": 2.999999999999999 + }, + "bv_indep_1_none_O0": { + "timeInSingleQubitDecomposition": 162.93785000000003, + "timeInTwoQubitDecomposition": 0.0, + "subCircuitComplexityChange": -1.0000000000000002, + "totalTwoQubitDecompositions": 0.0, + "totalSingleQubitDecompositions": 1.0000000000000002 + }, + "qft_indep_1_none_O0": { + "timeInSingleQubitDecomposition": 165.6142, + "timeInTwoQubitDecomposition": 0.0, + "subCircuitComplexityChange": -1.0000000000000002, + "totalTwoQubitDecompositions": 0.0, + "totalSingleQubitDecompositions": 1.0000000000000002 + }, + "qftentangled_indep_1_none_O0": { + "timeInSingleQubitDecomposition": 151.68349999999998, + "timeInTwoQubitDecomposition": 0.0, + "subCircuitComplexityChange": 2.0000000000000004, + "totalTwoQubitDecompositions": 0.0, + "totalSingleQubitDecompositions": 1.0000000000000002 + }, + "qpeinexact_indep_2_none_O0": { + "timeInSingleQubitDecomposition": 0.0, + "timeInTwoQubitDecomposition": 1552.0785000000003, + "subCircuitComplexityChange": -21.000000000000007, + "totalTwoQubitDecompositions": 1.0000000000000002, + "totalSingleQubitDecompositions": 0.0 + }, + "dj_indep_2_none_O0": { + "timeInSingleQubitDecomposition": 0.0, + "timeInTwoQubitDecomposition": 1442.9755000000005, + "subCircuitComplexityChange": -5.0, + "totalTwoQubitDecompositions": 1.0000000000000002, + "totalSingleQubitDecompositions": 0.0 + }, + "ghz_indep_1_none_O0": { + "timeInSingleQubitDecomposition": 174.4021, + "timeInTwoQubitDecomposition": 0.0, + "subCircuitComplexityChange": -1.0000000000000002, + "totalTwoQubitDecompositions": 0.0, + "totalSingleQubitDecompositions": 1.0000000000000002 + }, + "randomcircuit_indep_1_none_O0": { + "timeInSingleQubitDecomposition": 170.12144999999995, + "timeInTwoQubitDecomposition": 0.0, + "subCircuitComplexityChange": -1.0000000000000002, + "totalTwoQubitDecompositions": 0.0, + "totalSingleQubitDecompositions": 1.0000000000000002 + }, + "grover_indep_2_none_O0": { + "timeInSingleQubitDecomposition": 0.0, + "timeInTwoQubitDecomposition": 1405.2790999999997, + "subCircuitComplexityChange": -4.000000000000001, + "totalTwoQubitDecompositions": 1.0000000000000002, + "totalSingleQubitDecompositions": 0.0 + } + } +} diff --git a/thesis-evaluation/figures/numberOfTwoQubitCreations.pdf b/thesis-evaluation/figures/numberOfTwoQubitCreations.pdf new file mode 100644 index 0000000000..728e1e2b04 Binary files /dev/null and b/thesis-evaluation/figures/numberOfTwoQubitCreations.pdf differ diff --git a/thesis-evaluation/figures/subCircuitComplexityChange.pdf b/thesis-evaluation/figures/subCircuitComplexityChange.pdf new file mode 100644 index 0000000000..8b865a6972 Binary files /dev/null and b/thesis-evaluation/figures/subCircuitComplexityChange.pdf differ diff --git a/thesis-evaluation/figures/successfulSingleQubitDecompositions.pdf b/thesis-evaluation/figures/successfulSingleQubitDecompositions.pdf new file mode 100644 index 0000000000..510bdabccc Binary files /dev/null and b/thesis-evaluation/figures/successfulSingleQubitDecompositions.pdf differ diff --git a/thesis-evaluation/figures/successfulTwoQubitDecompositions.pdf b/thesis-evaluation/figures/successfulTwoQubitDecompositions.pdf new file mode 100644 index 0000000000..61f53b4ccb Binary files /dev/null and b/thesis-evaluation/figures/successfulTwoQubitDecompositions.pdf differ diff --git a/thesis-evaluation/figures/timeInCircuitCollection.pdf b/thesis-evaluation/figures/timeInCircuitCollection.pdf new file mode 100644 index 0000000000..77f3a8b073 Binary files /dev/null and b/thesis-evaluation/figures/timeInCircuitCollection.pdf differ diff --git a/thesis-evaluation/figures/timeInSingleQubitDecomposition.pdf b/thesis-evaluation/figures/timeInSingleQubitDecomposition.pdf new file mode 100644 index 0000000000..5f29d6ae98 Binary files /dev/null and b/thesis-evaluation/figures/timeInSingleQubitDecomposition.pdf differ diff --git a/thesis-evaluation/figures/timeInTwoQubitDecomposition.pdf b/thesis-evaluation/figures/timeInTwoQubitDecomposition.pdf new file mode 100644 index 0000000000..60a4f414b2 Binary files /dev/null and b/thesis-evaluation/figures/timeInTwoQubitDecomposition.pdf differ diff --git a/thesis-evaluation/figures/timePerSingleQubitDecomposition.pdf b/thesis-evaluation/figures/timePerSingleQubitDecomposition.pdf new file mode 100644 index 0000000000..60a02c1f5b Binary files /dev/null and b/thesis-evaluation/figures/timePerSingleQubitDecomposition.pdf differ diff --git a/thesis-evaluation/figures/timePerTwoQubitDecomposition.pdf b/thesis-evaluation/figures/timePerTwoQubitDecomposition.pdf new file mode 100644 index 0000000000..657d119dfd Binary files /dev/null and b/thesis-evaluation/figures/timePerTwoQubitDecomposition.pdf differ diff --git a/thesis-evaluation/figures/totalCircuitCollections.pdf b/thesis-evaluation/figures/totalCircuitCollections.pdf new file mode 100644 index 0000000000..1a5bf20d5f Binary files /dev/null and b/thesis-evaluation/figures/totalCircuitCollections.pdf differ diff --git a/thesis-evaluation/figures/totalSingleQubitDecompositions.pdf b/thesis-evaluation/figures/totalSingleQubitDecompositions.pdf new file mode 100644 index 0000000000..7fe4e67167 Binary files /dev/null and b/thesis-evaluation/figures/totalSingleQubitDecompositions.pdf differ diff --git a/thesis-evaluation/figures/totalTouchedGates.pdf b/thesis-evaluation/figures/totalTouchedGates.pdf new file mode 100644 index 0000000000..bd00fdf79a Binary files /dev/null and b/thesis-evaluation/figures/totalTouchedGates.pdf differ diff --git a/thesis-evaluation/figures/totalTwoQubitDecompositions.pdf b/thesis-evaluation/figures/totalTwoQubitDecompositions.pdf new file mode 100644 index 0000000000..106bb3f534 Binary files /dev/null and b/thesis-evaluation/figures/totalTwoQubitDecompositions.pdf differ diff --git a/thesis-evaluation/figures/twoQubitCreationTime.pdf b/thesis-evaluation/figures/twoQubitCreationTime.pdf new file mode 100644 index 0000000000..8a23f856d0 Binary files /dev/null and b/thesis-evaluation/figures/twoQubitCreationTime.pdf differ diff --git a/thesis-evaluation/qiskit_run.py b/thesis-evaluation/qiskit_run.py new file mode 100644 index 0000000000..87172d9fa0 --- /dev/null +++ b/thesis-evaluation/qiskit_run.py @@ -0,0 +1,140 @@ +# 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 + +import glob +import pathlib +import time + +import numpy as np +from qiskit import QuantumCircuit, qasm2, qasm3 +from qiskit.circuit.library.standard_gates.x import CXGate +from qiskit.quantum_info import Operator +from qiskit.synthesis import ( + OneQubitEulerDecomposer, + TwoQubitBasisDecomposer, +) + +np.set_printoptions(linewidth=np.inf) + +MQT_BENCH_DIR = "../../mqt-bench/generated_benchmarks/v3_qasm3" +MQT_BENCH_PATTERN = "*.qasm" +is_qasm3 = True + + +def split_by_barriers(qc: QuantumCircuit) -> list[QuantumCircuit]: + subcircuits = [] + + # Start with an empty circuit that has the same registers + current = QuantumCircuit(*qc.qregs, *qc.cregs) + + for instr in qc.data: + if instr.name in {"barrier", "measure"}: + # Finish current subcircuit if it has content + if current.data: + subcircuits.append(current) + # Start a fresh one + current = QuantumCircuit(*qc.qregs, *qc.cregs) + else: + current.append(instr.operation, instr.qubits, instr.clbits) + + # Append the last chunk if non-empty + if current.data: + subcircuits.append(current) + + return subcircuits + + +def circuit_complexity(qc: QuantumCircuit) -> int: + num_one_qubit_gates = qc.size(lambda instr: len(instr.qubits) == 1 and instr.name != "barrier") + num_two_qubit_gates = qc.size(lambda instr: len(instr.qubits) == 2 and instr.name != "barrier") + qc.size(lambda instr: len(instr.qubits) != 1 and len(instr.qubits) != 2) + + return num_one_qubit_gates + 10 * num_two_qubit_gates + + +def evaluate(): + stats = {} + + otherCX = QuantumCircuit(2) + otherCX.cx(1, 0) + otherCXGate = otherCX.to_gate() + oneQubitDec = OneQubitEulerDecomposer("ZYZ") + start_time = time.perf_counter_ns() + dec = TwoQubitBasisDecomposer(CXGate(), euler_basis="ZYZ") + dec2 = TwoQubitBasisDecomposer(otherCXGate, euler_basis="ZYZ") + end_time = time.perf_counter_ns() + (end_time - start_time) / 1000 + + for file in glob.glob(f"{MQT_BENCH_DIR}/{MQT_BENCH_PATTERN}"): + name = pathlib.Path(file).name.removesuffix(".qasm") + content = pathlib.Path(file).read_text(encoding="utf-8") + try: + if is_qasm3: + content = "\n".join(filter(lambda line: "measure " not in line, content.splitlines())) + qc: QuantumCircuit = qasm3.loads(content) + else: + qc: QuantumCircuit = qasm2.loads(content) + except Exception: + continue + + # respect barriers + subcircuits = split_by_barriers(qc) + + decomposition_times_1q = [] + decomposition_times_2q = [] + complexity_changes = [] + num_two_qubit_decompositions = 0 + num_single_qubit_decompositions = 0 + for subcircuit in subcircuits: + m = Operator(subcircuit) + if len(qc.qubits) == 1: + start_time = time.perf_counter_ns() + decomposed_circuit = oneQubitDec(m) + decomposed_circuit2 = None + end_time = time.perf_counter_ns() + decomposition_time = (end_time - start_time) / 1000 + decomposition_times_1q.append(decomposition_time) + + num_single_qubit_decompositions += 1 + elif len(qc.qubits) == 2: + start_time = time.perf_counter_ns() + decomposed_circuit = dec(m) + decomposed_circuit2 = dec2(m) + end_time = time.perf_counter_ns() + decomposition_time = (end_time - start_time) / 1000 + decomposition_times_2q.append(decomposition_time) + + num_two_qubit_decompositions += 1 + else: + msg = "Invalid circuit size!" + raise RuntimeError(msg) + + before_complexity = circuit_complexity(subcircuit) + after_complexity = circuit_complexity(decomposed_circuit) + + if decomposed_circuit2: + after_complexity2 = circuit_complexity(decomposed_circuit2) + if after_complexity2 < after_complexity: + decomposed_circuit = decomposed_circuit2 + after_complexity = after_complexity2 + + complexity_changes.append(before_complexity - after_complexity) + + stats[name] = { + "timeInSingleQubitDecomposition": sum(decomposition_times_1q), + "timeInTwoQubitDecomposition": sum(decomposition_times_2q), + "subCircuitComplexityChange": sum(complexity_changes), + "totalTwoQubitDecompositions": num_two_qubit_decompositions, + "totalSingleQubitDecompositions": num_single_qubit_decompositions, + } + + return stats + + +if __name__ == "__main__": + evaluate() diff --git a/thesis-evaluation/run.sh b/thesis-evaluation/run.sh new file mode 100755 index 0000000000..d417d6f3c9 --- /dev/null +++ b/thesis-evaluation/run.sh @@ -0,0 +1,40 @@ +#!/bin/sh +# 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 + +OUTPUT_DIR="tmp-output" + +MQT_CORE_ROOT_DIR=.. +MQT_BENCH_BENCHMARK_DIR="../../mqt-bench/generated_benchmarks/v3_qasm3" +MQT_BENCH_PATTERN="*.qasm" + +mkdir -p "${OUTPUT_DIR}/broken" + +benchmark_count="$(eza -1 ${MQT_BENCH_BENCHMARK_DIR}/${MQT_BENCH_PATTERN} | wc -l)" +i=0 +success=0 + +for benchmark_path in ${MQT_BENCH_BENCHMARK_DIR}/${MQT_BENCH_PATTERN}; do + i=$((i + 1)) + benchmark="$(basename "${benchmark_path}")" + echo "${i}/${benchmark_count}: ${benchmark}" + result="$("${MQT_CORE_ROOT_DIR}/build/mlir/tools/mqt-cc/mqt-cc" --mlir-timing --mlir-statistics "${benchmark_path}" 2>&1)" + if [ "${?}" -eq 0 ]; then + echo "${result}" | rg 'Total Execution Time' -A 8 >"${OUTPUT_DIR}/${benchmark}.timing" + echo "${result}" | rg '\(S\) ' >"${OUTPUT_DIR}/${benchmark}.statistic" + echo "${result}" | rg 'module \{' -A 99999999999999 >"${OUTPUT_DIR}/${benchmark}.mlir" + echo "${result}" >"${OUTPUT_DIR}/${benchmark}.all" + echo "SUCCESS" + success=$((success + 1)) + else + echo "${result}" >"${OUTPUT_DIR}/broken/${benchmark}.error" + echo "FAILED" + fi +done + +echo "COMPLETED: ${success}/${i}" diff --git a/thesis-evaluation/total_evaluate.py b/thesis-evaluation/total_evaluate.py new file mode 100644 index 0000000000..c7d03c7063 --- /dev/null +++ b/thesis-evaluation/total_evaluate.py @@ -0,0 +1,180 @@ +# 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 + +import json +import math +import pathlib +import subprocess + +import evaluate +import matplotlib.pyplot as plt +import qiskit_run + +OUT_DIR = "./figures" +if not pathlib.Path(OUT_DIR).exists(): + pathlib.Path(OUT_DIR).mkdir(exist_ok=True, parents=True) + +CACHE_FILE = "./evaluate_cache.json" +if pathlib.Path(CACHE_FILE).exists(): + with pathlib.Path(CACHE_FILE).open(encoding="utf-8") as f: + cache = json.load(f) + mqt_results = cache["mqt"] + qiskit_results = cache["qiskit"] +else: + ITERATIONS = 20 + all_mqt_results = [] + all_qiskit_results = [] + for _ in range(ITERATIONS): + # evaluate.evalue() will only process statistics files; need to re-run mqt-cc using run.sh + subprocess.run(["./run.sh"]).check_returncode() + all_mqt_results.append(evaluate.evaluate()) + all_qiskit_results.append(qiskit_run.evaluate()) + + def average_results( + all_results: list[dict[str, dict[str, int | float]]], + ) -> dict[str, dict[str, int | float]]: + result = {} + for r in all_results: + for benchmark_name, measurements in r.items(): + if benchmark_name not in result: + result[benchmark_name] = {} + for metric_name, value in measurements.items(): + if metric_name not in result[benchmark_name]: + result[benchmark_name][metric_name] = 0.0 + result[benchmark_name][metric_name] += value / ITERATIONS + return result + + # [benchmark_name -> [metric_name -> value]] + mqt_results = average_results(all_mqt_results) + qiskit_results = average_results(all_qiskit_results) + + with pathlib.Path(CACHE_FILE).open("w", encoding="utf-8") as f: + cache = { + "mqt": mqt_results, + "qiskit": qiskit_results, + } + json.dump(cache, f) + + +x: dict[str, list[str]] = {} +y1: dict[str, list[int | float]] = {} +y2: dict[str, list[int | float]] = {} + + +def define_division_metric(new_metric: str, old_metric: str, divisor_metric: str, benchmark_name: str) -> None: + y1[new_metric] = [*y1.get(new_metric, []), m[old_metric] / m[divisor_metric] if m[divisor_metric] > 0 else float("nan")] + y2[new_metric] = [*y2.get(new_metric, []), q[old_metric] / q[divisor_metric] if q[divisor_metric] > 0 else float("nan")] + x[new_metric] = [*x.get(new_metric, []), benchmark_name] + + +for name in sorted(mqt_results.keys() & qiskit_results.keys()): + m = mqt_results[name] + q = qiskit_results[name] + + for metric in m.keys() | q.keys(): + if metric in m: + y1[metric] = [*y1.get(metric, []), m[metric]] + if metric in q: + y2[metric] = [*y2.get(metric, []), q[metric]] + + x[metric] = [*x.get(metric, []), name] + + define_division_metric( + "timePerSingleQubitDecomposition", + "timeInSingleQubitDecomposition", + "totalSingleQubitDecompositions", + name, + ) + define_division_metric( + "timePerTwoQubitDecomposition", + "timeInTwoQubitDecomposition", + "totalTwoQubitDecompositions", + name, + ) + +names = [] +titles = { + "subCircuitComplexityChange": "Complexity Change after Decomposition", + "successfulSingleQubitDecompositions": "Number of Successful Single-Qubit Decompositions", + "successfulTwoQubitDecompositions": "Number of Successful Two-Qubit Decompositions", + "timeInCircuitCollection": "Sub-Circuit Collection Time [µs]", + "timeInSingleQubitDecomposition": "Total Time for Single-Qubit Decompositions [µs]", + "timeInTwoQubitDecomposition": "Total Time for Two-Qubit Decompositions [µs]", + "totalCircuitCollections": "Number of Sub-Circuit Collections", + "totalSingleQubitDecompositions": "Total Number of Single-Qubit Decompositions", + "totalTouchedGates": "Total Number of Gates in Collected Sub-Circuits", + "totalTwoQubitDecompositions": "Total Number of Two-Qubit Decompositions", + "timePerTwoQubitDecomposition": "Time / Two-Qubit Decomposition [µs]", + "timePerSingleQubitDecomposition": "Time / Single-Qubit Decomposition [µs]", +} +legend_positions = { + "totalTouchedGates": "upper left", + "timeInCircuitCollection": "upper left", + "timePerSingleQubitDecomposition": "upper left", + "timePerTwoQubitDecomposition": "lower right", + "twoQubitCreationTime": "lower right", +} +for metric in x: + plt.title(titles.get(metric, metric)) + # x_values = x[metric] # use for benchmark names on x axis + x_values = [str(i) for i in range(len(x[metric]))] + + DEFAULT_POINT_SIZE = 100 + scale1 = [] + scale2 = [] + num_erased_y = 0 + for i in range(len(y1[metric])): + y1i = y1[metric][i - num_erased_y] if metric in y1 else None + y2i = y2[metric][i - num_erased_y] if metric in y2 else None + if (not y1i and not y2i) or (y1i and y2i and math.isnan(y1i) and math.isnan(y2i)): + x_values.pop(i - num_erased_y) + if metric in y1: + y1[metric].pop(i - num_erased_y) + if metric in y2: + y2[metric].pop(i - num_erased_y) + num_erased_y += 1 + elif not y1i or math.isnan(y1i): + scale1.append(0) + scale2.append(DEFAULT_POINT_SIZE) + elif not y2i or math.isnan(y2i): + scale1.append(DEFAULT_POINT_SIZE) + scale2.append(0) + else: + scale1.append(DEFAULT_POINT_SIZE) + scale2.append(DEFAULT_POINT_SIZE) + + ymin = float("inf") + if metric in y1: + mqt_scatter = plt.scatter(x_values, y1[metric], color="blue", s=scale1, alpha=0.4) + mqt_scatter.set_label("MQT") + ymin = min(ymin, *y1[metric]) + if metric in y2: + qiskit_scatter = plt.scatter(x_values, y2[metric], color="red", s=scale2, alpha=0.4) + qiskit_scatter.set_label("Qiskit") + ymin = min(ymin, *y2[metric]) + + # plt.xticks(x_values) # does not work for strings + if ymin > 0: + # let matplotlib handle non-positive values automatically + plt.ylim(bottom=0) + + locs, labels = plt.yticks() + yint = [int(each) for each in locs] + plt.yticks(yint) + + names = x[metric] + leg = plt.legend(loc=legend_positions.get(metric, "best")) + for handle in leg.legend_handles: + handle.set_sizes([DEFAULT_POINT_SIZE]) + plt.savefig(f"{OUT_DIR}/{metric}.pdf", format="pdf", bbox_inches="tight", pad_inches=0) + # plt.show() + plt.clf() + +for i, name in enumerate(names): + if "_indep_1_none_O0" in name or "_indep_2_none_O0" in name: + pass