diff --git a/CHANGELOG.md b/CHANGELOG.md index e77994fb95..2cb9cc836b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,10 +11,11 @@ This project adheres to [Semantic Versioning], with the exception that minor rel ### Added +- ✨ Add a `gate-decomposition` pass for optimizing sequences of single- and two-qubit gates ([#1206]) ([**@taminob**]) - ✨ Add conversions between Jeff and QCO ([#1479], [#1548]) ([**@denialhaag**]) - ✨ Add a `place-and-route` pass for mapping circuits to architectures with restricted topologies ([#1537], [#1547]) ([**@MatthiasReumann**]) - ✨ Add initial infrastructure for new QC and QCO MLIR dialects - ([#1264], [#1330], [#1402], [#1428], [#1430], [#1436], [#1443], [#1446], [#1464], [#1465], [#1470], [#1471], [#1472], [#1474], [#1475], [#1506], [#1510], [#1513], [#1521], [#1548], [#1550]) + ([#1264], [#1330], [#1402], [#1426], [#1428], [#1430], [#1436], [#1443], [#1446], [#1464], [#1465], [#1470], [#1471], [#1472], [#1474], [#1475], [#1506], [#1510], [#1513], [#1521], [#1548], [#1550]) ([**@burgholzer**], [**@denialhaag**], [**@taminob**], [**@DRovara**], [**@li-mingbao**], [**@Ectras**], [**@MatthiasReumann**], [**@simon1hofmann**]) ### Changed @@ -359,6 +360,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 @@ -410,6 +412,7 @@ _📚 Refer to the [GitHub Release Notes](https://github.com/munich-quantum-tool [#1210]: https://github.com/munich-quantum-toolkit/core/pull/1210 [#1209]: https://github.com/munich-quantum-toolkit/core/pull/1209 [#1207]: https://github.com/munich-quantum-toolkit/core/pull/1207 +[#1206]: https://github.com/munich-quantum-toolkit/core/pull/1206 [#1186]: https://github.com/munich-quantum-toolkit/core/pull/1186 [#1181]: https://github.com/munich-quantum-toolkit/core/pull/1181 [#1180]: https://github.com/munich-quantum-toolkit/core/pull/1180 diff --git a/mlir/include/mlir/Compiler/CompilerPipeline.h b/mlir/include/mlir/Compiler/CompilerPipeline.h index ee7fbc9adf..1ab8193ff2 100644 --- a/mlir/include/mlir/Compiler/CompilerPipeline.h +++ b/mlir/include/mlir/Compiler/CompilerPipeline.h @@ -120,6 +120,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/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.h new file mode 100644 index 0000000000..dff0cd20c8 --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.h @@ -0,0 +1,248 @@ +/* + * 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 "WeylDecomposition.h" + +#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). + * + * @note Adapted from TwoQubitBasisDecomposer in the IBM Qiskit framework. + * (C) Copyright IBM 2023 + * + * 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. + */ +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). + */ + [[nodiscard]] static TwoQubitBasisDecomposer create(const Gate& basisGate, + double basisFidelity); + + /** + * 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; + +protected: + // NOLINTBEGIN(modernize-pass-by-value) + /** + * Constructs decomposer instance. + */ + TwoQubitBasisDecomposer( + Gate basisGate, double basisFidelity, + const decomposition::TwoQubitWeylDecomposition& basisDecomposer, + bool isSuperControlled, const Eigen::Matrix2cd& u0l, + const Eigen::Matrix2cd& u0r, const Eigen::Matrix2cd& u1l, + const Eigen::Matrix2cd& u1ra, const Eigen::Matrix2cd& u1rb, + const Eigen::Matrix2cd& u2la, const Eigen::Matrix2cd& u2lb, + const Eigen::Matrix2cd& u2ra, const Eigen::Matrix2cd& u2rb, + const Eigen::Matrix2cd& u3l, const Eigen::Matrix2cd& u3r, + const Eigen::Matrix2cd& q0l, const Eigen::Matrix2cd& q0r, + const Eigen::Matrix2cd& q1la, const Eigen::Matrix2cd& q1lb, + const Eigen::Matrix2cd& q1ra, const Eigen::Matrix2cd& q1rb, + const Eigen::Matrix2cd& q2l, const Eigen::Matrix2cd& 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 + * + * @note The inline storage of llvm::SmallVector must be set to 0 to ensure + * correct Eigen alignment via heap allocation + */ + [[nodiscard]] static llvm::SmallVector + decomp0(const decomposition::TwoQubitWeylDecomposition& target); + + /** + * 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``. + * + * @note The inline storage of llvm::SmallVector must be set to 0 to ensure + * correct Eigen alignment via heap allocation + */ + [[nodiscard]] llvm::SmallVector + decomp1(const decomposition::TwoQubitWeylDecomposition& target) const; + + /** + * 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. + * + * @note The inline storage of llvm::SmallVector must be set to 0 to ensure + * correct Eigen alignment via heap allocation + */ + [[nodiscard]] llvm::SmallVector decomp2Supercontrolled( + const decomposition::TwoQubitWeylDecomposition& target) const; + + /** + * 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. + * + * @note The inline storage of llvm::SmallVector must be set to 0 to ensure + * correct Eigen alignment via heap allocation + */ + [[nodiscard]] llvm::SmallVector decomp3Supercontrolled( + const decomposition::TwoQubitWeylDecomposition& target) const; + + /** + * 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, 4> + traces(const decomposition::TwoQubitWeylDecomposition& target) const; + /** + * 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 + unitaryToGateSequence(const Eigen::Matrix2cd& 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); + + [[nodiscard]] static bool relativeEq(double lhs, double rhs, double epsilon, + double maxRelative); + +private: + // basis gate of this decomposer instance + Gate basisGate{}; + // fidelity with which the basis gate decomposition has been calculated + double 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 + Eigen::Matrix2cd u0l; + Eigen::Matrix2cd u0r; + Eigen::Matrix2cd u1l; + Eigen::Matrix2cd u1ra; + Eigen::Matrix2cd u1rb; + Eigen::Matrix2cd u2la; + Eigen::Matrix2cd u2lb; + Eigen::Matrix2cd u2ra; + Eigen::Matrix2cd u2rb; + Eigen::Matrix2cd u3l; + Eigen::Matrix2cd u3r; + + // pre-built components for decomposition with 2 basis gates + Eigen::Matrix2cd q0l; + Eigen::Matrix2cd q0r; + Eigen::Matrix2cd q1la; + Eigen::Matrix2cd q1lb; + Eigen::Matrix2cd q1ra; + Eigen::Matrix2cd q1rb; + Eigen::Matrix2cd q2l; + Eigen::Matrix2cd q2r; +}; + +} // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h new file mode 100644 index 0000000000..91e8a26dd6 --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h @@ -0,0 +1,50 @@ +/* + * 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. + */ +inline constexpr auto DEFAULT_ATOL = 1e-12; + +/** + * EulerBasis for a euler decomposition. + */ +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, +}; + +/** + * Get list of gates potentially involved in euler basis after euler + * decomposition. + */ +[[nodiscard]] llvm::SmallVector +getGateTypesForEulerBasis(EulerBasis eulerBasis); + +} // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.h new file mode 100644 index 0000000000..e238ef1769 --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.h @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "EulerBasis.h" +#include "GateSequence.h" +#include "ir/operations/OpType.hpp" + +#include + +#include +#include + +namespace mlir::qco::decomposition { + +/** + * Decomposition of single-qubit matrices into rotation gates using a KAK + * decomposition. + * + * @note only the following euler bases are supported for now: + * ZYZ, ZXZ, XYX and XZX + */ +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 Eigen::Matrix2cd& unitaryMatrix, + bool simplify, std::optional atol); + + /** + * 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 Eigen::Matrix2cd& matrix, + EulerBasis basis); + +private: + static std::array paramsZyz(const Eigen::Matrix2cd& matrix); + + static std::array paramsZxz(const Eigen::Matrix2cd& matrix); + + static std::array paramsXyx(const Eigen::Matrix2cd& matrix); + + static std::array paramsXzx(const Eigen::Matrix2cd& matrix); + + /** + * @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(double theta, double phi, double lambda, double phase, + qc::OpType kGate, qc::OpType aGate, bool simplify, + std::optional atol); +}; +} // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Gate.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Gate.h new file mode 100644 index 0000000000..c7af09fbd8 --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Gate.h @@ -0,0 +1,31 @@ +/* + * 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 + +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/Dialect/QCO/Transforms/Decomposition/GateSequence.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h new file mode 100644 index 0000000000..118fe07fce --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h @@ -0,0 +1,59 @@ +/* + * 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 +#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. + */ + double globalPhase{}; + /** + * @return true if the global phase adjustment is not zero. + */ + [[nodiscard]] bool hasGlobalPhase() const; + + /** + * Calculate complexity of sequence according to getComplexity(). + */ + [[nodiscard]] std::size_t complexity() const; + + /** + * Calculate overall unitary matrix of the sequence. + */ + [[nodiscard]] Eigen::Matrix4cd getUnitaryMatrix() const; +}; +/** + * 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/Dialect/QCO/Transforms/Decomposition/Helpers.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h new file mode 100644 index 0000000000..314f10142f --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h @@ -0,0 +1,70 @@ +/* + * 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 "mlir/Dialect/QCO/IR/QCOInterfaces.h" + +#include +#include + +#include +#include + +namespace mlir::qco::helpers { + +[[nodiscard]] qc::OpType getQcType(UnitaryOpInterface op); + +// NOLINTBEGIN(misc-include-cleaner) +template +[[nodiscard]] auto selfAdjointEvd(const Eigen::Matrix& a) { + Eigen::SelfAdjointEigenSolver> s; + s.compute(a); + auto vecs = s.eigenvectors().eval(); + auto vals = s.eigenvalues(); + return std::make_pair(vecs, vals); +} + +template +[[nodiscard]] bool isUnitaryMatrix(const Eigen::Matrix& matrix, + double tolerance = 1e-12) { + return (matrix.transpose().conjugate() * matrix).isIdentity(tolerance); +} +// NOLINTEND(misc-include-cleaner) + +/** + * Euclidean remainder of a modulo b. + * The returned value is never negative. + */ +[[nodiscard]] double remEuclid(double a, double b); + +/** + * Wrap angle into interval [-π,π). If within atol of the endpoint, clamp to -π. + */ +[[nodiscard]] double mod2pi(double angle, double angleZeroEpsilon = 1e-13); + +/** + * Calculate fidelity value of given trace. + */ +[[nodiscard]] double traceToFidelity(const std::complex& x); + +/** + * Get complexity of gate operating on given number of qubits. + */ +[[nodiscard]] std::size_t getComplexity(qc::OpType type, + std::size_t numOfQubits); + +/** + * Return complex factor which can be multiplied with the operation matrix. + */ +[[nodiscard]] std::complex globalPhaseFactor(double globalPhase); + +} // namespace mlir::qco::helpers diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h new file mode 100644 index 0000000000..bfa08c8a5c --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h @@ -0,0 +1,61 @@ +/* + * 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 + +namespace mlir::qco::decomposition { + +inline constexpr double SQRT2 = 1.414213562373095048801688724209698079L; +inline constexpr double FRAC1_SQRT2 = + 0.707106781186547524400844362104849039284835937688474036588L; + +[[nodiscard]] Eigen::Matrix2cd uMatrix(double lambda, double phi, double theta); + +[[nodiscard]] Eigen::Matrix2cd u2Matrix(double lambda, double phi); + +[[nodiscard]] Eigen::Matrix2cd rxMatrix(double theta); + +[[nodiscard]] Eigen::Matrix2cd ryMatrix(double theta); + +[[nodiscard]] Eigen::Matrix2cd rzMatrix(double theta); + +[[nodiscard]] Eigen::Matrix4cd rxxMatrix(double theta); + +[[nodiscard]] Eigen::Matrix4cd ryyMatrix(double theta); + +[[nodiscard]] Eigen::Matrix4cd rzzMatrix(double theta); + +[[nodiscard]] Eigen::Matrix2cd pMatrix(double lambda); + +inline const Eigen::Matrix4cd SWAP_GATE{ + {1, 0, 0, 0}, {0, 0, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 1}}; +inline const Eigen::Matrix2cd H_GATE{{FRAC1_SQRT2, FRAC1_SQRT2}, + {FRAC1_SQRT2, -FRAC1_SQRT2}}; +inline const Eigen::Matrix2cd IPZ{{{0, 1}, 0}, {0, {0, -1}}}; +inline const Eigen::Matrix2cd IPY{{0, 1}, {-1, 0}}; +inline const Eigen::Matrix2cd IPX{{0, {0, 1}}, {{0, 1}, 0}}; + +[[nodiscard]] Eigen::Matrix4cd +expandToTwoQubits(const Eigen::Matrix2cd& singleQubitMatrix, QubitId qubitId); + +[[nodiscard]] Eigen::Matrix4cd +fixTwoQubitMatrixQubitOrder(const Eigen::Matrix4cd& twoQubitMatrix, + const llvm::SmallVector& qubitIds); + +[[nodiscard]] Eigen::Matrix2cd getSingleQubitMatrix(const Gate& gate); + +// TODO: remove? only used for verification of circuit and in unittests +[[nodiscard]] Eigen::Matrix4cd getTwoQubitMatrix(const Gate& gate); + +} // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.h new file mode 100644 index 0000000000..28b713cd8c --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.h @@ -0,0 +1,251 @@ +/* + * 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 // NOLINT(misc-include-cleaner) + +#include +#include +#include +#include +#include +#include + +namespace mlir::qco::decomposition { +/** + * Allowed deviation for internal assert statements which ensure the correctness + * of the decompositions. + */ +constexpr double 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). + * + * @note Adapted from TwoQubitWeylDecomposition in the IBM Qiskit framework. + * (C) Copyright IBM 2023 + * + * 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. + */ +class TwoQubitWeylDecomposition { +public: + /** + * 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 Eigen::Matrix4cd& unitaryMatrix, + std::optional fidelity); + + ~TwoQubitWeylDecomposition() = default; + TwoQubitWeylDecomposition(const TwoQubitWeylDecomposition&) = default; + TwoQubitWeylDecomposition(TwoQubitWeylDecomposition&&) = default; + TwoQubitWeylDecomposition& + operator=(const TwoQubitWeylDecomposition&) = default; + TwoQubitWeylDecomposition& operator=(TwoQubitWeylDecomposition&&) = default; + + /** + * Calculate matrix of canonical gate based on its parameters a, b, c. + */ + [[nodiscard]] Eigen::Matrix4cd getCanonicalMatrix() const { + return getCanonicalMatrix(a_, b_, c_); + } + + /** + * First parameter of canonical gate. + * + * @note must be multiplied by -2.0 for rotation angle of RXX gate + */ + [[nodiscard]] double a() const { return a_; } + /** + * Second parameter of canonical gate. + * + * @note must be multiplied by -2.0 for rotation angle of RYY gate + */ + [[nodiscard]] double b() const { return b_; } + /** + * Third parameter of canonical gate. + * + * @note must be multiplied by -2.0 for rotation angle of RZZ gate + */ + [[nodiscard]] double c() const { return c_; } + /** + * Necessary global phase adjustment after applying decomposition. + */ + [[nodiscard]] double globalPhase() const { return globalPhase_; } + + /** + * "Left" qubit after canonical gate. + * + * q1 - k2r - C - k1r - + * A + * q0 - k2l - N - *k1l* - + */ + [[nodiscard]] const Eigen::Matrix2cd& k1l() const { return k1l_; } + /** + * "Left" qubit before canonical gate. + * + * q1 - k2r - C - k1r - + * A + * q0 - *k2l* - N - k1l - + */ + [[nodiscard]] const Eigen::Matrix2cd& k2l() const { return k2l_; } + /** + * "Right" qubit after canonical gate. + * + * q1 - k2r - C - *k1r* - + * A + * q0 - k2l - N - k1l - + */ + [[nodiscard]] const Eigen::Matrix2cd& k1r() const { return k1r_; } + /** + * "Right" qubit before canonical gate. + * + * q1 - *k2r* - C - k1r - + * A + * q0 - k2l - N - k1l - + */ + [[nodiscard]] const Eigen::Matrix2cd& k2r() const { return k2r_; } + + /** + * Calculate matrix of canonical gate based on given parameters a, b, c. + */ + [[nodiscard]] static Eigen::Matrix4cd getCanonicalMatrix(double a, double b, + double c); + +protected: + 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 + }; + + enum class MagicBasisTransform : std::uint8_t { + Into, + OutOf, + }; + + /** + * Threshold for imprecision in computation of diagonalization. + */ + static constexpr auto DIAGONALIZATION_PRECISION = 1e-13; + + TwoQubitWeylDecomposition() = default; + + [[nodiscard]] static Eigen::Matrix4cd + magicBasisTransform(const Eigen::Matrix4cd& unitary, + MagicBasisTransform direction); + + [[nodiscard]] static double closestPartialSwap(double a, double b, double c); + + /** + * 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 Eigen::Matrix4cd& m, double precision); + + /** + * 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 Eigen::Matrix4cd& specialUnitary); + + /** + * 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 std::complex + getTrace(double a, double b, double c, double ap, double bp, double cp); + + /** + * Choose the best specialization 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; + + /** + * @return true if the specialization flipped the original decomposition + */ + bool applySpecialization(); + +private: + // a, b, c are the parameters of the canonical gate (CAN) + double a_{}; // rotation of RXX gate in CAN (must be taken times -2.0) + double b_{}; // rotation of RYY gate in CAN (must be taken times -2.0) + double c_{}; // rotation of RZZ gate in CAN (must be taken times -2.0) + double globalPhase_{}; // global phase adjustment + /** + * q1 - k2r - C - k1r - + * A + * q0 - k2l - N - k1l - + */ + Eigen::Matrix2cd k1l_; // "left" qubit after canonical gate + Eigen::Matrix2cd k2l_; // "left" qubit before canonical gate + Eigen::Matrix2cd k1r_; // "right" qubit after canonical gate + Eigen::Matrix2cd k2r_; // "right" qubit before canonical gate + Specialization specialization{ + Specialization::General}; // detected symmetries in the matrix + EulerBasis defaultEulerBasis{ + EulerBasis::U3}; // recommended euler basis for k1l/k2l/k1r/k2r + std::optional // desired fidelity; + requestedFidelity; // if set to std::nullopt, no automatic + // specialization will be applied + double calculatedFidelity{}; // actual fidelity of decomposition + Eigen::Matrix4cd unitaryMatrix; // original matrix for this decomposition +}; +} // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h index c3589793e6..184cac211b 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h @@ -16,11 +16,19 @@ #include #include +namespace mlir { + +class RewritePatternSet; + +} // namespace mlir + namespace mlir::qco { #define GEN_PASS_DECL #include "mlir/Dialect/QCO/Transforms/Passes.h.inc" // IWYU pragma: export +void populateGateDecompositionPatterns(mlir::RewritePatternSet& patterns); + //===----------------------------------------------------------------------===// // Registration //===----------------------------------------------------------------------===// diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index 6f090fd44d..8ee7d77349 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -11,6 +11,19 @@ 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. + }]; +} + //===----------------------------------------------------------------------===// // Transpilation Passes //===----------------------------------------------------------------------===// diff --git a/mlir/lib/Compiler/CMakeLists.txt b/mlir/lib/Compiler/CMakeLists.txt index 990ac0cb7e..be5d049be9 100644 --- a/mlir/lib/Compiler/CMakeLists.txt +++ b/mlir/lib/Compiler/CMakeLists.txt @@ -21,6 +21,7 @@ add_mlir_library( MLIRQCOToQC MLIRQCToQIR MQT::MLIRSupport + MLIRQCOTransforms DISABLE_INSTALL) mqt_mlir_target_use_project_options(MQTCompilerPipeline) diff --git a/mlir/lib/Compiler/CompilerPipeline.cpp b/mlir/lib/Compiler/CompilerPipeline.cpp index 18040d7720..ac2ee8ed27 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/Dialect/QCO/Transforms/Passes.h" #include "mlir/Support/PrettyPrinting.h" #include @@ -51,6 +52,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) { @@ -140,8 +146,7 @@ QuantumCompilerPipeline::runPipeline(ModuleOp module, } } // Stage 5: Optimization passes - // TODO: Add optimization passes - if (failed(runStage([&](PassManager& pm) { addCleanupPasses(pm); }))) { + if (failed(runStage([&](PassManager& pm) { addOptimizationPasses(pm); }))) { return failure(); } if (record != nullptr && config_.recordIntermediates) { diff --git a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt index 3b965dce5e..30b7b7d64a 100644 --- a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt +++ b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt @@ -15,7 +15,7 @@ add_mlir_library( PRIVATE MLIRQCODialect MLIRQCOUtils - ${dialect_libs} + MQT::CoreIR DEPENDS MLIRQCOTransformsIncGen) diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp new file mode 100644 index 0000000000..dea8dda589 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp @@ -0,0 +1,401 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.h" + +#include "ir/Definitions.hpp" +#include "mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Gate.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mlir::qco::decomposition { +TwoQubitBasisDecomposer TwoQubitBasisDecomposer::create(const Gate& basisGate, + double basisFidelity) { + using namespace std::complex_literals; + + const Eigen::Matrix2cd k12RArr{ + {1i * FRAC1_SQRT2, FRAC1_SQRT2}, + {-FRAC1_SQRT2, -1i * FRAC1_SQRT2}, + }; + const Eigen::Matrix2cd k12LArr{ + {{0.5, 0.5}, {0.5, 0.5}}, + {{-0.5, 0.5}, {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(); + std::complex temp{0.5, -0.5}; + const Eigen::Matrix2cd k11l{ + {temp * (-1i * std::exp(-1i * b)), temp * std::exp(-1i * b)}, + {temp * (-1i * std::exp(1i * b)), temp * -std::exp(1i * b)}}; + const Eigen::Matrix2cd k11r{ + {FRAC1_SQRT2 * (1i * std::exp(-1i * b)), + FRAC1_SQRT2 * -std::exp(-1i * b)}, + {FRAC1_SQRT2 * std::exp(1i * b), FRAC1_SQRT2 * (-1i * std::exp(1i * b))}}; + const Eigen::Matrix2cd k32lK21l{ + {FRAC1_SQRT2 * std::complex{1., std::cos(2. * b)}, + FRAC1_SQRT2 * (1i * std::sin(2. * b))}, + {FRAC1_SQRT2 * (1i * std::sin(2. * b)), + FRAC1_SQRT2 * std::complex{1., -std::cos(2. * b)}}}; + temp = std::complex{0.5, 0.5}; + const Eigen::Matrix2cd k21r{ + {temp * (-1i * std::exp(-2i * b)), temp * std::exp(-2i * b)}, + {temp * (1i * std::exp(2i * b)), temp * std::exp(2i * b)}, + }; + const Eigen::Matrix2cd k22l{ + {FRAC1_SQRT2, -FRAC1_SQRT2}, + {FRAC1_SQRT2, FRAC1_SQRT2}, + }; + const Eigen::Matrix2cd k22r{{0, 1}, {-1, 0}}; + const Eigen::Matrix2cd k31l{ + {FRAC1_SQRT2 * std::exp(-1i * b), FRAC1_SQRT2 * std::exp(-1i * b)}, + {FRAC1_SQRT2 * -std::exp(1i * b), FRAC1_SQRT2 * std::exp(1i * b)}, + }; + const Eigen::Matrix2cd k31r{ + {1i * std::exp(1i * b), 0}, + {0, -1i * std::exp(-1i * b)}, + }; + temp = std::complex{0.5, 0.5}; + const Eigen::Matrix2cd k32r{ + {temp * std::exp(1i * b), temp * -std::exp(-1i * b)}, + {temp * (-1i * std::exp(1i * b)), temp * (-1i * std::exp(-1i * 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, + }; +} + +std::optional TwoQubitBasisDecomposer::twoQubitDecompose( + const decomposition::TwoQubitWeylDecomposition& targetDecomposition, + const llvm::SmallVector& target1qEulerBases, + std::optional basisFidelity, bool approximate, + std::optional numBasisGateUses) const { + if (target1qEulerBases.empty()) { + llvm::reportFatalUsageError( + "Unable to perform two-qubit basis decomposition without at least " + "one euler basis!"); + } + + auto getBasisFidelity = [&]() { + if (approximate) { + return basisFidelity.value_or(this->basisFidelity); + } + return 1.0; + }; + double actualBasisFidelity = getBasisFidelity(); + auto traces = this->traces(targetDecomposition); + auto getDefaultNbasis = [&]() -> std::uint8_t { + // determine smallest number of basis gates required to fulfill given + // basis fidelity constraint + auto bestValue = std::numeric_limits::lowest(); + auto bestIndex = -1; + for (int i = 0; std::cmp_less(i, traces.size()); ++i) { + // lower basis fidelity means it becomes easier to use fewer basis gates + // through a rougher approximation + auto value = helpers::traceToFidelity(traces[i]) * + std::pow(actualBasisFidelity, i); + if (value > bestValue) { + bestIndex = i; + bestValue = value; + } + } + // index in traces equals number of basis gates; return -1/255 if no + // matching number of basis gates was found (should never happen) + return static_cast(bestIndex); + }; + // number of basis gates that need to be used in the decomposition + auto bestNbasis = numBasisGateUses.value_or(getDefaultNbasis()); + if (bestNbasis > 1 && !isSuperControlled) { + // cannot reliably decompose with more than one basis gate and a + // non-super-controlled basis gate + return std::nullopt; + } + 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); + } + llvm::reportFatalInternalError( + "Invalid number of basis gates to use in basis decomposition (" + + llvm::Twine(bestNbasis) + ")!"); + llvm_unreachable(""); + }; + auto decomposition = chooseDecomposition(); + llvm::SmallVector eulerDecompositions; + for (auto&& decomp : decomposition) { + assert(helpers::isUnitaryMatrix(decomp)); + auto eulerDecomp = unitaryToGateSequence(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) { + 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; +} + +llvm::SmallVector +TwoQubitBasisDecomposer::decomp0(const TwoQubitWeylDecomposition& target) { + return { + target.k1r() * target.k2r(), + target.k1l() * target.k2l(), + }; +} + +llvm::SmallVector TwoQubitBasisDecomposer::decomp1( + const 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(), + }; +} + +llvm::SmallVector +TwoQubitBasisDecomposer::decomp2Supercontrolled( + const TwoQubitWeylDecomposition& target) const { + if (!isSuperControlled) { + llvm::reportFatalInternalError( + "Basis gate of TwoQubitBasisDecomposer is not super-controlled " + "- no guarantee for exact decomposition with two basis gates"); + } + 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, + }; +} + +llvm::SmallVector +TwoQubitBasisDecomposer::decomp3Supercontrolled( + const TwoQubitWeylDecomposition& target) const { + if (!isSuperControlled) { + llvm::reportFatalInternalError( + "Basis gate of TwoQubitBasisDecomposer is not super-controlled " + "- no guarantee for exact decomposition with three basis gates"); + } + 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, + }; +} + +std::array, 4> +TwoQubitBasisDecomposer::traces(const TwoQubitWeylDecomposition& target) const { + return { + 4. * std::complex{std::cos(target.a()) * std::cos(target.b()) * + std::cos(target.c()), + std::sin(target.a()) * std::sin(target.b()) * + std::sin(target.c())}, + 4. * std::complex{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())}, + std::complex{4. * std::cos(target.c()), 0.}, + std::complex{4., 0.}, + }; +} + +OneQubitGateSequence TwoQubitBasisDecomposer::unitaryToGateSequence( + const Eigen::Matrix2cd& 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) { + assert(!targetBasisList.empty()); + + auto calculateError = [](const OneQubitGateSequence& sequence) -> double { + 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); + // check top-left 2x2 matrix of generated circuit since the circuit + // operates only on one qubit + assert((circuit.getUnitaryMatrix().block<2, 2>(0, 0).isApprox( + unitaryMat, SANITY_CHECK_PRECISION))); + auto error = calculateError(circuit); + if (error < minError) { + bestCircuit = circuit; + minError = error; + } + } + return bestCircuit; +} + +bool TwoQubitBasisDecomposer::relativeEq(double lhs, double rhs, double epsilon, + double 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; +} + +} // namespace mlir::qco::decomposition diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerBasis.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerBasis.cpp new file mode 100644 index 0000000000..22219ef28f --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerBasis.cpp @@ -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 + */ + +#include "mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h" + +#include "ir/operations/OpType.hpp" + +#include +#include + +namespace mlir::qco::decomposition { + +[[nodiscard]] 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::SX}; + case EulerBasis::PSX: + [[fallthrough]]; + case EulerBasis::U1X: + return {qc::RZ, qc::P}; + } + llvm::reportFatalInternalError( + "Unsupported euler basis for translation to gate types"); +} + +} // namespace mlir::qco::decomposition diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.cpp new file mode 100644 index 0000000000..ba70d7d57e --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.cpp @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.h" + +#include "ir/Definitions.hpp" +#include "ir/operations/OpType.hpp" +#include "mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h" + +#include +#include + +#include +#include +#include +#include +#include + +namespace mlir::qco::decomposition { + +OneQubitGateSequence +EulerDecomposition::generateCircuit(EulerBasis targetBasis, + const Eigen::Matrix2cd& 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: + llvm::reportFatalInternalError("Unsupported euler basis for circuit " + "generation in decomposition!"); + } +} + +std::array +EulerDecomposition::anglesFromUnitary(const Eigen::Matrix2cd& matrix, + EulerBasis basis) { + if (basis == EulerBasis::XYX) { + return paramsXyx(matrix); + } + if (basis == EulerBasis::XZX) { + return paramsXzx(matrix); + } + if (basis == EulerBasis::ZYZ) { + return paramsZyz(matrix); + } + if (basis == EulerBasis::ZXZ) { + return paramsZxz(matrix); + } + llvm::reportFatalInternalError( + "Unsupported euler basis for angle computation in decomposition!"); +} + +std::array +EulerDecomposition::paramsZyz(const Eigen::Matrix2cd& 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}; +} + +std::array +EulerDecomposition::paramsZxz(const Eigen::Matrix2cd& matrix) { + const auto [theta, phi, lam, phase] = paramsZyz(matrix); + return {theta, phi + (qc::PI / 2.), lam - (qc::PI / 2.), phase}; +} + +std::array +EulerDecomposition::paramsXyx(const Eigen::Matrix2cd& matrix) { + const Eigen::Matrix2cd matZyz{ + {0.5 * (matrix(0, 0) + matrix(0, 1) + matrix(1, 0) + matrix(1, 1)), + 0.5 * (matrix(0, 0) - matrix(0, 1) + matrix(1, 0) - matrix(1, 1))}, + {0.5 * (matrix(0, 0) + matrix(0, 1) - matrix(1, 0) - matrix(1, 1)), + 0.5 * (matrix(0, 0) - matrix(0, 1) - matrix(1, 0) + matrix(1, 1))}, + }; + auto [theta, phi, lam, phase] = paramsZyz(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.), + }; +} + +std::array +EulerDecomposition::paramsXzx(const Eigen::Matrix2cd& matrix) { + auto det = matrix.determinant(); + auto phase = 0.5 * std::arg(det); + auto sqrtDet = std::sqrt(det); + const Eigen::Matrix2cd matZxz{ + { + {(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] = paramsZxz(matZxz); + return {theta, phi, lam, phase + phase_zxz}; +} + +OneQubitGateSequence EulerDecomposition::decomposeKAK( + double theta, double phi, double lambda, double phase, qc::OpType kGate, + qc::OpType aGate, bool simplify, std::optional atol) { + double angleZeroEpsilon = atol.value_or(DEFAULT_ATOL); + if (!simplify) { + // setting atol to negative value to make all angle checks true; this will + // effectively disable the simplification since all rotations appear to be + // "necessary" + 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/lib/Dialect/QCO/Transforms/Decomposition/GateDecompositionPass.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/GateDecompositionPass.cpp new file mode 100644 index 0000000000..1a1dfb9bd4 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/GateDecompositionPass.cpp @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Dialect/QCO/Transforms/Passes.h" + +#include +#include +#include + +#include + +namespace mlir::qco { + +#define GEN_PASS_DEF_GATEDECOMPOSITIONPASS +#include "mlir/Dialect/QCO/Transforms/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 { + + 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); + + // 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(); + } + } +}; + +} // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/GateDecompositionPattern.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/GateDecompositionPattern.cpp new file mode 100644 index 0000000000..19625401a0 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/GateDecompositionPattern.cpp @@ -0,0 +1,722 @@ +/* + * 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/QCOInterfaces.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Gate.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.h" +#include "mlir/Dialect/QCO/Transforms/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 basisGates 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 basisGates, + llvm::SmallVector eulerBasis, + bool singleQubitOnly, bool forceApplication) + : OpInterfaceRewritePattern(context), + decomposerBasisGates{std::move(basisGates)}, + decomposerEulerBases{std::move(eulerBasis)}, + singleQubitOnly{singleQubitOnly}, forceApplication{forceApplication} { + for (auto&& basisGate : decomposerBasisGates) { + basisDecomposers.push_back(decomposition::TwoQubitBasisDecomposer::create( + basisGate, DEFAULT_FIDELITY)); + } + auto&& [singleQubitGates, twoQubitGates] = getDecompositionGates(); + availableSingleQubitGates = std::move(singleQubitGates); + availableTwoQubitGates = std::move(twoQubitGates); + } + + 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 detection + // 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 = [](UnitaryOpInterface op, bool singleQubitOnly) { + if (singleQubitOnly) { + return TwoQubitSeries::getSingleQubitSeries(op); + } + return TwoQubitSeries::getTwoQubitSeries(op); + }; + auto series = collectSeries(op, singleQubitOnly); + + auto containsForeignGates = !series.containsOnlyGates( + availableSingleQubitGates, availableTwoQubitGates); + + if (series.gates.empty() || (series.gates.size() < 3 && + !(forceApplication && containsForeignGates))) { + // empty or too short and only contains valid gates anyway + return mlir::failure(); + } + + // sequence and cached complexity to avoid repeated recomputations + auto bestSequence = std::make_pair(decomposition::TwoQubitGateSequence{}, + std::numeric_limits::max()); + + 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(); + } + for (auto&& eulerBasis : decomposerEulerBases) { + auto sequence = decomposition::EulerDecomposition::generateCircuit( + eulerBasis, *unitaryMatrix, true, std::nullopt); + + auto newComplexity = sequence.complexity(); + if (newComplexity < bestSequence.second) { + bestSequence = std::make_pair(std::move(sequence), newComplexity); + } + } + } 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); + + for (const auto& decomposer : basisDecomposers) { + auto sequence = decomposer.twoQubitDecompose( + targetDecomposition, decomposerEulerBases, DEFAULT_FIDELITY, false, + std::nullopt); + if (sequence) { + // decomposition successful + auto newComplexity = sequence->complexity(); + if (newComplexity < bestSequence.second) { + // this decomposition is better than any successful decomposition + // before + bestSequence = std::make_pair(*sequence, newComplexity); + } + } + } + } + + if (bestSequence.second == std::numeric_limits::max()) { + // unable to decompose series + return mlir::failure(); + } + if (bestSequence.second >= series.complexity && + !(forceApplication && containsForeignGates)) { + // decomposition is longer/more complex than input series; result will + // always be used (even if more complex) if forceApplication is set and + // the input series contained at least one gate unavailable for the + // decomposition + return mlir::failure(); + } + + applySeries(rewriter, series, bestSequence.first); + + 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; + /** + * Qubit series of MLIR operations involving up to two qubits. + */ + 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], + &UnitaryOpInterface::isSingleQubit)) { + 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], + &UnitaryOpInterface::isSingleQubit)) { + 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], + &UnitaryOpInterface::isTwoQubit)) { + foundGate = result.appendTwoQubitGate(*user); + break; // go back to single-qubit collection + } + } + } + return result; + } + + [[nodiscard]] std::optional + getSingleQubitUnitaryMatrix() { + Eigen::Matrix2cd unitaryMatrix = Eigen::Matrix2cd::Identity(); + 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() { + Eigen::Matrix4cd unitaryMatrix = Eigen::Matrix4cd::Identity(); + Eigen::Matrix4cd 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; + } + } else { + llvm::reportFatalInternalError( + "Gate in series has neither one nor two qubits - decomposition " + "not possible!"); + } + 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) const { + 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. + * If the operation is not valid for a one- or two-qubit series, + * leave in/out qubits uninitialized and the gate list empty. + */ + explicit TwoQubitSeries(UnitaryOpInterface initialOperation) { + if (isBarrier(initialOperation)) { + // not a valid single- or two-qubit series + // (barrier cannot be decomposed) + return; + } + if (initialOperation.isSingleQubit()) { + inQubits = {initialOperation.getInputQubit(0), mlir::Value{}}; + outQubits = {initialOperation.getOutputQubit(0), mlir::Value{}}; + gates.push_back({.op = initialOperation, .qubitIds = {0}}); + } else if (initialOperation.isTwoQubit()) { + inQubits = {initialOperation.getInputQubit(0), + initialOperation.getInputQubit(1)}; + outQubits = {initialOperation.getOutputQubit(0), + initialOperation.getOutputQubit(1)}; + gates.push_back({.op = initialOperation, .qubitIds = {0, 1}}); + } else { + // not a valid single- or two-qubit series (more than two qubits) + return; + } + complexity += helpers::getComplexity(helpers::getQcType(initialOperation), + initialOperation.getNumQubits()); + } + + /** + * Add a single-qubit operation to the series. + * + * @param nextGate Gate to be added, must have exactly one qubit + * + * @return true if series continues, otherwise false + */ + 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()) { + llvm::reportFatalInternalError("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; + } + + /** + * Add a two-qubit operation to the series. + * + * @param nextGate Gate to be added, must have exactly two qubits + * + * @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 possibility 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()) { + // series already has two qubits, thus it is finished because of this + // new qubit + return false; + } + auto&& opInQubits = nextGate.getInputQubits(); + // iterator in the operation input of nextGate to "old" qubit that + // already has previous single-qubit gates in this series + auto it2 = llvm::find(opInQubits, firstQubitIt != outQubits.end() + ? *firstQubitIt + : *secondQubitIt); + // operation is a user of the "old" qubit since it was found this way; + // thus should always succeed + 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, the + // other in qubit 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) { + llvm::SmallVector backtrackedGates; + auto prependSingleQubitGate = [&](UnitaryOpInterface op) { + inQubits[qubitId] = op.getInputQubit(0); + backtrackedGates.push_back({.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 && unitaryOp.isSingleQubit() && !isBarrier(unitaryOp)) { + prependSingleQubitGate(unitaryOp); + } else { + break; + } + } + + gates.insert(gates.begin(), + std::make_move_iterator(backtrackedGates.rbegin()), + std::make_move_iterator(backtrackedGates.rend())); + } + + [[nodiscard]] static bool isBarrier(UnitaryOpInterface op) { + return llvm::isa_and_present(op); + } + + /** + * Get user (should only be one due to dialect's one-use policy) of given + * qubit. If the filter returns false for this user, std::nullopt will be + * returned instead. + */ + template + static std::optional getUser(mlir::Value qubit, + Func&& filter) { + if (qubit) { + auto users = qubit.getUsers(); + auto userIt = users.begin(); + if (!qubit.hasOneUse()) { + llvm::reportFatalUsageError("Qubit has more than one use - unable to " + "collect gate series for decomposition!"); + } + auto user = mlir::dyn_cast(*userIt); + if (user && std::invoke(std::forward(filter), user)) { + return user; + } + } + return std::nullopt; + }; + }; + + /** + * Create controlled version of given gate operation type. + * + * @param rewriter Rewriter instance to apply modifications + * @param location Location for the created operations + * @param ctrlQubits Qubits that serve as controls + * @param inQubitsAndParams Qubits and parameters for inner gate + * (as required by the builder of the gate); + * all qubits must be of type mlir::Value + * and all parameters must have another type + */ + 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(std::forward(inQubitsAndParams)), ...); + return rewriter.create( + location, ctrlQubits, mlir::ValueRange{inQubits}, + [&](auto&& in) -> llvm::SmallVector { + return rewriter.create(location, in)->getResults(); + }); + } + + /** + * Replace given series by given sequence. + * This is done using the rewriter to create the MLIR operations described by + * the sequence between the input and output qubits of the series and then + * deleting all gates of the series. + */ + 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 { + llvm::reportFatalInternalError( + "Invalid number of qubit IDs while trying to apply " + "decomposition result to MLIR!"); + } + }; + + if (sequence.hasGlobalPhase()) { + rewriter.create(location, sequence.globalPhase); + } + +#ifndef NDEBUG + Eigen::Matrix4cd unitaryMatrix = Eigen::Matrix4cd::Identity(); +#endif // NDEBUG + + auto addSingleQubitRotationGate = [&](auto&& gate) { + assert(gate.qubitId.size() == 1); + UnitaryOpInterface newGate; + if (gate.type == qc::RX) { + newGate = rewriter.create(location, inQubits[gate.qubitId[0]], + gate.parameter[0]); + } else if (gate.type == qc::RY) { + newGate = rewriter.create(location, inQubits[gate.qubitId[0]], + gate.parameter[0]); + } else if (gate.type == qc::RZ) { + newGate = rewriter.create(location, inQubits[gate.qubitId[0]], + gate.parameter[0]); + } else { + llvm::reportFatalInternalError( + "Unknown single-qubit rotation gate while applying decomposition!"); + } +#ifndef NDEBUG + unitaryMatrix = decomposition::expandToTwoQubits( + newGate.getUnitaryMatrix().value(), + gate.qubitId[0]) * + unitaryMatrix; +#endif // NDEBUG + return newGate; + }; + + for (auto&& gate : sequence.gates) { + // these if branches should handle all gates in availableSingleQubitGates + // and availableTwoQubitGates; additional gates will need to be added when + // using new euler bases or basis gates + 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]]); +#ifndef NDEBUG + unitaryMatrix = decomposition::fixTwoQubitMatrixQubitOrder( + newGate.getUnitaryMatrix().value(), gate.qubitId) * + unitaryMatrix; +#endif // NDEBUG + updateInQubits(gate.qubitId, newGate); + } else if (gate.type == qc::RX || gate.type == qc::RY || + gate.type == qc::RZ) { + auto newGate = addSingleQubitRotationGate(gate); + updateInQubits(gate.qubitId, newGate); + } else { + llvm::reportFatalInternalError("Unsupported gate type in decomposition " + "while applying result to MLIR!"); + } + } + assert((unitaryMatrix * helpers::globalPhaseFactor(sequence.globalPhase)) + .isApprox( + series.getUnitaryMatrix().value_or(Eigen::Matrix4cd::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); + } + } + + /** + * Get all gates that are potentially in the circuit after the decomposition. + * This is based on the euler bases and basis gates passed to the constructor. + * + * @return Array with the following two elements: + * * All possible single-qubit gate types + * * All possible two-qubit gate types + */ + [[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: + // available basis gates + llvm::SmallVector decomposerBasisGates; + // available euler bases + llvm::SmallVector decomposerEulerBases; + + // cached basis decomposers; one for each basis gate + llvm::SmallVector basisDecomposers; + + // cached result of getDecompositionGates() + llvm::SetVector availableSingleQubitGates; + llvm::SetVector availableTwoQubitGates; + + // configuration of pattern + bool singleQubitOnly; + bool forceApplication; +}; + +/** + * @brief Populates the given pattern set with patterns for gate + * decomposition. + */ +void populateGateDecompositionPatterns(mlir::RewritePatternSet& patterns) { + 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::XZX); + eulerBases.push_back(GateDecompositionPattern::EulerBasis::ZXZ); + patterns.add(patterns.getContext(), basisGates, + eulerBases, false, false); +} + +} // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/GateSequence.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/GateSequence.cpp new file mode 100644 index 0000000000..e5e44ebd13 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/GateSequence.cpp @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h" + +#include "ir/operations/OpType.hpp" +#include "mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Gate.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" + +#include +#include + +#include +#include +#include + +namespace mlir::qco::decomposition { + +bool QubitGateSequence::hasGlobalPhase() const { + return std::abs(globalPhase) > DEFAULT_ATOL; +} + +std::size_t QubitGateSequence::complexity() const { + 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; +} + +Eigen::Matrix4cd QubitGateSequence::getUnitaryMatrix() const { + Eigen::Matrix4cd unitaryMatrix = Eigen::Matrix4cd::Identity(); + for (auto&& gate : gates) { + auto gateMatrix = getTwoQubitMatrix(gate); + unitaryMatrix = gateMatrix * unitaryMatrix; + } + unitaryMatrix *= helpers::globalPhaseFactor(globalPhase); + assert(helpers::isUnitaryMatrix(unitaryMatrix)); + return unitaryMatrix; +} + +} // namespace mlir::qco::decomposition diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp new file mode 100644 index 0000000000..3e6ea1a6a8 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h" + +#include "ir/operations/OpType.hpp" +#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" + +#include + +#include +#include +#include +#include +#include + +namespace mlir::qco::helpers { + +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; + } +} + +double remEuclid(double a, double b) { + auto r = std::fmod(a, b); + return (r < 0.0) ? r + std::abs(b) : r; +} + +double mod2pi(double angle, double angleZeroEpsilon) { + // 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 + std::numbers::pi, 2 * std::numbers::pi) - + std::numbers::pi; + if (std::abs(wrapped - std::numbers::pi) < angleZeroEpsilon) { + return -std::numbers::pi; + } + return wrapped; +} + +double traceToFidelity(const std::complex& x) { + auto xAbs = std::abs(x); + return (4.0 + xAbs * xAbs) / 20.0; +} + +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; +} + +std::complex globalPhaseFactor(double globalPhase) { + return std::exp(std::complex{0, 1} * globalPhase); +} + +} // namespace mlir::qco::helpers diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp new file mode 100644 index 0000000000..65611ce6b0 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp @@ -0,0 +1,216 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" + +#include "ir/operations/OpType.hpp" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Gate.h" + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace mlir::qco::decomposition { + +Eigen::Matrix2cd uMatrix(double lambda, double phi, double theta) { + return Eigen::Matrix2cd{{{{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.)}}}}; +} + +Eigen::Matrix2cd u2Matrix(double lambda, double phi) { + return Eigen::Matrix2cd{ + {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}}}; +} + +Eigen::Matrix2cd rxMatrix(double theta) { + auto halfTheta = theta / 2.; + auto cos = std::complex{std::cos(halfTheta), 0.}; + auto isin = std::complex{0., -std::sin(halfTheta)}; + return Eigen::Matrix2cd{{cos, isin}, {isin, cos}}; +} + +Eigen::Matrix2cd ryMatrix(double theta) { + auto halfTheta = theta / 2.; + std::complex cos{std::cos(halfTheta), 0.}; + std::complex sin{std::sin(halfTheta), 0.}; + return Eigen::Matrix2cd{{cos, -sin}, {sin, cos}}; +} + +Eigen::Matrix2cd rzMatrix(double theta) { + return Eigen::Matrix2cd{{{std::cos(theta / 2.), -std::sin(theta / 2.)}, 0}, + {0, {std::cos(theta / 2.), std::sin(theta / 2.)}}}; +} + +Eigen::Matrix4cd rxxMatrix(double theta) { + const auto cosTheta = std::cos(theta / 2.); + const auto sinTheta = std::sin(theta / 2.); + + return Eigen::Matrix4cd{{cosTheta, 0, 0, {0., -sinTheta}}, + {0, cosTheta, {0., -sinTheta}, 0}, + {0, {0., -sinTheta}, cosTheta, 0}, + {{0., -sinTheta}, 0, 0, cosTheta}}; +} + +Eigen::Matrix4cd ryyMatrix(double theta) { + const auto cosTheta = std::cos(theta / 2.); + const auto sinTheta = std::sin(theta / 2.); + + return Eigen::Matrix4cd{{{cosTheta, 0, 0, {0., sinTheta}}, + {0, cosTheta, {0., -sinTheta}, 0}, + {0, {0., -sinTheta}, cosTheta, 0}, + {{0., sinTheta}, 0, 0, cosTheta}}}; +} + +Eigen::Matrix4cd rzzMatrix(double theta) { + const auto cosTheta = std::cos(theta / 2.); + const auto sinTheta = std::sin(theta / 2.); + + return Eigen::Matrix4cd{{{cosTheta, -sinTheta}, 0, 0, 0}, + {0, {cosTheta, sinTheta}, 0, 0}, + {0, 0, {cosTheta, sinTheta}, 0}, + {0, 0, 0, {cosTheta, -sinTheta}}}; +} + +Eigen::Matrix2cd pMatrix(double lambda) { + return Eigen::Matrix2cd{{1, 0}, {0, {std::cos(lambda), std::sin(lambda)}}}; +} + +Eigen::Matrix4cd expandToTwoQubits(const Eigen::Matrix2cd& singleQubitMatrix, + QubitId qubitId) { + if (qubitId == 0) { + return Eigen::kroneckerProduct(Eigen::Matrix2cd::Identity(), + singleQubitMatrix); + } + if (qubitId == 1) { + return Eigen::kroneckerProduct(singleQubitMatrix, + Eigen::Matrix2cd::Identity()); + } + llvm::reportFatalInternalError("Invalid qubit id for single-qubit expansion"); +} + +Eigen::Matrix4cd +fixTwoQubitMatrixQubitOrder(const Eigen::Matrix4cd& 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; + } + llvm::reportFatalInternalError( + "Invalid qubit IDs for fixing two-qubit matrix"); +} + +Eigen::Matrix2cd getSingleQubitMatrix(const Gate& gate) { + if (gate.type == qc::SX) { + return Eigen::Matrix2cd{ + {std::complex{0.5, 0.5}, std::complex{0.5, -0.5}}, + {std::complex{0.5, -0.5}, std::complex{0.5, 0.5}}}; + } + if (gate.type == qc::RX) { + assert(gate.parameter.size() == 1); + return rxMatrix(gate.parameter[0]); + } + if (gate.type == qc::RY) { + assert(gate.parameter.size() == 1); + return ryMatrix(gate.parameter[0]); + } + if (gate.type == qc::RZ) { + assert(gate.parameter.size() == 1); + return rzMatrix(gate.parameter[0]); + } + if (gate.type == qc::X) { + return Eigen::Matrix2cd{{0, 1}, {1, 0}}; + } + if (gate.type == qc::I) { + return Eigen::Matrix2cd::Identity(); + } + if (gate.type == qc::P) { + assert(gate.parameter.size() == 1); + return pMatrix(gate.parameter[0]); + } + if (gate.type == qc::U) { + assert(gate.parameter.size() == 3); + return uMatrix(gate.parameter[0], gate.parameter[1], gate.parameter[2]); + } + if (gate.type == qc::U2) { + assert(gate.parameter.size() == 2); + return u2Matrix(gate.parameter[0], gate.parameter[1]); + } + if (gate.type == qc::H) { + return H_GATE; + } + llvm::reportFatalInternalError( + llvm::StringRef("unsupported gate type for single qubit matrix (" + + qc::toString(gate.type) + ")")); +} + +// TODO: remove? only used for verification of circuit and in unittests +Eigen::Matrix4cd getTwoQubitMatrix(const Gate& gate) { + if (gate.qubitId.empty()) { + return Eigen::Matrix4cd::Identity(); + } + 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 Eigen::Matrix4cd{ + {1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 1}, {0, 0, 1, 0}}; + } + if (gate.qubitId == llvm::SmallVector{1, 0}) { + return Eigen::Matrix4cd{ + {1, 0, 0, 0}, {0, 0, 0, 1}, {0, 0, 1, 0}, {0, 1, 0, 0}}; + } + llvm::reportFatalInternalError("Invalid qubit IDs for CX gate"); + } + if (gate.type == qc::RXX) { + assert(gate.parameter.size() == 1); + return rxxMatrix(gate.parameter[0]); + } + if (gate.type == qc::RYY) { + assert(gate.parameter.size() == 1); + return ryyMatrix(gate.parameter[0]); + } + if (gate.type == qc::RZZ) { + assert(gate.parameter.size() == 1); + return rzzMatrix(gate.parameter[0]); + } + if (gate.type == qc::I) { + return Eigen::Matrix4cd::Identity(); + } + llvm::reportFatalInternalError( + llvm::StringRef("Unsupported gate type for two qubit matrix (" + + qc::toString(gate.type) + ")")); + } + llvm::reportFatalInternalError( + "Invalid number of qubit IDs in compute_unitary"); +} + +} // namespace mlir::qco::decomposition diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.cpp new file mode 100644 index 0000000000..318f37ce7e --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.cpp @@ -0,0 +1,689 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.h" + +#include "ir/Definitions.hpp" +#include "ir/operations/OpType.hpp" +#include "mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" + +#include // NOLINT(misc-include-cleaner) +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mlir::qco::decomposition { +TwoQubitWeylDecomposition +TwoQubitWeylDecomposition::create(const Eigen::Matrix4cd& unitaryMatrix, + std::optional fidelity) { + auto u = unitaryMatrix; + auto detU = u.determinant(); + auto detPow = std::pow(detU, -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 Eigen::Matrix4cd 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 + auto [p, d] = diagonalizeComplexSymmetric(m2, DIAGONALIZATION_PRECISION); + + // extract Weyl coordinates from eigenvalues, map to [0, 2*pi) + // NOLINTNEXTLINE(misc-include-cleaner) + Eigen::Vector3d cs; + Eigen::Vector4d dReal = -1.0 * d.cwiseArg() / 2.0; + dReal(3) = -dReal(0) - dReal(1) - dReal(2); + for (int i = 0; i < 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 + Eigen::Matrix4cd 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 + Eigen::Matrix4cd temp = dReal.asDiagonal(); + temp *= std::complex{0, 1}; + // since the matrix is diagonal, matrix exponential is equivalent to + // element-wise exponential function + temp.diagonal() = temp.diagonal().array().exp().matrix(); + + // combined matrix k1 of 1q gates after canonical gate + Eigen::Matrix4cd 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 + Eigen::Matrix4cd 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( + Eigen::kroneckerProduct(K1l, K1r).isApprox(k1, SANITY_CHECK_PRECISION)); + assert( + Eigen::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; + decomposition.a_ = a; + decomposition.b_ = b; + decomposition.c_ = c; + decomposition.globalPhase_ = globalPhase; + decomposition.k1l_ = K1l; + decomposition.k2l_ = K2l; + decomposition.k1r_ = K1r; + decomposition.k2r_ = K2r; + decomposition.specialization = Specialization::General; + decomposition.defaultEulerBasis = EulerBasis::ZYZ; + decomposition.requestedFidelity = fidelity; + // will be calculated if a specialization is used; set to -1 for now + decomposition.calculatedFidelity = -1.0; + decomposition.unitaryMatrix = unitaryMatrix; + + // make sure decomposition is equal to input + assert( + (Eigen::kroneckerProduct(K1l, K1r) * decomposition.getCanonicalMatrix() * + Eigen::kroneckerProduct(K2l, K2r) * + helpers::globalPhaseFactor(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 && + decomposition.calculatedFidelity + 1.0e-13 < + *decomposition.requestedFidelity) { + llvm::reportFatalInternalError(llvm::formatv( + "TwoQubitWeylDecomposition: Calculated fidelity of " + "specialization is worse than requested fidelity ({0:F4} vs {1:F4})!", + decomposition.calculatedFidelity, *decomposition.requestedFidelity)); + } + decomposition.globalPhase_ += std::arg(trace); + + // final check if decomposition is still valid after specialization + assert((Eigen::kroneckerProduct(decomposition.k1l_, decomposition.k1r_) * + decomposition.getCanonicalMatrix() * + Eigen::kroneckerProduct(decomposition.k2l_, decomposition.k2r_) * + helpers::globalPhaseFactor(decomposition.globalPhase_)) + .isApprox(unitaryMatrix, SANITY_CHECK_PRECISION)); + + return decomposition; +} + +Eigen::Matrix4cd +TwoQubitWeylDecomposition::getCanonicalMatrix(double a, double b, double c) { + 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; +} + +Eigen::Matrix4cd +TwoQubitWeylDecomposition::magicBasisTransform(const Eigen::Matrix4cd& unitary, + MagicBasisTransform direction) { + using namespace std::complex_literals; + const Eigen::Matrix4cd bNonNormalized{ + {1, 1i, 0, 0}, + {0, 0, 1i, 1}, + {0, 0, 1i, -1}, + {1, -1i, 0, 0}, + }; + + const Eigen::Matrix4cd bNonNormalizedDagger{ + {0.5, 0, 0, 0.5}, + {-0.5i, 0, 0, 0.5i}, + {0, -0.5i, -0.5i, 0}, + {0, 0.5, -0.5, 0}, + }; + if (direction == MagicBasisTransform::OutOf) { + return bNonNormalizedDagger * unitary * bNonNormalized; + } + if (direction == MagicBasisTransform::Into) { + return bNonNormalized * unitary * bNonNormalizedDagger; + } + llvm::reportFatalInternalError("Unknown MagicBasisTransform direction!"); +} + +double TwoQubitWeylDecomposition::closestPartialSwap(double a, double b, + double 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()) + */ +std::pair +TwoQubitWeylDecomposition::diagonalizeComplexSymmetric( + const Eigen::Matrix4cd& m, double 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; + + constexpr auto maxDiagonalizationAttempts = 100; + for (int i = 0; i < maxDiagonalizationAttempts; ++i) { + double randA{}; + double 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 Eigen::Matrix4d m2Real = randA * m.real() + randB * m.imag(); + auto&& pReal = helpers::selfAdjointEvd(m2Real).first; + const Eigen::Matrix4cd p = pReal; + const Eigen::Vector4cd d = (p.transpose() * m * p).diagonal(); + + auto&& compare = p * d.asDiagonal() * 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(Eigen::Matrix4cd{d.asDiagonal()}.determinant() - 1.0) < + SANITY_CHECK_PRECISION); + return std::make_pair(p, d); + } + } + llvm::reportFatalInternalError( + "TwoQubitWeylDecomposition: failed to diagonalize M2 (" + + llvm::Twine(maxDiagonalizationAttempts) + " iterations)."); +} + +std::tuple +TwoQubitWeylDecomposition::decomposeTwoQubitProductGate( + const Eigen::Matrix4cd& specialUnitary) { + // for alternative approaches, see + // pennylane's math.decomposition.su2su2_to_tensor_products + // or quantumflow.kronecker_decomposition + + // first quadrant + Eigen::Matrix2cd 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 = Eigen::Matrix2cd{{specialUnitary(2, 0), specialUnitary(2, 1)}, + {specialUnitary(3, 0), specialUnitary(3, 1)}}; + detR = r.determinant(); + } + if (std::abs(detR) < 0.1) { + llvm::reportFatalInternalError( + "decompose_two_qubit_product_gate: unable to decompose: det_r < 0.1"); + } + r /= std::sqrt(detR); + // transpose with complex conjugate of each element + const Eigen::Matrix2cd rTConj = r.transpose().conjugate(); + + Eigen::Matrix4cd temp = + Eigen::kroneckerProduct(Eigen::Matrix2cd::Identity(), rTConj); + temp = specialUnitary * temp; + + // [[a, b, c, d], + // [e, f, g, h], => [[a, c], + // [i, j, k, l], [i, k]] + // [m, n, o, p]] + Eigen::Matrix2cd l{{temp(0, 0), temp(0, 2)}, {temp(2, 0), temp(2, 2)}}; + auto detL = l.determinant(); + if (std::abs(detL) < 0.9) { + llvm::reportFatalInternalError( + "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}; +} + +std::complex TwoQubitWeylDecomposition::getTrace(double a, double b, + double c, double ap, + double bp, double cp) { + auto da = a - ap; + auto db = b - bp; + auto dc = c - cp; + return 4. * std::complex{std::cos(da) * std::cos(db) * std::cos(dc), + std::sin(da) * std::sin(db) * std::sin(dc)}; +} + +TwoQubitWeylDecomposition::Specialization +TwoQubitWeylDecomposition::bestSpecialization() const { + auto isClose = [this](double ap, double bp, double 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; +} + +bool TwoQubitWeylDecomposition::applySpecialization() { + if (specialization != Specialization::General) { + llvm::reportFatalInternalError( + "Application of specialization only works on " + "general Weyl decompositions!"); + } + 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_ = Eigen::Matrix2cd::Identity(); + k1r_ = k1r_ * k2r_; + k2r_ = Eigen::Matrix2cd::Identity(); + } 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`. + if (c_ > 0.) { + // unmodified global phase + k1l_ = k1l_ * k2r_; + k1r_ = k1r_ * k2l_; + k2l_ = Eigen::Matrix2cd::Identity(); + k2r_ = Eigen::Matrix2cd::Identity(); + } else { + flippedFromOriginal = true; + + globalPhase_ += qc::PI_2; + k1l_ = k1l_ * IPZ * k2r_; + k1r_ = k1r_ * IPZ * k2l_; + k2l_ = Eigen::Matrix2cd::Identity(); + k2r_ = Eigen::Matrix2cd::Identity(); + } + a_ = qc::PI_4; + b_ = qc::PI_4; + c_ = qc::PI_4; + } 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_ = Eigen::Matrix2cd::Identity(); + } 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_ = Eigen::Matrix2cd::Identity(); + } 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) \cdot 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) \cdot 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 { + llvm::reportFatalInternalError( + "Unknown specialization for Weyl decomposition!"); + } + return flippedFromOriginal; +} + +} // namespace mlir::qco::decomposition diff --git a/mlir/unittests/Dialect/QCO/Transforms/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/CMakeLists.txt index 30ddc4dc38..f12b4f821e 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/CMakeLists.txt +++ b/mlir/unittests/Dialect/QCO/Transforms/CMakeLists.txt @@ -6,4 +6,5 @@ # # Licensed under the MIT License +add_subdirectory(Decomposition) add_subdirectory(Mapping) diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt new file mode 100644 index 0000000000..908d7b2046 --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt @@ -0,0 +1,37 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +set(target_name mqt-core-mlir-unittest-decomposition) +file(GLOB_RECURSE DECOMPOSITION_TEST_SOURCES *.cpp) +add_executable(${target_name} ${DECOMPOSITION_TEST_SOURCES}) + +target_link_libraries( + ${target_name} + PRIVATE GTest::gmock + GTest::gtest_main + LLVMFileCheck + MLIRPass + MLIRTransforms + MQTCompilerPipeline + MQT::CoreIR + Eigen3::Eigen + MLIRQCOTransforms) + +mqt_mlir_configure_unittest_target(${target_name}) + +gtest_discover_tests(${target_name} PROPERTIES LABELS mqt-mlir-unittests DISCOVERY_TIMEOUT 60) + +# %%%% 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 MLIRQCOTransforms) # discover tests gtest_discover_tests(${testname} +# DISCOVERY_TIMEOUT 60) set_target_properties(${testname} PROPERTIES FOLDER unittests) endif() diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_basis_decomposer.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_basis_decomposer.cpp new file mode 100644 index 0000000000..ec785eb666 --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_basis_decomposer.cpp @@ -0,0 +1,202 @@ +/* + * 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/Transforms/Decomposition/BasisDecomposer.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Gate.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.h" +#include "utils.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace mlir::qco; +using namespace mlir::qco::decomposition; + +class BasisDecomposerTest + : public testing::TestWithParam, Eigen::Matrix4cd (*)()>> { +public: + void SetUp() override { + basisGate = std::get<0>(GetParam()); + eulerBases = std::get<1>(GetParam()); + target = std::get<2>(GetParam())(); + targetDecomposition = std::make_unique( + TwoQubitWeylDecomposition::create(target, 1.0)); + } + + [[nodiscard]] static Eigen::Matrix4cd + restore(const TwoQubitGateSequence& sequence) { + Eigen::Matrix4cd matrix = Eigen::Matrix4cd::Identity(); + for (auto&& gate : sequence.gates) { + matrix = getTwoQubitMatrix(gate) * matrix; + } + + matrix *= helpers::globalPhaseFactor(sequence.globalPhase); + return matrix; + } + +protected: + Eigen::Matrix4cd target; + Gate basisGate; + llvm::SmallVector eulerBases; + std::unique_ptr 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) { + constexpr auto maxIterations = 2000; + std::mt19937 rng{123456UL}; + + const llvm::SmallVector basisGates{ + {.type = qc::X, .parameter = {}, .qubitId = {0, 1}}, + {.type = qc::X, .parameter = {}, .qubitId = {1, 0}}}; + const llvm::SmallVector eulerBases = { + EulerBasis::XYX, EulerBasis::ZXZ, EulerBasis::ZYZ, EulerBasis::XZX}; + std::uniform_int_distribution distBasisGate{ + 0, basisGates.size() - 1}; + std::uniform_int_distribution distEulerBases{ + 1, eulerBases.size() - 1}; + + auto selectRandomEulerBases = [&]() { + auto tmp = eulerBases; + llvm::shuffle(tmp.begin(), tmp.end(), rng); + tmp.resize(distEulerBases(rng)); + return tmp; + }; + auto selectRandomBasisGate = [&]() { return basisGates[distBasisGate(rng)]; }; + + for (int i = 0; i < maxIterations; ++i) { + auto originalMatrix = randomUnitaryMatrix(rng); + + auto targetDecomposition = + TwoQubitWeylDecomposition::create(originalMatrix, 1.0); + auto decomposer = + TwoQubitBasisDecomposer::create(selectRandomBasisGate(), 1.0); + auto decomposedSequence = decomposer.twoQubitDecompose( + targetDecomposition, selectRandomEulerBases(), 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'; + } +} + +INSTANTIATE_TEST_SUITE_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( + []() -> Eigen::Matrix4cd { return Eigen::Matrix4cd::Identity(); }, + []() -> Eigen::Matrix4cd { + return Eigen::kroneckerProduct(rzMatrix(1.0), ryMatrix(3.1)); + }, + []() -> Eigen::Matrix4cd { + return Eigen::kroneckerProduct(Eigen::Matrix2cd::Identity(), + rxMatrix(0.1)); + }))); + +INSTANTIATE_TEST_SUITE_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, EulerBasis::XYX}), + // targets to be decomposed + ::testing::Values( + []() -> Eigen::Matrix4cd { return rzzMatrix(2.0); }, + []() -> Eigen::Matrix4cd { + return ryyMatrix(1.0) * rzzMatrix(3.0) * rxxMatrix(2.0); + }, + []() -> Eigen::Matrix4cd { + return TwoQubitWeylDecomposition::getCanonicalMatrix(1.5, -0.2, + 0.0) * + Eigen::kroneckerProduct(rxMatrix(1.0), + Eigen::Matrix2cd::Identity()); + }, + []() -> Eigen::Matrix4cd { + return Eigen::kroneckerProduct(rxMatrix(1.0), ryMatrix(1.0)) * + TwoQubitWeylDecomposition::getCanonicalMatrix(1.1, 0.2, + 3.0) * + Eigen::kroneckerProduct(rxMatrix(1.0), + Eigen::Matrix2cd::Identity()); + }, + []() -> Eigen::Matrix4cd { + return Eigen::kroneckerProduct(H_GATE, IPZ) * + getTwoQubitMatrix( + {.type = qc::X, .parameter = {}, .qubitId = {0, 1}}) * + Eigen::kroneckerProduct(IPX, IPY); + }))); diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp new file mode 100644 index 0000000000..9bd9eedfbc --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" +#include "utils.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace mlir::qco; +using namespace mlir::qco::decomposition; + +class EulerDecompositionTest + : public testing::TestWithParam< + std::tuple> { +public: + [[nodiscard]] static Eigen::Matrix2cd + restore(const OneQubitGateSequence& sequence) { + Eigen::Matrix2cd matrix = Eigen::Matrix2cd::Identity(); + for (auto&& gate : sequence.gates) { + matrix = getSingleQubitMatrix(gate) * matrix; + } + + matrix *= helpers::globalPhaseFactor(sequence.globalPhase); + return matrix; + } + + void SetUp() override { + eulerBasis = std::get<0>(GetParam()); + originalMatrix = std::get<1>(GetParam())(); + } + +protected: + Eigen::Matrix2cd 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) { + constexpr auto maxIterations = 10000; + std::mt19937 rng{12345678UL}; + + auto eulerBases = std::array{EulerBasis::XYX, EulerBasis::XZX, + EulerBasis::ZYZ, EulerBasis::ZXZ}; + std::size_t currentEulerBase = 0; + for (int i = 0; i < maxIterations; ++i) { + auto originalMatrix = randomUnitaryMatrix(rng); + 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'; + } +} + +INSTANTIATE_TEST_CASE_P( + SingleQubitMatrices, EulerDecompositionTest, + testing::Combine(testing::Values(EulerBasis::XYX, EulerBasis::XZX, + EulerBasis::ZYZ, EulerBasis::ZXZ), + testing::Values( + []() -> Eigen::Matrix2cd { + return Eigen::Matrix2cd::Identity(); + }, + []() -> Eigen::Matrix2cd { return ryMatrix(2.0); }, + []() -> Eigen::Matrix2cd { return rxMatrix(0.5); }, + []() -> Eigen::Matrix2cd { return rzMatrix(3.14); }, + []() -> Eigen::Matrix2cd { return H_GATE; }))); diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp new file mode 100644 index 0000000000..a3d77f19ba --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -0,0 +1,170 @@ +/* + * 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/Transforms/Decomposition/Helpers.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.h" +#include "utils.h" + +#include +#include + +#include +#include +#include +#include + +using namespace mlir::qco; +using namespace mlir::qco::decomposition; + +class WeylDecompositionTest + : public testing::TestWithParam { +public: + [[nodiscard]] static Eigen::Matrix4cd + restore(const TwoQubitWeylDecomposition& decomposition) { + return k1(decomposition) * can(decomposition) * k2(decomposition) * + globalPhaseFactor(decomposition); + } + + [[nodiscard]] static std::complex + globalPhaseFactor(const TwoQubitWeylDecomposition& decomposition) { + return helpers::globalPhaseFactor(decomposition.globalPhase()); + } + [[nodiscard]] static Eigen::Matrix4cd + can(const TwoQubitWeylDecomposition& decomposition) { + return decomposition.getCanonicalMatrix(); + } + [[nodiscard]] static Eigen::Matrix4cd + k1(const TwoQubitWeylDecomposition& decomposition) { + return Eigen::kroneckerProduct(decomposition.k1l(), decomposition.k1r()); + } + [[nodiscard]] static Eigen::Matrix4cd + k2(const TwoQubitWeylDecomposition& decomposition) { + return Eigen::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) { + constexpr auto maxIterations = 5000; + std::mt19937 rng{1234567UL}; + + for (int i = 0; i < maxIterations; ++i) { + auto originalMatrix = randomUnitaryMatrix(rng); + 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'; + } +} + +INSTANTIATE_TEST_SUITE_P( + SingleQubitMatrices, WeylDecompositionTest, + ::testing::Values( + []() -> Eigen::Matrix4cd { return Eigen::Matrix4cd::Identity(); }, + []() -> Eigen::Matrix4cd { + return Eigen::kroneckerProduct(rzMatrix(1.0), ryMatrix(3.1)); + }, + []() -> Eigen::Matrix4cd { + return Eigen::kroneckerProduct(Eigen::Matrix2cd::Identity(), + rxMatrix(0.1)); + })); + +INSTANTIATE_TEST_SUITE_P( + TwoQubitMatrices, WeylDecompositionTest, + ::testing::Values( + []() -> Eigen::Matrix4cd { return rzzMatrix(2.0); }, + []() -> Eigen::Matrix4cd { + return ryyMatrix(1.0) * rzzMatrix(3.0) * rxxMatrix(2.0); + }, + []() -> Eigen::Matrix4cd { + return TwoQubitWeylDecomposition::getCanonicalMatrix(1.5, -0.2, 0.0) * + Eigen::kroneckerProduct(rxMatrix(1.0), + Eigen::Matrix2cd::Identity()); + }, + []() -> Eigen::Matrix4cd { + return Eigen::kroneckerProduct(rxMatrix(1.0), ryMatrix(1.0)) * + TwoQubitWeylDecomposition::getCanonicalMatrix(1.1, 0.2, 3.0) * + Eigen::kroneckerProduct(rxMatrix(1.0), + Eigen::Matrix2cd::Identity()); + }, + []() -> Eigen::Matrix4cd { + return Eigen::kroneckerProduct(H_GATE, IPZ) * + getTwoQubitMatrix( + {.type = qc::X, .parameter = {}, .qubitId = {0, 1}}) * + Eigen::kroneckerProduct(IPX, IPY); + })); + +INSTANTIATE_TEST_SUITE_P( + SpecializedMatrices, WeylDecompositionTest, + ::testing::Values( + // id + controlled + general already covered by other parametrizations + // swap equiv + []() -> Eigen::Matrix4cd { + return 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 + []() -> Eigen::Matrix4cd { + return TwoQubitWeylDecomposition::getCanonicalMatrix(0.5, 0.5, 0.5); + }, + // partial swap equiv (flipped) + []() -> Eigen::Matrix4cd { + return TwoQubitWeylDecomposition::getCanonicalMatrix(0.5, 0.5, -0.5); + }, + // mirror controlled equiv + []() -> Eigen::Matrix4cd { + return getTwoQubitMatrix( + {.type = qc::X, .parameter = {}, .qubitId = {0, 1}}) * + getTwoQubitMatrix( + {.type = qc::X, .parameter = {}, .qubitId = {1, 0}}); + }, + // sim aab equiv + []() -> Eigen::Matrix4cd { + return TwoQubitWeylDecomposition::getCanonicalMatrix(0.5, 0.5, 0.1); + }, + // sim abb equiv + []() -> Eigen::Matrix4cd { + return TwoQubitWeylDecomposition::getCanonicalMatrix(0.5, 0.1, 0.1); + }, + // sim ab-b equiv + []() -> Eigen::Matrix4cd { + return TwoQubitWeylDecomposition::getCanonicalMatrix(0.5, 0.1, -0.1); + })); diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/utils.h b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/utils.h new file mode 100644 index 0000000000..8d8ac738ac --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/utils.h @@ -0,0 +1,34 @@ +/* + * 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/Transforms/Decomposition/Helpers.h" + +#include + +#include +#include +#include + +template +[[nodiscard]] MatrixType randomUnitaryMatrix(std::mt19937& rng) { + // NOLINTNEXTLINE(misc-const-correctness) + std::uniform_real_distribution dist(-1.0, 1.0); + MatrixType randomMatrix; + for (auto& x : randomMatrix.reshaped()) { + x = std::complex(dist(rng), dist(rng)); + } + Eigen::HouseholderQR qr{}; // NOLINT(misc-include-cleaner) + qr.compute(randomMatrix); + const MatrixType unitaryMatrix = qr.householderQ(); + assert(mlir::qco::helpers::isUnitaryMatrix(unitaryMatrix)); + return unitaryMatrix; +}