From 38b54987eb041e253c178b43f95f6937836bef7f Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 22 Apr 2026 13:40:25 +0200 Subject: [PATCH 001/122] =?UTF-8?q?=E2=9C=A8=20Refactor=20unitary=20matrix?= =?UTF-8?q?=20calculations=20in=20quantum=20gate=20operations=20to=20use?= =?UTF-8?q?=20`std::exp`=20for=20complex=20exponentiation=20instead=20of?= =?UTF-8?q?=20`std::polar`.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/IR/Operations/StandardGates/GPhaseOp.cpp | 4 +++- .../Dialect/QCO/IR/Operations/StandardGates/POp.cpp | 4 +++- .../Dialect/QCO/IR/Operations/StandardGates/ROp.cpp | 12 +++++++----- .../Dialect/QCO/IR/Operations/StandardGates/RZOp.cpp | 4 ++-- .../QCO/IR/Operations/StandardGates/RZZOp.cpp | 4 ++-- .../Dialect/QCO/IR/Operations/StandardGates/TOp.cpp | 4 +++- .../QCO/IR/Operations/StandardGates/TdgOp.cpp | 4 +++- .../Dialect/QCO/IR/Operations/StandardGates/U2Op.cpp | 10 +++++----- .../Dialect/QCO/IR/Operations/StandardGates/UOp.cpp | 6 +++--- .../QCO/IR/Operations/StandardGates/XXMinusYYOp.cpp | 4 ++-- .../QCO/IR/Operations/StandardGates/XXPlusYYOp.cpp | 4 ++-- 11 files changed, 35 insertions(+), 25 deletions(-) diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/GPhaseOp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/GPhaseOp.cpp index aeb8a5c4b9..b9b6d90434 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/GPhaseOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/GPhaseOp.cpp @@ -63,8 +63,10 @@ void GPhaseOp::getCanonicalizationPatterns(RewritePatternSet& results, std::optional, 1, 1>> GPhaseOp::getUnitaryMatrix() { + using namespace std::complex_literals; + if (const auto theta = valueToDouble(getTheta())) { - return Eigen::Matrix, 1, 1>{std::polar(1.0, *theta)}; + return Eigen::Matrix, 1, 1>{std::exp(1i * *theta)}; } return std::nullopt; } diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/POp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/POp.cpp index 08ee6e068e..0060a2e727 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/POp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/POp.cpp @@ -66,8 +66,10 @@ void POp::getCanonicalizationPatterns(RewritePatternSet& results, } std::optional POp::getUnitaryMatrix() { + using namespace std::complex_literals; + if (const auto theta = valueToDouble(getTheta())) { - return Eigen::Matrix2cd{{1.0, 0.0}, {0.0, std::polar(1.0, *theta)}}; + return Eigen::Matrix2cd{{1.0, 0.0}, {0.0, std::exp(1i * *theta)}}; } return std::nullopt; } diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/ROp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/ROp.cpp index 5e7b04b268..42bf3c98b5 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/ROp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/ROp.cpp @@ -81,15 +81,17 @@ void ROp::getCanonicalizationPatterns(RewritePatternSet& results, } std::optional ROp::getUnitaryMatrix() { + using namespace std::complex_literals; + const auto theta = valueToDouble(getTheta()); const auto phi = valueToDouble(getPhi()); if (!theta || !phi) { return std::nullopt; } - const auto thetaSin = std::sin(*theta / 2.0); - const auto m01 = std::polar(thetaSin, -*phi - (std::numbers::pi / 2)); - const auto m10 = std::polar(thetaSin, *phi - (std::numbers::pi / 2)); - const std::complex thetaCos = std::cos(*theta / 2.0); - return Eigen::Matrix2cd{{thetaCos, m01}, {m10, thetaCos}}; + const auto s = std::sin(*theta / 2.0); + const auto c = std::cos(*theta / 2.0) + 0i; + const auto m01 = s * std::exp(1i * (-*phi - (std::numbers::pi / 2.0))); + const auto m10 = s * std::exp(1i * (*phi - (std::numbers::pi / 2.0))); + return Eigen::Matrix2cd{{c, m01}, {m10, c}}; } diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/RZOp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/RZOp.cpp index cad399846c..30a0377efd 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/RZOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/RZOp.cpp @@ -69,9 +69,9 @@ std::optional RZOp::getUnitaryMatrix() { using namespace std::complex_literals; if (const auto theta = valueToDouble(getTheta())) { - const auto m00 = std::polar(1.0, -*theta / 2.0); + const auto m00 = std::exp(1i * (-*theta / 2.0)); const auto m01 = 0i; - const auto m11 = std::polar(1.0, *theta / 2.0); + const auto m11 = std::exp(1i * (*theta / 2.0)); return Eigen::Matrix2cd{{m00, m01}, {m01, m11}}; } return std::nullopt; diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/RZZOp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/RZZOp.cpp index 5fb05c3379..2b7f7c2ae5 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/RZZOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/RZZOp.cpp @@ -88,8 +88,8 @@ std::optional RZZOp::getUnitaryMatrix() { if (const auto theta = valueToDouble(getTheta())) { const auto m0 = 0i; - const auto mp = std::polar(1.0, *theta / 2.0); - const auto mm = std::polar(1.0, -*theta / 2.0); + const auto mp = std::exp(1i * (*theta / 2.0)); + const auto mm = std::exp(1i * (-*theta / 2.0)); return Eigen::Matrix4cd{{mm, m0, m0, m0}, // row 0 {m0, mp, m0, m0}, // row 1 {m0, m0, mp, m0}, // row 2 diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/TOp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/TOp.cpp index 14afd9814a..e36322dc12 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/TOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/TOp.cpp @@ -57,6 +57,8 @@ void TOp::getCanonicalizationPatterns(RewritePatternSet& results, } Eigen::Matrix2cd TOp::getUnitaryMatrix() { - const auto m11 = std::polar(1.0, std::numbers::pi / 4.0); + using namespace std::complex_literals; + + const auto m11 = std::exp(1i * (std::numbers::pi / 4.0)); return Eigen::Matrix2cd{{1.0, 0.0}, {0.0, m11}}; } diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/TdgOp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/TdgOp.cpp index 21a2a07b23..a8eb77b629 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/TdgOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/TdgOp.cpp @@ -58,6 +58,8 @@ void TdgOp::getCanonicalizationPatterns(RewritePatternSet& results, } Eigen::Matrix2cd TdgOp::getUnitaryMatrix() { - const auto m11 = std::polar(1.0, -std::numbers::pi / 4.0); + using namespace std::complex_literals; + + const auto m11 = std::exp(1i * (-std::numbers::pi / 4.0)); return Eigen::Matrix2cd{{1.0, 0.0}, {0.0, m11}}; } diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/U2Op.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/U2Op.cpp index 63158dfdb8..600ab83f50 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/U2Op.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/U2Op.cpp @@ -114,10 +114,10 @@ std::optional U2Op::getUnitaryMatrix() { return std::nullopt; } - const auto m00 = 1.0 / std::numbers::sqrt2 + 0i; - const auto m01 = - std::polar(1.0 / std::numbers::sqrt2, *lambda + std::numbers::pi); - const auto m10 = std::polar(1.0 / std::numbers::sqrt2, *phi); - const auto m11 = std::polar(1.0 / std::numbers::sqrt2, *phi + *lambda); + const auto invSqrt2 = 1.0 / std::numbers::sqrt2; + const auto m00 = invSqrt2 + 0i; + const auto m01 = invSqrt2 * std::exp(1i * (*lambda + std::numbers::pi)); + const auto m10 = invSqrt2 * std::exp(1i * (*phi)); + const auto m11 = invSqrt2 * std::exp(1i * (*phi + *lambda)); return Eigen::Matrix2cd{{m00, m01}, {m10, m11}}; } diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/UOp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/UOp.cpp index 3a5e34dddd..e829ea4f0b 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/UOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/UOp.cpp @@ -138,8 +138,8 @@ std::optional UOp::getUnitaryMatrix() { const auto c = std::cos(*theta / 2.0); const auto s = std::sin(*theta / 2.0); const auto m00 = c + 0i; - const auto m01 = std::polar(s, *lambda + std::numbers::pi); - const auto m10 = std::polar(s, *phi); - const auto m11 = std::polar(c, *phi + *lambda); + const auto m01 = s * std::exp(1i * (*lambda + std::numbers::pi)); + const auto m10 = s * std::exp(1i * (*phi)); + const auto m11 = c * std::exp(1i * (*phi + *lambda)); return Eigen::Matrix2cd{{m00, m01}, {m10, m11}}; } diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXMinusYYOp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXMinusYYOp.cpp index 79dc169ae1..2923e39db0 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXMinusYYOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXMinusYYOp.cpp @@ -118,8 +118,8 @@ std::optional XXMinusYYOp::getUnitaryMatrix() { const auto m1 = 1.0 + 0i; const auto mc = std::cos(*theta / 2.0) + 0i; const auto s = std::sin(*theta / 2.0); - const auto msp = std::polar(s, *beta - (std::numbers::pi / 2.)); - const auto msm = std::polar(s, -*beta - (std::numbers::pi / 2.)); + const auto msp = s * std::exp(1i * (*beta - (std::numbers::pi / 2.0))); + const auto msm = s * std::exp(1i * (-*beta - (std::numbers::pi / 2.0))); return Eigen::Matrix4cd{{mc, m0, m0, msm}, // row 0 {m0, m1, m0, m0}, // row 1 {m0, m0, m1, m0}, // row 2 diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXPlusYYOp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXPlusYYOp.cpp index 03d8853371..aad0076727 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXPlusYYOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXPlusYYOp.cpp @@ -117,8 +117,8 @@ std::optional XXPlusYYOp::getUnitaryMatrix() { const auto m1 = 1.0 + 0i; const auto mc = std::cos(*theta / 2.0) + 0i; const auto s = std::sin(*theta / 2.0); - const auto msp = std::polar(s, *beta - (std::numbers::pi / 2)); - const auto msm = std::polar(s, -*beta - (std::numbers::pi / 2)); + const auto msp = s * std::exp(1i * (*beta - (std::numbers::pi / 2.0))); + const auto msm = s * std::exp(1i * (-*beta - (std::numbers::pi / 2.0))); return Eigen::Matrix4cd{{m1, m0, m0, m0}, // row 0 {m0, mc, msp, m0}, // row 1 {m0, msm, mc, m0}, // row 2 From db7276069e4c1788bf69a12f4d821d6a0eafa889 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 22 Apr 2026 14:37:06 +0200 Subject: [PATCH 002/122] =?UTF-8?q?=E2=9C=A8=20Implement=20quantum=20gate?= =?UTF-8?q?=20decomposition=20utilities,=20including=20two-qubit=20basis?= =?UTF-8?q?=20decomposer,=20Euler=20decomposition,=20and=20associated=20he?= =?UTF-8?q?lper=20functions.=20This=20update=20introduces=20new=20headers?= =?UTF-8?q?=20and=20source=20files=20for=20managing=20gate=20sequences,=20?= =?UTF-8?q?unitary=20matrices,=20and=20decomposition=20strategies,=20enhan?= =?UTF-8?q?cing=20the=20framework's=20capabilities=20for=20quantum=20circu?= =?UTF-8?q?it=20transformations.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tamino Bauknecht --- .../Decomposition/BasisDecomposer.h | 247 +++++++ .../QCO/Transforms/Decomposition/EulerBasis.h | 55 ++ .../Decomposition/EulerDecomposition.h | 147 ++++ .../QCO/Transforms/Decomposition/Gate.h | 41 ++ .../QCO/Transforms/Decomposition/GateKind.h | 44 ++ .../Transforms/Decomposition/GateSequence.h | 63 ++ .../QCO/Transforms/Decomposition/Helpers.h | 86 +++ .../Decomposition/UnitaryMatrices.h | 68 ++ .../Decomposition/WeylDecomposition.h | 250 +++++++ .../Decomposition/BasisDecomposer.cpp | 422 +++++++++++ .../Transforms/Decomposition/EulerBasis.cpp | 50 ++ .../Decomposition/EulerDecomposition.cpp | 310 ++++++++ .../Transforms/Decomposition/GateSequence.cpp | 54 ++ .../QCO/Transforms/Decomposition/Helpers.cpp | 124 ++++ .../Decomposition/UnitaryMatrices.cpp | 215 ++++++ .../Decomposition/WeylDecomposition.cpp | 682 ++++++++++++++++++ .../Transforms/Decomposition/CMakeLists.txt | 17 + .../Decomposition/decomposition_test_utils.h | 33 + .../Decomposition/test_basis_decomposer.cpp | 199 +++++ .../test_euler_decomposition.cpp | 97 +++ .../Decomposition/test_weyl_decomposition.cpp | 173 +++++ 21 files changed, 3377 insertions(+) create mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.h create mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h create mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.h create mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Gate.h create mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h create mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h create mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h create mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h create mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.h create mode 100644 mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp create mode 100644 mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerBasis.cpp create mode 100644 mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.cpp create mode 100644 mlir/lib/Dialect/QCO/Transforms/Decomposition/GateSequence.cpp create mode 100644 mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp create mode 100644 mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp create mode 100644 mlir/lib/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.cpp create mode 100644 mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt create mode 100644 mlir/unittests/Dialect/QCO/Transforms/Decomposition/decomposition_test_utils.h create mode 100644 mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_basis_decomposer.cpp create mode 100644 mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp create mode 100644 mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp 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..b7ce1ac4e6 --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.h @@ -0,0 +1,247 @@ +/* + * 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 + +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 + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Any modifications or derivative works of this code must retain this + * copyright notice, and modified files need to carry a notice + * indicating that they have been altered from the originals. + */ +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 decomposition. + * 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*/, + // Reserved for future error-aware synthesis (per-qubit + // op→error maps feeding 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..1834802039 --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h @@ -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 + */ + +#pragma once + +#include "GateKind.h" + +#include + +#include + +namespace mlir::qco::decomposition { +/** + * Default absolute tolerance used to treat small Euler angles as zero during + * simplification. + */ +inline constexpr auto DEFAULT_ATOL = 1e-12; + +/** + * Supported single-qubit Euler-style output bases. + * + * The listed values describe the gate alphabet that `EulerDecomposition` + * targets when converting a 2x2 unitary into a `OneQubitGateSequence`. + * Several entries share the angle-extraction routine and only differ in how + * the final circuit is emitted (e.g. `U3` vs `U321`, or `ZSX` vs `ZSXX`). + */ +enum class EulerBasis : std::uint8_t { + U3 = 0, ///< Single `u(theta, phi, lambda)` gate. + U321 = 1, ///< `u1`/`u2`/`u3` family — picks the smallest form per angles. + U = 2, ///< Same ZYZ angle extraction as `U3`, emitted as a single `u`. + ZYZ = 3, ///< `rz · ry · rz`. + ZXZ = 4, ///< `rz · rx · rz`. + XZX = 5, ///< `rx · rz · rx`. + XYX = 6, ///< `rx · ry · rx`. + ZSXX = 7, ///< `rz · sx` chain, with `sx · rz(±π) · sx` collapsed to `x`. + ZSX = 8, ///< Like `ZSXX` but without the `x` shortcut. +}; + +/** + * Return the gate types that may appear in a circuit emitted for `eulerBasis`. + * + * The result describes the basis alphabet, not the exact gate count. Some + * decompositions emit fewer than three gates after simplification. + */ +[[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..0a34812af3 --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.h @@ -0,0 +1,147 @@ +/* + * 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 + +#include +#include + +namespace mlir::qco::decomposition { + +/** + * Decompose a single-qubit unitary into a selected Euler-style gate basis. + * + * The returned sequence tracks both the emitted gates and the scalar phase + * needed to reconstruct the input matrix exactly. This is stronger than the + * usual "up to global phase" contract and is relied on by downstream + * canonicalization and testing code. + */ +class EulerDecomposition { +public: + /** + * Decompose a 2x2 unitary into the gate alphabet described by + * `targetBasis`. + * + * When `simplify` is true, near-zero angles are removed using `atol` (or + * `DEFAULT_ATOL` if no override is provided). The returned global phase keeps + * the decomposition exactly equal to `unitaryMatrix`. + */ + [[nodiscard]] static OneQubitGateSequence + generateCircuit(EulerBasis targetBasis, const Eigen::Matrix2cd& unitaryMatrix, + bool simplify, std::optional atol); + + /** + * Extract canonical Euler parameters for `matrix` in the requested basis. + * + * Some target bases reuse the same parameter extraction routine and differ + * only during circuit emission. The returned array always contains + * `(theta, phi, lambda, phase)` in this order. + */ + [[nodiscard]] static std::array + anglesFromUnitary(const Eigen::Matrix2cd& matrix, EulerBasis basis); + +private: + /// Extract parameters for a `RZ(phi) RY(theta) RZ(lambda)` factorization. + [[nodiscard]] static std::array + paramsZyz(const Eigen::Matrix2cd& matrix); + + /// Extract parameters for a `RZ(phi) RX(theta) RZ(lambda)` factorization. + [[nodiscard]] static std::array + paramsZxz(const Eigen::Matrix2cd& matrix); + + /// Extract parameters for a `RX(phi) RY(theta) RX(lambda)` factorization. + [[nodiscard]] static std::array + paramsXyx(const Eigen::Matrix2cd& matrix); + + /// Extract parameters for a `RX(phi) RZ(theta) RX(lambda)` factorization. + [[nodiscard]] static std::array + paramsXzx(const Eigen::Matrix2cd& matrix); + + /** + * Extract parameters for a `u1`/`p` + `sx` factorization. + * + * The returned angles are identical to `paramsZyz` but the phase is shifted + * by `-0.5 * (theta + phi + lambda)` so that the `rz`/`sx` circuits emitted + * by `decomposePsxGen` match the input matrix exactly (not only up to a + * global phase). + * + * @note Adapted from `params_u1x_inner` 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 + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Any modifications or derivative works of this code must retain this + * copyright notice, and modified files need to carry a notice + * indicating that they have been altered from the originals. + */ + [[nodiscard]] static std::array + paramsU1x(const Eigen::Matrix2cd& matrix); + + /** + * Emit a K-A-K circuit from already extracted Euler parameters. + * + * `kGate` is used for the outer rotations and `aGate` for the middle + * rotation. + * + * @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 + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Any modifications or derivative works of this code must retain this + * copyright notice, and modified files need to carry a notice + * indicating that they have been altered from the originals. + */ + [[nodiscard]] static OneQubitGateSequence + decomposeKAK(double theta, double phi, double lambda, double phase, + GateKind kGate, GateKind aGate, bool simplify, + std::optional atol); + + /** + * Emit an `rz`/`sx`-style circuit for the `ZSX` and `ZSXX` bases. + * + * The emitted sequence is structurally identical to the one produced by + * Qiskit's `circuit_psx_gen`. When `simplify` is enabled the number of `sx` + * gates shrinks based on `theta`: zero `sx` gates for `theta ~= 0`, one + * `sx` gate for `theta ~= pi/2`, and two `sx` gates otherwise. + * + * When `allowXShortcut` is true (i.e. for `ZSXX`), the general-case 2-`sx` + * path additionally collapses `sx . rz(+/- pi) . sx` into a single `x` + * gate when the middle rotation is congruent to +/- pi modulo 2 pi. + * + * @note Adapted from `circuit_psx_gen` 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 + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Any modifications or derivative works of this code must retain + * this copyright notice, and modified files need to carry a notice + * indicating that they have been altered from the originals. + */ + [[nodiscard]] static OneQubitGateSequence + decomposePsxGen(double theta, double phi, double lambda, double phase, + bool allowXShortcut, 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..429f10482f --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Gate.h @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "GateKind.h" + +#include + +#include + +namespace mlir::qco::decomposition { + +using QubitId = std::size_t; + +/** + * Lightweight decomposition-time gate record. + * + * This struct is intentionally independent from MLIR operations so helper code + * can build and manipulate abstract one- and two-qubit circuits before they + * are materialized back into the IR. + */ +struct Gate { + /// Operation kind represented by this gate. + GateKind type{GateKind::I}; + + /// Gate parameters in operation-specific order. + llvm::SmallVector parameter; + + /// Logical qubit ids used by the gate, in operand order. + llvm::SmallVector qubitId = {0}; +}; + +} // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h new file mode 100644 index 0000000000..3ba8b148cb --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h @@ -0,0 +1,44 @@ +/* + * 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 + +namespace mlir::qco::decomposition { + +/** + * Lightweight gate identifiers used by decomposition utilities. + * + * These kinds intentionally stay independent from the core IR `qc::OpType` + * enum so the MLIR/QCO decomposition layer does not depend on the `ir` + * package. + */ +enum class GateKind : std::uint8_t { + I = 0, + H, + P, + U, + U2, + X, + Y, + Z, + SX, + RX, + RY, + RZ, + R, + RXX, + RYY, + RZZ, + GPhase, +}; + +} // 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..9109b3e4fa --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h @@ -0,0 +1,63 @@ +/* + * 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 { + +/** + * Sequence of abstract decomposition gates plus a residual global phase. + * + * `gates` is stored in execution order: for a column state vector, the first + * gate in the vector is applied first. The reconstructed 4x4 unitary + * is therefore `U = e^{i * phi} * M_{n-1} * ... * M_0`, where `M_i` is the + * two-qubit matrix for `gates[i]` and `phi` is `globalPhase` in radians (via + * `helpers::globalPhaseFactor`). + */ +struct QubitGateSequence { + /// Expected short decomposition length; `SmallVector` inline storage size. + static constexpr unsigned GATES_INLINE_CAPACITY = 8; + + /// Gates in execution order (see struct comment). + llvm::SmallVector gates; + + /// Residual global phase in radians, not represented by explicit gates. + double globalPhase{}; + + /// True when `std::abs(globalPhase)` exceeds `DEFAULT_ATOL` in + /// `EulerBasis.h`. + [[nodiscard]] bool hasGlobalPhase() const; + + /// Heuristic complexity from `helpers::getComplexity()` for each gate, plus a + /// synthetic global-phase term when `hasGlobalPhase()` is true. + [[nodiscard]] std::size_t complexity() const; + + /** + * Reconstruct the overall two-qubit unitary represented by the sequence. + * + * Single-qubit gates are expanded to the two-qubit workspace convention used + * throughout the decomposition utilities. + */ + [[nodiscard]] Eigen::Matrix4cd getUnitaryMatrix() const; +}; + +/// Documents intent only; same type as `QubitGateSequence`. +using OneQubitGateSequence = QubitGateSequence; +/// Documents intent only; same type as `QubitGateSequence`. +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..e9ada91d49 --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" + +#include +#include + +#include +#include + +/// Numeric + classification helpers used by the decomposition passes. +/// Lives in `mlir::qco::helpers` (not `decomposition`) because some helpers +/// map IR ops back to decomposition kinds. + +namespace mlir::qco::helpers { + +/** + * Map a QCO unitary operation to the corresponding decomposition `GateKind`. + * + * For controlled operations, this returns the wrapped body operation type + * rather than the outer `ctrl` marker. + */ +[[nodiscard]] decomposition::GateKind getGateKind(UnitaryOpInterface op); + +// NOLINTBEGIN(misc-include-cleaner) +/// Eigen-decomposition of a self-adjoint matrix. Returns `(eigenvectors, +/// eigenvalues)`; eigenvalues are real and sorted ascending. +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); +} + +/// Check whether `matrix` is unitary within `tolerance` (i.e. `M^H M` is +/// approximately `I`, using Eigen's `isIdentity`). +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 [-pi, pi). If within atol of the endpoint, clamp to + * -pi. + */ +[[nodiscard]] double mod2pi(double angle, double angleZeroEpsilon = 1e-13); + +/** + * Convert a two-qubit trace overlap into the average gate fidelity metric used + * by the decomposition cost code. + */ +[[nodiscard]] double traceToFidelity(const std::complex& x); + +/** + * Return the heuristic cost assigned to a gate acting on `numOfQubits`. + */ +[[nodiscard]] std::size_t getComplexity(decomposition::GateKind type, + std::size_t numOfQubits); + +/** + * Return the scalar `e^(i * globalPhase)` factor for a stored global phase. + */ +[[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..150a5e6966 --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h @@ -0,0 +1,68 @@ +/* + * 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 + +/// Standard-basis matrix factories for the decomposition layer. Two-qubit +/// matrices use the same computational-basis index bit order as +/// ``UnitaryOpInterface::getUnitaryMatrix4x4`` (qubit 0 labels the high bit). + +namespace mlir::qco::decomposition { + +inline constexpr double FRAC1_SQRT2 = + 0.707106781186547524400844362104849039284835937688474036588L; + +/// Generic 3-parameter single-qubit unitary `U(theta, phi, lambda)`. +[[nodiscard]] Eigen::Matrix2cd uMatrix(double lambda, double phi, double theta); +/// `U2(phi, lambda) == U(pi/2, phi, lambda)`. +[[nodiscard]] Eigen::Matrix2cd u2Matrix(double lambda, double phi); +/// Axis rotations `exp(-i theta/2 * sigma_{x,y,z})`. +[[nodiscard]] Eigen::Matrix2cd rxMatrix(double theta); +[[nodiscard]] Eigen::Matrix2cd ryMatrix(double theta); +[[nodiscard]] Eigen::Matrix2cd rzMatrix(double theta); +/// Two-qubit Ising-style rotations on the `XX`, `YY`, `ZZ` generators. +[[nodiscard]] Eigen::Matrix4cd rxxMatrix(double theta); +[[nodiscard]] Eigen::Matrix4cd ryyMatrix(double theta); +[[nodiscard]] Eigen::Matrix4cd rzzMatrix(double theta); +/// Phase gate `diag(1, exp(i lambda))`. +[[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}}; +/// `i * sigma_{x,y,z}`; useful when factoring Pauli rotations out of a 2x2. +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}}; + +/// Kronecker-embed a 2x2 on wire ``qubitId`` (identity on the other wire). +[[nodiscard]] Eigen::Matrix4cd +expandToTwoQubits(const Eigen::Matrix2cd& singleQubitMatrix, QubitId qubitId); + +/// Reorder a 4x4 two-qubit matrix so its qubits match the canonical +/// `(low, high)` order given the operand-order `qubitIds`. No-op when the +/// operand order already matches. +[[nodiscard]] Eigen::Matrix4cd +fixTwoQubitMatrixQubitOrder(const Eigen::Matrix4cd& twoQubitMatrix, + const llvm::SmallVector& qubitIds); + +/// Construct the 2x2 / 4x4 matrix described by `gate`. Two-qubit gates are +/// returned in the convention matching `expandToTwoQubits` + the gate's own +/// operand order. +[[nodiscard]] Eigen::Matrix2cd getSingleQubitMatrix(const Gate& gate); +[[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..0747edf617 --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.h @@ -0,0 +1,250 @@ +/* + * 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 + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Any modifications or derivative works of this code must retain this + * copyright notice, and modified files need to carry a notice + * indicating that they have been altered from the originals. + */ +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 in 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 + /// Optional `traceToFidelity` floor for specialization; unset disables it. + std::optional requestedFidelity; + double calculatedFidelity{}; // actual fidelity of decomposition + Eigen::Matrix4cd unitaryMatrix; // original matrix for this decomposition +}; +} // namespace mlir::qco::decomposition 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..4965944a34 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp @@ -0,0 +1,422 @@ +/* + * 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 "mlir/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" + +#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}}, + }; + + // The Shende-Markov-Bullock 3-CX sandwich (and its 1/2-CX reductions) used + // below is derived for a basis CX whose 4x4 matrix is the Qiskit/LSB form + // `[[1,0,0,0],[0,0,0,1],[0,0,1,0],[0,1,0,0]]`, i.e. "control on the LSB + // factor, target on the MSB factor" of the tensor product. MQT's wider + // convention places operand 0 on the MSB factor, so `getTwoQubitMatrix` for + // the same logical CX gives the SWAP-conjugate + // `[[1,0,0,0],[0,1,0,0],[0,0,0,1],[0,0,1,0]]`. + // + // Because `SWAP * C(a,b,c) * SWAP = C(a,b,c)` but + // `SWAP * (K1l ⊗ K1r) * SWAP = (K1r ⊗ K1l)`, feeding the MSB matrix directly + // into the Weyl decomposer would swap the roles of `k1l`/`k1r` (and `k2l`/ + // `k2r`) relative to the hard-coded constants above. To keep the SMB algebra + // self-consistent we SWAP-conjugate the basis matrix here (restoring the + // Qiskit/LSB 4x4) and then absorb the resulting "left/right" relabeling at + // the emission boundary in `decomp{0,1,2,3}` below. This reproduces the + // pre-flip gate counts without having to re-derive every SMB constant for + // the MSB basis -- the two routes are algebraically equivalent. + const Eigen::Matrix4cd swap4{ + {1, 0, 0, 0}, {0, 0, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 1}}; + const Eigen::Matrix4cd basisMatrixLsb = + swap4 * getTwoQubitMatrix(basisGate) * swap4; + const auto basisDecomposer = decomposition::TwoQubitWeylDecomposition::create( + basisMatrixLsb, basisFidelity); + const auto isSuperControlled = + relativeEq(basisDecomposer.a(), std::numbers::pi / 4.0, 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 differs, but the + // worst case is a modest number of extra `Gate` slots; sequences are + // short-lived before lowering. + constexpr auto twoQubitSequenceDefaultCapacity = 21; + gates.gates.reserve(twoQubitSequenceDefaultCapacity); + gates.globalPhase -= bestNbasis * basisDecomposer.globalPhase(); + if (bestNbasis == 2) { + gates.globalPhase += std::numbers::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 + // With q0 = MSB, `kron(K1l, K1r)` places the "l" factor on qubit 0 and the + // "r" factor on qubit 1; Weyl emits the "r" factor at even indices. + addEulerDecomposition(2 * i, 1); + addEulerDecomposition((2 * i) + 1, 0); + + // add basis gate + gates.gates.push_back(basisGate); + } + + // add single-qubit decompositions after basis gate + addEulerDecomposition(2UL * bestNbasis, 1); + addEulerDecomposition((2UL * bestNbasis) + 1, 0); + + // 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, 2.0 * std::numbers::pi); + + return gates; +} + +// Ported SMB helpers assume Qiskit Weyl k-factor layout; QCO 4x4 input order +// swaps l/r vs that port. Swap k1l<->k1r and k2l<->k2r when reading ``target``, +// and swap adjacent pairs in each return vector so ``addEulerDecomposition`` +// maps matrices to the same wires as the upstream decomposer. ``decomp0`` +// cancels to the unswapped formula. +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.k2l().transpose().conjugate() * target.k2r(), + basisDecomposer.k2r().transpose().conjugate() * target.k2l(), + target.k1r() * basisDecomposer.k1l().transpose().conjugate(), + target.k1l() * basisDecomposer.k1r().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 { + q2l * target.k2r(), + q2r * target.k2l(), + q1la * rzMatrix(-2. * target.a()) * q1lb, + q1ra * rzMatrix(2. * target.b()) * q1rb, + target.k1r() * q0l, + target.k1l() * q0r, + }; +} + +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 { + u3l * target.k2r(), + u3r * target.k2l(), + u2la * rzMatrix(-2. * target.a()) * u2lb, + u2ra * rzMatrix(2. * target.b()) * u2rb, + u1l, + u1ra * rzMatrix(-2. * target.c()) * u1rb, + target.k1r() * u0l, + target.k1l() * u0r, + }; +} + +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((std::numbers::pi / 4.0) - target.a()) * + std::cos(basisDecomposer.b() - target.b()) * + std::cos(target.c()), + std::sin((std::numbers::pi / 4.0) - 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); + // Sequence is on qubit 0; check against ``expandToTwoQubits(unitaryMat, + // 0)``. + assert((circuit.getUnitaryMatrix().isApprox( + expandToTwoQubits(unitaryMat, 0), 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..9fc141284e --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerBasis.cpp @@ -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 + */ + +#include "mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h" + +#include +#include + +namespace mlir::qco::decomposition { + +[[nodiscard]] llvm::SmallVector +getGateTypesForEulerBasis(EulerBasis eulerBasis) { + switch (eulerBasis) { + case EulerBasis::ZYZ: + // Z-Y-Z style decompositions only emit `rz` and `ry`. + return {GateKind::RZ, GateKind::RY}; + case EulerBasis::ZXZ: + // Z-X-Z and X-Z-X share the same two-axis alphabet with swapped roles. + return {GateKind::RZ, GateKind::RX}; + case EulerBasis::XZX: + return {GateKind::RX, GateKind::RZ}; + case EulerBasis::XYX: + return {GateKind::RX, GateKind::RY}; + case EulerBasis::U3: + [[fallthrough]]; + case EulerBasis::U321: + [[fallthrough]]; + case EulerBasis::U: + // All U variants collapse to a single `u` operation at emission time. + return {GateKind::U}; + case EulerBasis::ZSX: + // `ZSX` only emits `rz` and `sx`. + return {GateKind::RZ, GateKind::SX}; + case EulerBasis::ZSXX: + // `ZSXX` additionally allows a bare `X` when the middle rotation is + // +/- pi, staying within the `{rz, sx, x}` alphabet. + return {GateKind::RZ, GateKind::SX, GateKind::X}; + } + 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..83139642f6 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.cpp @@ -0,0 +1,310 @@ +/* + * 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 "mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h" + +#include +#include + +#include +#include +#include + +namespace mlir::qco::decomposition { + +OneQubitGateSequence +EulerDecomposition::generateCircuit(EulerBasis targetBasis, + const Eigen::Matrix2cd& unitaryMatrix, + bool simplify, std::optional atol) { + // First normalize the input into basis-specific Euler parameters, then map + // those parameters to the target gate alphabet. + auto [theta, phi, lambda, phase] = + anglesFromUnitary(unitaryMatrix, targetBasis); + + switch (targetBasis) { + case EulerBasis::ZYZ: + return decomposeKAK(theta, phi, lambda, phase, GateKind::RZ, GateKind::RY, + simplify, atol); + case EulerBasis::ZXZ: + return decomposeKAK(theta, phi, lambda, phase, GateKind::RZ, GateKind::RX, + simplify, atol); + case EulerBasis::XZX: + return decomposeKAK(theta, phi, lambda, phase, GateKind::RX, GateKind::RZ, + simplify, atol); + case EulerBasis::XYX: + return decomposeKAK(theta, phi, lambda, phase, GateKind::RX, GateKind::RY, + simplify, atol); + case EulerBasis::U: + [[fallthrough]]; + case EulerBasis::U3: + [[fallthrough]]; + case EulerBasis::U321: + return OneQubitGateSequence{ + .gates = {{.type = GateKind::U, .parameter = {lambda, phi, theta}}}, + .globalPhase = phase - ((phi + lambda) / 2.), + }; + case EulerBasis::ZSX: + return decomposePsxGen(theta, phi, lambda, phase, /*allowXShortcut=*/false, + simplify, atol); + case EulerBasis::ZSXX: + return decomposePsxGen(theta, phi, lambda, phase, /*allowXShortcut=*/true, + simplify, atol); + } + llvm::reportFatalInternalError( + "Unsupported euler basis for circuit generation in decomposition!"); +} + +std::array +EulerDecomposition::anglesFromUnitary(const Eigen::Matrix2cd& matrix, + EulerBasis basis) { + switch (basis) { + case EulerBasis::XYX: + return paramsXyx(matrix); + case EulerBasis::XZX: + return paramsXzx(matrix); + case EulerBasis::ZYZ: + return paramsZyz(matrix); + case EulerBasis::ZXZ: + return paramsZxz(matrix); + case EulerBasis::U: + case EulerBasis::U3: + case EulerBasis::U321: + // The `u` gate parameterization is derived from the standard Z-Y-Z form. + return paramsZyz(matrix); + case EulerBasis::ZSX: + case EulerBasis::ZSXX: + // Qiskit's `params_u1x_inner` reuses Z-Y-Z angles but shifts the global + // phase by `-0.5 * (theta + phi + lambda)` so that the decomposition + // matches an `rz`/`sx` emission exactly (not only up to global phase). + return paramsU1x(matrix); + } + llvm::reportFatalInternalError( + "Unsupported euler basis for angle computation in decomposition!"); +} + +std::array +EulerDecomposition::paramsZyz(const Eigen::Matrix2cd& matrix) { + // Split the matrix determinant into a scalar phase and an SU(2) part, then + // recover the canonical Z-Y-Z angles from the relative entry magnitudes and + // phases. + 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) { + // Convert from the Z-Y-Z parameterization via the standard basis-change + // identity RX(a) = RZ(pi/2) RY(a) RZ(-pi/2). + const auto [theta, phi, lam, phase] = paramsZyz(matrix); + return {theta, phi + (std::numbers::pi / 2.0), lam - (std::numbers::pi / 2.0), + phase}; +} + +std::array +EulerDecomposition::paramsXyx(const Eigen::Matrix2cd& matrix) { + // Conjugating by Hadamards transforms an X-Y-X decomposition problem into a + // Z-Y-Z one, so we solve it there and map the angles back. + 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 + std::numbers::pi, 0.); + auto newLam = helpers::mod2pi(lam + std::numbers::pi, 0.); + return { + theta, + newPhi, + newLam, + phase + ((newPhi + newLam - phi - lam) / 2.), + }; +} + +std::array +EulerDecomposition::paramsU1x(const Eigen::Matrix2cd& matrix) { + // The determinant of the rz/sx emission depends on the Euler parameters. + // Shift the scalar phase so that `decomposePsxGen` can emit an exact + // (non-projective) decomposition in terms of `rz` and `sx`. + const auto [theta, phi, lambda, phase] = paramsZyz(matrix); + return {theta, phi, lambda, phase - (0.5 * (theta + phi + lambda))}; +} + +std::array +EulerDecomposition::paramsXzx(const Eigen::Matrix2cd& matrix) { + // Rewrite the matrix into a form where the residual SU(2) part can be + // interpreted as a Z-X-Z decomposition, then lift the resulting phase back + // to the original 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, GateKind kGate, GateKind aGate, + bool simplify, std::optional atol) { + // Treat tiny angles as zero when simplification is enabled. + 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 = {}, + // Track the scalar phase so emitted K-A-K rotations match the input + // unitary exactly (not only up to global phase). + .globalPhase = phase - ((phi + lambda) / 2.), + }; + if (std::abs(theta) <= angleZeroEpsilon) { + // A(0) vanishes, so K(lambda) A(0) K(phi) collapses to K(lambda + phi). + 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 - std::numbers::pi) <= angleZeroEpsilon) { + // At theta ~= pi, Euler parameters are non-unique. Rewrite into a stable + // equivalent form to keep emission deterministic. + sequence.globalPhase += phi; + lambda -= phi; + phi = 0.0; + } + if (std::abs(helpers::mod2pi(lambda + std::numbers::pi)) <= + angleZeroEpsilon || + std::abs(helpers::mod2pi(phi + std::numbers::pi)) <= angleZeroEpsilon) { + // Shift away from the -pi branch cut by an equivalent parameterization. + lambda += std::numbers::pi; + theta = -theta; + phi += std::numbers::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; +} + +OneQubitGateSequence +EulerDecomposition::decomposePsxGen(double theta, double phi, double lambda, + double phase, bool allowXShortcut, + bool simplify, std::optional atol) { + double angleZeroEpsilon = atol.value_or(DEFAULT_ATOL); + if (!simplify) { + // Disable all simplification checks by using a negative tolerance so that + // every `std::abs(...) < atol` comparison evaluates to false. + angleZeroEpsilon = -1.0; + } + + OneQubitGateSequence sequence{ + .gates = {}, + .globalPhase = phase, + }; + + // Append `RZ(angle)` and add `angle / 2` to `globalPhase` so the combined + // effect matches the `rz`/`sx` bookkeeping used here (RZ vs scalar phase). + // Small angles after `mod2pi` are dropped when simplification is enabled. + auto emitRzAsP = [&](double angle) { + const double canonicalAngle = helpers::mod2pi(angle); + if (std::abs(canonicalAngle) > angleZeroEpsilon) { + sequence.gates.push_back( + {.type = GateKind::RZ, .parameter = {canonicalAngle}}); + sequence.globalPhase += canonicalAngle / 2.0; + } + }; + + // Zero-`sx` decomposition: RZ(phi) . I . RZ(lambda) collapses to a single + // phase gate RZ(lambda + phi) (plus the matching phase correction). + if (std::abs(theta) < angleZeroEpsilon) { + emitRzAsP(lambda + phi); + return sequence; + } + + // Single-`sx` decomposition: + // RZ(phi) . RY(pi/2) . RZ(lambda) + // = P(phi + pi/2) . SX . P(lambda - pi/2) . e^{-i * pi / 4} + if (std::abs(theta - (std::numbers::pi / 2.0)) < angleZeroEpsilon) { + emitRzAsP(lambda - (std::numbers::pi / 2.0)); + sequence.gates.push_back({.type = GateKind::SX}); + emitRzAsP(phi + (std::numbers::pi / 2.0)); + return sequence; + } + + // General two-`sx` decomposition. + if (std::abs(theta - std::numbers::pi) < angleZeroEpsilon) { + sequence.globalPhase += lambda; + phi -= lambda; + lambda = 0.0; + } + if (std::abs(helpers::mod2pi(lambda + std::numbers::pi)) < angleZeroEpsilon || + std::abs(helpers::mod2pi(phi)) < angleZeroEpsilon) { + lambda += std::numbers::pi; + theta = -theta; + phi += std::numbers::pi; + sequence.globalPhase -= theta; + } + // Shift theta and phi to turn the decomposition from + // RZ(phi) . RY(theta) . RZ(lambda) + // = RZ(phi) . RX(-pi/2) . RZ(theta) . RX(+pi/2) . RZ(lambda) + // into P(phi + pi) . SX . P(theta + pi) . SX . P(lambda). + theta += std::numbers::pi; + phi += std::numbers::pi; + sequence.globalPhase -= std::numbers::pi / 2.0; + + emitRzAsP(lambda); + if (allowXShortcut && std::abs(helpers::mod2pi(theta)) < angleZeroEpsilon) { + // `SX . P(theta) . SX` with `theta` congruent to `+/- pi` simplifies to + // a bare `X` gate (up to the already-tracked global phase). + sequence.gates.push_back({.type = GateKind::X}); + } else { + sequence.gates.push_back({.type = GateKind::SX}); + emitRzAsP(theta); + sequence.gates.push_back({.type = GateKind::SX}); + } + emitRzAsP(phi); + return sequence; +} + +} // namespace mlir::qco::decomposition 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..f315aa505c --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/GateSequence.cpp @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h" + +#include "mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" + +#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()) { + // Count the same heuristic cost as an explicit global-phase gate. + c += helpers::getComplexity(GateKind::GPhase, 0); + } + return c; +} + +Eigen::Matrix4cd QubitGateSequence::getUnitaryMatrix() const { + Eigen::Matrix4cd unitaryMatrix = Eigen::Matrix4cd::Identity(); + for (auto&& gate : gates) { + // Left-multiply each gate matrix so the stored order matches execution + // order in the reconstructed unitary. + 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..de233d0163 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp @@ -0,0 +1,124 @@ +/* + * 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 "mlir/Dialect/QCO/IR/QCOOps.h" + +#include + +#include +#include +#include + +namespace mlir::qco::helpers { + +decomposition::GateKind getGateKind(UnitaryOpInterface op) { + Operation* raw = op.getOperation(); + if (auto ctrl = llvm::dyn_cast(raw)) { + // Controlled operations encode the physical gate in the body region. + raw = ctrl.getBodyUnitary().getOperation(); + } + if (llvm::isa(raw)) { + return decomposition::GateKind::I; + } + if (llvm::isa(raw)) { + return decomposition::GateKind::H; + } + if (llvm::isa(raw)) { + return decomposition::GateKind::P; + } + if (llvm::isa(raw)) { + return decomposition::GateKind::U; + } + if (llvm::isa(raw)) { + return decomposition::GateKind::U2; + } + if (llvm::isa(raw)) { + return decomposition::GateKind::X; + } + if (llvm::isa(raw)) { + return decomposition::GateKind::Y; + } + if (llvm::isa(raw)) { + return decomposition::GateKind::Z; + } + if (llvm::isa(raw)) { + return decomposition::GateKind::SX; + } + if (llvm::isa(raw)) { + return decomposition::GateKind::RX; + } + if (llvm::isa(raw)) { + return decomposition::GateKind::RY; + } + if (llvm::isa(raw)) { + return decomposition::GateKind::RZ; + } + if (llvm::isa(raw)) { + return decomposition::GateKind::R; + } + if (llvm::isa(raw)) { + return decomposition::GateKind::RXX; + } + if (llvm::isa(raw)) { + return decomposition::GateKind::RYY; + } + if (llvm::isa(raw)) { + return decomposition::GateKind::RZZ; + } + if (llvm::isa(raw)) { + return decomposition::GateKind::GPhase; + } + llvm::reportFatalInternalError("Unsupported QCO unitary operation kind"); +} + +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) { + // Canonicalize the upper endpoint back to -pi so callers always receive a + // half-open interval [-pi, pi). + 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(decomposition::GateKind type, + std::size_t numOfQubits) { + if (numOfQubits > 1) { + // Multi-qubit operations dominate the heuristic cost model. + constexpr std::size_t multiQubitFactor = 10; + return (numOfQubits - 1) * multiQubitFactor; + } + if (type == decomposition::GateKind::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..5319b68df2 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp @@ -0,0 +1,215 @@ +/* + * 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 +#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(singleQubitMatrix, + Eigen::Matrix2cd::Identity()); + } + if (qubitId == 1) { + return Eigen::kroneckerProduct(Eigen::Matrix2cd::Identity(), + singleQubitMatrix); + } + 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}) { + // `UnitaryOpInterface::getUnitaryMatrix4x4` uses a fixed index order; + // conjugate by SWAP when operand order is (1, 0) instead of (0, 1). + 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 == GateKind::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 == GateKind::RX) { + assert(gate.parameter.size() == 1); + return rxMatrix(gate.parameter[0]); + } + if (gate.type == GateKind::RY) { + assert(gate.parameter.size() == 1); + return ryMatrix(gate.parameter[0]); + } + if (gate.type == GateKind::RZ) { + assert(gate.parameter.size() == 1); + return rzMatrix(gate.parameter[0]); + } + if (gate.type == GateKind::X) { + return Eigen::Matrix2cd{{0, 1}, {1, 0}}; + } + if (gate.type == GateKind::I) { + return Eigen::Matrix2cd::Identity(); + } + if (gate.type == GateKind::P) { + assert(gate.parameter.size() == 1); + return pMatrix(gate.parameter[0]); + } + if (gate.type == GateKind::U) { + assert(gate.parameter.size() == 3); + return uMatrix(gate.parameter[0], gate.parameter[1], gate.parameter[2]); + } + if (gate.type == GateKind::U2) { + assert(gate.parameter.size() == 2); + return u2Matrix(gate.parameter[0], gate.parameter[1]); + } + if (gate.type == GateKind::H) { + return H_GATE; + } + llvm::reportFatalInternalError( + "unsupported gate type for single qubit matrix"); +} + +// 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 == GateKind::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 == GateKind::Z) { + // controlled Z (CZ) + return Eigen::Matrix4cd{ + {1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}, {0, 0, 0, -1}}; + } + if (gate.type == GateKind::RXX) { + assert(gate.parameter.size() == 1); + return rxxMatrix(gate.parameter[0]); + } + if (gate.type == GateKind::RYY) { + assert(gate.parameter.size() == 1); + return ryyMatrix(gate.parameter[0]); + } + if (gate.type == GateKind::RZZ) { + assert(gate.parameter.size() == 1); + return rzzMatrix(gate.parameter[0]); + } + if (gate.type == GateKind::I) { + return Eigen::Matrix4cd::Identity(); + } + llvm::reportFatalInternalError( + "Unsupported gate type for two qubit matrix"); + } + llvm::reportFatalInternalError( + "Invalid number of qubit IDs for two-qubit matrix construction"); +} + +} // 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..d4449b29e3 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.cpp @@ -0,0 +1,682 @@ +/* + * 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 "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 +#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.; + + // Numerical drift can still leave tiny determinant errors after root + // normalization. Re-normalize once more instead of aborting. + auto detNormalized = u.determinant(); + if (std::abs(detNormalized - std::complex{1.0, 0.0}) > + SANITY_CHECK_PRECISION && + std::abs(detNormalized) > SANITY_CHECK_PRECISION) { + u *= std::pow(detNormalized, -0.25); + } + + // 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 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, + (2.0 * std::numbers::pi)); + } + + // Reorder coordinates 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, (std::numbers::pi / 2.0)); + return std::min(tmp, (std::numbers::pi / 2.0) - 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] > (std::numbers::pi / 2.0)) { + cs[0] -= 3.0 * (std::numbers::pi / 2.0); + K1l = K1l * IPY; + K1r = K1r * IPY; + globalPhase += (std::numbers::pi / 2.0); + } + if (cs[1] > (std::numbers::pi / 2.0)) { + cs[1] -= 3.0 * (std::numbers::pi / 2.0); + K1l = K1l * IPX; + K1r = K1r * IPX; + globalPhase += (std::numbers::pi / 2.0); + } + auto conjs = 0; + if (cs[0] > (std::numbers::pi / 4.0)) { + cs[0] = (std::numbers::pi / 2.0) - cs[0]; + K1l = K1l * IPY; + K2r = IPY * K2r; + conjs += 1; + globalPhase -= (std::numbers::pi / 2.0); + } + if (cs[1] > (std::numbers::pi / 4.0)) { + cs[1] = (std::numbers::pi / 2.0) - cs[1]; + K1l = K1l * IPX; + K2r = IPX * K2r; + conjs += 1; + globalPhase += (std::numbers::pi / 2.0); + if (conjs == 1) { + globalPhase -= std::numbers::pi; + } + } + if (cs[2] > (std::numbers::pi / 2.0)) { + cs[2] -= 3.0 * (std::numbers::pi / 2.0); + K1l = K1l * IPZ; + K1r = K1r * IPZ; + globalPhase += (std::numbers::pi / 2.0); + if (conjs == 1) { + globalPhase -= std::numbers::pi; + } + } + if (conjs == 1) { + cs[2] = (std::numbers::pi / 2.0) - cs[2]; + K1l = K1l * IPZ; + K2r = IPZ * K2r; + globalPhase += (std::numbers::pi / 2.0); + } + if (cs[2] > (std::numbers::pi / 4.0)) { + cs[2] -= (std::numbers::pi / 2.0); + K1l = K1l * IPZ; + K1r = K1r * IPZ; + globalPhase -= (std::numbers::pi / 2.0); + } + + // 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( + (std::numbers::pi / 2.0) - 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 = GateKind::RXX, + .parameter = {-2.0 * a}, + .qubitId = {0, 1}, + }); + auto yy = getTwoQubitMatrix({ + .type = GateKind::RYY, + .parameter = {-2.0 * b}, + .qubitId = {0, 1}, + }); + auto zz = getTwoQubitMatrix({ + .type = GateKind::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.); +} + +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( + "decomposeTwoQubitProductGate: 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( + "decomposeTwoQubitProductGate: 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((std::numbers::pi / 4.0), (std::numbers::pi / 4.0), + (std::numbers::pi / 4.0)) || + isClose((std::numbers::pi / 4.0), (std::numbers::pi / 4.0), + -(std::numbers::pi / 4.0))) { + 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((std::numbers::pi / 4.0), (std::numbers::pi / 4.0), 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_ += (std::numbers::pi / 2.0); + k1l_ = k1l_ * IPZ * k2r_; + k1r_ = k1r_ * IPZ * k2l_; + k2l_ = Eigen::Matrix2cd::Identity(); + k2r_ = Eigen::Matrix2cd::Identity(); + } + a_ = (std::numbers::pi / 4.0); + b_ = (std::numbers::pi / 4.0); + c_ = (std::numbers::pi / 4.0); + } 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_ = (std::numbers::pi / 4.0); + b_ = (std::numbers::pi / 4.0); + // 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/Decomposition/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt new file mode 100644 index 0000000000..b2f48378fd --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt @@ -0,0 +1,17 @@ +# 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) +add_executable(${target_name} test_basis_decomposer.cpp test_euler_decomposition.cpp + test_weyl_decomposition.cpp) + +target_link_libraries(${target_name} PRIVATE GTest::gtest_main MLIRQCOTransforms Eigen3::Eigen) + +mqt_mlir_configure_unittest_target(${target_name}) + +gtest_discover_tests(${target_name} PROPERTIES LABELS mqt-mlir-unittests DISCOVERY_TIMEOUT 60) diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/decomposition_test_utils.h b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/decomposition_test_utils.h new file mode 100644 index 0000000000..58f3728a53 --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/decomposition_test_utils.h @@ -0,0 +1,33 @@ +/* + * 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) { + 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{}; + qr.compute(randomMatrix); + const MatrixType unitaryMatrix = qr.householderQ(); + assert(mlir::qco::helpers::isUnitaryMatrix(unitaryMatrix)); + return unitaryMatrix; +} 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..0871955472 --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_basis_decomposer.cpp @@ -0,0 +1,199 @@ +/* + * 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 "decomposition_test_utils.h" +#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 +#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, std::optional{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 = GateKind::X, .parameter = {}, .qubitId = {0, 1}}, + {.type = GateKind::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, std::optional{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( + ProductTwoQubitMatrices, BasisDecomposerTest, + testing::Combine( + // basis gates + testing::Values( + Gate{.type = GateKind::X, .parameter = {}, .qubitId = {0, 1}}, + Gate{.type = GateKind::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 = GateKind::X, .parameter = {}, .qubitId = {0, 1}}, + Gate{.type = GateKind::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 = GateKind::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..4aee00a92e --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -0,0 +1,97 @@ +/* + * 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 "decomposition_test_utils.h" +#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 +#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, std::nullopt); + 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 currentEulerBasis = 0; + for (int i = 0; i < maxIterations; ++i) { + auto originalMatrix = randomUnitaryMatrix(rng); + auto eulerBasis = eulerBases[currentEulerBasis++ % 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_SUITE_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..49846420eb --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -0,0 +1,173 @@ +/* + * 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 "decomposition_test_utils.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 + +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, std::optional{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, std::optional{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, std::optional{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( + ProductTwoQubitMatrices, 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 = GateKind::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 = GateKind::X, + .parameter = {}, + .qubitId = {0, 1}}) * + getTwoQubitMatrix({.type = GateKind::X, + .parameter = {}, + .qubitId = {1, 0}}) * + getTwoQubitMatrix( + {.type = GateKind::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 = GateKind::X, + .parameter = {}, + .qubitId = {0, 1}}) * + getTwoQubitMatrix( + {.type = GateKind::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); + })); From 741e4c1549263bf04466a8cbca1473b152f1af0e Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 22 Apr 2026 16:00:56 +0200 Subject: [PATCH 003/122] =?UTF-8?q?=E2=9C=A8=20Introduce=20native=20gate?= =?UTF-8?q?=20synthesis=20pass.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/include/mlir/Compiler/CompilerPipeline.h | 21 +- .../Transforms/NativeSynthesis/NativeSpec.h | 39 + .../NativeSynthesis/PassTwoQubitWindows.h | 59 ++ .../QCO/Transforms/NativeSynthesis/Policy.h | 56 ++ .../QCO/Transforms/NativeSynthesis/Scoring.h | 89 +++ .../Transforms/NativeSynthesis/SingleQubit.h | 52 ++ .../QCO/Transforms/NativeSynthesis/TwoQubit.h | 70 ++ .../QCO/Transforms/NativeSynthesis/Types.h | 144 ++++ .../QCO/Transforms/NativeSynthesis/Utils.h | 67 ++ .../mlir/Dialect/QCO/Transforms/Passes.h | 19 +- .../mlir/Dialect/QCO/Transforms/Passes.td | 67 ++ mlir/lib/Compiler/CMakeLists.txt | 1 + mlir/lib/Compiler/CompilerPipeline.cpp | 13 +- .../lib/Dialect/QCO/Transforms/CMakeLists.txt | 12 +- .../Transforms/NativeSynthesis/NativeSpec.cpp | 225 ++++++ .../QCO/Transforms/NativeSynthesis/Pass.cpp | 630 ++++++++++++++++ .../NativeSynthesis/PassTwoQubitWindows.cpp | 263 +++++++ .../QCO/Transforms/NativeSynthesis/Policy.cpp | 201 +++++ .../NativeSynthesis/SingleQubit.cpp | 384 ++++++++++ .../Transforms/NativeSynthesis/TwoQubit.cpp | 320 ++++++++ .../QCO/Transforms/NativeSynthesis/Utils.cpp | 231 ++++++ mlir/tools/mqt-cc/CMakeLists.txt | 3 +- mlir/tools/mqt-cc/mqt-cc.cpp | 32 +- .../Compiler/test_compiler_pipeline.cpp | 525 +++++++++++++ .../Dialect/QCO/Transforms/CMakeLists.txt | 2 + .../Transforms/NativeSynthesis/CMakeLists.txt | 21 + .../native_synthesis_pass_test_fixture.h | 359 +++++++++ .../native_synthesis_test_helpers.cpp | 428 +++++++++++ .../native_synthesis_test_helpers.h | 60 ++ ...est_native_synthesis_pass_custom_menus.cpp | 504 +++++++++++++ .../test_native_synthesis_pass_fusion.cpp | 610 +++++++++++++++ ...test_native_synthesis_pass_multi_qubit.cpp | 279 +++++++ .../test_native_synthesis_pass_profiles.cpp | 700 ++++++++++++++++++ .../test_native_synthesis_pass_scoring.cpp | 265 +++++++ 34 files changed, 6738 insertions(+), 13 deletions(-) create mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h create mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.h create mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h create mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Scoring.h create mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h create mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h create mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h create mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h create mode 100644 mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.cpp create mode 100644 mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp create mode 100644 mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp create mode 100644 mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Policy.cpp create mode 100644 mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp create mode 100644 mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp create mode 100644 mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp create mode 100644 mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt create mode 100644 mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_pass_test_fixture.h create mode 100644 mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp create mode 100644 mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.h create mode 100644 mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_custom_menus.cpp create mode 100644 mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_fusion.cpp create mode 100644 mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_multi_qubit.cpp create mode 100644 mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_profiles.cpp create mode 100644 mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_scoring.cpp diff --git a/mlir/include/mlir/Compiler/CompilerPipeline.h b/mlir/include/mlir/Compiler/CompilerPipeline.h index d15585827f..2809a44a0f 100644 --- a/mlir/include/mlir/Compiler/CompilerPipeline.h +++ b/mlir/include/mlir/Compiler/CompilerPipeline.h @@ -43,6 +43,25 @@ struct QuantumCompilerConfig { /// Disable quaternion-based single-qubit rotation gate merging bool disableMergeSingleQubitRotationGates = false; + + /// Comma-separated native gate menu. Recognised tokens: `u`, `x`, `sx`, + /// `rz` (or `p`), `rx`, `ry`, `r`, `cx`, `cz`, `rzz`. An empty or + /// whitespace-only string leaves native synthesis as a no-op (IR + /// unchanged). Common examples: + /// - `"x,sx,rz,cx"` — IBM basic (CX) + /// - `"x,sx,rz,rx,rzz,cz"` — IBM fractional + /// - `"r,cz"` — IQM default + /// - `"u,cx"` — generic U3 + CX + std::string nativeGates; + + /// Weight for two-qubit gates in local candidate scoring + double nativeGateScoreWeightTwoQ = 1.0; + + /// Weight for single-qubit gates in local candidate scoring + double nativeGateScoreWeightOneQ = 0.1; + + /// Weight for local candidate depth in local candidate scoring + double nativeGateScoreWeightDepth = 0.01; }; /** @@ -78,7 +97,7 @@ struct CompilationRecord { * 2. QC cleanup pipeline * 3. QCO dialect (value semantics) - enables SSA-based optimizations * 4. QCO cleanup pipeline - * 5. Quantum optimization passes + * 5. Optimization and native gate synthesis * 6. QCO cleanup pipeline * 7. QC dialect - converted back for backend lowering * 8. QC cleanup pipeline diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h new file mode 100644 index 0000000000..3136f85ae0 --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h @@ -0,0 +1,39 @@ +/* + * 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/NativeSynthesis/Types.h" + +#include +#include + +#include + +/// Parses the pass `native-gates` string into a `NativeProfileSpec` (emitters, +/// entanglers, `allowedGates`). Token set matches `Passes.td` on this pass. + +namespace mlir::qco::native_synth { + +/// Euler bases that can reconstruct a two-axis single-qubit unitary. +llvm::SmallVector +getEulerBasesForAxisPair(AxisPair axisPair); + +/// Resolve a comma-separated native gate menu (e.g. `"x,sx,rz,cx"`) into a +/// full `NativeProfileSpec`. Returns `std::nullopt` if the menu is empty, +/// contains unknown tokens, or cannot be covered by any supported +/// single-qubit synthesis strategy. +/// +/// Recognised tokens: `u`, `x`, `sx`, `rz` (or `p`), `rx`, `ry`, `r`, +/// `cx`, `cz`, `rzz`. +std::optional +resolveNativeGatesSpec(llvm::StringRef nativeGates); + +} // namespace mlir::qco::native_synth diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.h new file mode 100644 index 0000000000..23da65197f --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.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 + */ + +/// \file +/// Helpers for `NativeGateSynthesisPass` two-qubit window consolidation. Not +/// a stable public API; kept in-tree for reuse by the pass (and its tests). + +#pragma once + +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" + +#include +#include +#include +#include +#include +#include + +#include + +namespace mlir::qco::native_synth { + +/// State for one maximal two-qubit window (plus absorbed one-qubit ops) +/// during consolidation. +struct TwoQubitBlock { + Value wireA; + Value wireB; + llvm::SmallVector ops; + Eigen::Matrix4cd accum = Eigen::Matrix4cd::Identity(); + unsigned numTwoQ = 0; + unsigned numOneQ = 0; + bool anyNonNative = false; + bool open = true; +}; + +/// Pre-order walk: every op implementing `UnitaryOpInterface` under `root`. +void collectUnitaryOpsInPreOrder(Operation* root, std::vector& ops); + +/// Tracks overlapping two-qubit windows on a module slice; implemented in +/// ``NativeSynthesis/PassTwoQubitWindows.cpp``. +struct TwoQubitWindowConsolidator { + llvm::SmallVector blocks; + llvm::DenseMap wireToBlock; + + void closeBlock(size_t idx); + void closeBlockOnWire(Value v); + void process(Operation* op, const NativeProfileSpec& spec); + void materialize(IRRewriter& rewriter, const NativeProfileSpec& spec, + const ScoreWeights& weights); +}; + +} // namespace mlir::qco::native_synth diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h new file mode 100644 index 0000000000..7707cc968c --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" + +#include + +#include + +/// Menu checks and cost hints for synthesis candidates (no IR rewrites). + +namespace mlir::qco::native_synth { + +/// Score weights are valid iff they are finite and non-negative. +bool areValidScoreWeights(const ScoreWeights& weights); + +/// Whether the menu contains the corresponding two-qubit entangler. Used by +/// the 2q rewrite path to pick between CX and CZ emission. +bool usesCxEntangler(const NativeProfileSpec& spec); +bool usesCzEntangler(const NativeProfileSpec& spec); + +/// Whether an already-lowered single-qubit op is in the menu (i.e. no +/// further rewrite needed). `BarrierOp` / `GPhaseOp` always pass through +/// unchanged. +bool allowsSingleQubitOp(UnitaryOpInterface op, const NativeProfileSpec& spec); + +/// Count 1q/2q gates and compute the depth of a gate sequence. +CandidateMetrics +computeGateSequenceMetrics(const decomposition::QubitGateSequence& seq); + +/// Whether `op` has a direct (non-matrix) lowering via the corresponding +/// `decomposeTo*` helper in `SingleQubit.h`. +bool canDirectlyDecomposeToZSXX(Operation* op, bool supportsDirectRx); +bool canDirectlyDecomposeToU3(Operation* op); +bool canDirectlyDecomposeToR(Operation* op); +bool canDirectlyDecomposeToAxisPair(Operation* op, AxisPair axisPair); + +/// Estimated metrics for the direct and matrix-fallback lowerings. +CandidateMetrics +estimateDirectSingleQubitMetrics(Operation* op, + const SingleQubitEmitterSpec& emitter); +std::optional +estimateMatrixSingleQubitMetrics(UnitaryOpInterface unitary, + const SingleQubitEmitterSpec& emitter); + +} // namespace mlir::qco::native_synth diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Scoring.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Scoring.h new file mode 100644 index 0000000000..eb03f69a60 --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Scoring.h @@ -0,0 +1,89 @@ +/* + * 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/NativeSynthesis/Types.h" + +#include +#include + +#include +#include + +/// Deterministic candidate scoring and selection. All comparisons are total +/// orders, so the same input always picks the same candidate. + +namespace mlir::qco::native_synth { + +/// Primary cost `weighted`; when two weighted scores agree within FP tolerance, +/// `isBetterScore` breaks ties in order: `numTwoQ`, `depth`, `numOneQ`, +/// `tieBreakClass`, `enumerationIndex`. +struct CandidateScore { + double weighted = 0.0; + unsigned numTwoQ = 0; + unsigned depth = 0; + unsigned numOneQ = 0; + unsigned tieBreakClass = 0; + unsigned enumerationIndex = 0; +}; + +/// Project a candidate onto its `CandidateScore`. +template +CandidateScore scoreCandidate(const SynthesisCandidate& candidate, + const ScoreWeights& weights) { + return { + .weighted = + (weights.twoQ * static_cast(candidate.metrics.numTwoQ)) + + (weights.oneQ * static_cast(candidate.metrics.numOneQ)) + + (weights.depth * static_cast(candidate.metrics.depth)), + .numTwoQ = candidate.metrics.numTwoQ, + .depth = candidate.metrics.depth, + .numOneQ = candidate.metrics.numOneQ, + .tieBreakClass = static_cast(candidate.candidateClass), + .enumerationIndex = candidate.enumerationIndex, + }; +} + +/// Strict less-than: `true` iff `lhs` is a strictly better candidate than +/// `rhs`. Weighted costs within `1e-12` are treated as equal, so +/// floating-point noise does not flip the decision. +inline bool isBetterScore(const CandidateScore& lhs, + const CandidateScore& rhs) { + constexpr double scoreTolerance = 1e-12; + if (std::abs(lhs.weighted - rhs.weighted) > scoreTolerance) { + return lhs.weighted < rhs.weighted; + } + return std::tie(lhs.numTwoQ, lhs.depth, lhs.numOneQ, lhs.tieBreakClass, + lhs.enumerationIndex) < + std::tie(rhs.numTwoQ, rhs.depth, rhs.numOneQ, rhs.tieBreakClass, + rhs.enumerationIndex); +} + +/// Return the best candidate by `isBetterScore`, or `nullptr` on empty input. +template +const Candidate* selectBestCandidate(llvm::ArrayRef candidates, + const ScoreWeights& weights) { + if (candidates.empty()) { + return nullptr; + } + const auto* best = &candidates.front(); + auto bestScore = scoreCandidate(*best, weights); + for (const auto& candidate : llvm::drop_begin(candidates)) { + const auto candidateScore = scoreCandidate(candidate, weights); + if (isBetterScore(candidateScore, bestScore)) { + best = &candidate; + bestScore = candidateScore; + } + } + return best; +} + +} // namespace mlir::qco::native_synth diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h new file mode 100644 index 0000000000..e74de402be --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" + +#include +#include +#include + +#include + +/// Single-qubit lowering: `decomposeTo*` for symbolic matches, plus +/// `computeSynthesizedSingleQubitLength` / +/// `emitSynthesizedSingleQubitFromMatrix` for the Euler matrix fallback. + +namespace mlir::qco::native_synth { + +/// Direct (non-matrix) single-qubit lowering to each single-qubit emission +/// strategy. Returns the output qubit value, or a null `Value` if no direct +/// rule applies and a matrix-based fallback must be tried. +/// +/// When `supportsDirectRx` is true, `decomposeToZSXX` also passes `Rx` +/// through unchanged and lowers `Ry` / `R` via an `rz * rx * rz` sandwich. +Value decomposeToZSXX(IRRewriter& rewriter, Operation* op, Value inQubit, + bool supportsDirectRx); +Value decomposeToU3(IRRewriter& rewriter, Operation* op, Value inQubit); +Value decomposeToR(IRRewriter& rewriter, Operation* op, Value inQubit); +Value decomposeToAxisPair(IRRewriter& rewriter, Operation* op, Value inQubit, + AxisPair axisPair); + +/// Cost estimate in number of emitted ops for fusing a single-qubit unitary +/// with the given emitter. Returns `SIZE_MAX` if no Euler basis is available. +std::size_t +computeSynthesizedSingleQubitLength(const Eigen::Matrix2cd& matrix, + const SingleQubitEmitterSpec& emitter); + +/// Emit the fused `2×2` unitary as native ops, inserting a `qco.gphase` if the +/// emitted sequence carries a non-trivial residual global phase. +Value emitSynthesizedSingleQubitFromMatrix( + IRRewriter& rewriter, Location loc, Value inQubit, + const Eigen::Matrix2cd& matrix, const SingleQubitEmitterSpec& emitter); + +} // namespace mlir::qco::native_synth diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h new file mode 100644 index 0000000000..cbfce72f93 --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.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 "mlir/Dialect/QCO/IR/QCOInterfaces.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" + +#include +#include +#include +#include + +#include +#include + +/// Two-qubit lowering: Weyl decomposition + `TwoQubitBasisDecomposer` over +/// each `(entangler, emitter Euler basis, basis-use count 0..3)` allowed by +/// the menu; the scorer picks the cheapest exact sequence. + +namespace mlir::qco::native_synth { + +/// Whether every gate in `seq` is allowed by `spec`'s menu. +bool gateSequenceFitsMenu(const decomposition::TwoQubitGateSequence& seq, + const NativeProfileSpec& spec); + +/// Decompose a `4×4` target unitary into a gate sequence targeting the given +/// entangler basis, using `TwoQubitWeylDecomposition` + +/// `TwoQubitBasisDecomposer` with the supplied Euler basis and optional +/// basis-use count. +std::optional +decomposeTwoQubitFromMatrix(const Eigen::Matrix4cd& matrix, + EntanglerBasis entangler, + decomposition::EulerBasis eulerBasis, + std::optional numBasisUses); + +/// Enumerate all direct + matrix-fallback single-qubit rewrite candidates. +llvm::SmallVector> +collectSingleQubitCandidates(UnitaryOpInterface unitary, + const NativeProfileSpec& spec); + +/// Enumerate full two-qubit basis-decomposer candidates for a given `4×4` +/// target. +llvm::SmallVector, 0> +collectTwoQubitBasisCandidatesFromMatrix(const Eigen::Matrix4cd& targetMatrix, + const NativeProfileSpec& spec); + +/// Overload that reads the target matrix from a two-qubit op. +llvm::SmallVector, 0> +collectTwoQubitBasisCandidates(UnitaryOpInterface unitary, + const NativeProfileSpec& spec); + +/// Scoring metrics for the `rewriteXXPlusMinusYYViaRxxRyy` lowering (both +/// `XXPlusYY` and `XXMinusYY` branches emit the same gate counts). Keep in +/// sync when changing that rewrite. +CandidateMetrics xxPlusMinusYyRzzRewriteScoringMetrics(); + +/// Rewrite `XXPlusYY` / `XXMinusYY` via two `RZZ` blocks (menus with `rzz`). +/// Sets `rewriter`'s insertion point to `op` before emitting. +LogicalResult rewriteXXPlusMinusYYViaRxxRyy(IRRewriter& rewriter, + Operation* op); + +} // namespace mlir::qco::native_synth diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h new file mode 100644 index 0000000000..62d517c73a --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h @@ -0,0 +1,144 @@ +/* + * 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/EulerBasis.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h" + +#include + +#include +#include + +/// Types for native gate synthesis: menu, emitters, candidates, score weights. + +namespace mlir::qco::native_synth { + +/// Two-axis single-qubit families for `axis-pair-*` profiles. +enum class AxisPair : std::uint8_t { RxRz, RxRy, RyRz }; + +/// Single-qubit emission strategy. +enum class SingleQubitMode : std::uint8_t { + /// Emit `{X, Sx, Rz}` via the ZSXX Euler decomposition. When the spec's + /// `supportsDirectRx` is set, the emitter additionally passes Rx through + /// unchanged and expands Ry / R via an `rz * rx * rz` sandwich. + ZSXX, + /// Emit a single `u(theta, phi, lambda)` op. + U3, + /// Emit `R(theta, phi)` via the XYX Euler decomposition. + R, + /// Emit one of the three two-axis rotation pairs selected by `axisPair`. + AxisPair, +}; + +/// Two-qubit entangling basis selected by a profile. `None` means the menu +/// does not provide any entangler and two-qubit ops cannot be synthesized. +enum class EntanglerBasis : std::uint8_t { None, Cx, Cz }; + +/// Profile-level classification of a native gate. Used both to describe the +/// menu (`NativeProfileSpec::allowedGates`) and to classify already-lowered +/// output ops in policy checks. One-to-one with a recognised menu token. +enum class NativeGateKind : std::uint8_t { + U, + X, + Sx, + Rz, + Rx, + Ry, + R, + Cx, + Cz, + Rzz, +}; + +/// Single-qubit emitter specification: the target mode plus any modifiers +/// (axis pair, Euler bases to consider when decomposing, whether direct Rx +/// emission is permitted). +struct SingleQubitEmitterSpec { + SingleQubitMode mode = SingleQubitMode::U3; + AxisPair axisPair = AxisPair::RxRz; + llvm::SmallVector eulerBases; + /// Only meaningful for `SingleQubitMode::ZSXX`: when set, the emitter may + /// emit Rx / Ry / R directly (via an `rz * rx * rz` sandwich for the latter + /// two) instead of falling back to the ZSXX Euler sequence. + bool supportsDirectRx = false; +}; + +/// Resolved menu: emitters to try for 1q synthesis and entangler bases for 2q. +/// Built by `resolveNativeGatesSpec`. +struct NativeProfileSpec { + bool allowRzz = false; + /// Flattened menu; used for cheap "is this op already native?" checks. + std::set allowedGates; + llvm::SmallVector singleQubitEmitters; + llvm::SmallVector entanglerBases; +}; + +/// Weights for the deterministic local cost model. Candidate cost is +/// `twoQ * #2q + oneQ * #1q + depth * localDepth`; lower is better. +struct ScoreWeights { + double twoQ = 1.0; + double oneQ = 0.1; + double depth = 0.01; +}; + +/// Gate counts describing a synthesized candidate. +struct CandidateMetrics { + unsigned numOneQ = 0; + unsigned numTwoQ = 0; + unsigned depth = 0; +}; + +/// Tie-break classes in preference order (lower wins). Used as the final +/// structural tiebreaker in `isBetterScore` after the weighted cost and the +/// raw 2q/depth/1q counts. +enum class CandidateClass : std::uint8_t { + NativePassthrough = 0, + DirectSingleQ = 1, + MatrixSingleQ = 2, + TwoQubitBasisRewrite = 3, + XxPlusMinusViaRzz = 4, +}; + +/// Generic candidate wrapper carrying a typed rewrite plan payload. +/// `enumerationIndex` makes the candidate ordering stable across runs. +template struct SynthesisCandidate { + CandidateClass candidateClass = CandidateClass::NativePassthrough; + CandidateMetrics metrics; + unsigned enumerationIndex = 0; + Payload payload; +}; + +/// How to rewrite a single-qubit op onto the native menu. +/// +/// - `Direct`: pattern-match the op type and emit the target gates directly +/// via `decomposeTo*` (applicable to a small fixed set of op types per +/// emitter). +/// - `MatrixFallback`: fold the op to a 2x2 matrix and run an Euler +/// decomposition in the emitter's basis; handles anything constant. +enum class SingleQubitRewriteStrategy : std::uint8_t { Direct, MatrixFallback }; + +/// Picked single-qubit rewrite: which emitter to use and how to drive it. +struct SingleQubitRewritePlan { + SingleQubitRewriteStrategy strategy = SingleQubitRewriteStrategy::Direct; + SingleQubitEmitterSpec emitter; +}; + +/// Picked two-qubit rewrite: a pre-computed abstract gate sequence produced +/// by `TwoQubitBasisDecomposer` plus the single-qubit emitter and entangler +/// basis used when materializing the sequence back into MLIR. +struct TwoQubitRewritePlan { + decomposition::TwoQubitGateSequence sequence; + SingleQubitEmitterSpec emitter; + EntanglerBasis entanglerBasis = EntanglerBasis::None; +}; + +} // namespace mlir::qco::native_synth diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h new file mode 100644 index 0000000000..2c4f5f194b --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h" + +#include +#include +#include + +#include + +/// F64 helpers, global phase, SU(4) normalization, and 2q sequence emission. + +namespace mlir::qco::native_synth { + +/// Create an ``arith.constant`` F64. +Value createF64Const(IRRewriter& rewriter, Location loc, double value); + +/// If ``value`` is an F64 ``arith.constant``, return its value. +std::optional getConstantF64(Value value); + +/// Emit a `qco.gphase` if `phase` is non-negligible. +void emitGPhaseIfNonTrivial(IRRewriter& rewriter, Location loc, double phase); + +/// Matrix equality up to a unit-modulus global phase. +bool isEquivalentUpToGlobalPhase(const Eigen::Matrix4cd& lhs, + const Eigen::Matrix4cd& rhs, + double atol = 1e-10); + +/// Rescale `matrix` to determinant 1 (SU(4)) for Weyl / basis decomposers. +/// No-op if det is numerically zero. +void normalizeToSU4(Eigen::Matrix4cd& matrix); + +/// ``getUnitaryMatrix4x4`` then rescale to SU(4). +bool getNormalizedTwoQubitMatrix(UnitaryOpInterface unitary, + Eigen::Matrix4cd& matrix); + +/// 4x4 for a 2q block member (plain 2q, ``CtrlOp`` CX/CZ, or lifted 1q). Fails +/// for barriers, ``gphase``, multi-control, or non-constant matrix parameters. +bool getBlockTwoQubitMatrix(Operation* op, Eigen::Matrix4cd& matrix); + +/// Emit `seq` in order: abstract qubit id `0` → `qubit0`, id `1` → `qubit1`; +/// two-qubit steps become `CtrlOp` with `XOp`/`ZOp` on the target wire (CZ is +/// symmetric). Does not replace any existing op. +LogicalResult +emitTwoQubitGateSequenceAtLoc(IRRewriter& rewriter, Location loc, Value qubit0, + Value qubit1, + const decomposition::TwoQubitGateSequence& seq, + Value& outQubit0, Value& outQubit1); + +/// Emit a two-qubit gate sequence and replace `op` with the resulting tails. +LogicalResult +emitTwoQubitGateSequence(IRRewriter& rewriter, Operation* op, Value qubit0, + Value qubit1, + const decomposition::TwoQubitGateSequence& seq); + +} // namespace mlir::qco::native_synth diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h index c3589793e6..60a9216ef0 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h @@ -10,12 +10,12 @@ #pragma once -#include "mlir/Dialect/QCO/IR/QCODialect.h" - #include #include #include +#include + namespace mlir::qco { #define GEN_PASS_DECL @@ -29,4 +29,19 @@ namespace mlir::qco { #define GEN_PASS_REGISTRATION #include "mlir/Dialect/QCO/Transforms/Passes.h.inc" // IWYU pragma: export +/// Options for the native gate synthesis pass. +/// +/// @p nativeGates is a comma-separated list of gate tokens (see `Passes.td` +/// for recognised tokens). An empty or whitespace-only string is a no-op (IR +/// unchanged). +struct NativeGateSynthesisOptions { + std::string nativeGates; + double scoreWeightTwoQ = 1.0; + double scoreWeightOneQ = 0.1; + double scoreWeightDepth = 0.01; +}; + +std::unique_ptr +createNativeGateSynthesisPass(const NativeGateSynthesisOptions& options); + } // namespace mlir::qco diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index edca59797e..0e485cced9 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -101,4 +101,71 @@ def MappingPass : Pass<"place-and-route", "mlir::ModuleOp"> { "The number of inserted SWAPs">]; } +def NativeGateSynthesisPass : Pass<"native-gate-synthesis", "mlir::ModuleOp"> { + let dependentDialects = ["mlir::qco::QCODialect"]; + let summary = "Lower QCO unitary gates to a user-specified native gate menu."; + let description = [{ + This pass rewrites a module so that every remaining unitary operation is + allowed by the `native-gates` menu. `qco.barrier` and `qco.gphase` are + preserved; controlled gates (`qco.ctrl`) must have a single control and a + single target. + + The menu is a comma-separated list of gate tokens from which the pass + derives a single-qubit synthesis strategy (`u`, `zsxx`, IQM-style `r`, or + an axis pair `rx`/`ry`/`rz`) and the set of available two-qubit entanglers + (`cx`, `cz`, `rzz`). + + Recognised tokens: `u`, `x`, `sx`, `rz` (or `p`), `rx`, `ry`, `r`, `cx`, + `cz`, `rzz`. An empty or whitespace-only menu is a no-op, which is the + intended pipeline default when synthesis is not needed. An unrecognised + token or an invalid score weight (non-finite or negative) causes the pass + to fail. + + Example menus: + - `x,sx,rz,cx` (IBM basic) + - `x,sx,rz,rx,rzz,cz` (IBM fractional) + - `r,cz` (IQM default) + - `u,cx` (generic U3 + CX) + - `rx,rz,cx` (Rx/Rz axis pair + CX) + + The pass runs single-qubit fusion, then a two-qubit window pass (including + absorbed single-qubit padding), then up to four lowering sweeps over + remaining non-native unitaries. Two-qubit lowering may emit temporary + off-menu single-qubit gates; later sweeps try to absorb them. If any + off-menu single-qubit gates remain after that cap, the pass fails. + + It then fuses single-qubit runs again (seams between two-qubit blocks), + merges `rz` angles through `qco.ctrl` control chains where valid, fuses + single-qubit runs once more, and runs up to four optional lowering + + fusion rounds until the full menu holds (including `qco.ctrl` shells and + bare two-qubit gates). If anything is still off-menu, the pass fails. + + Candidate selection minimises the linear cost + `score-weight-twoq * #2q + score-weight-oneq * #1q + + score-weight-depth * local-depth`. Defaults (`1.0 / 0.1 / 0.01`) favour + minimising two-qubit count first, then single-qubit count, then depth. + + `qco.ctrl` wrappers whose body is `qco.x` or `qco.z` are left untouched + when `cx` or `cz` is on the menu; otherwise they are treated as a `4×4` + unitary and go through the same two-qubit search as bare two-qubit gates. + `qco.xx_plus_yy` / `qco.xx_minus_yy` are lowered via a dedicated + `rzz`-centric rewrite when `rzz` is on the menu, and via the general + two-qubit decomposition otherwise. + }]; + let options = + [Option<"nativeGates", "native-gates", "std::string", "\"\"", + "Comma-separated native gate menu. Empty or whitespace-only is " + "a no-op. Recognised tokens: u, x, sx, rz (or p), rx, ry, r, cx, " + "cz, rzz.">, + Option<"scoreWeightTwoQ", "score-weight-twoq", "double", "1.0", + "Weight for the number of two-qubit gates in candidate " + "scoring. Must be finite and non-negative.">, + Option<"scoreWeightOneQ", "score-weight-oneq", "double", "0.1", + "Weight for the number of single-qubit gates in candidate " + "scoring. Must be finite and non-negative.">, + Option<"scoreWeightDepth", "score-weight-depth", "double", "0.01", + "Weight for the local candidate depth in candidate scoring. " + "Must be finite and non-negative.">]; +} + #endif // MLIR_DIALECT_QCO_TRANSFORMS_PASSES_TD diff --git a/mlir/lib/Compiler/CMakeLists.txt b/mlir/lib/Compiler/CMakeLists.txt index 1dc45532fa..ddefa918a5 100644 --- a/mlir/lib/Compiler/CMakeLists.txt +++ b/mlir/lib/Compiler/CMakeLists.txt @@ -19,6 +19,7 @@ add_mlir_library( MLIRTransformUtils MLIRQCToQCO MLIRQCOToQC + MLIRQCOTransforms MLIRQCToQIR MLIRQCOTransforms MQT::MLIRSupport) diff --git a/mlir/lib/Compiler/CompilerPipeline.cpp b/mlir/lib/Compiler/CompilerPipeline.cpp index c66196a7d2..ea99ca7be0 100644 --- a/mlir/lib/Compiler/CompilerPipeline.cpp +++ b/mlir/lib/Compiler/CompilerPipeline.cpp @@ -136,19 +136,26 @@ QuantumCompilerPipeline::runPipeline(ModuleOp module, totalStages); } } - // Stage 5: Optimization passes + // Stage 5: Optimization and native gate synthesis if (failed(runStage([&](PassManager& pm) { if (!config_.disableMergeSingleQubitRotationGates) { pm.addPass(qco::createMergeSingleQubitRotationGates()); } + pm.addPass( + qco::createNativeGateSynthesisPass(qco::NativeGateSynthesisOptions{ + .nativeGates = config_.nativeGates, + .scoreWeightTwoQ = config_.nativeGateScoreWeightTwoQ, + .scoreWeightOneQ = config_.nativeGateScoreWeightOneQ, + .scoreWeightDepth = config_.nativeGateScoreWeightDepth, + })); }))) { return failure(); } if (record != nullptr && config_.recordIntermediates) { record->afterOptimization = captureIR(module); if (config_.printIRAfterAllStages) { - prettyPrintStage(module, "Optimization Passes", ++currentStage, - totalStages); + prettyPrintStage(module, "Optimization and Native Gate Synthesis", + ++currentStage, totalStages); } } // Stage 6: QCO cleanup diff --git a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt index fc6ee74b9d..ef9f6be753 100644 --- a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt +++ b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt @@ -12,6 +12,8 @@ add_mlir_library( MLIRQCOTransforms ${PASSES_SOURCES} LINK_LIBS + PUBLIC + Eigen3::Eigen PRIVATE MLIRQCODialect MLIRQCOUtils @@ -20,11 +22,11 @@ add_mlir_library( DEPENDS MLIRQCOTransformsIncGen) -# collect header files -file(GLOB_RECURSE PASSES_HEADERS_SOURCE - ${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Dialect/QCO/Transforms/*.h) -file(GLOB_RECURSE PASSES_HEADERS_BUILD - ${MQT_MLIR_BUILD_INCLUDE_DIR}/mlir/Dialect/QCO/Transforms/*.inc) +# collect header files (subdirs: NativeSynthesis/, Decomposition/, …) +file(GLOB_RECURSE PASSES_HEADERS_SOURCE CONFIGURE_DEPENDS + "${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Dialect/QCO/Transforms/**/*.h") +file(GLOB_RECURSE PASSES_HEADERS_BUILD CONFIGURE_DEPENDS + "${MQT_MLIR_BUILD_INCLUDE_DIR}/mlir/Dialect/QCO/Transforms/**/*.inc") # add public headers using file sets target_sources( diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.cpp new file mode 100644 index 0000000000..bc834d67ca --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.cpp @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h" + +#include +#include +#include + +namespace mlir::qco::native_synth { +namespace { + +std::optional parseGateToken(llvm::StringRef name) { + return llvm::StringSwitch>(name) + .Case("u", NativeGateKind::U) + .Case("x", NativeGateKind::X) + .Case("sx", NativeGateKind::Sx) + .Cases("rz", "p", NativeGateKind::Rz) + .Case("rx", NativeGateKind::Rx) + .Case("ry", NativeGateKind::Ry) + .Case("r", NativeGateKind::R) + .Case("cx", NativeGateKind::Cx) + .Case("cz", NativeGateKind::Cz) + .Case("rzz", NativeGateKind::Rzz) + .Default(std::nullopt); +} + +std::optional> +parseGateSet(llvm::StringRef nativeGates) { + std::set gates; + llvm::SmallVector parts; + nativeGates.split(parts, ',', /*MaxSplit=*/-1, /*KeepEmpty=*/false); + for (llvm::StringRef part : parts) { + const auto token = part.trim().lower(); + if (token.empty()) { + continue; + } + const auto gate = parseGateToken(token); + if (!gate) { + return std::nullopt; + } + gates.insert(*gate); + } + return gates; +} + +SingleQubitEmitterSpec makeEmitterSpec(SingleQubitMode mode, + AxisPair axisPair = AxisPair::RxRz, + bool supportsDirectRx = false) { + llvm::SmallVector bases; + switch (mode) { + case SingleQubitMode::ZSXX: + bases = {decomposition::EulerBasis::ZSXX}; + break; + case SingleQubitMode::U3: + bases = {decomposition::EulerBasis::U3}; + break; + case SingleQubitMode::R: + // XYX decomposes any 1Q unitary into Rx-Ry-Rx chains, all of which the + // R emitter lowers back into the native R(theta, phi) gate. + bases = {decomposition::EulerBasis::XYX}; + break; + case SingleQubitMode::AxisPair: + bases = getEulerBasesForAxisPair(axisPair); + break; + } + return {.mode = mode, + .axisPair = axisPair, + .eulerBases = std::move(bases), + .supportsDirectRx = supportsDirectRx}; +} + +void addEmitterIfAbsent(llvm::SmallVectorImpl& emitters, + SingleQubitMode mode, + AxisPair axisPair = AxisPair::RxRz, + bool supportsDirectRx = false) { + const bool present = llvm::any_of(emitters, [&](const auto& e) { + return e.mode == mode && e.axisPair == axisPair && + e.supportsDirectRx == supportsDirectRx; + }); + if (!present) { + emitters.push_back(makeEmitterSpec(mode, axisPair, supportsDirectRx)); + } +} + +std::set +allowedGatesForEmitter(const SingleQubitEmitterSpec& emitter) { + switch (emitter.mode) { + case SingleQubitMode::ZSXX: { + std::set gates{NativeGateKind::X, NativeGateKind::Sx, + NativeGateKind::Rz}; + if (emitter.supportsDirectRx) { + gates.insert(NativeGateKind::Rx); + } + return gates; + } + case SingleQubitMode::U3: + return {NativeGateKind::U}; + case SingleQubitMode::R: + return {NativeGateKind::R}; + case SingleQubitMode::AxisPair: + switch (emitter.axisPair) { + case AxisPair::RxRz: + return {NativeGateKind::Rx, NativeGateKind::Rz}; + case AxisPair::RxRy: + return {NativeGateKind::Rx, NativeGateKind::Ry}; + case AxisPair::RyRz: + return {NativeGateKind::Ry, NativeGateKind::Rz}; + } + break; + } + llvm_unreachable("unknown single-qubit mode"); +} + +std::set allowedGatesForEntangler(EntanglerBasis entangler) { + switch (entangler) { + case EntanglerBasis::None: + return {}; + case EntanglerBasis::Cx: + return {NativeGateKind::Cx}; + case EntanglerBasis::Cz: + return {NativeGateKind::Cz}; + } + llvm_unreachable("unknown entangler basis"); +} + +void populateAllowedGates(NativeProfileSpec& spec) { + spec.allowedGates.clear(); + for (const auto& emitter : spec.singleQubitEmitters) { + const auto allowed = allowedGatesForEmitter(emitter); + spec.allowedGates.insert(allowed.begin(), allowed.end()); + } + for (const auto entangler : spec.entanglerBases) { + const auto allowed = allowedGatesForEntangler(entangler); + spec.allowedGates.insert(allowed.begin(), allowed.end()); + } + if (spec.allowRzz) { + spec.allowedGates.insert(NativeGateKind::Rzz); + } +} + +} // namespace + +llvm::SmallVector +getEulerBasesForAxisPair(AxisPair axisPair) { + switch (axisPair) { + case AxisPair::RxRz: + return {decomposition::EulerBasis::XZX}; + case AxisPair::RxRy: + return {decomposition::EulerBasis::XYX}; + case AxisPair::RyRz: + return {decomposition::EulerBasis::ZYZ}; + } + llvm_unreachable("unknown axis pair"); +} + +std::optional +resolveNativeGatesSpec(llvm::StringRef nativeGates) { + const auto gates = parseGateSet(nativeGates); + if (!gates || gates->empty()) { + return std::nullopt; + } + const auto has = [&](NativeGateKind kind) { return gates->contains(kind); }; + + NativeProfileSpec spec; + + // Derive all legal single-qubit emitters from the declared menu. + if (has(NativeGateKind::U)) { + addEmitterIfAbsent(spec.singleQubitEmitters, SingleQubitMode::U3); + } + const bool hasXSxRz = has(NativeGateKind::X) && has(NativeGateKind::Sx) && + has(NativeGateKind::Rz); + if (hasXSxRz) { + addEmitterIfAbsent(spec.singleQubitEmitters, SingleQubitMode::ZSXX, + AxisPair::RxRz, + /*supportsDirectRx=*/has(NativeGateKind::Rx)); + } + if (has(NativeGateKind::R)) { + addEmitterIfAbsent(spec.singleQubitEmitters, SingleQubitMode::R); + } + struct AxisPairRule { + AxisPair axis; + NativeGateKind left; + NativeGateKind right; + }; + for (const auto& rule : { + AxisPairRule{.axis = AxisPair::RxRz, + .left = NativeGateKind::Rx, + .right = NativeGateKind::Rz}, + AxisPairRule{.axis = AxisPair::RxRy, + .left = NativeGateKind::Rx, + .right = NativeGateKind::Ry}, + AxisPairRule{.axis = AxisPair::RyRz, + .left = NativeGateKind::Ry, + .right = NativeGateKind::Rz}, + }) { + if (has(rule.left) && has(rule.right)) { + addEmitterIfAbsent(spec.singleQubitEmitters, SingleQubitMode::AxisPair, + rule.axis); + } + } + if (spec.singleQubitEmitters.empty()) { + return std::nullopt; + } + + if (has(NativeGateKind::Cx)) { + spec.entanglerBases.push_back(EntanglerBasis::Cx); + } + if (has(NativeGateKind::Cz)) { + spec.entanglerBases.push_back(EntanglerBasis::Cz); + } + spec.allowRzz = has(NativeGateKind::Rzz); + + populateAllowedGates(spec); + return spec; +} + +} // namespace mlir::qco::native_synth diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp new file mode 100644 index 0000000000..ad1a87b0dc --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp @@ -0,0 +1,630 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Scoring.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h" +#include "mlir/Dialect/QCO/Transforms/Passes.h" +#include "mlir/Dialect/Utils/Utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace mlir::qco { +#define GEN_PASS_DEF_NATIVEGATESYNTHESISPASS +#include "mlir/Dialect/QCO/Transforms/Passes.h.inc" +} // namespace mlir::qco + +namespace mlir::qco { + +using native_synth::allowsSingleQubitOp; +using native_synth::areValidScoreWeights; +using native_synth::CandidateClass; +using native_synth::collectSingleQubitCandidates; +using native_synth::collectTwoQubitBasisCandidates; +using native_synth::collectTwoQubitBasisCandidatesFromMatrix; +using native_synth::collectUnitaryOpsInPreOrder; +using native_synth::computeSynthesizedSingleQubitLength; +using native_synth::decomposeToAxisPair; +using native_synth::decomposeToR; +using native_synth::decomposeToU3; +using native_synth::decomposeToZSXX; +using native_synth::emitSynthesizedSingleQubitFromMatrix; +using native_synth::emitTwoQubitGateSequence; +using native_synth::getBlockTwoQubitMatrix; +using native_synth::NativeGateKind; +using native_synth::NativeProfileSpec; +using native_synth::resolveNativeGatesSpec; +using native_synth::rewriteXXPlusMinusYYViaRxxRyy; +using native_synth::ScoreWeights; +using native_synth::selectBestCandidate; +using native_synth::SingleQubitEmitterSpec; +using native_synth::SingleQubitMode; +using native_synth::SingleQubitRewritePlan; +using native_synth::SingleQubitRewriteStrategy; +using native_synth::SynthesisCandidate; +using native_synth::TwoQubitWindowConsolidator; +using native_synth::usesCxEntangler; +using native_synth::usesCzEntangler; +using native_synth::xxPlusMinusYyRzzRewriteScoringMetrics; + +namespace { + +/// Adjacent single-qubit unitaries on one wire considered for fusion. +struct OneQubitRun { + llvm::SmallVector ops; +}; + +/// If profitable, replace the run with one synthesized single-qubit op. +bool maybeFuseRun(IRRewriter& rewriter, OneQubitRun& run, + const NativeProfileSpec& spec) { + Eigen::Matrix2cd fused = Eigen::Matrix2cd::Identity(); + for (UnitaryOpInterface u : run.ops) { + Eigen::Matrix2cd m; + if (!u.getUnitaryMatrix2x2(m)) { + return false; + } + fused = m * fused; + } + + const bool anyNonNative = llvm::any_of(run.ops, [&](UnitaryOpInterface u) { + return !allowsSingleQubitOp(u, spec); + }); + + assert(!spec.singleQubitEmitters.empty() && "expected at least one emitter"); + const auto& emitter = spec.singleQubitEmitters.front(); + + // Fully native runs: fuse only if the emitter shortens the chain. + if (!anyNonNative && + computeSynthesizedSingleQubitLength(fused, emitter) >= run.ops.size()) { + return false; + } + + Operation* firstOp = run.ops.front().getOperation(); + const Value inQubit = run.ops.front().getInputQubit(0); + const Value outQubit = run.ops.back().getOutputQubit(0); + + rewriter.setInsertionPoint(firstOp); + Value replacement = emitSynthesizedSingleQubitFromMatrix( + rewriter, firstOp->getLoc(), inQubit, fused, emitter); + if (!replacement) { + return false; + } + rewriter.replaceAllUsesWith(outQubit, replacement); + for (auto& op : std::ranges::reverse_view(run.ops)) { + Operation* toErase = op.getOperation(); + rewriter.eraseOp(toErase); + } + return true; +} + +/// Single-qubit op eligible for fusion (constant `2×2`, not under `ctrl`). +UnitaryOpInterface fusibleSingleQubitOp(Operation* op) { + auto unitary = dyn_cast(op); + if (!unitary || !unitary.isSingleQubit()) { + return {}; + } + if (isa(op)) { + return {}; + } + if (isa_and_present(op->getParentOp())) { + return {}; + } + Eigen::Matrix2cd matrix; + if (!unitary.getUnitaryMatrix2x2(matrix)) { + return {}; + } + return unitary; +} + +Value applyDirectSingleQubitLowering(IRRewriter& rewriter, Operation* op, + Value in, + const SingleQubitEmitterSpec& emitter) { + switch (emitter.mode) { + case SingleQubitMode::ZSXX: + return decomposeToZSXX(rewriter, op, in, emitter.supportsDirectRx); + case SingleQubitMode::U3: + return decomposeToU3(rewriter, op, in); + case SingleQubitMode::R: + return decomposeToR(rewriter, op, in); + case SingleQubitMode::AxisPair: + return decomposeToAxisPair(rewriter, op, in, emitter.axisPair); + } + llvm_unreachable("unknown SingleQubitMode"); +} + +/// Lowers unitary QCO ops to a comma-separated native gate menu (single-qubit +/// fuse, two-qubit windows, synthesis sweeps, seam single-qubit fuse, `rz` +/// through `ctrl` controls, another single-qubit fuse, optional cleanup sweeps; +/// fails if anything remains off-menu). +struct NativeGateSynthesisPass + : impl::NativeGateSynthesisPassBase { + NativeGateSynthesisPass() = default; + explicit NativeGateSynthesisPass( + const NativeGateSynthesisPassOptions& options) + : NativeGateSynthesisPassBase(options) {} + explicit NativeGateSynthesisPass(const NativeGateSynthesisOptions& options) { + nativeGates = options.nativeGates; + scoreWeightTwoQ = options.scoreWeightTwoQ; + scoreWeightOneQ = options.scoreWeightOneQ; + scoreWeightDepth = options.scoreWeightDepth; + } + + void runOnOperation() override { + const ScoreWeights weights{.twoQ = scoreWeightTwoQ, + .oneQ = scoreWeightOneQ, + .depth = scoreWeightDepth}; + if (!areValidScoreWeights(weights)) { + getOperation().emitError() + << "invalid native synthesis score weights (twoq=" << scoreWeightTwoQ + << ", oneq=" << scoreWeightOneQ << ", depth=" << scoreWeightDepth + << ")"; + signalPassFailure(); + return; + } + + // Empty native-gates string: no-op. + if (llvm::StringRef(nativeGates).trim().empty()) { + return; + } + auto specOpt = resolveNativeGatesSpec(nativeGates); + if (!specOpt) { + getOperation().emitError() + << "unsupported native gate menu (native-gates='" << nativeGates + << "')"; + signalPassFailure(); + return; + } + const auto& spec = *specOpt; + + IRRewriter rewriter(&getContext()); + + fuseOneQubitRuns(rewriter, spec); + consolidateTwoQubitBlocks(rewriter, spec, weights); + // Two-qubit lowering can emit off-menu single-qubit ops (e.g. `rx`/`ry`); + // repeat until clean or hit the sweep cap before seam / `rz` cleanup (those + // steps assume a mostly on-menu single-qubit surface for best fusion). + constexpr unsigned kMaxSynthesisSweeps = 4; + for (unsigned i = 0; i < kMaxSynthesisSweeps; ++i) { + if (failed(synthesizeRemainingOps(rewriter, spec, weights))) { + signalPassFailure(); + return; + } + if (!hasNonNativeSingleQubitOps(spec)) { + break; + } + } + if (hasNonNativeSingleQubitOps(spec)) { + getOperation().emitError() + << "native gate synthesis did not converge within " + << kMaxSynthesisSweeps + << " sweeps (single-qubit ops remain outside the native menu)"; + signalPassFailure(); + return; + } + // Fuse single-qubit seams between two-qubit blocks (`maybeFuseRun` cost + // gate). + fuseOneQubitRuns(rewriter, spec); + // Fuse `rz` through control wires of `ctrl` (diagonal control phase). + fuseRzAcrossCtrlControls(rewriter); + fuseOneQubitRuns(rewriter, spec); + // Re-check full menu (single-qubit ops, native `ctrl`, allowed bare `rzz`). + constexpr unsigned kPostMenuCleanupSweeps = 4; + unsigned postMenuSweepsRemaining = kPostMenuCleanupSweeps; + while (hasNonNativeMenuOps(spec) && postMenuSweepsRemaining-- > 0) { + if (failed(synthesizeRemainingOps(rewriter, spec, weights))) { + signalPassFailure(); + return; + } + fuseOneQubitRuns(rewriter, spec); + } + if (hasNonNativeMenuOps(spec)) { + getOperation().emitError() + << "native gate synthesis: operations remain outside the native menu " + "after final cleanup"; + signalPassFailure(); + return; + } + } + + /// `CtrlOp` is already on-menu when the body is `X`/`Z` and the profile + /// supplies `cx` / `cz` entanglers. + static bool ctrlMatchesNativeMenu(CtrlOp ctrl, + const NativeProfileSpec& spec) { + if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { + return false; + } + Operation* body = ctrl.getBodyUnitary().getOperation(); + const bool hasCX = isa(body); + const bool hasCZ = isa(body); + if (!hasCX && !hasCZ) { + return false; + } + return (usesCxEntangler(spec) && hasCX) || (usesCzEntangler(spec) && hasCZ); + } + + /// Bare two-qubit on-menu: `rzz` when the profile allows it. + static bool bareTwoQubitMatchesNativeMenu(Operation* op, + const NativeProfileSpec& spec) { + return isa(op) && spec.allowRzz && + spec.allowedGates.contains(NativeGateKind::Rzz); + } + + /// True if any unitary is outside `spec` (single-qubit, `ctrl`, or bare + /// `rzz`). + bool hasNonNativeMenuOps(const NativeProfileSpec& spec) { + const mlir::WalkResult walkResult = + getOperation()->walk([&](Operation* op) { + if (isa(op)) { + return mlir::WalkResult::advance(); + } + if (isa_and_present(op->getParentOp())) { + return mlir::WalkResult::advance(); + } + if (auto ctrl = dyn_cast(op)) { + if (!ctrlMatchesNativeMenu(ctrl, spec)) { + return mlir::WalkResult::interrupt(); + } + return mlir::WalkResult::advance(); + } + auto unitary = dyn_cast(op); + if (!unitary) { + return mlir::WalkResult::advance(); + } + if (unitary.isSingleQubit()) { + if (!allowsSingleQubitOp(unitary, spec)) { + return mlir::WalkResult::interrupt(); + } + return mlir::WalkResult::advance(); + } + if (unitary.isTwoQubit()) { + if (!bareTwoQubitMatchesNativeMenu(op, spec)) { + return mlir::WalkResult::interrupt(); + } + return mlir::WalkResult::advance(); + } + return mlir::WalkResult::interrupt(); + }); + return walkResult.wasInterrupted(); + } + + /// Any off-menu single-qubit unitary (ignores `ctrl` region bodies). + bool hasNonNativeSingleQubitOps(const NativeProfileSpec& spec) { + const mlir::WalkResult walkResult = + getOperation()->walk([&](Operation* op) { + if (isa(op)) { + return mlir::WalkResult::advance(); + } + if (isa_and_present(op->getParentOp())) { + return mlir::WalkResult::advance(); + } + auto unitary = dyn_cast(op); + if (!unitary || !unitary.isSingleQubit()) { + return mlir::WalkResult::advance(); + } + if (!allowsSingleQubitOp(unitary, spec)) { + return mlir::WalkResult::interrupt(); + } + return mlir::WalkResult::advance(); + }); + return walkResult.wasInterrupted(); + } + +private: + /// Fuse adjacent single-qubit runs when the emitter wins on length or any op + /// is off-menu. + void fuseOneQubitRuns(IRRewriter& rewriter, const NativeProfileSpec& spec) { + llvm::SmallVector runs; + llvm::DenseMap tailOpToRun; + + getOperation()->walk([&](Operation* op) { + auto unitary = fusibleSingleQubitOp(op); + if (!unitary) { + return; + } + Value inQubit = unitary.getInputQubit(0); + Operation* defOp = inQubit.getDefiningOp(); + auto it = + (defOp != nullptr) ? tailOpToRun.find(defOp) : tailOpToRun.end(); + const bool canExtend = it != tailOpToRun.end() && inQubit.hasOneUse(); + if (canExtend) { + const size_t runIdx = it->second; + runs[runIdx].ops.push_back(unitary); + tailOpToRun.erase(it); + tailOpToRun[op] = runIdx; + } else { + runs.push_back(OneQubitRun{}); + runs.back().ops.push_back(unitary); + tailOpToRun[op] = runs.size() - 1; + } + }); + + for (auto& run : runs) { + if (run.ops.size() < 2) { + continue; + } + (void)maybeFuseRun(rewriter, run, spec); + } + } + + /// If `rz1` can reach another `rz` through at least one `ctrl` control hop, + /// merge angles into `rz1` and erase the partner. + static bool tryFuseRzForwardThroughCtrls(IRRewriter& rewriter, RZOp rz1) { + Value v = rz1.getQubitOut(); + RZOp partner; + unsigned hops = 0; + while (v.hasOneUse()) { + Operation* user = *v.getUsers().begin(); + if (auto rz2 = dyn_cast(user); rz2 && rz2.getQubitIn() == v) { + partner = rz2; + break; + } + auto ctrl = dyn_cast(user); + if (!ctrl) { + return false; + } + // Only control wires commute through `ctrl` here. + if (!llvm::is_contained(ctrl.getControlsIn(), v)) { + return false; + } + v = ctrl.getOutputForInput(v); + ++hops; + } + if (!partner || hops == 0) { + return false; + } + + // Fold angles; use a scalar constant when both inputs are constant. + const Location loc = rz1.getLoc(); + const Value theta1 = rz1.getTheta(); + const Value theta2 = partner.getTheta(); + const auto c1 = mlir::utils::valueToDouble(theta1); + const auto c2 = mlir::utils::valueToDouble(theta2); + rewriter.setInsertionPoint(rz1); + Value newTheta; + if (c1.has_value() && c2.has_value()) { + newTheta = mlir::utils::constantFromScalar(rewriter, loc, *c1 + *c2); + } else { + newTheta = arith::AddFOp::create(rewriter, loc, theta1, theta2); + } + rz1.getThetaMutable().assign(newTheta); + rewriter.replaceOp(partner, partner.getQubitIn()); + return true; + } + + /// Fixpoint: merge `rz` through `ctrl` control chains into the next `rz`. + void fuseRzAcrossCtrlControls(IRRewriter& rewriter) { + bool changed = true; + while (changed) { + changed = false; + llvm::SmallVector rzOps; + getOperation()->walk([&](RZOp rz) { rzOps.push_back(rz); }); + for (RZOp rz : rzOps) { + if (rz->getBlock() == nullptr) { + continue; + } + if (tryFuseRzForwardThroughCtrls(rewriter, rz)) { + changed = true; + } + } + } + } + + /// Two-qubit windows with absorbed single-qubit ops: replace when a cheaper + /// native sequence exists. + void consolidateTwoQubitBlocks(IRRewriter& rewriter, + const NativeProfileSpec& spec, + const ScoreWeights& weights) { + std::vector ops; + collectUnitaryOpsInPreOrder(getOperation(), ops); + TwoQubitWindowConsolidator consolidator; + for (Operation* op : ops) { + consolidator.process(op, spec); + } + consolidator.materialize(rewriter, spec, weights); + } + + /// Lower one single-qubit rewrite plan; null `Value` on failure. + static Value emitSingleQCandidate(IRRewriter& rewriter, Operation* op, + UnitaryOpInterface unitary, + const SingleQubitRewritePlan& plan) { + const Value in = unitary.getInputQubit(0); + if (plan.strategy == SingleQubitRewriteStrategy::Direct) { + return applyDirectSingleQubitLowering(rewriter, op, in, plan.emitter); + } + Eigen::Matrix2cd matrix; + if (!unitary.isSingleQubit() || !unitary.getUnitaryMatrix2x2(matrix)) { + return {}; + } + return emitSynthesizedSingleQubitFromMatrix(rewriter, op->getLoc(), in, + matrix, plan.emitter); + } + + LogicalResult synthesizeRemainingOps(IRRewriter& rewriter, + const NativeProfileSpec& spec, + const ScoreWeights& weights) { + std::vector ops; + collectUnitaryOpsInPreOrder(getOperation(), ops); + + for (Operation* op : ops) { + // Pointers were collected before this loop; erased ops must be skipped + // (`getBlock() == nullptr`). Do not rely on pointer identity alone. + if (op->getBlock() == nullptr) { + continue; + } + // Inner `CtrlOp` bodies are handled on the `CtrlOp` itself. + if (isa_and_present(op->getParentOp())) { + continue; + } + if (isa(op)) { + continue; + } + auto unitary = dyn_cast(op); + if (!unitary) { + continue; + } + + if (unitary.isSingleQubit()) { + if (!allowsSingleQubitOp(unitary, spec)) { + if (failed( + rewriteSingleQubit(rewriter, op, unitary, spec, weights))) { + return failure(); + } + } + continue; + } + + if (auto ctrl = dyn_cast(op)) { + if (failed(rewriteControlled(rewriter, ctrl, spec, weights))) { + return failure(); + } + continue; + } + + if (unitary.isTwoQubit()) { + if (failed(rewriteTwoQubit(rewriter, op, unitary, spec, weights))) { + return failure(); + } + continue; + } + } + return success(); + } + + static LogicalResult rewriteSingleQubit(IRRewriter& rewriter, Operation* op, + UnitaryOpInterface unitary, + const NativeProfileSpec& spec, + const ScoreWeights& weights) { + rewriter.setInsertionPoint(op); + const auto candidates = collectSingleQubitCandidates(unitary, spec); + const auto* best = selectBestCandidate(llvm::ArrayRef(candidates), weights); + const Value replaced = + best != nullptr + ? emitSingleQCandidate(rewriter, op, unitary, best->payload) + : Value{}; + if (!replaced) { + op->emitError("single-qubit operation not in selected native profile"); + return failure(); + } + rewriter.replaceOp(op, replaced); + return success(); + } + + static LogicalResult rewriteControlled(IRRewriter& rewriter, CtrlOp ctrl, + const NativeProfileSpec& spec, + const ScoreWeights& weights) { + if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { + ctrl.emitError("native synthesis currently only supports 1-control " + "1-target controlled gates"); + return failure(); + } + auto* body = ctrl.getBodyUnitary().getOperation(); + const bool hasCX = isa(body); + const bool hasCZ = isa(body); + if (!hasCX && !hasCZ) { + ctrl.emitError("native synthesis currently only supports CX/CZ bodies"); + return failure(); + } + if ((usesCxEntangler(spec) && hasCX) || (usesCzEntangler(spec) && hasCZ)) { + return success(); + } + // Otherwise treat as a generic `4×4` (Weyl + basis decomposer + scorer). + Eigen::Matrix4cd matrix; + if (!getBlockTwoQubitMatrix(ctrl.getOperation(), matrix)) { + ctrl.emitError("failed to compute 4x4 matrix for CtrlOp"); + return failure(); + } + native_synth::normalizeToSU4(matrix); // SU(4) convention for Weyl + + const auto candidates = + collectTwoQubitBasisCandidatesFromMatrix(matrix, spec); + if (const auto* best = + selectBestCandidate(llvm::ArrayRef(candidates), weights)) { + rewriter.setInsertionPoint(ctrl); + if (succeeded(emitTwoQubitGateSequence( + rewriter, ctrl.getOperation(), ctrl.getInputControl(0), + ctrl.getInputTarget(0), best->payload.sequence))) { + return success(); + } + } + ctrl.emitError("controlled gate not allowed by selected profile"); + return failure(); + } + + static LogicalResult rewriteTwoQubit(IRRewriter& rewriter, Operation* op, + UnitaryOpInterface unitary, + const NativeProfileSpec& spec, + const ScoreWeights& weights) { + if (spec.allowRzz && isa(op)) { + return success(); + } + if (spec.allowRzz && (isa(op) || isa(op))) { + llvm::SmallVector> candidates; + candidates.push_back(SynthesisCandidate{ + .candidateClass = CandidateClass::XxPlusMinusViaRzz, + .metrics = xxPlusMinusYyRzzRewriteScoringMetrics(), + .enumerationIndex = 0, + .payload = true, + }); + if (selectBestCandidate(llvm::ArrayRef(candidates), weights) != nullptr) { + rewriter.setInsertionPoint(op); + if (succeeded(rewriteXXPlusMinusYYViaRxxRyy(rewriter, op))) { + return success(); + } + } + } + if (!spec.entanglerBases.empty()) { + const auto candidates = collectTwoQubitBasisCandidates(unitary, spec); + if (const auto* best = + selectBestCandidate(llvm::ArrayRef(candidates), weights)) { + rewriter.setInsertionPoint(op); + if (succeeded(emitTwoQubitGateSequence( + rewriter, op, unitary.getInputQubit(0), + unitary.getInputQubit(1), best->payload.sequence))) { + return success(); + } + } + } + op->emitError("unsupported two-qubit operation for selected profile"); + return failure(); + } +}; + +} // namespace + +std::unique_ptr +createNativeGateSynthesisPass(const NativeGateSynthesisOptions& options) { + return std::make_unique(options); +} + +} // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp new file mode 100644 index 0000000000..8408617e5f --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp @@ -0,0 +1,263 @@ +/* + * 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/NativeSynthesis/PassTwoQubitWindows.h" + +#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Scoring.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h" + +#include +#include + +#include + +namespace mlir::qco::native_synth { +namespace { + +bool isNativeTwoQubitOp(Operation* op, const NativeProfileSpec& spec) { + if (auto ctrl = dyn_cast(op)) { + if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { + return false; + } + auto* body = ctrl.getBodyUnitary().getOperation(); + if (isa(body)) { + return usesCxEntangler(spec); + } + if (isa(body)) { + return usesCzEntangler(spec); + } + return false; + } + return spec.allowRzz && isa(op); +} + +bool shouldApplyBlockReplacement(const TwoQubitBlock& block, + const CandidateMetrics& best) { + if (block.anyNonNative) { + return true; + } + const bool shorterTwoQ = best.numTwoQ < block.numTwoQ; + const bool sameTwoQ = best.numTwoQ == block.numTwoQ; + const bool shorterOneQ = best.numOneQ < block.numOneQ; + return shorterTwoQ || (sameTwoQ && shorterOneQ); +} + +} // namespace + +static void materializeSingleTwoQubitBlock( + IRRewriter& rewriter, const TwoQubitBlock& block, + const SynthesisCandidate& best) { + Operation* firstOp = block.ops.front(); + auto firstUnitary = cast(firstOp); + const Value inA = firstUnitary.getInputQubit(0); + const Value inB = firstUnitary.getInputQubit(1); + const Value outA = block.wireA; + const Value outB = block.wireB; + + rewriter.setInsertionPoint(firstOp); + Value newA; + Value newB; + if (failed(emitTwoQubitGateSequenceAtLoc(rewriter, firstOp->getLoc(), inA, + inB, best.payload.sequence, newA, + newB))) { + firstOp->emitError("failed to emit synthesized two-qubit gate sequence"); + return; + } + if (best.payload.sequence.hasGlobalPhase()) { + emitGPhaseIfNonTrivial(rewriter, firstOp->getLoc(), + best.payload.sequence.globalPhase); + } + rewriter.replaceAllUsesWith(outA, newA); + rewriter.replaceAllUsesWith(outB, newB); + for (auto* toErase : std::ranges::reverse_view(block.ops)) { + rewriter.eraseOp(toErase); + } +} + +void collectUnitaryOpsInPreOrder(Operation* root, + std::vector& ops) { + root->walk([&](Operation* op) { + if (isa(op)) { + ops.push_back(op); + } + }); +} + +void TwoQubitWindowConsolidator::closeBlock(size_t idx) { + auto& block = blocks[idx]; + if (!block.open) { + return; + } + block.open = false; + wireToBlock.erase(block.wireA); + wireToBlock.erase(block.wireB); +} + +void TwoQubitWindowConsolidator::closeBlockOnWire(Value v) { + if (auto it = wireToBlock.find(v); it != wireToBlock.end()) { + closeBlock(it->second); + } +} + +void TwoQubitWindowConsolidator::process(Operation* op, + const NativeProfileSpec& spec) { + if (isa_and_present(op->getParentOp())) { + return; + } + auto unitary = dyn_cast(op); + if (!unitary) { + return; + } + if (isa(op)) { + for (Value v : op->getOperands()) { + closeBlockOnWire(v); + } + return; + } + + if (unitary.isTwoQubit()) { + Eigen::Matrix4cd opMatrix; + if (!getBlockTwoQubitMatrix(op, opMatrix)) { + closeBlockOnWire(unitary.getInputQubit(0)); + closeBlockOnWire(unitary.getInputQubit(1)); + return; + } + const Value v0 = unitary.getInputQubit(0); + const Value v1 = unitary.getInputQubit(1); + auto it0 = wireToBlock.find(v0); + auto it1 = wireToBlock.find(v1); + const bool tracked0 = it0 != wireToBlock.end(); + const bool tracked1 = it1 != wireToBlock.end(); + const bool sameBlock = tracked0 && tracked1 && it0->second == it1->second; + const bool singleUse = v0.hasOneUse() && v1.hasOneUse(); + + if (sameBlock && singleUse) { + const size_t idx = it0->second; + auto& block = blocks[idx]; + llvm::SmallVector ids; + if (v0 == block.wireA && v1 == block.wireB) { + ids = {0, 1}; + } else if (v0 == block.wireB && v1 == block.wireA) { + ids = {1, 0}; + } else { + closeBlock(idx); + return; + } + block.accum = decomposition::fixTwoQubitMatrixQubitOrder(opMatrix, ids) * + block.accum; + block.ops.push_back(op); + ++block.numTwoQ; + if (!isNativeTwoQubitOp(op, spec)) { + block.anyNonNative = true; + } + wireToBlock.erase(it0); + wireToBlock.erase(it1); + Value newA; + Value newB; + if (v0 == block.wireA) { + newA = unitary.getOutputQubit(0); + newB = unitary.getOutputQubit(1); + } else { + newA = unitary.getOutputQubit(1); + newB = unitary.getOutputQubit(0); + } + block.wireA = newA; + block.wireB = newB; + wireToBlock[newA] = idx; + wireToBlock[newB] = idx; + return; + } + + if (tracked0) { + closeBlock(it0->second); + } + if (tracked1 && (!tracked0 || it0->second != it1->second)) { + closeBlock(it1->second); + } + TwoQubitBlock nb; + nb.wireA = unitary.getOutputQubit(0); + nb.wireB = unitary.getOutputQubit(1); + nb.ops.push_back(op); + nb.numTwoQ = 1; + nb.accum = opMatrix; + nb.anyNonNative = !isNativeTwoQubitOp(op, spec); + const size_t idx = blocks.size(); + blocks.push_back(std::move(nb)); + wireToBlock[blocks[idx].wireA] = idx; + wireToBlock[blocks[idx].wireB] = idx; + return; + } + + if (unitary.isSingleQubit()) { + const Value v = unitary.getInputQubit(0); + auto it = wireToBlock.find(v); + if (it == wireToBlock.end()) { + return; + } + const size_t idx = it->second; + auto& block = blocks[idx]; + Eigen::Matrix2cd m; + if (!unitary.getUnitaryMatrix2x2(m) || !v.hasOneUse()) { + closeBlock(idx); + return; + } + const auto pad = (v == block.wireA) + ? decomposition::expandToTwoQubits(m, 0) + : decomposition::expandToTwoQubits(m, 1); + block.accum = pad * block.accum; + block.ops.push_back(op); + ++block.numOneQ; + if (!allowsSingleQubitOp(unitary, spec)) { + block.anyNonNative = true; + } + wireToBlock.erase(it); + if (v == block.wireA) { + block.wireA = unitary.getOutputQubit(0); + wireToBlock[block.wireA] = idx; + } else { + block.wireB = unitary.getOutputQubit(0); + wireToBlock[block.wireB] = idx; + } + return; + } + + for (Value v : op->getOperands()) { + closeBlockOnWire(v); + } +} + +void TwoQubitWindowConsolidator::materialize(IRRewriter& rewriter, + const NativeProfileSpec& spec, + const ScoreWeights& weights) { + for (const auto& block : blocks) { + if (block.ops.size() < 2) { + continue; + } + // Leave `block.accum` unnormalized: Weyl keeps stripped SU(4) phase in + // the candidate sequence's `globalPhase`. + const auto candidates = + collectTwoQubitBasisCandidatesFromMatrix(block.accum, spec); + const auto* best = selectBestCandidate(llvm::ArrayRef(candidates), weights); + if (best == nullptr) { + continue; + } + if (!shouldApplyBlockReplacement(block, best->metrics)) { + continue; + } + materializeSingleTwoQubitBlock(rewriter, block, *best); + } +} + +} // namespace mlir::qco::native_synth diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Policy.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Policy.cpp new file mode 100644 index 0000000000..878e011369 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Policy.cpp @@ -0,0 +1,201 @@ +/* + * 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/NativeSynthesis/Policy.h" + +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h" + +#include +#include +#include + +#include +#include + +namespace mlir::qco::native_synth { + +bool areValidScoreWeights(const ScoreWeights& weights) { + return std::isfinite(weights.twoQ) && std::isfinite(weights.oneQ) && + std::isfinite(weights.depth) && weights.twoQ >= 0.0 && + weights.oneQ >= 0.0 && weights.depth >= 0.0; +} + +bool usesCxEntangler(const NativeProfileSpec& spec) { + return llvm::is_contained(spec.entanglerBases, EntanglerBasis::Cx); +} + +bool usesCzEntangler(const NativeProfileSpec& spec) { + return llvm::is_contained(spec.entanglerBases, EntanglerBasis::Cz); +} + +namespace { +/// Map a single-qubit `UnitaryOpInterface` op to the `NativeGateKind` that +/// must appear in the menu for the op to be a no-op. Two-qubit kinds are +/// never valid here and therefore not returned. +std::optional singleQubitNativeGateKind(UnitaryOpInterface op) { + Operation* raw = op.getOperation(); + if (isa(raw)) { + return NativeGateKind::U; + } + if (isa(raw)) { + return NativeGateKind::X; + } + if (isa(raw)) { + return NativeGateKind::Sx; + } + if (isa(raw)) { + // `p` is a Z-rotation primitive for menu purposes. + return NativeGateKind::Rz; + } + if (isa(raw)) { + return NativeGateKind::Rx; + } + if (isa(raw)) { + return NativeGateKind::Ry; + } + if (isa(raw)) { + return NativeGateKind::R; + } + return std::nullopt; +} +} // namespace + +bool allowsSingleQubitOp(UnitaryOpInterface op, const NativeProfileSpec& spec) { + if (isa(op.getOperation())) { + return true; + } + const auto gate = singleQubitNativeGateKind(op); + return gate && spec.allowedGates.contains(*gate); +} + +CandidateMetrics +computeGateSequenceMetrics(const decomposition::QubitGateSequence& seq) { + CandidateMetrics metrics; + llvm::SmallVector qubitDepths(2, 0); + for (const auto& gate : seq.gates) { + if (gate.qubitId.size() == 2) { + ++metrics.numTwoQ; + const auto gateDepth = std::max(qubitDepths[0], qubitDepths[1]) + 1; + qubitDepths[0] = qubitDepths[1] = gateDepth; + metrics.depth = std::max(metrics.depth, gateDepth); + } else if (gate.qubitId.size() == 1) { + ++metrics.numOneQ; + const unsigned q = gate.qubitId[0]; + if (q >= qubitDepths.size()) { + qubitDepths.resize(q + 1, 0); + } + const auto gateDepth = qubitDepths[q] + 1; + qubitDepths[q] = gateDepth; + metrics.depth = std::max(metrics.depth, gateDepth); + } + } + return metrics; +} + +/// True when `decomposeTo*` should run instead of folding to a constant `2×2` +/// matrix: trivial `Id`/`P`, dynamic-angle ops the matrix path cannot close +/// over, and (for ZSXX with direct Rx) `Rx`/`Ry`/`R`. Static angles still use +/// matrix + Euler. +bool canDirectlyDecomposeToZSXX(Operation* op, bool supportsDirectRx) { + if (isa(op)) { + return true; + } + return supportsDirectRx && isa(op); +} + +bool canDirectlyDecomposeToU3(Operation* op) { + return isa(op); +} + +bool canDirectlyDecomposeToR(Operation* op) { + return isa(op); +} + +bool canDirectlyDecomposeToAxisPair(Operation* op, AxisPair axisPair) { + if (isa(op)) { + return true; + } + switch (axisPair) { + case AxisPair::RxRz: + // `p` on an Rx/Rz axis pair folds directly to `rz(theta)`. + return isa(op); + case AxisPair::RxRy: + // No cheap symbolic lowering of `p` without `rz` available. + return isa(op); + case AxisPair::RyRz: + return isa(op); + } + llvm_unreachable("unknown axis pair"); +} + +CandidateMetrics +estimateDirectSingleQubitMetrics(Operation* op, + const SingleQubitEmitterSpec& emitter) { + if (isa(op)) { + return {}; + } + // ZSXX + direct Rx: `ry`/`r` use a three-gate `rz * rx * rz` sandwich; other + // direct paths emit a single native op. + const bool threeGate = emitter.mode == SingleQubitMode::ZSXX && + emitter.supportsDirectRx && isa(op); + const unsigned count = threeGate ? 3U : 1U; + return {.numOneQ = count, .numTwoQ = 0, .depth = count}; +} + +std::optional +estimateMatrixSingleQubitMetrics(UnitaryOpInterface unitary, + const SingleQubitEmitterSpec& emitter) { + if (!unitary.isSingleQubit()) { + return std::nullopt; + } + Eigen::Matrix2cd matrix; + if (!unitary.getUnitaryMatrix2x2(matrix)) { + return std::nullopt; + } + + const auto countNonIdentity = + [](const decomposition::QubitGateSequence& seq) { + CandidateMetrics metrics; + for (const auto& gate : seq.gates) { + if (gate.type != decomposition::GateKind::I) { + ++metrics.numOneQ; + } + } + metrics.depth = metrics.numOneQ; + return metrics; + }; + + switch (emitter.mode) { + case SingleQubitMode::ZSXX: + return computeGateSequenceMetrics( + decomposition::EulerDecomposition::generateCircuit( + decomposition::EulerBasis::ZSXX, matrix, /*simplify=*/true, + std::nullopt)); + case SingleQubitMode::U3: + return CandidateMetrics{.numOneQ = 1, .numTwoQ = 0, .depth = 1}; + case SingleQubitMode::R: + return countNonIdentity(decomposition::EulerDecomposition::generateCircuit( + decomposition::EulerBasis::XYX, matrix, /*simplify=*/true, + std::nullopt)); + case SingleQubitMode::AxisPair: { + const auto bases = getEulerBasesForAxisPair(emitter.axisPair); + if (bases.empty()) { + return std::nullopt; + } + return countNonIdentity(decomposition::EulerDecomposition::generateCircuit( + bases.front(), matrix, /*simplify=*/true, std::nullopt)); + } + } + llvm_unreachable("unknown single-qubit mode"); +} + +} // namespace mlir::qco::native_synth diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp new file mode 100644 index 0000000000..a1ce820c8c --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp @@ -0,0 +1,384 @@ +/* + * 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/NativeSynthesis/SingleQubit.h" + +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h" + +#include +#include + +#include +#include +#include +#include + +namespace mlir::qco::native_synth { +namespace { + +constexpr double PI = std::numbers::pi; +constexpr double HALF_PI = PI / 2.0; + +/// Small convenience wrapper to avoid passing rewriter/loc everywhere. +struct SingleQubitEmitter { + IRRewriter* rewriter; + Location loc; + + [[nodiscard]] Value constF(double v) const { + return createF64Const(*rewriter, loc, v); + } + + [[nodiscard]] Value rx(Value q, double theta) const { + return RXOp::create(*rewriter, loc, q, constF(theta)).getOutputQubit(0); + } + [[nodiscard]] Value rx(Value q, Value theta) const { + return RXOp::create(*rewriter, loc, q, theta).getOutputQubit(0); + } + [[nodiscard]] Value ry(Value q, double theta) const { + return RYOp::create(*rewriter, loc, q, constF(theta)).getOutputQubit(0); + } + [[nodiscard]] Value ry(Value q, Value theta) const { + return RYOp::create(*rewriter, loc, q, theta).getOutputQubit(0); + } + [[nodiscard]] Value rz(Value q, double theta) const { + return RZOp::create(*rewriter, loc, q, constF(theta)).getOutputQubit(0); + } + [[nodiscard]] Value rz(Value q, Value theta) const { + return RZOp::create(*rewriter, loc, q, theta).getOutputQubit(0); + } + [[nodiscard]] Value sx(Value q) const { + return SXOp::create(*rewriter, loc, q).getOutputQubit(0); + } + [[nodiscard]] Value x(Value q) const { + return XOp::create(*rewriter, loc, q).getOutputQubit(0); + } + [[nodiscard]] Value r(Value q, double theta, double phi) const { + return ROp::create(*rewriter, loc, q, constF(theta), constF(phi)) + .getOutputQubit(0); + } + [[nodiscard]] Value r(Value q, Value theta, Value phi) const { + return ROp::create(*rewriter, loc, q, theta, phi).getOutputQubit(0); + } + [[nodiscard]] Value u(Value q, Value theta, Value phi, Value lambda) const { + return UOp::create(*rewriter, loc, q, theta, phi, lambda).getOutputQubit(0); + } + [[nodiscard]] Value u(Value q, double theta, double phi, + double lambda) const { + return u(q, constF(theta), constF(phi), constF(lambda)); + } +}; + +/// Materialize an `EulerBasis::ZSXX` decomposition (`rz` / `sx` / `x`) into +/// QCO ops. Returns null on unsupported abstract gate kinds. +Value emitEulerSequenceZsxx(SingleQubitEmitter e, Value q, + const decomposition::QubitGateSequence& seq) { + for (const auto& gate : seq.gates) { + switch (gate.type) { + case decomposition::GateKind::RZ: + if (gate.parameter.size() != 1) { + return {}; + } + q = e.rz(q, gate.parameter[0]); + break; + case decomposition::GateKind::SX: + q = e.sx(q); + break; + case decomposition::GateKind::X: + q = e.x(q); + break; + case decomposition::GateKind::I: + break; + default: + return {}; + } + } + return q; +} + +Value emitEulerSequenceR(SingleQubitEmitter e, Value q, + const decomposition::QubitGateSequence& seq) { + for (const auto& gate : seq.gates) { + switch (gate.type) { + case decomposition::GateKind::RX: + if (gate.parameter.size() != 1) { + return {}; + } + q = e.r(q, gate.parameter[0], 0.0); + break; + case decomposition::GateKind::RY: + if (gate.parameter.size() != 1) { + return {}; + } + q = e.r(q, gate.parameter[0], HALF_PI); + break; + case decomposition::GateKind::X: + q = e.r(q, PI, 0.0); + break; + case decomposition::GateKind::Y: + q = e.r(q, PI, HALF_PI); + break; + case decomposition::GateKind::I: + break; + default: + return {}; + } + } + return q; +} + +Value emitEulerSequenceAxisPair(SingleQubitEmitter e, Value q, AxisPair axis, + const decomposition::QubitGateSequence& seq) { + for (const auto& gate : seq.gates) { + switch (gate.type) { + case decomposition::GateKind::RX: + if (axis == AxisPair::RyRz || gate.parameter.size() != 1) { + return {}; + } + q = e.rx(q, gate.parameter[0]); + break; + case decomposition::GateKind::RY: + if (axis == AxisPair::RxRz || gate.parameter.size() != 1) { + return {}; + } + q = e.ry(q, gate.parameter[0]); + break; + case decomposition::GateKind::RZ: + if (axis == AxisPair::RxRy || gate.parameter.size() != 1) { + return {}; + } + q = e.rz(q, gate.parameter[0]); + break; + case decomposition::GateKind::X: + if (axis == AxisPair::RyRz) { + return {}; + } + q = e.rx(q, PI); + break; + case decomposition::GateKind::Y: + if (axis == AxisPair::RxRz) { + return {}; + } + q = e.ry(q, PI); + break; + case decomposition::GateKind::Z: + if (axis == AxisPair::RxRy) { + return {}; + } + q = e.rz(q, PI); + break; + case decomposition::GateKind::I: + break; + default: + return {}; + } + } + return q; +} + +decomposition::QubitGateSequence runEuler(decomposition::EulerBasis basis, + const Eigen::Matrix2cd& matrix) { + return decomposition::EulerDecomposition::generateCircuit( + basis, matrix, /*simplify=*/true, std::nullopt); +} + +} // namespace + +// Direct emitters only handle the gates listed in the matching +// `canDirectlyDecomposeTo*` predicate. Everything else is expected to reach the +// matrix-based Euler fallback, which produces an equivalent native sequence in +// the same basis. +Value decomposeToZSXX(IRRewriter& rewriter, Operation* op, Value inQubit, + bool supportsDirectRx) { + if (isa(op)) { + return inQubit; + } + SingleQubitEmitter e{.rewriter = &rewriter, .loc = op->getLoc()}; + if (auto p = dyn_cast(op)) { + return e.rz(inQubit, p.getTheta()); + } + if (!supportsDirectRx) { + return {}; + } + if (auto rx = dyn_cast(op)) { + return rx.getOutputQubit(0); + } + if (auto ry = dyn_cast(op)) { + return e.rz(e.rx(e.rz(inQubit, -HALF_PI), ry.getTheta()), HALF_PI); + } + if (auto r = dyn_cast(op)) { + auto negPhi = + arith::NegFOp::create(rewriter, op->getLoc(), r.getPhi()).getResult(); + return e.rz(e.rx(e.rz(inQubit, negPhi), r.getTheta()), r.getPhi()); + } + return {}; +} + +Value decomposeToU3(IRRewriter& rewriter, Operation* op, Value inQubit) { + SingleQubitEmitter e{.rewriter = &rewriter, .loc = op->getLoc()}; + if (isa(op)) { + return e.u(inQubit, 0.0, 0.0, 0.0); + } + if (auto u = dyn_cast(op)) { + return u.getOutputQubit(0); + } + if (auto rx = dyn_cast(op)) { + return e.u(inQubit, rx.getTheta(), e.constF(-HALF_PI), e.constF(HALF_PI)); + } + if (auto ry = dyn_cast(op)) { + return e.u(inQubit, ry.getTheta(), e.constF(0.0), e.constF(0.0)); + } + if (auto rz = dyn_cast(op)) { + return e.u(inQubit, e.constF(0.0), e.constF(0.0), rz.getTheta()); + } + if (auto p = dyn_cast(op)) { + return e.u(inQubit, e.constF(0.0), e.constF(0.0), p.getTheta()); + } + if (auto u2 = dyn_cast(op)) { + return e.u(inQubit, e.constF(HALF_PI), u2.getPhi(), u2.getLambda()); + } + if (auto r = dyn_cast(op)) { + auto loc = op->getLoc(); + auto phiMinus = + arith::AddFOp::create(rewriter, loc, r.getPhi(), e.constF(-HALF_PI)) + .getResult(); + auto negPhi = arith::NegFOp::create(rewriter, loc, r.getPhi()).getResult(); + auto minusPlus = + arith::AddFOp::create(rewriter, loc, negPhi, e.constF(HALF_PI)) + .getResult(); + return e.u(inQubit, r.getTheta(), phiMinus, minusPlus); + } + return {}; +} + +std::size_t +computeSynthesizedSingleQubitLength(const Eigen::Matrix2cd& matrix, + const SingleQubitEmitterSpec& emitter) { + // `U3` always emits a single gate; every other mode maps to a fixed Euler + // basis whose decomposition length we can measure directly. + switch (emitter.mode) { + case SingleQubitMode::U3: + return 1; + case SingleQubitMode::ZSXX: + return runEuler(decomposition::EulerBasis::ZSXX, matrix).gates.size(); + case SingleQubitMode::R: + return runEuler(decomposition::EulerBasis::XYX, matrix).gates.size(); + case SingleQubitMode::AxisPair: { + const auto bases = getEulerBasesForAxisPair(emitter.axisPair); + if (bases.empty()) { + return std::numeric_limits::max(); + } + return runEuler(bases.front(), matrix).gates.size(); + } + } + llvm_unreachable("unknown single-qubit mode"); +} + +Value emitSynthesizedSingleQubitFromMatrix( + IRRewriter& rewriter, Location loc, Value inQubit, + const Eigen::Matrix2cd& matrix, const SingleQubitEmitterSpec& emitter) { + SingleQubitEmitter e{.rewriter = &rewriter, .loc = loc}; + switch (emitter.mode) { + case SingleQubitMode::ZSXX: { + const auto seq = runEuler(decomposition::EulerBasis::ZSXX, matrix); + emitGPhaseIfNonTrivial(rewriter, loc, seq.globalPhase); + return emitEulerSequenceZsxx(e, inQubit, seq); + } + case SingleQubitMode::U3: { + using namespace std::complex_literals; + + Eigen::Matrix2cd m = matrix; + const auto det = m(0, 0) * m(1, 1) - m(0, 1) * m(1, 0); + const double phase = std::arg(det) / 2.0; + m *= std::exp(1i * (-phase)); + emitGPhaseIfNonTrivial(rewriter, loc, phase); + const auto angles = decomposition::EulerDecomposition::anglesFromUnitary( + m, decomposition::EulerBasis::ZYZ); + return e.u(inQubit, angles[0], angles[1], angles[2]); + } + case SingleQubitMode::R: { + const auto seq = runEuler(decomposition::EulerBasis::XYX, matrix); + emitGPhaseIfNonTrivial(rewriter, loc, seq.globalPhase); + return emitEulerSequenceR(e, inQubit, seq); + } + case SingleQubitMode::AxisPair: { + const auto bases = getEulerBasesForAxisPair(emitter.axisPair); + if (bases.empty()) { + return {}; + } + const auto seq = runEuler(bases.front(), matrix); + emitGPhaseIfNonTrivial(rewriter, loc, seq.globalPhase); + return emitEulerSequenceAxisPair(e, inQubit, emitter.axisPair, seq); + } + } + llvm_unreachable("unknown single-qubit mode"); +} + +Value decomposeToR(IRRewriter& rewriter, Operation* op, Value inQubit) { + if (isa(op)) { + return inQubit; + } + SingleQubitEmitter e{.rewriter = &rewriter, .loc = op->getLoc()}; + if (auto r = dyn_cast(op)) { + return r.getOutputQubit(0); + } + if (auto rx = dyn_cast(op)) { + return e.r(inQubit, rx.getTheta(), e.constF(0.0)); + } + if (auto ry = dyn_cast(op)) { + return e.r(inQubit, ry.getTheta(), e.constF(HALF_PI)); + } + return {}; +} + +Value decomposeToAxisPair(IRRewriter& rewriter, Operation* op, Value inQubit, + AxisPair axisPair) { + if (isa(op)) { + return inQubit; + } + SingleQubitEmitter e{.rewriter = &rewriter, .loc = op->getLoc()}; + switch (axisPair) { + case AxisPair::RxRz: + if (auto rx = dyn_cast(op)) { + return rx.getOutputQubit(0); + } + if (auto rz = dyn_cast(op)) { + return rz.getOutputQubit(0); + } + if (auto p = dyn_cast(op)) { + return e.rz(inQubit, p.getTheta()); + } + return {}; + case AxisPair::RxRy: + if (auto rx = dyn_cast(op)) { + return rx.getOutputQubit(0); + } + if (auto ry = dyn_cast(op)) { + return ry.getOutputQubit(0); + } + return {}; + case AxisPair::RyRz: + if (auto ry = dyn_cast(op)) { + return ry.getOutputQubit(0); + } + if (auto rz = dyn_cast(op)) { + return rz.getOutputQubit(0); + } + if (auto p = dyn_cast(op)) { + return e.rz(inQubit, p.getTheta()); + } + return {}; + } + llvm_unreachable("unknown axis pair"); +} + +} // namespace mlir::qco::native_synth diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp new file mode 100644 index 0000000000..064ee4cbaf --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp @@ -0,0 +1,320 @@ +/* + * 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/NativeSynthesis/TwoQubit.h" + +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h" + +#include + +#include +#include + +namespace mlir::qco::native_synth { +namespace { + +constexpr double PI = std::numbers::pi; +constexpr double HALF_PI = PI / 2.0; + +/// Whether the given single-qubit emitter can lower a decomposition-IR gate +/// of `kind` (an intermediate from Euler/Weyl, *not* a `NativeGateKind`) to a +/// native output sequence. Kept separate from `allowsSingleQubitOp`, which +/// operates on already-lowered MLIR output ops: some intermediate kinds map +/// to different native ops (e.g. the `R` emitter lowers RX/RY via `R(θ, φ)`). +bool emitterHandlesDecompositionGate(const SingleQubitEmitterSpec& emitter, + decomposition::GateKind kind) { + if (kind == decomposition::GateKind::I) { + return true; + } + switch (emitter.mode) { + case SingleQubitMode::ZSXX: + return kind == decomposition::GateKind::RZ || + kind == decomposition::GateKind::SX || + kind == decomposition::GateKind::X; + case SingleQubitMode::U3: + return kind == decomposition::GateKind::U; + case SingleQubitMode::R: + return kind == decomposition::GateKind::RX || + kind == decomposition::GateKind::RY || + kind == decomposition::GateKind::X || + kind == decomposition::GateKind::Y; + case SingleQubitMode::AxisPair: + switch (emitter.axisPair) { + case AxisPair::RxRz: + return kind == decomposition::GateKind::RX || + kind == decomposition::GateKind::RZ || + kind == decomposition::GateKind::X || + kind == decomposition::GateKind::Z; + case AxisPair::RxRy: + return kind == decomposition::GateKind::RX || + kind == decomposition::GateKind::RY || + kind == decomposition::GateKind::X || + kind == decomposition::GateKind::Y; + case AxisPair::RyRz: + return kind == decomposition::GateKind::RY || + kind == decomposition::GateKind::RZ || + kind == decomposition::GateKind::Y || + kind == decomposition::GateKind::Z; + } + break; + } + return false; +} + +/// Check that a single decomposition gate is allowed by the profile menu. +bool menuAllows(const decomposition::Gate& gate, + const NativeProfileSpec& spec) { + if (gate.qubitId.size() == 1) { + return std::ranges::any_of(spec.singleQubitEmitters, + [&gate](const SingleQubitEmitterSpec& emitter) { + return emitterHandlesDecompositionGate( + emitter, gate.type); + }); + } + if (gate.qubitId.size() == 2) { + switch (gate.type) { + case decomposition::GateKind::X: + return usesCxEntangler(spec); + case decomposition::GateKind::Z: + return usesCzEntangler(spec); + case decomposition::GateKind::RZZ: + return spec.allowRzz; + default: + return false; + } + } + return false; +} + +bool emitterHasDirectLowering(Operation* op, + const SingleQubitEmitterSpec& emitter) { + switch (emitter.mode) { + case SingleQubitMode::ZSXX: + return canDirectlyDecomposeToZSXX(op, emitter.supportsDirectRx); + case SingleQubitMode::U3: + return canDirectlyDecomposeToU3(op); + case SingleQubitMode::R: + return canDirectlyDecomposeToR(op); + case SingleQubitMode::AxisPair: + return canDirectlyDecomposeToAxisPair(op, emitter.axisPair); + } + llvm_unreachable("unknown single-qubit mode"); +} + +} // namespace + +bool gateSequenceFitsMenu(const decomposition::TwoQubitGateSequence& seq, + const NativeProfileSpec& spec) { + return std::ranges::all_of(seq.gates, + [&spec](const decomposition::Gate& gate) { + return menuAllows(gate, spec); + }); +} + +std::optional +decomposeTwoQubitFromMatrix(const Eigen::Matrix4cd& matrix, + EntanglerBasis entangler, + decomposition::EulerBasis eulerBasis, + std::optional numBasisUses) { + // Basis-gate qubit ids align with `getBlockTwoQubitMatrix` / CX layout. + const decomposition::Gate basisGate{ + .type = entangler == EntanglerBasis::Cz ? decomposition::GateKind::Z + : decomposition::GateKind::X, + .qubitId = {0, 1}, + }; + auto decomposer = + decomposition::TwoQubitBasisDecomposer::create(basisGate, 1.0); + auto weyl = + decomposition::TwoQubitWeylDecomposition::create(matrix, std::nullopt); + return decomposer.twoQubitDecompose( + weyl, llvm::SmallVector{eulerBasis}, + std::nullopt, /*approximate=*/false, numBasisUses); +} + +llvm::SmallVector> +collectSingleQubitCandidates(UnitaryOpInterface unitary, + const NativeProfileSpec& spec) { + llvm::SmallVector> candidates; + Operation* op = unitary.getOperation(); + unsigned enumerationIndex = 0; + const auto addCandidate = [&](CandidateClass klass, CandidateMetrics metrics, + SingleQubitRewriteStrategy strategy, + const SingleQubitEmitterSpec& emitter) { + candidates.push_back(SynthesisCandidate{ + .candidateClass = klass, + .metrics = metrics, + .enumerationIndex = enumerationIndex++, + .payload = + SingleQubitRewritePlan{.strategy = strategy, .emitter = emitter}, + }); + }; + for (const auto& emitter : spec.singleQubitEmitters) { + if (emitterHasDirectLowering(op, emitter)) { + addCandidate(CandidateClass::DirectSingleQ, + estimateDirectSingleQubitMetrics(op, emitter), + SingleQubitRewriteStrategy::Direct, emitter); + } + if (auto matrixMetrics = + estimateMatrixSingleQubitMetrics(unitary, emitter)) { + addCandidate(CandidateClass::MatrixSingleQ, *matrixMetrics, + SingleQubitRewriteStrategy::MatrixFallback, emitter); + } + } + return candidates; +} + +namespace { + +void tryAddTwoQubitBasisCandidatesForEmitterBasis( + llvm::SmallVector, 0>& candidates, + unsigned& enumerationIndex, const Eigen::Matrix4cd& targetMatrix, + const NativeProfileSpec& spec, EntanglerBasis entangler, + const SingleQubitEmitterSpec& emitter, decomposition::EulerBasis basis) { + for (std::uint8_t numBasisUses = 0; numBasisUses <= 3; ++numBasisUses) { + auto seq = decomposeTwoQubitFromMatrix(targetMatrix, entangler, basis, + numBasisUses); + if (!seq || + !isEquivalentUpToGlobalPhase(seq->getUnitaryMatrix(), targetMatrix) || + !gateSequenceFitsMenu(*seq, spec)) { + continue; + } + candidates.push_back(SynthesisCandidate{ + .candidateClass = CandidateClass::TwoQubitBasisRewrite, + .metrics = computeGateSequenceMetrics(*seq), + .enumerationIndex = enumerationIndex++, + .payload = {.sequence = *seq, + .emitter = emitter, + .entanglerBasis = entangler}, + }); + } +} + +} // namespace + +llvm::SmallVector, 0> +collectTwoQubitBasisCandidatesFromMatrix(const Eigen::Matrix4cd& targetMatrix, + const NativeProfileSpec& spec) { + llvm::SmallVector, 0> candidates; + if (spec.entanglerBases.empty()) { + return candidates; + } + unsigned enumerationIndex = 0; + for (const auto entangler : spec.entanglerBases) { + for (const auto& emitter : spec.singleQubitEmitters) { + for (const auto basis : emitter.eulerBases) { + tryAddTwoQubitBasisCandidatesForEmitterBasis( + candidates, enumerationIndex, targetMatrix, spec, entangler, + emitter, basis); + } + } + } + return candidates; +} + +CandidateMetrics xxPlusMinusYyRzzRewriteScoringMetrics() { + // Tallies for `rewriteXXPlusMinusYYViaRxxRyy` (identical for `XXPlusYY` and + // `XXMinusYY`): leading/final `rz` on `q0` (2) + `ryy` via `rzz` (four 1q + + // one `rzz`) + `rxx` via `rzz` (four `(rz, sx, rz)` per wire around each + // `rzz`, i.e. twelve 1q + one `rzz`). + constexpr unsigned numOneQ = 18; + constexpr unsigned numTwoQ = 2; + constexpr unsigned depth = 10; + return {.numOneQ = numOneQ, .numTwoQ = numTwoQ, .depth = depth}; +} + +llvm::SmallVector, 0> +collectTwoQubitBasisCandidates(UnitaryOpInterface unitary, + const NativeProfileSpec& spec) { + Eigen::Matrix4cd target; + if (!getNormalizedTwoQubitMatrix(unitary, target)) { + return {}; + } + return collectTwoQubitBasisCandidatesFromMatrix(target, spec); +} + +LogicalResult rewriteXXPlusMinusYYViaRxxRyy(IRRewriter& rewriter, + Operation* op) { + rewriter.setInsertionPoint(op); + const auto loc = op->getLoc(); + const auto constF = [&](double v) { + return createF64Const(rewriter, loc, v); + }; + const auto half = [&](Value v) -> Value { + if (auto c = getConstantF64(v)) { + return constF(*c * 0.5); + } + return arith::MulFOp::create(rewriter, loc, v, constF(0.5)).getResult(); + }; + const auto neg = [&](Value v) -> Value { + if (auto c = getConstantF64(v)) { + return constF(-*c); + } + return arith::NegFOp::create(rewriter, loc, v).getResult(); + }; + const auto emitH = [&](Value q) -> Value { + auto rz0 = RZOp::create(rewriter, loc, q, constF(HALF_PI)); + auto sx = SXOp::create(rewriter, loc, rz0.getOutputQubit(0)); + return RZOp::create(rewriter, loc, sx.getOutputQubit(0), constF(HALF_PI)) + .getOutputQubit(0); + }; + const auto emitRxxViaRzz = [&](Value q0, Value q1, + Value theta) -> std::pair { + q0 = emitH(q0); + q1 = emitH(q1); + auto rzz = RZZOp::create(rewriter, loc, q0, q1, theta); + q0 = rzz.getOutputQubit(0); + q1 = rzz.getOutputQubit(1); + return {emitH(q0), emitH(q1)}; + }; + const auto emitRyyViaRzz = [&](Value q0, Value q1, + Value theta) -> std::pair { + auto rx0 = RXOp::create(rewriter, loc, q0, constF(HALF_PI)); + auto rx1 = RXOp::create(rewriter, loc, q1, constF(HALF_PI)); + auto rzz = RZZOp::create(rewriter, loc, rx0.getOutputQubit(0), + rx1.getOutputQubit(0), theta); + auto rxb0 = + RXOp::create(rewriter, loc, rzz.getOutputQubit(0), constF(-HALF_PI)); + auto rxb1 = + RXOp::create(rewriter, loc, rzz.getOutputQubit(1), constF(-HALF_PI)); + return {rxb0.getOutputQubit(0), rxb1.getOutputQubit(0)}; + }; + + if (auto xxPlus = dyn_cast(op)) { + Value q0 = xxPlus.getInputQubit(0); + Value q1 = xxPlus.getInputQubit(1); + q0 = RZOp::create(rewriter, loc, q0, neg(xxPlus.getBeta())) + .getOutputQubit(0); + const auto halfTheta = half(xxPlus.getTheta()); + std::tie(q0, q1) = emitRyyViaRzz(q0, q1, halfTheta); + std::tie(q0, q1) = emitRxxViaRzz(q0, q1, halfTheta); + q0 = RZOp::create(rewriter, loc, q0, xxPlus.getBeta()).getOutputQubit(0); + rewriter.replaceOp(op, ValueRange{q0, q1}); + return success(); + } + if (auto xxMinus = dyn_cast(op)) { + Value q0 = xxMinus.getInputQubit(0); + Value q1 = xxMinus.getInputQubit(1); + q0 = RZOp::create(rewriter, loc, q0, neg(xxMinus.getBeta())) + .getOutputQubit(0); + const auto halfTheta = half(xxMinus.getTheta()); + std::tie(q0, q1) = emitRxxViaRzz(q0, q1, halfTheta); + std::tie(q0, q1) = emitRyyViaRzz(q0, q1, neg(halfTheta)); + q0 = RZOp::create(rewriter, loc, q0, xxMinus.getBeta()).getOutputQubit(0); + rewriter.replaceOp(op, ValueRange{q0, q1}); + return success(); + } + return failure(); +} + +} // namespace mlir::qco::native_synth diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp new file mode 100644 index 0000000000..c4963cebb6 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp @@ -0,0 +1,231 @@ +/* + * 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/NativeSynthesis/Utils.h" + +#include "mlir/Dialect/QCO/IR/QCOOps.h" + +#include +#include +#include +#include + +#include +#include + +namespace mlir::qco::native_synth { + +Value createF64Const(IRRewriter& rewriter, Location loc, double value) { + return arith::ConstantFloatOp::create(rewriter, loc, rewriter.getF64Type(), + llvm::APFloat(value)) + .getResult(); +} + +std::optional getConstantF64(Value value) { + if (auto constant = value.getDefiningOp()) { + if (auto floatAttr = llvm::dyn_cast(constant.getValue())) { + return floatAttr.getValueAsDouble(); + } + } + return std::nullopt; +} + +void emitGPhaseIfNonTrivial(IRRewriter& rewriter, Location loc, double phase) { + constexpr double epsilon = 1e-12; + if (std::abs(phase) > epsilon) { + GPhaseOp::create(rewriter, loc, createF64Const(rewriter, loc, phase)); + } +} + +bool isEquivalentUpToGlobalPhase(const Eigen::Matrix4cd& lhs, + const Eigen::Matrix4cd& rhs, double atol) { + const auto overlap = (rhs.adjoint() * lhs).trace(); + if (std::abs(overlap) <= atol) { + return false; + } + const auto factor = overlap / std::abs(overlap); + return lhs.isApprox(factor * rhs, atol); +} + +void normalizeToSU4(Eigen::Matrix4cd& matrix) { + using namespace std::complex_literals; + const std::complex det = matrix.determinant(); + if (std::abs(det) > 1e-16) { + matrix *= + std::pow(std::abs(det), -0.25) * std::exp(1i * (-std::arg(det) / 4.0)); + } +} + +bool getNormalizedTwoQubitMatrix(UnitaryOpInterface unitary, + Eigen::Matrix4cd& matrix) { + if (!unitary.getUnitaryMatrix4x4(matrix)) { + return false; + } + normalizeToSU4(matrix); + return true; +} + +bool getBlockTwoQubitMatrix(Operation* op, Eigen::Matrix4cd& matrix) { + if (isa(op)) { + return false; + } + if (auto ctrl = dyn_cast(op)) { + if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { + return false; + } + auto* body = ctrl.getBodyUnitary().getOperation(); + if (isa(body)) { + // CX matrix in the same 4x4 basis layout as ``getUnitaryMatrix4x4``. + matrix << 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0; + return true; + } + if (isa(body)) { + matrix = Eigen::Matrix4cd::Identity(); + matrix(3, 3) = -1.0; + return true; + } + return false; + } + auto unitary = dyn_cast(op); + if (!unitary || !unitary.isTwoQubit()) { + return false; + } + return unitary.getUnitaryMatrix4x4(matrix); +} + +namespace { + +/// Emit a single-qubit gate from a decomposition gate, threading `target`. +/// Returns `failure()` if the gate kind/parameter count is unsupported. +LogicalResult emitSingleQubitStep(IRRewriter& rewriter, Location loc, + const decomposition::Gate& gate, + Value& target) { + const auto emitConst = [&](double v) { + return createF64Const(rewriter, loc, v); + }; + switch (gate.type) { + case decomposition::GateKind::U: + if (gate.parameter.size() != 3) { + return failure(); + } + // EulerDecomposition emits `U` with parameters = {lambda, phi, theta} + // whereas `UOp` takes (theta, phi, lambda); reorder accordingly. + target = + UOp::create(rewriter, loc, target, emitConst(gate.parameter[2]), + emitConst(gate.parameter[1]), emitConst(gate.parameter[0])) + .getOutputQubit(0); + return success(); + case decomposition::GateKind::SX: + target = SXOp::create(rewriter, loc, target).getOutputQubit(0); + return success(); + case decomposition::GateKind::X: + target = XOp::create(rewriter, loc, target).getOutputQubit(0); + return success(); + case decomposition::GateKind::RX: + if (gate.parameter.size() != 1) { + return failure(); + } + target = RXOp::create(rewriter, loc, target, emitConst(gate.parameter[0])) + .getOutputQubit(0); + return success(); + case decomposition::GateKind::RY: + if (gate.parameter.size() != 1) { + return failure(); + } + target = RYOp::create(rewriter, loc, target, emitConst(gate.parameter[0])) + .getOutputQubit(0); + return success(); + case decomposition::GateKind::RZ: + if (gate.parameter.size() != 1) { + return failure(); + } + target = RZOp::create(rewriter, loc, target, emitConst(gate.parameter[0])) + .getOutputQubit(0); + return success(); + default: + return failure(); + } +} + +} // namespace + +LogicalResult +emitTwoQubitGateSequenceAtLoc(IRRewriter& rewriter, Location loc, Value qubit0, + Value qubit1, + const decomposition::TwoQubitGateSequence& seq, + Value& outQubit0, Value& outQubit1) { + for (const auto& gate : seq.gates) { + if (gate.qubitId.size() == 1) { + Value& target = (gate.qubitId[0] == 0) ? qubit0 : qubit1; + if (failed(emitSingleQubitStep(rewriter, loc, gate, target))) { + return failure(); + } + continue; + } + + const bool isCxOrCz = + gate.qubitId.size() == 2 && (gate.type == decomposition::GateKind::X || + gate.type == decomposition::GateKind::Z); + if (!isCxOrCz) { + return failure(); + } + + const decomposition::QubitId controlId = gate.qubitId[0]; + const decomposition::QubitId targetId = gate.qubitId[1]; + const Value controlIn = (controlId == 0) ? qubit0 : qubit1; + const Value targetIn = (targetId == 0) ? qubit0 : qubit1; + + auto ctrlOp = CtrlOp::create( + rewriter, loc, ValueRange{controlIn}, ValueRange{targetIn}, + [&](ValueRange targetArgs) -> llvm::SmallVector { + if (gate.type == decomposition::GateKind::X) { + return { + XOp::create(rewriter, loc, targetArgs[0]).getOutputQubit(0)}; + } + return {ZOp::create(rewriter, loc, targetArgs[0]).getOutputQubit(0)}; + }); + const Value controlOut = ctrlOp.getOutputControl(0); + const Value targetOut = ctrlOp.getOutputTarget(0); + Value next0 = qubit0; + Value next1 = qubit1; + if (controlId == 0) { + next0 = controlOut; + } else { + next1 = controlOut; + } + if (targetId == 0) { + next0 = targetOut; + } else { + next1 = targetOut; + } + qubit0 = next0; + qubit1 = next1; + } + + outQubit0 = qubit0; + outQubit1 = qubit1; + return success(); +} + +LogicalResult +emitTwoQubitGateSequence(IRRewriter& rewriter, Operation* op, Value qubit0, + Value qubit1, + const decomposition::TwoQubitGateSequence& seq) { + Value outQubit0; + Value outQubit1; + if (failed(emitTwoQubitGateSequenceAtLoc( + rewriter, op->getLoc(), qubit0, qubit1, seq, outQubit0, outQubit1))) { + return failure(); + } + rewriter.replaceOp(op, ValueRange{outQubit0, outQubit1}); + return success(); +} + +} // namespace mlir::qco::native_synth diff --git a/mlir/tools/mqt-cc/CMakeLists.txt b/mlir/tools/mqt-cc/CMakeLists.txt index adf0cd32d3..25adecd290 100644 --- a/mlir/tools/mqt-cc/CMakeLists.txt +++ b/mlir/tools/mqt-cc/CMakeLists.txt @@ -18,7 +18,8 @@ target_link_libraries( # Required for OpenQASM parsing MQT::CoreQASM MQT::CoreIR - MLIRQCTranslation) + MLIRQCTranslation + MLIRMemRefDialect) mqt_mlir_target_use_project_options(mqt-cc) llvm_update_compile_flags(mqt-cc) diff --git a/mlir/tools/mqt-cc/mqt-cc.cpp b/mlir/tools/mqt-cc/mqt-cc.cpp index 1a49fea64d..0a6c3fcf19 100644 --- a/mlir/tools/mqt-cc/mqt-cc.cpp +++ b/mlir/tools/mqt-cc/mqt-cc.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -76,6 +77,29 @@ static cl::opt disableMergeSingleQubitRotationGates( cl::desc("Disable quaternion-based single-qubit rotation gate merging"), cl::init(false)); +static cl::opt nativeGates( + "native-gates", + cl::desc("Comma-separated native gate menu for the native-gate-synthesis " + "pass (empty or whitespace-only disables synthesis)"), + cl::value_desc("csv"), cl::init("")); + +static cl::opt nativeGateScoreWeightTwoQ( + "native-gate-score-two-q", + cl::desc( + "Weight for two-qubit gates in native synthesis candidate scoring"), + cl::init(1.0)); + +static cl::opt nativeGateScoreWeightOneQ( + "native-gate-score-one-q", + cl::desc("Weight for single-qubit gates in native synthesis candidate " + "scoring"), + cl::init(0.1)); + +static cl::opt nativeGateScoreWeightDepth( + "native-gate-score-depth", + cl::desc("Weight for local depth in native synthesis candidate scoring"), + cl::init(0.01)); + /** * @brief Load and parse a .qasm file */ @@ -146,6 +170,7 @@ int main(int argc, char** argv) { registry.insert(); registry.insert(); registry.insert(); + registry.insert(); registry.insert(); registry.insert(); @@ -172,6 +197,10 @@ int main(int argc, char** argv) { config.printIRAfterAllStages = printIRAfterAllStages; config.disableMergeSingleQubitRotationGates = disableMergeSingleQubitRotationGates; + config.nativeGates = nativeGates.getValue(); + config.nativeGateScoreWeightTwoQ = nativeGateScoreWeightTwoQ.getValue(); + config.nativeGateScoreWeightOneQ = nativeGateScoreWeightOneQ.getValue(); + config.nativeGateScoreWeightDepth = nativeGateScoreWeightDepth.getValue(); // Run the compilation pipeline CompilationRecord record; @@ -192,7 +221,8 @@ int main(int argc, char** argv) { << record.afterQCOConversion << "\n"; outs() << "After Initial QCO Canonicalization:\n" << record.afterQCOCanon << "\n"; - outs() << "After Optimization:\n" << record.afterOptimization << "\n"; + outs() << "After Optimization and Native Gate Synthesis:\n" + << record.afterOptimization << "\n"; outs() << "After Final QCO Canonicalization:\n" << record.afterOptimizationCanon << "\n"; outs() << "After QCO-to-QC Conversion:\n" diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 0549b1f4ab..9c4964f5a9 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -15,6 +15,9 @@ #include "mlir/Dialect/QC/IR/QCDialect.h" #include "mlir/Dialect/QC/Translation/TranslateQuantumComputationToQC.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" #include "mlir/Dialect/QIR/Builder/QIRProgramBuilder.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" #include "mlir/Support/IRVerification.h" @@ -23,7 +26,11 @@ #include "qir_programs.h" #include "quantum_computation_programs.h" +#include #include +#include +#include +#include #include #include #include @@ -34,12 +41,16 @@ #include #include #include +#include #include #include +#include +#include #include #include #include +#include #include namespace mqt::test::compiler { @@ -663,4 +674,518 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(mlir::qc::multipleControlledXxMinusYY), MQT_NAMED_BUILDER(mlir::qir::multipleControlledXxMinusYY)})); +namespace { + +class CompilerPipelineNativeSynthesisConfigTest : public testing::Test { +protected: + std::unique_ptr context; + mlir::OwningOpRef module; + mlir::QuantumCompilerConfig config; + + void SetUp() override { + mlir::DialectRegistry registry; + registry.insert(); + context = std::make_unique(); + context->appendDialectRegistry(registry); + context->loadAllAvailableDialects(); + + module = mlir::qc::QCProgramBuilder::build(context.get(), + mlir::qc::staticQubitsWithOps); + ASSERT_TRUE(module); + + config.convertToQIR = false; + config.recordIntermediates = true; + } + + [[nodiscard]] mlir::CompilationRecord runPipelineAndExpectSuccess() const { + mlir::CompilationRecord record; + mlir::QuantumCompilerPipeline pipeline(config); + EXPECT_TRUE(pipeline.runPipeline(module.get(), &record).succeeded()); + return record; + } + + void runPipelineAndExpectFailure() const { + mlir::CompilationRecord record; + mlir::QuantumCompilerPipeline pipeline(config); + EXPECT_TRUE(failed(pipeline.runPipeline(module.get(), &record))); + } +}; + +/// Compute the 4×4 unitary of a two-qubit QCO module whose qubits are +/// introduced by `qco.static` ops with indices 0 and 1. Handles the op set +/// that stage-4/stage-5 IR can contain for the `staticQubitsWithOps` +/// program (pre-synthesis: `qco.h`; post-synthesis: `qco.rz`, `qco.sx`, +/// `qco.x`, `qco.p`, `qco.u`; and `qco.gphase`, which is skipped). Returns +/// `std::nullopt` if the IR contains an unsupported op or non-constant +/// parameters. +std::optional +computeStaticTwoQubitUnitary(mlir::ModuleOp module) { + if (module == nullptr) { + return std::nullopt; + } + + Eigen::Matrix4cd unitary = Eigen::Matrix4cd::Identity(); + llvm::DenseMap qubitIds; + + const auto getQubitId = [&](mlir::Value qubit) -> std::optional { + const auto it = qubitIds.find(qubit); + if (it == qubitIds.end()) { + return std::nullopt; + } + return it->second; + }; + + for (auto func : module.getOps()) { + for (auto& block : func.getBlocks()) { + for (auto& rawOp : block.getOperations()) { + if (auto staticOp = llvm::dyn_cast(&rawOp)) { + const auto index = static_cast(staticOp.getIndex()); + if (index >= 2) { + return std::nullopt; + } + qubitIds.try_emplace(staticOp.getResult(), index); + continue; + } + + if (llvm::isa(&rawOp)) { + continue; + } + + auto op = llvm::dyn_cast(&rawOp); + if (!op) { + continue; + } + + if (op.isSingleQubit()) { + const auto qid = getQubitId(op.getInputQubit(0)); + if (!qid) { + return std::nullopt; + } + Eigen::Matrix2cd oneQ; + if (!op.getUnitaryMatrix2x2(oneQ)) { + return std::nullopt; + } + unitary = + mlir::qco::decomposition::expandToTwoQubits(oneQ, *qid) * unitary; + qubitIds[op.getOutputQubit(0)] = *qid; + continue; + } + + if (op.isTwoQubit()) { + const auto q0 = getQubitId(op.getInputQubit(0)); + const auto q1 = getQubitId(op.getInputQubit(1)); + if (!q0 || !q1) { + return std::nullopt; + } + Eigen::Matrix4cd twoQ; + if (auto ctrl = llvm::dyn_cast(&rawOp)) { + if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { + return std::nullopt; + } + auto* body = ctrl.getBodyUnitary().getOperation(); + if (llvm::isa(body)) { + // CX matrix (same 4×4 layout as QCO unitary interface). + twoQ << 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0; + } else if (llvm::isa(body)) { + twoQ = Eigen::Matrix4cd::Identity(); + twoQ(3, 3) = -1.0; + } else { + return std::nullopt; + } + } else if (!op.getUnitaryMatrix4x4(twoQ)) { + return std::nullopt; + } + const llvm::SmallVector ids{ + static_cast(*q0), + static_cast(*q1)}; + unitary = + mlir::qco::decomposition::fixTwoQubitMatrixQubitOrder(twoQ, ids) * + unitary; + qubitIds[op.getOutputQubit(0)] = *q0; + qubitIds[op.getOutputQubit(1)] = *q1; + continue; + } + + return std::nullopt; + } + } + } + + return unitary; +} + +/// Check matrix equality up to a unit-modulus global phase. +bool isEquivalentUpToGlobalPhase(const Eigen::Matrix4cd& lhs, + const Eigen::Matrix4cd& rhs, + const double atol = 1e-10) { + const auto overlap = (rhs.adjoint() * lhs).trace(); + if (std::abs(overlap) <= atol) { + return false; + } + const auto factor = overlap / std::abs(overlap); + return lhs.isApprox(factor * rhs, atol); +} + +} // namespace + +TEST_F(CompilerPipelineNativeSynthesisConfigTest, + AppliesConfiguredNativeSynthesisProfileInStage5) { + config.nativeGates = "x,sx,rz,cx"; + + const auto record = runPipelineAndExpectSuccess(); + + // Stage 4 still contains unsynthesized H operations from the source program. + EXPECT_NE(record.afterQCOCanon.find("qco.h"), std::string::npos); + // Stage 5 must rewrite them when a native menu is configured. + EXPECT_EQ(record.afterOptimization.find("qco.h"), std::string::npos); +} + +TEST_F(CompilerPipelineNativeSynthesisConfigTest, + AppliesConfiguredU3CxNativeSynthesisProfileInStage5) { + config.nativeGates = "u,cx"; + + const auto record = runPipelineAndExpectSuccess(); + + EXPECT_NE(record.afterQCOCanon.find("qco.h"), std::string::npos); + EXPECT_EQ(record.afterOptimization.find("qco.h"), std::string::npos); + EXPECT_NE(record.afterOptimization.find("qco.u"), std::string::npos); +} + +TEST_F(CompilerPipelineNativeSynthesisConfigTest, + AppliesConfiguredExpandedNativeSynthesisProfileInStage5) { + config.nativeGates = "u,rx,rz,cx,cz"; + + const auto record = runPipelineAndExpectSuccess(); + + EXPECT_NE(record.afterQCOCanon.find("qco.h"), std::string::npos); + EXPECT_EQ(record.afterOptimization.find("qco.h"), std::string::npos); +} + +TEST_F(CompilerPipelineNativeSynthesisConfigTest, + RejectsInvalidNativeSynthesisScoreWeightsInStage5) { + config.nativeGates = "u,cx"; + config.nativeGateScoreWeightTwoQ = -1.0; + + runPipelineAndExpectFailure(); +} + +TEST_F(CompilerPipelineNativeSynthesisConfigTest, + RejectsUnderSpecifiedNativeSynthesisMenuInStage5) { + // A menu with only two-qubit entanglers cannot synthesize any single-qubit + // operation. + config.nativeGates = "cx,cz"; + + runPipelineAndExpectFailure(); +} + +TEST_F(CompilerPipelineNativeSynthesisConfigTest, + RejectsInvalidNativeGateTokenInStage5) { + // Unknown tokens in the menu must be rejected. + config.nativeGates = "not-a-gate"; + + runPipelineAndExpectFailure(); +} + +TEST_F(CompilerPipelineNativeSynthesisConfigTest, + LeavesIRUnchangedWhenNoNativeProfileIsConfigured) { + // Stage 5 must be a no-op when `nativeGates` is empty (the documented + // default): the stage-4 (QCO canonicalized) and stage-5 (optimization + + // native gate synthesis) IRs have to be byte-identical. + config.nativeGates = ""; + + const auto record = runPipelineAndExpectSuccess(); + + EXPECT_NE(record.afterQCOCanon.find("qco.h"), std::string::npos); + EXPECT_EQ(record.afterQCOCanon, record.afterOptimization); +} + +TEST_F(CompilerPipelineNativeSynthesisConfigTest, + NativeSynthesisPreservesUnitaryOnStaticQubits) { + // End-to-end unitary equivalence check: after the pipeline lowers + // `staticQubitsWithOps` (H on two static qubits) onto the `x,sx,rz,cx` + // native gate set, the 4×4 unitary of the IR after stage 5 must match the + // unitary of the pre-synthesis (`afterQCOCanon`) IR up to a global phase. + config.nativeGates = "x,sx,rz,cx"; + + const auto record = runPipelineAndExpectSuccess(); + + auto preSynth = mlir::parseSourceString(record.afterQCOCanon, + context.get()); + auto postSynth = mlir::parseSourceString( + record.afterOptimization, context.get()); + ASSERT_TRUE(preSynth); + ASSERT_TRUE(postSynth); + + const auto preU = computeStaticTwoQubitUnitary(preSynth.get()); + const auto postU = computeStaticTwoQubitUnitary(postSynth.get()); + ASSERT_TRUE(preU); + ASSERT_TRUE(postU); + EXPECT_TRUE(isEquivalentUpToGlobalPhase(*preU, *postU)); +} + +namespace { + +struct NativeSynthesisProgramTestCase { + std::string name; + QCProgramBuilderFn qcProgramBuilder; + + friend std::ostream& operator<<(std::ostream& os, + const NativeSynthesisProgramTestCase& info) { + return os << "NativeSynthesisProgram{" << info.name << "}"; + } +}; + +struct NativeSynthesisProfileTestCase { + std::string name; + std::string nativeGates; + bool expectUInStage5 = false; + llvm::SmallVector nonNativeOpsToEliminate; + + friend std::ostream& operator<<(std::ostream& os, + const NativeSynthesisProfileTestCase& info) { + return os << "NativeSynthesisProfile{" << info.name << "}"; + } +}; + +struct NativeSynthesisStage5TestCase { + NativeSynthesisProgramTestCase program; + NativeSynthesisProfileTestCase profile; + + friend std::ostream& operator<<(std::ostream& os, + const NativeSynthesisStage5TestCase& info) { + return os << info.profile << " / " << info.program; + } +}; + +mlir::OwningOpRef +buildQCModuleForNativeSynthesisProgram(mlir::MLIRContext* context, + const QCProgramBuilderFn builder) { + auto module = mlir::qc::QCProgramBuilder::build(context, builder.fn); + EXPECT_TRUE(module) << "failed to build QC module"; + return module; +} + +mlir::CompilationRecord +runPipelineWithNativeSynthesisConfig(mlir::ModuleOp module, + const std::string& nativeGates) { + mlir::QuantumCompilerConfig config; + config.convertToQIR = false; + config.recordIntermediates = true; + config.nativeGates = nativeGates; + + mlir::CompilationRecord record; + mlir::QuantumCompilerPipeline pipeline(config); + EXPECT_TRUE(pipeline.runPipeline(module, &record).succeeded()); + return record; +} + +class CompilerPipelineNativeSynthesisProgramsTest + : public testing::TestWithParam { +protected: + std::unique_ptr context; + + void SetUp() override { + mlir::DialectRegistry registry; + registry.insert(); + context = std::make_unique(); + context->appendDialectRegistry(registry); + context->loadAllAvailableDialects(); + } +}; + +} // namespace + +TEST_P(CompilerPipelineNativeSynthesisProgramsTest, + SynthesizesHOperationsInStage5) { + const auto& testCase = GetParam(); + auto module = buildQCModuleForNativeSynthesisProgram( + context.get(), testCase.program.qcProgramBuilder); + ASSERT_TRUE(module); + + const auto record = runPipelineWithNativeSynthesisConfig( + module.get(), testCase.profile.nativeGates); + + for (const auto& opName : testCase.profile.nonNativeOpsToEliminate) { + ASSERT_NE(record.afterQCOCanon.find(opName), std::string::npos) + << "Program must contain " << opName << " after QCO canonicalization"; + EXPECT_EQ(record.afterOptimization.find(opName), std::string::npos); + } + if (testCase.profile.expectUInStage5) { + EXPECT_NE(record.afterOptimization.find("qco.u"), std::string::npos); + } +} + +INSTANTIATE_TEST_SUITE_P( + NativeSynthesisStage5Programs, CompilerPipelineNativeSynthesisProgramsTest, + testing::Values( + NativeSynthesisStage5TestCase{ + .program = + NativeSynthesisProgramTestCase{ + "StaticQubitsWithOps", + MQT_NAMED_BUILDER(mlir::qc::staticQubitsWithOps), + }, + .profile = + NativeSynthesisProfileTestCase{ + .name = "IbmBasicCx", + .nativeGates = "x,sx,rz,cx", + .nonNativeOpsToEliminate = {"qco.h"}, + }, + }, + NativeSynthesisStage5TestCase{ + .program = + NativeSynthesisProgramTestCase{ + "ResetMultipleQubitsAfterSingleOp", + MQT_NAMED_BUILDER( + mlir::qc::resetMultipleQubitsAfterSingleOp), + }, + .profile = + NativeSynthesisProfileTestCase{ + .name = "IbmBasicCx", + .nativeGates = "x,sx,rz,cx", + .nonNativeOpsToEliminate = {"qco.h"}, + }, + }, + NativeSynthesisStage5TestCase{ + .program = + NativeSynthesisProgramTestCase{ + "S", + MQT_NAMED_BUILDER(mlir::qc::s), + }, + .profile = + NativeSynthesisProfileTestCase{ + .name = "IbmBasicCx", + .nativeGates = "x,sx,rz,cx", + .nonNativeOpsToEliminate = {"qco.s"}, + }, + }, + NativeSynthesisStage5TestCase{ + .program = + NativeSynthesisProgramTestCase{ + "T", + MQT_NAMED_BUILDER(mlir::qc::t_), + }, + .profile = + NativeSynthesisProfileTestCase{ + .name = "IbmBasicCx", + .nativeGates = "x,sx,rz,cx", + .nonNativeOpsToEliminate = {"qco.t"}, + }, + }, + NativeSynthesisStage5TestCase{ + .program = + NativeSynthesisProgramTestCase{ + "Y", + MQT_NAMED_BUILDER(mlir::qc::y), + }, + .profile = + NativeSynthesisProfileTestCase{ + .name = "IbmBasicCx", + .nativeGates = "x,sx,rz,cx", + .nonNativeOpsToEliminate = {"qco.y"}, + }, + }, + NativeSynthesisStage5TestCase{ + .program = + NativeSynthesisProgramTestCase{ + "StaticQubitsWithOps", + MQT_NAMED_BUILDER(mlir::qc::staticQubitsWithOps), + }, + .profile = + NativeSynthesisProfileTestCase{ + .name = "U3Cx", + .nativeGates = "u,cx", + .expectUInStage5 = true, + .nonNativeOpsToEliminate = {"qco.h"}, + }, + }, + NativeSynthesisStage5TestCase{ + .program = + NativeSynthesisProgramTestCase{ + "ResetMultipleQubitsAfterSingleOp", + MQT_NAMED_BUILDER( + mlir::qc::resetMultipleQubitsAfterSingleOp), + }, + .profile = + NativeSynthesisProfileTestCase{ + .name = "U3Cx", + .nativeGates = "u,cx", + .expectUInStage5 = true, + .nonNativeOpsToEliminate = {"qco.h"}, + }, + }, + NativeSynthesisStage5TestCase{ + .program = + NativeSynthesisProgramTestCase{ + "StaticQubitsWithOps", + MQT_NAMED_BUILDER(mlir::qc::staticQubitsWithOps), + }, + .profile = + NativeSynthesisProfileTestCase{ + .name = "IbmBasicCz", + .nativeGates = "x,sx,rz,cz", + .nonNativeOpsToEliminate = {"qco.h"}, + }, + }, + NativeSynthesisStage5TestCase{ + .program = + NativeSynthesisProgramTestCase{ + "StaticQubitsWithOps", + MQT_NAMED_BUILDER(mlir::qc::staticQubitsWithOps), + }, + .profile = + NativeSynthesisProfileTestCase{ + .name = "IbmFractional", + .nativeGates = "x,sx,rz,rx,rzz,cz", + .nonNativeOpsToEliminate = {"qco.h"}, + }, + }, + NativeSynthesisStage5TestCase{ + .program = + NativeSynthesisProgramTestCase{ + "StaticQubitsWithOps", + MQT_NAMED_BUILDER(mlir::qc::staticQubitsWithOps), + }, + .profile = + NativeSynthesisProfileTestCase{ + .name = "IqmDefault", + .nativeGates = "r,cz", + .nonNativeOpsToEliminate = {"qco.h"}, + }, + }, + NativeSynthesisStage5TestCase{ + .program = + NativeSynthesisProgramTestCase{ + "StaticQubitsWithOps", + MQT_NAMED_BUILDER(mlir::qc::staticQubitsWithOps), + }, + .profile = + NativeSynthesisProfileTestCase{ + .name = "AxisPairRxRzCx", + .nativeGates = "rx,rz,cx", + .nonNativeOpsToEliminate = {"qco.h"}, + }, + }, + NativeSynthesisStage5TestCase{ + .program = + NativeSynthesisProgramTestCase{ + "StaticQubitsWithOps", + MQT_NAMED_BUILDER(mlir::qc::staticQubitsWithOps), + }, + .profile = + NativeSynthesisProfileTestCase{ + .name = "U3Cz", + .nativeGates = "u,cz", + .expectUInStage5 = true, + .nonNativeOpsToEliminate = {"qco.h"}, + }, + })); + } // namespace mqt::test::compiler diff --git a/mlir/unittests/Dialect/QCO/Transforms/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/CMakeLists.txt index 9f9b03449d..232c2751c5 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/CMakeLists.txt +++ b/mlir/unittests/Dialect/QCO/Transforms/CMakeLists.txt @@ -6,5 +6,7 @@ # # Licensed under the MIT License +add_subdirectory(Decomposition) add_subdirectory(Mapping) add_subdirectory(Optimizations) +add_subdirectory(NativeSynthesis) diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt new file mode 100644 index 0000000000..7fce8b4eab --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt @@ -0,0 +1,21 @@ +# 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-native-synthesis) +add_executable( + ${target_name} + native_synthesis_test_helpers.cpp test_native_synthesis_pass_custom_menus.cpp + test_native_synthesis_pass_fusion.cpp test_native_synthesis_pass_multi_qubit.cpp + test_native_synthesis_pass_profiles.cpp test_native_synthesis_pass_scoring.cpp) + +target_link_libraries(${target_name} PRIVATE MLIRParser GTest::gtest_main MLIRQCProgramBuilder + MLIRQCToQCO MLIRQCOTransforms) + +mqt_mlir_configure_unittest_target(${target_name}) + +gtest_discover_tests(${target_name} PROPERTIES LABELS mqt-mlir-unittests DISCOVERY_TIMEOUT 60) diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_pass_test_fixture.h b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_pass_test_fixture.h new file mode 100644 index 0000000000..e7b4c48056 --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_pass_test_fixture.h @@ -0,0 +1,359 @@ +/* + * 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/Conversion/QCToQCO/QCToQCO.h" +#include "mlir/Dialect/QC/Builder/QCProgramBuilder.h" +#include "mlir/Dialect/QC/IR/QCDialect.h" +#include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Passes.h" +#include "native_synthesis_test_helpers.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +/// One row of the standard multi-profile equivalence sweeps in tests. +struct NativeSynthesisProfileSweepCase { + const char* nativeGates; + bool (*isNative)(mlir::OwningOpRef&); +}; + +class NativeSynthesisPassTest : public testing::Test { +protected: + void SetUp() override { + mlir::DialectRegistry registry; + registry.insert(); + context = std::make_unique(); + context->appendDialectRegistry(registry); + context->loadAllAvailableDialects(); + } + + template + static bool onlyTheseOps(mlir::OwningOpRef& moduleOp, + const bool allowCx, const bool allowCz) { + bool ok = true; + std::ignore = moduleOp->walk([&](mlir::qco::UnitaryOpInterface op) { + mlir::Operation* raw = op.getOperation(); + if (mlir::isa_and_present(raw->getParentOp())) { + return mlir::WalkResult::advance(); + } + if (mlir::isa(raw)) { + return mlir::WalkResult::advance(); + } + if (auto ctrl = mlir::dyn_cast(raw)) { + if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { + ok = false; + return mlir::WalkResult::interrupt(); + } + mlir::Operation* body = ctrl.getBodyUnitary().getOperation(); + const bool isCx = mlir::isa(body); + const bool isCz = mlir::isa(body); + if ((isCx && allowCx) || (isCz && allowCz)) { + return mlir::WalkResult::advance(); + } + ok = false; + return mlir::WalkResult::interrupt(); + } + + if (!mlir::isa(raw)) { + ok = false; + return mlir::WalkResult::interrupt(); + } + return mlir::WalkResult::advance(); + }); + return ok; + } + + static bool onlyIbmBasicCxOps(mlir::OwningOpRef& moduleOp) { + return onlyTheseOps(moduleOp, /*allowCx=*/true, + /*allowCz=*/false); + } + + static bool onlyIbmBasicCzOps(mlir::OwningOpRef& moduleOp) { + return onlyTheseOps(moduleOp, /*allowCx=*/false, + /*allowCz=*/true); + } + + static bool onlyGenericU3CxOps(mlir::OwningOpRef& moduleOp) { + return onlyTheseOps(moduleOp, /*allowCx=*/true, + /*allowCz=*/false); + } + + static bool onlyGenericU3CzOps(mlir::OwningOpRef& moduleOp) { + return onlyTheseOps(moduleOp, /*allowCx=*/false, + /*allowCz=*/true); + } + + static bool onlyIqmDefaultOps(mlir::OwningOpRef& moduleOp) { + return onlyTheseOps(moduleOp, /*allowCx=*/false, + /*allowCz=*/true); + } + + static bool + onlyIbmFractionalOps(mlir::OwningOpRef& moduleOp) { + return onlyTheseOps( + moduleOp, /*allowCx=*/false, /*allowCz=*/true); + } + + static bool + onlyAxisPairRxRzCxOps(mlir::OwningOpRef& moduleOp) { + return onlyTheseOps( + moduleOp, /*allowCx=*/true, /*allowCz=*/false); + } + + static bool + onlyAxisPairRxRyCxOps(mlir::OwningOpRef& moduleOp) { + return onlyTheseOps( + moduleOp, /*allowCx=*/true, /*allowCz=*/false); + } + + static bool + onlyAxisPairRyRzCzOps(mlir::OwningOpRef& moduleOp) { + return onlyTheseOps( + moduleOp, /*allowCx=*/false, /*allowCz=*/true); + } + + static bool + onlyUOrAxisPairRxRzCxOps(mlir::OwningOpRef& moduleOp) { + return onlyTheseOps(moduleOp, /*allowCx=*/true, + /*allowCz=*/false); + } + + static bool + onlyGenericU3CxOrCzOps(mlir::OwningOpRef& moduleOp) { + return onlyTheseOps(moduleOp, /*allowCx=*/true, + /*allowCz=*/true); + } + + /// The nine built-in reference profiles (IBM basic, U3, fractional, IQM, + /// axis pairs including ``rx,rz,cx``). Used by 2q / multi-qubit equivalence + /// sweeps. + static std::array + allNineEquivalenceProfiles() { + return {{{.nativeGates = "x,sx,rz,cx", .isNative = &onlyIbmBasicCxOps}, + {.nativeGates = "x,sx,rz,cz", .isNative = &onlyIbmBasicCzOps}, + {.nativeGates = "u,cx", .isNative = &onlyGenericU3CxOps}, + {.nativeGates = "u,cz", .isNative = &onlyGenericU3CzOps}, + {.nativeGates = "x,sx,rz,rx,rzz,cz", + .isNative = &onlyIbmFractionalOps}, + {.nativeGates = "r,cz", .isNative = &onlyIqmDefaultOps}, + {.nativeGates = "rx,ry,cx", .isNative = &onlyAxisPairRxRyCxOps}, + {.nativeGates = "ry,rz,cz", .isNative = &onlyAxisPairRyRzCzOps}, + {.nativeGates = "rx,rz,cx", .isNative = &onlyAxisPairRxRzCxOps}}}; + } + + /// CX-friendly profiles excluding IQM-default (CZ-only entangler), for + /// circuits that use a ``cx`` two-qubit primitive in the source. + static std::array + fiveCxEntanglerEquivalenceProfiles() { + return {{{.nativeGates = "x,sx,rz,cx", .isNative = &onlyIbmBasicCxOps}, + {.nativeGates = "u,cx", .isNative = &onlyGenericU3CxOps}, + {.nativeGates = "x,sx,rz,rx,rzz,cz", + .isNative = &onlyIbmFractionalOps}, + {.nativeGates = "rx,ry,cx", .isNative = &onlyAxisPairRxRyCxOps}, + {.nativeGates = "rx,rz,cx", .isNative = &onlyAxisPairRxRzCxOps}}}; + } + + [[nodiscard]] mlir::OwningOpRef + buildBroadOneQCanonicalizationCircuit() const { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + + builder.id(q0); + builder.x(q0); + builder.y(q1); + builder.z(q0); + builder.h(q1); + builder.s(q0); + builder.sdg(q1); + builder.t(q0); + builder.tdg(q1); + builder.sx(q0); + builder.sxdg(q1); + builder.rx(0.13, q0); + builder.ry(-0.47, q1); + builder.rz(0.29, q0); + builder.p(-0.38, q1); + builder.r(0.61, -0.22, q0); + builder.cz(q0, q1); + + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + } + + [[nodiscard]] mlir::OwningOpRef + buildZeroAngleCanonicalizationCircuit() const { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + + builder.rx(0.0, q0); + builder.ry(0.0, q1); + builder.rz(0.0, q0); + builder.p(0.0, q1); + builder.r(0.0, 0.0, q0); + builder.cz(q0, q1); + + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + } + + [[nodiscard]] mlir::OwningOpRef + buildIbmFractionalAllGateFamiliesCircuit() const { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + + builder.id(q0); + builder.x(q0); + builder.y(q1); + builder.z(q0); + builder.h(q1); + builder.s(q0); + builder.sdg(q1); + builder.t(q0); + builder.tdg(q1); + builder.sx(q0); + builder.sxdg(q1); + builder.rx(0.13, q0); + builder.ry(-0.47, q1); + builder.rz(0.29, q0); + builder.p(-0.38, q1); + builder.r(0.61, -0.22, q0); + + builder.cx(q0, q1); + builder.cz(q1, q0); + + builder.swap(q0, q1); + builder.iswap(q0, q1); + builder.dcx(q0, q1); + builder.ecr(q0, q1); + builder.rxx(0.17, q0, q1); + builder.ryy(-0.21, q0, q1); + builder.rzx(0.41, q0, q1); + builder.rzz(-0.33, q0, q1); + builder.xx_plus_yy(0.52, -0.14, q0, q1); + builder.xx_minus_yy(-0.37, 0.26, q0, q1); + + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + } + + static void runNativeSynthesis(mlir::OwningOpRef& moduleOp, + const std::string& nativeGates, + const double scoreWeightTwoQ = 1.0, + const double scoreWeightOneQ = 0.1, + const double scoreWeightDepth = 0.01) { + mlir::PassManager pm(moduleOp->getContext()); + pm.addPass(mlir::createQCToQCO()); + pm.addPass(mlir::qco::createNativeGateSynthesisPass( + mlir::qco::NativeGateSynthesisOptions{ + .nativeGates = nativeGates, + .scoreWeightTwoQ = scoreWeightTwoQ, + .scoreWeightOneQ = scoreWeightOneQ, + .scoreWeightDepth = scoreWeightDepth, + })); + ASSERT_TRUE(mlir::succeeded(pm.run(*moduleOp))); + } + + static void runQcToQco(mlir::OwningOpRef& moduleOp) { + mlir::PassManager pm(moduleOp->getContext()); + pm.addPass(mlir::createQCToQCO()); + ASSERT_TRUE(mlir::succeeded(pm.run(*moduleOp))); + } + + static std::string + moduleToString(const mlir::OwningOpRef& moduleOp) { + std::string text; + llvm::raw_string_ostream os(text); + moduleOp.get()->print(os); + return text; + } + + template + void expectNativeAfterSynthesis(BuildFn buildFn, + const std::string& nativeGates, + PredicateFn isNative, + const double scoreWeightTwoQ = 1.0, + const double scoreWeightOneQ = 0.1, + const double scoreWeightDepth = 0.01) { + auto moduleOp = buildFn(); + runNativeSynthesis(moduleOp, nativeGates, scoreWeightTwoQ, scoreWeightOneQ, + scoreWeightDepth); + EXPECT_TRUE(isNative(moduleOp)); + } + + template + void expectSynthesisFailure(BuildFn buildFn, const std::string& nativeGates, + const double scoreWeightTwoQ = 1.0, + const double scoreWeightOneQ = 0.1, + const double scoreWeightDepth = 0.01) { + auto moduleOp = buildFn(); + mlir::PassManager pm(moduleOp->getContext()); + pm.addPass(mlir::createQCToQCO()); + pm.addPass(mlir::qco::createNativeGateSynthesisPass( + mlir::qco::NativeGateSynthesisOptions{ + .nativeGates = nativeGates, + .scoreWeightTwoQ = scoreWeightTwoQ, + .scoreWeightOneQ = scoreWeightOneQ, + .scoreWeightDepth = scoreWeightDepth, + })); + EXPECT_TRUE(mlir::failed(pm.run(*moduleOp))); + } + + template + void expectEquivalentAndNativeAfterSynthesis( + BuildFn buildFn, const std::string& nativeGates, PredicateFn isNative, + UnitaryFn computeUnitary, const double scoreWeightTwoQ = 1.0, + const double scoreWeightOneQ = 0.1, + const double scoreWeightDepth = 0.01) { + auto expectedModule = buildFn(); + runQcToQco(expectedModule); + const auto expectedUnitary = computeUnitary(expectedModule); + ASSERT_TRUE(expectedUnitary.has_value()); + + auto synthesizedModule = buildFn(); + runNativeSynthesis(synthesizedModule, nativeGates, scoreWeightTwoQ, + scoreWeightOneQ, scoreWeightDepth); + EXPECT_TRUE(isNative(synthesizedModule)); + const auto synthesizedUnitary = computeUnitary(synthesizedModule); + ASSERT_TRUE(synthesizedUnitary.has_value()); + EXPECT_TRUE(mlir::qco::native_synth_test::isEquivalentUpToGlobalPhase( + *expectedUnitary, *synthesizedUnitary)); + } + + std::unique_ptr context; +}; diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp new file mode 100644 index 0000000000..920d7ff0e8 --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp @@ -0,0 +1,428 @@ +/* + * 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 "native_synthesis_test_helpers.h" + +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" + +#include +#include + +#include + +using namespace mlir; + +namespace mlir::qco::native_synth_test { + +std::complex phasedAmplitude(const double magnitude, + const double phase) { + return std::complex(magnitude, 0.0) * + std::exp(std::complex(0.0, phase)); +} + +Eigen::Matrix2cd u3Matrix(double theta, double phi, double lambda) { + using Complex = std::complex; + const Complex i(0.0, 1.0); + const double c = std::cos(theta / 2.0); + const double s = std::sin(theta / 2.0); + const Complex eiphi = std::exp(i * phi); + const Complex eilambda = std::exp(i * lambda); + const Complex eiphilambda = std::exp(i * (phi + lambda)); + + Eigen::Matrix2cd mat; + mat << c, -eilambda * s, eiphi * s, eiphilambda * c; + return mat; +} + +bool isUnitary(const Eigen::Matrix2cd& m, const double atol) { + const auto identity = Eigen::Matrix2cd::Identity(); + return (m * m.adjoint()).isApprox(identity, atol) && + (m.adjoint() * m).isApprox(identity, atol); +} + +std::optional evaluateConstF64(Value value) { + if (!value) { + return std::nullopt; + } + if (auto cst = value.getDefiningOp()) { + if (auto attr = llvm::dyn_cast(cst.getValue())) { + return attr.getValueAsDouble(); + } + return std::nullopt; + } + if (auto neg = value.getDefiningOp()) { + if (auto v = evaluateConstF64(neg.getOperand())) { + return -*v; + } + return std::nullopt; + } + if (auto add = value.getDefiningOp()) { + auto lhs = evaluateConstF64(add.getLhs()); + auto rhs = evaluateConstF64(add.getRhs()); + if (lhs && rhs) { + return *lhs + *rhs; + } + return std::nullopt; + } + if (auto sub = value.getDefiningOp()) { + auto lhs = evaluateConstF64(sub.getLhs()); + auto rhs = evaluateConstF64(sub.getRhs()); + if (lhs && rhs) { + return *lhs - *rhs; + } + return std::nullopt; + } + if (auto mul = value.getDefiningOp()) { + auto lhs = evaluateConstF64(mul.getLhs()); + auto rhs = evaluateConstF64(mul.getRhs()); + if (lhs && rhs) { + return *lhs * *rhs; + } + return std::nullopt; + } + if (auto div = value.getDefiningOp()) { + auto lhs = evaluateConstF64(div.getLhs()); + auto rhs = evaluateConstF64(div.getRhs()); + if (lhs && rhs) { + return *lhs / *rhs; + } + return std::nullopt; + } + return std::nullopt; +} + +/// Extract the 2x2 unitary matrix associated with a single-qubit op. +bool extractSingleQubitMatrix(qco::UnitaryOpInterface op, + Eigen::Matrix2cd& out) { + if (auto rz = dyn_cast(op.getOperation())) { + auto theta = evaluateConstF64(rz.getTheta()); + if (!theta) { + return false; + } + out = qco::decomposition::rzMatrix(*theta); + return true; + } + if (auto rx = dyn_cast(op.getOperation())) { + auto theta = evaluateConstF64(rx.getTheta()); + if (!theta) { + return false; + } + out = qco::decomposition::rxMatrix(*theta); + return true; + } + if (auto ry = dyn_cast(op.getOperation())) { + auto theta = evaluateConstF64(ry.getTheta()); + if (!theta) { + return false; + } + out = qco::decomposition::ryMatrix(*theta); + return true; + } + if (auto u = dyn_cast(op.getOperation())) { + auto theta = evaluateConstF64(u.getTheta()); + auto phi = evaluateConstF64(u.getPhi()); + auto lambda = evaluateConstF64(u.getLambda()); + if (!theta || !phi || !lambda) { + return false; + } + out = u3Matrix(*theta, *phi, *lambda); + return true; + } + if (auto p = dyn_cast(op.getOperation())) { + auto lambda = evaluateConstF64(p.getTheta()); + if (!lambda) { + return false; + } + out = qco::decomposition::pMatrix(*lambda); + return true; + } + if (auto r = dyn_cast(op.getOperation())) { + auto theta = evaluateConstF64(r.getTheta()); + auto phi = evaluateConstF64(r.getPhi()); + if (!theta || !phi) { + return false; + } + const auto thetaSin = std::sin(*theta / 2.0); + const auto m01 = phasedAmplitude(thetaSin, -*phi - (llvm::numbers::pi / 2)); + const auto m10 = phasedAmplitude(thetaSin, *phi - (llvm::numbers::pi / 2)); + const std::complex thetaCos = std::cos(*theta / 2.0); + out = Eigen::Matrix2cd{{thetaCos, m01}, {m10, thetaCos}}; + return true; + } + if (op.getUnitaryMatrix2x2(out)) { + return true; + } + auto dynamic = op.getUnitaryMatrix(); + if (!dynamic || dynamic->rows() != 2 || dynamic->cols() != 2) { + return false; + } + out = dynamic->template block<2, 2>(0, 0); + return true; +} + +/// 4×4 unitary for a two-qubit op (same layout as ``getUnitaryMatrix4x4``). +bool extractTwoQubitMatrix(qco::UnitaryOpInterface op, Eigen::Matrix4cd& out) { + if (auto ctrl = dyn_cast(op.getOperation())) { + if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { + return false; + } + auto* body = ctrl.getBodyUnitary().getOperation(); + if (isa(body)) { + out = Eigen::Matrix4cd::Identity(); + out(3, 3) = -1.0; + return true; + } + if (isa(body)) { + out << 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0; + return true; + } + return false; + } + if (op.getUnitaryMatrix4x4(out)) { + return true; + } + auto dynamic = op.getUnitaryMatrix(); + if (!dynamic || dynamic->rows() != 4 || dynamic->cols() != 4) { + return false; + } + out = *dynamic; + return true; +} + +std::optional +computeTwoQubitUnitaryFromModule(const OwningOpRef& moduleOp) { + ModuleOp module = moduleOp.get(); + if (!module) { + return std::nullopt; + } + Eigen::Matrix4cd unitary = Eigen::Matrix4cd::Identity(); + llvm::DenseMap qubitIds; + std::size_t nextQubitId = 0; + + for (auto func : module.getOps()) { + for (auto& block : func.getBlocks()) { + for (auto& rawOp : block.getOperations()) { + if (auto alloc = dyn_cast(&rawOp)) { + if (nextQubitId >= 2) { + return std::nullopt; + } + qubitIds.try_emplace(alloc.getResult(), nextQubitId++); + } + } + } + } + + auto getQubitId = [&](Value qubit) -> std::optional { + auto it = qubitIds.find(qubit); + if (it == qubitIds.end()) { + return std::nullopt; + } + return it->second; + }; + + for (auto func : module.getOps()) { + for (auto& block : func.getBlocks()) { + for (auto& rawOp : block.getOperations()) { + auto op = dyn_cast(&rawOp); + if (!op) { + continue; + } + if (isa(op.getOperation())) { + continue; + } + + if (op.isSingleQubit()) { + auto qid = getQubitId(op.getInputQubit(0)); + if (!qid) { + return std::nullopt; + } + Eigen::Matrix2cd oneQ; + if (!extractSingleQubitMatrix(op, oneQ)) { + return std::nullopt; + } + unitary = qco::decomposition::expandToTwoQubits(oneQ, *qid) * unitary; + qubitIds[op.getOutputQubit(0)] = *qid; + continue; + } + + if (op.isTwoQubit()) { + auto q0id = getQubitId(op.getInputQubit(0)); + auto q1id = getQubitId(op.getInputQubit(1)); + if (!q0id || !q1id) { + return std::nullopt; + } + Eigen::Matrix4cd twoQ; + if (!extractTwoQubitMatrix(op, twoQ)) { + return std::nullopt; + } + unitary = + expandTwoQToN(twoQ, *q0id, *q1id, /*numQubits=*/2) * unitary; + qubitIds[op.getOutputQubit(0)] = *q0id; + qubitIds[op.getOutputQubit(1)] = *q1id; + continue; + } + } + } + } + + if (nextQubitId != 2) { + return std::nullopt; + } + return unitary; +} + +/// Kronecker-embed ``m`` on wire ``q`` into a ``2^N``-dim unitary (same index +/// bit order as QCO 4×4 matrices: wire 0 is the high bit). +Eigen::MatrixXcd expandOneQToN(const Eigen::Matrix2cd& m, std::size_t q, + std::size_t numQubits) { + const auto dim = static_cast(1ULL << numQubits); + Eigen::MatrixXcd full = Eigen::MatrixXcd::Zero(dim, dim); + const auto bit = numQubits - 1 - q; + const std::size_t mask = 1ULL << bit; + for (Eigen::Index col = 0; col < dim; ++col) { + const auto colIdx = static_cast(col); + const std::size_t sIn = (colIdx >> bit) & 1ULL; + const std::size_t rest = colIdx & ~mask; + for (std::size_t sOut = 0; sOut < 2; ++sOut) { + const auto row = static_cast(rest | (sOut << bit)); + full(row, col) = + m(static_cast(sOut), static_cast(sIn)); + } + } + return full; +} + +/// Embed ``m`` on wires ``q0``, ``q1`` into a ``2^N``-dim unitary. +Eigen::MatrixXcd expandTwoQToN(const Eigen::Matrix4cd& m, std::size_t q0, + std::size_t q1, std::size_t numQubits) { + const auto dim = static_cast(1ULL << numQubits); + Eigen::MatrixXcd full = Eigen::MatrixXcd::Zero(dim, dim); + const auto bit0 = numQubits - 1 - q0; + const auto bit1 = numQubits - 1 - q1; + const std::size_t mask0 = 1ULL << bit0; + const std::size_t mask1 = 1ULL << bit1; + const std::size_t maskBoth = mask0 | mask1; + for (Eigen::Index col = 0; col < dim; ++col) { + const auto colIdx = static_cast(col); + const std::size_t s0In = (colIdx >> bit0) & 1ULL; + const std::size_t s1In = (colIdx >> bit1) & 1ULL; + // 2-bit index for the pair matches QCO 4×4 row/column layout. + const std::size_t smallIn = (s0In << 1) | s1In; + const std::size_t rest = colIdx & ~maskBoth; + for (std::size_t smallOut = 0; smallOut < 4; ++smallOut) { + const std::size_t s0Out = (smallOut >> 1) & 1ULL; + const std::size_t s1Out = smallOut & 1ULL; + const auto row = + static_cast(rest | (s0Out << bit0) | (s1Out << bit1)); + full(row, col) = m(static_cast(smallOut), + static_cast(smallIn)); + } + } + return full; +} + +/// Full ``2^N`` unitary from a QCO module (``alloc`` / ``static``, 1q/2q +/// unitaries, ``ctrl`` with X/Z body). ``std::nullopt`` on unsupported ops or +/// if ``N`` exceeds ``maxQubits``. +std::optional +computeNQubitUnitaryFromModule(const OwningOpRef& moduleOp, + std::size_t maxQubits) { + ModuleOp module = moduleOp.get(); + if (!module) { + return std::nullopt; + } + + llvm::DenseMap qubitIds; + std::size_t numQubits = 0; + + for (auto func : module.getOps()) { + for (auto& block : func.getBlocks()) { + for (auto& rawOp : block.getOperations()) { + if (auto alloc = dyn_cast(&rawOp)) { + if (numQubits >= maxQubits) { + return std::nullopt; + } + qubitIds.try_emplace(alloc.getResult(), numQubits++); + } else if (auto staticOp = dyn_cast(&rawOp)) { + const auto idx = static_cast(staticOp.getIndex()); + if (idx >= maxQubits) { + return std::nullopt; + } + qubitIds.try_emplace(staticOp.getResult(), idx); + numQubits = std::max(numQubits, idx + 1); + } + } + } + } + + if (numQubits == 0) { + return std::nullopt; + } + + const auto dim = static_cast(1ULL << numQubits); + Eigen::MatrixXcd unitary = Eigen::MatrixXcd::Identity(dim, dim); + + auto getQubitId = [&](Value qubit) -> std::optional { + auto it = qubitIds.find(qubit); + if (it == qubitIds.end()) { + return std::nullopt; + } + return it->second; + }; + + for (auto func : module.getOps()) { + for (auto& block : func.getBlocks()) { + for (auto& rawOp : block.getOperations()) { + auto op = dyn_cast(&rawOp); + if (!op) { + continue; + } + if (isa(op.getOperation())) { + continue; + } + + if (op.isSingleQubit()) { + auto qid = getQubitId(op.getInputQubit(0)); + if (!qid) { + return std::nullopt; + } + Eigen::Matrix2cd oneQ; + if (!extractSingleQubitMatrix(op, oneQ)) { + return std::nullopt; + } + unitary = expandOneQToN(oneQ, *qid, numQubits) * unitary; + qubitIds[op.getOutputQubit(0)] = *qid; + continue; + } + + if (op.isTwoQubit()) { + auto q0id = getQubitId(op.getInputQubit(0)); + auto q1id = getQubitId(op.getInputQubit(1)); + if (!q0id || !q1id) { + return std::nullopt; + } + Eigen::Matrix4cd twoQ; + if (!extractTwoQubitMatrix(op, twoQ)) { + return std::nullopt; + } + unitary = expandTwoQToN(twoQ, *q0id, *q1id, numQubits) * unitary; + qubitIds[op.getOutputQubit(0)] = *q0id; + qubitIds[op.getOutputQubit(1)] = *q1id; + continue; + } + } + } + } + + return unitary; +} + +} // namespace mlir::qco::native_synth_test diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.h b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.h new file mode 100644 index 0000000000..975c91ab51 --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.h @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace mlir::qco::native_synth_test { + +template +bool isEquivalentUpToGlobalPhase(const Matrix& lhs, const Matrix& rhs, + double atol = 1e-10) { + const auto overlap = (rhs.adjoint() * lhs).trace(); + if (std::abs(overlap) <= atol) { + return false; + } + const auto factor = overlap / std::abs(overlap); + return lhs.isApprox(factor * rhs, atol); +} + +[[nodiscard]] std::complex phasedAmplitude(double magnitude, + double phase); +[[nodiscard]] Eigen::Matrix2cd u3Matrix(double theta, double phi, + double lambda); +[[nodiscard]] bool isUnitary(const Eigen::Matrix2cd& m, double atol = 1e-10); +[[nodiscard]] std::optional evaluateConstF64(Value value); +bool extractSingleQubitMatrix(qco::UnitaryOpInterface op, + Eigen::Matrix2cd& out); +bool extractTwoQubitMatrix(qco::UnitaryOpInterface op, Eigen::Matrix4cd& out); +[[nodiscard]] std::optional +computeTwoQubitUnitaryFromModule(const OwningOpRef& moduleOp); +[[nodiscard]] Eigen::MatrixXcd +expandOneQToN(const Eigen::Matrix2cd& m, std::size_t q, std::size_t numQubits); +[[nodiscard]] Eigen::MatrixXcd expandTwoQToN(const Eigen::Matrix4cd& m, + std::size_t q0, std::size_t q1, + std::size_t numQubits); +[[nodiscard]] std::optional +computeNQubitUnitaryFromModule(const OwningOpRef& moduleOp, + std::size_t maxQubits = 6); + +} // namespace mlir::qco::native_synth_test diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_custom_menus.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_custom_menus.cpp new file mode 100644 index 0000000000..6939459da7 --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_custom_menus.cpp @@ -0,0 +1,504 @@ +/* + * 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 + */ + +// Custom native-gate menus, randomized equivalence, and IBM-fractional stress +// circuits for the native-gate synthesis pass. + +#include "native_synthesis_pass_test_fixture.h" + +#include + +#include +#include +#include +#include +#include +#include + +using namespace mlir; +using namespace mlir::qco; +using namespace mlir::qco::native_synth_test; + +namespace { + +std::vector splitCSV(const std::string& s) { + std::vector out; + std::string cur; + for (const char ch : s) { + if (ch == ',') { + if (!cur.empty()) { + out.push_back(cur); + cur.clear(); + } + continue; + } + if (ch != ' ' && ch != '\t' && ch != '\n' && ch != '\r') { + cur.push_back( + static_cast(std::tolower(static_cast(ch)))); + } + } + if (!cur.empty()) { + out.push_back(cur); + } + return out; +} + +struct CustomMenuSpec { + std::string menuCsv; + bool allowCx = false; + bool allowCz = false; + bool allowU = false; + bool allowX = false; + bool allowSX = false; + bool allowRZ = false; + bool allowRX = false; + bool allowRY = false; + bool allowR = false; + bool allowRzz = false; +}; + +CustomMenuSpec parseCustomMenu(const std::string& csv) { + CustomMenuSpec spec; + spec.menuCsv = csv; + for (const auto& tok : splitCSV(csv)) { + if (tok == "u") { + spec.allowU = true; + } else if (tok == "x") { + spec.allowX = true; + } else if (tok == "sx") { + spec.allowSX = true; + } else if (tok == "rz" || tok == "p") { + // ``p`` is an alias for Z-axis rotation in ``native-gates`` (see pass + // docs). + spec.allowRZ = true; + } else if (tok == "rx") { + spec.allowRX = true; + } else if (tok == "ry") { + spec.allowRY = true; + } else if (tok == "r") { + spec.allowR = true; + } else if (tok == "cx") { + spec.allowCx = true; + } else if (tok == "cz") { + spec.allowCz = true; + } else if (tok == "rzz") { + spec.allowRzz = true; + } + } + return spec; +} + +bool onlyAllowsMenuNativeOps(ModuleOp moduleOp, const CustomMenuSpec& spec) { + bool ok = true; + moduleOp.walk([&](Operation* op) { + if (!ok) { + return; + } + if (!isa(op)) { + return; + } + // Non-synthesized helper ops are allowed to remain. + if (isa(op)) { + return; + } + if (isa(op)) { + return; + } + + // Treat `p` as a phase/Z-rotation alias when `rz` is allowed. + if (isa(op)) { + ok = spec.allowRZ; + return; + } + + if (isa(op)) { + ok = spec.allowU; + return; + } + if (isa(op)) { + // `cx` is represented as a `qco.ctrl` with a `qco.x` in the body region. + if (isa_and_present(op->getParentOp())) { + ok = spec.allowCx; + } else { + ok = spec.allowX; + } + return; + } + if (isa(op)) { + ok = spec.allowSX; + return; + } + if (isa(op)) { + ok = spec.allowRZ; + return; + } + if (isa(op)) { + // Some decomposition paths treat `rx(pi)` as an `x`-family primitive. + ok = spec.allowRX || (spec.allowX && spec.allowSX && spec.allowRZ); + return; + } + if (isa(op)) { + ok = spec.allowRY; + return; + } + if (isa(op)) { + // `cz` is represented as a `qco.ctrl` with a `qco.z` in the body region. + if (isa_and_present(op->getParentOp())) { + ok = spec.allowCz; + } else { + ok = false; + } + return; + } + if (isa(op)) { + ok = spec.allowR; + return; + } + if (isa(op)) { + ok = spec.allowRzz; + return; + } + if (auto ctrl = dyn_cast(op)) { + if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { + ok = false; + return; + } + Operation* body = ctrl.getBodyUnitary().getOperation(); + if (isa(body)) { + ok = spec.allowCx; + return; + } + if (isa(body)) { + ok = spec.allowCz; + return; + } + ok = false; + return; + } + ok = false; + }); + return ok; +} + +} // namespace + +TEST_F(NativeSynthesisPassTest, RandomizedCustomMenusAndCircuitsAreEquivalent) { + // Sample many valid custom menus and generate matching random input circuits. + // For each case, we assert that native synthesis (a) succeeds, (b) emits only + // ops allowed by the menu, and (c) preserves the 2-qubit unitary up to global + // phase. + std::mt19937 rng(0xC0FFEE); + std::uniform_real_distribution angle(-1.0, 1.0); + std::uniform_int_distribution stepsDist(4, 14); + std::uniform_int_distribution gateDist(0, 9); + std::uniform_int_distribution whichQubit(0, 1); + + // Menus are chosen from known-valid families that the pass supports. + const std::vector menuPool = { + "u,cx", "u,cz", "x,sx,rz,rx,cx", "rx,rz,cx", "rx,ry,cx", + "ry,rz,cz", "r,cz", "u,rx,rz,cx,cz", "u,rx,rz,cx", + }; + std::uniform_int_distribution menuDist(0, menuPool.size() - 1); + + constexpr int numCases = 18; + for (int caseIdx = 0; caseIdx < numCases; ++caseIdx) { + const std::string& menuCsv = menuPool[menuDist(rng)]; + const auto menuSpec = parseCustomMenu(menuCsv); + + // Build an input circuit that uses only two qubits and (if present) only + // the entangler types allowed by the menu. Use a mix of operations that the + // pass is expected to rewrite into the menu. + auto buildCircuit = [&]() { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + + const int steps = stepsDist(rng); + for (int i = 0; i < steps; ++i) { + const auto q = (whichQubit(rng) == 0) ? q0 : q1; + + // Choose operations based on the menu family to avoid generating inputs + // that are not exactly synthesizable with the configured gateset. + if (menuSpec.allowU) { + // Keep input gates within the robust unitary evaluator set. + switch (gateDist(rng) % 5) { + case 0: + builder.rz(angle(rng), q); + break; + case 1: + builder.rx(angle(rng), q); + break; + case 2: + builder.ry(angle(rng), q); + break; + case 3: + builder.p(angle(rng), q); + break; + case 4: + if (menuSpec.allowCz) { + builder.cz(q0, q1); + } else if (menuSpec.allowCx) { + builder.cx(q0, q1); + } else { + builder.rz(angle(rng), q); + } + break; + default: + break; + } + } else if (menuSpec.allowR && menuSpec.allowCz && !menuSpec.allowCx) { + // Minimal r/cz menu: generate only operations directly expressible in + // that gateset so synthesis is required to succeed. + switch (gateDist(rng) % 4) { + case 0: + builder.r(angle(rng), angle(rng), q); + break; + case 1: + // X/Y-like rotations expressed via r(theta, phi). + builder.r(std::numbers::pi, angle(rng), q); + break; + case 2: + builder.r(angle(rng), angle(rng), q); + break; + case 3: + builder.cz(q0, q1); + break; + default: + break; + } + } else if (menuSpec.allowRX && menuSpec.allowRY && menuSpec.allowCx && + !menuSpec.allowRZ) { + // Axis-pair RX/RY with CX: avoid Z-axis primitives. + switch (gateDist(rng) % 6) { + case 0: + builder.rx(angle(rng), q); + break; + case 1: + builder.ry(angle(rng), q); + break; + case 2: + builder.rx(std::numbers::pi, q); + break; + case 3: + builder.ry(std::numbers::pi, q); + break; + case 4: + builder.ry(angle(rng), q); + break; + case 5: + builder.cx(q0, q1); + break; + default: + break; + } + } else if (menuSpec.allowRX && menuSpec.allowRZ && menuSpec.allowCx) { + // Axis-pair RX/RZ with CX. + switch (gateDist(rng) % 6) { + case 0: + builder.rx(angle(rng), q); + break; + case 1: + builder.rz(angle(rng), q); + break; + case 2: + builder.rx(std::numbers::pi, q); + break; + case 3: + builder.rz(std::numbers::pi, q); + break; + case 4: + builder.rz(angle(rng), q); + break; + case 5: + builder.cx(q0, q1); + break; + default: + break; + } + } else if (menuSpec.allowRY && menuSpec.allowRZ && menuSpec.allowCz) { + // Axis-pair RY/RZ with CZ. + switch (gateDist(rng) % 6) { + case 0: + builder.ry(angle(rng), q); + break; + case 1: + builder.rz(angle(rng), q); + break; + case 2: + builder.ry(std::numbers::pi, q); + break; + case 3: + builder.rz(std::numbers::pi, q); + break; + case 4: + builder.rz(angle(rng), q); + break; + case 5: + builder.cz(q0, q1); + break; + default: + break; + } + } else { + // IBM-basic-ish menus (x,sx,rz[,rx],cx): use Z/SX patterns + CX. + switch (gateDist(rng) % 7) { + case 0: + builder.rz(angle(rng), q); + break; + case 1: + builder.p(angle(rng), q); + break; + case 2: + builder.rz(angle(rng), q); + break; + case 3: + builder.rx(menuSpec.allowRX ? angle(rng) : std::numbers::pi, q); + break; + case 4: + builder.rz(angle(rng), q); + break; + case 5: + if (menuSpec.allowRX) { + builder.rx(angle(rng), q); + } else { + builder.p(angle(rng), q); + } + break; + case 6: + if (menuSpec.allowCx) { + builder.cx(q0, q1); + } else { + builder.rz(angle(rng), q); + } + break; + default: + break; + } + } + } + + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }; + + // Build the random circuit exactly once, then clone it for the expected and + // synthesized paths so the unitary comparison is meaningful. + auto input = buildCircuit(); + const auto inputText = moduleToString(input); + + auto expected = + mlir::parseSourceString(inputText, context.get()); + ASSERT_TRUE(expected) << "case=" << caseIdx; + runQcToQco(expected); + const auto expectedUnitary = computeTwoQubitUnitaryFromModule(expected); + if (!expectedUnitary.has_value()) { + ADD_FAILURE() << "Failed to reconstruct expected unitary for case=" + << caseIdx << " menu=" << menuCsv << "\nIR:\n" + << moduleToString(expected); + continue; + } + + auto synthesized = + mlir::parseSourceString(inputText, context.get()); + ASSERT_TRUE(synthesized) << "case=" << caseIdx; + { + PassManager pm(synthesized->getContext()); + pm.addPass(createQCToQCO()); + pm.addPass( + qco::createNativeGateSynthesisPass(qco::NativeGateSynthesisOptions{ + .nativeGates = menuCsv, + .scoreWeightTwoQ = 1.0, + .scoreWeightOneQ = 0.1, + .scoreWeightDepth = 0.01, + })); + if (failed(pm.run(*synthesized))) { + ADD_FAILURE() << "Native synthesis failed for menu=" << menuCsv + << " case=" << caseIdx << "\nQC/QCO IR:\n" + << moduleToString(synthesized); + continue; + } + } + + EXPECT_TRUE(onlyAllowsMenuNativeOps(synthesized.get(), menuSpec)) + << "menu=" << menuCsv << "\nIR:\n" + << moduleToString(synthesized); + + const auto synthesizedUnitary = + computeTwoQubitUnitaryFromModule(synthesized); + ASSERT_TRUE(synthesizedUnitary.has_value()) << "case=" << caseIdx; + EXPECT_TRUE( + isEquivalentUpToGlobalPhase(*expectedUnitary, *synthesizedUnitary)) + << "menu=" << menuCsv << " case=" << caseIdx; + } +} + +TEST_F(NativeSynthesisPassTest, + LargeCircuitEquivalentAndNativeGatesIbmFractional) { + auto buildStressCircuit = [&](MLIRContext* ctx) { + mlir::qc::QCProgramBuilder builder(ctx); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + builder.h(q0); + builder.sxdg(q0); + builder.ry(-0.22, q1); + builder.swap(q0, q1); + builder.rxx(0.53, q0, q1); + builder.ecr(q0, q1); + builder.p(0.31, q0); + builder.rzz(-0.44, q0, q1); + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }; + expectEquivalentAndNativeAfterSynthesis( + [&] { return buildStressCircuit(context.get()); }, "x,sx,rz,rx,rzz,cz", + &NativeSynthesisPassTest::onlyIbmFractionalOps, + computeTwoQubitUnitaryFromModule); +} + +TEST_F(NativeSynthesisPassTest, + AllGateFamiliesEquivalentAndNativeIbmFractional) { + expectEquivalentAndNativeAfterSynthesis( + [&] { return buildIbmFractionalAllGateFamiliesCircuit(); }, + "x,sx,rz,rx,rzz,cz", &NativeSynthesisPassTest::onlyIbmFractionalOps, + computeTwoQubitUnitaryFromModule); +} + +TEST_F(NativeSynthesisPassTest, XXPlusMinusYYEquivalentAndNativeIbmFractional) { + constexpr const char* kIbmFrac = "x,sx,rz,rx,rzz,cz"; + const auto assertEquivalent = [&](auto buildBody) { + expectEquivalentAndNativeAfterSynthesis( + [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + buildBody(builder, q0, q1); + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }, + kIbmFrac, &NativeSynthesisPassTest::onlyIbmFractionalOps, + computeTwoQubitUnitaryFromModule); + }; + + assertEquivalent( + [](mlir::qc::QCProgramBuilder& b, mlir::Value q0, mlir::Value q1) { + b.h(q0); + b.sx(q1); + b.xx_plus_yy(0.52, -0.14, q0, q1); + b.rz(0.31, q0); + }); + assertEquivalent([](mlir::qc::QCProgramBuilder& b, mlir::Value q0, + mlir::Value q1) { b.xx_minus_yy(-0.37, 0.26, q0, q1); }); +} diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_fusion.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_fusion.cpp new file mode 100644 index 0000000000..1dfcb7770d --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_fusion.cpp @@ -0,0 +1,610 @@ +/* + * 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 + */ + +// 1q run merging, 2q block consolidation, and RZX profile sweeps for the +// native-gate synthesis pass. Linked with sibling `test_native_synthesis_*.cpp` +// sources into `mqt-core-mlir-unittest-native-synthesis`. + +#include "native_synthesis_pass_test_fixture.h" + +#include + +using namespace mlir; +using namespace mlir::qco; +using namespace mlir::qco::native_synth_test; + +namespace { +// Count ops of a given MLIR op type across a module; used to assert the +// effects of the 1q-run-merging pre-synthesis step on concrete programs. +template +std::size_t countOpsOfTypeInModule(const OwningOpRef& moduleOp) { + std::size_t count = 0; + moduleOp.get()->walk([&](mlir::Operation* op) { + if (isa(op)) { + ++count; + } + }); + return count; +} +} // namespace + +// --- 1q-run-merging pre-synthesis step --- +// +// The tests below exercise the in-pass run merging that fuses adjacent +// single-qubit `UnitaryOpInterface` ops on the same wire before per-op +// native-gate emission. They cover (a) the reductions unlocked by fusion, +// (b) that the fusion respects boundaries (CX, barrier, multi-use), and +// (c) unitary equivalence over longer mixed chains. + +TEST_F(NativeSynthesisPassTest, OneQRunMergingCollapsesHadamardZHadamardToX) { + // H * Z * H = X (up to global phase). With fusion enabled, the ibm-basic + // emitter hits the ZSXX X-shortcut and emits a single X, whereas without + // fusion we would expect at least 3 RZ gates from two H decompositions and + // the Z. + auto buildFn = [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + builder.h(q0); + builder.z(q0); + builder.h(q0); + builder.dealloc(q0); + return builder.finalize(); + }; + + auto moduleOp = buildFn(); + runNativeSynthesis(moduleOp, "x,sx,rz,cx"); + EXPECT_TRUE(onlyIbmBasicCxOps(moduleOp)); + EXPECT_EQ(countOpsOfTypeInModule(moduleOp), 1U); + EXPECT_EQ(countOpsOfTypeInModule(moduleOp), 0U); + EXPECT_EQ(countOpsOfTypeInModule(moduleOp), 0U); +} + +TEST_F(NativeSynthesisPassTest, OneQRunMergingCancelsAdjacentSelfInverses) { + // H * H = I. Fusion collapses the run to no 1q ops at all. + auto buildFn = [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + builder.h(q0); + builder.h(q0); + builder.dealloc(q0); + return builder.finalize(); + }; + + auto moduleOp = buildFn(); + runNativeSynthesis(moduleOp, "x,sx,rz,cx"); + EXPECT_TRUE(onlyIbmBasicCxOps(moduleOp)); + EXPECT_EQ(countOpsOfTypeInModule(moduleOp), 0U); + EXPECT_EQ(countOpsOfTypeInModule(moduleOp), 0U); + EXPECT_EQ(countOpsOfTypeInModule(moduleOp), 0U); +} + +TEST_F(NativeSynthesisPassTest, OneQRunMergingReducesMixedChainToSingleU) { + // A long chain of distinct 1q ops on a single wire still collapses to a + // single UOp on the generic-u3-cx profile via fusion, regardless of the + // mix of non-native ops in the input. + auto buildFn = [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + builder.h(q0); + builder.s(q0); + builder.t(q0); + builder.y(q0); + builder.sx(q0); + builder.dealloc(q0); + return builder.finalize(); + }; + + auto moduleOp = buildFn(); + runNativeSynthesis(moduleOp, "u,cx"); + EXPECT_TRUE(onlyGenericU3CxOps(moduleOp)); + EXPECT_EQ(countOpsOfTypeInModule(moduleOp), 1U); +} + +TEST_F(NativeSynthesisPassTest, OneQRunMergingDoesNotFuseAcrossCX) { + // H(q0); CX(q0,q1); H(q0) must NOT be fused because CX breaks the run + // on q0. Equivalence still holds; to witness that fusion did not happen + // we assert we still see >=2 SX gates (one from each Hadamard expansion). + expectEquivalentAndNativeAfterSynthesis( + [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + builder.h(q0); + builder.cx(q0, q1); + builder.h(q0); + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }, + "x,sx,rz,cx", &NativeSynthesisPassTest::onlyIbmBasicCxOps, + computeTwoQubitUnitaryFromModule); + + auto moduleOp = [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + builder.h(q0); + builder.cx(q0, q1); + builder.h(q0); + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }(); + runNativeSynthesis(moduleOp, "x,sx,rz,cx"); + // Each H decomposes to rz(pi/2) sx rz(pi/2); without fusion we get two + // separate decompositions => at least 2 SX gates total. + EXPECT_GE(countOpsOfTypeInModule(moduleOp), 2U); +} + +TEST_F(NativeSynthesisPassTest, OneQRunMergingDoesNotFuseAcrossBarrier) { + // A barrier between two 1q ops on the same wire interrupts the run: + // `BarrierOp` is explicitly excluded from fusibility and its use of the + // qubit breaks the single-use precondition on the intermediate value. + auto moduleOp = [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + builder.h(q0); + builder.barrier({q0}); + builder.h(q0); + builder.dealloc(q0); + return builder.finalize(); + }(); + runNativeSynthesis(moduleOp, "x,sx,rz,cx"); + EXPECT_TRUE(onlyIbmBasicCxOps(moduleOp)); + // Two separate H decompositions survive => at least 2 SX gates. + EXPECT_GE(countOpsOfTypeInModule(moduleOp), 2U); +} + +TEST_F(NativeSynthesisPassTest, OneQRunMergingSkipsFullyNativeRuns) { + // A run consisting entirely of ops that are already native to the + // ibm-basic-cx profile (rz; sx; rz) is pass-through: the cost gate only + // fuses a fully-native run when fusion would produce strictly fewer ops + // than the original run. For `rz; sx; rz` the ZSXX decomposition of the + // fused matrix is itself three ops, so the run is left untouched. + auto moduleOp = [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + builder.rz(0.4, q0); + builder.sx(q0); + builder.rz(-0.9, q0); + builder.dealloc(q0); + return builder.finalize(); + }(); + runNativeSynthesis(moduleOp, "x,sx,rz,cx"); + EXPECT_TRUE(onlyIbmBasicCxOps(moduleOp)); + EXPECT_EQ(countOpsOfTypeInModule(moduleOp), 2U); + EXPECT_EQ(countOpsOfTypeInModule(moduleOp), 1U); +} + +TEST_F(NativeSynthesisPassTest, + OneQRunMergingCostGateFusesFullyNativeUChainGenericU3) { + // Phase-A cost-gate refinement (fully-native path): two adjacent native + // `u` ops on the same wire fuse into a single `u` because U3 mode always + // emits exactly one gate per fused 2x2 unitary. Without the cost gate, + // the fully-native run would be skipped; without fusion, the run would + // survive as two ops because there is no `MergeSubsequentU` canonicalizer. + auto moduleOp = [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + builder.u(0.3, 0.1, -0.2, q0); + builder.u(-0.5, 0.7, 0.4, q0); + builder.dealloc(q0); + return builder.finalize(); + }(); + runNativeSynthesis(moduleOp, "u,cx"); + EXPECT_TRUE(onlyGenericU3CxOps(moduleOp)); + EXPECT_EQ(countOpsOfTypeInModule(moduleOp), 1U); +} + +TEST_F(NativeSynthesisPassTest, OneQRunMergingEmitsGlobalPhaseOnU3) { + // Phase-A GPhase refinement: fusing `T; S` on the generic-u3-cx profile + // composes to a diagonal matrix whose SU(2) normalisation sheds a + // non-trivial residual phase of `3*pi/8`. The fusion emitter preserves + // the phase via a `qco.gphase` op so the synthesized IR reconstructs the + // original unitary exactly (not merely up to global phase). `T; S` is + // chosen over `T; T` because `MergeSubsequentT` would otherwise fold the + // latter to `S` upstream: `T; S` is not matched by any existing + // canonicalizer, so this test exercises the fusion path unambiguously. + auto moduleOp = [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + builder.t(q0); + builder.s(q0); + builder.dealloc(q0); + return builder.finalize(); + }(); + runNativeSynthesis(moduleOp, "u,cx"); + EXPECT_TRUE(onlyGenericU3CxOps(moduleOp)); + EXPECT_EQ(countOpsOfTypeInModule(moduleOp), 1U); + EXPECT_EQ(countOpsOfTypeInModule(moduleOp), 1U); +} + +TEST_F(NativeSynthesisPassTest, + OneQRunMergingOmitsGPhaseWhenResidualIsTrivial) { + // Negative complement of OneQRunMergingEmitsGlobalPhaseOnU3: each U3 with + // `lambda = -phi` has det = 1, so the composed unitary also has det = 1. + // The fusion path computes an SU(2)-normalised decomposition whose + // `globalPhase` is negligible, and `emitGPhaseIfNonTrivial` must skip + // emitting any `qco.gphase` op. + auto moduleOp = [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + builder.u(0.3, 0.2, -0.2, q0); + builder.u(0.5, 0.4, -0.4, q0); + builder.dealloc(q0); + return builder.finalize(); + }(); + runNativeSynthesis(moduleOp, "u,cx"); + EXPECT_TRUE(onlyGenericU3CxOps(moduleOp)); + EXPECT_EQ(countOpsOfTypeInModule(moduleOp), 1U); + EXPECT_EQ(countOpsOfTypeInModule(moduleOp), 0U); +} + +TEST_F(NativeSynthesisPassTest, + OneQRunMergingLongMixedChainEquivalentAcrossProfiles) { + // A ten-op mixed chain on a single wire must fuse to the correct unitary + // on every CX-friendly reference profile (see + // ``fiveCxEntanglerEquivalenceProfiles``), excluding IQM-default ``r,cz``, + // which uses a different two-qubit path. + auto buildFn = [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + builder.h(q0); + builder.t(q0); + builder.rx(0.37, q0); + builder.s(q0); + builder.ry(-0.21, q0); + builder.h(q0); + builder.z(q0); + builder.rz(0.52, q0); + builder.sx(q0); + builder.y(q0); + builder.cx(q0, q1); + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }; + + const auto profiles = + NativeSynthesisPassTest::fiveCxEntanglerEquivalenceProfiles(); + for (const auto& pc : profiles) { + // Expected and synthesized unitaries both come from the permissive + // default helper, which understands the full alphabet the builder emits + // and the R/RX/RY/RZ/U/P gates produced by synthesis. + auto expected = buildFn(); + runQcToQco(expected); + const auto expectedUnitary = computeTwoQubitUnitaryFromModule(expected); + ASSERT_TRUE(expectedUnitary.has_value()) + << "native-gates=" << pc.nativeGates; + + auto synth = buildFn(); + runNativeSynthesis(synth, pc.nativeGates); + EXPECT_TRUE(pc.isNative(synth)) << "native-gates=" << pc.nativeGates; + const auto synthUnitary = computeTwoQubitUnitaryFromModule(synth); + ASSERT_TRUE(synthUnitary.has_value()) << "native-gates=" << pc.nativeGates; + EXPECT_TRUE(isEquivalentUpToGlobalPhase(*expectedUnitary, *synthUnitary)) + << "native-gates=" << pc.nativeGates; + } +} + +// --- 2q-block-consolidation pre-synthesis step (Phase B) --- +// +// These tests exercise the in-pass 2q block consolidation that collects +// adjacent two-qubit ops (plus interleaved single-qubit ops) acting on the +// same pair of wires, composes a 4x4 unitary, and re-synthesizes the block +// via `TwoQubitBasisDecomposer`. They cover (a) reductions unlocked by +// consolidation, (b) fully-native blocks that are only rewritten when +// strictly shorter, and (c) boundary conditions such as wire swaps and +// interleaved barriers. + +TEST_F(NativeSynthesisPassTest, TwoQBlockConsolidationCancelsAdjacentCx) { + // Two CX(q0,q1) cancel to the identity. The consolidation step folds the + // pair into a trivial 4x4, which the decomposer realises with zero basis + // gate uses. + auto buildFn = [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + builder.cx(q0, q1); + builder.cx(q0, q1); + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }; + + auto expected = buildFn(); + runQcToQco(expected); + const auto expectedUnitary = computeTwoQubitUnitaryFromModule(expected); + ASSERT_TRUE(expectedUnitary.has_value()); + + auto synth = buildFn(); + runNativeSynthesis(synth, "u,cx"); + EXPECT_TRUE(onlyGenericU3CxOps(synth)); + EXPECT_EQ(countOpsOfTypeInModule(synth), 0U); + const auto synthUnitary = computeTwoQubitUnitaryFromModule(synth); + ASSERT_TRUE(synthUnitary.has_value()); + EXPECT_TRUE(isEquivalentUpToGlobalPhase(*expectedUnitary, *synthUnitary)); +} + +TEST_F(NativeSynthesisPassTest, + TwoQBlockConsolidationFusesCxThroughInterleavedOneQOps) { + // A non-native block containing interleaved single-qubit ops on the two + // wires must consolidate into a single 4x4 unitary that the decomposer + // synthesises with the target's entangler (CX). The resulting circuit + // must be unitarily equivalent to the original. + auto buildFn = [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + builder.h(q0); + builder.cx(q0, q1); + builder.t(q1); + builder.s(q0); + builder.cx(q0, q1); + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }; + + auto expected = buildFn(); + runQcToQco(expected); + const auto expectedUnitary = computeTwoQubitUnitaryFromModule(expected); + ASSERT_TRUE(expectedUnitary.has_value()); + + auto synth = buildFn(); + runNativeSynthesis(synth, "u,cx"); + EXPECT_TRUE(onlyGenericU3CxOps(synth)); + const auto synthUnitary = computeTwoQubitUnitaryFromModule(synth); + ASSERT_TRUE(synthUnitary.has_value()); + EXPECT_TRUE(isEquivalentUpToGlobalPhase(*expectedUnitary, *synthUnitary)); +} + +TEST_F(NativeSynthesisPassTest, + TwoQBlockConsolidationStopsAtDifferentPairBoundary) { + // Consolidation must not cross a 2q op that touches a different pair of + // wires. We arrange two back-to-back `cx(q0, q1)` separated by a + // `cx(q1, q2)` so block consolidation cannot fuse the outer pair into a + // single identity; equivalence still has to hold. + auto buildFn = [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + const auto q2 = builder.allocQubit(); + builder.cx(q0, q1); + builder.cx(q1, q2); + builder.cx(q0, q1); + builder.dealloc(q0); + builder.dealloc(q1); + builder.dealloc(q2); + return builder.finalize(); + }; + + auto synth = buildFn(); + runNativeSynthesis(synth, "u,cx"); + EXPECT_TRUE(onlyGenericU3CxOps(synth)); + // At least the middle CX(q1,q2) must survive because its pair differs + // from the outer CX(q0,q1) block; consolidation cannot eliminate it. + EXPECT_GE(countOpsOfTypeInModule(synth), 1U); +} + +TEST_F(NativeSynthesisPassTest, + TwoQBlockConsolidationDoesNotFuseAcrossBarrier) { + // A barrier between two CX(q0,q1) blocks must prevent them from being + // fused into a single block. Each CX stays an individual entangler. + auto buildFn = [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + builder.cx(q0, q1); + builder.barrier({q0, q1}); + builder.cx(q0, q1); + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }; + + auto synth = buildFn(); + runNativeSynthesis(synth, "u,cx"); + EXPECT_TRUE(onlyGenericU3CxOps(synth)); + // The barrier prevents block consolidation from cancelling the pair, so + // both CX ops survive as separate entanglers. + EXPECT_EQ(countOpsOfTypeInModule(synth), 2U); +} + +TEST_F(NativeSynthesisPassTest, TwoQBlockConsolidationHandlesSwappedWireOrder) { + // Three CXs in alternating direction form SWAP; consolidation must preserve + // the unitary. + auto buildFn = [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + builder.cx(q0, q1); + builder.cx(q1, q0); + builder.cx(q0, q1); + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }; + + auto expected = buildFn(); + runQcToQco(expected); + const auto expectedUnitary = computeTwoQubitUnitaryFromModule(expected); + ASSERT_TRUE(expectedUnitary.has_value()); + + auto synth = buildFn(); + runNativeSynthesis(synth, "u,cx"); + EXPECT_TRUE(onlyGenericU3CxOps(synth)); + const auto synthUnitary = computeTwoQubitUnitaryFromModule(synth); + ASSERT_TRUE(synthUnitary.has_value()); + EXPECT_TRUE(isEquivalentUpToGlobalPhase(*expectedUnitary, *synthUnitary)); +} + +TEST_F(NativeSynthesisPassTest, + TwoQBlockConsolidationEquivalentWhenBlockContainsDcx) { + // Convention audit: DCX is directional/asymmetric, so this checks that + // Phase-B block accumulation preserves operand ordering when a DCX appears + // inside an otherwise consolidatable block. + auto buildFn = [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + builder.h(q0); + builder.dcx(q0, q1); + builder.s(q1); + builder.cx(q0, q1); + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }; + + auto expected = buildFn(); + runQcToQco(expected); + const auto expectedUnitary = computeTwoQubitUnitaryFromModule(expected); + ASSERT_TRUE(expectedUnitary.has_value()); + + auto synth = buildFn(); + runNativeSynthesis(synth, "u,cx"); + EXPECT_TRUE(onlyGenericU3CxOps(synth)); + const auto synthUnitary = computeTwoQubitUnitaryFromModule(synth); + ASSERT_TRUE(synthUnitary.has_value()); + EXPECT_TRUE(isEquivalentUpToGlobalPhase(*expectedUnitary, *synthUnitary)); +} + +TEST_F(NativeSynthesisPassTest, + TwoQBlockConsolidationEquivalentWhenBlockContainsRzx) { + // Convention audit: RZX is directional/asymmetric. This test guards + // against BE/LE mismatches in mixed blocks containing RZX. + auto buildFn = [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + builder.x(q0); + builder.rzx(0.41, q0, q1); + builder.t(q1); + builder.cx(q0, q1); + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }; + + auto expected = buildFn(); + runQcToQco(expected); + const auto expectedUnitary = computeTwoQubitUnitaryFromModule(expected); + ASSERT_TRUE(expectedUnitary.has_value()); + + auto synth = buildFn(); + runNativeSynthesis(synth, "u,cx"); + EXPECT_TRUE(onlyGenericU3CxOps(synth)); + const auto synthUnitary = computeTwoQubitUnitaryFromModule(synth); + ASSERT_TRUE(synthUnitary.has_value()); + EXPECT_TRUE(isEquivalentUpToGlobalPhase(*expectedUnitary, *synthUnitary)); +} + +TEST_F(NativeSynthesisPassTest, + TwoQBlockConsolidationHandlesRzzOnIbmFractional) { + // Explicitly exercise a non-CX/CZ two-qubit gate inside a block on a + // profile that supports it natively. Consolidation may keep/reshape the + // block, but equivalence and profile validity must hold. + auto buildFn = [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + builder.h(q0); + builder.rzz(-0.29, q0, q1); + builder.s(q1); + builder.rzz(0.17, q0, q1); + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }; + + auto expected = buildFn(); + runQcToQco(expected); + const auto expectedUnitary = computeTwoQubitUnitaryFromModule(expected); + ASSERT_TRUE(expectedUnitary.has_value()); + + auto synth = buildFn(); + runNativeSynthesis(synth, "x,sx,rz,rx,rzz,cz"); + EXPECT_TRUE(onlyIbmFractionalOps(synth)); + const auto synthUnitary = computeTwoQubitUnitaryFromModule(synth); + ASSERT_TRUE(synthUnitary.has_value()); + EXPECT_TRUE(isEquivalentUpToGlobalPhase(*expectedUnitary, *synthUnitary)); +} + +TEST_F(NativeSynthesisPassTest, + RzxStandaloneSynthesisEquivalentAcrossProfiles) { + // Directed RZX tests (asymmetric 2q); both operand orders. + const auto profiles = NativeSynthesisPassTest::allNineEquivalenceProfiles(); + + // Representative generic and ``pi/2`` angles (operand order tested below). + const std::array angles{{0.41, std::numbers::pi / 2.0}}; + + for (const auto& profileCase : profiles) { + for (const double theta : angles) { + for (const bool swapOperands : {false, true}) { + auto buildFn = [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + if (swapOperands) { + builder.rzx(theta, q1, q0); + } else { + builder.rzx(theta, q0, q1); + } + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }; + + auto expected = buildFn(); + runQcToQco(expected); + const auto expectedUnitary = computeTwoQubitUnitaryFromModule(expected); + ASSERT_TRUE(expectedUnitary.has_value()) + << "native-gates=" << profileCase.nativeGates << " theta=" << theta + << " swapped=" << swapOperands; + + auto synth = buildFn(); + runNativeSynthesis(synth, profileCase.nativeGates); + EXPECT_TRUE(profileCase.isNative(synth)) + << "native-gates=" << profileCase.nativeGates << " theta=" << theta + << " swapped=" << swapOperands; + const auto synthUnitary = computeTwoQubitUnitaryFromModule(synth); + ASSERT_TRUE(synthUnitary.has_value()) + << "native-gates=" << profileCase.nativeGates << " theta=" << theta + << " swapped=" << swapOperands; + EXPECT_TRUE( + isEquivalentUpToGlobalPhase(*expectedUnitary, *synthUnitary)) + << "native-gates=" << profileCase.nativeGates << " theta=" << theta + << " swapped=" << swapOperands; + } + } + } +} diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_multi_qubit.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_multi_qubit.cpp new file mode 100644 index 0000000000..8514bc62a0 --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_multi_qubit.cpp @@ -0,0 +1,279 @@ +/* + * 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 + */ + +// Multi-qubit equivalence sweeps (3q circuit families, 5q stress) for the +// native-gate synthesis pass. + +#include "native_synthesis_pass_test_fixture.h" + +#include +#include + +using namespace mlir; +using namespace mlir::qco; +using namespace mlir::qco::native_synth_test; + +namespace { + +/// Controlled-phase decomposition: CP(θ) on (ctrl, tgt) expressed with only +/// single-qubit `p` and `cx`, which are supported by every targeted profile. +void emitControlledPhase(mlir::qc::QCProgramBuilder& builder, double theta, + Value ctrl, Value tgt) { + builder.p(theta / 2.0, ctrl); + builder.cx(ctrl, tgt); + builder.p(-theta / 2.0, tgt); + builder.cx(ctrl, tgt); + builder.p(theta / 2.0, tgt); +} + +/// Standard Clifford+T decomposition of CCX on (c1, c2, t). +void emitToffoli(mlir::qc::QCProgramBuilder& builder, Value c1, Value c2, + Value t) { + builder.h(t); + builder.cx(c2, t); + builder.tdg(t); + builder.cx(c1, t); + builder.t(t); + builder.cx(c2, t); + builder.tdg(t); + builder.cx(c1, t); + builder.t(c2); + builder.t(t); + builder.h(t); + builder.cx(c1, c2); + builder.t(c1); + builder.tdg(c2); + builder.cx(c1, c2); +} + +/// 3-qubit GHZ preparation: H on q0 then CX ladder. +OwningOpRef buildThreeQGhzCircuit(MLIRContext* context) { + mlir::qc::QCProgramBuilder builder(context); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + const auto q2 = builder.allocQubit(); + builder.h(q0); + builder.cx(q0, q1); + builder.cx(q1, q2); + builder.dealloc(q0); + builder.dealloc(q1); + builder.dealloc(q2); + return builder.finalize(); +} + +/// 3-qubit Toffoli via Clifford+T decomposition (15 gates). +OwningOpRef buildThreeQToffoliCircuit(MLIRContext* context) { + mlir::qc::QCProgramBuilder builder(context); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + const auto q2 = builder.allocQubit(); + emitToffoli(builder, q0, q1, q2); + builder.dealloc(q0); + builder.dealloc(q1); + builder.dealloc(q2); + return builder.finalize(); +} + +/// 3-qubit QFT; final wire reorder done with CXs (no native SWAP in several +/// menus). +OwningOpRef buildThreeQQftCircuit(MLIRContext* context) { + using std::numbers::pi; + mlir::qc::QCProgramBuilder builder(context); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + const auto q2 = builder.allocQubit(); + + builder.h(q2); + emitControlledPhase(builder, pi / 2.0, q1, q2); + builder.h(q1); + emitControlledPhase(builder, pi / 4.0, q0, q2); + emitControlledPhase(builder, pi / 2.0, q0, q1); + builder.h(q0); + + // SWAP(q0, q2) via three CXs. + builder.cx(q0, q2); + builder.cx(q2, q0); + builder.cx(q0, q2); + + builder.dealloc(q0); + builder.dealloc(q1); + builder.dealloc(q2); + return builder.finalize(); +} + +/// Deterministic Clifford+T mix on 3 qubits spanning every single-qubit family +/// accepted by `extractSingleQubitMatrix` and both CX/CZ entanglers. +OwningOpRef buildThreeQCliffordTMixCircuit(MLIRContext* context) { + mlir::qc::QCProgramBuilder builder(context); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + const auto q2 = builder.allocQubit(); + + builder.h(q0); + builder.t(q1); + builder.x(q2); + builder.cx(q0, q1); + builder.rz(0.37, q2); + builder.cz(q1, q2); + builder.sdg(q0); + builder.ry(-0.42, q1); + builder.cx(q2, q0); + builder.y(q1); + builder.tdg(q2); + builder.cx(q0, q1); + builder.p(0.21, q2); + builder.h(q2); + builder.cz(q0, q2); + builder.rx(-0.13, q1); + builder.s(q0); + + builder.dealloc(q0); + builder.dealloc(q1); + builder.dealloc(q2); + return builder.finalize(); +} + +struct ThreeQubitCircuitCase { + const char* name; + OwningOpRef (*build)(MLIRContext*); +}; + +const std::array THREE_QUBIT_CIRCUIT_CASES{{ + {.name = "ghz-3", .build = &buildThreeQGhzCircuit}, + {.name = "toffoli-3", .build = &buildThreeQToffoliCircuit}, + {.name = "qft-3", .build = &buildThreeQQftCircuit}, + {.name = "clifford-t-3", .build = &buildThreeQCliffordTMixCircuit}, +}}; + +} // namespace + +TEST_F(NativeSynthesisPassTest, ThreeQubitCircuitsEquivalentAcrossProfiles) { + const auto profiles = NativeSynthesisPassTest::allNineEquivalenceProfiles(); + + for (const auto& circuitCase : THREE_QUBIT_CIRCUIT_CASES) { + for (const auto& profileCase : profiles) { + auto expected = circuitCase.build(context.get()); + runQcToQco(expected); + const auto expectedUnitary = computeNQubitUnitaryFromModule(expected); + ASSERT_TRUE(expectedUnitary.has_value()) + << "circuit=" << circuitCase.name + << " native-gates=" << profileCase.nativeGates; + + auto synthesized = circuitCase.build(context.get()); + runNativeSynthesis(synthesized, profileCase.nativeGates); + EXPECT_TRUE(profileCase.isNative(synthesized)) + << "circuit=" << circuitCase.name + << " native-gates=" << profileCase.nativeGates; + + const auto synthesizedUnitary = + computeNQubitUnitaryFromModule(synthesized); + ASSERT_TRUE(synthesizedUnitary.has_value()) + << "circuit=" << circuitCase.name + << " native-gates=" << profileCase.nativeGates; + EXPECT_TRUE( + isEquivalentUpToGlobalPhase(*expectedUnitary, *synthesizedUnitary)) + << "circuit=" << circuitCase.name + << " native-gates=" << profileCase.nativeGates; + } + } +} + +namespace { + +/// 5-qubit stress circuit matching the structural +/// `LargeMultiQubitCircuitStaysWithinMinimalMenu` test. Designed to exercise +/// many overlapping 2q blocks and deep 1q chains, using only gates that are +/// supported by every targeted profile's synthesis path. +OwningOpRef buildFiveQubitStressCircuit(MLIRContext* context) { + mlir::qc::QCProgramBuilder builder(context); + builder.initialize(); + + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + const auto q2 = builder.allocQubit(); + const auto q3 = builder.allocQubit(); + const auto q4 = builder.allocQubit(); + + builder.h(q0); + builder.s(q1); + builder.t(q2); + builder.y(q3); + builder.h(q4); + + builder.cx(q0, q1); + builder.cz(q1, q2); + builder.swap(q2, q3); + builder.cx(q3, q4); + + for (int layer = 0; layer < 4; ++layer) { + builder.h(q0); + builder.s(q0); + builder.t(q0); + + builder.y(q1); + builder.h(q2); + builder.s(q3); + builder.t(q4); + + builder.cx(q0, q2); + builder.cz(q1, q3); + builder.cx(q2, q4); + + if ((layer % 2) == 0) { + builder.swap(q0, q1); + builder.swap(q3, q4); + } else { + builder.cx(q4, q0); + builder.cz(q2, q1); + } + } + + builder.p(0.25, q0); + builder.p(-0.5, q2); + builder.p(0.75, q4); + + builder.dealloc(q0); + builder.dealloc(q1); + builder.dealloc(q2); + builder.dealloc(q3); + builder.dealloc(q4); + return builder.finalize(); +} + +} // namespace + +TEST_F(NativeSynthesisPassTest, + FiveQubitStressCircuitEquivalentAcrossProfiles) { + const auto profiles = NativeSynthesisPassTest::allNineEquivalenceProfiles(); + + for (const auto& profileCase : profiles) { + auto expected = buildFiveQubitStressCircuit(context.get()); + runQcToQco(expected); + const auto expectedUnitary = computeNQubitUnitaryFromModule(expected); + ASSERT_TRUE(expectedUnitary.has_value()) + << "native-gates=" << profileCase.nativeGates; + + auto synthesized = buildFiveQubitStressCircuit(context.get()); + runNativeSynthesis(synthesized, profileCase.nativeGates); + EXPECT_TRUE(profileCase.isNative(synthesized)) + << "native-gates=" << profileCase.nativeGates; + + const auto synthesizedUnitary = computeNQubitUnitaryFromModule(synthesized); + ASSERT_TRUE(synthesizedUnitary.has_value()) + << "native-gates=" << profileCase.nativeGates; + EXPECT_TRUE( + isEquivalentUpToGlobalPhase(*expectedUnitary, *synthesizedUnitary)) + << "native-gates=" << profileCase.nativeGates; + } +} diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_profiles.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_profiles.cpp new file mode 100644 index 0000000000..21524314f7 --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_profiles.cpp @@ -0,0 +1,700 @@ +/* + * 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 "native_synthesis_pass_test_fixture.h" + +using namespace mlir; +using namespace mlir::qco; +using namespace mlir::qco::native_synth_test; + +TEST_F(NativeSynthesisPassTest, DecomposesToIbmBasicCxProfile) { + expectNativeAfterSynthesis( + [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + builder.h(q0); + builder.s(q0); + builder.t(q0); + builder.y(q0); + builder.cx(q0, q1); + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }, + "x,sx,rz,cx", &NativeSynthesisPassTest::onlyIbmBasicCxOps); +} + +TEST_F(NativeSynthesisPassTest, DecomposesSwapToIbmBasicCxProfile) { + expectNativeAfterSynthesis( + [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + builder.swap(q0, q1); + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }, + "x,sx,rz,cx", &NativeSynthesisPassTest::onlyIbmBasicCxOps); +} + +TEST_F(NativeSynthesisPassTest, DecomposesToGenericU3CxProfile) { + expectNativeAfterSynthesis( + [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + builder.h(q0); + builder.s(q0); + builder.t(q0); + builder.y(q0); + builder.cx(q0, q1); + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }, + "u,cx", &NativeSynthesisPassTest::onlyGenericU3CxOps); +} + +TEST_F(NativeSynthesisPassTest, DecomposesSwapToGenericU3CxProfile) { + expectNativeAfterSynthesis( + [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + builder.swap(q0, q1); + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }, + "u,cx", &NativeSynthesisPassTest::onlyGenericU3CxOps); +} + +TEST_F(NativeSynthesisPassTest, DecomposesCxToCzForIbmBasicCzProfile) { + expectNativeAfterSynthesis( + [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + builder.h(q1); + builder.cx(q0, q1); + builder.t(q1); + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }, + "x,sx,rz,cz", &NativeSynthesisPassTest::onlyIbmBasicCzOps); +} + +TEST_F(NativeSynthesisPassTest, DecomposesSwapToIbmBasicCzProfile) { + expectNativeAfterSynthesis( + [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + builder.swap(q0, q1); + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }, + "x,sx,rz,cz", &NativeSynthesisPassTest::onlyIbmBasicCzOps); +} + +TEST_F(NativeSynthesisPassTest, DecomposesSwapToGenericU3CzProfile) { + expectNativeAfterSynthesis( + [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + builder.swap(q0, q1); + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }, + "u,cz", &NativeSynthesisPassTest::onlyGenericU3CzOps); +} + +TEST_F(NativeSynthesisPassTest, DecomposesToIqmDefaultProfile) { + expectNativeAfterSynthesis( + [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + builder.x(q0); + builder.y(q0); + builder.sx(q0); + builder.cz(q0, q1); + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }, + "r,cz", &NativeSynthesisPassTest::onlyIqmDefaultOps); +} + +TEST_F(NativeSynthesisPassTest, DecomposesToIbmFractionalProfile) { + expectNativeAfterSynthesis( + [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + builder.h(q0); + builder.ry(0.37, q0); + builder.sxdg(q0); + builder.cx(q0, q1); + builder.rzz(0.23, q0, q1); + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }, + "x,sx,rz,rx,rzz,cz", &NativeSynthesisPassTest::onlyIbmFractionalOps); +} + +TEST_F(NativeSynthesisPassTest, DecomposesSwapToIbmFractionalProfile) { + expectNativeAfterSynthesis( + [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + builder.swap(q0, q1); + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }, + "x,sx,rz,rx,rzz,cz", &NativeSynthesisPassTest::onlyIbmFractionalOps); +} + +TEST_F(NativeSynthesisPassTest, DecomposesToAxisPairRxRzCxProfile) { + expectNativeAfterSynthesis( + [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + builder.h(q0); + builder.y(q0); + builder.cx(q0, q1); + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }, + "rx,rz,cx", &NativeSynthesisPassTest::onlyAxisPairRxRzCxOps); +} + +TEST_F(NativeSynthesisPassTest, DecomposesSwapViaBasisDecomposerAxisPairCx) { + expectNativeAfterSynthesis( + [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + builder.swap(q0, q1); + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }, + "rx,rz,cx", &NativeSynthesisPassTest::onlyAxisPairRxRzCxOps); +} + +TEST_F(NativeSynthesisPassTest, DecomposesRzToAxisPairRxRyCxProfile) { + expectNativeAfterSynthesis( + [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + builder.z(q0); + builder.cx(q0, q1); + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }, + "rx,ry,cx", &NativeSynthesisPassTest::onlyAxisPairRxRyCxOps); +} + +TEST_F(NativeSynthesisPassTest, DecomposesToAxisPairRyRzCzProfile) { + expectNativeAfterSynthesis( + [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + builder.x(q0); + builder.h(q0); + builder.cz(q0, q1); + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }, + "ry,rz,cz", &NativeSynthesisPassTest::onlyAxisPairRyRzCzOps); +} + +TEST_F(NativeSynthesisPassTest, DecomposesSwapViaBasisDecomposerAxisPairCz) { + expectNativeAfterSynthesis( + [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + builder.swap(q0, q1); + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }, + "ry,rz,cz", &NativeSynthesisPassTest::onlyAxisPairRyRzCzOps); +} + +TEST_F(NativeSynthesisPassTest, ConvertsCxToCzForAxisPairRyRzCzProfile) { + expectNativeAfterSynthesis( + [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + builder.cx(q0, q1); + builder.y(q1); + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }, + "ry,rz,cz", &NativeSynthesisPassTest::onlyAxisPairRyRzCzOps); +} + +TEST_F(NativeSynthesisPassTest, ConvertsCxToCzForIqmDefaultProfile) { + expectNativeAfterSynthesis( + [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + builder.cx(q0, q1); + builder.y(q1); + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }, + "r,cz", &NativeSynthesisPassTest::onlyIqmDefaultOps); +} + +TEST_F(NativeSynthesisPassTest, BroadOneQCanonicalizationIqmDefaultNoLeakage) { + auto moduleOp = buildBroadOneQCanonicalizationCircuit(); + runNativeSynthesis(moduleOp, "r,cz"); + EXPECT_TRUE(onlyIqmDefaultOps(moduleOp)); +} + +TEST_F(NativeSynthesisPassTest, + BroadOneQCanonicalizationAxisPairRyRzCzNoLeakage) { + auto moduleOp = buildBroadOneQCanonicalizationCircuit(); + runNativeSynthesis(moduleOp, "ry,rz,cz"); + EXPECT_TRUE(onlyAxisPairRyRzCzOps(moduleOp)); +} + +TEST_F(NativeSynthesisPassTest, BroadOneQCanonicalizationGenericU3CzNoLeakage) { + auto moduleOp = buildBroadOneQCanonicalizationCircuit(); + runNativeSynthesis(moduleOp, "u,cz"); + EXPECT_TRUE(onlyGenericU3CzOps(moduleOp)); +} + +TEST_F(NativeSynthesisPassTest, GenericProfileMatchesGenericU3CxBehavior) { + expectEquivalentAndNativeAfterSynthesis( + [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + builder.h(q0); + builder.y(q1); + builder.cx(q0, q1); + builder.s(q0); + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }, + "u,cx", &NativeSynthesisPassTest::onlyGenericU3CxOps, + computeTwoQubitUnitaryFromModule); +} + +TEST_F(NativeSynthesisPassTest, GenericProfileMatchesAxisPairRyRzCzBehavior) { + expectEquivalentAndNativeAfterSynthesis( + [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + builder.x(q0); + builder.h(q0); + builder.cz(q0, q1); + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }, + "ry,rz,cz", &NativeSynthesisPassTest::onlyAxisPairRyRzCzOps, + computeTwoQubitUnitaryFromModule); +} + +TEST_F(NativeSynthesisPassTest, ZeroAngleCanonicalizationIqmDefaultNoLeakage) { + auto moduleOp = buildZeroAngleCanonicalizationCircuit(); + runNativeSynthesis(moduleOp, "r,cz"); + EXPECT_TRUE(onlyIqmDefaultOps(moduleOp)); +} + +TEST_F(NativeSynthesisPassTest, + ZeroAngleCanonicalizationAxisPairRyRzNoLeakage) { + auto moduleOp = buildZeroAngleCanonicalizationCircuit(); + runNativeSynthesis(moduleOp, "ry,rz,cz"); + EXPECT_TRUE(onlyAxisPairRyRzCzOps(moduleOp)); +} + +TEST_F(NativeSynthesisPassTest, FailsForUnsupportedNativeGateMenu) { + expectSynthesisFailure( + [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + builder.h(q0); + builder.dealloc(q0); + return builder.finalize(); + }, + "not-a-gate"); +} + +TEST_F(NativeSynthesisPassTest, + CustomProfileAcceptsOverlappingOneQSupersetMenu) { + expectEquivalentAndNativeAfterSynthesis( + [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + builder.h(q0); + builder.y(q0); + builder.cx(q0, q1); + builder.s(q1); + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }, + "u,rx,rz,cx", &NativeSynthesisPassTest::onlyUOrAxisPairRxRzCxOps, + computeTwoQubitUnitaryFromModule); +} + +TEST_F(NativeSynthesisPassTest, CustomProfileMatchesIbmFractionalBehavior) { + expectEquivalentAndNativeAfterSynthesis( + [&] { return buildIbmFractionalAllGateFamiliesCircuit(); }, + "x,sx,rz,rx,cz,rzz", &NativeSynthesisPassTest::onlyIbmFractionalOps, + computeTwoQubitUnitaryFromModule); +} + +TEST_F(NativeSynthesisPassTest, CustomProfileAcceptsMultipleEntanglersMenu) { + expectEquivalentAndNativeAfterSynthesis( + [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + builder.h(q0); + builder.cx(q0, q1); + builder.s(q1); + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }, + "u,cx,cz", &NativeSynthesisPassTest::onlyGenericU3CxOrCzOps, + computeTwoQubitUnitaryFromModule); +} + +TEST_F(NativeSynthesisPassTest, + FailsForUnsupportedNativeGateMenuWithoutEmitter) { + expectSynthesisFailure( + [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + builder.h(q0); + builder.dealloc(q0); + return builder.finalize(); + }, + "rz,cx"); +} + +TEST_F(NativeSynthesisPassTest, MinimalIbmBasicCustomMenuAcceptsPhaseAlias) { + // `x,sx,rz,cx` is the minimal IBM-basic style menu. The synthesis pass may + // represent Z-axis phases using `p`, which should be accepted as an alias of + // `rz` for custom menus. + expectNativeAfterSynthesis( + [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + builder.p(0.13, q0); + builder.h(q0); + builder.cx(q0, q1); + builder.p(-0.27, q1); + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }, + "x,sx,rz,cx", &NativeSynthesisPassTest::onlyIbmBasicCxOps); +} + +TEST_F(NativeSynthesisPassTest, LargeMultiQubitCircuitStaysWithinMinimalMenu) { + // Stress-test: larger circuit (>2 qubits) with many 1Q/2Q ops that should + // still synthesize into the minimal IBM-basic custom menu. + expectNativeAfterSynthesis( + [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + const auto q2 = builder.allocQubit(); + const auto q3 = builder.allocQubit(); + const auto q4 = builder.allocQubit(); + + // A mix of non-native 1Q ops (h/s/t/y) and entanglers (cx/cz/swap) + // across different pairs. + builder.h(q0); + builder.s(q1); + builder.t(q2); + builder.y(q3); + builder.h(q4); + + builder.cx(q0, q1); + builder.cz(q1, q2); + builder.swap(q2, q3); + builder.cx(q3, q4); + + // Add depth with repeated layers. + for (int layer = 0; layer < 8; ++layer) { + builder.h(q0); + builder.s(q0); + builder.t(q0); + + builder.y(q1); + builder.h(q2); + builder.s(q3); + builder.t(q4); + + builder.cx(q0, q2); + builder.cz(q1, q3); + builder.cx(q2, q4); + + if ((layer % 2) == 0) { + builder.swap(q0, q1); + builder.swap(q3, q4); + } else { + builder.cx(q4, q0); + builder.cz(q2, q1); + } + } + + // Include explicit phases too (these should end up as `rz`/`p`). + builder.p(0.25, q0); + builder.p(-0.5, q2); + builder.p(0.75, q4); + + builder.dealloc(q0); + builder.dealloc(q1); + builder.dealloc(q2); + builder.dealloc(q3); + builder.dealloc(q4); + return builder.finalize(); + }, + "x,sx,rz,cx", &NativeSynthesisPassTest::onlyIbmBasicCxOps); +} + +TEST_F(NativeSynthesisPassTest, FailsForNativeGateMenuWithoutSingleQEmitter) { + expectSynthesisFailure( + [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + builder.cx(q0, q1); + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }, + "cx,cz"); +} + +TEST_F(NativeSynthesisPassTest, FailsForNegativeScoreWeight) { + expectSynthesisFailure( + [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + builder.h(q0); + builder.dealloc(q0); + return builder.finalize(); + }, + "u,cx", -1.0, 0.1, 0.01); +} + +TEST_F(NativeSynthesisPassTest, CandidateSelectionIsDeterministicAcrossRuns) { + auto buildFn = [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + builder.swap(q0, q1); + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }; + + auto firstModule = buildFn(); + runNativeSynthesis(firstModule, "u,cx"); + auto secondModule = buildFn(); + runNativeSynthesis(secondModule, "u,cx"); + + EXPECT_EQ(moduleToString(firstModule), moduleToString(secondModule)); +} + +TEST_F(NativeSynthesisPassTest, + RichCustomMenuSelectionRemainsDeterministicAcrossWeightsAndRuns) { + auto buildFn = [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + builder.swap(q0, q1); + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }; + + auto firstModule = buildFn(); + runNativeSynthesis(firstModule, "u,rx,rz,cx,cz", 1.0, 0.1, 0.01); + auto secondModule = buildFn(); + runNativeSynthesis(secondModule, "u,rx,rz,cx,cz", 1.0, 0.1, 0.01); + EXPECT_EQ(moduleToString(firstModule), moduleToString(secondModule)); + + auto alternateWeightsModule = buildFn(); + runNativeSynthesis(alternateWeightsModule, "u,rx,rz,cx,cz", 3.0, 0.5, 0.0); + EXPECT_TRUE(onlyUOrAxisPairRxRzCxOps(alternateWeightsModule) || + onlyGenericU3CxOrCzOps(alternateWeightsModule)); +} + +TEST_F(NativeSynthesisPassTest, FailsForMultiControlledGateStructure) { + expectSynthesisFailure( + [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + const auto q2 = builder.allocQubit(); + builder.mcx({q0, q1}, q2); + builder.dealloc(q0); + builder.dealloc(q1); + builder.dealloc(q2); + return builder.finalize(); + }, + "x,sx,rz,cx"); +} + +TEST_F(NativeSynthesisPassTest, FailsForControlledTwoTargetGateStructure) { + expectSynthesisFailure( + [&] { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + const auto q2 = builder.allocQubit(); + builder.cswap(q0, q1, q2); + builder.dealloc(q0); + builder.dealloc(q1); + builder.dealloc(q2); + return builder.finalize(); + }, + "x,sx,rz,cx"); +} + +TEST_F(NativeSynthesisPassTest, + RandomizedEquivalentAcrossProfilesWithFixedSeed) { + auto buildStressCircuit = [&](MLIRContext* ctx, const char* nativeGates) { + mlir::qc::QCProgramBuilder builder(ctx); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + const std::string menu(nativeGates); + if (menu == "r,cz") { + builder.r(0.37, -0.42, q0); + builder.cz(q0, q1); + builder.r(-0.11, 0.21, q1); + } else if (menu == "ry,rz,cz") { + builder.ry(0.37, q0); + builder.rz(-0.42, q1); + builder.cz(q0, q1); + builder.rz(0.21, q0); + } else if (menu == "rx,ry,cx") { + builder.rx(0.37, q0); + builder.ry(-0.42, q1); + builder.cx(q0, q1); + builder.ry(0.21, q0); + } else if (menu == "rx,rz,cx") { + builder.rx(0.37, q0); + builder.rz(-0.42, q1); + builder.cx(q0, q1); + builder.rz(0.21, q0); + } else { + builder.h(q0); + builder.y(q1); + builder.cx(q0, q1); + builder.s(q0); + builder.cx(q1, q0); + } + + builder.dealloc(q0); + builder.dealloc(q1); + return builder.finalize(); + }; + + const auto profiles = NativeSynthesisPassTest::allNineEquivalenceProfiles(); + + for (const auto& profileCase : profiles) { + auto synthesizedModule = + buildStressCircuit(context.get(), profileCase.nativeGates); + PassManager prePm(synthesizedModule->getContext()); + prePm.addPass(createQCToQCO()); + ASSERT_TRUE(succeeded(prePm.run(*synthesizedModule))); + const auto expectedUnitary = + computeTwoQubitUnitaryFromModule(synthesizedModule); + ASSERT_TRUE(expectedUnitary.has_value()); + + PassManager synthPm(synthesizedModule->getContext()); + synthPm.addPass( + qco::createNativeGateSynthesisPass(qco::NativeGateSynthesisOptions{ + .nativeGates = profileCase.nativeGates, + })); + ASSERT_TRUE(succeeded(synthPm.run(*synthesizedModule))) + << "native-gates=" << profileCase.nativeGates; + EXPECT_TRUE(profileCase.isNative(synthesizedModule)) + << "native-gates=" << profileCase.nativeGates; + + const auto synthesizedUnitary = + computeTwoQubitUnitaryFromModule(synthesizedModule); + ASSERT_TRUE(synthesizedUnitary.has_value()); + EXPECT_TRUE( + isEquivalentUpToGlobalPhase(*expectedUnitary, *synthesizedUnitary)) + << "native-gates=" << profileCase.nativeGates; + } +} diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_scoring.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_scoring.cpp new file mode 100644 index 0000000000..330e9a091d --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_scoring.cpp @@ -0,0 +1,265 @@ +/* + * 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 + */ + +// Scoring helpers for native-gate synthesis plus ``XXPlusYY`` / ``XXMinusYY`` +// rewrite metric checks. + +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Scoring.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h" +#include "native_synthesis_pass_test_fixture.h" + +#include +#include +#include + +#include + +using namespace mlir; +using namespace mlir::qco; +using namespace mlir::qco::native_synth_test; + +namespace { + +/// Dummy payload: scoring helpers do not inspect the type. +struct ScoringTag {}; + +std::pair +countSingleAndTwoQubitUnitariesForXxRzzMetrics(ModuleOp module) { + unsigned numOneQ = 0; + unsigned numTwoQ = 0; + module.walk([&](Operation* op) { + if (isa(op)) { + return; + } + if (isa_and_present(op->getParentOp())) { + return; + } + auto unitary = dyn_cast(op); + if (!unitary) { + return; + } + if (unitary.isSingleQubit()) { + ++numOneQ; + return; + } + if (unitary.isTwoQubit()) { + ++numTwoQ; + } + }); + return {numOneQ, numTwoQ}; +} + +} // namespace + +TEST(NativeSynthesisScoringTest, ValidScoreWeights) { + using namespace mlir::qco::native_synth; + EXPECT_TRUE(areValidScoreWeights(ScoreWeights{})); + EXPECT_TRUE(areValidScoreWeights( + ScoreWeights{.twoQ = 0.0, .oneQ = 0.0, .depth = 0.0})); + EXPECT_TRUE(areValidScoreWeights( + ScoreWeights{.twoQ = 5.0, .oneQ = 2.5, .depth = 0.1})); + + EXPECT_FALSE(areValidScoreWeights(ScoreWeights{.twoQ = -1.0})); + EXPECT_FALSE(areValidScoreWeights(ScoreWeights{.oneQ = -0.1})); + EXPECT_FALSE(areValidScoreWeights(ScoreWeights{.depth = -0.01})); + + const double inf = std::numeric_limits::infinity(); + const double nan = std::numeric_limits::quiet_NaN(); + EXPECT_FALSE(areValidScoreWeights(ScoreWeights{.twoQ = inf})); + EXPECT_FALSE(areValidScoreWeights(ScoreWeights{.oneQ = inf})); + EXPECT_FALSE(areValidScoreWeights(ScoreWeights{.depth = inf})); + EXPECT_FALSE(areValidScoreWeights(ScoreWeights{.twoQ = nan})); + EXPECT_FALSE(areValidScoreWeights(ScoreWeights{.oneQ = nan})); + EXPECT_FALSE(areValidScoreWeights(ScoreWeights{.depth = nan})); +} + +TEST(NativeSynthesisScoringTest, ScoreCandidateAppliesWeightsLinearly) { + using namespace mlir::qco::native_synth; + SynthesisCandidate candidate; + candidate.metrics.numTwoQ = 3; + candidate.metrics.numOneQ = 5; + candidate.metrics.depth = 7; + candidate.candidateClass = CandidateClass::DirectSingleQ; + candidate.enumerationIndex = 11; + + const ScoreWeights weights{.twoQ = 2.0, .oneQ = 0.5, .depth = 0.1}; + const auto score = scoreCandidate(candidate, weights); + + EXPECT_DOUBLE_EQ(score.weighted, (2.0 * 3.0) + (0.5 * 5.0) + (0.1 * 7.0)); + EXPECT_EQ(score.numTwoQ, 3U); + EXPECT_EQ(score.numOneQ, 5U); + EXPECT_EQ(score.depth, 7U); + EXPECT_EQ(score.tieBreakClass, + static_cast(CandidateClass::DirectSingleQ)); + EXPECT_EQ(score.enumerationIndex, 11U); +} + +TEST(NativeSynthesisScoringTest, IsBetterScoreComparesWeightedFirst) { + using namespace mlir::qco::native_synth; + const CandidateScore lower{ + .weighted = 1.0, .numTwoQ = 10, .depth = 100, .numOneQ = 1000}; + const CandidateScore higher{.weighted = 2.0}; + EXPECT_TRUE(isBetterScore(lower, higher)); + EXPECT_FALSE(isBetterScore(higher, lower)); + EXPECT_FALSE(isBetterScore(lower, lower)); +} + +TEST(NativeSynthesisScoringTest, IsBetterScoreTieBreaksInDeclaredOrder) { + using namespace mlir::qco::native_synth; + const CandidateScore anchor{.weighted = 1.0, + .numTwoQ = 5, + .depth = 5, + .numOneQ = 5, + .tieBreakClass = 5, + .enumerationIndex = 5}; + + const CandidateScore fewerTwoQ{.weighted = 1.0, + .numTwoQ = 4, + .depth = 99, + .numOneQ = 99, + .tieBreakClass = 99, + .enumerationIndex = 99}; + EXPECT_TRUE(isBetterScore(fewerTwoQ, anchor)); + + const CandidateScore lowerDepth{.weighted = 1.0, + .numTwoQ = 5, + .depth = 4, + .numOneQ = 99, + .tieBreakClass = 99, + .enumerationIndex = 99}; + EXPECT_TRUE(isBetterScore(lowerDepth, anchor)); + + const CandidateScore fewerOneQ{.weighted = 1.0, + .numTwoQ = 5, + .depth = 5, + .numOneQ = 4, + .tieBreakClass = 99, + .enumerationIndex = 99}; + EXPECT_TRUE(isBetterScore(fewerOneQ, anchor)); + + const CandidateScore lowerClass{.weighted = 1.0, + .numTwoQ = 5, + .depth = 5, + .numOneQ = 5, + .tieBreakClass = 0, + .enumerationIndex = 99}; + EXPECT_TRUE(isBetterScore(lowerClass, anchor)); + + const CandidateScore lowerEnum{.weighted = 1.0, + .numTwoQ = 5, + .depth = 5, + .numOneQ = 5, + .tieBreakClass = 5, + .enumerationIndex = 0}; + EXPECT_TRUE(isBetterScore(lowerEnum, anchor)); +} + +TEST(NativeSynthesisScoringTest, IsBetterScoreTreatsCloseWeightedAsTie) { + using namespace mlir::qco::native_synth; + const CandidateScore a{.weighted = 1.0, .numTwoQ = 1}; + const CandidateScore b{.weighted = 1.0 + 1e-13, .numTwoQ = 0}; + EXPECT_TRUE(isBetterScore(b, a)); +} + +TEST(NativeSynthesisScoringTest, SelectBestCandidateReturnsNullForEmptyInput) { + using namespace mlir::qco::native_synth; + const llvm::SmallVector, 0> empty; + EXPECT_EQ(selectBestCandidate(llvm::ArrayRef(empty), ScoreWeights{}), + nullptr); +} + +TEST(NativeSynthesisScoringTest, SelectBestCandidatePicksLowestWeighted) { + using namespace mlir::qco::native_synth; + llvm::SmallVector, 3> candidates(3); + candidates[0].metrics.numTwoQ = 4U; + candidates[1].metrics.numTwoQ = 1U; + candidates[2].metrics.numTwoQ = 2U; + + const auto* best = + selectBestCandidate(llvm::ArrayRef(candidates), ScoreWeights{}); + ASSERT_NE(best, nullptr); + EXPECT_EQ(best, &candidates[1]); +} + +TEST(NativeSynthesisScoringTest, SelectBestCandidateHonoursWeightPreferences) { + using namespace mlir::qco::native_synth; + llvm::SmallVector, 2> candidates(2); + candidates[0].metrics.numTwoQ = 2U; + candidates[0].metrics.numOneQ = 0U; + candidates[1].metrics.numTwoQ = 1U; + candidates[1].metrics.numOneQ = 20U; + + EXPECT_EQ(selectBestCandidate(llvm::ArrayRef(candidates), ScoreWeights{}), + candidates.data()); + + const ScoreWeights heavyTwoQ{.twoQ = 10.0, .oneQ = 0.01, .depth = 0.0}; + EXPECT_EQ(selectBestCandidate(llvm::ArrayRef(candidates), heavyTwoQ), + candidates.data() + 1); +} + +TEST(NativeSynthesisScoringTest, + SelectBestCandidateTieBreaksByEnumerationOrder) { + using namespace mlir::qco::native_synth; + llvm::SmallVector, 3> candidates(3); + candidates[0].enumerationIndex = 2U; + candidates[1].enumerationIndex = 0U; + candidates[2].enumerationIndex = 1U; + + const auto* best = + selectBestCandidate(llvm::ArrayRef(candidates), ScoreWeights{}); + ASSERT_NE(best, nullptr); + EXPECT_EQ(best, &candidates[1]); +} + +TEST_F(NativeSynthesisPassTest, XxPlusMinusYyEmittedCountsMatchScoringMetrics) { + using namespace mlir::qco::native_synth; + + const auto runRewriteCase = [&](auto emitTwoQGate) { + mlir::qc::QCProgramBuilder builder(context.get()); + builder.initialize(); + const auto q0 = builder.allocQubit(); + const auto q1 = builder.allocQubit(); + emitTwoQGate(builder, q0, q1); + builder.dealloc(q0); + builder.dealloc(q1); + OwningOpRef module = builder.finalize(); + + PassManager pm(context.get()); + pm.addPass(createQCToQCO()); + ASSERT_TRUE(succeeded(pm.run(*module))); + + Operation* twoQOp = nullptr; + module->walk([&](Operation* op) { + if (isa(op)) { + twoQOp = op; + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + ASSERT_NE(twoQOp, nullptr); + + IRRewriter rewriter(context.get()); + ASSERT_TRUE(succeeded(rewriteXXPlusMinusYYViaRxxRyy(rewriter, twoQOp))); + + const auto expected = xxPlusMinusYyRzzRewriteScoringMetrics(); + const auto [numOneQ, numTwoQ] = + countSingleAndTwoQubitUnitariesForXxRzzMetrics(*module); + EXPECT_EQ(numOneQ, expected.numOneQ); + EXPECT_EQ(numTwoQ, expected.numTwoQ); + }; + + runRewriteCase([](mlir::qc::QCProgramBuilder& b, Value q0, Value q1) { + b.xx_plus_yy(0.52, -0.14, q0, q1); + }); + runRewriteCase([](mlir::qc::QCProgramBuilder& b, Value q0, Value q1) { + b.xx_minus_yy(-0.37, 0.26, q0, q1); + }); +} From f55f1cde0957e8b1856e3ee51049b2da635ac217 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 22 Apr 2026 16:01:17 +0200 Subject: [PATCH 004/122] =?UTF-8?q?=E2=9C=A8=20Enhance=20quantum=20gate=20?= =?UTF-8?q?decomposition=20tests=20with=20new=20utility=20functions=20and?= =?UTF-8?q?=20additional=20test=20cases=20for=20Euler=20and=20Weyl=20decom?= =?UTF-8?q?positions.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Decomposition/decomposition_test_utils.h | 31 +++++++ .../test_euler_decomposition.cpp | 89 +++++++++++++++++++ .../Decomposition/test_weyl_decomposition.cpp | 21 +++++ 3 files changed, 141 insertions(+) diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/decomposition_test_utils.h b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/decomposition_test_utils.h index 58f3728a53..ccdf8170a3 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/decomposition_test_utils.h +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/decomposition_test_utils.h @@ -12,12 +12,43 @@ #include "mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h" +#include #include #include +#include #include #include +/// Standard U3 matrix (same convention as QCO ``u`` angles). +[[nodiscard]] inline Eigen::Matrix2cd u3Matrix(double theta, double phi, + double lambda) { + using Complex = std::complex; + const Complex i(0.0, 1.0); + const double c = std::cos(theta / 2.0); + const double s = std::sin(theta / 2.0); + const Complex eiphi = std::exp(i * phi); + const Complex eilambda = std::exp(i * lambda); + const Complex eiphilambda = std::exp(i * (phi + lambda)); + + Eigen::Matrix2cd mat; + mat << c, -eilambda * s, eiphi * s, eiphilambda * c; + return mat; +} + +/// Compare up to a single global phase factor. +template +[[nodiscard]] bool isEquivalentUpToGlobalPhase(const Matrix& lhs, + const Matrix& rhs, + double atol = 1e-10) { + const auto overlap = (rhs.adjoint() * lhs).trace(); + if (std::abs(overlap) <= atol) { + return false; + } + const auto factor = overlap / std::abs(overlap); + return lhs.isApprox(factor * rhs, atol); +} + template [[nodiscard]] MatrixType randomUnitaryMatrix(std::mt19937& rng) { std::uniform_real_distribution dist(-1.0, 1.0); diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp index 4aee00a92e..4bb48be77c 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -11,6 +11,7 @@ #include "decomposition_test_utils.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.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" @@ -19,6 +20,7 @@ #include #include +#include #include #include #include @@ -26,6 +28,29 @@ using namespace mlir::qco; using namespace mlir::qco::decomposition; +namespace { + +std::size_t countGatesOfType(const OneQubitGateSequence& seq, GateKind kind) { + std::size_t count = 0; + for (const auto& gate : seq.gates) { + if (gate.type == kind) { + ++count; + } + } + return count; +} + +/// Compare ``seq.getUnitaryMatrix()`` to ``u`` embedded on qubit 0 (4×4 +/// layout). +bool sequenceMatchesSingleQubitMatrix(const Eigen::Matrix2cd& u, + const OneQubitGateSequence& seq, + double tol = 1e-10) { + const Eigen::Matrix4cd expanded = expandToTwoQubits(u, 0); + return expanded.isApprox(seq.getUnitaryMatrix(), tol); +} + +} // namespace + class EulerDecompositionTest : public testing::TestWithParam< std::tuple> { @@ -83,6 +108,70 @@ TEST(EulerDecompositionTest, Random) { } } +TEST(EulerDecompositionTest, ZyzAnglesFromUnitaryReconstructHadamard) { + Eigen::Matrix2cd hadamard; + hadamard << 1.0 / std::numbers::sqrt2, 1.0 / std::numbers::sqrt2, + 1.0 / std::numbers::sqrt2, -1.0 / std::numbers::sqrt2; + + const auto angles = + EulerDecomposition::anglesFromUnitary(hadamard, EulerBasis::ZYZ); + const Eigen::Matrix2cd reconstructed = + u3Matrix(angles[0], angles[1], angles[2]); + + EXPECT_TRUE(isEquivalentUpToGlobalPhase(hadamard, reconstructed)); +} + +TEST(EulerDecompositionTest, NativeEulerBasesRandomReconstruction) { + std::mt19937 rng(424242); + std::uniform_real_distribution angleDist(-std::numbers::pi, + std::numbers::pi); + for (int i = 0; i < 24; ++i) { + const double theta = angleDist(rng); + const double phi = angleDist(rng); + const double lambda = angleDist(rng); + const double phase = angleDist(rng); + const Eigen::Matrix2cd unitary = + std::exp(std::complex(0.0, phase)) * + u3Matrix(theta, phi, lambda); + const Eigen::Matrix4cd expanded = expandToTwoQubits(unitary, 0); + + const auto u3Seq = EulerDecomposition::generateCircuit( + EulerBasis::U3, unitary, true, std::nullopt); + const auto zsxSeq = EulerDecomposition::generateCircuit( + EulerBasis::ZSX, unitary, true, std::nullopt); + const auto zsxxSeq = EulerDecomposition::generateCircuit( + EulerBasis::ZSXX, unitary, true, std::nullopt); + + EXPECT_TRUE( + isEquivalentUpToGlobalPhase(expanded, u3Seq.getUnitaryMatrix())); + EXPECT_TRUE( + isEquivalentUpToGlobalPhase(expanded, zsxSeq.getUnitaryMatrix())); + EXPECT_TRUE(sequenceMatchesSingleQubitMatrix(unitary, zsxSeq)); + EXPECT_TRUE(sequenceMatchesSingleQubitMatrix(unitary, zsxxSeq)); + + const std::size_t zsxSx = countGatesOfType(zsxSeq, GateKind::SX); + const std::size_t zsxxSx = countGatesOfType(zsxxSeq, GateKind::SX); + const std::size_t zsxxX = countGatesOfType(zsxxSeq, GateKind::X); + EXPECT_EQ(countGatesOfType(zsxSeq, GateKind::X), 0U); + EXPECT_LE(zsxxX, 1U); + if (zsxxX == 0U) { + EXPECT_EQ(zsxSx, zsxxSx); + } else { + EXPECT_EQ(zsxSx, zsxxSx + 2U); + } + } +} + +TEST(EulerDecompositionTest, ZsxxPauliXUsesSingleXGate) { + Eigen::Matrix2cd pauliX; + pauliX << 0.0, 1.0, 1.0, 0.0; + const auto seq = EulerDecomposition::generateCircuit(EulerBasis::ZSXX, pauliX, + true, std::nullopt); + EXPECT_EQ(countGatesOfType(seq, GateKind::X), 1U); + EXPECT_EQ(countGatesOfType(seq, GateKind::SX), 0U); + EXPECT_TRUE(sequenceMatchesSingleQubitMatrix(pauliX, seq)); +} + INSTANTIATE_TEST_SUITE_P( SingleQubitMatrices, EulerDecompositionTest, testing::Combine(testing::Values(EulerBasis::XYX, EulerBasis::XZX, diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index 49846420eb..e74323ace2 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -72,6 +72,27 @@ TEST_P(WeylDecompositionTest, TestApproximation) { << restoredMatrix << '\n'; } +TEST(WeylDecompositionTest, CnotProducesValidWeylParametersAndUnitaryLocals) { + Eigen::Matrix4cd cnot = Eigen::Matrix4cd::Zero(); + cnot(0, 0) = 1.0; + cnot(1, 1) = 1.0; + cnot(2, 3) = 1.0; + cnot(3, 2) = 1.0; + + const auto decomp = TwoQubitWeylDecomposition::create(cnot, std::nullopt); + EXPECT_GE(decomp.a(), -1e-10); + EXPECT_GE(decomp.b(), -1e-10); + EXPECT_GE(decomp.c(), -1e-10); + constexpr double piOver4 = 0.7853981633974483; + EXPECT_LE(decomp.a(), piOver4 + 1e-10); + EXPECT_LE(decomp.b(), piOver4 + 1e-10); + EXPECT_LE(decomp.c(), piOver4 + 1e-10); + EXPECT_TRUE(helpers::isUnitaryMatrix(decomp.k1l())); + EXPECT_TRUE(helpers::isUnitaryMatrix(decomp.k2l())); + EXPECT_TRUE(helpers::isUnitaryMatrix(decomp.k1r())); + EXPECT_TRUE(helpers::isUnitaryMatrix(decomp.k2r())); +} + TEST(WeylDecompositionTest, Random) { constexpr auto maxIterations = 5000; std::mt19937 rng{1234567UL}; From 09942dfad5ec413bbd9a57b38d8046fcc8960224 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 22 Apr 2026 17:06:43 +0200 Subject: [PATCH 005/122] =?UTF-8?q?=E2=9C=A8=20Add=20comprehensive=20tests?= =?UTF-8?q?=20for=20native=20synthesis=20and=20gate=20decomposition,=20inc?= =?UTF-8?q?luding=20new=20utility=20functions=20and=20validation=20for=20g?= =?UTF-8?q?ate=20sequences.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Compiler/test_compiler_pipeline.cpp | 10 ++ .../Transforms/Decomposition/CMakeLists.txt | 17 ++- .../Decomposition/test_basis_decomposer.cpp | 14 +++ .../test_decomposition_get_gate_kind.cpp | 78 ++++++++++++++ .../test_decomposition_helpers.cpp | 59 ++++++++++ .../test_euler_decomposition.cpp | 52 +++++++++ .../Transforms/NativeSynthesis/CMakeLists.txt | 25 ++++- .../NativeSynthesis/test_native_policy.cpp | 102 ++++++++++++++++++ .../NativeSynthesis/test_native_spec.cpp | 66 ++++++++++++ 9 files changed, 415 insertions(+), 8 deletions(-) create mode 100644 mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_get_gate_kind.cpp create mode 100644 mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_helpers.cpp create mode 100644 mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_policy.cpp create mode 100644 mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_spec.cpp diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 9c4964f5a9..84f82c52ed 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -903,6 +903,16 @@ TEST_F(CompilerPipelineNativeSynthesisConfigTest, EXPECT_EQ(record.afterQCOCanon, record.afterOptimization); } +TEST_F(CompilerPipelineNativeSynthesisConfigTest, + LeavesIRUnchangedWhenNativeGatesIsWhitespaceOnly) { + config.nativeGates = " \t "; + + const auto record = runPipelineAndExpectSuccess(); + + EXPECT_NE(record.afterQCOCanon.find("qco.h"), std::string::npos); + EXPECT_EQ(record.afterQCOCanon, record.afterOptimization); +} + TEST_F(CompilerPipelineNativeSynthesisConfigTest, NativeSynthesisPreservesUnitaryOnStaticQubits) { // End-to-end unitary equivalence check: after the pipeline lowers diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt index b2f48378fd..7e41c01c6d 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt @@ -7,10 +7,21 @@ # Licensed under the MIT License set(target_name mqt-core-mlir-unittest-decomposition) -add_executable(${target_name} test_basis_decomposer.cpp test_euler_decomposition.cpp - test_weyl_decomposition.cpp) +add_executable( + ${target_name} + test_basis_decomposer.cpp test_decomposition_get_gate_kind.cpp test_decomposition_helpers.cpp + test_euler_decomposition.cpp test_weyl_decomposition.cpp) -target_link_libraries(${target_name} PRIVATE GTest::gtest_main MLIRQCOTransforms Eigen3::Eigen) +target_link_libraries( + ${target_name} + PRIVATE GTest::gtest_main + MLIRQCOProgramBuilder + MLIRQCOTransforms + MLIRQCOUtils + MLIRPass + MLIRSupport + LLVMSupport + Eigen3::Eigen) mqt_mlir_configure_unittest_target(${target_name}) diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_basis_decomposer.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_basis_decomposer.cpp index 0871955472..9c3b8157d9 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_basis_decomposer.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_basis_decomposer.cpp @@ -134,6 +134,20 @@ TEST(BasisDecomposerTest, Random) { } } +TEST(BasisDecomposerNumBasisTest, ForcesZeroBasisUsesForIdentityTarget) { + const Gate basis{.type = GateKind::X, .parameter = {}, .qubitId = {0, 1}}; + const auto decomposer = TwoQubitBasisDecomposer::create(basis, 1.0); + const Eigen::Matrix4cd target = Eigen::Matrix4cd::Identity(); + const auto weyl = + TwoQubitWeylDecomposition::create(target, std::optional{1.0}); + const llvm::SmallVector eulerBases{EulerBasis::ZYZ}; + const auto decomposed = decomposer.twoQubitDecompose(weyl, eulerBases, 1.0, + false, std::uint8_t{0}); + ASSERT_TRUE(decomposed.has_value()); + const Eigen::Matrix4cd restored = BasisDecomposerTest::restore(*decomposed); + EXPECT_TRUE(restored.isApprox(target)); +} + INSTANTIATE_TEST_SUITE_P( ProductTwoQubitMatrices, BasisDecomposerTest, testing::Combine( diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_get_gate_kind.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_get_gate_kind.cpp new file mode 100644 index 0000000000..1a3ad968bb --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_get_gate_kind.cpp @@ -0,0 +1,78 @@ +/* + * 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/Builder/QCOProgramBuilder.h" +#include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h" + +#include +#include +#include +#include +#include +#include +#include + +using namespace mlir; +using namespace mlir::qco; + +class DecompositionGetGateKindTest : public ::testing::Test { +protected: + MLIRContext context; + QCOProgramBuilder builder{&context}; + + void SetUp() override { + context.loadDialect(); + context.loadDialect(); + context.loadDialect(); + context.loadDialect(); + builder.initialize(); + } +}; + +TEST_F(DecompositionGetGateKindTest, MapsBareSingleQubitOps) { + Value q = builder.staticQubit(0); + q = builder.rx(0.25, q); + auto mod = builder.finalize(); + ASSERT_TRUE(mod); + RXOp rx; + mod->walk([&](RXOp op) { + rx = op; + return WalkResult::interrupt(); + }); + ASSERT_TRUE(rx); + EXPECT_EQ(helpers::getGateKind(cast(rx.getOperation())), + decomposition::GateKind::RX); +} + +TEST_F(DecompositionGetGateKindTest, MapsCtrlBodyNotWrapper) { + Value c = builder.staticQubit(0); + Value t = builder.staticQubit(1); + auto [cOut, tOut] = + builder.ctrl(ValueRange{c}, ValueRange{t}, + [&](ValueRange targets) -> SmallVector { + return {builder.z(targets[0])}; + }); + (void)cOut; + (void)tOut; + auto mod = builder.finalize(); + ASSERT_TRUE(mod); + CtrlOp ctrl; + mod->walk([&](CtrlOp op) { + ctrl = op; + return WalkResult::interrupt(); + }); + ASSERT_TRUE(ctrl); + EXPECT_EQ(helpers::getGateKind(cast(ctrl.getOperation())), + decomposition::GateKind::Z); +} diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_helpers.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_helpers.cpp new file mode 100644 index 0000000000..cdc871428c --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_helpers.cpp @@ -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 + */ + +#include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h" + +#include + +#include +#include +#include + +using namespace mlir::qco::helpers; +using namespace mlir::qco::decomposition; + +TEST(DecompositionHelpersTest, RemEuclidNeverNegative) { + EXPECT_DOUBLE_EQ(remEuclid(-1.0, 3.0), 2.0); + EXPECT_DOUBLE_EQ(remEuclid(7.0, 3.0), 1.0); + EXPECT_DOUBLE_EQ(remEuclid(0.0, 2.5), 0.0); +} + +TEST(DecompositionHelpersTest, Mod2piWrapsIntoHalfOpenInterval) { + EXPECT_NEAR(mod2pi(0.0), 0.0, 1e-14); + EXPECT_NEAR(mod2pi(std::numbers::pi), -std::numbers::pi, 1e-12); + EXPECT_NEAR(mod2pi(3.0 * std::numbers::pi), -std::numbers::pi, 1e-12); +} + +TEST(DecompositionHelpersTest, TraceToFidelityMatchesFormula) { + const std::complex x{3.0, 4.0}; + const double absx = 5.0; + EXPECT_DOUBLE_EQ(traceToFidelity(x), (4.0 + absx * absx) / 20.0); +} + +TEST(DecompositionHelpersTest, GetComplexitySingleQubitAndGphase) { + EXPECT_EQ(getComplexity(GateKind::X, 1), 1U); + EXPECT_EQ(getComplexity(GateKind::GPhase, 1), 0U); +} + +TEST(DecompositionHelpersTest, GetComplexityMultiQubitUsesFactorModel) { + EXPECT_EQ(getComplexity(GateKind::RZZ, 2), 10U); +} + +TEST(DecompositionHelpersTest, GlobalPhaseFactorUnitMagnitude) { + const auto z = globalPhaseFactor(1.25); + EXPECT_NEAR(std::abs(z), 1.0, 1e-14); +} + +TEST(DecompositionHelpersTest, IsUnitaryMatrixRejectsNonUnitary) { + Eigen::Matrix2cd m; + m << 2.0, 0.0, 0.0, 2.0; + EXPECT_FALSE(isUnitaryMatrix(m)); +} diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp index 4bb48be77c..9feb327467 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -172,6 +172,58 @@ TEST(EulerDecompositionTest, ZsxxPauliXUsesSingleXGate) { EXPECT_TRUE(sequenceMatchesSingleQubitMatrix(pauliX, seq)); } +TEST(EulerDecompositionTest, GetGateTypesForEulerBasis) { + const auto zyz = getGateTypesForEulerBasis(EulerBasis::ZYZ); + ASSERT_EQ(zyz.size(), 2U); + EXPECT_EQ(zyz[0], GateKind::RZ); + EXPECT_EQ(zyz[1], GateKind::RY); + + const auto uFamily = getGateTypesForEulerBasis(EulerBasis::U321); + ASSERT_EQ(uFamily.size(), 1U); + EXPECT_EQ(uFamily[0], GateKind::U); + + const auto zsxx = getGateTypesForEulerBasis(EulerBasis::ZSXX); + ASSERT_EQ(zsxx.size(), 3U); + EXPECT_EQ(zsxx[0], GateKind::RZ); + EXPECT_EQ(zsxx[1], GateKind::SX); + EXPECT_EQ(zsxx[2], GateKind::X); +} + +TEST(EulerDecompositionTest, UAndU321MatchU3Reconstruction) { + std::mt19937 rng(99991); + for (int i = 0; i < 32; ++i) { + const auto u = randomUnitaryMatrix(rng); + const auto seqU3 = EulerDecomposition::generateCircuit(EulerBasis::U3, u, + true, std::nullopt); + const auto seqU = EulerDecomposition::generateCircuit(EulerBasis::U, u, + true, std::nullopt); + const auto seqU321 = EulerDecomposition::generateCircuit( + EulerBasis::U321, u, true, std::nullopt); + EXPECT_TRUE(EulerDecompositionTest::restore(seqU3).isApprox(u)); + EXPECT_TRUE(EulerDecompositionTest::restore(seqU).isApprox(u)); + EXPECT_TRUE(EulerDecompositionTest::restore(seqU321).isApprox(u)); + } +} + +TEST(EulerDecompositionTest, AnglesFromUnitaryXZXReconstructsRx) { + const Eigen::Matrix2cd u = rxMatrix(0.7); + (void)EulerDecomposition::anglesFromUnitary(u, EulerBasis::XZX); + const auto seq = EulerDecomposition::generateCircuit(EulerBasis::XZX, u, + false, std::nullopt); + EXPECT_TRUE(EulerDecompositionTest::restore(seq).isApprox(u)); +} + +TEST(EulerDecompositionTest, GateSequenceComplexityAndGlobalPhase) { + OneQubitGateSequence seq; + seq.gates.push_back( + {.type = GateKind::RZ, .parameter = {0.2}, .qubitId = {0}}); + seq.globalPhase = 0.5; + EXPECT_TRUE(seq.hasGlobalPhase()); + EXPECT_GE(seq.complexity(), 1U); + seq.globalPhase = 0.0; + EXPECT_FALSE(seq.hasGlobalPhase()); +} + INSTANTIATE_TEST_SUITE_P( SingleQubitMatrices, EulerDecompositionTest, testing::Combine(testing::Values(EulerBasis::XYX, EulerBasis::XZX, diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt index 7fce8b4eab..72185daae6 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt @@ -9,12 +9,27 @@ set(target_name mqt-core-mlir-unittest-native-synthesis) add_executable( ${target_name} - native_synthesis_test_helpers.cpp test_native_synthesis_pass_custom_menus.cpp - test_native_synthesis_pass_fusion.cpp test_native_synthesis_pass_multi_qubit.cpp - test_native_synthesis_pass_profiles.cpp test_native_synthesis_pass_scoring.cpp) + native_synthesis_test_helpers.cpp + test_native_policy.cpp + test_native_spec.cpp + test_native_synthesis_pass_custom_menus.cpp + test_native_synthesis_pass_fusion.cpp + test_native_synthesis_pass_multi_qubit.cpp + test_native_synthesis_pass_profiles.cpp + test_native_synthesis_pass_scoring.cpp) -target_link_libraries(${target_name} PRIVATE MLIRParser GTest::gtest_main MLIRQCProgramBuilder - MLIRQCToQCO MLIRQCOTransforms) +target_link_libraries( + ${target_name} + PRIVATE MLIRParser + GTest::gtest_main + MLIRQCProgramBuilder + MLIRQCOProgramBuilder + MLIRQCOUtils + MLIRQCToQCO + MLIRQCOTransforms + MLIRPass + MLIRSupport + LLVMSupport) mqt_mlir_configure_unittest_target(${target_name}) diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_policy.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_policy.cpp new file mode 100644 index 0000000000..6634695f2b --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_policy.cpp @@ -0,0 +1,102 @@ +/* + * 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/Builder/QCOProgramBuilder.h" +#include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h" + +#include +#include +#include +#include +#include +#include +#include + +using namespace mlir; +using namespace mlir::qco; +using namespace mlir::qco::decomposition; +using namespace mlir::qco::native_synth; + +TEST(NativePolicyTest, ComputeGateSequenceMetricsDepth) { + QubitGateSequence seq; + seq.gates.push_back( + {.type = GateKind::RZ, .parameter = {0.1}, .qubitId = {0}}); + seq.gates.push_back( + {.type = GateKind::RZ, .parameter = {0.2}, .qubitId = {0}}); + seq.gates.push_back( + {.type = GateKind::RZZ, .parameter = {0.3}, .qubitId = {0, 1}}); + const CandidateMetrics m = computeGateSequenceMetrics(seq); + EXPECT_EQ(m.numOneQ, 2U); + EXPECT_EQ(m.numTwoQ, 1U); + EXPECT_EQ(m.depth, 3U); +} + +TEST(NativePolicyTest, UsesCxAndCzFromResolvedSpec) { + const auto cxOnly = resolveNativeGatesSpec("u,cx"); + ASSERT_TRUE(cxOnly); + EXPECT_TRUE(usesCxEntangler(*cxOnly)); + EXPECT_FALSE(usesCzEntangler(*cxOnly)); + + const auto both = resolveNativeGatesSpec("u,cx,cz"); + ASSERT_TRUE(both); + EXPECT_TRUE(usesCxEntangler(*both)); + EXPECT_TRUE(usesCzEntangler(*both)); +} + +class NativePolicyAllowsOpTest : public ::testing::Test { +protected: + MLIRContext context; + QCOProgramBuilder builder{&context}; + + void SetUp() override { + context.loadDialect(); + context.loadDialect(); + context.loadDialect(); + context.loadDialect(); + builder.initialize(); + } +}; + +TEST_F(NativePolicyAllowsOpTest, AllowsSingleQubitOpRespectsMenu) { + const auto spec = resolveNativeGatesSpec("x,sx,rz,cx"); + ASSERT_TRUE(spec); + Value q = builder.staticQubit(0); + q = builder.x(q); + auto mod = builder.finalize(); + ASSERT_TRUE(mod); + XOp xop; + mod->walk([&](XOp op) { + xop = op; + return WalkResult::interrupt(); + }); + ASSERT_TRUE(xop); + EXPECT_TRUE( + allowsSingleQubitOp(cast(xop.getOperation()), *spec)); +} + +TEST_F(NativePolicyAllowsOpTest, CanDirectlyDecomposeToU3OnRxInCircuit) { + Value q = builder.staticQubit(0); + q = builder.rx(0.1, q); + auto mod = builder.finalize(); + ASSERT_TRUE(mod); + RXOp rx; + mod->walk([&](RXOp op) { + rx = op; + return WalkResult::interrupt(); + }); + ASSERT_TRUE(rx); + EXPECT_TRUE(canDirectlyDecomposeToU3(rx.getOperation())); +} diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_spec.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_spec.cpp new file mode 100644 index 0000000000..0db2ef2d40 --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_spec.cpp @@ -0,0 +1,66 @@ +/* + * 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/NativeSynthesis/NativeSpec.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" + +#include +#include + +using namespace mlir::qco::decomposition; +using namespace mlir::qco::native_synth; + +TEST(NativeSpecTest, ResolveIbmBasicCx) { + const auto spec = resolveNativeGatesSpec("x,sx,rz,cx"); + ASSERT_TRUE(spec); + EXPECT_TRUE(spec->allowedGates.contains(NativeGateKind::Cx)); + EXPECT_TRUE(spec->allowedGates.contains(NativeGateKind::X)); + EXPECT_FALSE(spec->allowRzz); +} + +TEST(NativeSpecTest, ResolveRejectsUnknownToken) { + EXPECT_FALSE(resolveNativeGatesSpec("x,sx,rz,not-a-gate").has_value()); +} + +TEST(NativeSpecTest, ResolveEmptyOrWhitespaceOnlyReturnsNullopt) { + EXPECT_FALSE(resolveNativeGatesSpec("").has_value()); + EXPECT_FALSE(resolveNativeGatesSpec(" \t ").has_value()); + EXPECT_FALSE(resolveNativeGatesSpec(",,,").has_value()); +} + +TEST(NativeSpecTest, PhaseAliasPMatchesRzInIbmStyleMenu) { + const auto pMenu = resolveNativeGatesSpec("x,sx,p,cx"); + const auto rzMenu = resolveNativeGatesSpec("x,sx,rz,cx"); + ASSERT_TRUE(pMenu); + ASSERT_TRUE(rzMenu); + EXPECT_EQ(pMenu->allowedGates, rzMenu->allowedGates); +} + +TEST(NativeSpecTest, GetEulerBasesForAxisPair) { + const auto rxRz = getEulerBasesForAxisPair(AxisPair::RxRz); + ASSERT_EQ(rxRz.size(), 1U); + EXPECT_EQ(rxRz[0], EulerBasis::XZX); + + const auto rxRy = getEulerBasesForAxisPair(AxisPair::RxRy); + ASSERT_EQ(rxRy.size(), 1U); + EXPECT_EQ(rxRy[0], EulerBasis::XYX); + + const auto ryRz = getEulerBasesForAxisPair(AxisPair::RyRz); + ASSERT_EQ(ryRz.size(), 1U); + EXPECT_EQ(ryRz[0], EulerBasis::ZYZ); +} + +TEST(NativeSpecTest, RzzSetsAllowRzzFlag) { + const auto spec = resolveNativeGatesSpec("u,cx,rzz"); + ASSERT_TRUE(spec); + EXPECT_TRUE(spec->allowRzz); + EXPECT_TRUE(spec->allowedGates.contains(NativeGateKind::Rzz)); +} From 8833b72136e866461c3493809e7a4d631fbb4fd9 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 23 Apr 2026 10:06:50 +0200 Subject: [PATCH 006/122] =?UTF-8?q?=E2=9C=A8=20Update=20documentation=20fo?= =?UTF-8?q?r=20native=20gate=20synthesis=20pass=20with=20enhanced=20exampl?= =?UTF-8?q?es=20and=20execution=20details.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/include/mlir/Compiler/CompilerPipeline.h | 14 +++-- .../mlir/Dialect/QCO/Transforms/Passes.td | 51 ++++++++++--------- 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/mlir/include/mlir/Compiler/CompilerPipeline.h b/mlir/include/mlir/Compiler/CompilerPipeline.h index 2809a44a0f..c03a055fab 100644 --- a/mlir/include/mlir/Compiler/CompilerPipeline.h +++ b/mlir/include/mlir/Compiler/CompilerPipeline.h @@ -47,11 +47,15 @@ struct QuantumCompilerConfig { /// Comma-separated native gate menu. Recognised tokens: `u`, `x`, `sx`, /// `rz` (or `p`), `rx`, `ry`, `r`, `cx`, `cz`, `rzz`. An empty or /// whitespace-only string leaves native synthesis as a no-op (IR - /// unchanged). Common examples: - /// - `"x,sx,rz,cx"` — IBM basic (CX) - /// - `"x,sx,rz,rx,rzz,cz"` — IBM fractional - /// - `"r,cz"` — IQM default - /// - `"u,cx"` — generic U3 + CX + /// unchanged). Illustrative menus (use `cx` or `cz` as the entangler, or + /// both): + /// - `"x,sx,rz,cx"` / `"x,sx,rz,cz"` — IBM basic (no fractional 2q) + /// - `"x,sx,rz,rx,rzz,cx"` / `"...,cz"` — IBM fractional + /// - `"u,cx"` / `"u,cz"` — generic single-qubit `qco.u` (menu token `u`, not + /// `u3`) + /// - `"r,cz"` — IQM-style default + /// - `"rx,rz,cx"`, `"rx,ry,cz"`, `"ry,rz,cx"` — supported `rx`/`ry`/`rz` + /// pairs plus entangler std::string nativeGates; /// Weight for two-qubit gates in local candidate scoring diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index 0e485cced9..063a54b735 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -110,10 +110,12 @@ def NativeGateSynthesisPass : Pass<"native-gate-synthesis", "mlir::ModuleOp"> { preserved; controlled gates (`qco.ctrl`) must have a single control and a single target. - The menu is a comma-separated list of gate tokens from which the pass - derives a single-qubit synthesis strategy (`u`, `zsxx`, IQM-style `r`, or - an axis pair `rx`/`ry`/`rz`) and the set of available two-qubit entanglers - (`cx`, `cz`, `rzz`). + The menu is a comma-separated list of gate tokens (order not significant) + from which the pass builds a profile: a single-qubit synthesis mode + (generic `qco.u` when `u` is present; IBM-style surface gates when all of + `x`, `sx`, and `rz`/`p` are present; IQM-style `qco.r` when `r` is present; + or a supported rotation pair chosen from `rx`, `ry`, `rz`) plus optional + two-qubit entanglers `cx`, `cz`, and optional `rzz`. Recognised tokens: `u`, `x`, `sx`, `rz` (or `p`), `rx`, `ry`, `r`, `cx`, `cz`, `rzz`. An empty or whitespace-only menu is a no-op, which is the @@ -121,24 +123,25 @@ def NativeGateSynthesisPass : Pass<"native-gate-synthesis", "mlir::ModuleOp"> { token or an invalid score weight (non-finite or negative) causes the pass to fail. - Example menus: - - `x,sx,rz,cx` (IBM basic) - - `x,sx,rz,rx,rzz,cz` (IBM fractional) - - `r,cz` (IQM default) - - `u,cx` (generic U3 + CX) - - `rx,rz,cx` (Rx/Rz axis pair + CX) - - The pass runs single-qubit fusion, then a two-qubit window pass (including - absorbed single-qubit padding), then up to four lowering sweeps over - remaining non-native unitaries. Two-qubit lowering may emit temporary - off-menu single-qubit gates; later sweeps try to absorb them. If any - off-menu single-qubit gates remain after that cap, the pass fails. - - It then fuses single-qubit runs again (seams between two-qubit blocks), - merges `rz` angles through `qco.ctrl` control chains where valid, fuses - single-qubit runs once more, and runs up to four optional lowering + - fusion rounds until the full menu holds (including `qco.ctrl` shells and - bare two-qubit gates). If anything is still off-menu, the pass fails. + Example menus (each line is one illustrative menu; pick either `cx` or + `cz` as the entangler, or list both if both are native): + - IBM basic (no fractional two-qubit): `x,sx,rz,cx` or `x,sx,rz,cz` + - IBM fractional: `x,sx,rz,rx,rzz,cx` or `x,sx,rz,rx,rzz,cz` + - Generic single-qubit U: `u,cx` or `u,cz` + - IQM default: `r,cz` (or `r,cx` if CX is the native entangler) + - Rotation pair + entangler: `rx,rz,cx`, `rx,ry,cz`, `ry,rz,cx`, etc. + Supported pairs are exactly `rx`+`rz`, `rx`+`ry`, and `ry`+`rz`. + + Execution order (mirrors the implementation): fuse consecutive + single-qubit runs; consolidate two-qubit windows (including absorbed + single-qubit padding); run up to four synthesis sweeps over remaining + non-native unitaries until every single-qubit op matches the menu (two-qubit + lowering may temporarily emit off-menu 1q ops that later sweeps absorb—if + any remain after that cap, the pass fails); fuse 1q seams between two-qubit + blocks; merge `rz` through eligible `qco.ctrl` control wires; fuse 1q runs + again; then up to four further synthesis + fusion rounds until the full menu + holds (including native `qco.ctrl` shells and bare `rzz` when allowed). If + anything is still off-menu, the pass fails. Candidate selection minimises the linear cost `score-weight-twoq * #2q + score-weight-oneq * #1q + @@ -155,8 +158,8 @@ def NativeGateSynthesisPass : Pass<"native-gate-synthesis", "mlir::ModuleOp"> { let options = [Option<"nativeGates", "native-gates", "std::string", "\"\"", "Comma-separated native gate menu. Empty or whitespace-only is " - "a no-op. Recognised tokens: u, x, sx, rz (or p), rx, ry, r, cx, " - "cz, rzz.">, + "a no-op. Tokens: u, x, sx, rz (or p), rx, ry, r, cx, cz, rzz. " + "Examples: x,sx,rz,cx; x,sx,rz,rx,rzz,cz; u,cx; r,cz; rx,rz,cx.">, Option<"scoreWeightTwoQ", "score-weight-twoq", "double", "1.0", "Weight for the number of two-qubit gates in candidate " "scoring. Must be finite and non-negative.">, From b4257de56900b34cc340a2087a10c624a3a241f9 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 23 Apr 2026 14:06:25 +0200 Subject: [PATCH 007/122] =?UTF-8?q?=F0=9F=93=9D=20Add=20docstrings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Decomposition/BasisDecomposer.h | 3 - .../Decomposition/UnitaryMatrices.h | 4 +- .../Decomposition/WeylDecomposition.h | 36 ++++----- .../NativeSynthesis/PassTwoQubitWindows.h | 25 +++++- .../Transforms/NativeSynthesis/SingleQubit.h | 23 ++++-- .../QCO/Transforms/NativeSynthesis/Types.h | 10 ++- .../Decomposition/BasisDecomposer.cpp | 35 +++++++-- .../QCO/Transforms/Decomposition/Helpers.cpp | 4 + .../Decomposition/UnitaryMatrices.cpp | 18 +++-- .../Decomposition/WeylDecomposition.cpp | 24 ++++++ .../Transforms/NativeSynthesis/NativeSpec.cpp | 50 ++++++++++-- .../QCO/Transforms/NativeSynthesis/Pass.cpp | 59 +++++++++++++- .../NativeSynthesis/PassTwoQubitWindows.cpp | 78 ++++++++++++++++++- .../QCO/Transforms/NativeSynthesis/Policy.cpp | 5 ++ .../NativeSynthesis/SingleQubit.cpp | 42 +++++++++- .../Transforms/NativeSynthesis/TwoQubit.cpp | 37 +++++++++ .../QCO/Transforms/NativeSynthesis/Utils.cpp | 67 ++++++++++++---- .../Compiler/test_compiler_pipeline.cpp | 13 +--- .../Decomposition/decomposition_test_utils.h | 39 +++------- .../Decomposition/test_basis_decomposer.cpp | 1 + .../test_euler_decomposition.cpp | 1 + .../Decomposition/test_weyl_decomposition.cpp | 1 + .../native_synthesis_test_helpers.cpp | 12 +-- .../native_synthesis_test_helpers.h | 12 +-- mlir/unittests/TestCaseUtils.h | 22 ++++++ 25 files changed, 482 insertions(+), 139 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.h index b7ce1ac4e6..b52aff37e3 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.h @@ -202,9 +202,6 @@ class TwoQubitBasisDecomposer { [[nodiscard]] static OneQubitGateSequence unitaryToGateSequence(const Eigen::Matrix2cd& unitaryMat, const llvm::SmallVector& targetBasisList, - QubitId /*qubit*/, - // Reserved for future error-aware synthesis (per-qubit - // op→error maps feeding calculateError()). bool simplify, std::optional atol); [[nodiscard]] static bool relativeEq(double lhs, double rhs, double epsilon, diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h index 150a5e6966..de9064666c 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h @@ -25,9 +25,9 @@ inline constexpr double FRAC1_SQRT2 = 0.707106781186547524400844362104849039284835937688474036588L; /// Generic 3-parameter single-qubit unitary `U(theta, phi, lambda)`. -[[nodiscard]] Eigen::Matrix2cd uMatrix(double lambda, double phi, double theta); +[[nodiscard]] Eigen::Matrix2cd uMatrix(double theta, double phi, double lambda); /// `U2(phi, lambda) == U(pi/2, phi, lambda)`. -[[nodiscard]] Eigen::Matrix2cd u2Matrix(double lambda, double phi); +[[nodiscard]] Eigen::Matrix2cd u2Matrix(double phi, double lambda); /// Axis rotations `exp(-i theta/2 * sigma_{x,y,z})`. [[nodiscard]] Eigen::Matrix2cd rxMatrix(double theta); [[nodiscard]] Eigen::Matrix2cd ryMatrix(double theta); diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.h index 0747edf617..b59e11d4d1 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.h @@ -14,7 +14,6 @@ #include // NOLINT(misc-include-cleaner) -#include #include #include #include @@ -224,27 +223,22 @@ class TwoQubitWeylDecomposition { 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 + // Canonical gate parameters `(a, b, c)`; documented on the public accessors. + double a_{}; + double b_{}; + double c_{}; + double globalPhase_{}; + // Single-qubit factors surrounding the canonical gate; see the accessors + // for the per-field wiring diagram. + Eigen::Matrix2cd k1l_; + Eigen::Matrix2cd k2l_; + Eigen::Matrix2cd k1r_; + Eigen::Matrix2cd k2r_; + Specialization specialization{Specialization::General}; + EulerBasis defaultEulerBasis{EulerBasis::U3}; /// Optional `traceToFidelity` floor for specialization; unset disables it. std::optional requestedFidelity; - double calculatedFidelity{}; // actual fidelity of decomposition - Eigen::Matrix4cd unitaryMatrix; // original matrix for this decomposition + double calculatedFidelity{}; + Eigen::Matrix4cd unitaryMatrix; }; } // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.h index 23da65197f..9648803f37 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.h @@ -23,8 +23,6 @@ #include #include -#include - namespace mlir::qco::native_synth { /// State for one maximal two-qubit window (plus absorbed one-qubit ops) @@ -41,17 +39,38 @@ struct TwoQubitBlock { }; /// Pre-order walk: every op implementing `UnitaryOpInterface` under `root`. -void collectUnitaryOpsInPreOrder(Operation* root, std::vector& ops); +void collectUnitaryOpsInPreOrder(Operation* root, + llvm::SmallVectorImpl& ops); /// Tracks overlapping two-qubit windows on a module slice; implemented in /// ``NativeSynthesis/PassTwoQubitWindows.cpp``. struct TwoQubitWindowConsolidator { + /// Append-only list of windows discovered so far; closed windows are kept + /// so `materialize()` can still rewrite them. llvm::SmallVector blocks; + /// Maps each currently-open SSA qubit value to the index of the block + /// that owns its trailing wire. llvm::DenseMap wireToBlock; + /// Mark block `idx` as closed and remove its tracked wires from + /// `wireToBlock`. Idempotent: closing an already-closed block is a no-op. void closeBlock(size_t idx); + + /// If `v` is currently tracked, close the block that owns it; otherwise + /// do nothing. Used at synchronization points (barriers, fan-out, etc.). void closeBlockOnWire(Value v); + + /// State-machine step for one IR op, called in pre-order walk order. + /// Extends an existing window, starts a fresh one, or closes conflicting + /// windows depending on the op's kind and operand use pattern. See the + /// definition for the full decision table. void process(Operation* op, const NativeProfileSpec& spec); + + /// Rewrite each collected window whose accumulated unitary can be + /// realized more cheaply by the native-gate synthesizer. + /// Picks the best candidate per block via `selectBestCandidate`, + /// gates the replacement on `shouldApplyBlockReplacement`, and emits the + /// new sequence through `rewriter`. void materialize(IRRewriter& rewriter, const NativeProfileSpec& spec, const ScoreWeights& weights); }; diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h index e74de402be..7c3183673a 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h @@ -24,16 +24,29 @@ namespace mlir::qco::native_synth { -/// Direct (non-matrix) single-qubit lowering to each single-qubit emission -/// strategy. Returns the output qubit value, or a null `Value` if no direct -/// rule applies and a matrix-based fallback must be tried. +/// Direct (non-matrix) single-qubit lowering to the `ZSXX` emitter +/// (`{Rz, Sx, X}`). Returns the output qubit value, or a null `Value` if no +/// direct rule applies and a matrix-based fallback must be tried. /// -/// When `supportsDirectRx` is true, `decomposeToZSXX` also passes `Rx` -/// through unchanged and lowers `Ry` / `R` via an `rz * rx * rz` sandwich. +/// When `supportsDirectRx` is true, the emitter also passes `Rx` through +/// unchanged and lowers `Ry` / `R` via an `rz * rx * rz` sandwich. Value decomposeToZSXX(IRRewriter& rewriter, Operation* op, Value inQubit, bool supportsDirectRx); + +/// Direct (non-matrix) single-qubit lowering to a `U(theta, phi, lambda)` +/// output. Returns the output qubit value, or a null `Value` if no direct +/// rule applies and a matrix-based fallback must be tried. Value decomposeToU3(IRRewriter& rewriter, Operation* op, Value inQubit); + +/// Direct (non-matrix) single-qubit lowering to the `R(theta, phi)` emitter. +/// Returns the output qubit value, or a null `Value` if no direct rule +/// applies and a matrix-based fallback must be tried. Value decomposeToR(IRRewriter& rewriter, Operation* op, Value inQubit); + +/// Direct (non-matrix) single-qubit lowering to a two-axis emitter +/// identified by `axisPair` (e.g. `{Rx, Rz}`, `{Ry, Rz}`). Returns the +/// output qubit value, or a null `Value` if no direct rule applies and a +/// matrix-based fallback must be tried. Value decomposeToAxisPair(IRRewriter& rewriter, Operation* op, Value inQubit, AxisPair axisPair); diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h index 62d517c73a..c9d4390032 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h @@ -13,16 +13,17 @@ #include "mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h" +#include #include #include -#include /// Types for native gate synthesis: menu, emitters, candidates, score weights. namespace mlir::qco::native_synth { -/// Two-axis single-qubit families for `axis-pair-*` profiles. +/// Two-axis token pairs (`rx`+`rz`, `rx`+`ry`, `ry`+`rz`) that can be selected +/// as the single-qubit menu in a `NativeProfileSpec`. enum class AxisPair : std::uint8_t { RxRz, RxRy, RyRz }; /// Single-qubit emission strategy. @@ -46,6 +47,9 @@ enum class EntanglerBasis : std::uint8_t { None, Cx, Cz }; /// Profile-level classification of a native gate. Used both to describe the /// menu (`NativeProfileSpec::allowedGates`) and to classify already-lowered /// output ops in policy checks. One-to-one with a recognised menu token. +/// +/// The tokens `rz` and `p` are aliases and both map to `Rz` during menu +/// resolution (see `NativeSpec.cpp`). enum class NativeGateKind : std::uint8_t { U, X, @@ -77,7 +81,7 @@ struct SingleQubitEmitterSpec { struct NativeProfileSpec { bool allowRzz = false; /// Flattened menu; used for cheap "is this op already native?" checks. - std::set allowedGates; + llvm::DenseSet allowedGates; llvm::SmallVector singleQubitEmitters; llvm::SmallVector entanglerBases; }; diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp index 4965944a34..fb00d7dfab 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp @@ -181,8 +181,15 @@ std::optional TwoQubitBasisDecomposer::twoQubitDecompose( 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 + // Pick the number of basis gate uses `i ∈ {0, 1, 2, 3}` that maximizes + // expected_fidelity(i) = traceToFidelity(traces[i]) * basisFidelity^i + // i.e. "how well does using `i` basis gates approximate the target, + // assuming each basis gate has fidelity `basisFidelity`". With + // `basisFidelity == 1.0` (exact mode) the `pow` factor is constant and + // the larger `i` values tend to win because they can represent any + // SU(4); when `basisFidelity < 1.0` the `pow(...)^i` penalty lets + // shorter (lower-`i`) approximations win when the target is close + // enough. This is *not* a "smallest `i` above a threshold" rule. auto bestValue = std::numeric_limits::lowest(); auto bestIndex = -1; for (int i = 0; std::cmp_less(i, traces.size()); ++i) { @@ -228,8 +235,8 @@ std::optional TwoQubitBasisDecomposer::twoQubitDecompose( llvm::SmallVector eulerDecompositions; for (auto&& decomp : decomposition) { assert(helpers::isUnitaryMatrix(decomp)); - auto eulerDecomp = unitaryToGateSequence(decomp, target1qEulerBases, 0, - true, std::nullopt); + auto eulerDecomp = + unitaryToGateSequence(decomp, target1qEulerBases, true, std::nullopt); eulerDecompositions.push_back(eulerDecomp); } TwoQubitGateSequence gates{ @@ -244,6 +251,10 @@ std::optional TwoQubitBasisDecomposer::twoQubitDecompose( gates.gates.reserve(twoQubitSequenceDefaultCapacity); gates.globalPhase -= bestNbasis * basisDecomposer.globalPhase(); if (bestNbasis == 2) { + // The two-basis (2x CX/CZ) template in `decomp2Supercontrolled` produces + // a sequence whose global phase is off by `pi` relative to the target; + // compensate here so the emitted sequence reproduces the target + // unitary exactly, not just up to sign. gates.globalPhase += std::numbers::pi; } @@ -345,6 +356,16 @@ TwoQubitBasisDecomposer::decomp3Supercontrolled( std::array, 4> TwoQubitBasisDecomposer::traces(const TwoQubitWeylDecomposition& target) const { + // Returns the Hilbert-Schmidt traces between the target canonical gate and + // the best candidate reachable with `0, 1, 2, 3` uses of the basis gate, + // respectively. Fed into `traceToFidelity` by `getDefaultNbasis` to pick + // the best basis-gate count. The closed-form expressions specialize + // `TwoQubitWeylDecomposition::getTrace(a, b, c, ap, bp, cp)` for: + // i == 0: no basis gate (ap == bp == cp == 0) + // i == 1: one basis use (ap == pi/4, bp == basis.b, cp == 0) + // i == 2: two basis uses (ap == 0, bp == 0, cp == -target.c) + // i == 3: three basis uses (target reachable exactly -> trace == 4) + // so the array has length 4 and is indexed by the number of basis uses. return { 4. * std::complex{std::cos(target.a()) * std::cos(target.b()) * std::cos(target.c()), @@ -364,10 +385,8 @@ TwoQubitBasisDecomposer::traces(const TwoQubitWeylDecomposition& target) const { 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) { + const llvm::SmallVector& targetBasisList, bool simplify, + std::optional atol) { assert(!targetBasisList.empty()); auto calculateError = [](const OneQubitGateSequence& sequence) -> double { diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp index de233d0163..ad7137e0c0 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp @@ -100,6 +100,10 @@ double mod2pi(double angle, double angleZeroEpsilon) { } double traceToFidelity(const std::complex& x) { + // Average two-qubit process fidelity given the Hilbert-Schmidt overlap + // `x = tr(U_target^dag * U_actual)`. For a 4x4 unitary the general formula is + // `F_avg = (d + |tr|^2) / (d * (d + 1))` with `d = 4`, which reduces to the + // `(4 + |x|^2) / 20` expression below. See e.g. Horodecki/Nielsen. auto xAbs = std::abs(x); return (4.0 + xAbs * xAbs) / 20.0; } diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp index 5319b68df2..30a0ac7f98 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp @@ -21,7 +21,7 @@ namespace mlir::qco::decomposition { -Eigen::Matrix2cd uMatrix(double lambda, double phi, double theta) { +Eigen::Matrix2cd uMatrix(double theta, double phi, double lambda) { return Eigen::Matrix2cd{{{{std::cos(theta / 2.), 0.}, {-std::cos(lambda) * std::sin(theta / 2.), -std::sin(lambda) * std::sin(theta / 2.)}}, @@ -31,7 +31,7 @@ Eigen::Matrix2cd uMatrix(double lambda, double phi, double theta) { std::sin(lambda + phi) * std::cos(theta / 2.)}}}}; } -Eigen::Matrix2cd u2Matrix(double lambda, double phi) { +Eigen::Matrix2cd u2Matrix(double phi, double lambda) { return Eigen::Matrix2cd{ {FRAC1_SQRT2, {-std::cos(lambda) * FRAC1_SQRT2, -std::sin(lambda) * FRAC1_SQRT2}}, @@ -151,11 +151,13 @@ Eigen::Matrix2cd getSingleQubitMatrix(const Gate& gate) { } if (gate.type == GateKind::U) { assert(gate.parameter.size() == 3); - return uMatrix(gate.parameter[0], gate.parameter[1], gate.parameter[2]); + // EulerDecomposition stores `U` parameters as {lambda, phi, theta}. + return uMatrix(gate.parameter[2], gate.parameter[1], gate.parameter[0]); } if (gate.type == GateKind::U2) { assert(gate.parameter.size() == 2); - return u2Matrix(gate.parameter[0], gate.parameter[1]); + // `U2` parameters are stored as {lambda, phi}. + return u2Matrix(gate.parameter[1], gate.parameter[0]); } if (gate.type == GateKind::H) { return H_GATE; @@ -174,12 +176,18 @@ Eigen::Matrix4cd getTwoQubitMatrix(const Gate& gate) { } if (gate.qubitId.size() == 2) { if (gate.type == GateKind::X) { - // controlled X (CX) + // Controlled-X. The two matrices below are the *same* CX gate written in + // the two possible operand orderings used by `Gate::qubitId`: qubit 0 is + // the MSB of the 4x4 computational basis (matching + // `UnitaryOpInterface::getUnitaryMatrix4x4`), so swapping + // control/target wires produces a different basis-layout matrix. if (gate.qubitId == llvm::SmallVector{0, 1}) { + // control = wire 0 (MSB), target = wire 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}) { + // control = wire 1, target = wire 0 (MSB). return Eigen::Matrix4cd{ {1, 0, 0, 0}, {0, 0, 0, 1}, {0, 0, 1, 0}, {0, 1, 0, 0}}; } diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.cpp index d4449b29e3..e091fd8c76 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.cpp @@ -38,6 +38,11 @@ TwoQubitWeylDecomposition::create(const Eigen::Matrix4cd& unitaryMatrix, std::optional fidelity) { auto u = unitaryMatrix; auto detU = u.determinant(); + // Project into SU(4) by dividing out the fourth root of det(U): for a 4x4 + // unitary, |det(U)| == 1 so `det^{-1/4}` both enforces det == 1 and removes + // the global phase. The extracted phase is tracked separately in + // `globalPhase` (quarter of arg(det) to match the fourth-root choice) so the + // caller can reconstruct the original matrix exactly if needed. auto detPow = std::pow(detU, -0.25); u *= detPow; // remove global phase from unitary matrix auto globalPhase = std::arg(detU) / 4.; @@ -264,6 +269,12 @@ TwoQubitWeylDecomposition::create(const Eigen::Matrix4cd& unitaryMatrix, Eigen::Matrix4cd TwoQubitWeylDecomposition::getCanonicalMatrix(double a, double b, double c) { + // Canonical gate `U_d(a, b, c) = exp(i * (a*XX + b*YY + c*ZZ))`. XX/YY/ZZ + // commute pairwise, so any product order is equivalent; the order below is + // chosen to match common Qiskit/QuantumFlow references. The negated rotation + // angles (`-2 * a`, ...) compensate for the `RXX/RYY/RZZ` convention + // `exp(-i * theta/2 * XX)` used in `getTwoQubitMatrix`, so that the + // factored angles sum back to the intended `+a`, `+b`, `+c`. auto xx = getTwoQubitMatrix({ .type = GateKind::RXX, .parameter = {-2.0 * a}, @@ -286,6 +297,12 @@ Eigen::Matrix4cd TwoQubitWeylDecomposition::magicBasisTransform(const Eigen::Matrix4cd& unitary, MagicBasisTransform direction) { using namespace std::complex_literals; + // Makhlin "magic basis" transform. Conjugating a 2-qubit unitary by + // `bNonNormalized` maps SU(2) x SU(2) factors onto SO(4) and diagonalizes + // the canonical (Weyl) gate. The matrices are stored unnormalized: the + // `1/2` pre-factor that would normally appear in `B^dagger` is absorbed + // into `bNonNormalizedDagger` directly so the product `Bd * B == I` + // without an extra scalar. const Eigen::Matrix4cd bNonNormalized{ {1, 1i, 0, 0}, {0, 0, 1i, 1}, @@ -421,6 +438,13 @@ TwoQubitWeylDecomposition::decomposeTwoQubitProductGate( std::complex TwoQubitWeylDecomposition::getTrace(double a, double b, double c, double ap, double bp, double cp) { + // Closed-form Hilbert-Schmidt overlap `tr(U_d(a,b,c)^dag * U_d(ap,bp,cp))` + // between two canonical (Weyl) gates, expressed in terms of the coordinate + // differences. Feeding the result into `traceToFidelity` gives the average + // two-qubit gate fidelity between the two canonical gates, which + // `bestSpecialization` uses to rank candidate specializations. + // Reference: Zhang et al., "Geometric theory of nonlocal two-qubit + // operations", Phys. Rev. A 67, 042313 (2003), Eq. (20). auto da = a - ap; auto db = b - bp; auto dc = c - cp; diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.cpp index bc834d67ca..27780230c6 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.cpp @@ -10,13 +10,20 @@ #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h" +#include #include +#include #include #include +#include + namespace mlir::qco::native_synth { namespace { +/// Map a single native-gate token (lower-case, no whitespace) to its +/// `NativeGateKind`. `"p"` is accepted as an alias for `"rz"` since both +/// lower to `RZOp` in the IR. Returns `std::nullopt` for unknown tokens. std::optional parseGateToken(llvm::StringRef name) { return llvm::StringSwitch>(name) .Case("u", NativeGateKind::U) @@ -32,9 +39,13 @@ std::optional parseGateToken(llvm::StringRef name) { .Default(std::nullopt); } -std::optional> +/// Parse a comma-separated native-gate menu (e.g. `"u,cx,rzz"`) into the set +/// of `NativeGateKind`s it names. Whitespace is trimmed and tokens are +/// lower-cased; empty tokens are skipped silently. Returns `std::nullopt` if +/// any non-empty token fails to parse. +std::optional> parseGateSet(llvm::StringRef nativeGates) { - std::set gates; + llvm::DenseSet gates; llvm::SmallVector parts; nativeGates.split(parts, ',', /*MaxSplit=*/-1, /*KeepEmpty=*/false); for (llvm::StringRef part : parts) { @@ -51,6 +62,10 @@ parseGateSet(llvm::StringRef nativeGates) { return gates; } +/// Build a fully-resolved `SingleQubitEmitterSpec` for `mode`, including the +/// list of Euler bases the matrix-fallback path is allowed to use. `axisPair` +/// is only consulted for `SingleQubitMode::AxisPair`; `supportsDirectRx` is +/// only meaningful for `SingleQubitMode::ZSXX`. SingleQubitEmitterSpec makeEmitterSpec(SingleQubitMode mode, AxisPair axisPair = AxisPair::RxRz, bool supportsDirectRx = false) { @@ -77,6 +92,9 @@ SingleQubitEmitterSpec makeEmitterSpec(SingleQubitMode mode, .supportsDirectRx = supportsDirectRx}; } +/// Append a new emitter for `(mode, axisPair, supportsDirectRx)` to +/// `emitters` iff no equivalent entry is already present. Keeps the resolved +/// list deduplicated without relying on the caller's ordering. void addEmitterIfAbsent(llvm::SmallVectorImpl& emitters, SingleQubitMode mode, AxisPair axisPair = AxisPair::RxRz, @@ -90,14 +108,17 @@ void addEmitterIfAbsent(llvm::SmallVectorImpl& emitters, } } -std::set +/// Enumerate the native gate kinds that `emitter` may actually emit. Used +/// to build `NativeProfileSpec::allowedGates` so downstream passes can cheaply +/// test whether a concrete op belongs to the resolved menu. +llvm::SmallVector allowedGatesForEmitter(const SingleQubitEmitterSpec& emitter) { switch (emitter.mode) { case SingleQubitMode::ZSXX: { - std::set gates{NativeGateKind::X, NativeGateKind::Sx, - NativeGateKind::Rz}; + llvm::SmallVector gates{ + NativeGateKind::X, NativeGateKind::Sx, NativeGateKind::Rz}; if (emitter.supportsDirectRx) { - gates.insert(NativeGateKind::Rx); + gates.push_back(NativeGateKind::Rx); } return gates; } @@ -119,7 +140,10 @@ allowedGatesForEmitter(const SingleQubitEmitterSpec& emitter) { llvm_unreachable("unknown single-qubit mode"); } -std::set allowedGatesForEntangler(EntanglerBasis entangler) { +/// Enumerate the native entangling gate kinds that `entangler` may emit. +/// Returns an empty list for `EntanglerBasis::None`. +llvm::SmallVector +allowedGatesForEntangler(EntanglerBasis entangler) { switch (entangler) { case EntanglerBasis::None: return {}; @@ -131,6 +155,10 @@ std::set allowedGatesForEntangler(EntanglerBasis entangler) { llvm_unreachable("unknown entangler basis"); } +/// Rebuild `spec.allowedGates` as the union of the gate kinds produced by +/// every resolved emitter, entangler, and (optionally) `Rzz`. Idempotent: +/// clears the set first so calling this on an already-populated spec yields +/// the same result. void populateAllowedGates(NativeProfileSpec& spec) { spec.allowedGates.clear(); for (const auto& emitter : spec.singleQubitEmitters) { @@ -171,7 +199,13 @@ resolveNativeGatesSpec(llvm::StringRef nativeGates) { NativeProfileSpec spec; - // Derive all legal single-qubit emitters from the declared menu. + // Derive all legal single-qubit emitters from the declared menu. Each + // emitter mode requires the *conjunction* of its constituent gate kinds + // to be on the menu -- for example, ZSXX needs X, Sx, and Rz all present, + // because the decomposer unconditionally emits all three. `supportsDirectRx` + // is an independent capability that enables a fast-path for `Rx(theta)` + // inputs when `Rx` is additionally available, but ZSXX itself does not + // depend on `Rx`. if (has(NativeGateKind::U)) { addEmitterIfAbsent(spec.singleQubitEmitters, SingleQubitMode::U3); } diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp index ad1a87b0dc..bcca20b1c4 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp @@ -38,7 +38,6 @@ #include #include -#include namespace mlir::qco { #define GEN_PASS_DEF_NATIVEGATESYNTHESISPASS @@ -102,6 +101,12 @@ bool maybeFuseRun(IRRewriter& rewriter, OneQubitRun& run, }); assert(!spec.singleQubitEmitters.empty() && "expected at least one emitter"); + // Single-qubit fusion intentionally uses only the first emitter: a menu + // that declares multiple single-qubit emitters (e.g. ZSXX + U3) picks the + // canonical lowering via `front()` for fusion so the rewrite is + // deterministic. Picking the cheapest emitter per run would require running + // all emitters and comparing their lengths here; today this is the same + // tradeoff as elsewhere in the pass, so we keep it simple. const auto& emitter = spec.singleQubitEmitters.front(); // Fully native runs: fuse only if the emitter shortens the chain. @@ -147,6 +152,9 @@ UnitaryOpInterface fusibleSingleQubitOp(Operation* op) { return unitary; } +/// Dispatch `op`'s direct (non-matrix) single-qubit lowering to the +/// `decomposeTo*` helper for `emitter.mode`. Returns the output qubit value +/// or a null `Value` if no direct rule applies for this op. Value applyDirectSingleQubitLowering(IRRewriter& rewriter, Operation* op, Value in, const SingleQubitEmitterSpec& emitter) { @@ -169,10 +177,17 @@ Value applyDirectSingleQubitLowering(IRRewriter& rewriter, Operation* op, /// fails if anything remains off-menu). struct NativeGateSynthesisPass : impl::NativeGateSynthesisPassBase { + /// Default-construct the pass with the TableGen-generated option defaults. NativeGateSynthesisPass() = default; + + /// Construct the pass from the TableGen-generated options struct (forwards + /// all option values into the base class). explicit NativeGateSynthesisPass( const NativeGateSynthesisPassOptions& options) : NativeGateSynthesisPassBase(options) {} + + /// Construct the pass from the public `NativeGateSynthesisOptions` struct + /// used by pipeline code that cannot include the TableGen-generated header. explicit NativeGateSynthesisPass(const NativeGateSynthesisOptions& options) { nativeGates = options.nativeGates; scoreWeightTwoQ = options.scoreWeightTwoQ; @@ -180,6 +195,11 @@ struct NativeGateSynthesisPass scoreWeightDepth = options.scoreWeightDepth; } + /// Top-level pass entry point. Validates the score weights and native-gate + /// menu, then drives the staged rewrite pipeline: one-qubit run fusion, + /// two-qubit window consolidation, synthesis sweeps until the single-qubit + /// surface is native, seam cleanup, `rz`-through-`ctrl` folding, and a + /// final fusion pass. Fails the pass on invalid input or non-convergence. void runOnOperation() override { const ScoreWeights weights{.twoQ = scoreWeightTwoQ, .oneQ = scoreWeightOneQ, @@ -347,6 +367,11 @@ struct NativeGateSynthesisPass llvm::SmallVector runs; llvm::DenseMap tailOpToRun; + // Extend the current run only when this op consumes the run's *tail* + // output with no other uses: both the `tailOpToRun` lookup and + // `inQubit.hasOneUse()` are required. Without the single-use check a run + // could fuse gates on a wire that also feeds another path (fan-out), + // which would silently drop the sibling user. getOperation()->walk([&](Operation* op) { auto unitary = fusibleSingleQubitOp(op); if (!unitary) { @@ -379,6 +404,14 @@ struct NativeGateSynthesisPass /// If `rz1` can reach another `rz` through at least one `ctrl` control hop, /// merge angles into `rz1` and erase the partner. + /// + /// `Rz` commutes with a `ctrl` operation acting on the same wire when the + /// wire is a *control* line (controls only diagonalize the computational + /// basis and are invariant under Z-rotations). We walk the def-use chain + /// forward from `rz1`'s output, hopping through `ctrl`s where the wire is + /// used as a control, and fold into the next `rz` we find. The `hops == 0` + /// guard intentionally rejects two adjacent `rz`s with nothing in between + /// -- that case is handled by `fuseOneQubitRuns` above. static bool tryFuseRzForwardThroughCtrls(IRRewriter& rewriter, RZOp rz1) { Value v = rz1.getQubitOut(); RZOp partner; @@ -445,7 +478,7 @@ struct NativeGateSynthesisPass void consolidateTwoQubitBlocks(IRRewriter& rewriter, const NativeProfileSpec& spec, const ScoreWeights& weights) { - std::vector ops; + llvm::SmallVector ops; collectUnitaryOpsInPreOrder(getOperation(), ops); TwoQubitWindowConsolidator consolidator; for (Operation* op : ops) { @@ -470,10 +503,15 @@ struct NativeGateSynthesisPass matrix, plan.emitter); } + /// One synthesis sweep over the whole function: rewrite every remaining + /// off-menu unitary by dispatching to `rewriteSingleQubit` / + /// `rewriteControlled` / `rewriteTwoQubit`. Returns `failure()` as soon as + /// any op cannot be lowered to the native menu. Safe to call repeatedly; + /// `runOnOperation` iterates until convergence. LogicalResult synthesizeRemainingOps(IRRewriter& rewriter, const NativeProfileSpec& spec, const ScoreWeights& weights) { - std::vector ops; + llvm::SmallVector ops; collectUnitaryOpsInPreOrder(getOperation(), ops); for (Operation* op : ops) { @@ -521,6 +559,10 @@ struct NativeGateSynthesisPass return success(); } + /// Lower one off-menu single-qubit `op`: enumerate all valid rewrite + /// candidates for the active native profile, pick the best by `weights`, + /// emit it, and replace `op`. Returns `failure()` (with a diagnostic) if + /// no candidate fits the profile. static LogicalResult rewriteSingleQubit(IRRewriter& rewriter, Operation* op, UnitaryOpInterface unitary, const NativeProfileSpec& spec, @@ -540,6 +582,12 @@ struct NativeGateSynthesisPass return success(); } + /// Lower a single-control, single-target `CtrlOp` to the native profile. + /// Fast-path: already-native `CX`/`CZ` are kept as-is. Otherwise, lift the + /// controlled op to its 4x4 matrix (with SU(4) normalization), run the + /// Weyl-based basis-decomposer search, and emit the best candidate. + /// Returns `failure()` for multi-control ops, non-`X`/`Z` bodies, or when + /// no candidate fits the profile. static LogicalResult rewriteControlled(IRRewriter& rewriter, CtrlOp ctrl, const NativeProfileSpec& spec, const ScoreWeights& weights) { @@ -581,6 +629,11 @@ struct NativeGateSynthesisPass return failure(); } + /// Lower an off-menu generic two-qubit op (`RZZ`, `XXPlusYY`, `XXMinusYY`, + /// or any arbitrary 4x4 unitary). Handles the `Rzz`-native fast path and + /// the `XXPlusMinusYY -> Rzz` specialization first, then falls back to the + /// Weyl-based basis-decomposer search. Returns `failure()` (with a + /// diagnostic) when no candidate fits the profile. static LogicalResult rewriteTwoQubit(IRRewriter& rewriter, Operation* op, UnitaryOpInterface unitary, const NativeProfileSpec& spec, diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp index 8408617e5f..1cd9a8ccad 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp @@ -26,6 +26,10 @@ namespace mlir::qco::native_synth { namespace { +/// Check whether a two-qubit op `op` is already expressible by the resolved +/// native menu: a single-control `CX`/`CZ` consistent with the active +/// entangler, or `Rzz` when `spec.allowRzz` is set. Multi-control and other +/// two-qubit ops are considered non-native. bool isNativeTwoQubitOp(Operation* op, const NativeProfileSpec& spec) { if (auto ctrl = dyn_cast(op)) { if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { @@ -43,6 +47,11 @@ bool isNativeTwoQubitOp(Operation* op, const NativeProfileSpec& spec) { return spec.allowRzz && isa(op); } +/// Decide whether replacing a consolidated window with the candidate +/// described by `best` is worthwhile. Always replace a window that contains +/// any non-native op (we have to lower them anyway); otherwise only replace +/// when the candidate has strictly fewer two-qubit gates, or the same number +/// with strictly fewer one-qubit gates. bool shouldApplyBlockReplacement(const TwoQubitBlock& block, const CandidateMetrics& best) { if (block.anyNonNative) { @@ -56,6 +65,10 @@ bool shouldApplyBlockReplacement(const TwoQubitBlock& block, } // namespace +/// Emit the chosen synthesis sequence `best` at the location of the window's +/// first op, rewire the block's trailing SSA values (`wireA`, `wireB`) to +/// the newly emitted outputs, and erase the replaced ops in reverse order +/// so def-use edges are cleared before their defining ops disappear. static void materializeSingleTwoQubitBlock( IRRewriter& rewriter, const TwoQubitBlock& block, const SynthesisCandidate& best) { @@ -87,7 +100,7 @@ static void materializeSingleTwoQubitBlock( } void collectUnitaryOpsInPreOrder(Operation* root, - std::vector& ops) { + llvm::SmallVectorImpl& ops) { root->walk([&](Operation* op) { if (isa(op)) { ops.push_back(op); @@ -111,8 +124,33 @@ void TwoQubitWindowConsolidator::closeBlockOnWire(Value v) { } } +/// State-machine step for one IR op, invoked in walk order over the module. +/// +/// The consolidator tracks a set of *maximal two-qubit windows* -- contiguous +/// slices of the dataflow where at most two qubit wires interact -- so a +/// later pass can re-synthesize each window as a single 4x4 unitary. For +/// each op we update two pieces of state: +/// +/// * `blocks` -- append-only list of `TwoQubitBlock`s. Closed +/// blocks are kept so `materialize()` can rewrite +/// them later. +/// * `wireToBlock` -- maps each *currently-open* SSA qubit Value to the +/// index of the block that still owns it. +/// Re-keyed whenever an op produces a new output +/// Value on a tracked wire. +/// +/// Because `process` is called in pre-order over the IR, when we see an op +/// its input Values have already been processed (or were function +/// arguments). A block stays open for a wire as long as every op consuming +/// that wire is either (a) a single-qubit op absorbable into the block, or +/// (b) another two-qubit op on the *same* pair of wires. Any other +/// consumer -- a barrier, a control, a different pair of wires, a +/// multi-use fork -- closes the block. void TwoQubitWindowConsolidator::process(Operation* op, const NativeProfileSpec& spec) { + // Skip ops nested inside a `CtrlOp`'s body: those are handled as part of + // their enclosing controlled op (seen at the parent level), not as + // independent two-qubit gates. if (isa_and_present(op->getParentOp())) { return; } @@ -120,6 +158,9 @@ void TwoQubitWindowConsolidator::process(Operation* op, if (!unitary) { return; } + // Barriers and stand-alone global-phase ops are not unitaries we can + // absorb; they act as synchronization points that force any block + // touching their operand wires to close. if (isa(op)) { for (Value v : op->getOperands()) { closeBlockOnWire(v); @@ -128,6 +169,9 @@ void TwoQubitWindowConsolidator::process(Operation* op, } if (unitary.isTwoQubit()) { + // A two-qubit op for which we cannot build a 4x4 matrix (e.g. a + // multi-control `CtrlOp` with more than one control) is opaque to the + // window model; close any blocks on its inputs and bail out. Eigen::Matrix4cd opMatrix; if (!getBlockTwoQubitMatrix(op, opMatrix)) { closeBlockOnWire(unitary.getInputQubit(0)); @@ -140,12 +184,26 @@ void TwoQubitWindowConsolidator::process(Operation* op, auto it1 = wireToBlock.find(v1); const bool tracked0 = it0 != wireToBlock.end(); const bool tracked1 = it1 != wireToBlock.end(); + // "Same block" means the two input wires are currently the (wireA, + // wireB) pair of one existing block -- i.e. this op operates on the + // same pair as the previous two-qubit op in that block. Otherwise the + // op either extends into a *new* pair (merging two blocks, which we + // don't support) or starts a fresh block. const bool sameBlock = tracked0 && tracked1 && it0->second == it1->second; const bool singleUse = v0.hasOneUse() && v1.hasOneUse(); + // ---- Case A: extend the existing block --------------------------- + // Both inputs belong to the same open block and nothing else uses + // them. Absorb the new gate into the block's accumulated unitary and + // advance the tracked wires to this op's outputs. if (sameBlock && singleUse) { const size_t idx = it0->second; auto& block = blocks[idx]; + // `block.accum` is the composite 4x4 unitary of the gates absorbed so + // far, with qubit 0 == `wireA` and qubit 1 == `wireB`. The incoming + // op's `opMatrix` is in the (v0, v1) operand order, so we reorder it + // to the block's (wireA, wireB) convention before left-multiplying + // (newest gate on the left, matching matrix-times-column-state order). llvm::SmallVector ids; if (v0 == block.wireA && v1 == block.wireB) { ids = {0, 1}; @@ -180,6 +238,13 @@ void TwoQubitWindowConsolidator::process(Operation* op, return; } + // ---- Case B: close overlapping blocks, start a new one ---------- + // The inputs do not form a clean pair on an existing block (fan-out, + // straddling two different blocks, or only one wire tracked). Closing + // the affected blocks prevents wire-to-block aliasing from becoming + // inconsistent -- note the second `if` guards against double-closing + // the same block when both inputs happened to live in it but `sameBlock + // && singleUse` was false (e.g. only fan-out violated). if (tracked0) { closeBlock(it0->second); } @@ -200,6 +265,10 @@ void TwoQubitWindowConsolidator::process(Operation* op, return; } + // ---- Case C: single-qubit op on a tracked wire ------------------- + // Absorbable into the block's accumulated 4x4 by lifting the 2x2 to the + // appropriate tensor slot. If the wire is not tracked, the op simply + // does not interact with any open block and is left for other passes. if (unitary.isSingleQubit()) { const Value v = unitary.getInputQubit(0); auto it = wireToBlock.find(v); @@ -209,6 +278,10 @@ void TwoQubitWindowConsolidator::process(Operation* op, const size_t idx = it->second; auto& block = blocks[idx]; Eigen::Matrix2cd m; + // `!v.hasOneUse()` is the fan-out guard: if any other op also consumes + // this wire, we cannot soundly absorb this single-qubit gate into the + // block (the sibling user would see the pre-gate state). Close the + // block and let the outer pass rewrite the op individually. if (!unitary.getUnitaryMatrix2x2(m) || !v.hasOneUse()) { closeBlock(idx); return; @@ -233,6 +306,9 @@ void TwoQubitWindowConsolidator::process(Operation* op, return; } + // ---- Case D: any other unitary (e.g. >2-qubit ops) --------------- + // We can neither absorb nor continue a window through an op of unknown + // arity, so close every block that touches one of its operand wires. for (Value v : op->getOperands()) { closeBlockOnWire(v); } diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Policy.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Policy.cpp index 878e011369..53de538532 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Policy.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Policy.cpp @@ -80,6 +80,11 @@ bool allowsSingleQubitOp(UnitaryOpInterface op, const NativeProfileSpec& spec) { CandidateMetrics computeGateSequenceMetrics(const decomposition::QubitGateSequence& seq) { CandidateMetrics metrics; + // Per-qubit depth counters used as a mini scheduler: single-qubit gates + // advance only their own wire's counter, while two-qubit gates act as a + // *sync barrier* and advance both wires to `1 + max(...)`. This mirrors a + // simple ASAP scheduling model where entangling gates force alignment of + // the two wires they touch. llvm::SmallVector qubitDepths(2, 0); for (const auto& gate : seq.gates) { if (gate.qubitId.size() == 2) { diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp index a1ce820c8c..ab2d905d55 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp @@ -29,49 +29,64 @@ namespace { constexpr double PI = std::numbers::pi; constexpr double HALF_PI = PI / 2.0; -/// Small convenience wrapper to avoid passing rewriter/loc everywhere. +/// Small convenience wrapper to avoid passing rewriter/loc everywhere. Each +/// method creates the corresponding QCO op threaded through `q` and returns +/// its new output qubit value. struct SingleQubitEmitter { IRRewriter* rewriter; Location loc; + /// Create an `arith.constant` `f64` of value `v` at `loc`. [[nodiscard]] Value constF(double v) const { return createF64Const(*rewriter, loc, v); } + /// Emit `rx(theta)` with a compile-time scalar angle. [[nodiscard]] Value rx(Value q, double theta) const { return RXOp::create(*rewriter, loc, q, constF(theta)).getOutputQubit(0); } + /// Emit `rx(theta)` with a runtime `f64` angle value. [[nodiscard]] Value rx(Value q, Value theta) const { return RXOp::create(*rewriter, loc, q, theta).getOutputQubit(0); } + /// Emit `ry(theta)` with a compile-time scalar angle. [[nodiscard]] Value ry(Value q, double theta) const { return RYOp::create(*rewriter, loc, q, constF(theta)).getOutputQubit(0); } + /// Emit `ry(theta)` with a runtime `f64` angle value. [[nodiscard]] Value ry(Value q, Value theta) const { return RYOp::create(*rewriter, loc, q, theta).getOutputQubit(0); } + /// Emit `rz(theta)` with a compile-time scalar angle. [[nodiscard]] Value rz(Value q, double theta) const { return RZOp::create(*rewriter, loc, q, constF(theta)).getOutputQubit(0); } + /// Emit `rz(theta)` with a runtime `f64` angle value. [[nodiscard]] Value rz(Value q, Value theta) const { return RZOp::create(*rewriter, loc, q, theta).getOutputQubit(0); } + /// Emit `sx` (square-root-of-X). [[nodiscard]] Value sx(Value q) const { return SXOp::create(*rewriter, loc, q).getOutputQubit(0); } + /// Emit a Pauli `x`. [[nodiscard]] Value x(Value q) const { return XOp::create(*rewriter, loc, q).getOutputQubit(0); } + /// Emit `r(theta, phi)` with compile-time scalar angles. [[nodiscard]] Value r(Value q, double theta, double phi) const { return ROp::create(*rewriter, loc, q, constF(theta), constF(phi)) .getOutputQubit(0); } + /// Emit `r(theta, phi)` with runtime `f64` angle values. [[nodiscard]] Value r(Value q, Value theta, Value phi) const { return ROp::create(*rewriter, loc, q, theta, phi).getOutputQubit(0); } + /// Emit `u(theta, phi, lambda)` with runtime `f64` angle values. [[nodiscard]] Value u(Value q, Value theta, Value phi, Value lambda) const { return UOp::create(*rewriter, loc, q, theta, phi, lambda).getOutputQubit(0); } + /// Emit `u(theta, phi, lambda)` with compile-time scalar angles. [[nodiscard]] Value u(Value q, double theta, double phi, double lambda) const { return u(q, constF(theta), constF(phi), constF(lambda)); @@ -105,6 +120,10 @@ Value emitEulerSequenceZsxx(SingleQubitEmitter e, Value q, return q; } +/// Materialize an `EulerBasis::XYX` decomposition into `R(theta, phi)` ops +/// for the `R` emitter: `Rx(theta)` becomes `R(theta, 0)`, `Ry(theta)` +/// becomes `R(theta, pi/2)`, Pauli `X`/`Y` become `R(pi, *)`, `I` is a +/// no-op. Returns null on any unsupported abstract gate kind. Value emitEulerSequenceR(SingleQubitEmitter e, Value q, const decomposition::QubitGateSequence& seq) { for (const auto& gate : seq.gates) { @@ -136,6 +155,12 @@ Value emitEulerSequenceR(SingleQubitEmitter e, Value q, return q; } +/// Materialize an Euler decomposition in the two rotation axes named by +/// `axis` (e.g. `{Rx, Rz}`). Every gate kind that falls outside the two +/// chosen axes (or has the wrong parameter count) is rejected by returning +/// a null `Value`; the matrix-based fallback is expected to pick a +/// different basis in that case. Pauli gates are lowered to the +/// corresponding `R*(pi)` when their axis is available. Value emitEulerSequenceAxisPair(SingleQubitEmitter e, Value q, AxisPair axis, const decomposition::QubitGateSequence& seq) { for (const auto& gate : seq.gates) { @@ -185,6 +210,10 @@ Value emitEulerSequenceAxisPair(SingleQubitEmitter e, Value q, AxisPair axis, return q; } +/// Decompose `matrix` numerically into a gate sequence in `basis` with +/// zero-rotations pruned (`simplify=true`). Pure forwarder around +/// `EulerDecomposition::generateCircuit` kept as a one-liner to match the +/// matrix-based fallback call sites in `decomposeTo*`. decomposition::QubitGateSequence runEuler(decomposition::EulerBasis basis, const Eigen::Matrix2cd& matrix) { return decomposition::EulerDecomposition::generateCircuit( @@ -224,10 +253,10 @@ Value decomposeToZSXX(IRRewriter& rewriter, Operation* op, Value inQubit, } Value decomposeToU3(IRRewriter& rewriter, Operation* op, Value inQubit) { - SingleQubitEmitter e{.rewriter = &rewriter, .loc = op->getLoc()}; if (isa(op)) { - return e.u(inQubit, 0.0, 0.0, 0.0); + return inQubit; } + SingleQubitEmitter e{.rewriter = &rewriter, .loc = op->getLoc()}; if (auto u = dyn_cast(op)) { return u.getOutputQubit(0); } @@ -296,6 +325,13 @@ Value emitSynthesizedSingleQubitFromMatrix( case SingleQubitMode::U3: { using namespace std::complex_literals; + // Project `matrix` into SU(2) before running the Euler decomposition. + // For a 2x2 unitary, det(U) sits on the unit circle, so dividing by the + // square root of det fixes det == 1. We use `arg(det) / 2` (not + // `/ 4` as in the 4x4 case) because `sqrt(det) = exp(i * arg(det) / 2)`. + // The removed global phase is re-emitted via `emitGPhaseIfNonTrivial` + // so the final sequence equals the original unitary, not just SU(2)-up + // to global phase. Eigen::Matrix2cd m = matrix; const auto det = m(0, 0) * m(1, 1) - m(0, 1) * m(1, 0); const double phase = std::arg(det) / 2.0; diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp index 064ee4cbaf..457427023c 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp @@ -97,6 +97,10 @@ bool menuAllows(const decomposition::Gate& gate, return false; } +/// Can `emitter` lower the single-qubit `op` directly (without the matrix +/// fallback)? Dispatches to the mode-specific `canDirectlyDecomposeTo*` +/// predicate; these predicates encode which abstract gate kinds each +/// emitter understands as-is. bool emitterHasDirectLowering(Operation* op, const SingleQubitEmitterSpec& emitter) { switch (emitter.mode) { @@ -176,14 +180,31 @@ collectSingleQubitCandidates(UnitaryOpInterface unitary, namespace { +/// Try every `numBasisUses` in `{0, 1, 2, 3}` for the `(entangler, emitter, +/// basis)` triple, running the Weyl-based basis decomposer for each. Any +/// resulting gate sequence that both matches `targetMatrix` up to global +/// phase AND stays inside the native menu is appended to `candidates` (with +/// a freshly-incremented `enumerationIndex` to keep scoring deterministic). void tryAddTwoQubitBasisCandidatesForEmitterBasis( llvm::SmallVector, 0>& candidates, unsigned& enumerationIndex, const Eigen::Matrix4cd& targetMatrix, const NativeProfileSpec& spec, EntanglerBasis entangler, const SingleQubitEmitterSpec& emitter, decomposition::EulerBasis basis) { + // An arbitrary 2-qubit unitary can always be realized using at most three + // copies of any fixed (non-diagonal) entangler plus local gates -- this is + // a consequence of the KAK/Weyl decomposition. Trying all four candidate + // counts (0..3) and scoring them with the gate-sequence metric lets the + // outer pass pick the cheapest realization for the particular target + // unitary (e.g. local unitaries collapse to 0 entanglers, SWAP uses 3). for (std::uint8_t numBasisUses = 0; numBasisUses <= 3; ++numBasisUses) { auto seq = decomposeTwoQubitFromMatrix(targetMatrix, entangler, basis, numBasisUses); + // Two independent checks: `isEquivalentUpToGlobalPhase` verifies the + // numerical decomposition actually reproduces the target; `fitsMenu` + // verifies every emitted gate kind is in the backend native set. Both + // are required because the decomposer can legitimately produce an + // accurate sequence that still contains non-native gates (e.g. when the + // requested emitter supports fewer axes than the target unitary needs). if (!seq || !isEquivalentUpToGlobalPhase(seq->getUnitaryMatrix(), targetMatrix) || !gateSequenceFitsMenu(*seq, spec)) { @@ -268,6 +289,9 @@ LogicalResult rewriteXXPlusMinusYYViaRxxRyy(IRRewriter& rewriter, return RZOp::create(rewriter, loc, sx.getOutputQubit(0), constF(HALF_PI)) .getOutputQubit(0); }; + // Realize `Rxx(theta)` as `(H ⊗ H) * Rzz(theta) * (H ⊗ H)`: Hadamard + // conjugation maps the Z axis to X on each qubit, and the tensor-product + // identity `(H ⊗ H) * ZZ * (H ⊗ H) == XX` lifts that to the entangler. const auto emitRxxViaRzz = [&](Value q0, Value q1, Value theta) -> std::pair { q0 = emitH(q0); @@ -277,6 +301,10 @@ LogicalResult rewriteXXPlusMinusYYViaRxxRyy(IRRewriter& rewriter, q1 = rzz.getOutputQubit(1); return {emitH(q0), emitH(q1)}; }; + // Realize `Ryy(theta)` as `(Rx(-pi/2) ⊗ Rx(-pi/2)) * Rzz(theta) * + // (Rx(pi/2) ⊗ Rx(pi/2))`: Rx(pi/2) maps Z to Y on each qubit, so the + // conjugation transports `ZZ` to `YY` just like the Hadamard sandwich + // above maps it to `XX`. const auto emitRyyViaRzz = [&](Value q0, Value q1, Value theta) -> std::pair { auto rx0 = RXOp::create(rewriter, loc, q0, constF(HALF_PI)); @@ -290,6 +318,15 @@ LogicalResult rewriteXXPlusMinusYYViaRxxRyy(IRRewriter& rewriter, return {rxb0.getOutputQubit(0), rxb1.getOutputQubit(0)}; }; + // `XXPlusYY(theta, beta)` and `XXMinusYY(theta, beta)` both act as + // Rz(-beta) on q0 -> entangling core -> Rz(+beta) on q0, + // but differ in the entangling core: + // XXPlusYY: exp(-i * theta/4 * (XX + YY)) == Ryy(theta/2) * Rxx(theta/2) + // XXMinusYY: exp(-i * theta/4 * (XX - YY)) == Rxx(theta/2) * Ryy(-theta/2) + // (XX and YY commute, so the two multiplication orders produce identical + // unitaries; the distinct order and sign below are what makes `XXMinusYY` + // the "minus" variant and must be preserved even though an order flip + // alone would also compile.) if (auto xxPlus = dyn_cast(op)) { Value q0 = xxPlus.getInputQubit(0); Value q1 = xxPlus.getInputQubit(1); diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp index c4963cebb6..387977954a 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp @@ -57,6 +57,10 @@ bool isEquivalentUpToGlobalPhase(const Eigen::Matrix4cd& lhs, void normalizeToSU4(Eigen::Matrix4cd& matrix) { using namespace std::complex_literals; const std::complex det = matrix.determinant(); + // Project `matrix` into SU(4) by dividing out the fourth root of its + // determinant (det(SU(N)) == 1). `|det|^{-1/4}` fixes the magnitude and + // `exp(-i * arg(det) / 4)` removes the global phase so the Weyl + // decomposition downstream operates on a special-unitary input. if (std::abs(det) > 1e-16) { matrix *= std::pow(std::abs(det), -0.25) * std::exp(1i * (-std::arg(det) / 4.0)); @@ -102,15 +106,30 @@ bool getBlockTwoQubitMatrix(Operation* op, Eigen::Matrix4cd& matrix) { namespace { -/// Emit a single-qubit gate from a decomposition gate, threading `target`. -/// Returns `failure()` if the gate kind/parameter count is unsupported. -LogicalResult emitSingleQubitStep(IRRewriter& rewriter, Location loc, - const decomposition::Gate& gate, - Value& target) { +/// Emit a single-qubit gate from a decomposition gate, threading `target` and +/// recording the inserted op (if any) in `insertedOps` so the caller can roll +/// back on failure. Returns `failure()` if the gate kind/parameter count is +/// unsupported. +LogicalResult +emitSingleQubitStep(IRRewriter& rewriter, Location loc, + const decomposition::Gate& gate, Value& target, + llvm::SmallVectorImpl& insertedOps) { const auto emitConst = [&](double v) { - return createF64Const(rewriter, loc, v); + auto constant = arith::ConstantFloatOp::create( + rewriter, loc, rewriter.getF64Type(), llvm::APFloat(v)); + insertedOps.push_back(constant); + return constant.getResult(); + }; + const auto record = [&](auto op) { + insertedOps.push_back(op.getOperation()); + return op; }; switch (gate.type) { + case decomposition::GateKind::I: + // Identity is a no-op; leave the threaded `target` unchanged. Euler + // decomposers do not emit explicit identity steps today, so this case is + // kept defensively to mirror the handling in `SingleQubit.cpp`. + return success(); case decomposition::GateKind::U: if (gate.parameter.size() != 3) { return failure(); @@ -118,35 +137,39 @@ LogicalResult emitSingleQubitStep(IRRewriter& rewriter, Location loc, // EulerDecomposition emits `U` with parameters = {lambda, phi, theta} // whereas `UOp` takes (theta, phi, lambda); reorder accordingly. target = - UOp::create(rewriter, loc, target, emitConst(gate.parameter[2]), - emitConst(gate.parameter[1]), emitConst(gate.parameter[0])) + record(UOp::create(rewriter, loc, target, emitConst(gate.parameter[2]), + emitConst(gate.parameter[1]), + emitConst(gate.parameter[0]))) .getOutputQubit(0); return success(); case decomposition::GateKind::SX: - target = SXOp::create(rewriter, loc, target).getOutputQubit(0); + target = record(SXOp::create(rewriter, loc, target)).getOutputQubit(0); return success(); case decomposition::GateKind::X: - target = XOp::create(rewriter, loc, target).getOutputQubit(0); + target = record(XOp::create(rewriter, loc, target)).getOutputQubit(0); return success(); case decomposition::GateKind::RX: if (gate.parameter.size() != 1) { return failure(); } - target = RXOp::create(rewriter, loc, target, emitConst(gate.parameter[0])) + target = record(RXOp::create(rewriter, loc, target, + emitConst(gate.parameter[0]))) .getOutputQubit(0); return success(); case decomposition::GateKind::RY: if (gate.parameter.size() != 1) { return failure(); } - target = RYOp::create(rewriter, loc, target, emitConst(gate.parameter[0])) + target = record(RYOp::create(rewriter, loc, target, + emitConst(gate.parameter[0]))) .getOutputQubit(0); return success(); case decomposition::GateKind::RZ: if (gate.parameter.size() != 1) { return failure(); } - target = RZOp::create(rewriter, loc, target, emitConst(gate.parameter[0])) + target = record(RZOp::create(rewriter, loc, target, + emitConst(gate.parameter[0]))) .getOutputQubit(0); return success(); default: @@ -154,6 +177,16 @@ LogicalResult emitSingleQubitStep(IRRewriter& rewriter, Location loc, } } +/// Erase all ops tracked in `insertedOps` in reverse insertion order. Clears +/// the vector on return. +void rollbackInsertedOps(IRRewriter& rewriter, + llvm::SmallVectorImpl& insertedOps) { + for (Operation* op : llvm::reverse(insertedOps)) { + rewriter.eraseOp(op); + } + insertedOps.clear(); +} + } // namespace LogicalResult @@ -161,10 +194,13 @@ emitTwoQubitGateSequenceAtLoc(IRRewriter& rewriter, Location loc, Value qubit0, Value qubit1, const decomposition::TwoQubitGateSequence& seq, Value& outQubit0, Value& outQubit1) { + llvm::SmallVector insertedOps; for (const auto& gate : seq.gates) { if (gate.qubitId.size() == 1) { Value& target = (gate.qubitId[0] == 0) ? qubit0 : qubit1; - if (failed(emitSingleQubitStep(rewriter, loc, gate, target))) { + if (failed( + emitSingleQubitStep(rewriter, loc, gate, target, insertedOps))) { + rollbackInsertedOps(rewriter, insertedOps); return failure(); } continue; @@ -174,6 +210,7 @@ emitTwoQubitGateSequenceAtLoc(IRRewriter& rewriter, Location loc, Value qubit0, gate.qubitId.size() == 2 && (gate.type == decomposition::GateKind::X || gate.type == decomposition::GateKind::Z); if (!isCxOrCz) { + rollbackInsertedOps(rewriter, insertedOps); return failure(); } @@ -191,6 +228,8 @@ emitTwoQubitGateSequenceAtLoc(IRRewriter& rewriter, Location loc, Value qubit0, } return {ZOp::create(rewriter, loc, targetArgs[0]).getOutputQubit(0)}; }); + // Erasing the `CtrlOp` also removes its nested body op. + insertedOps.push_back(ctrlOp.getOperation()); const Value controlOut = ctrlOp.getOutputControl(0); const Value targetOut = ctrlOp.getOutputTarget(0); Value next0 = qubit0; diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 84f82c52ed..d3d565ea86 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -45,7 +45,6 @@ #include #include -#include #include #include #include @@ -818,17 +817,7 @@ computeStaticTwoQubitUnitary(mlir::ModuleOp module) { return unitary; } -/// Check matrix equality up to a unit-modulus global phase. -bool isEquivalentUpToGlobalPhase(const Eigen::Matrix4cd& lhs, - const Eigen::Matrix4cd& rhs, - const double atol = 1e-10) { - const auto overlap = (rhs.adjoint() * lhs).trace(); - if (std::abs(overlap) <= atol) { - return false; - } - const auto factor = overlap / std::abs(overlap); - return lhs.isApprox(factor * rhs, atol); -} +using mqt::test::isEquivalentUpToGlobalPhase; } // namespace diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/decomposition_test_utils.h b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/decomposition_test_utils.h index ccdf8170a3..9273d2f0f2 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/decomposition_test_utils.h +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/decomposition_test_utils.h @@ -10,43 +10,26 @@ #pragma once +#include "TestCaseUtils.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" #include #include #include -#include #include #include -/// Standard U3 matrix (same convention as QCO ``u`` angles). -[[nodiscard]] inline Eigen::Matrix2cd u3Matrix(double theta, double phi, - double lambda) { - using Complex = std::complex; - const Complex i(0.0, 1.0); - const double c = std::cos(theta / 2.0); - const double s = std::sin(theta / 2.0); - const Complex eiphi = std::exp(i * phi); - const Complex eilambda = std::exp(i * lambda); - const Complex eiphilambda = std::exp(i * (phi + lambda)); +namespace mlir::qco::decomposition_test { - Eigen::Matrix2cd mat; - mat << c, -eilambda * s, eiphi * s, eiphilambda * c; - return mat; -} +using mqt::test::isEquivalentUpToGlobalPhase; -/// Compare up to a single global phase factor. -template -[[nodiscard]] bool isEquivalentUpToGlobalPhase(const Matrix& lhs, - const Matrix& rhs, - double atol = 1e-10) { - const auto overlap = (rhs.adjoint() * lhs).trace(); - if (std::abs(overlap) <= atol) { - return false; - } - const auto factor = overlap / std::abs(overlap); - return lhs.isApprox(factor * rhs, atol); +/// Standard `U3(theta, phi, lambda)` matrix. Thin wrapper over the library +/// `uMatrix` so every test uses the same implementation. +[[nodiscard]] inline Eigen::Matrix2cd u3Matrix(double theta, double phi, + double lambda) { + return decomposition::uMatrix(theta, phi, lambda); } template @@ -59,6 +42,8 @@ template Eigen::HouseholderQR qr{}; qr.compute(randomMatrix); const MatrixType unitaryMatrix = qr.householderQ(); - assert(mlir::qco::helpers::isUnitaryMatrix(unitaryMatrix)); + assert(helpers::isUnitaryMatrix(unitaryMatrix)); return unitaryMatrix; } + +} // namespace mlir::qco::decomposition_test diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_basis_decomposer.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_basis_decomposer.cpp index 9c3b8157d9..563721eb44 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_basis_decomposer.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_basis_decomposer.cpp @@ -29,6 +29,7 @@ using namespace mlir::qco; using namespace mlir::qco::decomposition; +using namespace mlir::qco::decomposition_test; class BasisDecomposerTest : public testing::TestWithParam { diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp index 920d7ff0e8..15083d5033 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp @@ -29,17 +29,7 @@ std::complex phasedAmplitude(const double magnitude, } Eigen::Matrix2cd u3Matrix(double theta, double phi, double lambda) { - using Complex = std::complex; - const Complex i(0.0, 1.0); - const double c = std::cos(theta / 2.0); - const double s = std::sin(theta / 2.0); - const Complex eiphi = std::exp(i * phi); - const Complex eilambda = std::exp(i * lambda); - const Complex eiphilambda = std::exp(i * (phi + lambda)); - - Eigen::Matrix2cd mat; - mat << c, -eilambda * s, eiphi * s, eiphilambda * c; - return mat; + return decomposition::uMatrix(theta, phi, lambda); } bool isUnitary(const Eigen::Matrix2cd& m, const double atol) { diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.h b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.h index 975c91ab51..7fcc04f11e 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.h +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.h @@ -10,6 +10,7 @@ #pragma once +#include "TestCaseUtils.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include @@ -26,16 +27,7 @@ namespace mlir::qco::native_synth_test { -template -bool isEquivalentUpToGlobalPhase(const Matrix& lhs, const Matrix& rhs, - double atol = 1e-10) { - const auto overlap = (rhs.adjoint() * lhs).trace(); - if (std::abs(overlap) <= atol) { - return false; - } - const auto factor = overlap / std::abs(overlap); - return lhs.isApprox(factor * rhs, atol); -} +using mqt::test::isEquivalentUpToGlobalPhase; [[nodiscard]] std::complex phasedAmplitude(double magnitude, double phase); diff --git a/mlir/unittests/TestCaseUtils.h b/mlir/unittests/TestCaseUtils.h index 570c86c87f..7603fda8be 100644 --- a/mlir/unittests/TestCaseUtils.h +++ b/mlir/unittests/TestCaseUtils.h @@ -18,6 +18,8 @@ #include #include +#include +#include // NOLINT(misc-include-cleaner) #include #include #include @@ -26,6 +28,26 @@ namespace mqt::test { +/** + * Check whether two unitary matrices are equal up to a single unit-modulus + * global phase factor. + * + * The comparison is symmetric and numerically stable in the sense that a near + * zero overlap (``|trace(rhs^H * lhs)| <= atol``) is treated as "not + * equivalent" to avoid division by a tiny number. + */ +template +[[nodiscard]] bool isEquivalentUpToGlobalPhase(const Matrix& lhs, + const Matrix& rhs, + double atol = 1e-10) { + const auto overlap = (rhs.adjoint() * lhs).trace(); + if (std::abs(overlap) <= atol) { + return false; + } + const auto factor = overlap / std::abs(overlap); + return lhs.isApprox(factor * rhs, atol); +} + template struct NamedBuilder { const char* name = nullptr; void (*fn)(BuilderT&) = nullptr; From 1ade32bc3589986e496f649aa5205b932192082b Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 23 Apr 2026 15:45:46 +0200 Subject: [PATCH 008/122] =?UTF-8?q?=E2=9C=85=20Refactor=20native=20synthes?= =?UTF-8?q?is=20tests.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Transforms/NativeSynthesis/CMakeLists.txt | 2 +- .../native_synthesis_pass_test_fixture.h | 91 +-- ...est_native_synthesis_pass_custom_menus.cpp | 56 +- .../test_native_synthesis_pass_fusion.cpp | 544 +++++---------- ...test_native_synthesis_pass_multi_qubit.cpp | 178 +---- .../test_native_synthesis_pass_profiles.cpp | 641 ++++++------------ .../test_native_synthesis_pass_scoring.cpp | 71 +- mlir/unittests/programs/qc_programs.cpp | 519 ++++++++++++++ mlir/unittests/programs/qc_programs.h | 169 +++++ 9 files changed, 1134 insertions(+), 1137 deletions(-) diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt index 72185daae6..1e04b0f07b 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt @@ -22,7 +22,7 @@ target_link_libraries( ${target_name} PRIVATE MLIRParser GTest::gtest_main - MLIRQCProgramBuilder + MLIRQCPrograms MLIRQCOProgramBuilder MLIRQCOUtils MLIRQCToQCO diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_pass_test_fixture.h b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_pass_test_fixture.h index e7b4c48056..f0a12d4ddc 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_pass_test_fixture.h +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_pass_test_fixture.h @@ -18,11 +18,13 @@ #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "native_synthesis_test_helpers.h" +#include "qc_programs.h" #include #include #include #include +#include #include #include #include @@ -44,7 +46,8 @@ class NativeSynthesisPassTest : public testing::Test { void SetUp() override { mlir::DialectRegistry registry; registry.insert(); + mlir::arith::ArithDialect, mlir::func::FuncDialect, + mlir::memref::MemRefDialect>(); context = std::make_unique(); context->appendDialectRegistry(registry); context->loadAllAvailableDialects(); @@ -182,94 +185,20 @@ class NativeSynthesisPassTest : public testing::Test { [[nodiscard]] mlir::OwningOpRef buildBroadOneQCanonicalizationCircuit() const { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - - builder.id(q0); - builder.x(q0); - builder.y(q1); - builder.z(q0); - builder.h(q1); - builder.s(q0); - builder.sdg(q1); - builder.t(q0); - builder.tdg(q1); - builder.sx(q0); - builder.sxdg(q1); - builder.rx(0.13, q0); - builder.ry(-0.47, q1); - builder.rz(0.29, q0); - builder.p(-0.38, q1); - builder.r(0.61, -0.22, q0); - builder.cz(q0, q1); - - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::nativeSynthBroadOneQCanonicalization); } [[nodiscard]] mlir::OwningOpRef buildZeroAngleCanonicalizationCircuit() const { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - - builder.rx(0.0, q0); - builder.ry(0.0, q1); - builder.rz(0.0, q0); - builder.p(0.0, q1); - builder.r(0.0, 0.0, q0); - builder.cz(q0, q1); - - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::nativeSynthZeroAngleCanonicalization); } [[nodiscard]] mlir::OwningOpRef buildIbmFractionalAllGateFamiliesCircuit() const { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - - builder.id(q0); - builder.x(q0); - builder.y(q1); - builder.z(q0); - builder.h(q1); - builder.s(q0); - builder.sdg(q1); - builder.t(q0); - builder.tdg(q1); - builder.sx(q0); - builder.sxdg(q1); - builder.rx(0.13, q0); - builder.ry(-0.47, q1); - builder.rz(0.29, q0); - builder.p(-0.38, q1); - builder.r(0.61, -0.22, q0); - - builder.cx(q0, q1); - builder.cz(q1, q0); - - builder.swap(q0, q1); - builder.iswap(q0, q1); - builder.dcx(q0, q1); - builder.ecr(q0, q1); - builder.rxx(0.17, q0, q1); - builder.ryy(-0.21, q0, q1); - builder.rzx(0.41, q0, q1); - builder.rzz(-0.33, q0, q1); - builder.xx_plus_yy(0.52, -0.14, q0, q1); - builder.xx_minus_yy(-0.37, 0.26, q0, q1); - - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::nativeSynthIbmFractionalAllGateFamilies); } static void runNativeSynthesis(mlir::OwningOpRef& moduleOp, diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_custom_menus.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_custom_menus.cpp index 6939459da7..917a16add2 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_custom_menus.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_custom_menus.cpp @@ -444,21 +444,8 @@ TEST_F(NativeSynthesisPassTest, RandomizedCustomMenusAndCircuitsAreEquivalent) { TEST_F(NativeSynthesisPassTest, LargeCircuitEquivalentAndNativeGatesIbmFractional) { auto buildStressCircuit = [&](MLIRContext* ctx) { - mlir::qc::QCProgramBuilder builder(ctx); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - builder.h(q0); - builder.sxdg(q0); - builder.ry(-0.22, q1); - builder.swap(q0, q1); - builder.rxx(0.53, q0, q1); - builder.ecr(q0, q1); - builder.p(0.31, q0); - builder.rzz(-0.44, q0, q1); - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build( + ctx, mlir::qc::nativeSynthCustomMenusIbmFractionalTwoQStress); }; expectEquivalentAndNativeAfterSynthesis( [&] { return buildStressCircuit(context.get()); }, "x,sx,rz,rx,rzz,cz", @@ -476,29 +463,18 @@ TEST_F(NativeSynthesisPassTest, TEST_F(NativeSynthesisPassTest, XXPlusMinusYYEquivalentAndNativeIbmFractional) { constexpr const char* kIbmFrac = "x,sx,rz,rx,rzz,cz"; - const auto assertEquivalent = [&](auto buildBody) { - expectEquivalentAndNativeAfterSynthesis( - [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - buildBody(builder, q0, q1); - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); - }, - kIbmFrac, &NativeSynthesisPassTest::onlyIbmFractionalOps, - computeTwoQubitUnitaryFromModule); - }; - - assertEquivalent( - [](mlir::qc::QCProgramBuilder& b, mlir::Value q0, mlir::Value q1) { - b.h(q0); - b.sx(q1); - b.xx_plus_yy(0.52, -0.14, q0, q1); - b.rz(0.31, q0); - }); - assertEquivalent([](mlir::qc::QCProgramBuilder& b, mlir::Value q0, - mlir::Value q1) { b.xx_minus_yy(-0.37, 0.26, q0, q1); }); + expectEquivalentAndNativeAfterSynthesis( + [&] { + return mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::nativeSynthCustomMenusXxPlusYyChain); + }, + kIbmFrac, &NativeSynthesisPassTest::onlyIbmFractionalOps, + computeTwoQubitUnitaryFromModule); + expectEquivalentAndNativeAfterSynthesis( + [&] { + return mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::nativeSynthCustomMenusXxMinusYyOnly); + }, + kIbmFrac, &NativeSynthesisPassTest::onlyIbmFractionalOps, + computeTwoQubitUnitaryFromModule); } diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_fusion.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_fusion.cpp index 1dfcb7770d..f46e617c2d 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_fusion.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_fusion.cpp @@ -9,12 +9,16 @@ */ // 1q run merging, 2q block consolidation, and RZX profile sweeps for the -// native-gate synthesis pass. Linked with sibling `test_native_synthesis_*.cpp` -// sources into `mqt-core-mlir-unittest-native-synthesis`. +// native-gate synthesis pass. Includes a few ``TEST_P`` matrices (U3 GPhase +// pair, generic-u3-cx two-qubit equivalence rows). Linked with sibling +// ``test_native_synthesis_*.cpp`` sources into +// ``mqt-core-mlir-unittest-native-synthesis``. #include "native_synthesis_pass_test_fixture.h" +#include #include +#include using namespace mlir; using namespace mlir::qco; @@ -33,8 +37,104 @@ std::size_t countOpsOfTypeInModule(const OwningOpRef& moduleOp) { }); return count; } + +struct OneQU3FusionGPhaseRow { + const char* name; + void (*program)(mlir::qc::QCProgramBuilder&); + unsigned expectGPhaseCount; +}; + +struct TwoQBlockEquivGenericU3CxRow { + const char* name; + void (*program)(mlir::qc::QCProgramBuilder&); + std::optional expectExactCtrlOpCount; +}; } // namespace +class NativeSynthesisOneQFusionU3GPhaseTest + : public NativeSynthesisPassTest, + public testing::WithParamInterface { +public: + using NativeSynthesisPassTest::onlyGenericU3CxOps; +}; + +TEST_P(NativeSynthesisOneQFusionU3GPhaseTest, FusesAdjacentNativeUChain) { + const OneQU3FusionGPhaseRow& param = GetParam(); + auto moduleOp = + mlir::qc::QCProgramBuilder::build(context.get(), param.program); + runNativeSynthesis(moduleOp, "u,cx"); + EXPECT_TRUE(onlyGenericU3CxOps(moduleOp)); + EXPECT_EQ(countOpsOfTypeInModule(moduleOp), 1U); + EXPECT_EQ(countOpsOfTypeInModule(moduleOp), + param.expectGPhaseCount); +} + +INSTANTIATE_TEST_SUITE_P( + OneQRunMergingU3GPhaseMatrix, NativeSynthesisOneQFusionU3GPhaseTest, + testing::Values(OneQU3FusionGPhaseRow{"EmitsGlobalPhaseOnU3", + mlir::qc::nativeSynthFusionTS, + /*expectGPhaseCount=*/1U}, + OneQU3FusionGPhaseRow{"OmitsGPhaseWhenResidualIsTrivial", + mlir::qc::nativeSynthFusionUUTwoQDet1, + /*expectGPhaseCount=*/0U}), + [](const testing::TestParamInfo& info) { + return info.param.name; + }); + +class NativeSynthesisTwoQBlockEquivGenericU3CxTest + : public NativeSynthesisPassTest, + public testing::WithParamInterface { +public: + using NativeSynthesisPassTest::onlyGenericU3CxOps; +}; + +TEST_P(NativeSynthesisTwoQBlockEquivGenericU3CxTest, + EquivalentUnderConsolidation) { + const TwoQBlockEquivGenericU3CxRow& param = GetParam(); + auto buildFn = [&] { + return mlir::qc::QCProgramBuilder::build(context.get(), param.program); + }; + + auto expected = buildFn(); + runQcToQco(expected); + const auto expectedUnitary = computeTwoQubitUnitaryFromModule(expected); + ASSERT_TRUE(expectedUnitary.has_value()); + + auto synth = buildFn(); + runNativeSynthesis(synth, "u,cx"); + EXPECT_TRUE(onlyGenericU3CxOps(synth)); + if (param.expectExactCtrlOpCount.has_value()) { + EXPECT_EQ(countOpsOfTypeInModule(synth), + *param.expectExactCtrlOpCount); + } + const auto synthUnitary = computeTwoQubitUnitaryFromModule(synth); + ASSERT_TRUE(synthUnitary.has_value()); + EXPECT_TRUE(isEquivalentUpToGlobalPhase(*expectedUnitary, *synthUnitary)); +} + +INSTANTIATE_TEST_SUITE_P( + TwoQBlockEquivGenericU3CxMatrix, + NativeSynthesisTwoQBlockEquivGenericU3CxTest, + testing::Values( + TwoQBlockEquivGenericU3CxRow{"AdjacentCxCancel", + mlir::qc::nativeSynthFusionCxCx, + /*expectExactCtrlOpCount=*/0U}, + TwoQBlockEquivGenericU3CxRow{ + "FusesCxThroughInterleavedOneQOps", + mlir::qc::nativeSynthFusionHCxInterleavedTCx, std::nullopt}, + TwoQBlockEquivGenericU3CxRow{"HandlesSwappedWireOrder", + mlir::qc::nativeSynthFusionSwapCxPattern, + std::nullopt}, + TwoQBlockEquivGenericU3CxRow{"EquivalentWhenBlockContainsDcx", + mlir::qc::nativeSynthFusionHDcxSCx, + std::nullopt}, + TwoQBlockEquivGenericU3CxRow{"EquivalentWhenBlockContainsRzx", + mlir::qc::nativeSynthFusionXRzxTCx, + std::nullopt}), + [](const testing::TestParamInfo& info) { + return info.param.name; + }); + // --- 1q-run-merging pre-synthesis step --- // // The tests below exercise the in-pass run merging that fuses adjacent @@ -49,14 +149,8 @@ TEST_F(NativeSynthesisPassTest, OneQRunMergingCollapsesHadamardZHadamardToX) { // fusion we would expect at least 3 RZ gates from two H decompositions and // the Z. auto buildFn = [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - builder.h(q0); - builder.z(q0); - builder.h(q0); - builder.dealloc(q0); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::nativeSynthFusionHadamardZHadamard); }; auto moduleOp = buildFn(); @@ -70,13 +164,8 @@ TEST_F(NativeSynthesisPassTest, OneQRunMergingCollapsesHadamardZHadamardToX) { TEST_F(NativeSynthesisPassTest, OneQRunMergingCancelsAdjacentSelfInverses) { // H * H = I. Fusion collapses the run to no 1q ops at all. auto buildFn = [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - builder.h(q0); - builder.h(q0); - builder.dealloc(q0); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::nativeSynthFusionHadamardHadamard); }; auto moduleOp = buildFn(); @@ -92,16 +181,8 @@ TEST_F(NativeSynthesisPassTest, OneQRunMergingReducesMixedChainToSingleU) { // single UOp on the generic-u3-cx profile via fusion, regardless of the // mix of non-native ops in the input. auto buildFn = [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - builder.h(q0); - builder.s(q0); - builder.t(q0); - builder.y(q0); - builder.sx(q0); - builder.dealloc(q0); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::nativeSynthFusionMixedChainHSTYSX); }; auto moduleOp = buildFn(); @@ -116,32 +197,14 @@ TEST_F(NativeSynthesisPassTest, OneQRunMergingDoesNotFuseAcrossCX) { // we assert we still see >=2 SX gates (one from each Hadamard expansion). expectEquivalentAndNativeAfterSynthesis( [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - builder.h(q0); - builder.cx(q0, q1); - builder.h(q0); - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::nativeSynthFusionHadamardCxHadamard); }, "x,sx,rz,cx", &NativeSynthesisPassTest::onlyIbmBasicCxOps, computeTwoQubitUnitaryFromModule); - auto moduleOp = [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - builder.h(q0); - builder.cx(q0, q1); - builder.h(q0); - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); - }(); + auto moduleOp = mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::nativeSynthFusionHadamardCxHadamard); runNativeSynthesis(moduleOp, "x,sx,rz,cx"); // Each H decomposes to rz(pi/2) sx rz(pi/2); without fusion we get two // separate decompositions => at least 2 SX gates total. @@ -152,16 +215,8 @@ TEST_F(NativeSynthesisPassTest, OneQRunMergingDoesNotFuseAcrossBarrier) { // A barrier between two 1q ops on the same wire interrupts the run: // `BarrierOp` is explicitly excluded from fusibility and its use of the // qubit breaks the single-use precondition on the intermediate value. - auto moduleOp = [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - builder.h(q0); - builder.barrier({q0}); - builder.h(q0); - builder.dealloc(q0); - return builder.finalize(); - }(); + auto moduleOp = mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::nativeSynthFusionHadamardBarrierHadamard); runNativeSynthesis(moduleOp, "x,sx,rz,cx"); EXPECT_TRUE(onlyIbmBasicCxOps(moduleOp)); // Two separate H decompositions survive => at least 2 SX gates. @@ -174,16 +229,8 @@ TEST_F(NativeSynthesisPassTest, OneQRunMergingSkipsFullyNativeRuns) { // fuses a fully-native run when fusion would produce strictly fewer ops // than the original run. For `rz; sx; rz` the ZSXX decomposition of the // fused matrix is itself three ops, so the run is left untouched. - auto moduleOp = [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - builder.rz(0.4, q0); - builder.sx(q0); - builder.rz(-0.9, q0); - builder.dealloc(q0); - return builder.finalize(); - }(); + auto moduleOp = mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::nativeSynthFusionRzSxRz); runNativeSynthesis(moduleOp, "x,sx,rz,cx"); EXPECT_TRUE(onlyIbmBasicCxOps(moduleOp)); EXPECT_EQ(countOpsOfTypeInModule(moduleOp), 2U); @@ -197,65 +244,16 @@ TEST_F(NativeSynthesisPassTest, // emits exactly one gate per fused 2x2 unitary. Without the cost gate, // the fully-native run would be skipped; without fusion, the run would // survive as two ops because there is no `MergeSubsequentU` canonicalizer. - auto moduleOp = [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - builder.u(0.3, 0.1, -0.2, q0); - builder.u(-0.5, 0.7, 0.4, q0); - builder.dealloc(q0); - return builder.finalize(); - }(); + auto moduleOp = mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::nativeSynthFusionUUTwoQGenericU3); runNativeSynthesis(moduleOp, "u,cx"); EXPECT_TRUE(onlyGenericU3CxOps(moduleOp)); EXPECT_EQ(countOpsOfTypeInModule(moduleOp), 1U); } -TEST_F(NativeSynthesisPassTest, OneQRunMergingEmitsGlobalPhaseOnU3) { - // Phase-A GPhase refinement: fusing `T; S` on the generic-u3-cx profile - // composes to a diagonal matrix whose SU(2) normalisation sheds a - // non-trivial residual phase of `3*pi/8`. The fusion emitter preserves - // the phase via a `qco.gphase` op so the synthesized IR reconstructs the - // original unitary exactly (not merely up to global phase). `T; S` is - // chosen over `T; T` because `MergeSubsequentT` would otherwise fold the - // latter to `S` upstream: `T; S` is not matched by any existing - // canonicalizer, so this test exercises the fusion path unambiguously. - auto moduleOp = [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - builder.t(q0); - builder.s(q0); - builder.dealloc(q0); - return builder.finalize(); - }(); - runNativeSynthesis(moduleOp, "u,cx"); - EXPECT_TRUE(onlyGenericU3CxOps(moduleOp)); - EXPECT_EQ(countOpsOfTypeInModule(moduleOp), 1U); - EXPECT_EQ(countOpsOfTypeInModule(moduleOp), 1U); -} - -TEST_F(NativeSynthesisPassTest, - OneQRunMergingOmitsGPhaseWhenResidualIsTrivial) { - // Negative complement of OneQRunMergingEmitsGlobalPhaseOnU3: each U3 with - // `lambda = -phi` has det = 1, so the composed unitary also has det = 1. - // The fusion path computes an SU(2)-normalised decomposition whose - // `globalPhase` is negligible, and `emitGPhaseIfNonTrivial` must skip - // emitting any `qco.gphase` op. - auto moduleOp = [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - builder.u(0.3, 0.2, -0.2, q0); - builder.u(0.5, 0.4, -0.4, q0); - builder.dealloc(q0); - return builder.finalize(); - }(); - runNativeSynthesis(moduleOp, "u,cx"); - EXPECT_TRUE(onlyGenericU3CxOps(moduleOp)); - EXPECT_EQ(countOpsOfTypeInModule(moduleOp), 1U); - EXPECT_EQ(countOpsOfTypeInModule(moduleOp), 0U); -} +// GPhase expectations for adjacent native ``U`` fusion are covered by +// ``OneQRunMergingU3GPhaseMatrix`` (``EmitsGlobalPhaseOnU3`` / +// ``OmitsGPhaseWhenResidualIsTrivial``). TEST_F(NativeSynthesisPassTest, OneQRunMergingLongMixedChainEquivalentAcrossProfiles) { @@ -264,24 +262,8 @@ TEST_F(NativeSynthesisPassTest, // ``fiveCxEntanglerEquivalenceProfiles``), excluding IQM-default ``r,cz``, // which uses a different two-qubit path. auto buildFn = [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - builder.h(q0); - builder.t(q0); - builder.rx(0.37, q0); - builder.s(q0); - builder.ry(-0.21, q0); - builder.h(q0); - builder.z(q0); - builder.rz(0.52, q0); - builder.sx(q0); - builder.y(q0); - builder.cx(q0, q1); - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::nativeSynthFusionLongMixedTenOpCx); }; const auto profiles = @@ -316,69 +298,8 @@ TEST_F(NativeSynthesisPassTest, // strictly shorter, and (c) boundary conditions such as wire swaps and // interleaved barriers. -TEST_F(NativeSynthesisPassTest, TwoQBlockConsolidationCancelsAdjacentCx) { - // Two CX(q0,q1) cancel to the identity. The consolidation step folds the - // pair into a trivial 4x4, which the decomposer realises with zero basis - // gate uses. - auto buildFn = [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - builder.cx(q0, q1); - builder.cx(q0, q1); - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); - }; - - auto expected = buildFn(); - runQcToQco(expected); - const auto expectedUnitary = computeTwoQubitUnitaryFromModule(expected); - ASSERT_TRUE(expectedUnitary.has_value()); - - auto synth = buildFn(); - runNativeSynthesis(synth, "u,cx"); - EXPECT_TRUE(onlyGenericU3CxOps(synth)); - EXPECT_EQ(countOpsOfTypeInModule(synth), 0U); - const auto synthUnitary = computeTwoQubitUnitaryFromModule(synth); - ASSERT_TRUE(synthUnitary.has_value()); - EXPECT_TRUE(isEquivalentUpToGlobalPhase(*expectedUnitary, *synthUnitary)); -} - -TEST_F(NativeSynthesisPassTest, - TwoQBlockConsolidationFusesCxThroughInterleavedOneQOps) { - // A non-native block containing interleaved single-qubit ops on the two - // wires must consolidate into a single 4x4 unitary that the decomposer - // synthesises with the target's entangler (CX). The resulting circuit - // must be unitarily equivalent to the original. - auto buildFn = [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - builder.h(q0); - builder.cx(q0, q1); - builder.t(q1); - builder.s(q0); - builder.cx(q0, q1); - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); - }; - - auto expected = buildFn(); - runQcToQco(expected); - const auto expectedUnitary = computeTwoQubitUnitaryFromModule(expected); - ASSERT_TRUE(expectedUnitary.has_value()); - - auto synth = buildFn(); - runNativeSynthesis(synth, "u,cx"); - EXPECT_TRUE(onlyGenericU3CxOps(synth)); - const auto synthUnitary = computeTwoQubitUnitaryFromModule(synth); - ASSERT_TRUE(synthUnitary.has_value()); - EXPECT_TRUE(isEquivalentUpToGlobalPhase(*expectedUnitary, *synthUnitary)); -} +// Generic-u3-cx two-qubit block equivalence rows (including adjacent-CX +// cancellation) live in ``TwoQBlockEquivGenericU3CxMatrix``. TEST_F(NativeSynthesisPassTest, TwoQBlockConsolidationStopsAtDifferentPairBoundary) { @@ -387,18 +308,8 @@ TEST_F(NativeSynthesisPassTest, // `cx(q1, q2)` so block consolidation cannot fuse the outer pair into a // single identity; equivalence still has to hold. auto buildFn = [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - const auto q2 = builder.allocQubit(); - builder.cx(q0, q1); - builder.cx(q1, q2); - builder.cx(q0, q1); - builder.dealloc(q0); - builder.dealloc(q1); - builder.dealloc(q2); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::nativeSynthFusionThreeLineCx01Cx12Cx01); }; auto synth = buildFn(); @@ -414,16 +325,8 @@ TEST_F(NativeSynthesisPassTest, // A barrier between two CX(q0,q1) blocks must prevent them from being // fused into a single block. Each CX stays an individual entangler. auto buildFn = [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - builder.cx(q0, q1); - builder.barrier({q0, q1}); - builder.cx(q0, q1); - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::nativeSynthFusionCxBarrierCx); }; auto synth = buildFn(); @@ -434,115 +337,14 @@ TEST_F(NativeSynthesisPassTest, EXPECT_EQ(countOpsOfTypeInModule(synth), 2U); } -TEST_F(NativeSynthesisPassTest, TwoQBlockConsolidationHandlesSwappedWireOrder) { - // Three CXs in alternating direction form SWAP; consolidation must preserve - // the unitary. - auto buildFn = [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - builder.cx(q0, q1); - builder.cx(q1, q0); - builder.cx(q0, q1); - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); - }; - - auto expected = buildFn(); - runQcToQco(expected); - const auto expectedUnitary = computeTwoQubitUnitaryFromModule(expected); - ASSERT_TRUE(expectedUnitary.has_value()); - - auto synth = buildFn(); - runNativeSynthesis(synth, "u,cx"); - EXPECT_TRUE(onlyGenericU3CxOps(synth)); - const auto synthUnitary = computeTwoQubitUnitaryFromModule(synth); - ASSERT_TRUE(synthUnitary.has_value()); - EXPECT_TRUE(isEquivalentUpToGlobalPhase(*expectedUnitary, *synthUnitary)); -} - -TEST_F(NativeSynthesisPassTest, - TwoQBlockConsolidationEquivalentWhenBlockContainsDcx) { - // Convention audit: DCX is directional/asymmetric, so this checks that - // Phase-B block accumulation preserves operand ordering when a DCX appears - // inside an otherwise consolidatable block. - auto buildFn = [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - builder.h(q0); - builder.dcx(q0, q1); - builder.s(q1); - builder.cx(q0, q1); - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); - }; - - auto expected = buildFn(); - runQcToQco(expected); - const auto expectedUnitary = computeTwoQubitUnitaryFromModule(expected); - ASSERT_TRUE(expectedUnitary.has_value()); - - auto synth = buildFn(); - runNativeSynthesis(synth, "u,cx"); - EXPECT_TRUE(onlyGenericU3CxOps(synth)); - const auto synthUnitary = computeTwoQubitUnitaryFromModule(synth); - ASSERT_TRUE(synthUnitary.has_value()); - EXPECT_TRUE(isEquivalentUpToGlobalPhase(*expectedUnitary, *synthUnitary)); -} - -TEST_F(NativeSynthesisPassTest, - TwoQBlockConsolidationEquivalentWhenBlockContainsRzx) { - // Convention audit: RZX is directional/asymmetric. This test guards - // against BE/LE mismatches in mixed blocks containing RZX. - auto buildFn = [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - builder.x(q0); - builder.rzx(0.41, q0, q1); - builder.t(q1); - builder.cx(q0, q1); - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); - }; - - auto expected = buildFn(); - runQcToQco(expected); - const auto expectedUnitary = computeTwoQubitUnitaryFromModule(expected); - ASSERT_TRUE(expectedUnitary.has_value()); - - auto synth = buildFn(); - runNativeSynthesis(synth, "u,cx"); - EXPECT_TRUE(onlyGenericU3CxOps(synth)); - const auto synthUnitary = computeTwoQubitUnitaryFromModule(synth); - ASSERT_TRUE(synthUnitary.has_value()); - EXPECT_TRUE(isEquivalentUpToGlobalPhase(*expectedUnitary, *synthUnitary)); -} - TEST_F(NativeSynthesisPassTest, TwoQBlockConsolidationHandlesRzzOnIbmFractional) { // Explicitly exercise a non-CX/CZ two-qubit gate inside a block on a // profile that supports it natively. Consolidation may keep/reshape the // block, but equivalence and profile validity must hold. auto buildFn = [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - builder.h(q0); - builder.rzz(-0.29, q0, q1); - builder.s(q1); - builder.rzz(0.17, q0, q1); - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::nativeSynthFusionHRzzSRzz); }; auto expected = buildFn(); @@ -563,48 +365,52 @@ TEST_F(NativeSynthesisPassTest, // Directed RZX tests (asymmetric 2q); both operand orders. const auto profiles = NativeSynthesisPassTest::allNineEquivalenceProfiles(); - // Representative generic and ``pi/2`` angles (operand order tested below). - const std::array angles{{0.41, std::numbers::pi / 2.0}}; + // Four directed RZX fixtures: two angles × two operand orders. + struct RzxStandaloneRow { + double theta; + bool swapOperands; + void (*program)(mlir::qc::QCProgramBuilder&); + }; + const std::array rzxRows{{ + RzxStandaloneRow{.theta = 0.41, + .swapOperands = false, + .program = mlir::qc::nativeSynthFusionRzx041Q0First}, + RzxStandaloneRow{.theta = 0.41, + .swapOperands = true, + .program = mlir::qc::nativeSynthFusionRzx041Q1First}, + RzxStandaloneRow{.theta = std::numbers::pi / 2.0, + .swapOperands = false, + .program = mlir::qc::nativeSynthFusionRzxPiHalfQ0First}, + RzxStandaloneRow{.theta = std::numbers::pi / 2.0, + .swapOperands = true, + .program = mlir::qc::nativeSynthFusionRzxPiHalfQ1First}, + }}; for (const auto& profileCase : profiles) { - for (const double theta : angles) { - for (const bool swapOperands : {false, true}) { - auto buildFn = [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - if (swapOperands) { - builder.rzx(theta, q1, q0); - } else { - builder.rzx(theta, q0, q1); - } - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); - }; - - auto expected = buildFn(); - runQcToQco(expected); - const auto expectedUnitary = computeTwoQubitUnitaryFromModule(expected); - ASSERT_TRUE(expectedUnitary.has_value()) - << "native-gates=" << profileCase.nativeGates << " theta=" << theta - << " swapped=" << swapOperands; - - auto synth = buildFn(); - runNativeSynthesis(synth, profileCase.nativeGates); - EXPECT_TRUE(profileCase.isNative(synth)) - << "native-gates=" << profileCase.nativeGates << " theta=" << theta - << " swapped=" << swapOperands; - const auto synthUnitary = computeTwoQubitUnitaryFromModule(synth); - ASSERT_TRUE(synthUnitary.has_value()) - << "native-gates=" << profileCase.nativeGates << " theta=" << theta - << " swapped=" << swapOperands; - EXPECT_TRUE( - isEquivalentUpToGlobalPhase(*expectedUnitary, *synthUnitary)) - << "native-gates=" << profileCase.nativeGates << " theta=" << theta - << " swapped=" << swapOperands; - } + for (const RzxStandaloneRow& row : rzxRows) { + auto buildFn = [&] { + return mlir::qc::QCProgramBuilder::build(context.get(), row.program); + }; + + auto expected = buildFn(); + runQcToQco(expected); + const auto expectedUnitary = computeTwoQubitUnitaryFromModule(expected); + ASSERT_TRUE(expectedUnitary.has_value()) + << "native-gates=" << profileCase.nativeGates + << " theta=" << row.theta << " swapped=" << row.swapOperands; + + auto synth = buildFn(); + runNativeSynthesis(synth, profileCase.nativeGates); + EXPECT_TRUE(profileCase.isNative(synth)) + << "native-gates=" << profileCase.nativeGates + << " theta=" << row.theta << " swapped=" << row.swapOperands; + const auto synthUnitary = computeTwoQubitUnitaryFromModule(synth); + ASSERT_TRUE(synthUnitary.has_value()) + << "native-gates=" << profileCase.nativeGates + << " theta=" << row.theta << " swapped=" << row.swapOperands; + EXPECT_TRUE(isEquivalentUpToGlobalPhase(*expectedUnitary, *synthUnitary)) + << "native-gates=" << profileCase.nativeGates + << " theta=" << row.theta << " swapped=" << row.swapOperands; } } } diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_multi_qubit.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_multi_qubit.cpp index 8514bc62a0..f0eb51e7ca 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_multi_qubit.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_multi_qubit.cpp @@ -14,7 +14,6 @@ #include "native_synthesis_pass_test_fixture.h" #include -#include using namespace mlir; using namespace mlir::qco; @@ -22,126 +21,24 @@ using namespace mlir::qco::native_synth_test; namespace { -/// Controlled-phase decomposition: CP(θ) on (ctrl, tgt) expressed with only -/// single-qubit `p` and `cx`, which are supported by every targeted profile. -void emitControlledPhase(mlir::qc::QCProgramBuilder& builder, double theta, - Value ctrl, Value tgt) { - builder.p(theta / 2.0, ctrl); - builder.cx(ctrl, tgt); - builder.p(-theta / 2.0, tgt); - builder.cx(ctrl, tgt); - builder.p(theta / 2.0, tgt); -} - -/// Standard Clifford+T decomposition of CCX on (c1, c2, t). -void emitToffoli(mlir::qc::QCProgramBuilder& builder, Value c1, Value c2, - Value t) { - builder.h(t); - builder.cx(c2, t); - builder.tdg(t); - builder.cx(c1, t); - builder.t(t); - builder.cx(c2, t); - builder.tdg(t); - builder.cx(c1, t); - builder.t(c2); - builder.t(t); - builder.h(t); - builder.cx(c1, c2); - builder.t(c1); - builder.tdg(c2); - builder.cx(c1, c2); -} - -/// 3-qubit GHZ preparation: H on q0 then CX ladder. OwningOpRef buildThreeQGhzCircuit(MLIRContext* context) { - mlir::qc::QCProgramBuilder builder(context); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - const auto q2 = builder.allocQubit(); - builder.h(q0); - builder.cx(q0, q1); - builder.cx(q1, q2); - builder.dealloc(q0); - builder.dealloc(q1); - builder.dealloc(q2); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build( + context, mlir::qc::nativeSynthMultiQThreeQGhz); } -/// 3-qubit Toffoli via Clifford+T decomposition (15 gates). OwningOpRef buildThreeQToffoliCircuit(MLIRContext* context) { - mlir::qc::QCProgramBuilder builder(context); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - const auto q2 = builder.allocQubit(); - emitToffoli(builder, q0, q1, q2); - builder.dealloc(q0); - builder.dealloc(q1); - builder.dealloc(q2); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build( + context, mlir::qc::nativeSynthMultiQThreeQToffoli); } -/// 3-qubit QFT; final wire reorder done with CXs (no native SWAP in several -/// menus). OwningOpRef buildThreeQQftCircuit(MLIRContext* context) { - using std::numbers::pi; - mlir::qc::QCProgramBuilder builder(context); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - const auto q2 = builder.allocQubit(); - - builder.h(q2); - emitControlledPhase(builder, pi / 2.0, q1, q2); - builder.h(q1); - emitControlledPhase(builder, pi / 4.0, q0, q2); - emitControlledPhase(builder, pi / 2.0, q0, q1); - builder.h(q0); - - // SWAP(q0, q2) via three CXs. - builder.cx(q0, q2); - builder.cx(q2, q0); - builder.cx(q0, q2); - - builder.dealloc(q0); - builder.dealloc(q1); - builder.dealloc(q2); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build( + context, mlir::qc::nativeSynthMultiQThreeQQft); } -/// Deterministic Clifford+T mix on 3 qubits spanning every single-qubit family -/// accepted by `extractSingleQubitMatrix` and both CX/CZ entanglers. OwningOpRef buildThreeQCliffordTMixCircuit(MLIRContext* context) { - mlir::qc::QCProgramBuilder builder(context); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - const auto q2 = builder.allocQubit(); - - builder.h(q0); - builder.t(q1); - builder.x(q2); - builder.cx(q0, q1); - builder.rz(0.37, q2); - builder.cz(q1, q2); - builder.sdg(q0); - builder.ry(-0.42, q1); - builder.cx(q2, q0); - builder.y(q1); - builder.tdg(q2); - builder.cx(q0, q1); - builder.p(0.21, q2); - builder.h(q2); - builder.cz(q0, q2); - builder.rx(-0.13, q1); - builder.s(q0); - - builder.dealloc(q0); - builder.dealloc(q1); - builder.dealloc(q2); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build( + context, mlir::qc::nativeSynthMultiQThreeQCliffordTMix); } struct ThreeQubitCircuitCase { @@ -191,64 +88,9 @@ TEST_F(NativeSynthesisPassTest, ThreeQubitCircuitsEquivalentAcrossProfiles) { namespace { -/// 5-qubit stress circuit matching the structural -/// `LargeMultiQubitCircuitStaysWithinMinimalMenu` test. Designed to exercise -/// many overlapping 2q blocks and deep 1q chains, using only gates that are -/// supported by every targeted profile's synthesis path. OwningOpRef buildFiveQubitStressCircuit(MLIRContext* context) { - mlir::qc::QCProgramBuilder builder(context); - builder.initialize(); - - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - const auto q2 = builder.allocQubit(); - const auto q3 = builder.allocQubit(); - const auto q4 = builder.allocQubit(); - - builder.h(q0); - builder.s(q1); - builder.t(q2); - builder.y(q3); - builder.h(q4); - - builder.cx(q0, q1); - builder.cz(q1, q2); - builder.swap(q2, q3); - builder.cx(q3, q4); - - for (int layer = 0; layer < 4; ++layer) { - builder.h(q0); - builder.s(q0); - builder.t(q0); - - builder.y(q1); - builder.h(q2); - builder.s(q3); - builder.t(q4); - - builder.cx(q0, q2); - builder.cz(q1, q3); - builder.cx(q2, q4); - - if ((layer % 2) == 0) { - builder.swap(q0, q1); - builder.swap(q3, q4); - } else { - builder.cx(q4, q0); - builder.cz(q2, q1); - } - } - - builder.p(0.25, q0); - builder.p(-0.5, q2); - builder.p(0.75, q4); - - builder.dealloc(q0); - builder.dealloc(q1); - builder.dealloc(q2); - builder.dealloc(q3); - builder.dealloc(q4); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build( + context, mlir::qc::nativeSynthMultiQFiveQStressFourLayers); } } // namespace diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_profiles.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_profiles.cpp index 21524314f7..d331f743f8 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_profiles.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_profiles.cpp @@ -14,135 +14,197 @@ using namespace mlir; using namespace mlir::qco; using namespace mlir::qco::native_synth_test; -TEST_F(NativeSynthesisPassTest, DecomposesToIbmBasicCxProfile) { +namespace { + +/// Row for ``native-gates`` menu + IR predicate used by several profile +/// matrices. +struct NativeSynthMenuRow { + const char* name; + const char* nativeGates; + bool (*isNative)(OwningOpRef&); +}; + +} // namespace + +class NativeSynthesisSwapProfileTest + : public NativeSynthesisPassTest, + public testing::WithParamInterface { +public: + using NativeSynthesisPassTest::onlyAxisPairRxRzCxOps; + using NativeSynthesisPassTest::onlyAxisPairRyRzCzOps; + using NativeSynthesisPassTest::onlyGenericU3CxOps; + using NativeSynthesisPassTest::onlyGenericU3CzOps; + using NativeSynthesisPassTest::onlyIbmBasicCxOps; + using NativeSynthesisPassTest::onlyIbmBasicCzOps; + using NativeSynthesisPassTest::onlyIbmFractionalOps; +}; + +TEST_P(NativeSynthesisSwapProfileTest, DecomposesSwapToProfile) { + const NativeSynthMenuRow& param = GetParam(); expectNativeAfterSynthesis( [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - builder.h(q0); - builder.s(q0); - builder.t(q0); - builder.y(q0); - builder.cx(q0, q1); - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build(context.get(), mlir::qc::swap); }, - "x,sx,rz,cx", &NativeSynthesisPassTest::onlyIbmBasicCxOps); -} - -TEST_F(NativeSynthesisPassTest, DecomposesSwapToIbmBasicCxProfile) { + param.nativeGates, param.isNative); +} + +INSTANTIATE_TEST_SUITE_P( + SwapMenuMatrix, NativeSynthesisSwapProfileTest, + testing::Values( + NativeSynthMenuRow{"IbmBasicCx", "x,sx,rz,cx", + &NativeSynthesisSwapProfileTest::onlyIbmBasicCxOps}, + NativeSynthMenuRow{"GenericU3Cx", "u,cx", + &NativeSynthesisSwapProfileTest::onlyGenericU3CxOps}, + NativeSynthMenuRow{"IbmBasicCz", "x,sx,rz,cz", + &NativeSynthesisSwapProfileTest::onlyIbmBasicCzOps}, + NativeSynthMenuRow{"GenericU3Cz", "u,cz", + &NativeSynthesisSwapProfileTest::onlyGenericU3CzOps}, + NativeSynthMenuRow{ + "IbmFractional", "x,sx,rz,rx,rzz,cz", + &NativeSynthesisSwapProfileTest::onlyIbmFractionalOps}, + NativeSynthMenuRow{ + "AxisPairRxRzCx", "rx,rz,cx", + &NativeSynthesisSwapProfileTest::onlyAxisPairRxRzCxOps}, + NativeSynthMenuRow{ + "AxisPairRyRzCz", "ry,rz,cz", + &NativeSynthesisSwapProfileTest::onlyAxisPairRyRzCzOps}), + [](const testing::TestParamInfo& info) { + return info.param.name; + }); + +class NativeSynthesisHstycxMenuTest + : public NativeSynthesisPassTest, + public testing::WithParamInterface { +public: + using NativeSynthesisPassTest::onlyGenericU3CxOps; + using NativeSynthesisPassTest::onlyIbmBasicCxOps; +}; + +TEST_P(NativeSynthesisHstycxMenuTest, DecomposesHstycxTwoQToProfile) { + const NativeSynthMenuRow& param = GetParam(); expectNativeAfterSynthesis( [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - builder.swap(q0, q1); - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::nativeSynthProfilesHstycxTwoQ); }, - "x,sx,rz,cx", &NativeSynthesisPassTest::onlyIbmBasicCxOps); -} - -TEST_F(NativeSynthesisPassTest, DecomposesToGenericU3CxProfile) { - expectNativeAfterSynthesis( - [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - builder.h(q0); - builder.s(q0); - builder.t(q0); - builder.y(q0); - builder.cx(q0, q1); - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); - }, - "u,cx", &NativeSynthesisPassTest::onlyGenericU3CxOps); -} - -TEST_F(NativeSynthesisPassTest, DecomposesSwapToGenericU3CxProfile) { + param.nativeGates, param.isNative); +} + +INSTANTIATE_TEST_SUITE_P( + HstycxTwoQMenuMatrix, NativeSynthesisHstycxMenuTest, + testing::Values( + NativeSynthMenuRow{"IbmBasicCx", "x,sx,rz,cx", + &NativeSynthesisHstycxMenuTest::onlyIbmBasicCxOps}, + NativeSynthMenuRow{"GenericU3Cx", "u,cx", + &NativeSynthesisHstycxMenuTest::onlyGenericU3CxOps}), + [](const testing::TestParamInfo& info) { + return info.param.name; + }); + +class NativeSynthesisCxYOnQ1MenuTest + : public NativeSynthesisPassTest, + public testing::WithParamInterface { +public: + using NativeSynthesisPassTest::onlyAxisPairRyRzCzOps; + using NativeSynthesisPassTest::onlyIqmDefaultOps; +}; + +TEST_P(NativeSynthesisCxYOnQ1MenuTest, ConvertsCxToCzForProfile) { + const NativeSynthMenuRow& param = GetParam(); expectNativeAfterSynthesis( [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - builder.swap(q0, q1); - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::nativeSynthProfilesCxYOnQ1); }, - "u,cx", &NativeSynthesisPassTest::onlyGenericU3CxOps); -} + param.nativeGates, param.isNative); +} + +INSTANTIATE_TEST_SUITE_P( + CxYOnQ1MenuMatrix, NativeSynthesisCxYOnQ1MenuTest, + testing::Values( + NativeSynthMenuRow{ + "AxisPairRyRzCz", "ry,rz,cz", + &NativeSynthesisCxYOnQ1MenuTest::onlyAxisPairRyRzCzOps}, + NativeSynthMenuRow{"IqmDefault", "r,cz", + &NativeSynthesisCxYOnQ1MenuTest::onlyIqmDefaultOps}), + [](const testing::TestParamInfo& info) { + return info.param.name; + }); + +class NativeSynthesisBroadOneQMenuTest + : public NativeSynthesisPassTest, + public testing::WithParamInterface { +public: + using NativeSynthesisPassTest::onlyAxisPairRyRzCzOps; + using NativeSynthesisPassTest::onlyGenericU3CzOps; + using NativeSynthesisPassTest::onlyIqmDefaultOps; +}; + +TEST_P(NativeSynthesisBroadOneQMenuTest, CanonicalizationNoLeakage) { + const NativeSynthMenuRow& param = GetParam(); + auto moduleOp = buildBroadOneQCanonicalizationCircuit(); + runNativeSynthesis(moduleOp, param.nativeGates); + EXPECT_TRUE(param.isNative(moduleOp)); +} + +INSTANTIATE_TEST_SUITE_P( + BroadOneQMenuMatrix, NativeSynthesisBroadOneQMenuTest, + testing::Values( + NativeSynthMenuRow{ + "IqmDefault", "r,cz", + &NativeSynthesisBroadOneQMenuTest::onlyIqmDefaultOps}, + NativeSynthMenuRow{ + "AxisPairRyRzCz", "ry,rz,cz", + &NativeSynthesisBroadOneQMenuTest::onlyAxisPairRyRzCzOps}, + NativeSynthMenuRow{ + "GenericU3Cz", "u,cz", + &NativeSynthesisBroadOneQMenuTest::onlyGenericU3CzOps}), + [](const testing::TestParamInfo& info) { + return info.param.name; + }); + +class NativeSynthesisZeroAngleMenuTest + : public NativeSynthesisPassTest, + public testing::WithParamInterface { +public: + using NativeSynthesisPassTest::onlyAxisPairRyRzCzOps; + using NativeSynthesisPassTest::onlyIqmDefaultOps; +}; + +TEST_P(NativeSynthesisZeroAngleMenuTest, CanonicalizationNoLeakage) { + const NativeSynthMenuRow& param = GetParam(); + auto moduleOp = buildZeroAngleCanonicalizationCircuit(); + runNativeSynthesis(moduleOp, param.nativeGates); + EXPECT_TRUE(param.isNative(moduleOp)); +} + +INSTANTIATE_TEST_SUITE_P( + ZeroAngleMenuMatrix, NativeSynthesisZeroAngleMenuTest, + testing::Values( + NativeSynthMenuRow{ + "IqmDefault", "r,cz", + &NativeSynthesisZeroAngleMenuTest::onlyIqmDefaultOps}, + NativeSynthMenuRow{ + "AxisPairRyRzCz", "ry,rz,cz", + &NativeSynthesisZeroAngleMenuTest::onlyAxisPairRyRzCzOps}), + [](const testing::TestParamInfo& info) { + return info.param.name; + }); TEST_F(NativeSynthesisPassTest, DecomposesCxToCzForIbmBasicCzProfile) { expectNativeAfterSynthesis( [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - builder.h(q1); - builder.cx(q0, q1); - builder.t(q1); - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::nativeSynthProfilesHCxTOnQ1); }, "x,sx,rz,cz", &NativeSynthesisPassTest::onlyIbmBasicCzOps); } -TEST_F(NativeSynthesisPassTest, DecomposesSwapToIbmBasicCzProfile) { - expectNativeAfterSynthesis( - [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - builder.swap(q0, q1); - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); - }, - "x,sx,rz,cz", &NativeSynthesisPassTest::onlyIbmBasicCzOps); -} - -TEST_F(NativeSynthesisPassTest, DecomposesSwapToGenericU3CzProfile) { - expectNativeAfterSynthesis( - [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - builder.swap(q0, q1); - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); - }, - "u,cz", &NativeSynthesisPassTest::onlyGenericU3CzOps); -} - TEST_F(NativeSynthesisPassTest, DecomposesToIqmDefaultProfile) { expectNativeAfterSynthesis( [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - builder.x(q0); - builder.y(q0); - builder.sx(q0); - builder.cz(q0, q1); - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::nativeSynthProfilesXYSXCz); }, "r,cz", &NativeSynthesisPassTest::onlyIqmDefaultOps); } @@ -150,33 +212,8 @@ TEST_F(NativeSynthesisPassTest, DecomposesToIqmDefaultProfile) { TEST_F(NativeSynthesisPassTest, DecomposesToIbmFractionalProfile) { expectNativeAfterSynthesis( [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - builder.h(q0); - builder.ry(0.37, q0); - builder.sxdg(q0); - builder.cx(q0, q1); - builder.rzz(0.23, q0, q1); - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); - }, - "x,sx,rz,rx,rzz,cz", &NativeSynthesisPassTest::onlyIbmFractionalOps); -} - -TEST_F(NativeSynthesisPassTest, DecomposesSwapToIbmFractionalProfile) { - expectNativeAfterSynthesis( - [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - builder.swap(q0, q1); - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::nativeSynthProfilesFractionalChain); }, "x,sx,rz,rx,rzz,cz", &NativeSynthesisPassTest::onlyIbmFractionalOps); } @@ -184,31 +221,8 @@ TEST_F(NativeSynthesisPassTest, DecomposesSwapToIbmFractionalProfile) { TEST_F(NativeSynthesisPassTest, DecomposesToAxisPairRxRzCxProfile) { expectNativeAfterSynthesis( [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - builder.h(q0); - builder.y(q0); - builder.cx(q0, q1); - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); - }, - "rx,rz,cx", &NativeSynthesisPassTest::onlyAxisPairRxRzCxOps); -} - -TEST_F(NativeSynthesisPassTest, DecomposesSwapViaBasisDecomposerAxisPairCx) { - expectNativeAfterSynthesis( - [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - builder.swap(q0, q1); - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::nativeSynthProfilesHYcx); }, "rx,rz,cx", &NativeSynthesisPassTest::onlyAxisPairRxRzCxOps); } @@ -216,116 +230,17 @@ TEST_F(NativeSynthesisPassTest, DecomposesSwapViaBasisDecomposerAxisPairCx) { TEST_F(NativeSynthesisPassTest, DecomposesRzToAxisPairRxRyCxProfile) { expectNativeAfterSynthesis( [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - builder.z(q0); - builder.cx(q0, q1); - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::nativeSynthProfilesZCx); }, "rx,ry,cx", &NativeSynthesisPassTest::onlyAxisPairRxRyCxOps); } -TEST_F(NativeSynthesisPassTest, DecomposesToAxisPairRyRzCzProfile) { - expectNativeAfterSynthesis( - [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - builder.x(q0); - builder.h(q0); - builder.cz(q0, q1); - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); - }, - "ry,rz,cz", &NativeSynthesisPassTest::onlyAxisPairRyRzCzOps); -} - -TEST_F(NativeSynthesisPassTest, DecomposesSwapViaBasisDecomposerAxisPairCz) { - expectNativeAfterSynthesis( - [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - builder.swap(q0, q1); - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); - }, - "ry,rz,cz", &NativeSynthesisPassTest::onlyAxisPairRyRzCzOps); -} - -TEST_F(NativeSynthesisPassTest, ConvertsCxToCzForAxisPairRyRzCzProfile) { - expectNativeAfterSynthesis( - [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - builder.cx(q0, q1); - builder.y(q1); - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); - }, - "ry,rz,cz", &NativeSynthesisPassTest::onlyAxisPairRyRzCzOps); -} - -TEST_F(NativeSynthesisPassTest, ConvertsCxToCzForIqmDefaultProfile) { - expectNativeAfterSynthesis( - [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - builder.cx(q0, q1); - builder.y(q1); - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); - }, - "r,cz", &NativeSynthesisPassTest::onlyIqmDefaultOps); -} - -TEST_F(NativeSynthesisPassTest, BroadOneQCanonicalizationIqmDefaultNoLeakage) { - auto moduleOp = buildBroadOneQCanonicalizationCircuit(); - runNativeSynthesis(moduleOp, "r,cz"); - EXPECT_TRUE(onlyIqmDefaultOps(moduleOp)); -} - -TEST_F(NativeSynthesisPassTest, - BroadOneQCanonicalizationAxisPairRyRzCzNoLeakage) { - auto moduleOp = buildBroadOneQCanonicalizationCircuit(); - runNativeSynthesis(moduleOp, "ry,rz,cz"); - EXPECT_TRUE(onlyAxisPairRyRzCzOps(moduleOp)); -} - -TEST_F(NativeSynthesisPassTest, BroadOneQCanonicalizationGenericU3CzNoLeakage) { - auto moduleOp = buildBroadOneQCanonicalizationCircuit(); - runNativeSynthesis(moduleOp, "u,cz"); - EXPECT_TRUE(onlyGenericU3CzOps(moduleOp)); -} - TEST_F(NativeSynthesisPassTest, GenericProfileMatchesGenericU3CxBehavior) { expectEquivalentAndNativeAfterSynthesis( [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - builder.h(q0); - builder.y(q1); - builder.cx(q0, q1); - builder.s(q0); - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::nativeSynthProfilesHq0Yq1CxSq0); }, "u,cx", &NativeSynthesisPassTest::onlyGenericU3CxOps, computeTwoQubitUnitaryFromModule); @@ -334,43 +249,17 @@ TEST_F(NativeSynthesisPassTest, GenericProfileMatchesGenericU3CxBehavior) { TEST_F(NativeSynthesisPassTest, GenericProfileMatchesAxisPairRyRzCzBehavior) { expectEquivalentAndNativeAfterSynthesis( [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - builder.x(q0); - builder.h(q0); - builder.cz(q0, q1); - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::nativeSynthProfilesXHCz); }, "ry,rz,cz", &NativeSynthesisPassTest::onlyAxisPairRyRzCzOps, computeTwoQubitUnitaryFromModule); } -TEST_F(NativeSynthesisPassTest, ZeroAngleCanonicalizationIqmDefaultNoLeakage) { - auto moduleOp = buildZeroAngleCanonicalizationCircuit(); - runNativeSynthesis(moduleOp, "r,cz"); - EXPECT_TRUE(onlyIqmDefaultOps(moduleOp)); -} - -TEST_F(NativeSynthesisPassTest, - ZeroAngleCanonicalizationAxisPairRyRzNoLeakage) { - auto moduleOp = buildZeroAngleCanonicalizationCircuit(); - runNativeSynthesis(moduleOp, "ry,rz,cz"); - EXPECT_TRUE(onlyAxisPairRyRzCzOps(moduleOp)); -} - TEST_F(NativeSynthesisPassTest, FailsForUnsupportedNativeGateMenu) { expectSynthesisFailure( [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - builder.h(q0); - builder.dealloc(q0); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build(context.get(), mlir::qc::h); }, "not-a-gate"); } @@ -379,17 +268,8 @@ TEST_F(NativeSynthesisPassTest, CustomProfileAcceptsOverlappingOneQSupersetMenu) { expectEquivalentAndNativeAfterSynthesis( [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - builder.h(q0); - builder.y(q0); - builder.cx(q0, q1); - builder.s(q1); - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::nativeSynthProfilesHYSameWireCxSq1); }, "u,rx,rz,cx", &NativeSynthesisPassTest::onlyUOrAxisPairRxRzCxOps, computeTwoQubitUnitaryFromModule); @@ -405,16 +285,8 @@ TEST_F(NativeSynthesisPassTest, CustomProfileMatchesIbmFractionalBehavior) { TEST_F(NativeSynthesisPassTest, CustomProfileAcceptsMultipleEntanglersMenu) { expectEquivalentAndNativeAfterSynthesis( [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - builder.h(q0); - builder.cx(q0, q1); - builder.s(q1); - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::nativeSynthProfilesHCxSq1); }, "u,cx,cz", &NativeSynthesisPassTest::onlyGenericU3CxOrCzOps, computeTwoQubitUnitaryFromModule); @@ -424,12 +296,7 @@ TEST_F(NativeSynthesisPassTest, FailsForUnsupportedNativeGateMenuWithoutEmitter) { expectSynthesisFailure( [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - builder.h(q0); - builder.dealloc(q0); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build(context.get(), mlir::qc::h); }, "rz,cx"); } @@ -440,17 +307,8 @@ TEST_F(NativeSynthesisPassTest, MinimalIbmBasicCustomMenuAcceptsPhaseAlias) { // `rz` for custom menus. expectNativeAfterSynthesis( [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - builder.p(0.13, q0); - builder.h(q0); - builder.cx(q0, q1); - builder.p(-0.27, q1); - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::nativeSynthProfilesPhaseHCxPhase); }, "x,sx,rz,cx", &NativeSynthesisPassTest::onlyIbmBasicCxOps); } @@ -460,63 +318,9 @@ TEST_F(NativeSynthesisPassTest, LargeMultiQubitCircuitStaysWithinMinimalMenu) { // still synthesize into the minimal IBM-basic custom menu. expectNativeAfterSynthesis( [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - const auto q2 = builder.allocQubit(); - const auto q3 = builder.allocQubit(); - const auto q4 = builder.allocQubit(); - - // A mix of non-native 1Q ops (h/s/t/y) and entanglers (cx/cz/swap) - // across different pairs. - builder.h(q0); - builder.s(q1); - builder.t(q2); - builder.y(q3); - builder.h(q4); - - builder.cx(q0, q1); - builder.cz(q1, q2); - builder.swap(q2, q3); - builder.cx(q3, q4); - - // Add depth with repeated layers. - for (int layer = 0; layer < 8; ++layer) { - builder.h(q0); - builder.s(q0); - builder.t(q0); - - builder.y(q1); - builder.h(q2); - builder.s(q3); - builder.t(q4); - - builder.cx(q0, q2); - builder.cz(q1, q3); - builder.cx(q2, q4); - - if ((layer % 2) == 0) { - builder.swap(q0, q1); - builder.swap(q3, q4); - } else { - builder.cx(q4, q0); - builder.cz(q2, q1); - } - } - - // Include explicit phases too (these should end up as `rz`/`p`). - builder.p(0.25, q0); - builder.p(-0.5, q2); - builder.p(0.75, q4); - - builder.dealloc(q0); - builder.dealloc(q1); - builder.dealloc(q2); - builder.dealloc(q3); - builder.dealloc(q4); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build( + context.get(), + mlir::qc::nativeSynthProfilesLargeFiveQStressEightLayers); }, "x,sx,rz,cx", &NativeSynthesisPassTest::onlyIbmBasicCxOps); } @@ -524,14 +328,8 @@ TEST_F(NativeSynthesisPassTest, LargeMultiQubitCircuitStaysWithinMinimalMenu) { TEST_F(NativeSynthesisPassTest, FailsForNativeGateMenuWithoutSingleQEmitter) { expectSynthesisFailure( [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - builder.cx(q0, q1); - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build(context.get(), + mlir::qc::singleControlledX); }, "cx,cz"); } @@ -539,26 +337,15 @@ TEST_F(NativeSynthesisPassTest, FailsForNativeGateMenuWithoutSingleQEmitter) { TEST_F(NativeSynthesisPassTest, FailsForNegativeScoreWeight) { expectSynthesisFailure( [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - builder.h(q0); - builder.dealloc(q0); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build(context.get(), mlir::qc::h); }, "u,cx", -1.0, 0.1, 0.01); } TEST_F(NativeSynthesisPassTest, CandidateSelectionIsDeterministicAcrossRuns) { auto buildFn = [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - builder.swap(q0, q1); - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::nativeSynthDeterminismTwoQubitSwap); }; auto firstModule = buildFn(); @@ -572,14 +359,8 @@ TEST_F(NativeSynthesisPassTest, CandidateSelectionIsDeterministicAcrossRuns) { TEST_F(NativeSynthesisPassTest, RichCustomMenuSelectionRemainsDeterministicAcrossWeightsAndRuns) { auto buildFn = [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - builder.swap(q0, q1); - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::nativeSynthDeterminismTwoQubitSwap); }; auto firstModule = buildFn(); @@ -597,16 +378,8 @@ TEST_F(NativeSynthesisPassTest, TEST_F(NativeSynthesisPassTest, FailsForMultiControlledGateStructure) { expectSynthesisFailure( [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - const auto q2 = builder.allocQubit(); - builder.mcx({q0, q1}, q2); - builder.dealloc(q0); - builder.dealloc(q1); - builder.dealloc(q2); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build(context.get(), + mlir::qc::multipleControlledX); }, "x,sx,rz,cx"); } @@ -614,16 +387,8 @@ TEST_F(NativeSynthesisPassTest, FailsForMultiControlledGateStructure) { TEST_F(NativeSynthesisPassTest, FailsForControlledTwoTargetGateStructure) { expectSynthesisFailure( [&] { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - const auto q2 = builder.allocQubit(); - builder.cswap(q0, q1, q2); - builder.dealloc(q0); - builder.dealloc(q1); - builder.dealloc(q2); - return builder.finalize(); + return mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::singleControlledSwap); }, "x,sx,rz,cx"); } diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_scoring.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_scoring.cpp index 330e9a091d..cb4930871e 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_scoring.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_scoring.cpp @@ -222,44 +222,35 @@ TEST(NativeSynthesisScoringTest, TEST_F(NativeSynthesisPassTest, XxPlusMinusYyEmittedCountsMatchScoringMetrics) { using namespace mlir::qco::native_synth; - const auto runRewriteCase = [&](auto emitTwoQGate) { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - emitTwoQGate(builder, q0, q1); - builder.dealloc(q0); - builder.dealloc(q1); - OwningOpRef module = builder.finalize(); - - PassManager pm(context.get()); - pm.addPass(createQCToQCO()); - ASSERT_TRUE(succeeded(pm.run(*module))); - - Operation* twoQOp = nullptr; - module->walk([&](Operation* op) { - if (isa(op)) { - twoQOp = op; - return WalkResult::interrupt(); - } - return WalkResult::advance(); - }); - ASSERT_NE(twoQOp, nullptr); - - IRRewriter rewriter(context.get()); - ASSERT_TRUE(succeeded(rewriteXXPlusMinusYYViaRxxRyy(rewriter, twoQOp))); - - const auto expected = xxPlusMinusYyRzzRewriteScoringMetrics(); - const auto [numOneQ, numTwoQ] = - countSingleAndTwoQubitUnitariesForXxRzzMetrics(*module); - EXPECT_EQ(numOneQ, expected.numOneQ); - EXPECT_EQ(numTwoQ, expected.numTwoQ); - }; - - runRewriteCase([](mlir::qc::QCProgramBuilder& b, Value q0, Value q1) { - b.xx_plus_yy(0.52, -0.14, q0, q1); - }); - runRewriteCase([](mlir::qc::QCProgramBuilder& b, Value q0, Value q1) { - b.xx_minus_yy(-0.37, 0.26, q0, q1); - }); + const auto runRewriteCase = + [&](void (*emitProgram)(mlir::qc::QCProgramBuilder&)) { + OwningOpRef module = + mlir::qc::QCProgramBuilder::build(context.get(), emitProgram); + + PassManager pm(context.get()); + pm.addPass(createQCToQCO()); + ASSERT_TRUE(succeeded(pm.run(*module))); + + Operation* twoQOp = nullptr; + module->walk([&](Operation* op) { + if (isa(op)) { + twoQOp = op; + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + ASSERT_NE(twoQOp, nullptr); + + IRRewriter rewriter(context.get()); + ASSERT_TRUE(succeeded(rewriteXXPlusMinusYYViaRxxRyy(rewriter, twoQOp))); + + const auto expected = xxPlusMinusYyRzzRewriteScoringMetrics(); + const auto [numOneQ, numTwoQ] = + countSingleAndTwoQubitUnitariesForXxRzzMetrics(*module); + EXPECT_EQ(numOneQ, expected.numOneQ); + EXPECT_EQ(numTwoQ, expected.numTwoQ); + }; + + runRewriteCase(mlir::qc::nativeSynthScoringXxPlusYyOnly); + runRewriteCase(mlir::qc::nativeSynthScoringXxMinusYyOnly); } diff --git a/mlir/unittests/programs/qc_programs.cpp b/mlir/unittests/programs/qc_programs.cpp index fc896ce45e..21bc9c2b6e 100644 --- a/mlir/unittests/programs/qc_programs.cpp +++ b/mlir/unittests/programs/qc_programs.cpp @@ -11,6 +11,7 @@ #include "qc_programs.h" #include "mlir/Dialect/QC/Builder/QCProgramBuilder.h" +#include "mlir/IR/Value.h" #include @@ -1280,4 +1281,522 @@ void invCtrlSandwich(QCProgramBuilder& b) { }); } +namespace { + +void emitNativeSynthControlledPhase(QCProgramBuilder& b, const double theta, + mlir::Value ctrl, mlir::Value tgt) { + b.p(theta / 2.0, ctrl); + b.cx(ctrl, tgt); + b.p(-theta / 2.0, tgt); + b.cx(ctrl, tgt); + b.p(theta / 2.0, tgt); +} + +void emitNativeSynthToffoli(QCProgramBuilder& b, mlir::Value c1, mlir::Value c2, + mlir::Value t) { + b.h(t); + b.cx(c2, t); + b.tdg(t); + b.cx(c1, t); + b.t(t); + b.cx(c2, t); + b.tdg(t); + b.cx(c1, t); + b.t(c2); + b.t(t); + b.h(t); + b.cx(c1, c2); + b.t(c1); + b.tdg(c2); + b.cx(c1, c2); +} + +/// Shared by ``nativeSynthBroadOneQCanonicalization`` and +/// ``nativeSynthIbmFractionalAllGateFamilies``: wide 1q sweep on two qubits, +/// ending before any two-qubit primitive. +void emitNativeSynthFixtureBroad1qPrefix(QCProgramBuilder& b, mlir::Value q0, + mlir::Value q1) { + b.id(q0); + b.x(q0); + b.y(q1); + b.z(q0); + b.h(q1); + b.s(q0); + b.sdg(q1); + b.t(q0); + b.tdg(q1); + b.sx(q0); + b.sxdg(q1); + b.rx(0.13, q0); + b.ry(-0.47, q1); + b.rz(0.29, q0); + b.p(-0.38, q1); + b.r(0.61, -0.22, q0); +} + +void emitNativeSynthFiveQStressLayers(QCProgramBuilder& b, + const int numLayers) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + const auto q2 = b.allocQubit(); + const auto q3 = b.allocQubit(); + const auto q4 = b.allocQubit(); + b.h(q0); + b.s(q1); + b.t(q2); + b.y(q3); + b.h(q4); + b.cx(q0, q1); + b.cz(q1, q2); + b.swap(q2, q3); + b.cx(q3, q4); + for (int layer = 0; layer < numLayers; ++layer) { + b.h(q0); + b.s(q0); + b.t(q0); + b.y(q1); + b.h(q2); + b.s(q3); + b.t(q4); + b.cx(q0, q2); + b.cz(q1, q3); + b.cx(q2, q4); + if ((layer % 2) == 0) { + b.swap(q0, q1); + b.swap(q3, q4); + } else { + b.cx(q4, q0); + b.cz(q2, q1); + } + } + b.p(0.25, q0); + b.p(-0.5, q2); + b.p(0.75, q4); +} + +void emitNativeSynthTwoQRzx(QCProgramBuilder& b, const double theta, + const bool controlOnFirstWire) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + if (controlOnFirstWire) { + b.rzx(theta, q0, q1); + } else { + b.rzx(theta, q1, q0); + } +} + +} // namespace + +void nativeSynthBroadOneQCanonicalization(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + emitNativeSynthFixtureBroad1qPrefix(b, q0, q1); + b.cz(q0, q1); +} + +void nativeSynthZeroAngleCanonicalization(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.rx(0.0, q0); + b.ry(0.0, q1); + b.rz(0.0, q0); + b.p(0.0, q1); + b.r(0.0, 0.0, q0); + b.cz(q0, q1); +} + +void nativeSynthIbmFractionalAllGateFamilies(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + emitNativeSynthFixtureBroad1qPrefix(b, q0, q1); + b.cx(q0, q1); + b.cz(q1, q0); + b.swap(q0, q1); + b.iswap(q0, q1); + b.dcx(q0, q1); + b.ecr(q0, q1); + b.rxx(0.17, q0, q1); + b.ryy(-0.21, q0, q1); + b.rzx(0.41, q0, q1); + b.rzz(-0.33, q0, q1); + b.xx_plus_yy(0.52, -0.14, q0, q1); + b.xx_minus_yy(-0.37, 0.26, q0, q1); +} + +void nativeSynthFusionHadamardZHadamard(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + b.h(q0); + b.z(q0); + b.h(q0); +} + +void nativeSynthFusionHadamardHadamard(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + b.h(q0); + b.h(q0); +} + +void nativeSynthFusionMixedChainHSTYSX(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + b.h(q0); + b.s(q0); + b.t(q0); + b.y(q0); + b.sx(q0); +} + +void nativeSynthFusionHadamardCxHadamard(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.cx(q0, q1); + b.h(q0); +} + +void nativeSynthFusionHadamardBarrierHadamard(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + b.h(q0); + b.barrier({q0}); + b.h(q0); +} + +void nativeSynthFusionRzSxRz(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + b.rz(0.4, q0); + b.sx(q0); + b.rz(-0.9, q0); +} + +void nativeSynthFusionUUTwoQGenericU3(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + b.u(0.3, 0.1, -0.2, q0); + b.u(-0.5, 0.7, 0.4, q0); +} + +void nativeSynthFusionTS(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + b.t(q0); + b.s(q0); +} + +void nativeSynthFusionUUTwoQDet1(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + b.u(0.3, 0.2, -0.2, q0); + b.u(0.5, 0.4, -0.4, q0); +} + +void nativeSynthFusionLongMixedTenOpCx(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.t(q0); + b.rx(0.37, q0); + b.s(q0); + b.ry(-0.21, q0); + b.h(q0); + b.z(q0); + b.rz(0.52, q0); + b.sx(q0); + b.y(q0); + b.cx(q0, q1); +} + +void nativeSynthFusionCxCx(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.cx(q0, q1); + b.cx(q0, q1); +} + +void nativeSynthFusionHCxInterleavedTCx(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.cx(q0, q1); + b.t(q1); + b.s(q0); + b.cx(q0, q1); +} + +void nativeSynthFusionThreeLineCx01Cx12Cx01(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + const auto q2 = b.allocQubit(); + b.cx(q0, q1); + b.cx(q1, q2); + b.cx(q0, q1); +} + +void nativeSynthFusionCxBarrierCx(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.cx(q0, q1); + b.barrier({q0, q1}); + b.cx(q0, q1); +} + +void nativeSynthFusionSwapCxPattern(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.cx(q0, q1); + b.cx(q1, q0); + b.cx(q0, q1); +} + +void nativeSynthFusionHDcxSCx(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.dcx(q0, q1); + b.s(q1); + b.cx(q0, q1); +} + +void nativeSynthFusionXRzxTCx(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.x(q0); + b.rzx(0.41, q0, q1); + b.t(q1); + b.cx(q0, q1); +} + +void nativeSynthFusionHRzzSRzz(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.rzz(-0.29, q0, q1); + b.s(q1); + b.rzz(0.17, q0, q1); +} + +void nativeSynthFusionRzx041Q0First(QCProgramBuilder& b) { + emitNativeSynthTwoQRzx(b, 0.41, /*controlOnFirstWire=*/true); +} + +void nativeSynthFusionRzx041Q1First(QCProgramBuilder& b) { + emitNativeSynthTwoQRzx(b, 0.41, /*controlOnFirstWire=*/false); +} + +void nativeSynthFusionRzxPiHalfQ0First(QCProgramBuilder& b) { + emitNativeSynthTwoQRzx(b, std::numbers::pi / 2.0, + /*controlOnFirstWire=*/true); +} + +void nativeSynthFusionRzxPiHalfQ1First(QCProgramBuilder& b) { + emitNativeSynthTwoQRzx(b, std::numbers::pi / 2.0, + /*controlOnFirstWire=*/false); +} + +void nativeSynthProfilesHstycxTwoQ(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.s(q0); + b.t(q0); + b.y(q0); + b.cx(q0, q1); +} + +void nativeSynthProfilesHCxTOnQ1(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q1); + b.cx(q0, q1); + b.t(q1); +} + +void nativeSynthProfilesXYSXCz(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.x(q0); + b.y(q0); + b.sx(q0); + b.cz(q0, q1); +} + +void nativeSynthProfilesFractionalChain(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.ry(0.37, q0); + b.sxdg(q0); + b.cx(q0, q1); + b.rzz(0.23, q0, q1); +} + +void nativeSynthProfilesHYcx(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.y(q0); + b.cx(q0, q1); +} + +void nativeSynthProfilesZCx(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.z(q0); + b.cx(q0, q1); +} + +void nativeSynthProfilesXHCz(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.x(q0); + b.h(q0); + b.cz(q0, q1); +} + +void nativeSynthProfilesCxYOnQ1(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.cx(q0, q1); + b.y(q1); +} + +void nativeSynthProfilesHq0Yq1CxSq0(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.y(q1); + b.cx(q0, q1); + b.s(q0); +} + +void nativeSynthProfilesHCxSq1(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.cx(q0, q1); + b.s(q1); +} + +void nativeSynthProfilesHYSameWireCxSq1(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.y(q0); + b.cx(q0, q1); + b.s(q1); +} + +void nativeSynthProfilesPhaseHCxPhase(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.p(0.13, q0); + b.h(q0); + b.cx(q0, q1); + b.p(-0.27, q1); +} + +void nativeSynthProfilesLargeFiveQStressEightLayers(QCProgramBuilder& b) { + emitNativeSynthFiveQStressLayers(b, /*numLayers=*/8); +} + +void nativeSynthMultiQThreeQGhz(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + const auto q2 = b.allocQubit(); + b.h(q0); + b.cx(q0, q1); + b.cx(q1, q2); +} + +void nativeSynthMultiQThreeQToffoli(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + const auto q2 = b.allocQubit(); + emitNativeSynthToffoli(b, q0, q1, q2); +} + +void nativeSynthMultiQThreeQQft(QCProgramBuilder& b) { + using std::numbers::pi; + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + const auto q2 = b.allocQubit(); + b.h(q2); + emitNativeSynthControlledPhase(b, pi / 2.0, q1, q2); + b.h(q1); + emitNativeSynthControlledPhase(b, pi / 4.0, q0, q2); + emitNativeSynthControlledPhase(b, pi / 2.0, q0, q1); + b.h(q0); + b.cx(q0, q2); + b.cx(q2, q0); + b.cx(q0, q2); +} + +void nativeSynthMultiQThreeQCliffordTMix(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + const auto q2 = b.allocQubit(); + b.h(q0); + b.t(q1); + b.x(q2); + b.cx(q0, q1); + b.rz(0.37, q2); + b.cz(q1, q2); + b.sdg(q0); + b.ry(-0.42, q1); + b.cx(q2, q0); + b.y(q1); + b.tdg(q2); + b.cx(q0, q1); + b.p(0.21, q2); + b.h(q2); + b.cz(q0, q2); + b.rx(-0.13, q1); + b.s(q0); +} + +void nativeSynthMultiQFiveQStressFourLayers(QCProgramBuilder& b) { + emitNativeSynthFiveQStressLayers(b, /*numLayers=*/4); +} + +void nativeSynthCustomMenusIbmFractionalTwoQStress(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.sxdg(q0); + b.ry(-0.22, q1); + b.swap(q0, q1); + b.rxx(0.53, q0, q1); + b.ecr(q0, q1); + b.p(0.31, q0); + b.rzz(-0.44, q0, q1); +} + +void nativeSynthCustomMenusXxPlusYyChain(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.sx(q1); + b.xx_plus_yy(0.52, -0.14, q0, q1); + b.rz(0.31, q0); +} + +void nativeSynthCustomMenusXxMinusYyOnly(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.xx_minus_yy(-0.37, 0.26, q0, q1); +} + +void nativeSynthScoringXxPlusYyOnly(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.xx_plus_yy(0.52, -0.14, q0, q1); +} + +void nativeSynthScoringXxMinusYyOnly(QCProgramBuilder& b) { + nativeSynthCustomMenusXxMinusYyOnly(b); +} + +void nativeSynthDeterminismTwoQubitSwap(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.swap(q0, q1); + b.dealloc(q0); + b.dealloc(q1); +} + } // namespace mlir::qc diff --git a/mlir/unittests/programs/qc_programs.h b/mlir/unittests/programs/qc_programs.h index 2539014bf2..4dbc123de4 100644 --- a/mlir/unittests/programs/qc_programs.h +++ b/mlir/unittests/programs/qc_programs.h @@ -833,4 +833,173 @@ void tripleNestedInv(QCProgramBuilder& b); /// Creates a circuit with inverse modifiers interleaved by a control modifier. void invCtrlSandwich(QCProgramBuilder& b); + +// --- Native gate synthesis (mlir/unittests/.../NativeSynthesis) ----------- // + +/// Wide single-qubit sweep on two qubits, then ``cz``; exercises broad 1q +/// canonicalization without entangler-specific shortcuts. +void nativeSynthBroadOneQCanonicalization(QCProgramBuilder& b); + +/// Degenerate rotations (all zero angles) on two qubits then ``cz``; checks +/// that trivial phases collapse cleanly on phase-sensitive menus. +void nativeSynthZeroAngleCanonicalization(QCProgramBuilder& b); + +/// Same 1q prefix as ``nativeSynthBroadOneQCanonicalization`` followed by a +/// representative mix of two-qubit primitives (CX, CZ, SWAP, iSWAP, DCX, ECR, +/// Pauli rotations, RZZ, XX±YY); used for IBM-fractional-style coverage. +void nativeSynthIbmFractionalAllGateFamilies(QCProgramBuilder& b); + +/// Single wire: ``H Z H`` (fusion should collapse toward ``X`` on IBM-style +/// menus). +void nativeSynthFusionHadamardZHadamard(QCProgramBuilder& b); + +/// Single wire: adjacent ``H H`` (identity run; fusion should remove 1q ops). +void nativeSynthFusionHadamardHadamard(QCProgramBuilder& b); + +/// Single wire: ``H S T Y SX`` mixed non-native chain for generic-U3 fusion. +void nativeSynthFusionMixedChainHSTYSX(QCProgramBuilder& b); + +/// Two qubits: ``H`` on control, ``CX``, ``H`` on control; 1q runs must not +/// fuse across the entangler. +void nativeSynthFusionHadamardCxHadamard(QCProgramBuilder& b); + +/// ``H``, barrier, ``H`` on one wire; barrier must break 1q-run merging. +void nativeSynthFusionHadamardBarrierHadamard(QCProgramBuilder& b); + +/// Fully native IBM-style ``rz; sx; rz`` triple on one wire (cost-gate / +/// skip-fully-native path). +void nativeSynthFusionRzSxRz(QCProgramBuilder& b); + +/// Two adjacent native ``U`` on one wire (generic ``u,cx`` profile; cost-gate +/// fuses to one ``U``). +void nativeSynthFusionUUTwoQGenericU3(QCProgramBuilder& b); + +/// ``T S`` on one wire; fused SU(2) normalisation emits a non-trivial +/// ``qco.gphase`` on the generic-U3 path. +void nativeSynthFusionTS(QCProgramBuilder& b); + +/// Two ``U`` with ``lambda = -phi`` each (det=1); fused result must omit +/// ``gphase`` (trivial residual phase). +void nativeSynthFusionUUTwoQDet1(QCProgramBuilder& b); + +/// Long mixed 1q chain on ``q0`` then ``CX(q0,q1)``; profile-sweep equivalence +/// for CX-friendly menus. +void nativeSynthFusionLongMixedTenOpCx(QCProgramBuilder& b); + +/// Two identical ``CX`` on the same pair (block consolidation / cancellation). +void nativeSynthFusionCxCx(QCProgramBuilder& b); + +/// ``H``, ``CX``, interleaved 1q on both wires, ``CX``; consolidation to one +/// 4×4 block on ``u,cx``. +void nativeSynthFusionHCxInterleavedTCx(QCProgramBuilder& b); + +/// Three ``CX`` on lines ``0-1``, ``1-2``, ``0-1``; consolidation must not +/// merge across the middle pair. +void nativeSynthFusionThreeLineCx01Cx12Cx01(QCProgramBuilder& b); + +/// Two ``CX`` separated by a barrier on the pair; consolidation must not fuse +/// across the barrier. +void nativeSynthFusionCxBarrierCx(QCProgramBuilder& b); + +/// Alternating-direction ``CX`` triple (SWAP pattern) on two qubits. +void nativeSynthFusionSwapCxPattern(QCProgramBuilder& b); + +/// ``H``, ``DCX``, ``S`` on target, ``CX``; asymmetric DCX inside a block. +void nativeSynthFusionHDcxSCx(QCProgramBuilder& b); + +/// ``X``, ``RZX``, ``T`` on target, ``CX``; directional RZX inside a block. +void nativeSynthFusionXRzxTCx(QCProgramBuilder& b); + +/// ``H``, two ``RZZ`` with ``S`` on target; IBM-fractional RZZ consolidation. +void nativeSynthFusionHRzzSRzz(QCProgramBuilder& b); + +/// Standalone ``RZX(0.41)`` with control on the first allocated qubit. +void nativeSynthFusionRzx041Q0First(QCProgramBuilder& b); + +/// Standalone ``RZX(0.41)`` with control on the second allocated qubit. +void nativeSynthFusionRzx041Q1First(QCProgramBuilder& b); + +/// Standalone ``RZX(pi/2)`` with control on the first allocated qubit. +void nativeSynthFusionRzxPiHalfQ0First(QCProgramBuilder& b); + +/// Standalone ``RZX(pi/2)`` with control on the second allocated qubit. +void nativeSynthFusionRzxPiHalfQ1First(QCProgramBuilder& b); + +/// Two qubits: ``H,S,T,Y`` on ``q0`` then ``CX(q0,q1)``; profile decomposition +/// (HSTY + CX) smoke shape. +void nativeSynthProfilesHstycxTwoQ(QCProgramBuilder& b); + +/// ``H`` on target, ``CX``, ``T`` on target; CX→CZ style menus on IBM-basic +/// CZ. +void nativeSynthProfilesHCxTOnQ1(QCProgramBuilder& b); + +/// ``X``, ``Y``, ``SX`` on control, ``CZ``; IQM-style ``r,cz`` profile fixture. +void nativeSynthProfilesXYSXCz(QCProgramBuilder& b); + +/// ``H``, ``RY``, ``SXdg``, ``CX``, ``RZZ``; IBM-fractional chain profile. +void nativeSynthProfilesFractionalChain(QCProgramBuilder& b); + +/// ``H``, ``Y``, ``CX``; axis-pair ``rx,rz,cx`` profile fixture. +void nativeSynthProfilesHYcx(QCProgramBuilder& b); + +/// ``Z``, ``CX``; axis-pair ``rx,ry,cx`` (Rz decomposition) fixture. +void nativeSynthProfilesZCx(QCProgramBuilder& b); + +/// ``X``, ``H``, ``CZ``; axis-pair ``ry,rz,cz`` / generic overlap checks. +void nativeSynthProfilesXHCz(QCProgramBuilder& b); + +/// ``CX`` then ``Y`` on target; Cx→Cz / ``r,cz`` conversion on a fixed pair. +void nativeSynthProfilesCxYOnQ1(QCProgramBuilder& b); + +/// ``H(q0)``, ``Y(q1)``, ``CX``, ``S(q0)``; generic ``u,cx`` equivalence menu. +void nativeSynthProfilesHq0Yq1CxSq0(QCProgramBuilder& b); + +/// ``H``, ``CX``, ``S`` on target; custom menu with multiple entanglers +/// (``u,cx,cz``). +void nativeSynthProfilesHCxSq1(QCProgramBuilder& b); + +/// ``H``, ``Y`` on same wire as control, ``CX``, ``S`` on target; overlapping +/// one-qubit superset custom menu. +void nativeSynthProfilesHYSameWireCxSq1(QCProgramBuilder& b); + +/// Phase before/after ``H`` and ``CX``; minimal IBM-basic menu with ``p`` as +/// phase alias. +void nativeSynthProfilesPhaseHCxPhase(QCProgramBuilder& b); + +/// Five-qubit stress circuit with eight repeated layers; large multi-qubit +/// minimal-menu synthesis. +void nativeSynthProfilesLargeFiveQStressEightLayers(QCProgramBuilder& b); + +/// Three-qubit GHZ preparation (``H``, ``CX`` chain). +void nativeSynthMultiQThreeQGhz(QCProgramBuilder& b); + +/// Three-qubit Toffoli (decomposed) for multi-profile equivalence. +void nativeSynthMultiQThreeQToffoli(QCProgramBuilder& b); + +/// Three-qubit QFT-style controlled phases and permutations. +void nativeSynthMultiQThreeQQft(QCProgramBuilder& b); + +/// Three-qubit Clifford+``T``+rotations mix; moderate-depth multi-qubit sweep. +void nativeSynthMultiQThreeQCliffordTMix(QCProgramBuilder& b); + +/// Same five-qubit layer template as the eight-layer profile, but four layers. +void nativeSynthMultiQFiveQStressFourLayers(QCProgramBuilder& b); + +/// Two-qubit stress: IBM-fractional primitives (SWAP, RXX, ECR, RZZ, …). +void nativeSynthCustomMenusIbmFractionalTwoQStress(QCProgramBuilder& b); + +/// ``H``, ``SX``, ``XX+YY``, ``RZ``; custom-menu ``XX+YY`` chain behaviour. +void nativeSynthCustomMenusXxPlusYyChain(QCProgramBuilder& b); + +/// Single ``XX-YY`` on a pair; custom menu / scoring delegate shape. +void nativeSynthCustomMenusXxMinusYyOnly(QCProgramBuilder& b); + +/// Single ``XX+YY`` on a pair; scoring metrics on emitted counts. +void nativeSynthScoringXxPlusYyOnly(QCProgramBuilder& b); + +/// Forwards to ``nativeSynthCustomMenusXxMinusYyOnly``; scoring-only alias. +void nativeSynthScoringXxMinusYyOnly(QCProgramBuilder& b); + +/// Two-qubit ``swap`` with explicit ``allocQubit`` / ``dealloc`` ordering. +void nativeSynthDeterminismTwoQubitSwap(QCProgramBuilder& b); } // namespace mlir::qc From 4b5f1b2a498f6728da603572edb08cf7ed5e75b1 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 23 Apr 2026 16:39:18 +0200 Subject: [PATCH 009/122] =?UTF-8?q?=E2=9C=A8=20Enhance=20two-qubit=20gate?= =?UTF-8?q?=20sequence=20emission=20by=20adding=20support=20for=20residual?= =?UTF-8?q?=20global=20phases.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h | 6 +++++- mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h index 2c4f5f194b..7a28f411b7 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h @@ -51,7 +51,9 @@ bool getBlockTwoQubitMatrix(Operation* op, Eigen::Matrix4cd& matrix); /// Emit `seq` in order: abstract qubit id `0` → `qubit0`, id `1` → `qubit1`; /// two-qubit steps become `CtrlOp` with `XOp`/`ZOp` on the target wire (CZ is -/// symmetric). Does not replace any existing op. +/// symmetric). Does not replace any existing op and does not emit +/// ``seq.globalPhase`` (callers that use ``emitTwoQubitGateSequence`` get a +/// trailing ``qco.gphase`` from that wrapper when needed). LogicalResult emitTwoQubitGateSequenceAtLoc(IRRewriter& rewriter, Location loc, Value qubit0, Value qubit1, @@ -59,6 +61,8 @@ emitTwoQubitGateSequenceAtLoc(IRRewriter& rewriter, Location loc, Value qubit0, Value& outQubit0, Value& outQubit1); /// Emit a two-qubit gate sequence and replace `op` with the resulting tails. +/// Emits a trailing ``qco.gphase`` when ``seq`` carries a non-trivial residual +/// global phase (same contract as ``seq.getUnitaryMatrix()``). LogicalResult emitTwoQubitGateSequence(IRRewriter& rewriter, Operation* op, Value qubit0, Value qubit1, diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp index 387977954a..ea836b645f 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp @@ -263,6 +263,12 @@ emitTwoQubitGateSequence(IRRewriter& rewriter, Operation* op, Value qubit0, rewriter, op->getLoc(), qubit0, qubit1, seq, outQubit0, outQubit1))) { return failure(); } + // Match `seq.getUnitaryMatrix()` / `PassTwoQubitWindows` materialization: + // residual phase from Weyl + basis decomposition is not represented as 2q + // ops in `seq.gates`. + if (seq.hasGlobalPhase()) { + emitGPhaseIfNonTrivial(rewriter, op->getLoc(), seq.globalPhase); + } rewriter.replaceOp(op, ValueRange{outQubit0, outQubit1}); return success(); } From d719a64640cd2d09ed2086b5dbe44b37db22efe0 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Fri, 24 Apr 2026 11:09:21 +0200 Subject: [PATCH 010/122] =?UTF-8?q?=E2=9C=A8=20Euler=20sequence=20support?= =?UTF-8?q?=20for=20matrix=20synthesis=20in=20single-qubit=20operations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Transforms/NativeSynthesis/SingleQubit.h | 11 +++- .../QCO/Transforms/NativeSynthesis/Pass.cpp | 56 ++++++++++++++----- .../NativeSynthesis/SingleQubit.cpp | 51 +++++++++++++---- 3 files changed, 92 insertions(+), 26 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h index 7c3183673a..b8a1cb370f 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h @@ -17,6 +17,7 @@ #include #include +#include /// Single-qubit lowering: `decomposeTo*` for symbolic matches, plus /// `computeSynthesizedSingleQubitLength` / @@ -50,6 +51,13 @@ Value decomposeToR(IRRewriter& rewriter, Operation* op, Value inQubit); Value decomposeToAxisPair(IRRewriter& rewriter, Operation* op, Value inQubit, AxisPair axisPair); +/// Euler sequence for matrix synthesis for non-`U3` emitters (same basis as +/// `emitSynthesizedSingleQubitFromMatrix`). `nullopt` for `U3` (single `u` +/// gate, no cached Euler list) or when the axis pair has no Euler basis. +std::optional +eulerSequenceForMatrixSynthesis(const Eigen::Matrix2cd& matrix, + const SingleQubitEmitterSpec& emitter); + /// Cost estimate in number of emitted ops for fusing a single-qubit unitary /// with the given emitter. Returns `SIZE_MAX` if no Euler basis is available. std::size_t @@ -60,6 +68,7 @@ computeSynthesizedSingleQubitLength(const Eigen::Matrix2cd& matrix, /// emitted sequence carries a non-trivial residual global phase. Value emitSynthesizedSingleQubitFromMatrix( IRRewriter& rewriter, Location loc, Value inQubit, - const Eigen::Matrix2cd& matrix, const SingleQubitEmitterSpec& emitter); + const Eigen::Matrix2cd& matrix, const SingleQubitEmitterSpec& emitter, + const decomposition::QubitGateSequence* reuseEulerSeq = nullptr); } // namespace mlir::qco::native_synth diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp index bcca20b1c4..b3cae4a962 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp @@ -36,7 +36,10 @@ #include #include +#include #include +#include +#include #include namespace mlir::qco { @@ -53,13 +56,13 @@ using native_synth::collectSingleQubitCandidates; using native_synth::collectTwoQubitBasisCandidates; using native_synth::collectTwoQubitBasisCandidatesFromMatrix; using native_synth::collectUnitaryOpsInPreOrder; -using native_synth::computeSynthesizedSingleQubitLength; using native_synth::decomposeToAxisPair; using native_synth::decomposeToR; using native_synth::decomposeToU3; using native_synth::decomposeToZSXX; using native_synth::emitSynthesizedSingleQubitFromMatrix; using native_synth::emitTwoQubitGateSequence; +using native_synth::eulerSequenceForMatrixSynthesis; using native_synth::getBlockTwoQubitMatrix; using native_synth::NativeGateKind; using native_synth::NativeProfileSpec; @@ -101,17 +104,35 @@ bool maybeFuseRun(IRRewriter& rewriter, OneQubitRun& run, }); assert(!spec.singleQubitEmitters.empty() && "expected at least one emitter"); - // Single-qubit fusion intentionally uses only the first emitter: a menu - // that declares multiple single-qubit emitters (e.g. ZSXX + U3) picks the - // canonical lowering via `front()` for fusion so the rewrite is - // deterministic. Picking the cheapest emitter per run would require running - // all emitters and comparing their lengths here; today this is the same - // tradeoff as elsewhere in the pass, so we keep it simple. - const auto& emitter = spec.singleQubitEmitters.front(); - - // Fully native runs: fuse only if the emitter shortens the chain. - if (!anyNonNative && - computeSynthesizedSingleQubitLength(fused, emitter) >= run.ops.size()) { + + constexpr auto kInvalidLen = std::numeric_limits::max(); + const SingleQubitEmitterSpec* bestEmitter = nullptr; + std::size_t bestLen = kInvalidLen; + std::optional bestEuler; + for (const auto& emitter : spec.singleQubitEmitters) { + std::size_t len = 0; + std::optional euler; + if (emitter.mode == SingleQubitMode::U3) { + len = 1; + } else { + euler = eulerSequenceForMatrixSynthesis(fused, emitter); + if (!euler) { + continue; + } + len = euler->gates.size(); + } + if (bestEmitter == nullptr || len < bestLen) { + bestLen = len; + bestEmitter = &emitter; + bestEuler = std::move(euler); + } + } + if (bestEmitter == nullptr) { + return false; + } + + // Fully native runs: fuse only if some emitter strictly shortens the chain. + if (!anyNonNative && bestLen >= run.ops.size()) { return false; } @@ -120,8 +141,15 @@ bool maybeFuseRun(IRRewriter& rewriter, OneQubitRun& run, const Value outQubit = run.ops.back().getOutputQubit(0); rewriter.setInsertionPoint(firstOp); - Value replacement = emitSynthesizedSingleQubitFromMatrix( - rewriter, firstOp->getLoc(), inQubit, fused, emitter); + Value replacement; + if (bestEmitter->mode == SingleQubitMode::U3) { + replacement = emitSynthesizedSingleQubitFromMatrix( + rewriter, firstOp->getLoc(), inQubit, fused, *bestEmitter); + } else { + assert(bestEuler.has_value()); + replacement = emitSynthesizedSingleQubitFromMatrix( + rewriter, firstOp->getLoc(), inQubit, fused, *bestEmitter, &*bestEuler); + } if (!replacement) { return false; } diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp index ab2d905d55..360e5cde7f 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp @@ -18,10 +18,12 @@ #include #include +#include #include #include #include #include +#include namespace mlir::qco::native_synth { namespace { @@ -289,40 +291,58 @@ Value decomposeToU3(IRRewriter& rewriter, Operation* op, Value inQubit) { return {}; } -std::size_t -computeSynthesizedSingleQubitLength(const Eigen::Matrix2cd& matrix, - const SingleQubitEmitterSpec& emitter) { - // `U3` always emits a single gate; every other mode maps to a fixed Euler - // basis whose decomposition length we can measure directly. +std::optional +eulerSequenceForMatrixSynthesis(const Eigen::Matrix2cd& matrix, + const SingleQubitEmitterSpec& emitter) { switch (emitter.mode) { case SingleQubitMode::U3: - return 1; + return std::nullopt; case SingleQubitMode::ZSXX: - return runEuler(decomposition::EulerBasis::ZSXX, matrix).gates.size(); + return runEuler(decomposition::EulerBasis::ZSXX, matrix); case SingleQubitMode::R: - return runEuler(decomposition::EulerBasis::XYX, matrix).gates.size(); + return runEuler(decomposition::EulerBasis::XYX, matrix); case SingleQubitMode::AxisPair: { const auto bases = getEulerBasesForAxisPair(emitter.axisPair); if (bases.empty()) { - return std::numeric_limits::max(); + return std::nullopt; } - return runEuler(bases.front(), matrix).gates.size(); + return runEuler(bases.front(), matrix); } } llvm_unreachable("unknown single-qubit mode"); } +std::size_t +computeSynthesizedSingleQubitLength(const Eigen::Matrix2cd& matrix, + const SingleQubitEmitterSpec& emitter) { + if (emitter.mode == SingleQubitMode::U3) { + return 1; + } + const auto seq = eulerSequenceForMatrixSynthesis(matrix, emitter); + if (!seq) { + return std::numeric_limits::max(); + } + return seq->gates.size(); +} + Value emitSynthesizedSingleQubitFromMatrix( IRRewriter& rewriter, Location loc, Value inQubit, - const Eigen::Matrix2cd& matrix, const SingleQubitEmitterSpec& emitter) { + const Eigen::Matrix2cd& matrix, const SingleQubitEmitterSpec& emitter, + const decomposition::QubitGateSequence* reuseEulerSeq) { SingleQubitEmitter e{.rewriter = &rewriter, .loc = loc}; switch (emitter.mode) { case SingleQubitMode::ZSXX: { + if (reuseEulerSeq != nullptr) { + emitGPhaseIfNonTrivial(rewriter, loc, reuseEulerSeq->globalPhase); + return emitEulerSequenceZsxx(e, inQubit, *reuseEulerSeq); + } const auto seq = runEuler(decomposition::EulerBasis::ZSXX, matrix); emitGPhaseIfNonTrivial(rewriter, loc, seq.globalPhase); return emitEulerSequenceZsxx(e, inQubit, seq); } case SingleQubitMode::U3: { + assert(reuseEulerSeq == nullptr && + "U3 matrix emission does not use a cached Euler sequence"); using namespace std::complex_literals; // Project `matrix` into SU(2) before running the Euler decomposition. @@ -342,6 +362,10 @@ Value emitSynthesizedSingleQubitFromMatrix( return e.u(inQubit, angles[0], angles[1], angles[2]); } case SingleQubitMode::R: { + if (reuseEulerSeq != nullptr) { + emitGPhaseIfNonTrivial(rewriter, loc, reuseEulerSeq->globalPhase); + return emitEulerSequenceR(e, inQubit, *reuseEulerSeq); + } const auto seq = runEuler(decomposition::EulerBasis::XYX, matrix); emitGPhaseIfNonTrivial(rewriter, loc, seq.globalPhase); return emitEulerSequenceR(e, inQubit, seq); @@ -351,6 +375,11 @@ Value emitSynthesizedSingleQubitFromMatrix( if (bases.empty()) { return {}; } + if (reuseEulerSeq != nullptr) { + emitGPhaseIfNonTrivial(rewriter, loc, reuseEulerSeq->globalPhase); + return emitEulerSequenceAxisPair(e, inQubit, emitter.axisPair, + *reuseEulerSeq); + } const auto seq = runEuler(bases.front(), matrix); emitGPhaseIfNonTrivial(rewriter, loc, seq.globalPhase); return emitEulerSequenceAxisPair(e, inQubit, emitter.axisPair, seq); From 0597cee6e166e9c01538b0743c8cfca69d444f97 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Fri, 24 Apr 2026 13:53:14 +0200 Subject: [PATCH 011/122] =?UTF-8?q?=E2=9C=A8=20Refactor=20parameter=20orde?= =?UTF-8?q?ring=20for=20U=20and=20U2=20gates=20in=20decomposition=20and=20?= =?UTF-8?q?synthesis=20utilities?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/NativeSynthesis/Utils.h | 16 ++++-- .../Decomposition/EulerDecomposition.cpp | 2 +- .../Decomposition/UnitaryMatrices.cpp | 11 ++-- .../QCO/Transforms/NativeSynthesis/Utils.cpp | 50 ++++++++++++++++--- 4 files changed, 61 insertions(+), 18 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h index 7a28f411b7..b31d544b9b 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h @@ -49,11 +49,17 @@ bool getNormalizedTwoQubitMatrix(UnitaryOpInterface unitary, /// for barriers, ``gphase``, multi-control, or non-constant matrix parameters. bool getBlockTwoQubitMatrix(Operation* op, Eigen::Matrix4cd& matrix); -/// Emit `seq` in order: abstract qubit id `0` → `qubit0`, id `1` → `qubit1`; -/// two-qubit steps become `CtrlOp` with `XOp`/`ZOp` on the target wire (CZ is -/// symmetric). Does not replace any existing op and does not emit -/// ``seq.globalPhase`` (callers that use ``emitTwoQubitGateSequence`` get a -/// trailing ``qco.gphase`` from that wrapper when needed). +/// Emit `seq` in order: abstract qubit id `0` → `qubit0`, id `1` → `qubit1`. +/// +/// Supported two-qubit ``GateKind``s: ``RZZ`` and controlled Pauli ``X``/``Z`` +/// (``CtrlOp`` wrapping ``XOp``/``ZOp``; CZ is symmetric in the controls). +/// +/// Single-qubit steps support ``I``, ``U``, ``U2``, ``SX``, ``X``, ``RX``, +/// ``RY``, ``RZ``. +/// +/// Does not replace any existing op and does not emit ``seq.globalPhase`` +/// (callers that use ``emitTwoQubitGateSequence`` get a trailing ``qco.gphase`` +/// from that wrapper when needed). LogicalResult emitTwoQubitGateSequenceAtLoc(IRRewriter& rewriter, Location loc, Value qubit0, Value qubit1, diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.cpp index 83139642f6..542439b045 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.cpp @@ -49,7 +49,7 @@ EulerDecomposition::generateCircuit(EulerBasis targetBasis, [[fallthrough]]; case EulerBasis::U321: return OneQubitGateSequence{ - .gates = {{.type = GateKind::U, .parameter = {lambda, phi, theta}}}, + .gates = {{.type = GateKind::U, .parameter = {theta, phi, lambda}}}, .globalPhase = phase - ((phi + lambda) / 2.), }; case EulerBasis::ZSX: diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp index 30a0ac7f98..d6f1f1e2c7 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp @@ -151,13 +151,16 @@ Eigen::Matrix2cd getSingleQubitMatrix(const Gate& gate) { } if (gate.type == GateKind::U) { assert(gate.parameter.size() == 3); - // EulerDecomposition stores `U` parameters as {lambda, phi, theta}. - return uMatrix(gate.parameter[2], gate.parameter[1], gate.parameter[0]); + const double theta = gate.parameter[0]; + const double phi = gate.parameter[1]; + const double lambda = gate.parameter[2]; + return uMatrix(theta, phi, lambda); } if (gate.type == GateKind::U2) { assert(gate.parameter.size() == 2); - // `U2` parameters are stored as {lambda, phi}. - return u2Matrix(gate.parameter[1], gate.parameter[0]); + const double phi = gate.parameter[0]; + const double lambda = gate.parameter[1]; + return u2Matrix(phi, lambda); } if (gate.type == GateKind::H) { return H_GATE; diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp index ea836b645f..12fe702ce8 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp @@ -134,12 +134,19 @@ emitSingleQubitStep(IRRewriter& rewriter, Location loc, if (gate.parameter.size() != 3) { return failure(); } - // EulerDecomposition emits `U` with parameters = {lambda, phi, theta} - // whereas `UOp` takes (theta, phi, lambda); reorder accordingly. target = - record(UOp::create(rewriter, loc, target, emitConst(gate.parameter[2]), + record(UOp::create(rewriter, loc, target, emitConst(gate.parameter[0]), emitConst(gate.parameter[1]), - emitConst(gate.parameter[0]))) + emitConst(gate.parameter[2]))) + .getOutputQubit(0); + return success(); + case decomposition::GateKind::U2: + if (gate.parameter.size() != 2) { + return failure(); + } + target = + record(U2Op::create(rewriter, loc, target, emitConst(gate.parameter[0]), + emitConst(gate.parameter[1]))) .getOutputQubit(0); return success(); case decomposition::GateKind::SX: @@ -206,10 +213,37 @@ emitTwoQubitGateSequenceAtLoc(IRRewriter& rewriter, Location loc, Value qubit0, continue; } - const bool isCxOrCz = - gate.qubitId.size() == 2 && (gate.type == decomposition::GateKind::X || - gate.type == decomposition::GateKind::Z); - if (!isCxOrCz) { + if (gate.qubitId.size() != 2) { + rollbackInsertedOps(rewriter, insertedOps); + return failure(); + } + + if (gate.type == decomposition::GateKind::RZZ) { + if (gate.parameter.size() != 1) { + rollbackInsertedOps(rewriter, insertedOps); + return failure(); + } + const decomposition::QubitId a = gate.qubitId[0]; + const decomposition::QubitId b = gate.qubitId[1]; + if (a + b != 1) { + rollbackInsertedOps(rewriter, insertedOps); + return failure(); + } + const Value va = (a == 0) ? qubit0 : qubit1; + const Value vb = (b == 0) ? qubit0 : qubit1; + Value thetaVal = createF64Const(rewriter, loc, gate.parameter[0]); + insertedOps.push_back(thetaVal.getDefiningOp()); + auto rzz = RZZOp::create(rewriter, loc, va, vb, thetaVal); + insertedOps.push_back(rzz.getOperation()); + qubit0 = (gate.qubitId[0] == 0) ? rzz.getOutputQubit(0) + : rzz.getOutputQubit(1); + qubit1 = (gate.qubitId[0] == 1) ? rzz.getOutputQubit(0) + : rzz.getOutputQubit(1); + continue; + } + + if (gate.type != decomposition::GateKind::X && + gate.type != decomposition::GateKind::Z) { rollbackInsertedOps(rewriter, insertedOps); return failure(); } From b8ff476292dac690e8bfb966c4638a815ef1f207 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Fri, 24 Apr 2026 14:23:32 +0200 Subject: [PATCH 012/122] =?UTF-8?q?=F0=9F=93=9D=20Clean=20up=20documentati?= =?UTF-8?q?on=20and=20comments.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Transforms/NativeSynthesis/NativeSpec.h | 4 +--- .../NativeSynthesis/PassTwoQubitWindows.h | 11 ++++----- .../QCO/Transforms/NativeSynthesis/Policy.h | 3 +-- .../QCO/Transforms/NativeSynthesis/Scoring.h | 3 +-- .../Transforms/NativeSynthesis/SingleQubit.h | 4 ++-- .../QCO/Transforms/NativeSynthesis/TwoQubit.h | 7 ++---- .../QCO/Transforms/NativeSynthesis/Types.h | 10 ++------ .../QCO/Transforms/NativeSynthesis/Utils.h | 14 +---------- .../mlir/Dialect/QCO/Transforms/Passes.h | 3 +-- .../mlir/Dialect/QCO/Transforms/Passes.td | 7 ------ .../Transforms/NativeSynthesis/NativeSpec.cpp | 23 +++++-------------- .../QCO/Transforms/NativeSynthesis/Pass.cpp | 18 +++++---------- .../NativeSynthesis/PassTwoQubitWindows.cpp | 3 +-- .../QCO/Transforms/NativeSynthesis/Policy.cpp | 3 +-- .../NativeSynthesis/SingleQubit.cpp | 12 +++------- .../Transforms/NativeSynthesis/TwoQubit.cpp | 12 +++------- .../QCO/Transforms/NativeSynthesis/Utils.cpp | 9 ++------ 17 files changed, 37 insertions(+), 109 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h index 3136f85ae0..4cb0f1f759 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h @@ -27,9 +27,7 @@ llvm::SmallVector getEulerBasesForAxisPair(AxisPair axisPair); /// Resolve a comma-separated native gate menu (e.g. `"x,sx,rz,cx"`) into a -/// full `NativeProfileSpec`. Returns `std::nullopt` if the menu is empty, -/// contains unknown tokens, or cannot be covered by any supported -/// single-qubit synthesis strategy. +/// full `NativeProfileSpec`. /// /// Recognised tokens: `u`, `x`, `sx`, `rz` (or `p`), `rx`, `ry`, `r`, /// `cx`, `cz`, `rzz`. diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.h index 9648803f37..a7dff9bb4b 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.h @@ -9,8 +9,7 @@ */ /// \file -/// Helpers for `NativeGateSynthesisPass` two-qubit window consolidation. Not -/// a stable public API; kept in-tree for reuse by the pass (and its tests). +/// Helpers for `NativeGateSynthesisPass` two-qubit window consolidation. #pragma once @@ -42,8 +41,7 @@ struct TwoQubitBlock { void collectUnitaryOpsInPreOrder(Operation* root, llvm::SmallVectorImpl& ops); -/// Tracks overlapping two-qubit windows on a module slice; implemented in -/// ``NativeSynthesis/PassTwoQubitWindows.cpp``. +/// Tracks overlapping two-qubit windows on a module slice. struct TwoQubitWindowConsolidator { /// Append-only list of windows discovered so far; closed windows are kept /// so `materialize()` can still rewrite them. @@ -53,7 +51,7 @@ struct TwoQubitWindowConsolidator { llvm::DenseMap wireToBlock; /// Mark block `idx` as closed and remove its tracked wires from - /// `wireToBlock`. Idempotent: closing an already-closed block is a no-op. + /// `wireToBlock`. void closeBlock(size_t idx); /// If `v` is currently tracked, close the block that owns it; otherwise @@ -62,8 +60,7 @@ struct TwoQubitWindowConsolidator { /// State-machine step for one IR op, called in pre-order walk order. /// Extends an existing window, starts a fresh one, or closes conflicting - /// windows depending on the op's kind and operand use pattern. See the - /// definition for the full decision table. + /// windows depending on the op's kind and operand use pattern. void process(Operation* op, const NativeProfileSpec& spec); /// Rewrite each collected window whose accumulated unitary can be diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h index 7707cc968c..9e19ce6b8c 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h @@ -30,8 +30,7 @@ bool usesCxEntangler(const NativeProfileSpec& spec); bool usesCzEntangler(const NativeProfileSpec& spec); /// Whether an already-lowered single-qubit op is in the menu (i.e. no -/// further rewrite needed). `BarrierOp` / `GPhaseOp` always pass through -/// unchanged. +/// further rewrite needed). bool allowsSingleQubitOp(UnitaryOpInterface op, const NativeProfileSpec& spec); /// Count 1q/2q gates and compute the depth of a gate sequence. diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Scoring.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Scoring.h index eb03f69a60..243506fdef 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Scoring.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Scoring.h @@ -53,8 +53,7 @@ CandidateScore scoreCandidate(const SynthesisCandidate& candidate, } /// Strict less-than: `true` iff `lhs` is a strictly better candidate than -/// `rhs`. Weighted costs within `1e-12` are treated as equal, so -/// floating-point noise does not flip the decision. +/// `rhs`. inline bool isBetterScore(const CandidateScore& lhs, const CandidateScore& rhs) { constexpr double scoreTolerance = 1e-12; diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h index b8a1cb370f..8ea96fa94e 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h @@ -59,12 +59,12 @@ eulerSequenceForMatrixSynthesis(const Eigen::Matrix2cd& matrix, const SingleQubitEmitterSpec& emitter); /// Cost estimate in number of emitted ops for fusing a single-qubit unitary -/// with the given emitter. Returns `SIZE_MAX` if no Euler basis is available. +/// with the given emitter. std::size_t computeSynthesizedSingleQubitLength(const Eigen::Matrix2cd& matrix, const SingleQubitEmitterSpec& emitter); -/// Emit the fused `2×2` unitary as native ops, inserting a `qco.gphase` if the +/// Emit the fused `2×2` unitary as native ops, inserting a global phase if the /// emitted sequence carries a non-trivial residual global phase. Value emitSynthesizedSingleQubitFromMatrix( IRRewriter& rewriter, Location loc, Value inQubit, diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h index cbfce72f93..5a4b6e44bc 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h @@ -33,8 +33,7 @@ bool gateSequenceFitsMenu(const decomposition::TwoQubitGateSequence& seq, /// Decompose a `4×4` target unitary into a gate sequence targeting the given /// entangler basis, using `TwoQubitWeylDecomposition` + -/// `TwoQubitBasisDecomposer` with the supplied Euler basis and optional -/// basis-use count. +/// `TwoQubitBasisDecomposer` with the supplied Euler basis. std::optional decomposeTwoQubitFromMatrix(const Eigen::Matrix4cd& matrix, EntanglerBasis entangler, @@ -58,12 +57,10 @@ collectTwoQubitBasisCandidates(UnitaryOpInterface unitary, const NativeProfileSpec& spec); /// Scoring metrics for the `rewriteXXPlusMinusYYViaRxxRyy` lowering (both -/// `XXPlusYY` and `XXMinusYY` branches emit the same gate counts). Keep in -/// sync when changing that rewrite. +/// `XXPlusYY` and `XXMinusYY` branches emit the same gate counts). CandidateMetrics xxPlusMinusYyRzzRewriteScoringMetrics(); /// Rewrite `XXPlusYY` / `XXMinusYY` via two `RZZ` blocks (menus with `rzz`). -/// Sets `rewriter`'s insertion point to `op` before emitting. LogicalResult rewriteXXPlusMinusYYViaRxxRyy(IRRewriter& rewriter, Operation* op); diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h index c9d4390032..2e9c9407a8 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h @@ -40,16 +40,12 @@ enum class SingleQubitMode : std::uint8_t { AxisPair, }; -/// Two-qubit entangling basis selected by a profile. `None` means the menu -/// does not provide any entangler and two-qubit ops cannot be synthesized. +/// Two-qubit entangling basis selected by a profile. enum class EntanglerBasis : std::uint8_t { None, Cx, Cz }; /// Profile-level classification of a native gate. Used both to describe the /// menu (`NativeProfileSpec::allowedGates`) and to classify already-lowered -/// output ops in policy checks. One-to-one with a recognised menu token. -/// -/// The tokens `rz` and `p` are aliases and both map to `Rz` during menu -/// resolution (see `NativeSpec.cpp`). +/// output ops in policy checks. enum class NativeGateKind : std::uint8_t { U, X, @@ -80,7 +76,6 @@ struct SingleQubitEmitterSpec { /// Built by `resolveNativeGatesSpec`. struct NativeProfileSpec { bool allowRzz = false; - /// Flattened menu; used for cheap "is this op already native?" checks. llvm::DenseSet allowedGates; llvm::SmallVector singleQubitEmitters; llvm::SmallVector entanglerBases; @@ -113,7 +108,6 @@ enum class CandidateClass : std::uint8_t { }; /// Generic candidate wrapper carrying a typed rewrite plan payload. -/// `enumerationIndex` makes the candidate ordering stable across runs. template struct SynthesisCandidate { CandidateClass candidateClass = CandidateClass::NativePassthrough; CandidateMetrics metrics; diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h index b31d544b9b..fe55a62a07 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h @@ -49,17 +49,7 @@ bool getNormalizedTwoQubitMatrix(UnitaryOpInterface unitary, /// for barriers, ``gphase``, multi-control, or non-constant matrix parameters. bool getBlockTwoQubitMatrix(Operation* op, Eigen::Matrix4cd& matrix); -/// Emit `seq` in order: abstract qubit id `0` → `qubit0`, id `1` → `qubit1`. -/// -/// Supported two-qubit ``GateKind``s: ``RZZ`` and controlled Pauli ``X``/``Z`` -/// (``CtrlOp`` wrapping ``XOp``/``ZOp``; CZ is symmetric in the controls). -/// -/// Single-qubit steps support ``I``, ``U``, ``U2``, ``SX``, ``X``, ``RX``, -/// ``RY``, ``RZ``. -/// -/// Does not replace any existing op and does not emit ``seq.globalPhase`` -/// (callers that use ``emitTwoQubitGateSequence`` get a trailing ``qco.gphase`` -/// from that wrapper when needed). +/// Emit `seq` in order. LogicalResult emitTwoQubitGateSequenceAtLoc(IRRewriter& rewriter, Location loc, Value qubit0, Value qubit1, @@ -67,8 +57,6 @@ emitTwoQubitGateSequenceAtLoc(IRRewriter& rewriter, Location loc, Value qubit0, Value& outQubit0, Value& outQubit1); /// Emit a two-qubit gate sequence and replace `op` with the resulting tails. -/// Emits a trailing ``qco.gphase`` when ``seq`` carries a non-trivial residual -/// global phase (same contract as ``seq.getUnitaryMatrix()``). LogicalResult emitTwoQubitGateSequence(IRRewriter& rewriter, Operation* op, Value qubit0, Value qubit1, diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h index 60a9216ef0..55ea67d0db 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h @@ -32,8 +32,7 @@ namespace mlir::qco { /// Options for the native gate synthesis pass. /// /// @p nativeGates is a comma-separated list of gate tokens (see `Passes.td` -/// for recognised tokens). An empty or whitespace-only string is a no-op (IR -/// unchanged). +/// for recognised tokens). struct NativeGateSynthesisOptions { std::string nativeGates; double scoreWeightTwoQ = 1.0; diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index 063a54b735..56bf8459b3 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -147,13 +147,6 @@ def NativeGateSynthesisPass : Pass<"native-gate-synthesis", "mlir::ModuleOp"> { `score-weight-twoq * #2q + score-weight-oneq * #1q + score-weight-depth * local-depth`. Defaults (`1.0 / 0.1 / 0.01`) favour minimising two-qubit count first, then single-qubit count, then depth. - - `qco.ctrl` wrappers whose body is `qco.x` or `qco.z` are left untouched - when `cx` or `cz` is on the menu; otherwise they are treated as a `4×4` - unitary and go through the same two-qubit search as bare two-qubit gates. - `qco.xx_plus_yy` / `qco.xx_minus_yy` are lowered via a dedicated - `rzz`-centric rewrite when `rzz` is on the menu, and via the general - two-qubit decomposition otherwise. }]; let options = [Option<"nativeGates", "native-gates", "std::string", "\"\"", diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.cpp index 27780230c6..9e861e79bf 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.cpp @@ -22,8 +22,7 @@ namespace mlir::qco::native_synth { namespace { /// Map a single native-gate token (lower-case, no whitespace) to its -/// `NativeGateKind`. `"p"` is accepted as an alias for `"rz"` since both -/// lower to `RZOp` in the IR. Returns `std::nullopt` for unknown tokens. +/// `NativeGateKind`. std::optional parseGateToken(llvm::StringRef name) { return llvm::StringSwitch>(name) .Case("u", NativeGateKind::U) @@ -40,9 +39,7 @@ std::optional parseGateToken(llvm::StringRef name) { } /// Parse a comma-separated native-gate menu (e.g. `"u,cx,rzz"`) into the set -/// of `NativeGateKind`s it names. Whitespace is trimmed and tokens are -/// lower-cased; empty tokens are skipped silently. Returns `std::nullopt` if -/// any non-empty token fails to parse. +/// of `NativeGateKind`s it names. std::optional> parseGateSet(llvm::StringRef nativeGates) { llvm::DenseSet gates; @@ -63,9 +60,7 @@ parseGateSet(llvm::StringRef nativeGates) { } /// Build a fully-resolved `SingleQubitEmitterSpec` for `mode`, including the -/// list of Euler bases the matrix-fallback path is allowed to use. `axisPair` -/// is only consulted for `SingleQubitMode::AxisPair`; `supportsDirectRx` is -/// only meaningful for `SingleQubitMode::ZSXX`. +/// list of Euler bases the matrix-fallback path is allowed to use. SingleQubitEmitterSpec makeEmitterSpec(SingleQubitMode mode, AxisPair axisPair = AxisPair::RxRz, bool supportsDirectRx = false) { @@ -93,8 +88,7 @@ SingleQubitEmitterSpec makeEmitterSpec(SingleQubitMode mode, } /// Append a new emitter for `(mode, axisPair, supportsDirectRx)` to -/// `emitters` iff no equivalent entry is already present. Keeps the resolved -/// list deduplicated without relying on the caller's ordering. +/// `emitters` iff no equivalent entry is already present. void addEmitterIfAbsent(llvm::SmallVectorImpl& emitters, SingleQubitMode mode, AxisPair axisPair = AxisPair::RxRz, @@ -108,9 +102,7 @@ void addEmitterIfAbsent(llvm::SmallVectorImpl& emitters, } } -/// Enumerate the native gate kinds that `emitter` may actually emit. Used -/// to build `NativeProfileSpec::allowedGates` so downstream passes can cheaply -/// test whether a concrete op belongs to the resolved menu. +/// Enumerate the native gate kinds that `emitter` may actually emit. llvm::SmallVector allowedGatesForEmitter(const SingleQubitEmitterSpec& emitter) { switch (emitter.mode) { @@ -141,7 +133,6 @@ allowedGatesForEmitter(const SingleQubitEmitterSpec& emitter) { } /// Enumerate the native entangling gate kinds that `entangler` may emit. -/// Returns an empty list for `EntanglerBasis::None`. llvm::SmallVector allowedGatesForEntangler(EntanglerBasis entangler) { switch (entangler) { @@ -156,9 +147,7 @@ allowedGatesForEntangler(EntanglerBasis entangler) { } /// Rebuild `spec.allowedGates` as the union of the gate kinds produced by -/// every resolved emitter, entangler, and (optionally) `Rzz`. Idempotent: -/// clears the set first so calling this on an already-populated spec yields -/// the same result. +/// every resolved emitter, entangler, and (optionally) `Rzz`. void populateAllowedGates(NativeProfileSpec& spec) { spec.allowedGates.clear(); for (const auto& emitter : spec.singleQubitEmitters) { diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp index b3cae4a962..cc38c5b1f1 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp @@ -201,8 +201,7 @@ Value applyDirectSingleQubitLowering(IRRewriter& rewriter, Operation* op, /// Lowers unitary QCO ops to a comma-separated native gate menu (single-qubit /// fuse, two-qubit windows, synthesis sweeps, seam single-qubit fuse, `rz` -/// through `ctrl` controls, another single-qubit fuse, optional cleanup sweeps; -/// fails if anything remains off-menu). +/// through `ctrl` controls, another single-qubit fuse, optional cleanup sweeps. struct NativeGateSynthesisPass : impl::NativeGateSynthesisPassBase { /// Default-construct the pass with the TableGen-generated option defaults. @@ -260,8 +259,7 @@ struct NativeGateSynthesisPass fuseOneQubitRuns(rewriter, spec); consolidateTwoQubitBlocks(rewriter, spec, weights); // Two-qubit lowering can emit off-menu single-qubit ops (e.g. `rx`/`ry`); - // repeat until clean or hit the sweep cap before seam / `rz` cleanup (those - // steps assume a mostly on-menu single-qubit surface for best fusion). + // repeat until clean or hit the sweep cap before seam / `rz` cleanup. constexpr unsigned kMaxSynthesisSweeps = 4; for (unsigned i = 0; i < kMaxSynthesisSweeps; ++i) { if (failed(synthesizeRemainingOps(rewriter, spec, weights))) { @@ -280,8 +278,7 @@ struct NativeGateSynthesisPass signalPassFailure(); return; } - // Fuse single-qubit seams between two-qubit blocks (`maybeFuseRun` cost - // gate). + // Fuse single-qubit seams between two-qubit blocks. fuseOneQubitRuns(rewriter, spec); // Fuse `rz` through control wires of `ctrl` (diagonal control phase). fuseRzAcrossCtrlControls(rewriter); @@ -543,8 +540,7 @@ struct NativeGateSynthesisPass collectUnitaryOpsInPreOrder(getOperation(), ops); for (Operation* op : ops) { - // Pointers were collected before this loop; erased ops must be skipped - // (`getBlock() == nullptr`). Do not rely on pointer identity alone. + // Pointers were collected before this loop. if (op->getBlock() == nullptr) { continue; } @@ -589,8 +585,7 @@ struct NativeGateSynthesisPass /// Lower one off-menu single-qubit `op`: enumerate all valid rewrite /// candidates for the active native profile, pick the best by `weights`, - /// emit it, and replace `op`. Returns `failure()` (with a diagnostic) if - /// no candidate fits the profile. + /// emit it, and replace `op`. static LogicalResult rewriteSingleQubit(IRRewriter& rewriter, Operation* op, UnitaryOpInterface unitary, const NativeProfileSpec& spec, @@ -660,8 +655,7 @@ struct NativeGateSynthesisPass /// Lower an off-menu generic two-qubit op (`RZZ`, `XXPlusYY`, `XXMinusYY`, /// or any arbitrary 4x4 unitary). Handles the `Rzz`-native fast path and /// the `XXPlusMinusYY -> Rzz` specialization first, then falls back to the - /// Weyl-based basis-decomposer search. Returns `failure()` (with a - /// diagnostic) when no candidate fits the profile. + /// Weyl-based basis-decomposer search. static LogicalResult rewriteTwoQubit(IRRewriter& rewriter, Operation* op, UnitaryOpInterface unitary, const NativeProfileSpec& spec, diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp index 1cd9a8ccad..f58150e641 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp @@ -169,8 +169,7 @@ void TwoQubitWindowConsolidator::process(Operation* op, } if (unitary.isTwoQubit()) { - // A two-qubit op for which we cannot build a 4x4 matrix (e.g. a - // multi-control `CtrlOp` with more than one control) is opaque to the + // A two-qubit op for which we cannot build a 4x4 matrix is opaque to the // window model; close any blocks on its inputs and bail out. Eigen::Matrix4cd opMatrix; if (!getBlockTwoQubitMatrix(op, opMatrix)) { diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Policy.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Policy.cpp index 53de538532..627fbd6230 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Policy.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Policy.cpp @@ -39,8 +39,7 @@ bool usesCzEntangler(const NativeProfileSpec& spec) { namespace { /// Map a single-qubit `UnitaryOpInterface` op to the `NativeGateKind` that -/// must appear in the menu for the op to be a no-op. Two-qubit kinds are -/// never valid here and therefore not returned. +/// must appear in the menu for the op to be a no-op. std::optional singleQubitNativeGateKind(UnitaryOpInterface op) { Operation* raw = op.getOperation(); if (isa(raw)) { diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp index 360e5cde7f..10116a4190 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp @@ -96,7 +96,7 @@ struct SingleQubitEmitter { }; /// Materialize an `EulerBasis::ZSXX` decomposition (`rz` / `sx` / `x`) into -/// QCO ops. Returns null on unsupported abstract gate kinds. +/// QCO ops. Value emitEulerSequenceZsxx(SingleQubitEmitter e, Value q, const decomposition::QubitGateSequence& seq) { for (const auto& gate : seq.gates) { @@ -125,7 +125,7 @@ Value emitEulerSequenceZsxx(SingleQubitEmitter e, Value q, /// Materialize an `EulerBasis::XYX` decomposition into `R(theta, phi)` ops /// for the `R` emitter: `Rx(theta)` becomes `R(theta, 0)`, `Ry(theta)` /// becomes `R(theta, pi/2)`, Pauli `X`/`Y` become `R(pi, *)`, `I` is a -/// no-op. Returns null on any unsupported abstract gate kind. +/// no-op. Value emitEulerSequenceR(SingleQubitEmitter e, Value q, const decomposition::QubitGateSequence& seq) { for (const auto& gate : seq.gates) { @@ -213,9 +213,7 @@ Value emitEulerSequenceAxisPair(SingleQubitEmitter e, Value q, AxisPair axis, } /// Decompose `matrix` numerically into a gate sequence in `basis` with -/// zero-rotations pruned (`simplify=true`). Pure forwarder around -/// `EulerDecomposition::generateCircuit` kept as a one-liner to match the -/// matrix-based fallback call sites in `decomposeTo*`. +/// zero-rotations pruned (`simplify=true`). decomposition::QubitGateSequence runEuler(decomposition::EulerBasis basis, const Eigen::Matrix2cd& matrix) { return decomposition::EulerDecomposition::generateCircuit( @@ -224,10 +222,6 @@ decomposition::QubitGateSequence runEuler(decomposition::EulerBasis basis, } // namespace -// Direct emitters only handle the gates listed in the matching -// `canDirectlyDecomposeTo*` predicate. Everything else is expected to reach the -// matrix-based Euler fallback, which produces an equivalent native sequence in -// the same basis. Value decomposeToZSXX(IRRewriter& rewriter, Operation* op, Value inQubit, bool supportsDirectRx) { if (isa(op)) { diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp index 457427023c..cb75968816 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp @@ -29,9 +29,7 @@ constexpr double HALF_PI = PI / 2.0; /// Whether the given single-qubit emitter can lower a decomposition-IR gate /// of `kind` (an intermediate from Euler/Weyl, *not* a `NativeGateKind`) to a -/// native output sequence. Kept separate from `allowsSingleQubitOp`, which -/// operates on already-lowered MLIR output ops: some intermediate kinds map -/// to different native ops (e.g. the `R` emitter lowers RX/RY via `R(θ, φ)`). +/// native output sequence. bool emitterHandlesDecompositionGate(const SingleQubitEmitterSpec& emitter, decomposition::GateKind kind) { if (kind == decomposition::GateKind::I) { @@ -97,10 +95,7 @@ bool menuAllows(const decomposition::Gate& gate, return false; } -/// Can `emitter` lower the single-qubit `op` directly (without the matrix -/// fallback)? Dispatches to the mode-specific `canDirectlyDecomposeTo*` -/// predicate; these predicates encode which abstract gate kinds each -/// emitter understands as-is. +/// Whether `emitter` can lower the single-qubit `op` directly. bool emitterHasDirectLowering(Operation* op, const SingleQubitEmitterSpec& emitter) { switch (emitter.mode) { @@ -183,8 +178,7 @@ namespace { /// Try every `numBasisUses` in `{0, 1, 2, 3}` for the `(entangler, emitter, /// basis)` triple, running the Weyl-based basis decomposer for each. Any /// resulting gate sequence that both matches `targetMatrix` up to global -/// phase AND stays inside the native menu is appended to `candidates` (with -/// a freshly-incremented `enumerationIndex` to keep scoring deterministic). +/// phase AND stays inside the native menu is appended to `candidates`. void tryAddTwoQubitBasisCandidatesForEmitterBasis( llvm::SmallVector, 0>& candidates, unsigned& enumerationIndex, const Eigen::Matrix4cd& targetMatrix, diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp index 12fe702ce8..548dc5a185 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp @@ -108,8 +108,7 @@ namespace { /// Emit a single-qubit gate from a decomposition gate, threading `target` and /// recording the inserted op (if any) in `insertedOps` so the caller can roll -/// back on failure. Returns `failure()` if the gate kind/parameter count is -/// unsupported. +/// back on failure. LogicalResult emitSingleQubitStep(IRRewriter& rewriter, Location loc, const decomposition::Gate& gate, Value& target, @@ -126,9 +125,6 @@ emitSingleQubitStep(IRRewriter& rewriter, Location loc, }; switch (gate.type) { case decomposition::GateKind::I: - // Identity is a no-op; leave the threaded `target` unchanged. Euler - // decomposers do not emit explicit identity steps today, so this case is - // kept defensively to mirror the handling in `SingleQubit.cpp`. return success(); case decomposition::GateKind::U: if (gate.parameter.size() != 3) { @@ -184,8 +180,7 @@ emitSingleQubitStep(IRRewriter& rewriter, Location loc, } } -/// Erase all ops tracked in `insertedOps` in reverse insertion order. Clears -/// the vector on return. +/// Erase all ops tracked in `insertedOps` in reverse insertion order. void rollbackInsertedOps(IRRewriter& rewriter, llvm::SmallVectorImpl& insertedOps) { for (Operation* op : llvm::reverse(insertedOps)) { From cc9dfc5afdaa8c9847b66a2717f0c48cdc0c71ca Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Fri, 24 Apr 2026 14:59:54 +0200 Subject: [PATCH 013/122] =?UTF-8?q?=E2=9C=A8=20Support=20arbitrary=20singl?= =?UTF-8?q?e=20controlled=20operation=20in=20native=20synthesis.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/NativeSynthesis/Pass.cpp | 22 +++++++++------ .../test_native_synthesis_pass_profiles.cpp | 15 ++++++++++ mlir/unittests/programs/qc_programs.cpp | 28 +++++++++++++++++++ mlir/unittests/programs/qc_programs.h | 5 ++++ 4 files changed, 61 insertions(+), 9 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp index cc38c5b1f1..45297e6698 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp @@ -609,8 +609,6 @@ struct NativeGateSynthesisPass /// Fast-path: already-native `CX`/`CZ` are kept as-is. Otherwise, lift the /// controlled op to its 4x4 matrix (with SU(4) normalization), run the /// Weyl-based basis-decomposer search, and emit the best candidate. - /// Returns `failure()` for multi-control ops, non-`X`/`Z` bodies, or when - /// no candidate fits the profile. static LogicalResult rewriteControlled(IRRewriter& rewriter, CtrlOp ctrl, const NativeProfileSpec& spec, const ScoreWeights& weights) { @@ -622,18 +620,24 @@ struct NativeGateSynthesisPass auto* body = ctrl.getBodyUnitary().getOperation(); const bool hasCX = isa(body); const bool hasCZ = isa(body); - if (!hasCX && !hasCZ) { - ctrl.emitError("native synthesis currently only supports CX/CZ bodies"); - return failure(); - } if ((usesCxEntangler(spec) && hasCX) || (usesCzEntangler(spec) && hasCZ)) { return success(); } // Otherwise treat as a generic `4×4` (Weyl + basis decomposer + scorer). Eigen::Matrix4cd matrix; - if (!getBlockTwoQubitMatrix(ctrl.getOperation(), matrix)) { - ctrl.emitError("failed to compute 4x4 matrix for CtrlOp"); - return failure(); + if (hasCX || hasCZ) { + if (!getBlockTwoQubitMatrix(ctrl.getOperation(), matrix)) { + ctrl.emitError("failed to compute 4x4 matrix for CtrlOp"); + return failure(); + } + } else { + auto u = cast(ctrl.getOperation()); + if (!u.isTwoQubit() || !u.getUnitaryMatrix4x4(matrix)) { + ctrl.emitError( + "native synthesis: cannot build a constant 4x4 matrix for this " + "controlled gate (unsupported body or non-constant parameters)"); + return failure(); + } } native_synth::normalizeToSU4(matrix); // SU(4) convention for Weyl diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_profiles.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_profiles.cpp index d331f743f8..9ce2b2d018 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_profiles.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_profiles.cpp @@ -236,6 +236,21 @@ TEST_F(NativeSynthesisPassTest, DecomposesRzToAxisPairRxRyCxProfile) { "rx,ry,cx", &NativeSynthesisPassTest::onlyAxisPairRxRyCxOps); } +/// Single-control / single-target QC→QCO ``ctrl`` shells from +/// ``allSingleControlledGateFamiliesOneCtrlOneTarget`` must reach the generic +/// ``u,cx`` menu. +TEST_F(NativeSynthesisPassTest, + AllSingleControlledOneCtrlOneTargetFamiliesReachesU3Cx) { + expectNativeAfterSynthesis( + [&] { + return mlir::qc::QCProgramBuilder::build( + context.get(), + mlir::qc:: + nativeSynthAllSingleControlledGateFamiliesOneCtrlOneTarget); + }, + "u,cx", &NativeSynthesisPassTest::onlyGenericU3CxOps); +} + TEST_F(NativeSynthesisPassTest, GenericProfileMatchesGenericU3CxBehavior) { expectEquivalentAndNativeAfterSynthesis( [&] { diff --git a/mlir/unittests/programs/qc_programs.cpp b/mlir/unittests/programs/qc_programs.cpp index 21bc9c2b6e..73fad59c3d 100644 --- a/mlir/unittests/programs/qc_programs.cpp +++ b/mlir/unittests/programs/qc_programs.cpp @@ -1799,4 +1799,32 @@ void nativeSynthDeterminismTwoQubitSwap(QCProgramBuilder& b) { b.dealloc(q1); } +void nativeSynthAllSingleControlledGateFamiliesOneCtrlOneTarget( + QCProgramBuilder& b) { + auto q = b.allocQubitRegister(2); + const mlir::Value c = q[0]; + const mlir::Value t = q[1]; + + b.cgphase(0.07, c); + + b.cid(c, t); + b.cx(c, t); + b.cy(c, t); + b.cz(c, t); + b.ch(c, t); + b.cs(c, t); + b.csdg(c, t); + b.ct(c, t); + b.ctdg(c, t); + b.csx(c, t); + b.csxdg(c, t); + + b.crx(0.11, c, t); + b.cry(0.12, c, t); + b.crz(0.13, c, t); + b.cp(0.14, c, t); + b.cr(0.15, 0.16, c, t); + b.cu2(0.17, 0.18, c, t); + b.cu(0.19, 0.2, 0.21, c, t); +} } // namespace mlir::qc diff --git a/mlir/unittests/programs/qc_programs.h b/mlir/unittests/programs/qc_programs.h index 4dbc123de4..a8473a17f9 100644 --- a/mlir/unittests/programs/qc_programs.h +++ b/mlir/unittests/programs/qc_programs.h @@ -1002,4 +1002,9 @@ void nativeSynthScoringXxMinusYyOnly(QCProgramBuilder& b); /// Two-qubit ``swap`` with explicit ``allocQubit`` / ``dealloc`` ordering. void nativeSynthDeterminismTwoQubitSwap(QCProgramBuilder& b); + +/// Single-control ops whose QCO lowering uses only 1-control / 1-target +/// ``ctrl`` shells (native gate synthesis supports these today). +void nativeSynthAllSingleControlledGateFamiliesOneCtrlOneTarget( + QCProgramBuilder& b); } // namespace mlir::qc From 12cb3036b6cfe6f73da9fd3dc6587f5103d45d88 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Fri, 24 Apr 2026 15:10:00 +0200 Subject: [PATCH 014/122] =?UTF-8?q?=F0=9F=93=9D=20Update=20documentation?= =?UTF-8?q?=20for=20native=20gate=20menu=20in=20QuantumCompilerConfig,=20c?= =?UTF-8?q?larifying=20examples=20and=20improving=20readability.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/include/mlir/Compiler/CompilerPipeline.h | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/mlir/include/mlir/Compiler/CompilerPipeline.h b/mlir/include/mlir/Compiler/CompilerPipeline.h index c03a055fab..c7daf29aff 100644 --- a/mlir/include/mlir/Compiler/CompilerPipeline.h +++ b/mlir/include/mlir/Compiler/CompilerPipeline.h @@ -45,17 +45,15 @@ struct QuantumCompilerConfig { bool disableMergeSingleQubitRotationGates = false; /// Comma-separated native gate menu. Recognised tokens: `u`, `x`, `sx`, - /// `rz` (or `p`), `rx`, `ry`, `r`, `cx`, `cz`, `rzz`. An empty or - /// whitespace-only string leaves native synthesis as a no-op (IR - /// unchanged). Illustrative menus (use `cx` or `cz` as the entangler, or + /// `rz` (or `p`), `rx`, `ry`, `r`, `cx`, `cz`, `rzz`. + /// Illustrative menus (use `cx` or `cz` as the entangler, or /// both): /// - `"x,sx,rz,cx"` / `"x,sx,rz,cz"` — IBM basic (no fractional 2q) /// - `"x,sx,rz,rx,rzz,cx"` / `"...,cz"` — IBM fractional - /// - `"u,cx"` / `"u,cz"` — generic single-qubit `qco.u` (menu token `u`, not - /// `u3`) + /// - `"u,cx"` / `"u,cz"` — generic single-qubit U3 + CX/CY /// - `"r,cz"` — IQM-style default - /// - `"rx,rz,cx"`, `"rx,ry,cz"`, `"ry,rz,cx"` — supported `rx`/`ry`/`rz` - /// pairs plus entangler + /// - `"rx,rz,cx"`, `"rx,ry,cz"`, `"ry,rz,cx"` — supported RX/RY/RZ pairs plus + /// entangler std::string nativeGates; /// Weight for two-qubit gates in local candidate scoring From 76dd5affe528c2928dbbde57b698f439f8f4f5b2 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Fri, 24 Apr 2026 15:41:15 +0200 Subject: [PATCH 015/122] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20linter=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/IR/Operations/StandardGates/TOp.cpp | 1 + .../QCO/IR/Operations/StandardGates/TdgOp.cpp | 1 + .../Decomposition/BasisDecomposer.cpp | 7 ++++ .../Transforms/Decomposition/EulerBasis.cpp | 2 ++ .../Decomposition/EulerDecomposition.cpp | 5 +++ .../Transforms/Decomposition/GateSequence.cpp | 1 + .../QCO/Transforms/Decomposition/Helpers.cpp | 6 +++- .../Decomposition/UnitaryMatrices.cpp | 2 ++ .../Decomposition/WeylDecomposition.cpp | 8 +++-- .../Transforms/NativeSynthesis/NativeSpec.cpp | 30 ++++++++-------- .../QCO/Transforms/NativeSynthesis/Pass.cpp | 21 ++++++++---- .../NativeSynthesis/PassTwoQubitWindows.cpp | 16 +++++---- .../QCO/Transforms/NativeSynthesis/Policy.cpp | 13 +++++-- .../NativeSynthesis/SingleQubit.cpp | 33 ++++++++++++------ .../Transforms/NativeSynthesis/TwoQubit.cpp | 34 +++++++++++-------- 15 files changed, 121 insertions(+), 59 deletions(-) diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/TOp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/TOp.cpp index e36322dc12..1ff7e851ee 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/TOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/TOp.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/TdgOp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/TdgOp.cpp index a8eb77b629..a83283eb04 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/TdgOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/TdgOp.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp index fb00d7dfab..72eee0ba00 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp @@ -10,9 +10,13 @@ #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 #include @@ -22,8 +26,11 @@ #include #include #include +#include +#include #include #include +#include #include namespace mlir::qco::decomposition { diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerBasis.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerBasis.cpp index 9fc141284e..a12771bdd9 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerBasis.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerBasis.cpp @@ -10,6 +10,8 @@ #include "mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" + #include #include diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.cpp index 542439b045..171fae2d45 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.cpp @@ -10,14 +10,19 @@ #include "mlir/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.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 { diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/GateSequence.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/GateSequence.cpp index f315aa505c..29db6a303e 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/GateSequence.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/GateSequence.cpp @@ -11,6 +11,7 @@ #include "mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp index ad7137e0c0..8d035a697c 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp @@ -10,12 +10,16 @@ #include "mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h" +#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" #include +#include +#include #include #include +#include #include namespace mlir::qco::helpers { @@ -105,7 +109,7 @@ double traceToFidelity(const std::complex& x) { // `F_avg = (d + |tr|^2) / (d * (d + 1))` with `d = 4`, which reduces to the // `(4 + |x|^2) / 20` expression below. See e.g. Horodecki/Nielsen. auto xAbs = std::abs(x); - return (4.0 + xAbs * xAbs) / 20.0; + return (4.0 + (xAbs * xAbs)) / 20.0; } std::size_t getComplexity(decomposition::GateKind type, diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp index d6f1f1e2c7..340afaf62f 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp @@ -10,6 +10,8 @@ #include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Gate.h" + #include #include #include diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.cpp index e091fd8c76..caaf617bcb 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.cpp @@ -10,7 +10,9 @@ #include "mlir/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" @@ -312,8 +314,8 @@ TwoQubitWeylDecomposition::magicBasisTransform(const Eigen::Matrix4cd& unitary, const Eigen::Matrix4cd bNonNormalizedDagger{ {0.5, 0, 0, 0.5}, - {-0.5i, 0, 0, 0.5i}, - {0, -0.5i, -0.5i, 0}, + {std::complex{0.0, -0.5}, 0, 0, std::complex{0.0, 0.5}}, + {0, std::complex{0.0, -0.5}, std::complex{0.0, -0.5}, 0}, {0, 0.5, -0.5, 0}, }; if (direction == MagicBasisTransform::OutOf) { @@ -330,7 +332,7 @@ double TwoQubitWeylDecomposition::closestPartialSwap(double a, double b, 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.); + return m + (am * bm * cm * (6. + (ab * ab) + (bc * bc) + (ca * ca)) / 18.); } std::pair diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.cpp index 9e861e79bf..2a18f947d7 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.cpp @@ -10,6 +10,8 @@ #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" + #include #include #include @@ -17,13 +19,13 @@ #include #include +#include namespace mlir::qco::native_synth { -namespace { /// Map a single native-gate token (lower-case, no whitespace) to its /// `NativeGateKind`. -std::optional parseGateToken(llvm::StringRef name) { +static std::optional parseGateToken(llvm::StringRef name) { return llvm::StringSwitch>(name) .Case("u", NativeGateKind::U) .Case("x", NativeGateKind::X) @@ -40,7 +42,7 @@ std::optional parseGateToken(llvm::StringRef name) { /// Parse a comma-separated native-gate menu (e.g. `"u,cx,rzz"`) into the set /// of `NativeGateKind`s it names. -std::optional> +static std::optional> parseGateSet(llvm::StringRef nativeGates) { llvm::DenseSet gates; llvm::SmallVector parts; @@ -61,9 +63,9 @@ parseGateSet(llvm::StringRef nativeGates) { /// Build a fully-resolved `SingleQubitEmitterSpec` for `mode`, including the /// list of Euler bases the matrix-fallback path is allowed to use. -SingleQubitEmitterSpec makeEmitterSpec(SingleQubitMode mode, - AxisPair axisPair = AxisPair::RxRz, - bool supportsDirectRx = false) { +static SingleQubitEmitterSpec +makeEmitterSpec(SingleQubitMode mode, AxisPair axisPair = AxisPair::RxRz, + bool supportsDirectRx = false) { llvm::SmallVector bases; switch (mode) { case SingleQubitMode::ZSXX: @@ -89,10 +91,10 @@ SingleQubitEmitterSpec makeEmitterSpec(SingleQubitMode mode, /// Append a new emitter for `(mode, axisPair, supportsDirectRx)` to /// `emitters` iff no equivalent entry is already present. -void addEmitterIfAbsent(llvm::SmallVectorImpl& emitters, - SingleQubitMode mode, - AxisPair axisPair = AxisPair::RxRz, - bool supportsDirectRx = false) { +static void +addEmitterIfAbsent(llvm::SmallVectorImpl& emitters, + SingleQubitMode mode, AxisPair axisPair = AxisPair::RxRz, + bool supportsDirectRx = false) { const bool present = llvm::any_of(emitters, [&](const auto& e) { return e.mode == mode && e.axisPair == axisPair && e.supportsDirectRx == supportsDirectRx; @@ -103,7 +105,7 @@ void addEmitterIfAbsent(llvm::SmallVectorImpl& emitters, } /// Enumerate the native gate kinds that `emitter` may actually emit. -llvm::SmallVector +static llvm::SmallVector allowedGatesForEmitter(const SingleQubitEmitterSpec& emitter) { switch (emitter.mode) { case SingleQubitMode::ZSXX: { @@ -133,7 +135,7 @@ allowedGatesForEmitter(const SingleQubitEmitterSpec& emitter) { } /// Enumerate the native entangling gate kinds that `entangler` may emit. -llvm::SmallVector +static llvm::SmallVector allowedGatesForEntangler(EntanglerBasis entangler) { switch (entangler) { case EntanglerBasis::None: @@ -148,7 +150,7 @@ allowedGatesForEntangler(EntanglerBasis entangler) { /// Rebuild `spec.allowedGates` as the union of the gate kinds produced by /// every resolved emitter, entangler, and (optionally) `Rzz`. -void populateAllowedGates(NativeProfileSpec& spec) { +static void populateAllowedGates(NativeProfileSpec& spec) { spec.allowedGates.clear(); for (const auto& emitter : spec.singleQubitEmitters) { const auto allowed = allowedGatesForEmitter(emitter); @@ -163,8 +165,6 @@ void populateAllowedGates(NativeProfileSpec& spec) { } } -} // namespace - llvm::SmallVector getEulerBasesForAxisPair(AxisPair axisPair) { switch (axisPair) { diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp index 45297e6698..42f5d90385 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp @@ -10,6 +10,7 @@ #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h" @@ -29,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -39,8 +41,10 @@ #include #include #include +#include #include #include +#include namespace mlir::qco { #define GEN_PASS_DEF_NATIVEGATESYNTHESISPASS @@ -87,9 +91,11 @@ struct OneQubitRun { llvm::SmallVector ops; }; +} // namespace + /// If profitable, replace the run with one synthesized single-qubit op. -bool maybeFuseRun(IRRewriter& rewriter, OneQubitRun& run, - const NativeProfileSpec& spec) { +static bool maybeFuseRun(IRRewriter& rewriter, OneQubitRun& run, + const NativeProfileSpec& spec) { Eigen::Matrix2cd fused = Eigen::Matrix2cd::Identity(); for (UnitaryOpInterface u : run.ops) { Eigen::Matrix2cd m; @@ -162,7 +168,7 @@ bool maybeFuseRun(IRRewriter& rewriter, OneQubitRun& run, } /// Single-qubit op eligible for fusion (constant `2×2`, not under `ctrl`). -UnitaryOpInterface fusibleSingleQubitOp(Operation* op) { +static UnitaryOpInterface fusibleSingleQubitOp(Operation* op) { auto unitary = dyn_cast(op); if (!unitary || !unitary.isSingleQubit()) { return {}; @@ -183,9 +189,9 @@ UnitaryOpInterface fusibleSingleQubitOp(Operation* op) { /// Dispatch `op`'s direct (non-matrix) single-qubit lowering to the /// `decomposeTo*` helper for `emitter.mode`. Returns the output qubit value /// or a null `Value` if no direct rule applies for this op. -Value applyDirectSingleQubitLowering(IRRewriter& rewriter, Operation* op, - Value in, - const SingleQubitEmitterSpec& emitter) { +static Value +applyDirectSingleQubitLowering(IRRewriter& rewriter, Operation* op, Value in, + const SingleQubitEmitterSpec& emitter) { switch (emitter.mode) { case SingleQubitMode::ZSXX: return decomposeToZSXX(rewriter, op, in, emitter.supportsDirectRx); @@ -199,6 +205,8 @@ Value applyDirectSingleQubitLowering(IRRewriter& rewriter, Operation* op, llvm_unreachable("unknown SingleQubitMode"); } +namespace { + /// Lowers unitary QCO ops to a comma-separated native gate menu (single-qubit /// fuse, two-qubit windows, synthesis sweeps, seam single-qubit fuse, `rz` /// through `ctrl` controls, another single-qubit fuse, optional cleanup sweeps. @@ -222,6 +230,7 @@ struct NativeGateSynthesisPass scoreWeightDepth = options.scoreWeightDepth; } +protected: /// Top-level pass entry point. Validates the score weights and native-gate /// menu, then drives the staged rewrite pipeline: one-qubit run fusion, /// two-qubit window consolidation, synthesis sweeps until the single-qubit diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp index f58150e641..5990e2930b 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp @@ -16,21 +16,27 @@ #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Scoring.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h" #include +#include #include +#include +#include +#include +#include #include +#include namespace mlir::qco::native_synth { -namespace { /// Check whether a two-qubit op `op` is already expressible by the resolved /// native menu: a single-control `CX`/`CZ` consistent with the active /// entangler, or `Rzz` when `spec.allowRzz` is set. Multi-control and other /// two-qubit ops are considered non-native. -bool isNativeTwoQubitOp(Operation* op, const NativeProfileSpec& spec) { +static bool isNativeTwoQubitOp(Operation* op, const NativeProfileSpec& spec) { if (auto ctrl = dyn_cast(op)) { if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { return false; @@ -52,8 +58,8 @@ bool isNativeTwoQubitOp(Operation* op, const NativeProfileSpec& spec) { /// any non-native op (we have to lower them anyway); otherwise only replace /// when the candidate has strictly fewer two-qubit gates, or the same number /// with strictly fewer one-qubit gates. -bool shouldApplyBlockReplacement(const TwoQubitBlock& block, - const CandidateMetrics& best) { +static bool shouldApplyBlockReplacement(const TwoQubitBlock& block, + const CandidateMetrics& best) { if (block.anyNonNative) { return true; } @@ -63,8 +69,6 @@ bool shouldApplyBlockReplacement(const TwoQubitBlock& block, return shorterTwoQ || (sameTwoQ && shorterOneQ); } -} // namespace - /// Emit the chosen synthesis sequence `best` at the location of the window's /// first op, rewire the block's trailing SSA values (`wireA`, `wireB`) to /// the newly emitted outputs, and erase the replaced ops in reverse order diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Policy.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Policy.cpp index 627fbd6230..bbaf3e695f 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Policy.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Policy.cpp @@ -11,15 +11,23 @@ #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" #include #include +#include +#include #include +#include #include #include +#include namespace mlir::qco::native_synth { @@ -37,10 +45,10 @@ bool usesCzEntangler(const NativeProfileSpec& spec) { return llvm::is_contained(spec.entanglerBases, EntanglerBasis::Cz); } -namespace { /// Map a single-qubit `UnitaryOpInterface` op to the `NativeGateKind` that /// must appear in the menu for the op to be a no-op. -std::optional singleQubitNativeGateKind(UnitaryOpInterface op) { +static std::optional +singleQubitNativeGateKind(UnitaryOpInterface op) { Operation* raw = op.getOperation(); if (isa(raw)) { return NativeGateKind::U; @@ -66,7 +74,6 @@ std::optional singleQubitNativeGateKind(UnitaryOpInterface op) { } return std::nullopt; } -} // namespace bool allowsSingleQubitOp(UnitaryOpInterface op, const NativeProfileSpec& spec) { if (isa(op.getOperation())) { diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp index 10116a4190..c46bb849bd 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp @@ -11,26 +11,35 @@ #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h" +#include #include #include +#include +#include #include #include #include +#include #include #include #include namespace mlir::qco::native_synth { -namespace { constexpr double PI = std::numbers::pi; constexpr double HALF_PI = PI / 2.0; +namespace { + /// Small convenience wrapper to avoid passing rewriter/loc everywhere. Each /// method creates the corresponding QCO op threaded through `q` and returns /// its new output qubit value. @@ -95,10 +104,13 @@ struct SingleQubitEmitter { } }; +} // namespace + /// Materialize an `EulerBasis::ZSXX` decomposition (`rz` / `sx` / `x`) into /// QCO ops. -Value emitEulerSequenceZsxx(SingleQubitEmitter e, Value q, - const decomposition::QubitGateSequence& seq) { +static Value +emitEulerSequenceZsxx(SingleQubitEmitter e, Value q, + const decomposition::QubitGateSequence& seq) { for (const auto& gate : seq.gates) { switch (gate.type) { case decomposition::GateKind::RZ: @@ -126,8 +138,8 @@ Value emitEulerSequenceZsxx(SingleQubitEmitter e, Value q, /// for the `R` emitter: `Rx(theta)` becomes `R(theta, 0)`, `Ry(theta)` /// becomes `R(theta, pi/2)`, Pauli `X`/`Y` become `R(pi, *)`, `I` is a /// no-op. -Value emitEulerSequenceR(SingleQubitEmitter e, Value q, - const decomposition::QubitGateSequence& seq) { +static Value emitEulerSequenceR(SingleQubitEmitter e, Value q, + const decomposition::QubitGateSequence& seq) { for (const auto& gate : seq.gates) { switch (gate.type) { case decomposition::GateKind::RX: @@ -163,8 +175,9 @@ Value emitEulerSequenceR(SingleQubitEmitter e, Value q, /// a null `Value`; the matrix-based fallback is expected to pick a /// different basis in that case. Pauli gates are lowered to the /// corresponding `R*(pi)` when their axis is available. -Value emitEulerSequenceAxisPair(SingleQubitEmitter e, Value q, AxisPair axis, - const decomposition::QubitGateSequence& seq) { +static Value +emitEulerSequenceAxisPair(SingleQubitEmitter e, Value q, AxisPair axis, + const decomposition::QubitGateSequence& seq) { for (const auto& gate : seq.gates) { switch (gate.type) { case decomposition::GateKind::RX: @@ -214,14 +227,12 @@ Value emitEulerSequenceAxisPair(SingleQubitEmitter e, Value q, AxisPair axis, /// Decompose `matrix` numerically into a gate sequence in `basis` with /// zero-rotations pruned (`simplify=true`). -decomposition::QubitGateSequence runEuler(decomposition::EulerBasis basis, - const Eigen::Matrix2cd& matrix) { +static decomposition::QubitGateSequence +runEuler(decomposition::EulerBasis basis, const Eigen::Matrix2cd& matrix) { return decomposition::EulerDecomposition::generateCircuit( basis, matrix, /*simplify=*/true, std::nullopt); } -} // namespace - Value decomposeToZSXX(IRRewriter& rewriter, Operation* op, Value inQubit, bool supportsDirectRx) { if (isa(op)) { diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp index cb75968816..ac32b3c82a 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp @@ -12,17 +12,28 @@ #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/Gate.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h" +#include +#include +#include #include +#include +#include #include +#include #include +#include namespace mlir::qco::native_synth { -namespace { constexpr double PI = std::numbers::pi; constexpr double HALF_PI = PI / 2.0; @@ -30,8 +41,9 @@ constexpr double HALF_PI = PI / 2.0; /// Whether the given single-qubit emitter can lower a decomposition-IR gate /// of `kind` (an intermediate from Euler/Weyl, *not* a `NativeGateKind`) to a /// native output sequence. -bool emitterHandlesDecompositionGate(const SingleQubitEmitterSpec& emitter, - decomposition::GateKind kind) { +static bool +emitterHandlesDecompositionGate(const SingleQubitEmitterSpec& emitter, + decomposition::GateKind kind) { if (kind == decomposition::GateKind::I) { return true; } @@ -71,8 +83,8 @@ bool emitterHandlesDecompositionGate(const SingleQubitEmitterSpec& emitter, } /// Check that a single decomposition gate is allowed by the profile menu. -bool menuAllows(const decomposition::Gate& gate, - const NativeProfileSpec& spec) { +static bool menuAllows(const decomposition::Gate& gate, + const NativeProfileSpec& spec) { if (gate.qubitId.size() == 1) { return std::ranges::any_of(spec.singleQubitEmitters, [&gate](const SingleQubitEmitterSpec& emitter) { @@ -96,8 +108,8 @@ bool menuAllows(const decomposition::Gate& gate, } /// Whether `emitter` can lower the single-qubit `op` directly. -bool emitterHasDirectLowering(Operation* op, - const SingleQubitEmitterSpec& emitter) { +static bool emitterHasDirectLowering(Operation* op, + const SingleQubitEmitterSpec& emitter) { switch (emitter.mode) { case SingleQubitMode::ZSXX: return canDirectlyDecomposeToZSXX(op, emitter.supportsDirectRx); @@ -111,8 +123,6 @@ bool emitterHasDirectLowering(Operation* op, llvm_unreachable("unknown single-qubit mode"); } -} // namespace - bool gateSequenceFitsMenu(const decomposition::TwoQubitGateSequence& seq, const NativeProfileSpec& spec) { return std::ranges::all_of(seq.gates, @@ -173,13 +183,11 @@ collectSingleQubitCandidates(UnitaryOpInterface unitary, return candidates; } -namespace { - /// Try every `numBasisUses` in `{0, 1, 2, 3}` for the `(entangler, emitter, /// basis)` triple, running the Weyl-based basis decomposer for each. Any /// resulting gate sequence that both matches `targetMatrix` up to global /// phase AND stays inside the native menu is appended to `candidates`. -void tryAddTwoQubitBasisCandidatesForEmitterBasis( +static void tryAddTwoQubitBasisCandidatesForEmitterBasis( llvm::SmallVector, 0>& candidates, unsigned& enumerationIndex, const Eigen::Matrix4cd& targetMatrix, const NativeProfileSpec& spec, EntanglerBasis entangler, @@ -215,8 +223,6 @@ void tryAddTwoQubitBasisCandidatesForEmitterBasis( } } -} // namespace - llvm::SmallVector, 0> collectTwoQubitBasisCandidatesFromMatrix(const Eigen::Matrix4cd& targetMatrix, const NativeProfileSpec& spec) { From 9a6e85e3592e73b065e38735c7aff8f8e41db710 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Fri, 24 Apr 2026 17:21:30 +0200 Subject: [PATCH 016/122] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20linter=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/Decomposition/Helpers.cpp | 1 + .../Decomposition/UnitaryMatrices.cpp | 1 + .../Transforms/NativeSynthesis/NativeSpec.cpp | 1 + .../NativeSynthesis/PassTwoQubitWindows.cpp | 2 + .../QCO/Transforms/NativeSynthesis/Policy.cpp | 1 + .../NativeSynthesis/SingleQubit.cpp | 2 + .../Transforms/NativeSynthesis/TwoQubit.cpp | 7 ++- .../QCO/Transforms/NativeSynthesis/Utils.cpp | 23 +++++--- .../Compiler/test_compiler_pipeline.cpp | 52 +++++++++++-------- .../Decomposition/test_basis_decomposer.cpp | 19 ++++--- .../test_decomposition_get_gate_kind.cpp | 2 + .../test_decomposition_helpers.cpp | 2 +- .../test_euler_decomposition.cpp | 18 +++---- .../Decomposition/test_weyl_decomposition.cpp | 1 + .../native_synthesis_pass_test_fixture.h | 2 + .../native_synthesis_test_helpers.cpp | 11 +++- .../native_synthesis_test_helpers.h | 3 -- 17 files changed, 93 insertions(+), 55 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp index 8d035a697c..3382034907 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp @@ -12,6 +12,7 @@ #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" #include #include diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp index 340afaf62f..675abd4b19 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp @@ -11,6 +11,7 @@ #include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Gate.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" #include #include diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.cpp index 2a18f947d7..8c6d8d2e0e 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.cpp @@ -10,6 +10,7 @@ #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" #include diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp index 5990e2930b..7444956876 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp @@ -12,6 +12,7 @@ #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Gate.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Scoring.h" @@ -24,6 +25,7 @@ #include #include #include +#include #include #include diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Policy.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Policy.cpp index bbaf3e695f..edb1c4c317 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Policy.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Policy.cpp @@ -10,6 +10,7 @@ #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h" +#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.h" diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp index c46bb849bd..4512a28d6b 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp @@ -23,7 +23,9 @@ #include #include #include +#include #include +#include #include #include diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp index ac32b3c82a..6b629c5e15 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp @@ -10,6 +10,7 @@ #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h" +#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" @@ -23,15 +24,17 @@ #include #include -#include #include #include +#include #include #include #include #include #include +#include +#include namespace mlir::qco::native_synth { @@ -120,7 +123,7 @@ static bool emitterHasDirectLowering(Operation* op, case SingleQubitMode::AxisPair: return canDirectlyDecomposeToAxisPair(op, emitter.axisPair); } - llvm_unreachable("unknown single-qubit mode"); + return false; } bool gateSequenceFitsMenu(const decomposition::TwoQubitGateSequence& seq, diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp index 548dc5a185..fed7c8a680 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp @@ -10,15 +10,27 @@ #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h" +#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Gate.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h" #include #include +#include +#include #include #include +#include +#include +#include +#include +#include #include #include +#include namespace mlir::qco::native_synth { @@ -104,12 +116,10 @@ bool getBlockTwoQubitMatrix(Operation* op, Eigen::Matrix4cd& matrix) { return unitary.getUnitaryMatrix4x4(matrix); } -namespace { - /// Emit a single-qubit gate from a decomposition gate, threading `target` and /// recording the inserted op (if any) in `insertedOps` so the caller can roll /// back on failure. -LogicalResult +static LogicalResult emitSingleQubitStep(IRRewriter& rewriter, Location loc, const decomposition::Gate& gate, Value& target, llvm::SmallVectorImpl& insertedOps) { @@ -181,16 +191,15 @@ emitSingleQubitStep(IRRewriter& rewriter, Location loc, } /// Erase all ops tracked in `insertedOps` in reverse insertion order. -void rollbackInsertedOps(IRRewriter& rewriter, - llvm::SmallVectorImpl& insertedOps) { +static void +rollbackInsertedOps(IRRewriter& rewriter, + llvm::SmallVectorImpl& insertedOps) { for (Operation* op : llvm::reverse(insertedOps)) { rewriter.eraseOp(op); } insertedOps.clear(); } -} // namespace - LogicalResult emitTwoQubitGateSequenceAtLoc(IRRewriter& rewriter, Location loc, Value qubit0, Value qubit1, diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index d3d565ea86..2debaeab41 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -17,6 +17,7 @@ #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Gate.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" #include "mlir/Dialect/QIR/Builder/QIRProgramBuilder.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" @@ -44,6 +45,7 @@ #include #include #include +#include #include #include @@ -710,10 +712,14 @@ class CompilerPipelineNativeSynthesisConfigTest : public testing::Test { void runPipelineAndExpectFailure() const { mlir::CompilationRecord record; mlir::QuantumCompilerPipeline pipeline(config); - EXPECT_TRUE(failed(pipeline.runPipeline(module.get(), &record))); + EXPECT_TRUE(mlir::failed(pipeline.runPipeline(module.get(), &record))); } }; +} // namespace + +using mqt::test::isEquivalentUpToGlobalPhase; + /// Compute the 4×4 unitary of a two-qubit QCO module whose qubits are /// introduced by `qco.static` ops with indices 0 and 1. Handles the op set /// that stage-4/stage-5 IR can contain for the `staticQubitsWithOps` @@ -721,7 +727,7 @@ class CompilerPipelineNativeSynthesisConfigTest : public testing::Test { /// `qco.x`, `qco.p`, `qco.u`; and `qco.gphase`, which is skipped). Returns /// `std::nullopt` if the IR contains an unsupported op or non-constant /// parameters. -std::optional +static std::optional computeStaticTwoQubitUnitary(mlir::ModuleOp module) { if (module == nullptr) { return std::nullopt; @@ -817,10 +823,6 @@ computeStaticTwoQubitUnitary(mlir::ModuleOp module) { return unitary; } -using mqt::test::isEquivalentUpToGlobalPhase; - -} // namespace - TEST_F(CompilerPipelineNativeSynthesisConfigTest, AppliesConfiguredNativeSynthesisProfileInStage5) { config.nativeGates = "x,sx,rz,cx"; @@ -931,11 +933,6 @@ namespace { struct NativeSynthesisProgramTestCase { std::string name; QCProgramBuilderFn qcProgramBuilder; - - friend std::ostream& operator<<(std::ostream& os, - const NativeSynthesisProgramTestCase& info) { - return os << "NativeSynthesisProgram{" << info.name << "}"; - } }; struct NativeSynthesisProfileTestCase { @@ -943,24 +940,31 @@ struct NativeSynthesisProfileTestCase { std::string nativeGates; bool expectUInStage5 = false; llvm::SmallVector nonNativeOpsToEliminate; - - friend std::ostream& operator<<(std::ostream& os, - const NativeSynthesisProfileTestCase& info) { - return os << "NativeSynthesisProfile{" << info.name << "}"; - } }; struct NativeSynthesisStage5TestCase { NativeSynthesisProgramTestCase program; NativeSynthesisProfileTestCase profile; - - friend std::ostream& operator<<(std::ostream& os, - const NativeSynthesisStage5TestCase& info) { - return os << info.profile << " / " << info.program; - } }; -mlir::OwningOpRef +} // namespace + +static std::ostream& operator<<(std::ostream& os, + const NativeSynthesisProgramTestCase& info) { + return os << "NativeSynthesisProgram{" << info.name << "}"; +} + +static std::ostream& operator<<(std::ostream& os, + const NativeSynthesisProfileTestCase& info) { + return os << "NativeSynthesisProfile{" << info.name << "}"; +} + +static std::ostream& operator<<(std::ostream& os, + const NativeSynthesisStage5TestCase& info) { + return os << info.profile << " / " << info.program; +} + +static mlir::OwningOpRef buildQCModuleForNativeSynthesisProgram(mlir::MLIRContext* context, const QCProgramBuilderFn builder) { auto module = mlir::qc::QCProgramBuilder::build(context, builder.fn); @@ -968,7 +972,7 @@ buildQCModuleForNativeSynthesisProgram(mlir::MLIRContext* context, return module; } -mlir::CompilationRecord +static mlir::CompilationRecord runPipelineWithNativeSynthesisConfig(mlir::ModuleOp module, const std::string& nativeGates) { mlir::QuantumCompilerConfig config; @@ -982,6 +986,8 @@ runPipelineWithNativeSynthesisConfig(mlir::ModuleOp module, return record; } +namespace { + class CompilerPipelineNativeSynthesisProgramsTest : public testing::TestWithParam { protected: diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_basis_decomposer.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_basis_decomposer.cpp index 563721eb44..abc371076a 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_basis_decomposer.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_basis_decomposer.cpp @@ -12,6 +12,7 @@ #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/GateKind.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" @@ -21,6 +22,8 @@ #include #include +#include +#include #include #include #include @@ -35,14 +38,6 @@ 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, std::optional{1.0})); - } - [[nodiscard]] static Eigen::Matrix4cd restore(const TwoQubitGateSequence& sequence) { Eigen::Matrix4cd matrix = Eigen::Matrix4cd::Identity(); @@ -55,6 +50,14 @@ class BasisDecomposerTest } protected: + 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, std::optional{1.0})); + } + Eigen::Matrix4cd target; Gate basisGate; llvm::SmallVector eulerBases; diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_get_gate_kind.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_get_gate_kind.cpp index 1a3ad968bb..aa7e803cfc 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_get_gate_kind.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_get_gate_kind.cpp @@ -21,6 +21,8 @@ #include #include #include +#include +#include #include using namespace mlir; diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_helpers.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_helpers.cpp index cdc871428c..d1f29e6cc2 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_helpers.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_helpers.cpp @@ -35,7 +35,7 @@ TEST(DecompositionHelpersTest, Mod2piWrapsIntoHalfOpenInterval) { TEST(DecompositionHelpersTest, TraceToFidelityMatchesFormula) { const std::complex x{3.0, 4.0}; const double absx = 5.0; - EXPECT_DOUBLE_EQ(traceToFidelity(x), (4.0 + absx * absx) / 20.0); + EXPECT_DOUBLE_EQ(traceToFidelity(x), (4.0 + (absx * absx)) / 20.0); } TEST(DecompositionHelpersTest, GetComplexitySingleQubitAndGphase) { diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp index 909bcc9723..774cd61752 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -20,6 +20,9 @@ #include #include +#include +#include +#include #include #include #include @@ -29,9 +32,8 @@ using namespace mlir::qco; using namespace mlir::qco::decomposition; using namespace mlir::qco::decomposition_test; -namespace { - -std::size_t countGatesOfType(const OneQubitGateSequence& seq, GateKind kind) { +static std::size_t countGatesOfType(const OneQubitGateSequence& seq, + GateKind kind) { std::size_t count = 0; for (const auto& gate : seq.gates) { if (gate.type == kind) { @@ -43,15 +45,13 @@ std::size_t countGatesOfType(const OneQubitGateSequence& seq, GateKind kind) { /// Compare ``seq.getUnitaryMatrix()`` to ``u`` embedded on qubit 0 (4×4 /// layout). -bool sequenceMatchesSingleQubitMatrix(const Eigen::Matrix2cd& u, - const OneQubitGateSequence& seq, - double tol = 1e-10) { +static bool sequenceMatchesSingleQubitMatrix(const Eigen::Matrix2cd& u, + const OneQubitGateSequence& seq, + double tol = 1e-10) { const Eigen::Matrix4cd expanded = expandToTwoQubits(u, 0); return expanded.isApprox(seq.getUnitaryMatrix(), tol); } -} // namespace - class EulerDecompositionTest : public testing::TestWithParam< std::tuple> { @@ -67,12 +67,12 @@ class EulerDecompositionTest return matrix; } +protected: void SetUp() override { eulerBasis = std::get<0>(GetParam()); originalMatrix = std::get<1>(GetParam())(); } -protected: Eigen::Matrix2cd originalMatrix; EulerBasis eulerBasis{}; }; diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index 5dfc20fd84..7dc9b088db 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -9,6 +9,7 @@ */ #include "decomposition_test_utils.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.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" diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_pass_test_fixture.h b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_pass_test_fixture.h index f0a12d4ddc..a1ff1a1be6 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_pass_test_fixture.h +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_pass_test_fixture.h @@ -27,7 +27,9 @@ #include #include #include +#include #include +#include #include #include diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp index 15083d5033..d9c66241ed 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp @@ -13,10 +13,16 @@ #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" +#include #include +#include +#include #include +#include #include +#include +#include using namespace mlir; @@ -141,8 +147,9 @@ bool extractSingleQubitMatrix(qco::UnitaryOpInterface op, return false; } const auto thetaSin = std::sin(*theta / 2.0); - const auto m01 = phasedAmplitude(thetaSin, -*phi - (llvm::numbers::pi / 2)); - const auto m10 = phasedAmplitude(thetaSin, *phi - (llvm::numbers::pi / 2)); + const auto m01 = + phasedAmplitude(thetaSin, -*phi - (std::numbers::pi / 2.0)); + const auto m10 = phasedAmplitude(thetaSin, *phi - (std::numbers::pi / 2.0)); const std::complex thetaCos = std::cos(*theta / 2.0); out = Eigen::Matrix2cd{{thetaCos, m01}, {m10, thetaCos}}; return true; diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.h b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.h index 7fcc04f11e..6cda461ac8 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.h +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.h @@ -14,9 +14,6 @@ #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include -#include -#include -#include #include #include #include From dddc7d4a4efd72a2ea944475635cd7212f351da2 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Fri, 24 Apr 2026 17:44:37 +0200 Subject: [PATCH 017/122] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20linter=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/Decomposition/Helpers.cpp | 1 - .../QCO/Transforms/NativeSynthesis/Pass.cpp | 49 +++++----- .../NativeSynthesis/PassTwoQubitWindows.cpp | 18 ++-- .../QCO/Transforms/NativeSynthesis/Policy.cpp | 36 ++++---- .../NativeSynthesis/SingleQubit.cpp | 52 +++++------ .../Transforms/NativeSynthesis/TwoQubit.cpp | 5 +- .../QCO/Transforms/NativeSynthesis/Utils.cpp | 11 +-- .../Decomposition/test_basis_decomposer.cpp | 1 + .../test_decomposition_get_gate_kind.cpp | 14 +-- .../test_euler_decomposition.cpp | 1 + .../Decomposition/test_weyl_decomposition.cpp | 1 + .../native_synthesis_pass_test_fixture.h | 13 +-- .../native_synthesis_test_helpers.cpp | 38 ++++---- .../NativeSynthesis/test_native_policy.cpp | 9 +- ...est_native_synthesis_pass_custom_menus.cpp | 89 +++++++++++-------- .../test_native_synthesis_pass_fusion.cpp | 41 ++++++--- ...test_native_synthesis_pass_multi_qubit.cpp | 27 +++--- .../test_native_synthesis_pass_scoring.cpp | 2 +- 18 files changed, 233 insertions(+), 175 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp index 3382034907..d797308c8f 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp @@ -14,7 +14,6 @@ #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" -#include #include #include diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp index 42f5d90385..031320e2fe 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp @@ -169,14 +169,14 @@ static bool maybeFuseRun(IRRewriter& rewriter, OneQubitRun& run, /// Single-qubit op eligible for fusion (constant `2×2`, not under `ctrl`). static UnitaryOpInterface fusibleSingleQubitOp(Operation* op) { - auto unitary = dyn_cast(op); + auto unitary = llvm::dyn_cast(op); if (!unitary || !unitary.isSingleQubit()) { return {}; } - if (isa(op)) { + if (llvm::isa(op)) { return {}; } - if (isa_and_present(op->getParentOp())) { + if (llvm::isa_and_present(op->getParentOp())) { return {}; } Eigen::Matrix2cd matrix; @@ -319,8 +319,8 @@ struct NativeGateSynthesisPass return false; } Operation* body = ctrl.getBodyUnitary().getOperation(); - const bool hasCX = isa(body); - const bool hasCZ = isa(body); + const bool hasCX = llvm::isa(body); + const bool hasCZ = llvm::isa(body); if (!hasCX && !hasCZ) { return false; } @@ -330,7 +330,7 @@ struct NativeGateSynthesisPass /// Bare two-qubit on-menu: `rzz` when the profile allows it. static bool bareTwoQubitMatchesNativeMenu(Operation* op, const NativeProfileSpec& spec) { - return isa(op) && spec.allowRzz && + return llvm::isa(op) && spec.allowRzz && spec.allowedGates.contains(NativeGateKind::Rzz); } @@ -339,19 +339,19 @@ struct NativeGateSynthesisPass bool hasNonNativeMenuOps(const NativeProfileSpec& spec) { const mlir::WalkResult walkResult = getOperation()->walk([&](Operation* op) { - if (isa(op)) { + if (llvm::isa(op)) { return mlir::WalkResult::advance(); } - if (isa_and_present(op->getParentOp())) { + if (llvm::isa_and_present(op->getParentOp())) { return mlir::WalkResult::advance(); } - if (auto ctrl = dyn_cast(op)) { + if (auto ctrl = llvm::dyn_cast(op)) { if (!ctrlMatchesNativeMenu(ctrl, spec)) { return mlir::WalkResult::interrupt(); } return mlir::WalkResult::advance(); } - auto unitary = dyn_cast(op); + auto unitary = llvm::dyn_cast(op); if (!unitary) { return mlir::WalkResult::advance(); } @@ -376,13 +376,13 @@ struct NativeGateSynthesisPass bool hasNonNativeSingleQubitOps(const NativeProfileSpec& spec) { const mlir::WalkResult walkResult = getOperation()->walk([&](Operation* op) { - if (isa(op)) { + if (llvm::isa(op)) { return mlir::WalkResult::advance(); } - if (isa_and_present(op->getParentOp())) { + if (llvm::isa_and_present(op->getParentOp())) { return mlir::WalkResult::advance(); } - auto unitary = dyn_cast(op); + auto unitary = llvm::dyn_cast(op); if (!unitary || !unitary.isSingleQubit()) { return mlir::WalkResult::advance(); } @@ -452,11 +452,11 @@ struct NativeGateSynthesisPass unsigned hops = 0; while (v.hasOneUse()) { Operation* user = *v.getUsers().begin(); - if (auto rz2 = dyn_cast(user); rz2 && rz2.getQubitIn() == v) { + if (auto rz2 = llvm::dyn_cast(user); rz2 && rz2.getQubitIn() == v) { partner = rz2; break; } - auto ctrl = dyn_cast(user); + auto ctrl = llvm::dyn_cast(user); if (!ctrl) { return false; } @@ -554,13 +554,13 @@ struct NativeGateSynthesisPass continue; } // Inner `CtrlOp` bodies are handled on the `CtrlOp` itself. - if (isa_and_present(op->getParentOp())) { + if (llvm::isa_and_present(op->getParentOp())) { continue; } - if (isa(op)) { + if (llvm::isa(op)) { continue; } - auto unitary = dyn_cast(op); + auto unitary = llvm::dyn_cast(op); if (!unitary) { continue; } @@ -575,7 +575,7 @@ struct NativeGateSynthesisPass continue; } - if (auto ctrl = dyn_cast(op)) { + if (auto ctrl = llvm::dyn_cast(op)) { if (failed(rewriteControlled(rewriter, ctrl, spec, weights))) { return failure(); } @@ -627,8 +627,8 @@ struct NativeGateSynthesisPass return failure(); } auto* body = ctrl.getBodyUnitary().getOperation(); - const bool hasCX = isa(body); - const bool hasCZ = isa(body); + const bool hasCX = llvm::isa(body); + const bool hasCZ = llvm::isa(body); if ((usesCxEntangler(spec) && hasCX) || (usesCzEntangler(spec) && hasCZ)) { return success(); } @@ -640,7 +640,7 @@ struct NativeGateSynthesisPass return failure(); } } else { - auto u = cast(ctrl.getOperation()); + auto u = llvm::cast(ctrl.getOperation()); if (!u.isTwoQubit() || !u.getUnitaryMatrix4x4(matrix)) { ctrl.emitError( "native synthesis: cannot build a constant 4x4 matrix for this " @@ -673,10 +673,11 @@ struct NativeGateSynthesisPass UnitaryOpInterface unitary, const NativeProfileSpec& spec, const ScoreWeights& weights) { - if (spec.allowRzz && isa(op)) { + if (spec.allowRzz && llvm::isa(op)) { return success(); } - if (spec.allowRzz && (isa(op) || isa(op))) { + if (spec.allowRzz && + (llvm::isa(op) || llvm::isa(op))) { llvm::SmallVector> candidates; candidates.push_back(SynthesisCandidate{ .candidateClass = CandidateClass::XxPlusMinusViaRzz, diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp index 7444956876..ad971e4ccb 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp @@ -39,20 +39,20 @@ namespace mlir::qco::native_synth { /// entangler, or `Rzz` when `spec.allowRzz` is set. Multi-control and other /// two-qubit ops are considered non-native. static bool isNativeTwoQubitOp(Operation* op, const NativeProfileSpec& spec) { - if (auto ctrl = dyn_cast(op)) { + if (auto ctrl = llvm::dyn_cast(op)) { if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { return false; } auto* body = ctrl.getBodyUnitary().getOperation(); - if (isa(body)) { + if (llvm::isa(body)) { return usesCxEntangler(spec); } - if (isa(body)) { + if (llvm::isa(body)) { return usesCzEntangler(spec); } return false; } - return spec.allowRzz && isa(op); + return spec.allowRzz && llvm::isa(op); } /// Decide whether replacing a consolidated window with the candidate @@ -79,7 +79,7 @@ static void materializeSingleTwoQubitBlock( IRRewriter& rewriter, const TwoQubitBlock& block, const SynthesisCandidate& best) { Operation* firstOp = block.ops.front(); - auto firstUnitary = cast(firstOp); + auto firstUnitary = llvm::cast(firstOp); const Value inA = firstUnitary.getInputQubit(0); const Value inB = firstUnitary.getInputQubit(1); const Value outA = block.wireA; @@ -108,7 +108,7 @@ static void materializeSingleTwoQubitBlock( void collectUnitaryOpsInPreOrder(Operation* root, llvm::SmallVectorImpl& ops) { root->walk([&](Operation* op) { - if (isa(op)) { + if (llvm::isa(op)) { ops.push_back(op); } }); @@ -157,17 +157,17 @@ void TwoQubitWindowConsolidator::process(Operation* op, // Skip ops nested inside a `CtrlOp`'s body: those are handled as part of // their enclosing controlled op (seen at the parent level), not as // independent two-qubit gates. - if (isa_and_present(op->getParentOp())) { + if (llvm::isa_and_present(op->getParentOp())) { return; } - auto unitary = dyn_cast(op); + auto unitary = llvm::dyn_cast(op); if (!unitary) { return; } // Barriers and stand-alone global-phase ops are not unitaries we can // absorb; they act as synchronization points that force any block // touching their operand wires to close. - if (isa(op)) { + if (llvm::isa(op)) { for (Value v : op->getOperands()) { closeBlockOnWire(v); } diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Policy.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Policy.cpp index edb1c4c317..1918b03d7b 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Policy.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Policy.cpp @@ -51,33 +51,33 @@ bool usesCzEntangler(const NativeProfileSpec& spec) { static std::optional singleQubitNativeGateKind(UnitaryOpInterface op) { Operation* raw = op.getOperation(); - if (isa(raw)) { + if (llvm::isa(raw)) { return NativeGateKind::U; } - if (isa(raw)) { + if (llvm::isa(raw)) { return NativeGateKind::X; } - if (isa(raw)) { + if (llvm::isa(raw)) { return NativeGateKind::Sx; } - if (isa(raw)) { + if (llvm::isa(raw)) { // `p` is a Z-rotation primitive for menu purposes. return NativeGateKind::Rz; } - if (isa(raw)) { + if (llvm::isa(raw)) { return NativeGateKind::Rx; } - if (isa(raw)) { + if (llvm::isa(raw)) { return NativeGateKind::Ry; } - if (isa(raw)) { + if (llvm::isa(raw)) { return NativeGateKind::R; } return std::nullopt; } bool allowsSingleQubitOp(UnitaryOpInterface op, const NativeProfileSpec& spec) { - if (isa(op.getOperation())) { + if (llvm::isa(op.getOperation())) { return true; } const auto gate = singleQubitNativeGateKind(op); @@ -118,33 +118,33 @@ computeGateSequenceMetrics(const decomposition::QubitGateSequence& seq) { /// over, and (for ZSXX with direct Rx) `Rx`/`Ry`/`R`. Static angles still use /// matrix + Euler. bool canDirectlyDecomposeToZSXX(Operation* op, bool supportsDirectRx) { - if (isa(op)) { + if (llvm::isa(op)) { return true; } - return supportsDirectRx && isa(op); + return supportsDirectRx && llvm::isa(op); } bool canDirectlyDecomposeToU3(Operation* op) { - return isa(op); + return llvm::isa(op); } bool canDirectlyDecomposeToR(Operation* op) { - return isa(op); + return llvm::isa(op); } bool canDirectlyDecomposeToAxisPair(Operation* op, AxisPair axisPair) { - if (isa(op)) { + if (llvm::isa(op)) { return true; } switch (axisPair) { case AxisPair::RxRz: // `p` on an Rx/Rz axis pair folds directly to `rz(theta)`. - return isa(op); + return llvm::isa(op); case AxisPair::RxRy: // No cheap symbolic lowering of `p` without `rz` available. - return isa(op); + return llvm::isa(op); case AxisPair::RyRz: - return isa(op); + return llvm::isa(op); } llvm_unreachable("unknown axis pair"); } @@ -152,13 +152,13 @@ bool canDirectlyDecomposeToAxisPair(Operation* op, AxisPair axisPair) { CandidateMetrics estimateDirectSingleQubitMetrics(Operation* op, const SingleQubitEmitterSpec& emitter) { - if (isa(op)) { + if (llvm::isa(op)) { return {}; } // ZSXX + direct Rx: `ry`/`r` use a three-gate `rz * rx * rz` sandwich; other // direct paths emit a single native op. const bool threeGate = emitter.mode == SingleQubitMode::ZSXX && - emitter.supportsDirectRx && isa(op); + emitter.supportsDirectRx && llvm::isa(op); const unsigned count = threeGate ? 3U : 1U; return {.numOneQ = count, .numTwoQ = 0, .depth = count}; } diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp index 4512a28d6b..9ff5b73842 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp @@ -237,23 +237,23 @@ runEuler(decomposition::EulerBasis basis, const Eigen::Matrix2cd& matrix) { Value decomposeToZSXX(IRRewriter& rewriter, Operation* op, Value inQubit, bool supportsDirectRx) { - if (isa(op)) { + if (llvm::isa(op)) { return inQubit; } SingleQubitEmitter e{.rewriter = &rewriter, .loc = op->getLoc()}; - if (auto p = dyn_cast(op)) { + if (auto p = llvm::dyn_cast(op)) { return e.rz(inQubit, p.getTheta()); } if (!supportsDirectRx) { return {}; } - if (auto rx = dyn_cast(op)) { + if (auto rx = llvm::dyn_cast(op)) { return rx.getOutputQubit(0); } - if (auto ry = dyn_cast(op)) { + if (auto ry = llvm::dyn_cast(op)) { return e.rz(e.rx(e.rz(inQubit, -HALF_PI), ry.getTheta()), HALF_PI); } - if (auto r = dyn_cast(op)) { + if (auto r = llvm::dyn_cast(op)) { auto negPhi = arith::NegFOp::create(rewriter, op->getLoc(), r.getPhi()).getResult(); return e.rz(e.rx(e.rz(inQubit, negPhi), r.getTheta()), r.getPhi()); @@ -262,29 +262,29 @@ Value decomposeToZSXX(IRRewriter& rewriter, Operation* op, Value inQubit, } Value decomposeToU3(IRRewriter& rewriter, Operation* op, Value inQubit) { - if (isa(op)) { + if (llvm::isa(op)) { return inQubit; } SingleQubitEmitter e{.rewriter = &rewriter, .loc = op->getLoc()}; - if (auto u = dyn_cast(op)) { + if (auto u = llvm::dyn_cast(op)) { return u.getOutputQubit(0); } - if (auto rx = dyn_cast(op)) { + if (auto rx = llvm::dyn_cast(op)) { return e.u(inQubit, rx.getTheta(), e.constF(-HALF_PI), e.constF(HALF_PI)); } - if (auto ry = dyn_cast(op)) { + if (auto ry = llvm::dyn_cast(op)) { return e.u(inQubit, ry.getTheta(), e.constF(0.0), e.constF(0.0)); } - if (auto rz = dyn_cast(op)) { + if (auto rz = llvm::dyn_cast(op)) { return e.u(inQubit, e.constF(0.0), e.constF(0.0), rz.getTheta()); } - if (auto p = dyn_cast(op)) { + if (auto p = llvm::dyn_cast(op)) { return e.u(inQubit, e.constF(0.0), e.constF(0.0), p.getTheta()); } - if (auto u2 = dyn_cast(op)) { + if (auto u2 = llvm::dyn_cast(op)) { return e.u(inQubit, e.constF(HALF_PI), u2.getPhi(), u2.getLambda()); } - if (auto r = dyn_cast(op)) { + if (auto r = llvm::dyn_cast(op)) { auto loc = op->getLoc(); auto phiMinus = arith::AddFOp::create(rewriter, loc, r.getPhi(), e.constF(-HALF_PI)) @@ -396,17 +396,17 @@ Value emitSynthesizedSingleQubitFromMatrix( } Value decomposeToR(IRRewriter& rewriter, Operation* op, Value inQubit) { - if (isa(op)) { + if (llvm::isa(op)) { return inQubit; } SingleQubitEmitter e{.rewriter = &rewriter, .loc = op->getLoc()}; - if (auto r = dyn_cast(op)) { + if (auto r = llvm::dyn_cast(op)) { return r.getOutputQubit(0); } - if (auto rx = dyn_cast(op)) { + if (auto rx = llvm::dyn_cast(op)) { return e.r(inQubit, rx.getTheta(), e.constF(0.0)); } - if (auto ry = dyn_cast(op)) { + if (auto ry = llvm::dyn_cast(op)) { return e.r(inQubit, ry.getTheta(), e.constF(HALF_PI)); } return {}; @@ -414,38 +414,38 @@ Value decomposeToR(IRRewriter& rewriter, Operation* op, Value inQubit) { Value decomposeToAxisPair(IRRewriter& rewriter, Operation* op, Value inQubit, AxisPair axisPair) { - if (isa(op)) { + if (llvm::isa(op)) { return inQubit; } SingleQubitEmitter e{.rewriter = &rewriter, .loc = op->getLoc()}; switch (axisPair) { case AxisPair::RxRz: - if (auto rx = dyn_cast(op)) { + if (auto rx = llvm::dyn_cast(op)) { return rx.getOutputQubit(0); } - if (auto rz = dyn_cast(op)) { + if (auto rz = llvm::dyn_cast(op)) { return rz.getOutputQubit(0); } - if (auto p = dyn_cast(op)) { + if (auto p = llvm::dyn_cast(op)) { return e.rz(inQubit, p.getTheta()); } return {}; case AxisPair::RxRy: - if (auto rx = dyn_cast(op)) { + if (auto rx = llvm::dyn_cast(op)) { return rx.getOutputQubit(0); } - if (auto ry = dyn_cast(op)) { + if (auto ry = llvm::dyn_cast(op)) { return ry.getOutputQubit(0); } return {}; case AxisPair::RyRz: - if (auto ry = dyn_cast(op)) { + if (auto ry = llvm::dyn_cast(op)) { return ry.getOutputQubit(0); } - if (auto rz = dyn_cast(op)) { + if (auto rz = llvm::dyn_cast(op)) { return rz.getOutputQubit(0); } - if (auto p = dyn_cast(op)) { + if (auto p = llvm::dyn_cast(op)) { return e.rz(inQubit, p.getTheta()); } return {}; diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp index 6b629c5e15..b8f8e33c81 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include @@ -330,7 +331,7 @@ LogicalResult rewriteXXPlusMinusYYViaRxxRyy(IRRewriter& rewriter, // unitaries; the distinct order and sign below are what makes `XXMinusYY` // the "minus" variant and must be preserved even though an order flip // alone would also compile.) - if (auto xxPlus = dyn_cast(op)) { + if (auto xxPlus = llvm::dyn_cast(op)) { Value q0 = xxPlus.getInputQubit(0); Value q1 = xxPlus.getInputQubit(1); q0 = RZOp::create(rewriter, loc, q0, neg(xxPlus.getBeta())) @@ -342,7 +343,7 @@ LogicalResult rewriteXXPlusMinusYYViaRxxRyy(IRRewriter& rewriter, rewriter.replaceOp(op, ValueRange{q0, q1}); return success(); } - if (auto xxMinus = dyn_cast(op)) { + if (auto xxMinus = llvm::dyn_cast(op)) { Value q0 = xxMinus.getInputQubit(0); Value q1 = xxMinus.getInputQubit(1); q0 = RZOp::create(rewriter, loc, q0, neg(xxMinus.getBeta())) diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp index fed7c8a680..551b344fb5 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -89,27 +90,27 @@ bool getNormalizedTwoQubitMatrix(UnitaryOpInterface unitary, } bool getBlockTwoQubitMatrix(Operation* op, Eigen::Matrix4cd& matrix) { - if (isa(op)) { + if (llvm::isa(op)) { return false; } - if (auto ctrl = dyn_cast(op)) { + if (auto ctrl = llvm::dyn_cast(op)) { if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { return false; } auto* body = ctrl.getBodyUnitary().getOperation(); - if (isa(body)) { + if (llvm::isa(body)) { // CX matrix in the same 4x4 basis layout as ``getUnitaryMatrix4x4``. matrix << 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0; return true; } - if (isa(body)) { + if (llvm::isa(body)) { matrix = Eigen::Matrix4cd::Identity(); matrix(3, 3) = -1.0; return true; } return false; } - auto unitary = dyn_cast(op); + auto unitary = llvm::dyn_cast(op); if (!unitary || !unitary.isTwoQubit()) { return false; } diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_basis_decomposer.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_basis_decomposer.cpp index abc371076a..cb984faec1 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_basis_decomposer.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_basis_decomposer.cpp @@ -34,6 +34,7 @@ using namespace mlir::qco; using namespace mlir::qco::decomposition; using namespace mlir::qco::decomposition_test; +// NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_P` at global scope class BasisDecomposerTest : public testing::TestWithParam, Eigen::Matrix4cd (*)()>> { diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_get_gate_kind.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_get_gate_kind.cpp index aa7e803cfc..2247bd474d 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_get_gate_kind.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_get_gate_kind.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -28,6 +29,7 @@ using namespace mlir; using namespace mlir::qco; +// NOLINTNEXTLINE(misc-use-internal-linkage) class DecompositionGetGateKindTest : public ::testing::Test { protected: MLIRContext context; @@ -53,8 +55,9 @@ TEST_F(DecompositionGetGateKindTest, MapsBareSingleQubitOps) { return WalkResult::interrupt(); }); ASSERT_TRUE(rx); - EXPECT_EQ(helpers::getGateKind(cast(rx.getOperation())), - decomposition::GateKind::RX); + EXPECT_EQ( + helpers::getGateKind(llvm::cast(rx.getOperation())), + decomposition::GateKind::RX); } TEST_F(DecompositionGetGateKindTest, MapsCtrlBodyNotWrapper) { @@ -62,7 +65,7 @@ TEST_F(DecompositionGetGateKindTest, MapsCtrlBodyNotWrapper) { Value t = builder.staticQubit(1); auto [cOut, tOut] = builder.ctrl(ValueRange{c}, ValueRange{t}, - [&](ValueRange targets) -> SmallVector { + [&](ValueRange targets) -> llvm::SmallVector { return {builder.z(targets[0])}; }); (void)cOut; @@ -75,6 +78,7 @@ TEST_F(DecompositionGetGateKindTest, MapsCtrlBodyNotWrapper) { return WalkResult::interrupt(); }); ASSERT_TRUE(ctrl); - EXPECT_EQ(helpers::getGateKind(cast(ctrl.getOperation())), - decomposition::GateKind::Z); + EXPECT_EQ( + helpers::getGateKind(llvm::cast(ctrl.getOperation())), + decomposition::GateKind::Z); } diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp index 774cd61752..fc165d9b60 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -52,6 +52,7 @@ static bool sequenceMatchesSingleQubitMatrix(const Eigen::Matrix2cd& u, return expanded.isApprox(seq.getUnitaryMatrix(), tol); } +// NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_P` at global scope class EulerDecompositionTest : public testing::TestWithParam< std::tuple> { diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index 7dc9b088db..b61d66020d 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -25,6 +25,7 @@ using namespace mlir::qco; using namespace mlir::qco::decomposition; using namespace mlir::qco::decomposition_test; +// NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_P` at global scope class WeylDecompositionTest : public testing::TestWithParam { public: diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_pass_test_fixture.h b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_pass_test_fixture.h index a1ff1a1be6..19f4d4419e 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_pass_test_fixture.h +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_pass_test_fixture.h @@ -21,6 +21,7 @@ #include "qc_programs.h" #include +#include #include #include #include @@ -61,20 +62,20 @@ class NativeSynthesisPassTest : public testing::Test { bool ok = true; std::ignore = moduleOp->walk([&](mlir::qco::UnitaryOpInterface op) { mlir::Operation* raw = op.getOperation(); - if (mlir::isa_and_present(raw->getParentOp())) { + if (llvm::isa_and_present(raw->getParentOp())) { return mlir::WalkResult::advance(); } - if (mlir::isa(raw)) { + if (llvm::isa(raw)) { return mlir::WalkResult::advance(); } - if (auto ctrl = mlir::dyn_cast(raw)) { + if (auto ctrl = llvm::dyn_cast(raw)) { if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { ok = false; return mlir::WalkResult::interrupt(); } mlir::Operation* body = ctrl.getBodyUnitary().getOperation(); - const bool isCx = mlir::isa(body); - const bool isCz = mlir::isa(body); + const bool isCx = llvm::isa(body); + const bool isCz = llvm::isa(body); if ((isCx && allowCx) || (isCz && allowCz)) { return mlir::WalkResult::advance(); } @@ -82,7 +83,7 @@ class NativeSynthesisPassTest : public testing::Test { return mlir::WalkResult::interrupt(); } - if (!mlir::isa(raw)) { + if (!llvm::isa(raw)) { ok = false; return mlir::WalkResult::interrupt(); } diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp index d9c66241ed..43a48b78df 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp @@ -10,6 +10,7 @@ #include "native_synthesis_test_helpers.h" +#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" @@ -18,11 +19,16 @@ #include #include #include +#include +#include +#include #include #include #include +#include #include +#include using namespace mlir; @@ -98,7 +104,7 @@ std::optional evaluateConstF64(Value value) { /// Extract the 2x2 unitary matrix associated with a single-qubit op. bool extractSingleQubitMatrix(qco::UnitaryOpInterface op, Eigen::Matrix2cd& out) { - if (auto rz = dyn_cast(op.getOperation())) { + if (auto rz = llvm::dyn_cast(op.getOperation())) { auto theta = evaluateConstF64(rz.getTheta()); if (!theta) { return false; @@ -106,7 +112,7 @@ bool extractSingleQubitMatrix(qco::UnitaryOpInterface op, out = qco::decomposition::rzMatrix(*theta); return true; } - if (auto rx = dyn_cast(op.getOperation())) { + if (auto rx = llvm::dyn_cast(op.getOperation())) { auto theta = evaluateConstF64(rx.getTheta()); if (!theta) { return false; @@ -114,7 +120,7 @@ bool extractSingleQubitMatrix(qco::UnitaryOpInterface op, out = qco::decomposition::rxMatrix(*theta); return true; } - if (auto ry = dyn_cast(op.getOperation())) { + if (auto ry = llvm::dyn_cast(op.getOperation())) { auto theta = evaluateConstF64(ry.getTheta()); if (!theta) { return false; @@ -122,7 +128,7 @@ bool extractSingleQubitMatrix(qco::UnitaryOpInterface op, out = qco::decomposition::ryMatrix(*theta); return true; } - if (auto u = dyn_cast(op.getOperation())) { + if (auto u = llvm::dyn_cast(op.getOperation())) { auto theta = evaluateConstF64(u.getTheta()); auto phi = evaluateConstF64(u.getPhi()); auto lambda = evaluateConstF64(u.getLambda()); @@ -132,7 +138,7 @@ bool extractSingleQubitMatrix(qco::UnitaryOpInterface op, out = u3Matrix(*theta, *phi, *lambda); return true; } - if (auto p = dyn_cast(op.getOperation())) { + if (auto p = llvm::dyn_cast(op.getOperation())) { auto lambda = evaluateConstF64(p.getTheta()); if (!lambda) { return false; @@ -140,7 +146,7 @@ bool extractSingleQubitMatrix(qco::UnitaryOpInterface op, out = qco::decomposition::pMatrix(*lambda); return true; } - if (auto r = dyn_cast(op.getOperation())) { + if (auto r = llvm::dyn_cast(op.getOperation())) { auto theta = evaluateConstF64(r.getTheta()); auto phi = evaluateConstF64(r.getPhi()); if (!theta || !phi) { @@ -167,17 +173,17 @@ bool extractSingleQubitMatrix(qco::UnitaryOpInterface op, /// 4×4 unitary for a two-qubit op (same layout as ``getUnitaryMatrix4x4``). bool extractTwoQubitMatrix(qco::UnitaryOpInterface op, Eigen::Matrix4cd& out) { - if (auto ctrl = dyn_cast(op.getOperation())) { + if (auto ctrl = llvm::dyn_cast(op.getOperation())) { if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { return false; } auto* body = ctrl.getBodyUnitary().getOperation(); - if (isa(body)) { + if (llvm::isa(body)) { out = Eigen::Matrix4cd::Identity(); out(3, 3) = -1.0; return true; } - if (isa(body)) { + if (llvm::isa(body)) { out << 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0; return true; } @@ -207,7 +213,7 @@ computeTwoQubitUnitaryFromModule(const OwningOpRef& moduleOp) { for (auto func : module.getOps()) { for (auto& block : func.getBlocks()) { for (auto& rawOp : block.getOperations()) { - if (auto alloc = dyn_cast(&rawOp)) { + if (auto alloc = llvm::dyn_cast(&rawOp)) { if (nextQubitId >= 2) { return std::nullopt; } @@ -228,11 +234,11 @@ computeTwoQubitUnitaryFromModule(const OwningOpRef& moduleOp) { for (auto func : module.getOps()) { for (auto& block : func.getBlocks()) { for (auto& rawOp : block.getOperations()) { - auto op = dyn_cast(&rawOp); + auto op = llvm::dyn_cast(&rawOp); if (!op) { continue; } - if (isa(op.getOperation())) { + if (llvm::isa(op.getOperation())) { continue; } @@ -343,12 +349,12 @@ computeNQubitUnitaryFromModule(const OwningOpRef& moduleOp, for (auto func : module.getOps()) { for (auto& block : func.getBlocks()) { for (auto& rawOp : block.getOperations()) { - if (auto alloc = dyn_cast(&rawOp)) { + if (auto alloc = llvm::dyn_cast(&rawOp)) { if (numQubits >= maxQubits) { return std::nullopt; } qubitIds.try_emplace(alloc.getResult(), numQubits++); - } else if (auto staticOp = dyn_cast(&rawOp)) { + } else if (auto staticOp = llvm::dyn_cast(&rawOp)) { const auto idx = static_cast(staticOp.getIndex()); if (idx >= maxQubits) { return std::nullopt; @@ -378,11 +384,11 @@ computeNQubitUnitaryFromModule(const OwningOpRef& moduleOp, for (auto func : module.getOps()) { for (auto& block : func.getBlocks()) { for (auto& rawOp : block.getOperations()) { - auto op = dyn_cast(&rawOp); + auto op = llvm::dyn_cast(&rawOp); if (!op) { continue; } - if (isa(op.getOperation())) { + if (llvm::isa(op.getOperation())) { continue; } diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_policy.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_policy.cpp index 6634695f2b..63f379ec7b 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_policy.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_policy.cpp @@ -16,13 +16,17 @@ #include "mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" #include #include +#include #include #include #include #include +#include +#include #include using namespace mlir; @@ -56,6 +60,7 @@ TEST(NativePolicyTest, UsesCxAndCzFromResolvedSpec) { EXPECT_TRUE(usesCzEntangler(*both)); } +// NOLINTNEXTLINE(misc-use-internal-linkage) class NativePolicyAllowsOpTest : public ::testing::Test { protected: MLIRContext context; @@ -83,8 +88,8 @@ TEST_F(NativePolicyAllowsOpTest, AllowsSingleQubitOpRespectsMenu) { return WalkResult::interrupt(); }); ASSERT_TRUE(xop); - EXPECT_TRUE( - allowsSingleQubitOp(cast(xop.getOperation()), *spec)); + EXPECT_TRUE(allowsSingleQubitOp( + llvm::cast(xop.getOperation()), *spec)); } TEST_F(NativePolicyAllowsOpTest, CanDirectlyDecomposeToU3OnRxInCircuit) { diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_custom_menus.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_custom_menus.cpp index 917a16add2..5966500ca8 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_custom_menus.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_custom_menus.cpp @@ -12,8 +12,22 @@ // circuits for the native-gate synthesis pass. #include "native_synthesis_pass_test_fixture.h" +#include "native_synthesis_test_helpers.h" +#include "qc_programs.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include #include #include @@ -28,7 +42,23 @@ using namespace mlir::qco::native_synth_test; namespace { -std::vector splitCSV(const std::string& s) { +struct CustomMenuSpec { + std::string menuCsv; + bool allowCx = false; + bool allowCz = false; + bool allowU = false; + bool allowX = false; + bool allowSX = false; + bool allowRZ = false; + bool allowRX = false; + bool allowRY = false; + bool allowR = false; + bool allowRzz = false; +}; + +} // namespace + +static std::vector splitCSV(const std::string& s) { std::vector out; std::string cur; for (const char ch : s) { @@ -50,21 +80,7 @@ std::vector splitCSV(const std::string& s) { return out; } -struct CustomMenuSpec { - std::string menuCsv; - bool allowCx = false; - bool allowCz = false; - bool allowU = false; - bool allowX = false; - bool allowSX = false; - bool allowRZ = false; - bool allowRX = false; - bool allowRY = false; - bool allowR = false; - bool allowRzz = false; -}; - -CustomMenuSpec parseCustomMenu(const std::string& csv) { +static CustomMenuSpec parseCustomMenu(const std::string& csv) { CustomMenuSpec spec; spec.menuCsv = csv; for (const auto& tok : splitCSV(csv)) { @@ -95,87 +111,88 @@ CustomMenuSpec parseCustomMenu(const std::string& csv) { return spec; } -bool onlyAllowsMenuNativeOps(ModuleOp moduleOp, const CustomMenuSpec& spec) { +static bool onlyAllowsMenuNativeOps(ModuleOp moduleOp, + const CustomMenuSpec& spec) { bool ok = true; moduleOp.walk([&](Operation* op) { if (!ok) { return; } - if (!isa(op)) { + if (!llvm::isa(op)) { return; } // Non-synthesized helper ops are allowed to remain. - if (isa(op)) { + if (llvm::isa(op)) { return; } - if (isa(op)) { + if (llvm::isa(op)) { return; } // Treat `p` as a phase/Z-rotation alias when `rz` is allowed. - if (isa(op)) { + if (llvm::isa(op)) { ok = spec.allowRZ; return; } - if (isa(op)) { + if (llvm::isa(op)) { ok = spec.allowU; return; } - if (isa(op)) { + if (llvm::isa(op)) { // `cx` is represented as a `qco.ctrl` with a `qco.x` in the body region. - if (isa_and_present(op->getParentOp())) { + if (llvm::isa_and_present(op->getParentOp())) { ok = spec.allowCx; } else { ok = spec.allowX; } return; } - if (isa(op)) { + if (llvm::isa(op)) { ok = spec.allowSX; return; } - if (isa(op)) { + if (llvm::isa(op)) { ok = spec.allowRZ; return; } - if (isa(op)) { + if (llvm::isa(op)) { // Some decomposition paths treat `rx(pi)` as an `x`-family primitive. ok = spec.allowRX || (spec.allowX && spec.allowSX && spec.allowRZ); return; } - if (isa(op)) { + if (llvm::isa(op)) { ok = spec.allowRY; return; } - if (isa(op)) { + if (llvm::isa(op)) { // `cz` is represented as a `qco.ctrl` with a `qco.z` in the body region. - if (isa_and_present(op->getParentOp())) { + if (llvm::isa_and_present(op->getParentOp())) { ok = spec.allowCz; } else { ok = false; } return; } - if (isa(op)) { + if (llvm::isa(op)) { ok = spec.allowR; return; } - if (isa(op)) { + if (llvm::isa(op)) { ok = spec.allowRzz; return; } - if (auto ctrl = dyn_cast(op)) { + if (auto ctrl = llvm::dyn_cast(op)) { if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { ok = false; return; } Operation* body = ctrl.getBodyUnitary().getOperation(); - if (isa(body)) { + if (llvm::isa(body)) { ok = spec.allowCx; return; } - if (isa(body)) { + if (llvm::isa(body)) { ok = spec.allowCz; return; } @@ -187,8 +204,6 @@ bool onlyAllowsMenuNativeOps(ModuleOp moduleOp, const CustomMenuSpec& spec) { return ok; } -} // namespace - TEST_F(NativeSynthesisPassTest, RandomizedCustomMenusAndCircuitsAreEquivalent) { // Sample many valid custom menus and generate matching random input circuits. // For each case, we assert that native synthesis (a) succeeds, (b) emits only diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_fusion.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_fusion.cpp index f46e617c2d..7324ccc094 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_fusion.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_fusion.cpp @@ -15,8 +15,20 @@ // ``mqt-core-mlir-unittest-native-synthesis``. #include "native_synthesis_pass_test_fixture.h" +#include "native_synthesis_test_helpers.h" +#include "qc_programs.h" + +#include +#include +#include +#include +#include +#include +#include +#include #include +#include #include #include @@ -25,18 +37,6 @@ using namespace mlir::qco; using namespace mlir::qco::native_synth_test; namespace { -// Count ops of a given MLIR op type across a module; used to assert the -// effects of the 1q-run-merging pre-synthesis step on concrete programs. -template -std::size_t countOpsOfTypeInModule(const OwningOpRef& moduleOp) { - std::size_t count = 0; - moduleOp.get()->walk([&](mlir::Operation* op) { - if (isa(op)) { - ++count; - } - }); - return count; -} struct OneQU3FusionGPhaseRow { const char* name; @@ -49,8 +49,24 @@ struct TwoQBlockEquivGenericU3CxRow { void (*program)(mlir::qc::QCProgramBuilder&); std::optional expectExactCtrlOpCount; }; + } // namespace +// Count ops of a given MLIR op type across a module; used to assert the +// effects of the 1q-run-merging pre-synthesis step on concrete programs. +template +static std::size_t +countOpsOfTypeInModule(const OwningOpRef& moduleOp) { + std::size_t count = 0; + moduleOp.get()->walk([&](Operation* op) { + if (llvm::isa(op)) { + ++count; + } + }); + return count; +} + +// NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_P` at global scope class NativeSynthesisOneQFusionU3GPhaseTest : public NativeSynthesisPassTest, public testing::WithParamInterface { @@ -81,6 +97,7 @@ INSTANTIATE_TEST_SUITE_P( return info.param.name; }); +// NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_P` at global scope class NativeSynthesisTwoQBlockEquivGenericU3CxTest : public NativeSynthesisPassTest, public testing::WithParamInterface { diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_multi_qubit.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_multi_qubit.cpp index f0eb51e7ca..891a270cf7 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_multi_qubit.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_multi_qubit.cpp @@ -12,6 +12,14 @@ // native-gate synthesis pass. #include "native_synthesis_pass_test_fixture.h" +#include "native_synthesis_test_helpers.h" +#include "qc_programs.h" + +#include +#include +#include +#include +#include #include @@ -19,28 +27,29 @@ using namespace mlir; using namespace mlir::qco; using namespace mlir::qco::native_synth_test; -namespace { - -OwningOpRef buildThreeQGhzCircuit(MLIRContext* context) { +static OwningOpRef buildThreeQGhzCircuit(MLIRContext* context) { return mlir::qc::QCProgramBuilder::build( context, mlir::qc::nativeSynthMultiQThreeQGhz); } -OwningOpRef buildThreeQToffoliCircuit(MLIRContext* context) { +static OwningOpRef buildThreeQToffoliCircuit(MLIRContext* context) { return mlir::qc::QCProgramBuilder::build( context, mlir::qc::nativeSynthMultiQThreeQToffoli); } -OwningOpRef buildThreeQQftCircuit(MLIRContext* context) { +static OwningOpRef buildThreeQQftCircuit(MLIRContext* context) { return mlir::qc::QCProgramBuilder::build( context, mlir::qc::nativeSynthMultiQThreeQQft); } -OwningOpRef buildThreeQCliffordTMixCircuit(MLIRContext* context) { +static OwningOpRef +buildThreeQCliffordTMixCircuit(MLIRContext* context) { return mlir::qc::QCProgramBuilder::build( context, mlir::qc::nativeSynthMultiQThreeQCliffordTMix); } +namespace { + struct ThreeQubitCircuitCase { const char* name; OwningOpRef (*build)(MLIRContext*); @@ -86,15 +95,11 @@ TEST_F(NativeSynthesisPassTest, ThreeQubitCircuitsEquivalentAcrossProfiles) { } } -namespace { - -OwningOpRef buildFiveQubitStressCircuit(MLIRContext* context) { +static OwningOpRef buildFiveQubitStressCircuit(MLIRContext* context) { return mlir::qc::QCProgramBuilder::build( context, mlir::qc::nativeSynthMultiQFiveQStressFourLayers); } -} // namespace - TEST_F(NativeSynthesisPassTest, FiveQubitStressCircuitEquivalentAcrossProfiles) { const auto profiles = NativeSynthesisPassTest::allNineEquivalenceProfiles(); diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_scoring.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_scoring.cpp index cb4930871e..1fb45083f6 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_scoring.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_scoring.cpp @@ -42,7 +42,7 @@ countSingleAndTwoQubitUnitariesForXxRzzMetrics(ModuleOp module) { if (isa_and_present(op->getParentOp())) { return; } - auto unitary = dyn_cast(op); + auto unitary = llvm::dyn_cast(op); if (!unitary) { return; } From d30e3f1500105e3cf49de04169e028ad9a38dc1f Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Fri, 24 Apr 2026 18:06:09 +0200 Subject: [PATCH 018/122] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20linter=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/Decomposition/Helpers.cpp | 1 + .../test_native_synthesis_pass_fusion.cpp | 1 - .../test_native_synthesis_pass_profiles.cpp | 17 +++++++++++ .../test_native_synthesis_pass_scoring.cpp | 28 ++++++++++++++----- mlir/unittests/programs/qc_programs.cpp | 26 ++++++++--------- 5 files changed, 51 insertions(+), 22 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp index d797308c8f..3382034907 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp @@ -14,6 +14,7 @@ #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" +#include #include #include diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_fusion.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_fusion.cpp index 7324ccc094..5e784f4ca9 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_fusion.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_fusion.cpp @@ -23,7 +23,6 @@ #include #include #include -#include #include #include diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_profiles.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_profiles.cpp index 9ce2b2d018..3ee930d447 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_profiles.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_profiles.cpp @@ -9,6 +9,18 @@ */ #include "native_synthesis_pass_test_fixture.h" +#include "native_synthesis_test_helpers.h" +#include "qc_programs.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include using namespace mlir; using namespace mlir::qco; @@ -26,6 +38,7 @@ struct NativeSynthMenuRow { } // namespace +// NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_P` at global scope class NativeSynthesisSwapProfileTest : public NativeSynthesisPassTest, public testing::WithParamInterface { @@ -72,6 +85,7 @@ INSTANTIATE_TEST_SUITE_P( return info.param.name; }); +// NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_P` at global scope class NativeSynthesisHstycxMenuTest : public NativeSynthesisPassTest, public testing::WithParamInterface { @@ -101,6 +115,7 @@ INSTANTIATE_TEST_SUITE_P( return info.param.name; }); +// NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_P` at global scope class NativeSynthesisCxYOnQ1MenuTest : public NativeSynthesisPassTest, public testing::WithParamInterface { @@ -131,6 +146,7 @@ INSTANTIATE_TEST_SUITE_P( return info.param.name; }); +// NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_P` at global scope class NativeSynthesisBroadOneQMenuTest : public NativeSynthesisPassTest, public testing::WithParamInterface { @@ -163,6 +179,7 @@ INSTANTIATE_TEST_SUITE_P( return info.param.name; }); +// NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_P` at global scope class NativeSynthesisZeroAngleMenuTest : public NativeSynthesisPassTest, public testing::WithParamInterface { diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_scoring.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_scoring.cpp index 1fb45083f6..d25f4dbe97 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_scoring.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_scoring.cpp @@ -15,12 +15,26 @@ #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Scoring.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h" #include "native_synthesis_pass_test_fixture.h" +#include "qc_programs.h" +#include #include #include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include +#include #include +#include using namespace mlir; using namespace mlir::qco; @@ -31,15 +45,17 @@ namespace { /// Dummy payload: scoring helpers do not inspect the type. struct ScoringTag {}; -std::pair +} // namespace + +static std::pair countSingleAndTwoQubitUnitariesForXxRzzMetrics(ModuleOp module) { unsigned numOneQ = 0; unsigned numTwoQ = 0; module.walk([&](Operation* op) { - if (isa(op)) { + if (llvm::isa(op)) { return; } - if (isa_and_present(op->getParentOp())) { + if (llvm::isa_and_present(op->getParentOp())) { return; } auto unitary = llvm::dyn_cast(op); @@ -57,8 +73,6 @@ countSingleAndTwoQubitUnitariesForXxRzzMetrics(ModuleOp module) { return {numOneQ, numTwoQ}; } -} // namespace - TEST(NativeSynthesisScoringTest, ValidScoreWeights) { using namespace mlir::qco::native_synth; EXPECT_TRUE(areValidScoreWeights(ScoreWeights{})); @@ -202,7 +216,7 @@ TEST(NativeSynthesisScoringTest, SelectBestCandidateHonoursWeightPreferences) { const ScoreWeights heavyTwoQ{.twoQ = 10.0, .oneQ = 0.01, .depth = 0.0}; EXPECT_EQ(selectBestCandidate(llvm::ArrayRef(candidates), heavyTwoQ), - candidates.data() + 1); + &candidates[1]); } TEST(NativeSynthesisScoringTest, @@ -233,7 +247,7 @@ TEST_F(NativeSynthesisPassTest, XxPlusMinusYyEmittedCountsMatchScoringMetrics) { Operation* twoQOp = nullptr; module->walk([&](Operation* op) { - if (isa(op)) { + if (llvm::isa(op)) { twoQOp = op; return WalkResult::interrupt(); } diff --git a/mlir/unittests/programs/qc_programs.cpp b/mlir/unittests/programs/qc_programs.cpp index 73fad59c3d..4c71f3893b 100644 --- a/mlir/unittests/programs/qc_programs.cpp +++ b/mlir/unittests/programs/qc_programs.cpp @@ -1281,10 +1281,9 @@ void invCtrlSandwich(QCProgramBuilder& b) { }); } -namespace { - -void emitNativeSynthControlledPhase(QCProgramBuilder& b, const double theta, - mlir::Value ctrl, mlir::Value tgt) { +static void emitNativeSynthControlledPhase(QCProgramBuilder& b, + const double theta, mlir::Value ctrl, + mlir::Value tgt) { b.p(theta / 2.0, ctrl); b.cx(ctrl, tgt); b.p(-theta / 2.0, tgt); @@ -1292,8 +1291,8 @@ void emitNativeSynthControlledPhase(QCProgramBuilder& b, const double theta, b.p(theta / 2.0, tgt); } -void emitNativeSynthToffoli(QCProgramBuilder& b, mlir::Value c1, mlir::Value c2, - mlir::Value t) { +static void emitNativeSynthToffoli(QCProgramBuilder& b, mlir::Value c1, + mlir::Value c2, mlir::Value t) { b.h(t); b.cx(c2, t); b.tdg(t); @@ -1314,8 +1313,9 @@ void emitNativeSynthToffoli(QCProgramBuilder& b, mlir::Value c1, mlir::Value c2, /// Shared by ``nativeSynthBroadOneQCanonicalization`` and /// ``nativeSynthIbmFractionalAllGateFamilies``: wide 1q sweep on two qubits, /// ending before any two-qubit primitive. -void emitNativeSynthFixtureBroad1qPrefix(QCProgramBuilder& b, mlir::Value q0, - mlir::Value q1) { +static void emitNativeSynthFixtureBroad1qPrefix(QCProgramBuilder& b, + mlir::Value q0, + mlir::Value q1) { b.id(q0); b.x(q0); b.y(q1); @@ -1334,8 +1334,8 @@ void emitNativeSynthFixtureBroad1qPrefix(QCProgramBuilder& b, mlir::Value q0, b.r(0.61, -0.22, q0); } -void emitNativeSynthFiveQStressLayers(QCProgramBuilder& b, - const int numLayers) { +static void emitNativeSynthFiveQStressLayers(QCProgramBuilder& b, + const int numLayers) { const auto q0 = b.allocQubit(); const auto q1 = b.allocQubit(); const auto q2 = b.allocQubit(); @@ -1374,8 +1374,8 @@ void emitNativeSynthFiveQStressLayers(QCProgramBuilder& b, b.p(0.75, q4); } -void emitNativeSynthTwoQRzx(QCProgramBuilder& b, const double theta, - const bool controlOnFirstWire) { +static void emitNativeSynthTwoQRzx(QCProgramBuilder& b, const double theta, + const bool controlOnFirstWire) { const auto q0 = b.allocQubit(); const auto q1 = b.allocQubit(); if (controlOnFirstWire) { @@ -1385,8 +1385,6 @@ void emitNativeSynthTwoQRzx(QCProgramBuilder& b, const double theta, } } -} // namespace - void nativeSynthBroadOneQCanonicalization(QCProgramBuilder& b) { const auto q0 = b.allocQubit(); const auto q1 = b.allocQubit(); From 92b3ab482ace8bde40ccb94f1be24349cf53b5ac Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Fri, 24 Apr 2026 18:18:46 +0200 Subject: [PATCH 019/122] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20linter=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NativeSynthesis/test_native_synthesis_pass_scoring.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_scoring.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_scoring.cpp index d25f4dbe97..35ba14825b 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_scoring.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_scoring.cpp @@ -14,6 +14,7 @@ #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Scoring.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" #include "native_synthesis_pass_test_fixture.h" #include "qc_programs.h" From b9bbfea0e98c2bc0477553b11d3a63a27d1bf8b6 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Fri, 24 Apr 2026 18:36:15 +0200 Subject: [PATCH 020/122] =?UTF-8?q?=E2=9C=85=20Introduce=20helper=20functi?= =?UTF-8?q?ons=20for=20retrieving=20unitary=20qubit=20operands=20and=20res?= =?UTF-8?q?ults=20in=20native=20synthesis=20tests.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../native_synthesis_test_helpers.cpp | 88 ++++++++++++++++--- 1 file changed, 76 insertions(+), 12 deletions(-) diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp index 43a48b78df..607e154726 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp @@ -34,6 +34,34 @@ using namespace mlir; namespace mlir::qco::native_synth_test { +namespace { + +[[nodiscard]] std::optional +getUnitaryQubitOperand(qco::UnitaryOpInterface op, std::size_t index) { + if (index >= op.getNumQubits()) { + return std::nullopt; + } + Value v = op->getOperand(index); + if (!llvm::isa(v.getType())) { + return std::nullopt; + } + return v; +} + +[[nodiscard]] std::optional +getUnitaryQubitResult(qco::UnitaryOpInterface op, std::size_t index) { + if (index >= op.getNumQubits()) { + return std::nullopt; + } + Value v = op->getResult(index); + if (!llvm::isa(v.getType())) { + return std::nullopt; + } + return v; +} + +} // namespace + std::complex phasedAmplitude(const double magnitude, const double phase) { return std::complex(magnitude, 0.0) * @@ -243,7 +271,11 @@ computeTwoQubitUnitaryFromModule(const OwningOpRef& moduleOp) { } if (op.isSingleQubit()) { - auto qid = getQubitId(op.getInputQubit(0)); + const auto qIn = getUnitaryQubitOperand(op, 0); + if (!qIn) { + return std::nullopt; + } + auto qid = getQubitId(*qIn); if (!qid) { return std::nullopt; } @@ -252,13 +284,22 @@ computeTwoQubitUnitaryFromModule(const OwningOpRef& moduleOp) { return std::nullopt; } unitary = qco::decomposition::expandToTwoQubits(oneQ, *qid) * unitary; - qubitIds[op.getOutputQubit(0)] = *qid; + const auto qOut = getUnitaryQubitResult(op, 0); + if (!qOut) { + return std::nullopt; + } + qubitIds[*qOut] = *qid; continue; } if (op.isTwoQubit()) { - auto q0id = getQubitId(op.getInputQubit(0)); - auto q1id = getQubitId(op.getInputQubit(1)); + const auto q0In = getUnitaryQubitOperand(op, 0); + const auto q1In = getUnitaryQubitOperand(op, 1); + if (!q0In || !q1In) { + return std::nullopt; + } + auto q0id = getQubitId(*q0In); + auto q1id = getQubitId(*q1In); if (!q0id || !q1id) { return std::nullopt; } @@ -268,8 +309,13 @@ computeTwoQubitUnitaryFromModule(const OwningOpRef& moduleOp) { } unitary = expandTwoQToN(twoQ, *q0id, *q1id, /*numQubits=*/2) * unitary; - qubitIds[op.getOutputQubit(0)] = *q0id; - qubitIds[op.getOutputQubit(1)] = *q1id; + const auto q0Out = getUnitaryQubitResult(op, 0); + const auto q1Out = getUnitaryQubitResult(op, 1); + if (!q0Out || !q1Out) { + return std::nullopt; + } + qubitIds[*q0Out] = *q0id; + qubitIds[*q1Out] = *q1id; continue; } } @@ -393,7 +439,11 @@ computeNQubitUnitaryFromModule(const OwningOpRef& moduleOp, } if (op.isSingleQubit()) { - auto qid = getQubitId(op.getInputQubit(0)); + const auto qIn = getUnitaryQubitOperand(op, 0); + if (!qIn) { + return std::nullopt; + } + auto qid = getQubitId(*qIn); if (!qid) { return std::nullopt; } @@ -402,13 +452,22 @@ computeNQubitUnitaryFromModule(const OwningOpRef& moduleOp, return std::nullopt; } unitary = expandOneQToN(oneQ, *qid, numQubits) * unitary; - qubitIds[op.getOutputQubit(0)] = *qid; + const auto qOut = getUnitaryQubitResult(op, 0); + if (!qOut) { + return std::nullopt; + } + qubitIds[*qOut] = *qid; continue; } if (op.isTwoQubit()) { - auto q0id = getQubitId(op.getInputQubit(0)); - auto q1id = getQubitId(op.getInputQubit(1)); + const auto q0In = getUnitaryQubitOperand(op, 0); + const auto q1In = getUnitaryQubitOperand(op, 1); + if (!q0In || !q1In) { + return std::nullopt; + } + auto q0id = getQubitId(*q0In); + auto q1id = getQubitId(*q1In); if (!q0id || !q1id) { return std::nullopt; } @@ -417,8 +476,13 @@ computeNQubitUnitaryFromModule(const OwningOpRef& moduleOp, return std::nullopt; } unitary = expandTwoQToN(twoQ, *q0id, *q1id, numQubits) * unitary; - qubitIds[op.getOutputQubit(0)] = *q0id; - qubitIds[op.getOutputQubit(1)] = *q1id; + const auto q0Out = getUnitaryQubitResult(op, 0); + const auto q1Out = getUnitaryQubitResult(op, 1); + if (!q0Out || !q1Out) { + return std::nullopt; + } + qubitIds[*q0Out] = *q0id; + qubitIds[*q1Out] = *q1id; continue; } } From 170dee3dc1dd8d6d8f89d7e505bc19efe6b3f2f9 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Fri, 24 Apr 2026 18:36:27 +0200 Subject: [PATCH 021/122] =?UTF-8?q?=F0=9F=94=A7=20Update=20qubit=20compari?= =?UTF-8?q?son=20logic=20in=20mergeTwoTargetOneParameter=20to=20ensure=20b?= =?UTF-8?q?oth=20output=20and=20input=20qubits=20match=20correctly.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/include/mlir/Dialect/QCO/QCOUtils.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mlir/include/mlir/Dialect/QCO/QCOUtils.h b/mlir/include/mlir/Dialect/QCO/QCOUtils.h index f888509129..d642f2683d 100644 --- a/mlir/include/mlir/Dialect/QCO/QCOUtils.h +++ b/mlir/include/mlir/Dialect/QCO/QCOUtils.h @@ -188,7 +188,8 @@ mlir::LogicalResult mergeTwoTargetOneParameter(OpType op, } // Confirm operations act on the same qubits - if (op.getOutputQubit(1) != nextOp.getInputQubit(1)) { + if (op.getOutputQubit(0) != nextOp.getInputQubit(0) || + op.getOutputQubit(1) != nextOp.getInputQubit(1)) { return failure(); } From 36cf394e16131f383e22a8bda5eadc51dc4cc60f Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Fri, 24 Apr 2026 18:50:09 +0200 Subject: [PATCH 022/122] =?UTF-8?q?=F0=9F=94=A7=20Refactor=20single-qubit?= =?UTF-8?q?=20matrix=20extraction=20logic=20to=20use=20raw=20operation=20p?= =?UTF-8?q?ointers=20and=20ensure=20operand=20count=20validation=20for=20R?= =?UTF-8?q?Z,=20RX,=20RY,=20U,=20P,=20and=20R=20operations=20in=20native?= =?UTF-8?q?=20synthesis=20tests.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../native_synthesis_test_helpers.cpp | 54 +++++++++++++------ 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp index 607e154726..003ce7fc18 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp @@ -132,51 +132,75 @@ std::optional evaluateConstF64(Value value) { /// Extract the 2x2 unitary matrix associated with a single-qubit op. bool extractSingleQubitMatrix(qco::UnitaryOpInterface op, Eigen::Matrix2cd& out) { - if (auto rz = llvm::dyn_cast(op.getOperation())) { - auto theta = evaluateConstF64(rz.getTheta()); + if (llvm::isa(op.getOperation())) { + auto* raw = op.getOperation(); + if (raw->getNumOperands() < 2) { + return false; + } + auto theta = evaluateConstF64(raw->getOperand(1)); if (!theta) { return false; } out = qco::decomposition::rzMatrix(*theta); return true; } - if (auto rx = llvm::dyn_cast(op.getOperation())) { - auto theta = evaluateConstF64(rx.getTheta()); + if (llvm::isa(op.getOperation())) { + auto* raw = op.getOperation(); + if (raw->getNumOperands() < 2) { + return false; + } + auto theta = evaluateConstF64(raw->getOperand(1)); if (!theta) { return false; } out = qco::decomposition::rxMatrix(*theta); return true; } - if (auto ry = llvm::dyn_cast(op.getOperation())) { - auto theta = evaluateConstF64(ry.getTheta()); + if (llvm::isa(op.getOperation())) { + auto* raw = op.getOperation(); + if (raw->getNumOperands() < 2) { + return false; + } + auto theta = evaluateConstF64(raw->getOperand(1)); if (!theta) { return false; } out = qco::decomposition::ryMatrix(*theta); return true; } - if (auto u = llvm::dyn_cast(op.getOperation())) { - auto theta = evaluateConstF64(u.getTheta()); - auto phi = evaluateConstF64(u.getPhi()); - auto lambda = evaluateConstF64(u.getLambda()); + if (llvm::isa(op.getOperation())) { + auto* raw = op.getOperation(); + if (raw->getNumOperands() < 4) { + return false; + } + auto theta = evaluateConstF64(raw->getOperand(1)); + auto phi = evaluateConstF64(raw->getOperand(2)); + auto lambda = evaluateConstF64(raw->getOperand(3)); if (!theta || !phi || !lambda) { return false; } out = u3Matrix(*theta, *phi, *lambda); return true; } - if (auto p = llvm::dyn_cast(op.getOperation())) { - auto lambda = evaluateConstF64(p.getTheta()); + if (llvm::isa(op.getOperation())) { + auto* raw = op.getOperation(); + if (raw->getNumOperands() < 2) { + return false; + } + auto lambda = evaluateConstF64(raw->getOperand(1)); if (!lambda) { return false; } out = qco::decomposition::pMatrix(*lambda); return true; } - if (auto r = llvm::dyn_cast(op.getOperation())) { - auto theta = evaluateConstF64(r.getTheta()); - auto phi = evaluateConstF64(r.getPhi()); + if (llvm::isa(op.getOperation())) { + auto* raw = op.getOperation(); + if (raw->getNumOperands() < 3) { + return false; + } + auto theta = evaluateConstF64(raw->getOperand(1)); + auto phi = evaluateConstF64(raw->getOperand(2)); if (!theta || !phi) { return false; } From 0b102ae99fa73bb19693b799da96ce1a4459d89f Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Fri, 24 Apr 2026 19:02:10 +0200 Subject: [PATCH 023/122] =?UTF-8?q?=F0=9F=94=A7=20Enhance=20qubit=20compar?= =?UTF-8?q?ison=20logic=20in=20QCO=20operations=20to=20validate=20both=20q?= =?UTF-8?q?ubit=20outputs=20and=20inputs,=20ensuring=20correct=20operation?= =?UTF-8?q?=20merging=20in=20XXMinusYY=20and=20XXPlusYY=20patterns.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/include/mlir/Dialect/QCO/QCOUtils.h | 9 +++++++-- .../QCO/IR/Operations/StandardGates/XXMinusYYOp.cpp | 3 ++- .../QCO/IR/Operations/StandardGates/XXPlusYYOp.cpp | 3 ++- .../NativeSynthesis/native_synthesis_test_helpers.cpp | 9 +++------ 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/QCOUtils.h b/mlir/include/mlir/Dialect/QCO/QCOUtils.h index d642f2683d..6cf5af9d7e 100644 --- a/mlir/include/mlir/Dialect/QCO/QCOUtils.h +++ b/mlir/include/mlir/Dialect/QCO/QCOUtils.h @@ -62,7 +62,8 @@ removeInversePairTwoTargetZeroParameter(OpType op, PatternRewriter& rewriter) { } // Confirm operations act on the same qubits - if (op.getOutputQubit(1) != nextOp.getInputQubit(1)) { + if (op.getOutputQubit(0) != nextOp.getInputQubit(0) || + op.getOutputQubit(1) != nextOp.getInputQubit(1)) { return failure(); } @@ -156,13 +157,17 @@ mlir::LogicalResult mergeOneTargetOneParameter(OpType op, return failure(); } + if (nextOp.getInputQubit(0) != op.getOutputQubit(0)) { + return failure(); + } + // Compute and set the new parameter auto newParameter = arith::AddFOp::create( rewriter, op.getLoc(), op.getOperand(1), nextOp.getOperand(1)); op->setOperand(1, newParameter.getResult()); // Replace the second operation with the result of the first operation - rewriter.replaceOp(nextOp, op.getResult()); + rewriter.replaceOp(nextOp, op.getOutputQubit(0)); return success(); } diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXMinusYYOp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXMinusYYOp.cpp index 2923e39db0..17bce2198a 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXMinusYYOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXMinusYYOp.cpp @@ -51,7 +51,8 @@ struct MergeSubsequentXXMinusYY final : OpRewritePattern { } // Confirm operations act on the same qubits - if (op.getOutputQubit(1) != nextOp.getInputQubit(1)) { + if (op.getOutputQubit(0) != nextOp.getInputQubit(0) || + op.getOutputQubit(1) != nextOp.getInputQubit(1)) { return failure(); } diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXPlusYYOp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXPlusYYOp.cpp index aad0076727..d5e1410cab 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXPlusYYOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXPlusYYOp.cpp @@ -50,7 +50,8 @@ struct MergeSubsequentXXPlusYY final : OpRewritePattern { } // Confirm operations act on the same qubits - if (op.getOutputQubit(1) != nextOp.getInputQubit(1)) { + if (op.getOutputQubit(0) != nextOp.getInputQubit(0) || + op.getOutputQubit(1) != nextOp.getInputQubit(1)) { return failure(); } diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp index 003ce7fc18..a128e8c219 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp @@ -10,6 +10,7 @@ #include "native_synthesis_test_helpers.h" +#include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" @@ -34,9 +35,7 @@ using namespace mlir; namespace mlir::qco::native_synth_test { -namespace { - -[[nodiscard]] std::optional +[[nodiscard]] static std::optional getUnitaryQubitOperand(qco::UnitaryOpInterface op, std::size_t index) { if (index >= op.getNumQubits()) { return std::nullopt; @@ -48,7 +47,7 @@ getUnitaryQubitOperand(qco::UnitaryOpInterface op, std::size_t index) { return v; } -[[nodiscard]] std::optional +[[nodiscard]] static std::optional getUnitaryQubitResult(qco::UnitaryOpInterface op, std::size_t index) { if (index >= op.getNumQubits()) { return std::nullopt; @@ -60,8 +59,6 @@ getUnitaryQubitResult(qco::UnitaryOpInterface op, std::size_t index) { return v; } -} // namespace - std::complex phasedAmplitude(const double magnitude, const double phase) { return std::complex(magnitude, 0.0) * From cece82a7f65ed02623deb90bfe326ad06aee5640 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 27 Apr 2026 11:01:18 +0200 Subject: [PATCH 024/122] =?UTF-8?q?=F0=9F=90=9B=20Fix=20Ubuntu=20Tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/include/mlir/Dialect/QCO/QCOUtils.h | 12 +++--------- .../QCO/IR/Operations/StandardGates/XXMinusYYOp.cpp | 3 +-- .../QCO/IR/Operations/StandardGates/XXPlusYYOp.cpp | 3 +-- .../Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp | 10 +++++++--- 4 files changed, 12 insertions(+), 16 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/QCOUtils.h b/mlir/include/mlir/Dialect/QCO/QCOUtils.h index 6cf5af9d7e..f888509129 100644 --- a/mlir/include/mlir/Dialect/QCO/QCOUtils.h +++ b/mlir/include/mlir/Dialect/QCO/QCOUtils.h @@ -62,8 +62,7 @@ removeInversePairTwoTargetZeroParameter(OpType op, PatternRewriter& rewriter) { } // Confirm operations act on the same qubits - if (op.getOutputQubit(0) != nextOp.getInputQubit(0) || - op.getOutputQubit(1) != nextOp.getInputQubit(1)) { + if (op.getOutputQubit(1) != nextOp.getInputQubit(1)) { return failure(); } @@ -157,17 +156,13 @@ mlir::LogicalResult mergeOneTargetOneParameter(OpType op, return failure(); } - if (nextOp.getInputQubit(0) != op.getOutputQubit(0)) { - return failure(); - } - // Compute and set the new parameter auto newParameter = arith::AddFOp::create( rewriter, op.getLoc(), op.getOperand(1), nextOp.getOperand(1)); op->setOperand(1, newParameter.getResult()); // Replace the second operation with the result of the first operation - rewriter.replaceOp(nextOp, op.getOutputQubit(0)); + rewriter.replaceOp(nextOp, op.getResult()); return success(); } @@ -193,8 +188,7 @@ mlir::LogicalResult mergeTwoTargetOneParameter(OpType op, } // Confirm operations act on the same qubits - if (op.getOutputQubit(0) != nextOp.getInputQubit(0) || - op.getOutputQubit(1) != nextOp.getInputQubit(1)) { + if (op.getOutputQubit(1) != nextOp.getInputQubit(1)) { return failure(); } diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXMinusYYOp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXMinusYYOp.cpp index 17bce2198a..2923e39db0 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXMinusYYOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXMinusYYOp.cpp @@ -51,8 +51,7 @@ struct MergeSubsequentXXMinusYY final : OpRewritePattern { } // Confirm operations act on the same qubits - if (op.getOutputQubit(0) != nextOp.getInputQubit(0) || - op.getOutputQubit(1) != nextOp.getInputQubit(1)) { + if (op.getOutputQubit(1) != nextOp.getInputQubit(1)) { return failure(); } diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXPlusYYOp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXPlusYYOp.cpp index d5e1410cab..aad0076727 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXPlusYYOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXPlusYYOp.cpp @@ -50,8 +50,7 @@ struct MergeSubsequentXXPlusYY final : OpRewritePattern { } // Confirm operations act on the same qubits - if (op.getOutputQubit(0) != nextOp.getInputQubit(0) || - op.getOutputQubit(1) != nextOp.getInputQubit(1)) { + if (op.getOutputQubit(1) != nextOp.getInputQubit(1)) { return failure(); } diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp index 031320e2fe..4091c1bf68 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp @@ -447,12 +447,16 @@ struct NativeGateSynthesisPass /// guard intentionally rejects two adjacent `rz`s with nothing in between /// -- that case is handled by `fuseOneQubitRuns` above. static bool tryFuseRzForwardThroughCtrls(IRRewriter& rewriter, RZOp rz1) { - Value v = rz1.getQubitOut(); + Value v = rz1->getResult(0); + if (!llvm::isa(v.getType())) { + return false; + } RZOp partner; unsigned hops = 0; while (v.hasOneUse()) { Operation* user = *v.getUsers().begin(); - if (auto rz2 = llvm::dyn_cast(user); rz2 && rz2.getQubitIn() == v) { + if (auto rz2 = llvm::dyn_cast(user); + rz2 && rz2->getOperand(0) == v) { partner = rz2; break; } @@ -485,7 +489,7 @@ struct NativeGateSynthesisPass newTheta = arith::AddFOp::create(rewriter, loc, theta1, theta2); } rz1.getThetaMutable().assign(newTheta); - rewriter.replaceOp(partner, partner.getQubitIn()); + rewriter.replaceOp(partner, partner->getOperand(0)); return true; } From f0034c4b15cafadc9054bc5fe8ba363d91a58f79 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 27 Apr 2026 12:47:45 +0200 Subject: [PATCH 025/122] =?UTF-8?q?=E2=9C=85=20Increase=20Coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_native_synthesis_pass_fusion.cpp | 9 +++++++++ .../test_native_synthesis_pass_scoring.cpp | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_fusion.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_fusion.cpp index 5e784f4ca9..0cee7aa2da 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_fusion.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_fusion.cpp @@ -128,6 +128,15 @@ TEST_P(NativeSynthesisTwoQBlockEquivGenericU3CxTest, EXPECT_TRUE(isEquivalentUpToGlobalPhase(*expectedUnitary, *synthUnitary)); } +TEST(NativeSynthesisFusionTest, + IsEquivalentUpToGlobalPhaseRejectsNearZeroOverlap) { + const Eigen::Matrix2cd lhs = Eigen::Matrix2cd::Identity(); + const Eigen::Matrix2cd rhs = + (Eigen::Matrix2cd() << 1.0, 0.0, 0.0, -1.0).finished(); + // overlap = trace(rhs^H * lhs) = trace(Z) = 0 -> early false branch. + EXPECT_FALSE(isEquivalentUpToGlobalPhase(lhs, rhs, 1e-10)); +} + INSTANTIATE_TEST_SUITE_P( TwoQBlockEquivGenericU3CxMatrix, NativeSynthesisTwoQBlockEquivGenericU3CxTest, diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_scoring.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_scoring.cpp index 35ba14825b..94e71d6d15 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_scoring.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_scoring.cpp @@ -184,6 +184,24 @@ TEST(NativeSynthesisScoringTest, IsBetterScoreTreatsCloseWeightedAsTie) { EXPECT_TRUE(isBetterScore(b, a)); } +TEST(NativeSynthesisScoringTest, IsBetterScoreFallsBackToTupleTieBreak) { + using namespace mlir::qco::native_synth; + // Within tolerance: force the lexicographic tuple comparison path. + const CandidateScore lhs{.weighted = 2.0 + 1e-13, + .numTwoQ = 3, + .depth = 4, + .numOneQ = 5, + .tieBreakClass = 6, + .enumerationIndex = 7}; + const CandidateScore rhs{.weighted = 2.0, + .numTwoQ = 3, + .depth = 4, + .numOneQ = 5, + .tieBreakClass = 6, + .enumerationIndex = 8}; + EXPECT_TRUE(isBetterScore(lhs, rhs)); +} + TEST(NativeSynthesisScoringTest, SelectBestCandidateReturnsNullForEmptyInput) { using namespace mlir::qco::native_synth; const llvm::SmallVector, 0> empty; From 56e46f7cbffc668b3130e04980b8c3fb5f73bcc5 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 27 Apr 2026 12:48:06 +0200 Subject: [PATCH 026/122] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20linter=20warning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp index 4091c1bf68..2ebb5bd694 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp @@ -8,6 +8,7 @@ * Licensed under the MIT License */ +#include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h" From bf0a02b5e13290097cd55500a55610305efe929e Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 27 Apr 2026 12:58:40 +0200 Subject: [PATCH 027/122] =?UTF-8?q?=E2=9C=85=20Increase=20Coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Dialect/QCO/Transforms/NativeSynthesis/Scoring.h | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Scoring.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Scoring.h index 243506fdef..c0f8fc3c1b 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Scoring.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Scoring.h @@ -60,10 +60,11 @@ inline bool isBetterScore(const CandidateScore& lhs, if (std::abs(lhs.weighted - rhs.weighted) > scoreTolerance) { return lhs.weighted < rhs.weighted; } - return std::tie(lhs.numTwoQ, lhs.depth, lhs.numOneQ, lhs.tieBreakClass, - lhs.enumerationIndex) < - std::tie(rhs.numTwoQ, rhs.depth, rhs.numOneQ, rhs.tieBreakClass, - rhs.enumerationIndex); + const auto lhsTie = std::tie(lhs.numTwoQ, lhs.depth, lhs.numOneQ, + lhs.tieBreakClass, lhs.enumerationIndex); + const auto rhsTie = std::tie(rhs.numTwoQ, rhs.depth, rhs.numOneQ, + rhs.tieBreakClass, rhs.enumerationIndex); + return lhsTie < rhsTie; } /// Return the best candidate by `isBetterScore`, or `nullptr` on empty input. From 5f8ce03e13f7619f4025168132d8cec835ce25a5 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 27 Apr 2026 13:41:17 +0200 Subject: [PATCH 028/122] =?UTF-8?q?=F0=9F=90=9B=20Fix=20Windows=20Tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp index ad971e4ccb..2b1e1654d8 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp @@ -108,6 +108,9 @@ static void materializeSingleTwoQubitBlock( void collectUnitaryOpsInPreOrder(Operation* root, llvm::SmallVectorImpl& ops) { root->walk([&](Operation* op) { + if (llvm::isa_and_present(op->getParentOp())) { + return; + } if (llvm::isa(op)) { ops.push_back(op); } From de8e4741231cb30b813aaf1214c0974105597218 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 27 Apr 2026 14:10:58 +0200 Subject: [PATCH 029/122] =?UTF-8?q?=F0=9F=90=87=20Address=20Rabbit's=20Com?= =?UTF-8?q?ments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/include/mlir/Compiler/CompilerPipeline.h | 2 +- .../Transforms/Decomposition/GateSequence.h | 4 + .../Transforms/NativeSynthesis/NativeSpec.h | 7 +- .../NativeSynthesis/PassTwoQubitWindows.h | 5 +- .../Transforms/NativeSynthesis/SingleQubit.h | 1 + .../QCO/Transforms/NativeSynthesis/TwoQubit.h | 5 +- mlir/lib/Compiler/CMakeLists.txt | 1 - mlir/lib/Compiler/CompilerPipeline.cpp | 2 +- .../lib/Dialect/QCO/Transforms/CMakeLists.txt | 4 +- .../Decomposition/BasisDecomposer.cpp | 20 +++-- .../Decomposition/EulerDecomposition.cpp | 4 +- .../QCO/Transforms/Decomposition/Helpers.cpp | 81 +++++++------------ .../Decomposition/UnitaryMatrices.cpp | 15 +++- .../QCO/Transforms/NativeSynthesis/Pass.cpp | 17 ++-- .../NativeSynthesis/PassTwoQubitWindows.cpp | 35 +++++--- .../QCO/Transforms/NativeSynthesis/Policy.cpp | 11 ++- .../NativeSynthesis/SingleQubit.cpp | 30 +++++-- .../Transforms/NativeSynthesis/TwoQubit.cpp | 7 +- mlir/tools/mqt-cc/mqt-cc.cpp | 6 +- .../Dialect/QCO/Transforms/CMakeLists.txt | 2 +- .../Decomposition/decomposition_test_utils.h | 20 ++++- .../Decomposition/test_basis_decomposer.cpp | 4 +- .../test_decomposition_helpers.cpp | 5 ++ .../Decomposition/test_weyl_decomposition.cpp | 5 +- .../NativeSynthesis/test_native_policy.cpp | 31 +++++++ ...est_native_synthesis_pass_custom_menus.cpp | 35 +++++--- ...test_native_synthesis_pass_multi_qubit.cpp | 71 ++++++++-------- .../test_native_synthesis_pass_scoring.cpp | 2 +- 28 files changed, 264 insertions(+), 168 deletions(-) diff --git a/mlir/include/mlir/Compiler/CompilerPipeline.h b/mlir/include/mlir/Compiler/CompilerPipeline.h index c7daf29aff..cf414f084c 100644 --- a/mlir/include/mlir/Compiler/CompilerPipeline.h +++ b/mlir/include/mlir/Compiler/CompilerPipeline.h @@ -50,7 +50,7 @@ struct QuantumCompilerConfig { /// both): /// - `"x,sx,rz,cx"` / `"x,sx,rz,cz"` — IBM basic (no fractional 2q) /// - `"x,sx,rz,rx,rzz,cx"` / `"...,cz"` — IBM fractional - /// - `"u,cx"` / `"u,cz"` — generic single-qubit U3 + CX/CY + /// - `"u,cx"` / `"u,cz"` — generic single-qubit U3 + CX/CZ /// - `"r,cz"` — IQM-style default /// - `"rx,rz,cx"`, `"rx,ry,cz"`, `"ry,rz,cx"` — supported RX/RY/RZ pairs plus /// entangler diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h index 9109b3e4fa..89543e2844 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h @@ -56,8 +56,12 @@ struct QubitGateSequence { }; /// Documents intent only; same type as `QubitGateSequence`. +/// `QubitGateSequence::getUnitaryMatrix()` still returns an `Eigen::Matrix4cd` +/// in the shared two-qubit workspace convention, even for one-qubit sequences. using OneQubitGateSequence = QubitGateSequence; /// Documents intent only; same type as `QubitGateSequence`. +/// `QubitGateSequence::getUnitaryMatrix()` returns an `Eigen::Matrix4cd` +/// in the two-qubit workspace convention. using TwoQubitGateSequence = QubitGateSequence; } // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h index 4cb0f1f759..b524bfd7aa 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h @@ -17,9 +17,6 @@ #include -/// Parses the pass `native-gates` string into a `NativeProfileSpec` (emitters, -/// entanglers, `allowedGates`). Token set matches `Passes.td` on this pass. - namespace mlir::qco::native_synth { /// Euler bases that can reconstruct a two-axis single-qubit unitary. @@ -29,6 +26,10 @@ getEulerBasesForAxisPair(AxisPair axisPair); /// Resolve a comma-separated native gate menu (e.g. `"x,sx,rz,cx"`) into a /// full `NativeProfileSpec`. /// +/// Parses the pass `native-gates` string into a `NativeProfileSpec` +/// (single-qubit emitters, entangler bases, and `allowedGates`). Token set +/// matches `Passes.td` on this pass. +/// /// Recognised tokens: `u`, `x`, `sx`, `rz` (or `p`), `rx`, `ry`, `r`, /// `cx`, `cz`, `rzz`. std::optional diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.h index a7dff9bb4b..504e5a28a5 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.h @@ -21,6 +21,7 @@ #include #include #include +#include namespace mlir::qco::native_synth { @@ -68,8 +69,8 @@ struct TwoQubitWindowConsolidator { /// Picks the best candidate per block via `selectBestCandidate`, /// gates the replacement on `shouldApplyBlockReplacement`, and emits the /// new sequence through `rewriter`. - void materialize(IRRewriter& rewriter, const NativeProfileSpec& spec, - const ScoreWeights& weights); + LogicalResult materialize(IRRewriter& rewriter, const NativeProfileSpec& spec, + const ScoreWeights& weights); }; } // namespace mlir::qco::native_synth diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h index 8ea96fa94e..0b4f288325 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h @@ -10,6 +10,7 @@ #pragma once +#include "mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" #include diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h index 5a4b6e44bc..4611b6e907 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h @@ -56,12 +56,11 @@ llvm::SmallVector, 0> collectTwoQubitBasisCandidates(UnitaryOpInterface unitary, const NativeProfileSpec& spec); -/// Scoring metrics for the `rewriteXXPlusMinusYYViaRxxRyy` lowering (both +/// Scoring metrics for the `rewriteXXPlusMinusYYViaRzz` lowering (both /// `XXPlusYY` and `XXMinusYY` branches emit the same gate counts). CandidateMetrics xxPlusMinusYyRzzRewriteScoringMetrics(); /// Rewrite `XXPlusYY` / `XXMinusYY` via two `RZZ` blocks (menus with `rzz`). -LogicalResult rewriteXXPlusMinusYYViaRxxRyy(IRRewriter& rewriter, - Operation* op); +LogicalResult rewriteXXPlusMinusYYViaRzz(IRRewriter& rewriter, Operation* op); } // namespace mlir::qco::native_synth diff --git a/mlir/lib/Compiler/CMakeLists.txt b/mlir/lib/Compiler/CMakeLists.txt index ddefa918a5..ec9cff3bb2 100644 --- a/mlir/lib/Compiler/CMakeLists.txt +++ b/mlir/lib/Compiler/CMakeLists.txt @@ -21,7 +21,6 @@ add_mlir_library( MLIRQCOToQC MLIRQCOTransforms MLIRQCToQIR - MLIRQCOTransforms MQT::MLIRSupport) mqt_mlir_target_use_project_options(MQTCompilerPipeline) diff --git a/mlir/lib/Compiler/CompilerPipeline.cpp b/mlir/lib/Compiler/CompilerPipeline.cpp index ea99ca7be0..2f79f339a0 100644 --- a/mlir/lib/Compiler/CompilerPipeline.cpp +++ b/mlir/lib/Compiler/CompilerPipeline.cpp @@ -81,7 +81,7 @@ QuantumCompilerPipeline::runPipeline(ModuleOp module, // 2. QC cleanup // 3. QC-to-QCO conversion // 4. QCO cleanup - // 5. Optimization passes + // 5. Optimization and Native Gate Synthesis // 6. QCO cleanup // 7. QCO-to-QC conversion // 8. QC cleanup diff --git a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt index ef9f6be753..62e3190066 100644 --- a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt +++ b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt @@ -24,9 +24,9 @@ add_mlir_library( # collect header files (subdirs: NativeSynthesis/, Decomposition/, …) file(GLOB_RECURSE PASSES_HEADERS_SOURCE CONFIGURE_DEPENDS - "${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Dialect/QCO/Transforms/**/*.h") + "${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Dialect/QCO/Transforms/*.h") file(GLOB_RECURSE PASSES_HEADERS_BUILD CONFIGURE_DEPENDS - "${MQT_MLIR_BUILD_INCLUDE_DIR}/mlir/Dialect/QCO/Transforms/**/*.inc") + "${MQT_MLIR_BUILD_INCLUDE_DIR}/mlir/Dialect/QCO/Transforms/*.inc") # add public headers using file sets target_sources( diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp index 72eee0ba00..fb3e62fdbf 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp @@ -108,7 +108,6 @@ TwoQubitBasisDecomposer TwoQubitBasisDecomposer::create(const Gate& basisGate, {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))}, @@ -204,13 +203,18 @@ std::optional TwoQubitBasisDecomposer::twoQubitDecompose( // through a rougher approximation auto value = helpers::traceToFidelity(traces[i]) * std::pow(actualBasisFidelity, i); + if (std::isnan(value)) { + continue; + } 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) + if (bestIndex < 0) { + llvm::reportFatalInternalError("Unable to select basis-gate count: all " + "candidate fidelities are NaN"); + } return static_cast(bestIndex); }; // number of basis gates that need to be used in the decomposition @@ -242,9 +246,8 @@ std::optional TwoQubitBasisDecomposer::twoQubitDecompose( llvm::SmallVector eulerDecompositions; for (auto&& decomp : decomposition) { assert(helpers::isUnitaryMatrix(decomp)); - auto eulerDecomp = - unitaryToGateSequence(decomp, target1qEulerBases, true, std::nullopt); - eulerDecompositions.push_back(eulerDecomp); + eulerDecompositions.push_back( + unitaryToGateSequence(decomp, target1qEulerBases, true, std::nullopt)); } TwoQubitGateSequence gates{ .gates = {}, @@ -254,7 +257,8 @@ std::optional TwoQubitBasisDecomposer::twoQubitDecompose( // gate. We might overallocate a bit if the Euler basis differs, but the // worst case is a modest number of extra `Gate` slots; sequences are // short-lived before lowering. - constexpr auto twoQubitSequenceDefaultCapacity = 21; + const auto twoQubitSequenceDefaultCapacity = + static_cast((11 * bestNbasis) + 10); gates.gates.reserve(twoQubitSequenceDefaultCapacity); gates.globalPhase -= bestNbasis * basisDecomposer.globalPhase(); if (bestNbasis == 2) { @@ -288,7 +292,7 @@ std::optional TwoQubitBasisDecomposer::twoQubitDecompose( // add single-qubit decompositions after basis gate addEulerDecomposition(2UL * bestNbasis, 1); - addEulerDecomposition((2UL * bestNbasis) + 1, 0); + addEulerDecomposition((2UL * bestNbasis) + 1UL, 0); // 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 diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.cpp index 171fae2d45..d781990cda 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.cpp @@ -115,7 +115,9 @@ EulerDecomposition::paramsZyz(const Eigen::Matrix2cd& matrix) { std::array EulerDecomposition::paramsZxz(const Eigen::Matrix2cd& matrix) { // Convert from the Z-Y-Z parameterization via the standard basis-change - // identity RX(a) = RZ(pi/2) RY(a) RZ(-pi/2). + // identity RY(a) = RZ(pi/2) RX(a) RZ(-pi/2), i.e. + // RZ(phi) RY(theta) RZ(lambda) = + // RZ(phi + pi/2) RX(theta) RZ(lambda - pi/2). const auto [theta, phi, lam, phase] = paramsZyz(matrix); return {theta, phi + (std::numbers::pi / 2.0), lam - (std::numbers::pi / 2.0), phase}; diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp index 3382034907..357ff4e46b 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp @@ -14,6 +14,7 @@ #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" +#include #include #include #include @@ -31,61 +32,37 @@ decomposition::GateKind getGateKind(UnitaryOpInterface op) { // Controlled operations encode the physical gate in the body region. raw = ctrl.getBodyUnitary().getOperation(); } - if (llvm::isa(raw)) { - return decomposition::GateKind::I; - } - if (llvm::isa(raw)) { - return decomposition::GateKind::H; - } - if (llvm::isa(raw)) { - return decomposition::GateKind::P; - } - if (llvm::isa(raw)) { - return decomposition::GateKind::U; - } - if (llvm::isa(raw)) { - return decomposition::GateKind::U2; - } - if (llvm::isa(raw)) { - return decomposition::GateKind::X; - } - if (llvm::isa(raw)) { - return decomposition::GateKind::Y; - } - if (llvm::isa(raw)) { - return decomposition::GateKind::Z; - } - if (llvm::isa(raw)) { - return decomposition::GateKind::SX; - } - if (llvm::isa(raw)) { - return decomposition::GateKind::RX; - } - if (llvm::isa(raw)) { - return decomposition::GateKind::RY; - } - if (llvm::isa(raw)) { - return decomposition::GateKind::RZ; - } - if (llvm::isa(raw)) { - return decomposition::GateKind::R; - } - if (llvm::isa(raw)) { - return decomposition::GateKind::RXX; - } - if (llvm::isa(raw)) { - return decomposition::GateKind::RYY; - } - if (llvm::isa(raw)) { - return decomposition::GateKind::RZZ; - } - if (llvm::isa(raw)) { - return decomposition::GateKind::GPhase; - } - llvm::reportFatalInternalError("Unsupported QCO unitary operation kind"); + return llvm::TypeSwitch(raw) + .Case([](auto) { return decomposition::GateKind::I; }) + .Case([](auto) { return decomposition::GateKind::H; }) + .Case([](auto) { return decomposition::GateKind::P; }) + .Case([](auto) { return decomposition::GateKind::U; }) + .Case([](auto) { return decomposition::GateKind::U2; }) + .Case([](auto) { return decomposition::GateKind::X; }) + .Case([](auto) { return decomposition::GateKind::Y; }) + .Case([](auto) { return decomposition::GateKind::Z; }) + .Case([](auto) { return decomposition::GateKind::SX; }) + .Case([](auto) { return decomposition::GateKind::RX; }) + .Case([](auto) { return decomposition::GateKind::RY; }) + .Case([](auto) { return decomposition::GateKind::RZ; }) + .Case([](auto) { return decomposition::GateKind::R; }) + .Case([](auto) { return decomposition::GateKind::RXX; }) + .Case([](auto) { return decomposition::GateKind::RYY; }) + .Case([](auto) { return decomposition::GateKind::RZZ; }) + .Case([](auto) { return decomposition::GateKind::GPhase; }) + .Default([](Operation*) -> decomposition::GateKind { + llvm::reportFatalInternalError( + "Unsupported QCO unitary operation kind"); + llvm_unreachable("unsupported gate kind"); + }); } double remEuclid(double a, double b) { + if (b == 0.0) { + llvm::reportFatalInternalError( + "remEuclid expects non-zero divisor; callers like mod2pi pass positive " + "constants"); + } auto r = std::fmod(a, b); return (r < 0.0) ? r + std::abs(b) : r; } diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp index 675abd4b19..cf218ea3b1 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp @@ -172,7 +172,8 @@ Eigen::Matrix2cd getSingleQubitMatrix(const Gate& gate) { "unsupported gate type for single qubit matrix"); } -// TODO: remove? only used for verification of circuit and in unittests +// Reconstruct a two-qubit workspace matrix for a decomposition `Gate`. +// Used by sequence verification and `QubitGateSequence::getUnitaryMatrix()`. Eigen::Matrix4cd getTwoQubitMatrix(const Gate& gate) { if (gate.qubitId.empty()) { return Eigen::Matrix4cd::Identity(); @@ -181,18 +182,26 @@ Eigen::Matrix4cd getTwoQubitMatrix(const Gate& gate) { return expandToTwoQubits(getSingleQubitMatrix(gate), gate.qubitId[0]); } if (gate.qubitId.size() == 2) { + const bool validPair01 = + gate.qubitId == llvm::SmallVector{0, 1}; + const bool validPair10 = + gate.qubitId == llvm::SmallVector{1, 0}; + if (!validPair01 && !validPair10) { + llvm::reportFatalInternalError( + "Invalid two-qubit gate qubit IDs: expected {0,1} or {1,0}"); + } if (gate.type == GateKind::X) { // Controlled-X. The two matrices below are the *same* CX gate written in // the two possible operand orderings used by `Gate::qubitId`: qubit 0 is // the MSB of the 4x4 computational basis (matching // `UnitaryOpInterface::getUnitaryMatrix4x4`), so swapping // control/target wires produces a different basis-layout matrix. - if (gate.qubitId == llvm::SmallVector{0, 1}) { + if (validPair01) { // control = wire 0 (MSB), target = wire 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}) { + if (validPair10) { // control = wire 1, target = wire 0 (MSB). return Eigen::Matrix4cd{ {1, 0, 0, 0}, {0, 0, 0, 1}, {0, 0, 1, 0}, {0, 1, 0, 0}}; diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp index 2ebb5bd694..06906bd4a8 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp @@ -72,7 +72,7 @@ using native_synth::getBlockTwoQubitMatrix; using native_synth::NativeGateKind; using native_synth::NativeProfileSpec; using native_synth::resolveNativeGatesSpec; -using native_synth::rewriteXXPlusMinusYYViaRxxRyy; +using native_synth::rewriteXXPlusMinusYYViaRzz; using native_synth::ScoreWeights; using native_synth::selectBestCandidate; using native_synth::SingleQubitEmitterSpec; @@ -267,7 +267,10 @@ struct NativeGateSynthesisPass IRRewriter rewriter(&getContext()); fuseOneQubitRuns(rewriter, spec); - consolidateTwoQubitBlocks(rewriter, spec, weights); + if (failed(consolidateTwoQubitBlocks(rewriter, spec, weights))) { + signalPassFailure(); + return; + } // Two-qubit lowering can emit off-menu single-qubit ops (e.g. `rx`/`ry`); // repeat until clean or hit the sweep cap before seam / `rz` cleanup. constexpr unsigned kMaxSynthesisSweeps = 4; @@ -514,16 +517,16 @@ struct NativeGateSynthesisPass /// Two-qubit windows with absorbed single-qubit ops: replace when a cheaper /// native sequence exists. - void consolidateTwoQubitBlocks(IRRewriter& rewriter, - const NativeProfileSpec& spec, - const ScoreWeights& weights) { + LogicalResult consolidateTwoQubitBlocks(IRRewriter& rewriter, + const NativeProfileSpec& spec, + const ScoreWeights& weights) { llvm::SmallVector ops; collectUnitaryOpsInPreOrder(getOperation(), ops); TwoQubitWindowConsolidator consolidator; for (Operation* op : ops) { consolidator.process(op, spec); } - consolidator.materialize(rewriter, spec, weights); + return consolidator.materialize(rewriter, spec, weights); } /// Lower one single-qubit rewrite plan; null `Value` on failure. @@ -692,7 +695,7 @@ struct NativeGateSynthesisPass }); if (selectBestCandidate(llvm::ArrayRef(candidates), weights) != nullptr) { rewriter.setInsertionPoint(op); - if (succeeded(rewriteXXPlusMinusYYViaRxxRyy(rewriter, op))) { + if (succeeded(rewriteXXPlusMinusYYViaRzz(rewriter, op))) { return success(); } } diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp index 2b1e1654d8..c2d181c919 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp @@ -29,6 +29,7 @@ #include #include +#include #include #include @@ -75,7 +76,7 @@ static bool shouldApplyBlockReplacement(const TwoQubitBlock& block, /// first op, rewire the block's trailing SSA values (`wireA`, `wireB`) to /// the newly emitted outputs, and erase the replaced ops in reverse order /// so def-use edges are cleared before their defining ops disappear. -static void materializeSingleTwoQubitBlock( +static LogicalResult materializeSingleTwoQubitBlock( IRRewriter& rewriter, const TwoQubitBlock& block, const SynthesisCandidate& best) { Operation* firstOp = block.ops.front(); @@ -92,7 +93,7 @@ static void materializeSingleTwoQubitBlock( inB, best.payload.sequence, newA, newB))) { firstOp->emitError("failed to emit synthesized two-qubit gate sequence"); - return; + return failure(); } if (best.payload.sequence.hasGlobalPhase()) { emitGPhaseIfNonTrivial(rewriter, firstOp->getLoc(), @@ -103,6 +104,7 @@ static void materializeSingleTwoQubitBlock( for (auto* toErase : std::ranges::reverse_view(block.ops)) { rewriter.eraseOp(toErase); } + return success(); } void collectUnitaryOpsInPreOrder(Operation* root, @@ -192,12 +194,17 @@ void TwoQubitWindowConsolidator::process(Operation* op, auto it1 = wireToBlock.find(v1); const bool tracked0 = it0 != wireToBlock.end(); const bool tracked1 = it1 != wireToBlock.end(); + const std::optional idx0 = + tracked0 ? std::optional(it0->second) : std::nullopt; + const std::optional idx1 = + tracked1 ? std::optional(it1->second) : std::nullopt; // "Same block" means the two input wires are currently the (wireA, // wireB) pair of one existing block -- i.e. this op operates on the // same pair as the previous two-qubit op in that block. Otherwise the // op either extends into a *new* pair (merging two blocks, which we // don't support) or starts a fresh block. - const bool sameBlock = tracked0 && tracked1 && it0->second == it1->second; + const bool sameBlock = + idx0.has_value() && idx1.has_value() && *idx0 == *idx1; const bool singleUse = v0.hasOneUse() && v1.hasOneUse(); // ---- Case A: extend the existing block --------------------------- @@ -205,7 +212,7 @@ void TwoQubitWindowConsolidator::process(Operation* op, // them. Absorb the new gate into the block's accumulated unitary and // advance the tracked wires to this op's outputs. if (sameBlock && singleUse) { - const size_t idx = it0->second; + const size_t idx = *idx0; auto& block = blocks[idx]; // `block.accum` is the composite 4x4 unitary of the gates absorbed so // far, with qubit 0 == `wireA` and qubit 1 == `wireB`. The incoming @@ -253,11 +260,11 @@ void TwoQubitWindowConsolidator::process(Operation* op, // inconsistent -- note the second `if` guards against double-closing // the same block when both inputs happened to live in it but `sameBlock // && singleUse` was false (e.g. only fan-out violated). - if (tracked0) { - closeBlock(it0->second); + if (idx0.has_value()) { + closeBlock(*idx0); } - if (tracked1 && (!tracked0 || it0->second != it1->second)) { - closeBlock(it1->second); + if (idx1.has_value() && (!idx0.has_value() || *idx0 != *idx1)) { + closeBlock(*idx1); } TwoQubitBlock nb; nb.wireA = unitary.getOutputQubit(0); @@ -322,9 +329,10 @@ void TwoQubitWindowConsolidator::process(Operation* op, } } -void TwoQubitWindowConsolidator::materialize(IRRewriter& rewriter, - const NativeProfileSpec& spec, - const ScoreWeights& weights) { +LogicalResult +TwoQubitWindowConsolidator::materialize(IRRewriter& rewriter, + const NativeProfileSpec& spec, + const ScoreWeights& weights) { for (const auto& block : blocks) { if (block.ops.size() < 2) { continue; @@ -340,8 +348,11 @@ void TwoQubitWindowConsolidator::materialize(IRRewriter& rewriter, if (!shouldApplyBlockReplacement(block, best->metrics)) { continue; } - materializeSingleTwoQubitBlock(rewriter, block, *best); + if (failed(materializeSingleTwoQubitBlock(rewriter, block, *best))) { + return failure(); + } } + return success(); } } // namespace mlir::qco::native_synth diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Policy.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Policy.cpp index 1918b03d7b..5df581afbc 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Policy.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Policy.cpp @@ -96,8 +96,15 @@ computeGateSequenceMetrics(const decomposition::QubitGateSequence& seq) { for (const auto& gate : seq.gates) { if (gate.qubitId.size() == 2) { ++metrics.numTwoQ; - const auto gateDepth = std::max(qubitDepths[0], qubitDepths[1]) + 1; - qubitDepths[0] = qubitDepths[1] = gateDepth; + const unsigned q0 = gate.qubitId[0]; + const unsigned q1 = gate.qubitId[1]; + const unsigned neededSize = std::max(q0, q1) + 1; + if (neededSize > qubitDepths.size()) { + qubitDepths.resize(neededSize, 0); + } + const auto gateDepth = std::max(qubitDepths[q0], qubitDepths[q1]) + 1; + qubitDepths[q0] = gateDepth; + qubitDepths[q1] = gateDepth; metrics.depth = std::max(metrics.depth, gateDepth); } else if (gate.qubitId.size() == 1) { ++metrics.numOneQ; diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp index 9ff5b73842..375750a297 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp @@ -242,7 +242,12 @@ Value decomposeToZSXX(IRRewriter& rewriter, Operation* op, Value inQubit, } SingleQubitEmitter e{.rewriter = &rewriter, .loc = op->getLoc()}; if (auto p = llvm::dyn_cast(op)) { - return e.rz(inQubit, p.getTheta()); + auto q = e.rz(inQubit, p.getTheta()); + auto halfTheta = arith::MulFOp::create(rewriter, op->getLoc(), p.getTheta(), + e.constF(0.5)) + .getResult(); + GPhaseOp::create(rewriter, op->getLoc(), halfTheta); + return q; } if (!supportsDirectRx) { return {}; @@ -276,7 +281,12 @@ Value decomposeToU3(IRRewriter& rewriter, Operation* op, Value inQubit) { return e.u(inQubit, ry.getTheta(), e.constF(0.0), e.constF(0.0)); } if (auto rz = llvm::dyn_cast(op)) { - return e.u(inQubit, e.constF(0.0), e.constF(0.0), rz.getTheta()); + auto out = e.u(inQubit, e.constF(0.0), e.constF(0.0), rz.getTheta()); + auto halfTheta = arith::MulFOp::create(rewriter, op->getLoc(), + rz.getTheta(), e.constF(-0.5)) + .getResult(); + GPhaseOp::create(rewriter, op->getLoc(), halfTheta); + return out; } if (auto p = llvm::dyn_cast(op)) { return e.u(inQubit, e.constF(0.0), e.constF(0.0), p.getTheta()); @@ -363,9 +373,9 @@ Value emitSynthesizedSingleQubitFromMatrix( const auto det = m(0, 0) * m(1, 1) - m(0, 1) * m(1, 0); const double phase = std::arg(det) / 2.0; m *= std::exp(1i * (-phase)); - emitGPhaseIfNonTrivial(rewriter, loc, phase); const auto angles = decomposition::EulerDecomposition::anglesFromUnitary( m, decomposition::EulerBasis::ZYZ); + emitGPhaseIfNonTrivial(rewriter, loc, phase); return e.u(inQubit, angles[0], angles[1], angles[2]); } case SingleQubitMode::R: { @@ -427,7 +437,12 @@ Value decomposeToAxisPair(IRRewriter& rewriter, Operation* op, Value inQubit, return rz.getOutputQubit(0); } if (auto p = llvm::dyn_cast(op)) { - return e.rz(inQubit, p.getTheta()); + auto q = e.rz(inQubit, p.getTheta()); + auto halfTheta = arith::MulFOp::create(rewriter, op->getLoc(), + p.getTheta(), e.constF(0.5)) + .getResult(); + GPhaseOp::create(rewriter, op->getLoc(), halfTheta); + return q; } return {}; case AxisPair::RxRy: @@ -446,7 +461,12 @@ Value decomposeToAxisPair(IRRewriter& rewriter, Operation* op, Value inQubit, return rz.getOutputQubit(0); } if (auto p = llvm::dyn_cast(op)) { - return e.rz(inQubit, p.getTheta()); + auto q = e.rz(inQubit, p.getTheta()); + auto halfTheta = arith::MulFOp::create(rewriter, op->getLoc(), + p.getTheta(), e.constF(0.5)) + .getResult(); + GPhaseOp::create(rewriter, op->getLoc(), halfTheta); + return q; } return {}; } diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp index b8f8e33c81..015c69b061 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp @@ -248,13 +248,13 @@ collectTwoQubitBasisCandidatesFromMatrix(const Eigen::Matrix4cd& targetMatrix, } CandidateMetrics xxPlusMinusYyRzzRewriteScoringMetrics() { - // Tallies for `rewriteXXPlusMinusYYViaRxxRyy` (identical for `XXPlusYY` and + // Tallies for `rewriteXXPlusMinusYYViaRzz` (identical for `XXPlusYY` and // `XXMinusYY`): leading/final `rz` on `q0` (2) + `ryy` via `rzz` (four 1q + // one `rzz`) + `rxx` via `rzz` (four `(rz, sx, rz)` per wire around each // `rzz`, i.e. twelve 1q + one `rzz`). constexpr unsigned numOneQ = 18; constexpr unsigned numTwoQ = 2; - constexpr unsigned depth = 10; + constexpr unsigned depth = 12; return {.numOneQ = numOneQ, .numTwoQ = numTwoQ, .depth = depth}; } @@ -268,8 +268,7 @@ collectTwoQubitBasisCandidates(UnitaryOpInterface unitary, return collectTwoQubitBasisCandidatesFromMatrix(target, spec); } -LogicalResult rewriteXXPlusMinusYYViaRxxRyy(IRRewriter& rewriter, - Operation* op) { +LogicalResult rewriteXXPlusMinusYYViaRzz(IRRewriter& rewriter, Operation* op) { rewriter.setInsertionPoint(op); const auto loc = op->getLoc(); const auto constF = [&](double v) { diff --git a/mlir/tools/mqt-cc/mqt-cc.cpp b/mlir/tools/mqt-cc/mqt-cc.cpp index 0a6c3fcf19..577d34e3cd 100644 --- a/mlir/tools/mqt-cc/mqt-cc.cpp +++ b/mlir/tools/mqt-cc/mqt-cc.cpp @@ -84,19 +84,19 @@ static cl::opt nativeGates( cl::value_desc("csv"), cl::init("")); static cl::opt nativeGateScoreWeightTwoQ( - "native-gate-score-two-q", + "score-weight-twoq", cl::desc( "Weight for two-qubit gates in native synthesis candidate scoring"), cl::init(1.0)); static cl::opt nativeGateScoreWeightOneQ( - "native-gate-score-one-q", + "score-weight-oneq", cl::desc("Weight for single-qubit gates in native synthesis candidate " "scoring"), cl::init(0.1)); static cl::opt nativeGateScoreWeightDepth( - "native-gate-score-depth", + "score-weight-depth", cl::desc("Weight for local depth in native synthesis candidate scoring"), cl::init(0.01)); diff --git a/mlir/unittests/Dialect/QCO/Transforms/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/CMakeLists.txt index 232c2751c5..163412b775 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/CMakeLists.txt +++ b/mlir/unittests/Dialect/QCO/Transforms/CMakeLists.txt @@ -8,5 +8,5 @@ add_subdirectory(Decomposition) add_subdirectory(Mapping) -add_subdirectory(Optimizations) add_subdirectory(NativeSynthesis) +add_subdirectory(Optimizations) diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/decomposition_test_utils.h b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/decomposition_test_utils.h index 9273d2f0f2..4bf8d176af 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/decomposition_test_utils.h +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/decomposition_test_utils.h @@ -34,14 +34,28 @@ using mqt::test::isEquivalentUpToGlobalPhase; template [[nodiscard]] MatrixType randomUnitaryMatrix(std::mt19937& rng) { - std::uniform_real_distribution dist(-1.0, 1.0); + static_assert(MatrixType::RowsAtCompileTime != Eigen::Dynamic && + MatrixType::ColsAtCompileTime != Eigen::Dynamic, + "randomUnitaryMatrix requires fixed-size matrices"); + std::normal_distribution normalDist(0.0, 1.0); MatrixType randomMatrix; for (auto& x : randomMatrix.reshaped()) { - x = std::complex(dist(rng), dist(rng)); + x = std::complex(normalDist(rng), normalDist(rng)); } Eigen::HouseholderQR qr{}; qr.compute(randomMatrix); - const MatrixType unitaryMatrix = qr.householderQ(); + const MatrixType qMatrix = qr.householderQ(); + const MatrixType rMatrix = + qr.matrixQR().template triangularView(); + MatrixType dMatrix = MatrixType::Identity(); + constexpr Eigen::Index dim = MatrixType::RowsAtCompileTime; + for (Eigen::Index i = 0; i < dim; ++i) { + const auto rii = rMatrix(i, i); + const auto absRii = std::abs(rii); + dMatrix(i, i) = + absRii > 0.0 ? (rii / absRii) : std::complex{1.0, 0.0}; + } + const MatrixType unitaryMatrix = qMatrix * dMatrix; assert(helpers::isUnitaryMatrix(unitaryMatrix)); return unitaryMatrix; } diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_basis_decomposer.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_basis_decomposer.cpp index cb984faec1..6f2c433851 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_basis_decomposer.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_basis_decomposer.cpp @@ -106,8 +106,8 @@ TEST(BasisDecomposerTest, Random) { 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}; + std::uniform_int_distribution distEulerBases{1, + eulerBases.size()}; auto selectRandomEulerBases = [&]() { auto tmp = eulerBases; diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_helpers.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_helpers.cpp index d1f29e6cc2..afc2a7de45 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_helpers.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_helpers.cpp @@ -57,3 +57,8 @@ TEST(DecompositionHelpersTest, IsUnitaryMatrixRejectsNonUnitary) { m << 2.0, 0.0, 0.0, 2.0; EXPECT_FALSE(isUnitaryMatrix(m)); } + +TEST(DecompositionHelpersTest, IsUnitaryMatrixAcceptsUnitary) { + const Eigen::Matrix2cd m = Eigen::Matrix2cd::Identity(); + EXPECT_TRUE(isUnitaryMatrix(m)); +} diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index b61d66020d..6c71ff5502 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -75,7 +75,8 @@ TEST_P(WeylDecompositionTest, TestApproximation) { << restoredMatrix << '\n'; } -TEST(WeylDecompositionTest, CnotProducesValidWeylParametersAndUnitaryLocals) { +TEST(WeylDecompositionStandalone, + CnotProducesValidWeylParametersAndUnitaryLocals) { Eigen::Matrix4cd cnot = Eigen::Matrix4cd::Zero(); cnot(0, 0) = 1.0; cnot(1, 1) = 1.0; @@ -96,7 +97,7 @@ TEST(WeylDecompositionTest, CnotProducesValidWeylParametersAndUnitaryLocals) { EXPECT_TRUE(helpers::isUnitaryMatrix(decomp.k2r())); } -TEST(WeylDecompositionTest, Random) { +TEST(WeylDecompositionStandalone, Random) { constexpr auto maxIterations = 5000; std::mt19937 rng{1234567UL}; diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_policy.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_policy.cpp index 63f379ec7b..079e56677c 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_policy.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_policy.cpp @@ -92,6 +92,23 @@ TEST_F(NativePolicyAllowsOpTest, AllowsSingleQubitOpRespectsMenu) { llvm::cast(xop.getOperation()), *spec)); } +TEST_F(NativePolicyAllowsOpTest, RejectsSingleQubitOpNotInMenu) { + const auto spec = resolveNativeGatesSpec("u,cx"); + ASSERT_TRUE(spec); + Value q = builder.staticQubit(0); + q = builder.x(q); + auto mod = builder.finalize(); + ASSERT_TRUE(mod); + XOp xop; + mod->walk([&](XOp op) { + xop = op; + return WalkResult::interrupt(); + }); + ASSERT_TRUE(xop); + EXPECT_FALSE(allowsSingleQubitOp( + llvm::cast(xop.getOperation()), *spec)); +} + TEST_F(NativePolicyAllowsOpTest, CanDirectlyDecomposeToU3OnRxInCircuit) { Value q = builder.staticQubit(0); q = builder.rx(0.1, q); @@ -105,3 +122,17 @@ TEST_F(NativePolicyAllowsOpTest, CanDirectlyDecomposeToU3OnRxInCircuit) { ASSERT_TRUE(rx); EXPECT_TRUE(canDirectlyDecomposeToU3(rx.getOperation())); } + +TEST_F(NativePolicyAllowsOpTest, CannotDirectlyDecomposeHToU3) { + Value q = builder.staticQubit(0); + q = builder.h(q); + auto mod = builder.finalize(); + ASSERT_TRUE(mod); + HOp hop; + mod->walk([&](HOp op) { + hop = op; + return WalkResult::interrupt(); + }); + ASSERT_TRUE(hop); + EXPECT_FALSE(canDirectlyDecomposeToU3(hop.getOperation())); +} diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_custom_menus.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_custom_menus.cpp index 5966500ca8..f44ddde49c 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_custom_menus.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_custom_menus.cpp @@ -60,22 +60,31 @@ struct CustomMenuSpec { static std::vector splitCSV(const std::string& s) { std::vector out; - std::string cur; - for (const char ch : s) { - if (ch == ',') { - if (!cur.empty()) { - out.push_back(cur); - cur.clear(); + std::size_t tokenStart = 0; + while (tokenStart <= s.size()) { + const auto tokenEnd = s.find(',', tokenStart); + const auto end = (tokenEnd == std::string::npos) ? s.size() : tokenEnd; + std::size_t left = tokenStart; + while (left < end && + std::isspace(static_cast(s[left])) != 0) { + ++left; + } + std::size_t right = end; + while (right > left && + std::isspace(static_cast(s[right - 1])) != 0) { + --right; + } + if (left < right) { + std::string token = s.substr(left, right - left); + for (char& ch : token) { + ch = static_cast(std::tolower(static_cast(ch))); } - continue; + out.push_back(std::move(token)); } - if (ch != ' ' && ch != '\t' && ch != '\n' && ch != '\r') { - cur.push_back( - static_cast(std::tolower(static_cast(ch)))); + if (tokenEnd == std::string::npos) { + break; } - } - if (!cur.empty()) { - out.push_back(cur); + tokenStart = tokenEnd + 1; } return out; } diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_multi_qubit.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_multi_qubit.cpp index 891a270cf7..64985eb031 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_multi_qubit.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_multi_qubit.cpp @@ -64,35 +64,53 @@ const std::array THREE_QUBIT_CIRCUIT_CASES{{ } // namespace -TEST_F(NativeSynthesisPassTest, ThreeQubitCircuitsEquivalentAcrossProfiles) { - const auto profiles = NativeSynthesisPassTest::allNineEquivalenceProfiles(); - - for (const auto& circuitCase : THREE_QUBIT_CIRCUIT_CASES) { +class NativeSynthesisPassMultiQubitTest : public NativeSynthesisPassTest { +protected: + template + void verifyEquivalentAcrossProfiles(BuildFn buildFn, + const char* circuitName = nullptr) { + const auto profiles = allNineEquivalenceProfiles(); for (const auto& profileCase : profiles) { - auto expected = circuitCase.build(context.get()); + auto expected = buildFn(); runQcToQco(expected); const auto expectedUnitary = computeNQubitUnitaryFromModule(expected); ASSERT_TRUE(expectedUnitary.has_value()) - << "circuit=" << circuitCase.name - << " native-gates=" << profileCase.nativeGates; + << (circuitName != nullptr + ? std::string("circuit=") + circuitName + " " + : "") + << "native-gates=" << profileCase.nativeGates; - auto synthesized = circuitCase.build(context.get()); + auto synthesized = buildFn(); runNativeSynthesis(synthesized, profileCase.nativeGates); EXPECT_TRUE(profileCase.isNative(synthesized)) - << "circuit=" << circuitCase.name - << " native-gates=" << profileCase.nativeGates; + << (circuitName != nullptr + ? std::string("circuit=") + circuitName + " " + : "") + << "native-gates=" << profileCase.nativeGates; const auto synthesizedUnitary = computeNQubitUnitaryFromModule(synthesized); ASSERT_TRUE(synthesizedUnitary.has_value()) - << "circuit=" << circuitCase.name - << " native-gates=" << profileCase.nativeGates; + << (circuitName != nullptr + ? std::string("circuit=") + circuitName + " " + : "") + << "native-gates=" << profileCase.nativeGates; EXPECT_TRUE( isEquivalentUpToGlobalPhase(*expectedUnitary, *synthesizedUnitary)) - << "circuit=" << circuitCase.name - << " native-gates=" << profileCase.nativeGates; + << (circuitName != nullptr + ? std::string("circuit=") + circuitName + " " + : "") + << "native-gates=" << profileCase.nativeGates; } } +}; + +TEST_F(NativeSynthesisPassMultiQubitTest, + ThreeQubitCircuitsEquivalentAcrossProfiles) { + for (const auto& circuitCase : THREE_QUBIT_CIRCUIT_CASES) { + verifyEquivalentAcrossProfiles( + [&] { return circuitCase.build(context.get()); }, circuitCase.name); + } } static OwningOpRef buildFiveQubitStressCircuit(MLIRContext* context) { @@ -100,27 +118,8 @@ static OwningOpRef buildFiveQubitStressCircuit(MLIRContext* context) { context, mlir::qc::nativeSynthMultiQFiveQStressFourLayers); } -TEST_F(NativeSynthesisPassTest, +TEST_F(NativeSynthesisPassMultiQubitTest, FiveQubitStressCircuitEquivalentAcrossProfiles) { - const auto profiles = NativeSynthesisPassTest::allNineEquivalenceProfiles(); - - for (const auto& profileCase : profiles) { - auto expected = buildFiveQubitStressCircuit(context.get()); - runQcToQco(expected); - const auto expectedUnitary = computeNQubitUnitaryFromModule(expected); - ASSERT_TRUE(expectedUnitary.has_value()) - << "native-gates=" << profileCase.nativeGates; - - auto synthesized = buildFiveQubitStressCircuit(context.get()); - runNativeSynthesis(synthesized, profileCase.nativeGates); - EXPECT_TRUE(profileCase.isNative(synthesized)) - << "native-gates=" << profileCase.nativeGates; - - const auto synthesizedUnitary = computeNQubitUnitaryFromModule(synthesized); - ASSERT_TRUE(synthesizedUnitary.has_value()) - << "native-gates=" << profileCase.nativeGates; - EXPECT_TRUE( - isEquivalentUpToGlobalPhase(*expectedUnitary, *synthesizedUnitary)) - << "native-gates=" << profileCase.nativeGates; - } + verifyEquivalentAcrossProfiles( + [&] { return buildFiveQubitStressCircuit(context.get()); }); } diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_scoring.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_scoring.cpp index 94e71d6d15..2c5fe9aa3e 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_scoring.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_scoring.cpp @@ -275,7 +275,7 @@ TEST_F(NativeSynthesisPassTest, XxPlusMinusYyEmittedCountsMatchScoringMetrics) { ASSERT_NE(twoQOp, nullptr); IRRewriter rewriter(context.get()); - ASSERT_TRUE(succeeded(rewriteXXPlusMinusYYViaRxxRyy(rewriter, twoQOp))); + ASSERT_TRUE(succeeded(rewriteXXPlusMinusYYViaRzz(rewriter, twoQOp))); const auto expected = xxPlusMinusYyRzzRewriteScoringMetrics(); const auto [numOneQ, numTwoQ] = From 04a331203ed78fba8295fd88304435c9621f9110 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 27 Apr 2026 14:40:15 +0200 Subject: [PATCH 030/122] =?UTF-8?q?=F0=9F=90=87=20Address=20Rabbit's=20Com?= =?UTF-8?q?ments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp | 4 ++-- .../test_native_synthesis_pass_custom_menus.cpp | 1 + .../test_native_synthesis_pass_multi_qubit.cpp | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp index c2d181c919..c03669cccd 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp @@ -21,6 +21,7 @@ #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h" #include +#include #include #include #include @@ -30,7 +31,6 @@ #include #include -#include #include namespace mlir::qco::native_synth { @@ -101,7 +101,7 @@ static LogicalResult materializeSingleTwoQubitBlock( } rewriter.replaceAllUsesWith(outA, newA); rewriter.replaceAllUsesWith(outB, newB); - for (auto* toErase : std::ranges::reverse_view(block.ops)) { + for (auto* toErase : llvm::reverse(block.ops)) { rewriter.eraseOp(toErase); } return success(); diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_custom_menus.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_custom_menus.cpp index f44ddde49c..400f26b6da 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_custom_menus.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_custom_menus.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include using namespace mlir; diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_multi_qubit.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_multi_qubit.cpp index 64985eb031..3b75af293b 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_multi_qubit.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_multi_qubit.cpp @@ -22,6 +22,7 @@ #include #include +#include using namespace mlir; using namespace mlir::qco; @@ -64,6 +65,7 @@ const std::array THREE_QUBIT_CIRCUIT_CASES{{ } // namespace +// NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest fixture at global scope class NativeSynthesisPassMultiQubitTest : public NativeSynthesisPassTest { protected: template From f66e8201b43368b6920dba350edb5d6866bf8a97 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 27 Apr 2026 14:55:36 +0200 Subject: [PATCH 031/122] =?UTF-8?q?=F0=9F=90=9B=20Skip=20stale=20windows?= =?UTF-8?q?=20in=20TwoQubitWindowConsolidator=20to=20avoid=20erasing=20cap?= =?UTF-8?q?tured=20operations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp index c03669cccd..7101bd3254 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp @@ -337,6 +337,12 @@ TwoQubitWindowConsolidator::materialize(IRRewriter& rewriter, if (block.ops.size() < 2) { continue; } + // Rewriting earlier windows can erase ops that were captured while + // collecting blocks. Skip stale windows instead of touching dangling ops. + if (llvm::any_of(block.ops, + [](Operation* op) { return op->getBlock() == nullptr; })) { + continue; + } // Leave `block.accum` unnormalized: Weyl keeps stripped SU(4) phase in // the candidate sequence's `globalPhase`. const auto candidates = From 750ce8d06841211dd2eb19ce9a42553356923208 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 27 Apr 2026 15:44:20 +0200 Subject: [PATCH 032/122] =?UTF-8?q?=F0=9F=90=87=20Address=20Rabbit's=20Com?= =?UTF-8?q?ments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Transforms/NativeSynthesis/SingleQubit.h | 10 +++-- .../QCO/Transforms/NativeSynthesis/TwoQubit.h | 2 +- .../QCO/Transforms/NativeSynthesis/Pass.cpp | 39 ++++++++++++------- .../NativeSynthesis/PassTwoQubitWindows.cpp | 12 ++++-- .../Transforms/NativeSynthesis/TwoQubit.cpp | 4 +- .../test_decomposition_helpers.cpp | 1 + ...est_native_synthesis_pass_custom_menus.cpp | 21 +++++++++- ...test_native_synthesis_pass_multi_qubit.cpp | 23 ++++------- 8 files changed, 70 insertions(+), 42 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h index 0b4f288325..94a4918655 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h @@ -8,6 +8,12 @@ * Licensed under the MIT License */ +/// \file +/// Single-qubit native-synthesis lowering helpers. +/// Covers symbolic `decomposeTo*` rewrites plus matrix-fallback synthesis +/// utilities (`computeSynthesizedSingleQubitLength`, +/// `emitSynthesizedSingleQubitFromMatrix`). + #pragma once #include "mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h" @@ -20,10 +26,6 @@ #include #include -/// Single-qubit lowering: `decomposeTo*` for symbolic matches, plus -/// `computeSynthesizedSingleQubitLength` / -/// `emitSynthesizedSingleQubitFromMatrix` for the Euler matrix fallback. - namespace mlir::qco::native_synth { /// Direct (non-matrix) single-qubit lowering to the `ZSXX` emitter diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h index 4611b6e907..faa95030da 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h @@ -41,7 +41,7 @@ decomposeTwoQubitFromMatrix(const Eigen::Matrix4cd& matrix, std::optional numBasisUses); /// Enumerate all direct + matrix-fallback single-qubit rewrite candidates. -llvm::SmallVector> +llvm::SmallVector, 0> collectSingleQubitCandidates(UnitaryOpInterface unitary, const NativeProfileSpec& spec); diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp index 06906bd4a8..52080b30b3 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -44,7 +45,6 @@ #include #include #include -#include #include namespace mlir::qco { @@ -161,7 +161,7 @@ static bool maybeFuseRun(IRRewriter& rewriter, OneQubitRun& run, return false; } rewriter.replaceAllUsesWith(outQubit, replacement); - for (auto& op : std::ranges::reverse_view(run.ops)) { + for (auto& op : llvm::reverse(run.ops)) { Operation* toErase = op.getOperation(); rewriter.eraseOp(toErase); } @@ -555,10 +555,12 @@ struct NativeGateSynthesisPass const ScoreWeights& weights) { llvm::SmallVector ops; collectUnitaryOpsInPreOrder(getOperation(), ops); + llvm::DenseSet erasedOps; for (Operation* op : ops) { - // Pointers were collected before this loop. - if (op->getBlock() == nullptr) { + // Pointers were collected before this loop; avoid dereferencing ops + // erased by earlier rewrites in this same sweep. + if (erasedOps.contains(op)) { continue; } // Inner `CtrlOp` bodies are handled on the `CtrlOp` itself. @@ -579,14 +581,19 @@ struct NativeGateSynthesisPass rewriteSingleQubit(rewriter, op, unitary, spec, weights))) { return failure(); } + erasedOps.insert(op); } continue; } if (auto ctrl = llvm::dyn_cast(op)) { + const bool wasAlreadyNative = ctrlMatchesNativeMenu(ctrl, spec); if (failed(rewriteControlled(rewriter, ctrl, spec, weights))) { return failure(); } + if (!wasAlreadyNative) { + erasedOps.insert(op); + } continue; } @@ -594,6 +601,7 @@ struct NativeGateSynthesisPass if (failed(rewriteTwoQubit(rewriter, op, unitary, spec, weights))) { return failure(); } + erasedOps.insert(op); continue; } } @@ -660,17 +668,20 @@ struct NativeGateSynthesisPass const auto candidates = collectTwoQubitBasisCandidatesFromMatrix(matrix, spec); - if (const auto* best = - selectBestCandidate(llvm::ArrayRef(candidates), weights)) { - rewriter.setInsertionPoint(ctrl); - if (succeeded(emitTwoQubitGateSequence( - rewriter, ctrl.getOperation(), ctrl.getInputControl(0), - ctrl.getInputTarget(0), best->payload.sequence))) { - return success(); - } + const auto* best = selectBestCandidate(llvm::ArrayRef(candidates), weights); + if (best == nullptr) { + ctrl.emitError("controlled gate not allowed by selected profile"); + return failure(); } - ctrl.emitError("controlled gate not allowed by selected profile"); - return failure(); + rewriter.setInsertionPoint(ctrl); + if (failed(emitTwoQubitGateSequence( + rewriter, ctrl.getOperation(), ctrl.getInputControl(0), + ctrl.getInputTarget(0), best->payload.sequence))) { + ctrl.emitError( + "failed to emit two-qubit gate sequence for selected candidate"); + return failure(); + } + return success(); } /// Lower an off-menu generic two-qubit op (`RZZ`, `XXPlusYY`, `XXMinusYY`, diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp index 7101bd3254..68889fca4f 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp @@ -21,6 +21,7 @@ #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h" #include +#include #include #include #include @@ -333,14 +334,16 @@ LogicalResult TwoQubitWindowConsolidator::materialize(IRRewriter& rewriter, const NativeProfileSpec& spec, const ScoreWeights& weights) { + llvm::DenseSet erasedOps; for (const auto& block : blocks) { if (block.ops.size() < 2) { continue; } - // Rewriting earlier windows can erase ops that were captured while - // collecting blocks. Skip stale windows instead of touching dangling ops. + // Rewriting earlier windows can erase ops captured in later windows. + // Track erased op pointers and skip such windows without dereferencing + // potentially dangling `Operation*`. if (llvm::any_of(block.ops, - [](Operation* op) { return op->getBlock() == nullptr; })) { + [&](Operation* op) { return erasedOps.contains(op); })) { continue; } // Leave `block.accum` unnormalized: Weyl keeps stripped SU(4) phase in @@ -357,6 +360,9 @@ TwoQubitWindowConsolidator::materialize(IRRewriter& rewriter, if (failed(materializeSingleTwoQubitBlock(rewriter, block, *best))) { return failure(); } + for (Operation* op : block.ops) { + erasedOps.insert(op); + } } return success(); } diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp index 015c69b061..b8878eb16a 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp @@ -155,10 +155,10 @@ decomposeTwoQubitFromMatrix(const Eigen::Matrix4cd& matrix, std::nullopt, /*approximate=*/false, numBasisUses); } -llvm::SmallVector> +llvm::SmallVector, 0> collectSingleQubitCandidates(UnitaryOpInterface unitary, const NativeProfileSpec& spec) { - llvm::SmallVector> candidates; + llvm::SmallVector, 0> candidates; Operation* op = unitary.getOperation(); unsigned enumerationIndex = 0; const auto addCandidate = [&](CandidateClass klass, CandidateMetrics metrics, diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_helpers.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_helpers.cpp index afc2a7de45..283dfeaa8e 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_helpers.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_helpers.cpp @@ -11,6 +11,7 @@ #include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h" +#include #include #include diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_custom_menus.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_custom_menus.cpp index 400f26b6da..59cde808c7 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_custom_menus.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_custom_menus.cpp @@ -30,6 +30,7 @@ #include #include +#include #include #include #include @@ -167,8 +168,24 @@ static bool onlyAllowsMenuNativeOps(ModuleOp moduleOp, return; } if (llvm::isa(op)) { - // Some decomposition paths treat `rx(pi)` as an `x`-family primitive. - ok = spec.allowRX || (spec.allowX && spec.allowSX && spec.allowRZ); + if (spec.allowRX) { + ok = true; + return; + } + // If `rx` is not native, only the `rx(±pi)` case is accepted as an + // X-equivalent under the IBM-basic family fallback. + if (!(spec.allowX && spec.allowSX && spec.allowRZ)) { + ok = false; + return; + } + auto rx = llvm::cast(op); + const auto theta = evaluateConstF64(rx.getTheta()); + if (!theta.has_value()) { + ok = false; + return; + } + const double rem = std::remainder(*theta, 2.0 * std::numbers::pi); + ok = std::abs(std::abs(rem) - std::numbers::pi) <= 1e-10; return; } if (llvm::isa(op)) { diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_multi_qubit.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_multi_qubit.cpp index 3b75af293b..b9fa9b14f1 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_multi_qubit.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_multi_qubit.cpp @@ -73,36 +73,27 @@ class NativeSynthesisPassMultiQubitTest : public NativeSynthesisPassTest { const char* circuitName = nullptr) { const auto profiles = allNineEquivalenceProfiles(); for (const auto& profileCase : profiles) { + const std::string prefix = + circuitName != nullptr ? std::string("circuit=") + circuitName + " " + : ""; auto expected = buildFn(); runQcToQco(expected); const auto expectedUnitary = computeNQubitUnitaryFromModule(expected); ASSERT_TRUE(expectedUnitary.has_value()) - << (circuitName != nullptr - ? std::string("circuit=") + circuitName + " " - : "") - << "native-gates=" << profileCase.nativeGates; + << prefix << "native-gates=" << profileCase.nativeGates; auto synthesized = buildFn(); runNativeSynthesis(synthesized, profileCase.nativeGates); EXPECT_TRUE(profileCase.isNative(synthesized)) - << (circuitName != nullptr - ? std::string("circuit=") + circuitName + " " - : "") - << "native-gates=" << profileCase.nativeGates; + << prefix << "native-gates=" << profileCase.nativeGates; const auto synthesizedUnitary = computeNQubitUnitaryFromModule(synthesized); ASSERT_TRUE(synthesizedUnitary.has_value()) - << (circuitName != nullptr - ? std::string("circuit=") + circuitName + " " - : "") - << "native-gates=" << profileCase.nativeGates; + << prefix << "native-gates=" << profileCase.nativeGates; EXPECT_TRUE( isEquivalentUpToGlobalPhase(*expectedUnitary, *synthesizedUnitary)) - << (circuitName != nullptr - ? std::string("circuit=") + circuitName + " " - : "") - << "native-gates=" << profileCase.nativeGates; + << prefix << "native-gates=" << profileCase.nativeGates; } } }; From 1f20168466f88113d35b4d8f148e09589fb8c1aa Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 27 Apr 2026 16:13:38 +0200 Subject: [PATCH 033/122] =?UTF-8?q?=F0=9F=90=9B=20Fix=20Windows=20Tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.h index 504e5a28a5..12198555a8 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.h @@ -16,6 +16,7 @@ #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" #include +#include #include #include #include @@ -23,6 +24,8 @@ #include #include +#include + namespace mlir::qco::native_synth { /// State for one maximal two-qubit window (plus absorbed one-qubit ops) @@ -46,7 +49,7 @@ void collectUnitaryOpsInPreOrder(Operation* root, struct TwoQubitWindowConsolidator { /// Append-only list of windows discovered so far; closed windows are kept /// so `materialize()` can still rewrite them. - llvm::SmallVector blocks; + std::vector> blocks; /// Maps each currently-open SSA qubit value to the index of the block /// that owns its trailing wire. llvm::DenseMap wireToBlock; From 5c0f2d14e21abeaa8dbb5e0dfd15987221e42215 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 27 Apr 2026 17:53:37 +0200 Subject: [PATCH 034/122] =?UTF-8?q?=F0=9F=90=9B=20Fix=20Windows=20Tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NativeSynthesis/PassTwoQubitWindows.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp index 68889fca4f..7f98e9a759 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp @@ -191,6 +191,13 @@ void TwoQubitWindowConsolidator::process(Operation* op, } const Value v0 = unitary.getInputQubit(0); const Value v1 = unitary.getInputQubit(1); + // Defensive guard: malformed/degenerated two-qubit ops with identical + // input wires cannot be represented by this window model. Treat them as + // synchronization points and avoid map-iterator aliasing UB below. + if (v0 == v1) { + closeBlockOnWire(v0); + return; + } auto it0 = wireToBlock.find(v0); auto it1 = wireToBlock.find(v1); const bool tracked0 = it0 != wireToBlock.end(); @@ -237,7 +244,9 @@ void TwoQubitWindowConsolidator::process(Operation* op, block.anyNonNative = true; } wireToBlock.erase(it0); - wireToBlock.erase(it1); + if (it1 != it0) { + wireToBlock.erase(it1); + } Value newA; Value newB; if (v0 == block.wireA) { From 98d4bea1a8cc8d915968a92ff527a4abf53e3077 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 27 Apr 2026 18:26:09 +0200 Subject: [PATCH 035/122] =?UTF-8?q?=F0=9F=90=9B=20Fix=20Windows=20Tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Decomposition/BasisDecomposer.h | 27 ++++++++++--------- .../Decomposition/BasisDecomposer.cpp | 18 ++++++------- .../NativeSynthesis/PassTwoQubitWindows.cpp | 4 ++- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.h index b52aff37e3..233d3904e9 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.h @@ -22,9 +22,15 @@ #include #include #include +#include namespace mlir::qco::decomposition { +/// Intermediate single-qubit ``2×2`` unitaries produced while expanding a +/// two-qubit basis decomposition. +using TwoQubitLocalUnitaryList = + std::vector>; + /** * 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). @@ -116,10 +122,10 @@ class TwoQubitBasisDecomposer { * * 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 + * @note Stored in ``TwoQubitLocalUnitaryList`` so each matrix is allocated + * with Eigen's required alignment (portable across hosts / CRTs). */ - [[nodiscard]] static llvm::SmallVector + [[nodiscard]] static TwoQubitLocalUnitaryList decomp0(const decomposition::TwoQubitWeylDecomposition& target); /** @@ -136,10 +142,9 @@ class TwoQubitBasisDecomposer { * * 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 + * @note Stored in ``TwoQubitLocalUnitaryList`` (see ``decomp0``). */ - [[nodiscard]] llvm::SmallVector + [[nodiscard]] TwoQubitLocalUnitaryList decomp1(const decomposition::TwoQubitWeylDecomposition& target) const; /** @@ -165,10 +170,9 @@ class TwoQubitBasisDecomposer { * 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 + * @note Stored in ``TwoQubitLocalUnitaryList`` (see ``decomp0``). */ - [[nodiscard]] llvm::SmallVector decomp2Supercontrolled( + [[nodiscard]] TwoQubitLocalUnitaryList decomp2Supercontrolled( const decomposition::TwoQubitWeylDecomposition& target) const; /** @@ -180,10 +184,9 @@ class TwoQubitBasisDecomposer { * :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 + * @note Stored in ``TwoQubitLocalUnitaryList`` (see ``decomp0``). */ - [[nodiscard]] llvm::SmallVector decomp3Supercontrolled( + [[nodiscard]] TwoQubitLocalUnitaryList decomp3Supercontrolled( const decomposition::TwoQubitWeylDecomposition& target) const; /** diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp index fb3e62fdbf..f6ada58dc7 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp @@ -308,18 +308,18 @@ std::optional TwoQubitBasisDecomposer::twoQubitDecompose( // and swap adjacent pairs in each return vector so ``addEulerDecomposition`` // maps matrices to the same wires as the upstream decomposer. ``decomp0`` // cancels to the unswapped formula. -llvm::SmallVector +TwoQubitLocalUnitaryList TwoQubitBasisDecomposer::decomp0(const TwoQubitWeylDecomposition& target) { - return { + return TwoQubitLocalUnitaryList{ target.k1r() * target.k2r(), target.k1l() * target.k2l(), }; } -llvm::SmallVector TwoQubitBasisDecomposer::decomp1( +TwoQubitLocalUnitaryList TwoQubitBasisDecomposer::decomp1( const TwoQubitWeylDecomposition& target) const { // may not work for z != 0 and c != 0 (not always in Weyl chamber) - return { + return TwoQubitLocalUnitaryList{ basisDecomposer.k2l().transpose().conjugate() * target.k2r(), basisDecomposer.k2r().transpose().conjugate() * target.k2l(), target.k1r() * basisDecomposer.k1l().transpose().conjugate(), @@ -327,15 +327,14 @@ llvm::SmallVector TwoQubitBasisDecomposer::decomp1( }; } -llvm::SmallVector -TwoQubitBasisDecomposer::decomp2Supercontrolled( +TwoQubitLocalUnitaryList 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 { + return TwoQubitLocalUnitaryList{ q2l * target.k2r(), q2r * target.k2l(), q1la * rzMatrix(-2. * target.a()) * q1lb, @@ -345,15 +344,14 @@ TwoQubitBasisDecomposer::decomp2Supercontrolled( }; } -llvm::SmallVector -TwoQubitBasisDecomposer::decomp3Supercontrolled( +TwoQubitLocalUnitaryList 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 { + return TwoQubitLocalUnitaryList{ u3l * target.k2r(), u3r * target.k2l(), u2la * rzMatrix(-2. * target.a()) * u2lb, diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp index 7f98e9a759..8a9c50e411 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp @@ -127,7 +127,9 @@ void TwoQubitWindowConsolidator::closeBlock(size_t idx) { } block.open = false; wireToBlock.erase(block.wireA); - wireToBlock.erase(block.wireB); + if (block.wireB != block.wireA) { + wireToBlock.erase(block.wireB); + } } void TwoQubitWindowConsolidator::closeBlockOnWire(Value v) { From 12c3eacb57f7e17155f9c0b4d56752598bad99e1 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 27 Apr 2026 18:35:25 +0200 Subject: [PATCH 036/122] =?UTF-8?q?=F0=9F=90=87=20Address=20Rabbit's=20Com?= =?UTF-8?q?ments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/NativeSynthesis/Pass.cpp | 58 ++++++++++++++++--- .../NativeSynthesis/PassTwoQubitWindows.cpp | 10 ++-- 2 files changed, 54 insertions(+), 14 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp index 52080b30b3..06c5c30a38 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp @@ -80,6 +80,7 @@ using native_synth::SingleQubitMode; using native_synth::SingleQubitRewritePlan; using native_synth::SingleQubitRewriteStrategy; using native_synth::SynthesisCandidate; +using native_synth::TwoQubitRewritePlan; using native_synth::TwoQubitWindowConsolidator; using native_synth::usesCxEntangler; using native_synth::usesCzEntangler; @@ -685,9 +686,10 @@ struct NativeGateSynthesisPass } /// Lower an off-menu generic two-qubit op (`RZZ`, `XXPlusYY`, `XXMinusYY`, - /// or any arbitrary 4x4 unitary). Handles the `Rzz`-native fast path and - /// the `XXPlusMinusYY -> Rzz` specialization first, then falls back to the - /// Weyl-based basis-decomposer search. + /// or any arbitrary 4x4 unitary). Handles the `Rzz`-native fast path; for + /// `XXPlusYY` / `XXMinusYY` with `rzz` on the menu, scores the dedicated + /// `XX±YY -> Rzz` rewrite against Weyl basis candidates and picks the + /// cheaper option under `weights`. static LogicalResult rewriteTwoQubit(IRRewriter& rewriter, Operation* op, UnitaryOpInterface unitary, const NativeProfileSpec& spec, @@ -697,19 +699,57 @@ struct NativeGateSynthesisPass } if (spec.allowRzz && (llvm::isa(op) || llvm::isa(op))) { - llvm::SmallVector> candidates; - candidates.push_back(SynthesisCandidate{ + llvm::SmallVector>, + 8> + combined; + unsigned nextIndex = 0; + combined.push_back(SynthesisCandidate>{ .candidateClass = CandidateClass::XxPlusMinusViaRzz, .metrics = xxPlusMinusYyRzzRewriteScoringMetrics(), - .enumerationIndex = 0, - .payload = true, + .enumerationIndex = nextIndex++, + .payload = std::nullopt, }); - if (selectBestCandidate(llvm::ArrayRef(candidates), weights) != nullptr) { + if (!spec.entanglerBases.empty()) { + for (const auto& cand : collectTwoQubitBasisCandidates(unitary, spec)) { + combined.push_back( + SynthesisCandidate>{ + .candidateClass = cand.candidateClass, + .metrics = cand.metrics, + .enumerationIndex = nextIndex++, + .payload = cand.payload, + }); + } + } + if (const auto* best = + selectBestCandidate(llvm::ArrayRef(combined), weights)) { rewriter.setInsertionPoint(op); - if (succeeded(rewriteXXPlusMinusYYViaRzz(rewriter, op))) { + if (best->candidateClass == CandidateClass::XxPlusMinusViaRzz) { + if (succeeded(rewriteXXPlusMinusYYViaRzz(rewriter, op))) { + return success(); + } + if (!spec.entanglerBases.empty()) { + const auto basisCandidates = + collectTwoQubitBasisCandidates(unitary, spec); + if (const auto* basisBest = selectBestCandidate( + llvm::ArrayRef(basisCandidates), weights)) { + if (succeeded(emitTwoQubitGateSequence( + rewriter, op, unitary.getInputQubit(0), + unitary.getInputQubit(1), basisBest->payload.sequence))) { + return success(); + } + } + } + return failure(); + } + if (best->payload.has_value() && + succeeded(emitTwoQubitGateSequence( + rewriter, op, unitary.getInputQubit(0), + unitary.getInputQubit(1), best->payload->sequence))) { return success(); } } + op->emitError("unsupported two-qubit operation for selected profile"); + return failure(); } if (!spec.entanglerBases.empty()) { const auto candidates = collectTwoQubitBasisCandidates(unitary, spec); diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp index 8a9c50e411..26a27ff3d2 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp @@ -111,7 +111,7 @@ static LogicalResult materializeSingleTwoQubitBlock( void collectUnitaryOpsInPreOrder(Operation* root, llvm::SmallVectorImpl& ops) { root->walk([&](Operation* op) { - if (llvm::isa_and_present(op->getParentOp())) { + if (op->getParentOfType()) { return; } if (llvm::isa(op)) { @@ -162,10 +162,10 @@ void TwoQubitWindowConsolidator::closeBlockOnWire(Value v) { /// multi-use fork -- closes the block. void TwoQubitWindowConsolidator::process(Operation* op, const NativeProfileSpec& spec) { - // Skip ops nested inside a `CtrlOp`'s body: those are handled as part of - // their enclosing controlled op (seen at the parent level), not as - // independent two-qubit gates. - if (llvm::isa_and_present(op->getParentOp())) { + // Skip ops nested anywhere under a `CtrlOp` (e.g. `ctrl { inv { ... } }`): + // those are handled as part of the enclosing controlled op, not as + // independent gates for window tracking. + if (op->getParentOfType()) { return; } auto unitary = llvm::dyn_cast(op); From 01aa915c55ebb75bb909674d270ab01f07d17406 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 27 Apr 2026 19:05:34 +0200 Subject: [PATCH 037/122] =?UTF-8?q?=F0=9F=90=9B=20Fix=20Windows=20Tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp index 06c5c30a38..c2a07e81b7 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp @@ -178,7 +178,7 @@ static UnitaryOpInterface fusibleSingleQubitOp(Operation* op) { if (llvm::isa(op)) { return {}; } - if (llvm::isa_and_present(op->getParentOp())) { + if (op->getParentOfType()) { return {}; } Eigen::Matrix2cd matrix; @@ -347,7 +347,7 @@ struct NativeGateSynthesisPass if (llvm::isa(op)) { return mlir::WalkResult::advance(); } - if (llvm::isa_and_present(op->getParentOp())) { + if (op->getParentOfType()) { return mlir::WalkResult::advance(); } if (auto ctrl = llvm::dyn_cast(op)) { @@ -384,7 +384,7 @@ struct NativeGateSynthesisPass if (llvm::isa(op)) { return mlir::WalkResult::advance(); } - if (llvm::isa_and_present(op->getParentOp())) { + if (op->getParentOfType()) { return mlir::WalkResult::advance(); } auto unitary = llvm::dyn_cast(op); @@ -564,8 +564,9 @@ struct NativeGateSynthesisPass if (erasedOps.contains(op)) { continue; } - // Inner `CtrlOp` bodies are handled on the `CtrlOp` itself. - if (llvm::isa_and_present(op->getParentOp())) { + // Nested regions under any `CtrlOp` ancestor are handled on the `CtrlOp` + // itself (e.g. `ctrl { inv { ... } }`). + if (op->getParentOfType()) { continue; } if (llvm::isa(op)) { From 7c8aeb9b5724d599980d23f970408f292ba656de Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 27 Apr 2026 20:04:50 +0200 Subject: [PATCH 038/122] =?UTF-8?q?=F0=9F=90=9B=20Fix=20Windows=20Tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/NativeSynthesis/Pass.cpp | 27 ++++++++++++++----- .../NativeSynthesis/PassTwoQubitWindows.cpp | 20 +++++++++----- 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp index c2a07e81b7..883ff2723e 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp @@ -169,6 +169,18 @@ static bool maybeFuseRun(IRRewriter& rewriter, OneQubitRun& run, return true; } +/// True when `op` lives in a `ctrl`/`inv` region body (not the shell op). +/// Skips nested unitaries so they are handled via the enclosing modifier. +static bool isHiddenInsideCtrlOrInvBody(Operation* op) { + if (op->getParentOfType()) { + return true; + } + if (!llvm::isa(op) && op->getParentOfType()) { + return true; + } + return false; +} + /// Single-qubit op eligible for fusion (constant `2×2`, not under `ctrl`). static UnitaryOpInterface fusibleSingleQubitOp(Operation* op) { auto unitary = llvm::dyn_cast(op); @@ -178,7 +190,7 @@ static UnitaryOpInterface fusibleSingleQubitOp(Operation* op) { if (llvm::isa(op)) { return {}; } - if (op->getParentOfType()) { + if (isHiddenInsideCtrlOrInvBody(op)) { return {}; } Eigen::Matrix2cd matrix; @@ -347,7 +359,7 @@ struct NativeGateSynthesisPass if (llvm::isa(op)) { return mlir::WalkResult::advance(); } - if (op->getParentOfType()) { + if (isHiddenInsideCtrlOrInvBody(op)) { return mlir::WalkResult::advance(); } if (auto ctrl = llvm::dyn_cast(op)) { @@ -384,7 +396,7 @@ struct NativeGateSynthesisPass if (llvm::isa(op)) { return mlir::WalkResult::advance(); } - if (op->getParentOfType()) { + if (isHiddenInsideCtrlOrInvBody(op)) { return mlir::WalkResult::advance(); } auto unitary = llvm::dyn_cast(op); @@ -493,7 +505,8 @@ struct NativeGateSynthesisPass } else { newTheta = arith::AddFOp::create(rewriter, loc, theta1, theta2); } - rz1.getThetaMutable().assign(newTheta); + rewriter.modifyOpInPlace(rz1, + [&] { rz1.getThetaMutable().assign(newTheta); }); rewriter.replaceOp(partner, partner->getOperand(0)); return true; } @@ -564,9 +577,9 @@ struct NativeGateSynthesisPass if (erasedOps.contains(op)) { continue; } - // Nested regions under any `CtrlOp` ancestor are handled on the `CtrlOp` - // itself (e.g. `ctrl { inv { ... } }`). - if (op->getParentOfType()) { + // Nested regions under `ctrl` / `inv` are handled on the shell op + // (e.g. `ctrl { inv { ... } }`, `inv { ... }`). + if (isHiddenInsideCtrlOrInvBody(op)) { continue; } if (llvm::isa(op)) { diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp index 26a27ff3d2..ad94e91aa2 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp @@ -114,6 +114,9 @@ void collectUnitaryOpsInPreOrder(Operation* root, if (op->getParentOfType()) { return; } + if (!llvm::isa(op) && op->getParentOfType()) { + return; + } if (llvm::isa(op)) { ops.push_back(op); } @@ -162,12 +165,15 @@ void TwoQubitWindowConsolidator::closeBlockOnWire(Value v) { /// multi-use fork -- closes the block. void TwoQubitWindowConsolidator::process(Operation* op, const NativeProfileSpec& spec) { - // Skip ops nested anywhere under a `CtrlOp` (e.g. `ctrl { inv { ... } }`): - // those are handled as part of the enclosing controlled op, not as - // independent gates for window tracking. + // Skip ops nested under `ctrl` / `inv` (e.g. `ctrl { inv { ... } }`, + // `inv { ... }`): handled via the shell op, not as independent gates for + // window tracking. if (op->getParentOfType()) { return; } + if (!llvm::isa(op) && op->getParentOfType()) { + return; + } auto unitary = llvm::dyn_cast(op); if (!unitary) { return; @@ -245,9 +251,11 @@ void TwoQubitWindowConsolidator::process(Operation* op, if (!isNativeTwoQubitOp(op, spec)) { block.anyNonNative = true; } - wireToBlock.erase(it0); - if (it1 != it0) { - wireToBlock.erase(it1); + const Value eraseKeyA = it0->first; + const Value eraseKeyB = it1->first; + wireToBlock.erase(eraseKeyA); + if (eraseKeyA != eraseKeyB) { + wireToBlock.erase(eraseKeyB); } Value newA; Value newB; From db8335618643b6c07aa6d4ba8b2f8b9359e04598 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 29 Apr 2026 16:01:18 +0200 Subject: [PATCH 039/122] =?UTF-8?q?=F0=9F=8E=A8=20Revert=20unitary=20matri?= =?UTF-8?q?x=20calculations=20in=20QCO=20standard=20gates=20to=20use=20std?= =?UTF-8?q?::polar=20for=20complex=20exponentiation,=20improving=20numeric?= =?UTF-8?q?al=20stability=20and=20readability.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../IR/Operations/StandardGates/GPhaseOp.cpp | 4 +--- .../QCO/IR/Operations/StandardGates/POp.cpp | 4 +--- .../QCO/IR/Operations/StandardGates/ROp.cpp | 19 ++++++++++++------- .../QCO/IR/Operations/StandardGates/RZOp.cpp | 4 ++-- .../QCO/IR/Operations/StandardGates/RZZOp.cpp | 4 ++-- .../QCO/IR/Operations/StandardGates/TOp.cpp | 5 +---- .../QCO/IR/Operations/StandardGates/TdgOp.cpp | 5 +---- .../QCO/IR/Operations/StandardGates/U2Op.cpp | 10 +++++----- .../QCO/IR/Operations/StandardGates/UOp.cpp | 13 ++++++++++--- .../Operations/StandardGates/XXMinusYYOp.cpp | 11 +++++++++-- .../Operations/StandardGates/XXPlusYYOp.cpp | 11 +++++++++-- 11 files changed, 53 insertions(+), 37 deletions(-) diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/GPhaseOp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/GPhaseOp.cpp index b9b6d90434..aeb8a5c4b9 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/GPhaseOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/GPhaseOp.cpp @@ -63,10 +63,8 @@ void GPhaseOp::getCanonicalizationPatterns(RewritePatternSet& results, std::optional, 1, 1>> GPhaseOp::getUnitaryMatrix() { - using namespace std::complex_literals; - if (const auto theta = valueToDouble(getTheta())) { - return Eigen::Matrix, 1, 1>{std::exp(1i * *theta)}; + return Eigen::Matrix, 1, 1>{std::polar(1.0, *theta)}; } return std::nullopt; } diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/POp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/POp.cpp index 0060a2e727..08ee6e068e 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/POp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/POp.cpp @@ -66,10 +66,8 @@ void POp::getCanonicalizationPatterns(RewritePatternSet& results, } std::optional POp::getUnitaryMatrix() { - using namespace std::complex_literals; - if (const auto theta = valueToDouble(getTheta())) { - return Eigen::Matrix2cd{{1.0, 0.0}, {0.0, std::exp(1i * *theta)}}; + return Eigen::Matrix2cd{{1.0, 0.0}, {0.0, std::polar(1.0, *theta)}}; } return std::nullopt; } diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/ROp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/ROp.cpp index 42bf3c98b5..8490955b82 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/ROp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/ROp.cpp @@ -81,17 +81,22 @@ void ROp::getCanonicalizationPatterns(RewritePatternSet& results, } std::optional ROp::getUnitaryMatrix() { - using namespace std::complex_literals; - const auto theta = valueToDouble(getTheta()); const auto phi = valueToDouble(getPhi()); if (!theta || !phi) { return std::nullopt; } - const auto s = std::sin(*theta / 2.0); - const auto c = std::cos(*theta / 2.0) + 0i; - const auto m01 = s * std::exp(1i * (-*phi - (std::numbers::pi / 2.0))); - const auto m10 = s * std::exp(1i * (*phi - (std::numbers::pi / 2.0))); - return Eigen::Matrix2cd{{c, m01}, {m10, c}}; + const auto safePolarSigned = [](double radius, double angle) { + if (radius < 0.0) { + return std::polar(-radius, angle + std::numbers::pi); + } + return std::polar(radius, angle); + }; + + const auto thetaSin = std::sin(*theta / 2.0); + const auto m01 = safePolarSigned(thetaSin, -*phi - (std::numbers::pi / 2.0)); + const auto m10 = safePolarSigned(thetaSin, *phi - (std::numbers::pi / 2.0)); + const std::complex thetaCos = std::cos(*theta / 2.0); + return Eigen::Matrix2cd{{thetaCos, m01}, {m10, thetaCos}}; } diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/RZOp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/RZOp.cpp index 30a0377efd..cad399846c 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/RZOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/RZOp.cpp @@ -69,9 +69,9 @@ std::optional RZOp::getUnitaryMatrix() { using namespace std::complex_literals; if (const auto theta = valueToDouble(getTheta())) { - const auto m00 = std::exp(1i * (-*theta / 2.0)); + const auto m00 = std::polar(1.0, -*theta / 2.0); const auto m01 = 0i; - const auto m11 = std::exp(1i * (*theta / 2.0)); + const auto m11 = std::polar(1.0, *theta / 2.0); return Eigen::Matrix2cd{{m00, m01}, {m01, m11}}; } return std::nullopt; diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/RZZOp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/RZZOp.cpp index 2b7f7c2ae5..5fb05c3379 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/RZZOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/RZZOp.cpp @@ -88,8 +88,8 @@ std::optional RZZOp::getUnitaryMatrix() { if (const auto theta = valueToDouble(getTheta())) { const auto m0 = 0i; - const auto mp = std::exp(1i * (*theta / 2.0)); - const auto mm = std::exp(1i * (-*theta / 2.0)); + const auto mp = std::polar(1.0, *theta / 2.0); + const auto mm = std::polar(1.0, -*theta / 2.0); return Eigen::Matrix4cd{{mm, m0, m0, m0}, // row 0 {m0, mp, m0, m0}, // row 1 {m0, m0, mp, m0}, // row 2 diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/TOp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/TOp.cpp index 1ff7e851ee..14afd9814a 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/TOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/TOp.cpp @@ -17,7 +17,6 @@ #include #include -#include #include #include @@ -58,8 +57,6 @@ void TOp::getCanonicalizationPatterns(RewritePatternSet& results, } Eigen::Matrix2cd TOp::getUnitaryMatrix() { - using namespace std::complex_literals; - - const auto m11 = std::exp(1i * (std::numbers::pi / 4.0)); + const auto m11 = std::polar(1.0, std::numbers::pi / 4.0); return Eigen::Matrix2cd{{1.0, 0.0}, {0.0, m11}}; } diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/TdgOp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/TdgOp.cpp index a83283eb04..21a2a07b23 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/TdgOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/TdgOp.cpp @@ -17,7 +17,6 @@ #include #include -#include #include #include @@ -59,8 +58,6 @@ void TdgOp::getCanonicalizationPatterns(RewritePatternSet& results, } Eigen::Matrix2cd TdgOp::getUnitaryMatrix() { - using namespace std::complex_literals; - - const auto m11 = std::exp(1i * (-std::numbers::pi / 4.0)); + const auto m11 = std::polar(1.0, -std::numbers::pi / 4.0); return Eigen::Matrix2cd{{1.0, 0.0}, {0.0, m11}}; } diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/U2Op.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/U2Op.cpp index 600ab83f50..63158dfdb8 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/U2Op.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/U2Op.cpp @@ -114,10 +114,10 @@ std::optional U2Op::getUnitaryMatrix() { return std::nullopt; } - const auto invSqrt2 = 1.0 / std::numbers::sqrt2; - const auto m00 = invSqrt2 + 0i; - const auto m01 = invSqrt2 * std::exp(1i * (*lambda + std::numbers::pi)); - const auto m10 = invSqrt2 * std::exp(1i * (*phi)); - const auto m11 = invSqrt2 * std::exp(1i * (*phi + *lambda)); + const auto m00 = 1.0 / std::numbers::sqrt2 + 0i; + const auto m01 = + std::polar(1.0 / std::numbers::sqrt2, *lambda + std::numbers::pi); + const auto m10 = std::polar(1.0 / std::numbers::sqrt2, *phi); + const auto m11 = std::polar(1.0 / std::numbers::sqrt2, *phi + *lambda); return Eigen::Matrix2cd{{m00, m01}, {m10, m11}}; } diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/UOp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/UOp.cpp index e829ea4f0b..3a10f42c5e 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/UOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/UOp.cpp @@ -135,11 +135,18 @@ std::optional UOp::getUnitaryMatrix() { return std::nullopt; } + const auto safePolarSigned = [](double radius, double angle) { + if (radius < 0.0) { + return std::polar(-radius, angle + std::numbers::pi); + } + return std::polar(radius, angle); + }; + const auto c = std::cos(*theta / 2.0); const auto s = std::sin(*theta / 2.0); const auto m00 = c + 0i; - const auto m01 = s * std::exp(1i * (*lambda + std::numbers::pi)); - const auto m10 = s * std::exp(1i * (*phi)); - const auto m11 = c * std::exp(1i * (*phi + *lambda)); + const auto m01 = safePolarSigned(s, *lambda + std::numbers::pi); + const auto m10 = safePolarSigned(s, *phi); + const auto m11 = safePolarSigned(c, *phi + *lambda); return Eigen::Matrix2cd{{m00, m01}, {m10, m11}}; } diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXMinusYYOp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXMinusYYOp.cpp index 2923e39db0..ca52029407 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXMinusYYOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXMinusYYOp.cpp @@ -114,12 +114,19 @@ std::optional XXMinusYYOp::getUnitaryMatrix() { return std::nullopt; } + const auto safePolarSigned = [](double radius, double angle) { + if (radius < 0.0) { + return std::polar(-radius, angle + std::numbers::pi); + } + return std::polar(radius, angle); + }; + const auto m0 = 0.0 + 0i; const auto m1 = 1.0 + 0i; const auto mc = std::cos(*theta / 2.0) + 0i; const auto s = std::sin(*theta / 2.0); - const auto msp = s * std::exp(1i * (*beta - (std::numbers::pi / 2.0))); - const auto msm = s * std::exp(1i * (-*beta - (std::numbers::pi / 2.0))); + const auto msp = safePolarSigned(s, *beta - (std::numbers::pi / 2.0)); + const auto msm = safePolarSigned(s, -*beta - (std::numbers::pi / 2.0)); return Eigen::Matrix4cd{{mc, m0, m0, msm}, // row 0 {m0, m1, m0, m0}, // row 1 {m0, m0, m1, m0}, // row 2 diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXPlusYYOp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXPlusYYOp.cpp index aad0076727..1d15222a7d 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXPlusYYOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXPlusYYOp.cpp @@ -113,12 +113,19 @@ std::optional XXPlusYYOp::getUnitaryMatrix() { return std::nullopt; } + const auto safePolarSigned = [](double radius, double angle) { + if (radius < 0.0) { + return std::polar(-radius, angle + std::numbers::pi); + } + return std::polar(radius, angle); + }; + const auto m0 = 0.0 + 0i; const auto m1 = 1.0 + 0i; const auto mc = std::cos(*theta / 2.0) + 0i; const auto s = std::sin(*theta / 2.0); - const auto msp = s * std::exp(1i * (*beta - (std::numbers::pi / 2.0))); - const auto msm = s * std::exp(1i * (-*beta - (std::numbers::pi / 2.0))); + const auto msp = safePolarSigned(s, *beta - (std::numbers::pi / 2.0)); + const auto msm = safePolarSigned(s, -*beta - (std::numbers::pi / 2.0)); return Eigen::Matrix4cd{{m1, m0, m0, m0}, // row 0 {m0, mc, msp, m0}, // row 1 {m0, msm, mc, m0}, // row 2 From 98f11089c2833eba87a813e253b1a12c7d23794a Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 30 Apr 2026 14:02:57 +0200 Subject: [PATCH 040/122] =?UTF-8?q?=F0=9F=90=9B=20Try=20to=20fix=20Windows?= =?UTF-8?q?=20Tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp index 883ff2723e..7e26e334ab 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include #include @@ -713,8 +714,7 @@ struct NativeGateSynthesisPass } if (spec.allowRzz && (llvm::isa(op) || llvm::isa(op))) { - llvm::SmallVector>, - 8> + SmallVector>, 0> combined; unsigned nextIndex = 0; combined.push_back(SynthesisCandidate>{ From 0226c36fd0c887beb2db8c9bf306565ca7a5edfc Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 30 Apr 2026 14:47:03 +0200 Subject: [PATCH 041/122] =?UTF-8?q?=F0=9F=90=9B=20Try=20to=20fix=20Windows?= =?UTF-8?q?=20Tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/NativeSynthesis/Types.h | 3 ++- .../QCO/Transforms/NativeSynthesis/Pass.cpp | 22 ++++++++++++++----- .../NativeSynthesis/PassTwoQubitWindows.cpp | 9 +++++--- .../Transforms/NativeSynthesis/TwoQubit.cpp | 5 ++++- 4 files changed, 29 insertions(+), 10 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h index 2e9c9407a8..75a6783f3e 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h @@ -17,6 +17,7 @@ #include #include +#include /// Types for native gate synthesis: menu, emitters, candidates, score weights. @@ -134,7 +135,7 @@ struct SingleQubitRewritePlan { /// by `TwoQubitBasisDecomposer` plus the single-qubit emitter and entangler /// basis used when materializing the sequence back into MLIR. struct TwoQubitRewritePlan { - decomposition::TwoQubitGateSequence sequence; + std::shared_ptr sequence; SingleQubitEmitterSpec emitter; EntanglerBasis entanglerBasis = EntanglerBasis::None; }; diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp index 7e26e334ab..473d7e3a83 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp @@ -689,10 +689,14 @@ struct NativeGateSynthesisPass ctrl.emitError("controlled gate not allowed by selected profile"); return failure(); } + if (!best->payload.sequence) { + ctrl.emitError("internal error: missing two-qubit rewrite sequence"); + return failure(); + } rewriter.setInsertionPoint(ctrl); if (failed(emitTwoQubitGateSequence( rewriter, ctrl.getOperation(), ctrl.getInputControl(0), - ctrl.getInputTarget(0), best->payload.sequence))) { + ctrl.getInputTarget(0), *best->payload.sequence))) { ctrl.emitError( "failed to emit two-qubit gate sequence for selected candidate"); return failure(); @@ -746,19 +750,23 @@ struct NativeGateSynthesisPass collectTwoQubitBasisCandidates(unitary, spec); if (const auto* basisBest = selectBestCandidate( llvm::ArrayRef(basisCandidates), weights)) { + if (!basisBest->payload.sequence) { + return failure(); + } if (succeeded(emitTwoQubitGateSequence( rewriter, op, unitary.getInputQubit(0), - unitary.getInputQubit(1), basisBest->payload.sequence))) { + unitary.getInputQubit(1), + *basisBest->payload.sequence))) { return success(); } } } return failure(); } - if (best->payload.has_value() && + if (best->payload.has_value() && best->payload->sequence && succeeded(emitTwoQubitGateSequence( rewriter, op, unitary.getInputQubit(0), - unitary.getInputQubit(1), best->payload->sequence))) { + unitary.getInputQubit(1), *best->payload->sequence))) { return success(); } } @@ -769,10 +777,14 @@ struct NativeGateSynthesisPass const auto candidates = collectTwoQubitBasisCandidates(unitary, spec); if (const auto* best = selectBestCandidate(llvm::ArrayRef(candidates), weights)) { + if (!best->payload.sequence) { + op->emitError("internal error: missing two-qubit rewrite sequence"); + return failure(); + } rewriter.setInsertionPoint(op); if (succeeded(emitTwoQubitGateSequence( rewriter, op, unitary.getInputQubit(0), - unitary.getInputQubit(1), best->payload.sequence))) { + unitary.getInputQubit(1), *best->payload.sequence))) { return success(); } } diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp index ad94e91aa2..578e556583 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp @@ -80,6 +80,9 @@ static bool shouldApplyBlockReplacement(const TwoQubitBlock& block, static LogicalResult materializeSingleTwoQubitBlock( IRRewriter& rewriter, const TwoQubitBlock& block, const SynthesisCandidate& best) { + if (!best.payload.sequence) { + return failure(); + } Operation* firstOp = block.ops.front(); auto firstUnitary = llvm::cast(firstOp); const Value inA = firstUnitary.getInputQubit(0); @@ -91,14 +94,14 @@ static LogicalResult materializeSingleTwoQubitBlock( Value newA; Value newB; if (failed(emitTwoQubitGateSequenceAtLoc(rewriter, firstOp->getLoc(), inA, - inB, best.payload.sequence, newA, + inB, *best.payload.sequence, newA, newB))) { firstOp->emitError("failed to emit synthesized two-qubit gate sequence"); return failure(); } - if (best.payload.sequence.hasGlobalPhase()) { + if (best.payload.sequence->hasGlobalPhase()) { emitGPhaseIfNonTrivial(rewriter, firstOp->getLoc(), - best.payload.sequence.globalPhase); + best.payload.sequence->globalPhase); } rewriter.replaceAllUsesWith(outA, newA); rewriter.replaceAllUsesWith(outB, newB); diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp index b8878eb16a..25afc22443 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp @@ -32,6 +32,7 @@ #include #include +#include #include #include #include @@ -220,7 +221,9 @@ static void tryAddTwoQubitBasisCandidatesForEmitterBasis( .candidateClass = CandidateClass::TwoQubitBasisRewrite, .metrics = computeGateSequenceMetrics(*seq), .enumerationIndex = enumerationIndex++, - .payload = {.sequence = *seq, + .payload = {.sequence = + std::make_shared( + std::move(*seq)), .emitter = emitter, .entanglerBasis = entangler}, }); From d2b472be57157aa8b896abc8d816a6027662b36f Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 18 Jun 2026 15:22:17 +0200 Subject: [PATCH 042/122] =?UTF-8?q?=F0=9F=94=A5=20Remove=20`Eigen`=20and?= =?UTF-8?q?=20old=20single=20qubit=20synthesis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmake/ExternalDependencies.cmake | 30 -- mlir/include/mlir/Compiler/CompilerPipeline.h | 9 - .../Decomposition/BasisDecomposer.h | 122 +++--- .../QCO/Transforms/Decomposition/Euler.h | 26 ++ .../QCO/Transforms/Decomposition/EulerBasis.h | 55 --- .../Decomposition/EulerDecomposition.h | 147 ------- .../Transforms/Decomposition/GateSequence.h | 67 --- .../QCO/Transforms/Decomposition/Helpers.h | 30 +- .../Decomposition/UnitaryMatrices.h | 50 +-- .../Decomposition/WeylDecomposition.h | 44 +- .../Transforms/NativeSynthesis/NativeSpec.h | 10 +- .../NativeSynthesis/PassTwoQubitWindows.h | 21 +- .../QCO/Transforms/NativeSynthesis/Policy.h | 23 +- .../QCO/Transforms/NativeSynthesis/Scoring.h | 89 ---- .../Transforms/NativeSynthesis/SingleQubit.h | 38 +- .../QCO/Transforms/NativeSynthesis/TwoQubit.h | 62 ++- .../QCO/Transforms/NativeSynthesis/Types.h | 71 +--- .../QCO/Transforms/NativeSynthesis/Utils.h | 34 +- .../mlir/Dialect/QCO/Transforms/Passes.h | 3 - .../mlir/Dialect/QCO/Transforms/Passes.td | 26 +- mlir/include/mlir/Dialect/QCO/Utils/Matrix.h | 116 +++++ mlir/lib/Compiler/CompilerPipeline.cpp | 3 - .../QCO/IR/Operations/StandardGates/ROp.cpp | 9 +- .../QCO/IR/Operations/StandardGates/UOp.cpp | 10 +- .../Operations/StandardGates/XXMinusYYOp.cpp | 9 +- .../Operations/StandardGates/XXPlusYYOp.cpp | 9 +- .../lib/Dialect/QCO/Transforms/CMakeLists.txt | 2 - .../Decomposition/BasisDecomposer.cpp | 258 ++++------- .../QCO/Transforms/Decomposition/Euler.cpp | 58 +-- .../Transforms/Decomposition/EulerBasis.cpp | 52 --- .../Decomposition/EulerDecomposition.cpp | 317 -------------- .../Transforms/Decomposition/GateSequence.cpp | 55 --- .../QCO/Transforms/Decomposition/Helpers.cpp | 9 + .../Decomposition/UnitaryMatrices.cpp | 201 +++++---- .../Decomposition/WeylDecomposition.cpp | 402 +++++++++--------- .../Transforms/NativeSynthesis/NativeSpec.cpp | 57 ++- .../QCO/Transforms/NativeSynthesis/Pass.cpp | 373 ++++++---------- .../NativeSynthesis/PassTwoQubitWindows.cpp | 74 ++-- .../QCO/Transforms/NativeSynthesis/Policy.cpp | 115 ----- .../NativeSynthesis/SingleQubit.cpp | 245 +---------- .../Transforms/NativeSynthesis/TwoQubit.cpp | 293 ++++--------- .../QCO/Transforms/NativeSynthesis/Utils.cpp | 254 +---------- mlir/lib/Dialect/QCO/Utils/Matrix.cpp | 169 ++++++++ mlir/tools/mqt-cc/mqt-cc.cpp | 29 +- .../Compiler/test_compiler_pipeline.cpp | 37 +- .../Transforms/Decomposition/CMakeLists.txt | 5 +- .../Decomposition/decomposition_test_utils.h | 93 ++-- .../Decomposition/test_basis_decomposer.cpp | 160 +++---- .../test_decomposition_helpers.cpp | 8 +- .../test_euler_decomposition.cpp | 4 + .../test_matrix_euler_decomposition.cpp | 240 ----------- .../Decomposition/test_weyl_decomposition.cpp | 120 +++--- .../Transforms/NativeSynthesis/CMakeLists.txt | 3 +- .../native_synthesis_pass_test_fixture.h | 36 +- .../native_synthesis_test_helpers.cpp | 209 ++++++--- .../native_synthesis_test_helpers.h | 80 +++- .../NativeSynthesis/test_native_policy.cpp | 16 - .../NativeSynthesis/test_native_spec.cpp | 34 +- ...est_native_synthesis_pass_custom_menus.cpp | 3 - .../test_native_synthesis_pass_fusion.cpp | 18 +- .../test_native_synthesis_pass_profiles.cpp | 21 +- .../test_native_synthesis_pass_scoring.cpp | 289 ------------- .../Dialect/QCO/Utils/test_unitary_matrix.cpp | 137 ++++++ 63 files changed, 1856 insertions(+), 3733 deletions(-) delete mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h delete mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.h delete mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h delete mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Scoring.h delete mode 100644 mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerBasis.cpp delete mode 100644 mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.cpp delete mode 100644 mlir/lib/Dialect/QCO/Transforms/Decomposition/GateSequence.cpp delete mode 100644 mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_matrix_euler_decomposition.cpp delete mode 100644 mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_scoring.cpp diff --git a/cmake/ExternalDependencies.cmake b/cmake/ExternalDependencies.cmake index 2f294b9023..37e646cd0c 100644 --- a/cmake/ExternalDependencies.cmake +++ b/cmake/ExternalDependencies.cmake @@ -23,17 +23,6 @@ if(BUILD_MQT_CORE_BINDINGS) endif() if(BUILD_MQT_CORE_MLIR) - set(Eigen_VERSION - 5.0.1 - CACHE STRING "Eigen version") - set(Eigen_URL - https://gitlab.com/libeigen/eigen/-/archive/${Eigen_VERSION}/eigen-${Eigen_VERSION}.tar.gz) - set(EIGEN_BUILD_TESTING - OFF - CACHE INTERNAL "Disable building Eigen tests") - FetchContent_Declare(Eigen URL ${Eigen_URL} FIND_PACKAGE_ARGS ${Eigen_VERSION}) - list(APPEND FETCH_PACKAGES Eigen) - # Fetch jeff-mlir FetchContent_Declare( jeff-mlir @@ -134,25 +123,6 @@ list(APPEND FETCH_PACKAGES spdlog) # Make all declared dependencies available. FetchContent_MakeAvailable(${FETCH_PACKAGES}) -# Treat Eigen headers as system headers to avoid surfacing third-party warnings. -set(_eigen_target "") -if(TARGET Eigen3::Eigen) - set(_eigen_target Eigen3::Eigen) -elseif(TARGET Eigen::Eigen) - set(_eigen_target Eigen::Eigen) -endif() -if(_eigen_target) - get_target_property(_eigen_alias_target ${_eigen_target} ALIASED_TARGET) - if(_eigen_alias_target) - set(_eigen_target ${_eigen_alias_target}) - endif() - get_target_property(_eigen_includes ${_eigen_target} INTERFACE_INCLUDE_DIRECTORIES) - if(_eigen_includes) - set_target_properties(${_eigen_target} PROPERTIES INTERFACE_SYSTEM_INCLUDE_DIRECTORIES - "${_eigen_includes}") - endif() -endif() - # Install nlohmann_json with explicit MQT components. if(MQT_CORE_JSON_INSTALL AND TARGET nlohmann_json) set(MQT_CORE_JSON_CONFIG_INSTALL_DIR "${CMAKE_INSTALL_DATADIR}/cmake/nlohmann_json") diff --git a/mlir/include/mlir/Compiler/CompilerPipeline.h b/mlir/include/mlir/Compiler/CompilerPipeline.h index 11a03c944a..1844a75dd7 100644 --- a/mlir/include/mlir/Compiler/CompilerPipeline.h +++ b/mlir/include/mlir/Compiler/CompilerPipeline.h @@ -61,15 +61,6 @@ struct QuantumCompilerConfig { /// - `"rx,rz,cx"`, `"rx,ry,cz"`, `"ry,rz,cx"` — supported RX/RY/RZ pairs plus /// entangler std::string nativeGates; - - /// Weight for two-qubit gates in local candidate scoring - double nativeGateScoreWeightTwoQ = 1.0; - - /// Weight for single-qubit gates in local candidate scoring - double nativeGateScoreWeightOneQ = 0.1; - - /// Weight for local candidate depth in local candidate scoring - double nativeGateScoreWeightDepth = 0.01; }; /** diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.h index 63c6d63547..643f78cfe6 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.h @@ -10,26 +10,43 @@ #pragma once -#include "EulerBasis.h" -#include "GateSequence.h" +#include "Gate.h" #include "WeylDecomposition.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" #include -#include #include #include #include #include #include -#include namespace mlir::qco::decomposition { /// Intermediate single-qubit ``2×2`` unitaries produced while expanding a /// two-qubit basis decomposition. -using TwoQubitLocalUnitaryList = - std::vector>; +using TwoQubitLocalUnitaryList = llvm::SmallVector; + +/** + * Result of a two-qubit basis decomposition expressed as raw single-qubit + * factors interleaved with a fixed number of basis-gate (entangler) uses. + * + * The factors are stored in emission order. For `i` in `[0, numBasisUses)` the + * pair `(singleQubitFactors[2*i], singleQubitFactors[2*i + 1])` is applied to + * qubits `1` and `0` respectively, followed by one entangler. The final pair + * `(singleQubitFactors[2*numBasisUses], singleQubitFactors[2*numBasisUses+1])` + * is applied after the last entangler. The list therefore has length + * `2 * (numBasisUses + 1)`. + */ +struct TwoQubitNativeDecomposition { + /// Number of basis-gate (entangler) uses. + std::uint8_t numBasisUses = 0; + /// Single-qubit factors in emission order (see struct comment). + TwoQubitLocalUnitaryList singleQubitFactors; + /// Residual global phase (radians) not represented by factors/entanglers. + double globalPhase = 0.0; +}; /** * Decomposer that must be initialized with a two-qubit basis gate that will @@ -66,21 +83,15 @@ class TwoQubitBasisDecomposer { * * @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. + * @param numBasisGateUses Force use of given number of basis gates. When + * unset, the optimal count is selected from the + * Hilbert-Schmidt traces. + * @return The single-qubit factors and entangler count, or `std::nullopt` + * when more than one basis gate would be required but the basis gate + * is not super-controlled. */ - [[nodiscard]] std::optional twoQubitDecompose( + [[nodiscard]] std::optional twoQubitDecompose( const decomposition::TwoQubitWeylDecomposition& targetDecomposition, - const llvm::SmallVector& target1qEulerBases, - std::optional basisFidelity, bool approximate, std::optional numBasisGateUses) const; protected: @@ -91,16 +102,13 @@ class TwoQubitBasisDecomposer { 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) + bool isSuperControlled, const Matrix2x2& u0l, const Matrix2x2& u0r, + const Matrix2x2& u1l, const Matrix2x2& u1ra, const Matrix2x2& u1rb, + const Matrix2x2& u2la, const Matrix2x2& u2lb, const Matrix2x2& u2ra, + const Matrix2x2& u2rb, const Matrix2x2& u3l, const Matrix2x2& u3r, + const Matrix2x2& q0l, const Matrix2x2& q0r, const Matrix2x2& q1la, + const Matrix2x2& q1lb, const Matrix2x2& q1ra, const Matrix2x2& q1rb, + const Matrix2x2& q2l, const Matrix2x2& q2r) : basisGate{std::move(basisGate)}, basisFidelity{basisFidelity}, basisDecomposer{basisDecomposer}, isSuperControlled{isSuperControlled}, u0l{u0l}, u0r{u0r}, u1l{u1l}, u1ra{u1ra}, u1rb{u1rb}, u2la{u2la}, @@ -121,9 +129,6 @@ class TwoQubitBasisDecomposer { * 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 Stored in ``TwoQubitLocalUnitaryList`` so each matrix is allocated - * with Eigen's required alignment (portable across hosts / CRTs). */ [[nodiscard]] static TwoQubitLocalUnitaryList decomp0(const decomposition::TwoQubitWeylDecomposition& target); @@ -141,8 +146,6 @@ class TwoQubitBasisDecomposer { * \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 Stored in ``TwoQubitLocalUnitaryList`` (see ``decomp0``). */ [[nodiscard]] TwoQubitLocalUnitaryList decomp1(const decomposition::TwoQubitWeylDecomposition& target) const; @@ -169,8 +172,6 @@ class TwoQubitBasisDecomposer { * 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 Stored in ``TwoQubitLocalUnitaryList`` (see ``decomp0``). */ [[nodiscard]] TwoQubitLocalUnitaryList decomp2Supercontrolled( const decomposition::TwoQubitWeylDecomposition& target) const; @@ -183,8 +184,6 @@ class TwoQubitBasisDecomposer { * 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 Stored in ``TwoQubitLocalUnitaryList`` (see ``decomp0``). */ [[nodiscard]] TwoQubitLocalUnitaryList decomp3Supercontrolled( const decomposition::TwoQubitWeylDecomposition& target) const; @@ -197,15 +196,6 @@ class TwoQubitBasisDecomposer { */ [[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, bool simplify, - std::optional atol); [[nodiscard]] static bool relativeEq(double lhs, double rhs, double epsilon, double maxRelative); @@ -221,27 +211,27 @@ class TwoQubitBasisDecomposer { 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; + Matrix2x2 u0l; + Matrix2x2 u0r; + Matrix2x2 u1l; + Matrix2x2 u1ra; + Matrix2x2 u1rb; + Matrix2x2 u2la; + Matrix2x2 u2lb; + Matrix2x2 u2ra; + Matrix2x2 u2rb; + Matrix2x2 u3l; + Matrix2x2 u3r; // pre-built components for decomposition with 2 basis gates - Eigen::Matrix2cd q0l; - Eigen::Matrix2cd q0r; - Eigen::Matrix2cd q1la; - Eigen::Matrix2cd q1lb; - Eigen::Matrix2cd q1ra; - Eigen::Matrix2cd q1rb; - Eigen::Matrix2cd q2l; - Eigen::Matrix2cd q2r; + Matrix2x2 q0l; + Matrix2x2 q0r; + Matrix2x2 q1la; + Matrix2x2 q1lb; + Matrix2x2 q1ra; + Matrix2x2 q1rb; + Matrix2x2 q2l; + Matrix2x2 q2r; }; } // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h index 8fb0018b28..5d02898c3b 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h @@ -32,6 +32,7 @@ enum class EulerBasis : std::uint8_t { XYX = 3, ///< `RX(phi) * RY(theta) * RX(lambda)`. U = 4, ///< `U(theta, phi, lambda)`. ZSXX = 5, ///< `RZ` / `SX` / `X` synthesis via ZYZ decomposition. + R = 6, ///< `R(.,0) * R(.,pi/2) * R(.,0)` (XYX with `Rx`/`Ry` as `R`). }; /** @@ -42,6 +43,31 @@ enum class EulerBasis : std::uint8_t { */ [[nodiscard]] std::optional parseEulerBasis(StringRef basis); +/** + * @brief Euler angles `(theta, phi, lambda)` and global phase for a 2x2 + * unitary. + * + * The decomposition obeys `matrix == e^{i*phase} * K(phi) * A(theta) * + * K(lambda)` where `(K, A)` are the rotation axes of the chosen @ref + * EulerBasis. + */ +struct EulerAngles { + double theta = 0.0; ///< Middle rotation angle. + double phi = 0.0; ///< First outer rotation angle. + double lambda = 0.0; ///< Second outer rotation angle. + double phase = 0.0; ///< Global phase in radians. +}; + +/** + * @brief Extracts `(theta, phi, lambda, phase)` of @p matrix in @p basis. + * + * @param matrix The single-qubit unitary to decompose. + * @param basis The target Euler basis. + * @return The extracted Euler angles and global phase. + */ +[[nodiscard]] EulerAngles anglesFromUnitary(const Matrix2x2& matrix, + EulerBasis basis); + /** * @brief Synthesizes a composed single-qubit unitary as gates in @p basis. * diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h deleted file mode 100644 index 388dec6a43..0000000000 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#pragma once - -#include "GateKind.h" - -#include - -#include - -namespace mlir::qco::decomposition { -/** - * Default absolute tolerance used to treat small Euler angles as zero during - * simplification. - */ -inline constexpr auto DEFAULT_ATOL = 1e-12; - -/** - * Supported single-qubit Euler-style output bases. - * - * The listed values describe the gate alphabet that `EulerDecomposition` - * targets when converting a 2x2 unitary into a `OneQubitGateSequence`. - * Several entries share the angle-extraction routine and only differ in how - * the final circuit is emitted (e.g. `U3` vs `U321`, or `ZSX` vs `ZSXX`). - */ -enum class GateEulerBasis : std::uint8_t { - U3 = 0, ///< Single `u(theta, phi, lambda)` gate. - U321 = 1, ///< `u1`/`u2`/`u3` family — picks the smallest form per angles. - U = 2, ///< Same ZYZ angle extraction as `U3`, emitted as a single `u`. - ZYZ = 3, ///< `rz · ry · rz`. - ZXZ = 4, ///< `rz · rx · rz`. - XZX = 5, ///< `rx · rz · rx`. - XYX = 6, ///< `rx · ry · rx`. - ZSXX = 7, ///< `rz · sx` chain, with `sx · rz(±π) · sx` collapsed to `x`. - ZSX = 8, ///< Like `ZSXX` but without the `x` shortcut. -}; - -/** - * Return the gate types that may appear in a circuit emitted for `eulerBasis`. - * - * The result describes the basis alphabet, not the exact gate count. Some - * decompositions emit fewer than three gates after simplification. - */ -[[nodiscard]] llvm::SmallVector -getGateTypesForEulerBasis(GateEulerBasis 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 deleted file mode 100644 index a3c950e402..0000000000 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.h +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#pragma once - -#include "EulerBasis.h" -#include "GateSequence.h" - -#include -#include -#include - -namespace mlir::qco::decomposition { - -/** - * Decompose a single-qubit unitary into a selected Euler-style gate basis. - * - * The returned sequence tracks both the emitted gates and the scalar phase - * needed to reconstruct the input matrix exactly. This is stronger than the - * usual "up to global phase" contract and is relied on by downstream - * canonicalization and testing code. - */ -class EulerDecomposition { -public: - /** - * Decompose a 2x2 unitary into the gate alphabet described by - * `targetBasis`. - * - * When `simplify` is true, near-zero angles are removed using `atol` (or - * `DEFAULT_ATOL` if no override is provided). The returned global phase keeps - * the decomposition exactly equal to `unitaryMatrix`. - */ - [[nodiscard]] static OneQubitGateSequence - generateCircuit(GateEulerBasis targetBasis, - const Eigen::Matrix2cd& unitaryMatrix, bool simplify, - std::optional atol); - - /** - * Extract canonical Euler parameters for `matrix` in the requested basis. - * - * Some target bases reuse the same parameter extraction routine and differ - * only during circuit emission. The returned array always contains - * `(theta, phi, lambda, phase)` in this order. - */ - [[nodiscard]] static std::array - anglesFromUnitary(const Eigen::Matrix2cd& matrix, GateEulerBasis basis); - -private: - /// Extract parameters for a `RZ(phi) RY(theta) RZ(lambda)` factorization. - [[nodiscard]] static std::array - paramsZyz(const Eigen::Matrix2cd& matrix); - - /// Extract parameters for a `RZ(phi) RX(theta) RZ(lambda)` factorization. - [[nodiscard]] static std::array - paramsZxz(const Eigen::Matrix2cd& matrix); - - /// Extract parameters for a `RX(phi) RY(theta) RX(lambda)` factorization. - [[nodiscard]] static std::array - paramsXyx(const Eigen::Matrix2cd& matrix); - - /// Extract parameters for a `RX(phi) RZ(theta) RX(lambda)` factorization. - [[nodiscard]] static std::array - paramsXzx(const Eigen::Matrix2cd& matrix); - - /** - * Extract parameters for a `u1`/`p` + `sx` factorization. - * - * The returned angles are identical to `paramsZyz` but the phase is shifted - * by `-0.5 * (theta + phi + lambda)` so that the `rz`/`sx` circuits emitted - * by `decomposePsxGen` match the input matrix exactly (not only up to a - * global phase). - * - * @note Adapted from `params_u1x_inner` 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 - * https://www.apache.org/licenses/LICENSE-2.0. - * - * Any modifications or derivative works of this code must retain this - * copyright notice, and modified files need to carry a notice - * indicating that they have been altered from the originals. - */ - [[nodiscard]] static std::array - paramsU1x(const Eigen::Matrix2cd& matrix); - - /** - * Emit a K-A-K circuit from already extracted Euler parameters. - * - * `kGate` is used for the outer rotations and `aGate` for the middle - * rotation. - * - * @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 - * https://www.apache.org/licenses/LICENSE-2.0. - * - * Any modifications or derivative works of this code must retain this - * copyright notice, and modified files need to carry a notice - * indicating that they have been altered from the originals. - */ - [[nodiscard]] static OneQubitGateSequence - decomposeKAK(double theta, double phi, double lambda, double phase, - GateKind kGate, GateKind aGate, bool simplify, - std::optional atol); - - /** - * Emit an `rz`/`sx`-style circuit for the `ZSX` and `ZSXX` bases. - * - * The emitted sequence is structurally identical to the one produced by - * Qiskit's `circuit_psx_gen`. When `simplify` is enabled the number of `sx` - * gates shrinks based on `theta`: zero `sx` gates for `theta ~= 0`, one - * `sx` gate for `theta ~= pi/2`, and two `sx` gates otherwise. - * - * When `allowXShortcut` is true (i.e. for `ZSXX`), the general-case 2-`sx` - * path additionally collapses `sx . rz(+/- pi) . sx` into a single `x` - * gate when the middle rotation is congruent to +/- pi modulo 2 pi. - * - * @note Adapted from `circuit_psx_gen` 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 - * https://www.apache.org/licenses/LICENSE-2.0. - * - * Any modifications or derivative works of this code must retain - * this copyright notice, and modified files need to carry a notice - * indicating that they have been altered from the originals. - */ - [[nodiscard]] static OneQubitGateSequence - decomposePsxGen(double theta, double phi, double lambda, double phase, - bool allowXShortcut, bool simplify, - std::optional atol); -}; -} // 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 deleted file mode 100644 index 89543e2844..0000000000 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#pragma once - -#include "Gate.h" - -#include -#include - -#include - -namespace mlir::qco::decomposition { - -/** - * Sequence of abstract decomposition gates plus a residual global phase. - * - * `gates` is stored in execution order: for a column state vector, the first - * gate in the vector is applied first. The reconstructed 4x4 unitary - * is therefore `U = e^{i * phi} * M_{n-1} * ... * M_0`, where `M_i` is the - * two-qubit matrix for `gates[i]` and `phi` is `globalPhase` in radians (via - * `helpers::globalPhaseFactor`). - */ -struct QubitGateSequence { - /// Expected short decomposition length; `SmallVector` inline storage size. - static constexpr unsigned GATES_INLINE_CAPACITY = 8; - - /// Gates in execution order (see struct comment). - llvm::SmallVector gates; - - /// Residual global phase in radians, not represented by explicit gates. - double globalPhase{}; - - /// True when `std::abs(globalPhase)` exceeds `DEFAULT_ATOL` in - /// `EulerBasis.h`. - [[nodiscard]] bool hasGlobalPhase() const; - - /// Heuristic complexity from `helpers::getComplexity()` for each gate, plus a - /// synthetic global-phase term when `hasGlobalPhase()` is true. - [[nodiscard]] std::size_t complexity() const; - - /** - * Reconstruct the overall two-qubit unitary represented by the sequence. - * - * Single-qubit gates are expanded to the two-qubit workspace convention used - * throughout the decomposition utilities. - */ - [[nodiscard]] Eigen::Matrix4cd getUnitaryMatrix() const; -}; - -/// Documents intent only; same type as `QubitGateSequence`. -/// `QubitGateSequence::getUnitaryMatrix()` still returns an `Eigen::Matrix4cd` -/// in the shared two-qubit workspace convention, even for one-qubit sequences. -using OneQubitGateSequence = QubitGateSequence; -/// Documents intent only; same type as `QubitGateSequence`. -/// `QubitGateSequence::getUnitaryMatrix()` returns an `Eigen::Matrix4cd` -/// in the two-qubit workspace convention. -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 index e9ada91d49..56d7dd176f 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h @@ -12,12 +12,9 @@ #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" - -#include -#include +#include "mlir/Dialect/QCO/Utils/Matrix.h" #include -#include /// Numeric + classification helpers used by the decomposition passes. /// Lives in `mlir::qco::helpers` (not `decomposition`) because some helpers @@ -33,26 +30,15 @@ namespace mlir::qco::helpers { */ [[nodiscard]] decomposition::GateKind getGateKind(UnitaryOpInterface op); -// NOLINTBEGIN(misc-include-cleaner) -/// Eigen-decomposition of a self-adjoint matrix. Returns `(eigenvectors, -/// eigenvalues)`; eigenvalues are real and sorted ascending. -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); -} +/// Check whether `matrix` is unitary within `tolerance` (i.e. `M^H M` is +/// approximately the identity). +[[nodiscard]] bool isUnitaryMatrix(const Matrix2x2& matrix, + double tolerance = 1e-12); /// Check whether `matrix` is unitary within `tolerance` (i.e. `M^H M` is -/// approximately `I`, using Eigen's `isIdentity`). -template -[[nodiscard]] bool isUnitaryMatrix(const Eigen::Matrix& matrix, - double tolerance = 1e-12) { - return (matrix.transpose().conjugate() * matrix).isIdentity(tolerance); -} -// NOLINTEND(misc-include-cleaner) +/// approximately the identity). +[[nodiscard]] bool isUnitaryMatrix(const Matrix4x4& matrix, + double tolerance = 1e-12); /** * Euclidean remainder of a modulo b. diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h index de9064666c..1077705d01 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h @@ -11,8 +11,8 @@ #pragma once #include "Gate.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" -#include #include /// Standard-basis matrix factories for the decomposition layer. Two-qubit @@ -25,44 +25,46 @@ inline constexpr double FRAC1_SQRT2 = 0.707106781186547524400844362104849039284835937688474036588L; /// Generic 3-parameter single-qubit unitary `U(theta, phi, lambda)`. -[[nodiscard]] Eigen::Matrix2cd uMatrix(double theta, double phi, double lambda); +[[nodiscard]] Matrix2x2 uMatrix(double theta, double phi, double lambda); /// `U2(phi, lambda) == U(pi/2, phi, lambda)`. -[[nodiscard]] Eigen::Matrix2cd u2Matrix(double phi, double lambda); +[[nodiscard]] Matrix2x2 u2Matrix(double phi, double lambda); /// Axis rotations `exp(-i theta/2 * sigma_{x,y,z})`. -[[nodiscard]] Eigen::Matrix2cd rxMatrix(double theta); -[[nodiscard]] Eigen::Matrix2cd ryMatrix(double theta); -[[nodiscard]] Eigen::Matrix2cd rzMatrix(double theta); +[[nodiscard]] Matrix2x2 rxMatrix(double theta); +[[nodiscard]] Matrix2x2 ryMatrix(double theta); +[[nodiscard]] Matrix2x2 rzMatrix(double theta); /// Two-qubit Ising-style rotations on the `XX`, `YY`, `ZZ` generators. -[[nodiscard]] Eigen::Matrix4cd rxxMatrix(double theta); -[[nodiscard]] Eigen::Matrix4cd ryyMatrix(double theta); -[[nodiscard]] Eigen::Matrix4cd rzzMatrix(double theta); +[[nodiscard]] Matrix4x4 rxxMatrix(double theta); +[[nodiscard]] Matrix4x4 ryyMatrix(double theta); +[[nodiscard]] Matrix4x4 rzzMatrix(double theta); /// Phase gate `diag(1, exp(i lambda))`. -[[nodiscard]] Eigen::Matrix2cd pMatrix(double lambda); +[[nodiscard]] Matrix2x2 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}}; -/// `i * sigma_{x,y,z}`; useful when factoring Pauli rotations out of a 2x2. -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}}; +/// `SWAP` gate (4x4). +[[nodiscard]] const Matrix4x4& swapGate(); +/// Hadamard gate (2x2). +[[nodiscard]] const Matrix2x2& hGate(); +/// `i * sigma_z`; useful when factoring Pauli rotations out of a 2x2. +[[nodiscard]] const Matrix2x2& ipz(); +/// `i * sigma_y`. +[[nodiscard]] const Matrix2x2& ipy(); +/// `i * sigma_x`. +[[nodiscard]] const Matrix2x2& ipx(); /// Kronecker-embed a 2x2 on wire ``qubitId`` (identity on the other wire). -[[nodiscard]] Eigen::Matrix4cd -expandToTwoQubits(const Eigen::Matrix2cd& singleQubitMatrix, QubitId qubitId); +[[nodiscard]] Matrix4x4 expandToTwoQubits(const Matrix2x2& singleQubitMatrix, + QubitId qubitId); /// Reorder a 4x4 two-qubit matrix so its qubits match the canonical /// `(low, high)` order given the operand-order `qubitIds`. No-op when the /// operand order already matches. -[[nodiscard]] Eigen::Matrix4cd -fixTwoQubitMatrixQubitOrder(const Eigen::Matrix4cd& twoQubitMatrix, +[[nodiscard]] Matrix4x4 +fixTwoQubitMatrixQubitOrder(const Matrix4x4& twoQubitMatrix, const llvm::SmallVector& qubitIds); /// Construct the 2x2 / 4x4 matrix described by `gate`. Two-qubit gates are /// returned in the convention matching `expandToTwoQubits` + the gate's own /// operand order. -[[nodiscard]] Eigen::Matrix2cd getSingleQubitMatrix(const Gate& gate); -[[nodiscard]] Eigen::Matrix4cd getTwoQubitMatrix(const Gate& gate); +[[nodiscard]] Matrix2x2 getSingleQubitMatrix(const Gate& gate); +[[nodiscard]] Matrix4x4 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 index 984d8fa84c..896279ffb9 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.h @@ -10,9 +10,9 @@ #pragma once -#include "EulerBasis.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" -#include // NOLINT(misc-include-cleaner) +#include #include #include #include @@ -57,7 +57,7 @@ class TwoQubitWeylDecomposition { * gate and thus potentially decreasing the number of basis * gates. */ - static TwoQubitWeylDecomposition create(const Eigen::Matrix4cd& unitaryMatrix, + static TwoQubitWeylDecomposition create(const Matrix4x4& unitaryMatrix, std::optional fidelity); ~TwoQubitWeylDecomposition() = default; @@ -70,7 +70,7 @@ class TwoQubitWeylDecomposition { /** * Calculate matrix of canonical gate based on its parameters a, b, c. */ - [[nodiscard]] Eigen::Matrix4cd getCanonicalMatrix() const { + [[nodiscard]] Matrix4x4 getCanonicalMatrix() const { return getCanonicalMatrix(a_, b_, c_); } @@ -104,7 +104,7 @@ class TwoQubitWeylDecomposition { * A * q0 - k2l - N - *k1l* - */ - [[nodiscard]] const Eigen::Matrix2cd& k1l() const { return k1l_; } + [[nodiscard]] const Matrix2x2& k1l() const { return k1l_; } /** * "Left" qubit before canonical gate. * @@ -112,7 +112,7 @@ class TwoQubitWeylDecomposition { * A * q0 - *k2l* - N - k1l - */ - [[nodiscard]] const Eigen::Matrix2cd& k2l() const { return k2l_; } + [[nodiscard]] const Matrix2x2& k2l() const { return k2l_; } /** * "Right" qubit after canonical gate. * @@ -120,7 +120,7 @@ class TwoQubitWeylDecomposition { * A * q0 - k2l - N - k1l - */ - [[nodiscard]] const Eigen::Matrix2cd& k1r() const { return k1r_; } + [[nodiscard]] const Matrix2x2& k1r() const { return k1r_; } /** * "Right" qubit before canonical gate. * @@ -128,13 +128,13 @@ class TwoQubitWeylDecomposition { * A * q0 - k2l - N - k1l - */ - [[nodiscard]] const Eigen::Matrix2cd& k2r() const { return k2r_; } + [[nodiscard]] const Matrix2x2& 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); + [[nodiscard]] static Matrix4x4 getCanonicalMatrix(double a, double b, + double c); protected: enum class Specialization : std::uint8_t { @@ -165,9 +165,8 @@ class TwoQubitWeylDecomposition { TwoQubitWeylDecomposition() = default; - [[nodiscard]] static Eigen::Matrix4cd - magicBasisTransform(const Eigen::Matrix4cd& unitary, - MagicBasisTransform direction); + [[nodiscard]] static Matrix4x4 + magicBasisTransform(const Matrix4x4& unitary, MagicBasisTransform direction); [[nodiscard]] static double closestPartialSwap(double a, double b, double c); @@ -185,8 +184,8 @@ class TwoQubitWeylDecomposition { * * @return pair of (P, D.diagonal()) */ - [[nodiscard]] static std::pair - diagonalizeComplexSymmetric(const Eigen::Matrix4cd& m, double precision); + [[nodiscard]] static std::pair> + diagonalizeComplexSymmetric(const Matrix4x4& m, double precision); /** * Decompose a special unitary matrix C that is the combination of two @@ -199,8 +198,8 @@ class TwoQubitWeylDecomposition { * @return single-qubit matrices A and B and the required * global phase adjustment */ - static std::tuple - decomposeTwoQubitProductGate(const Eigen::Matrix4cd& specialUnitary); + static std::tuple + decomposeTwoQubitProductGate(const Matrix4x4& specialUnitary); /** * Calculate trace of two sets of parameters for the canonical gate. @@ -229,15 +228,14 @@ class TwoQubitWeylDecomposition { double globalPhase_{}; // Single-qubit factors surrounding the canonical gate; see the accessors // for the per-field wiring diagram. - Eigen::Matrix2cd k1l_; - Eigen::Matrix2cd k2l_; - Eigen::Matrix2cd k1r_; - Eigen::Matrix2cd k2r_; + Matrix2x2 k1l_; + Matrix2x2 k2l_; + Matrix2x2 k1r_; + Matrix2x2 k2r_; Specialization specialization{Specialization::General}; - GateEulerBasis defaultEulerBasis{GateEulerBasis::U3}; /// Optional `traceToFidelity` floor for specialization; unset disables it. std::optional requestedFidelity; double calculatedFidelity{}; - Eigen::Matrix4cd unitaryMatrix; + Matrix4x4 unitaryMatrix; }; } // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h index f661a1e35e..4993a85758 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h @@ -10,18 +10,20 @@ #pragma once +#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" -#include #include #include namespace mlir::qco::native_synth { -/// Euler bases that can reconstruct a two-axis single-qubit unitary. -llvm::SmallVector -getEulerBasesForAxisPair(AxisPair axisPair); +/// Euler basis used to synthesize an arbitrary single-qubit unitary into the +/// gates emitted by `emitter`. This is the deterministic replacement for the +/// scored multi-basis search. +[[nodiscard]] decomposition::EulerBasis +emitterEulerBasis(const SingleQubitEmitterSpec& emitter); /// Resolve a comma-separated native gate menu (e.g. `"x,sx,rz,cx"`) into a /// full `NativeProfileSpec`. diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.h index 12198555a8..2aa9437c3e 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.h @@ -14,9 +14,8 @@ #pragma once #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" -#include -#include #include #include #include @@ -24,6 +23,7 @@ #include #include +#include #include namespace mlir::qco::native_synth { @@ -34,7 +34,7 @@ struct TwoQubitBlock { Value wireA; Value wireB; llvm::SmallVector ops; - Eigen::Matrix4cd accum = Eigen::Matrix4cd::Identity(); + Matrix4x4 accum = Matrix4x4::identity(); unsigned numTwoQ = 0; unsigned numOneQ = 0; bool anyNonNative = false; @@ -49,7 +49,7 @@ void collectUnitaryOpsInPreOrder(Operation* root, struct TwoQubitWindowConsolidator { /// Append-only list of windows discovered so far; closed windows are kept /// so `materialize()` can still rewrite them. - std::vector> blocks; + std::vector blocks; /// Maps each currently-open SSA qubit value to the index of the block /// that owns its trailing wire. llvm::DenseMap wireToBlock; @@ -67,13 +67,12 @@ struct TwoQubitWindowConsolidator { /// windows depending on the op's kind and operand use pattern. void process(Operation* op, const NativeProfileSpec& spec); - /// Rewrite each collected window whose accumulated unitary can be - /// realized more cheaply by the native-gate synthesizer. - /// Picks the best candidate per block via `selectBestCandidate`, - /// gates the replacement on `shouldApplyBlockReplacement`, and emits the - /// new sequence through `rewriter`. - LogicalResult materialize(IRRewriter& rewriter, const NativeProfileSpec& spec, - const ScoreWeights& weights); + /// Rewrite each collected window whose accumulated unitary can be realized + /// with strictly fewer entanglers (or that contains non-native ops). The + /// deterministic two-qubit synthesizer emits the replacement through + /// `rewriter`. + LogicalResult materialize(IRRewriter& rewriter, + const NativeProfileSpec& spec); }; } // namespace mlir::qco::native_synth diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h index 9e19ce6b8c..7b7b00fb9b 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h @@ -15,15 +15,10 @@ #include -#include - -/// Menu checks and cost hints for synthesis candidates (no IR rewrites). +/// Menu membership checks for native synthesis (no IR rewrites). namespace mlir::qco::native_synth { -/// Score weights are valid iff they are finite and non-negative. -bool areValidScoreWeights(const ScoreWeights& weights); - /// Whether the menu contains the corresponding two-qubit entangler. Used by /// the 2q rewrite path to pick between CX and CZ emission. bool usesCxEntangler(const NativeProfileSpec& spec); @@ -33,23 +28,13 @@ bool usesCzEntangler(const NativeProfileSpec& spec); /// further rewrite needed). bool allowsSingleQubitOp(UnitaryOpInterface op, const NativeProfileSpec& spec); -/// Count 1q/2q gates and compute the depth of a gate sequence. -CandidateMetrics -computeGateSequenceMetrics(const decomposition::QubitGateSequence& seq); - /// Whether `op` has a direct (non-matrix) lowering via the corresponding -/// `decomposeTo*` helper in `SingleQubit.h`. +/// `decomposeTo*` helper in `SingleQubit.h`. These are used for ops whose +/// angles are not compile-time constants, so no constant ``2×2`` matrix is +/// available for the matrix-driven path. bool canDirectlyDecomposeToZSXX(Operation* op, bool supportsDirectRx); bool canDirectlyDecomposeToU3(Operation* op); bool canDirectlyDecomposeToR(Operation* op); bool canDirectlyDecomposeToAxisPair(Operation* op, AxisPair axisPair); -/// Estimated metrics for the direct and matrix-fallback lowerings. -CandidateMetrics -estimateDirectSingleQubitMetrics(Operation* op, - const SingleQubitEmitterSpec& emitter); -std::optional -estimateMatrixSingleQubitMetrics(UnitaryOpInterface unitary, - const SingleQubitEmitterSpec& emitter); - } // namespace mlir::qco::native_synth diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Scoring.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Scoring.h deleted file mode 100644 index c0f8fc3c1b..0000000000 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Scoring.h +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#pragma once - -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" - -#include -#include - -#include -#include - -/// Deterministic candidate scoring and selection. All comparisons are total -/// orders, so the same input always picks the same candidate. - -namespace mlir::qco::native_synth { - -/// Primary cost `weighted`; when two weighted scores agree within FP tolerance, -/// `isBetterScore` breaks ties in order: `numTwoQ`, `depth`, `numOneQ`, -/// `tieBreakClass`, `enumerationIndex`. -struct CandidateScore { - double weighted = 0.0; - unsigned numTwoQ = 0; - unsigned depth = 0; - unsigned numOneQ = 0; - unsigned tieBreakClass = 0; - unsigned enumerationIndex = 0; -}; - -/// Project a candidate onto its `CandidateScore`. -template -CandidateScore scoreCandidate(const SynthesisCandidate& candidate, - const ScoreWeights& weights) { - return { - .weighted = - (weights.twoQ * static_cast(candidate.metrics.numTwoQ)) + - (weights.oneQ * static_cast(candidate.metrics.numOneQ)) + - (weights.depth * static_cast(candidate.metrics.depth)), - .numTwoQ = candidate.metrics.numTwoQ, - .depth = candidate.metrics.depth, - .numOneQ = candidate.metrics.numOneQ, - .tieBreakClass = static_cast(candidate.candidateClass), - .enumerationIndex = candidate.enumerationIndex, - }; -} - -/// Strict less-than: `true` iff `lhs` is a strictly better candidate than -/// `rhs`. -inline bool isBetterScore(const CandidateScore& lhs, - const CandidateScore& rhs) { - constexpr double scoreTolerance = 1e-12; - if (std::abs(lhs.weighted - rhs.weighted) > scoreTolerance) { - return lhs.weighted < rhs.weighted; - } - const auto lhsTie = std::tie(lhs.numTwoQ, lhs.depth, lhs.numOneQ, - lhs.tieBreakClass, lhs.enumerationIndex); - const auto rhsTie = std::tie(rhs.numTwoQ, rhs.depth, rhs.numOneQ, - rhs.tieBreakClass, rhs.enumerationIndex); - return lhsTie < rhsTie; -} - -/// Return the best candidate by `isBetterScore`, or `nullptr` on empty input. -template -const Candidate* selectBestCandidate(llvm::ArrayRef candidates, - const ScoreWeights& weights) { - if (candidates.empty()) { - return nullptr; - } - const auto* best = &candidates.front(); - auto bestScore = scoreCandidate(*best, weights); - for (const auto& candidate : llvm::drop_begin(candidates)) { - const auto candidateScore = scoreCandidate(candidate, weights); - if (isBetterScore(candidateScore, bestScore)) { - best = &candidate; - bestScore = candidateScore; - } - } - return best; -} - -} // namespace mlir::qco::native_synth diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h index 94a4918655..b09c48b4dd 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h @@ -10,22 +10,19 @@ /// \file /// Single-qubit native-synthesis lowering helpers. -/// Covers symbolic `decomposeTo*` rewrites plus matrix-fallback synthesis -/// utilities (`computeSynthesizedSingleQubitLength`, -/// `emitSynthesizedSingleQubitFromMatrix`). +/// Covers symbolic `decomposeTo*` rewrites (used for dynamic-angle ops) plus +/// the matrix-driven `emitSingleQubitMatrix` synthesizer that lowers any +/// constant ``2×2`` unitary via the shared `Euler.h` synthesis. #pragma once -#include "mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" -#include #include #include -#include -#include - namespace mlir::qco::native_synth { /// Direct (non-matrix) single-qubit lowering to the `ZSXX` emitter @@ -54,24 +51,11 @@ Value decomposeToR(IRRewriter& rewriter, Operation* op, Value inQubit); Value decomposeToAxisPair(IRRewriter& rewriter, Operation* op, Value inQubit, AxisPair axisPair); -/// Euler sequence for matrix synthesis for non-`U3` emitters (same basis as -/// `emitSynthesizedSingleQubitFromMatrix`). `nullopt` for `U3` (single `u` -/// gate, no cached Euler list) or when the axis pair has no Euler basis. -std::optional -eulerSequenceForMatrixSynthesis(const Eigen::Matrix2cd& matrix, - const SingleQubitEmitterSpec& emitter); - -/// Cost estimate in number of emitted ops for fusing a single-qubit unitary -/// with the given emitter. -std::size_t -computeSynthesizedSingleQubitLength(const Eigen::Matrix2cd& matrix, - const SingleQubitEmitterSpec& emitter); - -/// Emit the fused `2×2` unitary as native ops, inserting a global phase if the -/// emitted sequence carries a non-trivial residual global phase. -Value emitSynthesizedSingleQubitFromMatrix( - IRRewriter& rewriter, Location loc, Value inQubit, - const Eigen::Matrix2cd& matrix, const SingleQubitEmitterSpec& emitter, - const decomposition::QubitGateSequence* reuseEulerSeq = nullptr); +/// Synthesize a constant ``2×2`` unitary `matrix` into native gates of `basis` +/// (including a `qco.gphase` when the residual phase is non-trivial) and +/// return the resulting output qubit. Wraps `decomposition::Euler`. +Value emitSingleQubitMatrix(IRRewriter& rewriter, Location loc, Value inQubit, + const Matrix2x2& matrix, + decomposition::EulerBasis basis); } // namespace mlir::qco::native_synth diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h index 651532d589..cd90b21861 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h @@ -10,55 +10,41 @@ #pragma once -#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" #include #include +#include #include -#include #include -#include -/// Two-qubit lowering: Weyl decomposition + `TwoQubitBasisDecomposer` over -/// each `(entangler, emitter Euler basis, basis-use count 0..3)` allowed by -/// the menu; the scorer picks the cheapest exact sequence. +/// Deterministic two-qubit lowering: Weyl decomposition + the +/// `TwoQubitBasisDecomposer` with a fixed entangler (CX before CZ) and the +/// first emitter's Euler basis for the surrounding single-qubit factors. namespace mlir::qco::native_synth { -/// Whether every gate in `seq` is allowed by `spec`'s menu. -bool gateSequenceFitsMenu(const decomposition::TwoQubitGateSequence& seq, - const NativeProfileSpec& spec); - -/// Decompose a `4×4` target unitary into a gate sequence targeting the given -/// entangler basis, using `TwoQubitWeylDecomposition` + -/// `TwoQubitBasisDecomposer` with the supplied Euler basis. -std::optional -decomposeTwoQubitFromMatrix(const Eigen::Matrix4cd& matrix, - EntanglerBasis entangler, - decomposition::GateEulerBasis eulerBasis, - std::optional numBasisUses); - -/// Enumerate all direct + matrix-fallback single-qubit rewrite candidates. -llvm::SmallVector, 0> -collectSingleQubitCandidates(UnitaryOpInterface unitary, - const NativeProfileSpec& spec); - -/// Enumerate full two-qubit basis-decomposer candidates for a given `4×4` -/// target. -llvm::SmallVector, 0> -collectTwoQubitBasisCandidatesFromMatrix(const Eigen::Matrix4cd& targetMatrix, - const NativeProfileSpec& spec); - -/// Overload that reads the target matrix from a two-qubit op. -llvm::SmallVector, 0> -collectTwoQubitBasisCandidates(UnitaryOpInterface unitary, - const NativeProfileSpec& spec); - -/// Scoring metrics for the `rewriteXXPlusMinusYYViaRzz` lowering (both -/// `XXPlusYY` and `XXMinusYY` branches emit the same gate counts). -CandidateMetrics xxPlusMinusYyRzzRewriteScoringMetrics(); +/// Number of entanglers (basis-gate uses) the minimal KAK decomposition of +/// `target` requires for the entangler selected by `spec` (CX before CZ). +/// Returns `std::nullopt` when `spec` has no usable entangler basis. +std::optional +twoQubitEntanglerCount(const Matrix4x4& target, const NativeProfileSpec& spec); + +/// Synthesize the two-qubit unitary `target` (raw `4×4`, any global phase) at +/// `(qubit0, qubit1)` into native entanglers and single-qubit gates of `spec`. +/// The entangler is chosen deterministically (CX before CZ) and the +/// single-qubit factors use the first emitter's Euler basis. Writes the output +/// qubit values to `outQubit0` / `outQubit1`. +/// +/// Returns `failure()` when the profile has no usable entangler basis or the +/// KAK decomposition is not realizable with that entangler. +LogicalResult emitTwoQubitNative(IRRewriter& rewriter, Location loc, + Value qubit0, Value qubit1, + const Matrix4x4& target, + const NativeProfileSpec& spec, + Value& outQubit0, Value& outQubit1); /// Rewrite `XXPlusYY` / `XXMinusYY` via two `RZZ` blocks (menus with `rzz`). LogicalResult rewriteXXPlusMinusYYViaRzz(IRRewriter& rewriter, Operation* op); diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h index a0607f4b64..cd32596bb9 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h @@ -10,16 +10,12 @@ #pragma once -#include "mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h" - #include #include #include -#include -/// Types for native gate synthesis: menu, emitters, candidates, score weights. +/// Types for native gate synthesis: the resolved menu and its emitters. namespace mlir::qco::native_synth { @@ -61,12 +57,10 @@ enum class NativeGateKind : std::uint8_t { }; /// Single-qubit emitter specification: the target mode plus any modifiers -/// (axis pair, Euler bases to consider when decomposing, whether direct Rx -/// emission is permitted). +/// (axis pair, whether direct Rx emission is permitted). struct SingleQubitEmitterSpec { SingleQubitMode mode = SingleQubitMode::U3; AxisPair axisPair = AxisPair::RxRz; - llvm::SmallVector eulerBases; /// Only meaningful for `SingleQubitMode::ZSXX`: when set, the emitter may /// emit Rx / Ry / R directly (via an `rz * rx * rz` sandwich for the latter /// two) instead of falling back to the ZSXX Euler sequence. @@ -74,7 +68,8 @@ struct SingleQubitEmitterSpec { }; /// Resolved menu: emitters to try for 1q synthesis and entangler bases for 2q. -/// Built by `resolveNativeGatesSpec`. +/// Built by `resolveNativeGatesSpec`. Single-qubit synthesis is deterministic: +/// the first emitter is preferred and its Euler basis drives matrix synthesis. struct NativeProfileSpec { bool allowRzz = false; llvm::DenseSet allowedGates; @@ -82,62 +77,4 @@ struct NativeProfileSpec { llvm::SmallVector entanglerBases; }; -/// Weights for the deterministic local cost model. Candidate cost is -/// `twoQ * #2q + oneQ * #1q + depth * localDepth`; lower is better. -struct ScoreWeights { - double twoQ = 1.0; - double oneQ = 0.1; - double depth = 0.01; -}; - -/// Gate counts describing a synthesized candidate. -struct CandidateMetrics { - unsigned numOneQ = 0; - unsigned numTwoQ = 0; - unsigned depth = 0; -}; - -/// Tie-break classes in preference order (lower wins). Used as the final -/// structural tiebreaker in `isBetterScore` after the weighted cost and the -/// raw 2q/depth/1q counts. -enum class CandidateClass : std::uint8_t { - NativePassthrough = 0, - DirectSingleQ = 1, - MatrixSingleQ = 2, - TwoQubitBasisRewrite = 3, - XxPlusMinusViaRzz = 4, -}; - -/// Generic candidate wrapper carrying a typed rewrite plan payload. -template struct SynthesisCandidate { - CandidateClass candidateClass = CandidateClass::NativePassthrough; - CandidateMetrics metrics; - unsigned enumerationIndex = 0; - Payload payload; -}; - -/// How to rewrite a single-qubit op onto the native menu. -/// -/// - `Direct`: pattern-match the op type and emit the target gates directly -/// via `decomposeTo*` (applicable to a small fixed set of op types per -/// emitter). -/// - `MatrixFallback`: fold the op to a 2x2 matrix and run an Euler -/// decomposition in the emitter's basis; handles anything constant. -enum class SingleQubitRewriteStrategy : std::uint8_t { Direct, MatrixFallback }; - -/// Picked single-qubit rewrite: which emitter to use and how to drive it. -struct SingleQubitRewritePlan { - SingleQubitRewriteStrategy strategy = SingleQubitRewriteStrategy::Direct; - SingleQubitEmitterSpec emitter; -}; - -/// Picked two-qubit rewrite: a pre-computed abstract gate sequence produced -/// by `TwoQubitBasisDecomposer` plus the single-qubit emitter and entangler -/// basis used when materializing the sequence back into MLIR. -struct TwoQubitRewritePlan { - std::shared_ptr sequence; - SingleQubitEmitterSpec emitter; - EntanglerBasis entanglerBasis = EntanglerBasis::None; -}; - } // namespace mlir::qco::native_synth diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h index ff9f5bfc5b..ac05307042 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h @@ -11,25 +11,16 @@ #pragma once #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" #include -#include -#include #include -/// F64 helpers, global phase, SU(4) normalization, and 2q sequence emission. +/// F64 helpers, global phase, and SU(4) normalization for two-qubit synthesis. namespace mlir::qco::native_synth { -/// Convert a compile-time QCO 2x2 matrix into Eigen form. -[[nodiscard]] Eigen::Matrix2cd toEigen(const Matrix2x2& matrix); - -/// Convert a compile-time QCO 4x4 matrix into Eigen form. -[[nodiscard]] Eigen::Matrix4cd toEigen(const Matrix4x4& matrix); - /// Create an ``arith.constant`` F64. Value createF64Const(IRRewriter& rewriter, Location loc, double value); @@ -40,33 +31,18 @@ std::optional getConstantF64(Value value); void emitGPhaseIfNonTrivial(IRRewriter& rewriter, Location loc, double phase); /// Matrix equality up to a unit-modulus global phase. -bool isEquivalentUpToGlobalPhase(const Eigen::Matrix4cd& lhs, - const Eigen::Matrix4cd& rhs, +bool isEquivalentUpToGlobalPhase(const Matrix4x4& lhs, const Matrix4x4& rhs, double atol = 1e-10); /// Rescale `matrix` to determinant 1 (SU(4)) for Weyl / basis decomposers. /// No-op if det is numerically zero. -void normalizeToSU4(Eigen::Matrix4cd& matrix); +void normalizeToSU4(Matrix4x4& matrix); /// ``getUnitaryMatrix4x4`` then rescale to SU(4). -bool getNormalizedTwoQubitMatrix(UnitaryOpInterface unitary, - Eigen::Matrix4cd& matrix); +bool getNormalizedTwoQubitMatrix(UnitaryOpInterface unitary, Matrix4x4& matrix); /// 4x4 for a 2q block member (plain 2q, ``CtrlOp`` CX/CZ, or lifted 1q). Fails /// for barriers, ``gphase``, multi-control, or non-constant matrix parameters. -bool getBlockTwoQubitMatrix(Operation* op, Eigen::Matrix4cd& matrix); - -/// Emit `seq` in order. -LogicalResult -emitTwoQubitGateSequenceAtLoc(IRRewriter& rewriter, Location loc, Value qubit0, - Value qubit1, - const decomposition::TwoQubitGateSequence& seq, - Value& outQubit0, Value& outQubit1); - -/// Emit a two-qubit gate sequence and replace `op` with the resulting tails. -LogicalResult -emitTwoQubitGateSequence(IRRewriter& rewriter, Operation* op, Value qubit0, - Value qubit1, - const decomposition::TwoQubitGateSequence& seq); +bool getBlockTwoQubitMatrix(Operation* op, Matrix4x4& matrix); } // namespace mlir::qco::native_synth diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h index 358f615bd0..39289a1062 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h @@ -35,9 +35,6 @@ namespace mlir::qco { /// for recognised tokens). struct NativeGateSynthesisOptions { std::string nativeGates; - double scoreWeightTwoQ = 1.0; - double scoreWeightOneQ = 0.1; - double scoreWeightDepth = 0.01; }; std::unique_ptr diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index 59ef29b071..a633fa3244 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -189,25 +189,15 @@ def NativeGateSynthesisPass : Pass<"native-gate-synthesis", "mlir::ModuleOp"> { holds (including native `qco.ctrl` shells and bare `rzz` when allowed). If anything is still off-menu, the pass fails. - Candidate selection minimises the linear cost - `score-weight-twoq * #2q + score-weight-oneq * #1q + - score-weight-depth * local-depth`. Defaults (`1.0 / 0.1 / 0.01`) favour - minimising two-qubit count first, then single-qubit count, then depth. + Lowering is deterministic: the entangler is chosen as `cx` before `cz`, the + single-qubit factors use the first emitter's Euler basis, and the minimal + KAK entangler count drives two-qubit window replacement. }]; - let options = - [Option<"nativeGates", "native-gates", "std::string", "\"\"", - "Comma-separated native gate menu. Empty or whitespace-only is " - "a no-op. Tokens: u, x, sx, rz (or p), rx, ry, r, cx, cz, rzz. " - "Examples: x,sx,rz,cx; x,sx,rz,rx,rzz,cz; u,cx; r,cz; rx,rz,cx.">, - Option<"scoreWeightTwoQ", "score-weight-twoq", "double", "1.0", - "Weight for the number of two-qubit gates in candidate " - "scoring. Must be finite and non-negative.">, - Option<"scoreWeightOneQ", "score-weight-oneq", "double", "0.1", - "Weight for the number of single-qubit gates in candidate " - "scoring. Must be finite and non-negative.">, - Option<"scoreWeightDepth", "score-weight-depth", "double", "0.01", - "Weight for the local candidate depth in candidate scoring. " - "Must be finite and non-negative.">]; + let options = [Option< + "nativeGates", "native-gates", "std::string", "\"\"", + "Comma-separated native gate menu. Empty or whitespace-only is " + "a no-op. Tokens: u, x, sx, rz (or p), rx, ry, r, cx, cz, rzz. " + "Examples: x,sx,rz,cx; x,sx,rz,rx,rzz,cz; u,cx; r,cz; rx,rz,cx.">]; } //===----------------------------------------------------------------------===// diff --git a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h index 8f49d372d7..1ac9b8f8c3 100644 --- a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h +++ b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h @@ -183,6 +183,12 @@ struct Matrix2x2 { */ [[nodiscard]] Matrix2x2 adjoint() const; + /** + * @brief Returns the (non-conjugate) transpose of this matrix. + * @return Transposed matrix `A^T`. + */ + [[nodiscard]] Matrix2x2 transpose() const; + /** * @brief Returns the trace of this matrix. * @return Sum of diagonal entries. @@ -195,6 +201,13 @@ struct Matrix2x2 { */ [[nodiscard]] Complex determinant() const; + /** + * @brief Checks whether this matrix is approximately the identity. + * @param tol Maximum allowed complex modulus of each entry difference. + * @return True if every entry is within @p tol of the identity. + */ + [[nodiscard]] bool isIdentity(double tol = MATRIX_TOLERANCE) const; + /** * @brief Checks approximate equality using an absolute entry-wise tolerance. * @@ -322,6 +335,12 @@ struct Matrix4x4 { */ [[nodiscard]] Matrix4x4 adjoint() const; + /** + * @brief Returns the (non-conjugate) transpose of this matrix. + * @return Transposed matrix `A^T`. + */ + [[nodiscard]] Matrix4x4 transpose() const; + /** * @brief Returns the trace of this matrix. * @return Sum of diagonal entries. @@ -334,6 +353,53 @@ struct Matrix4x4 { */ [[nodiscard]] Complex determinant() const; + /** + * @brief Checks whether this matrix is approximately the identity. + * @param tol Maximum allowed complex modulus of each entry difference. + * @return True if every entry is within @p tol of the identity. + */ + [[nodiscard]] bool isIdentity(double tol = MATRIX_TOLERANCE) const; + + /** + * @brief Returns the four diagonal entries `(m00, m11, m22, m33)`. + * @return Array of diagonal entries. + */ + [[nodiscard]] std::array diagonal() const; + + /** + * @brief Builds a diagonal matrix from four diagonal entries. + * @param diagonalEntries Diagonal entries `(m00, m11, m22, m33)`. + * @return Diagonal matrix with the given entries. + */ + [[nodiscard]] static Matrix4x4 + fromDiagonal(const std::array& diagonalEntries); + + /** + * @brief Returns the entries of column @p col, top to bottom. + * @param col Column index in `[0, K_COLS)`. + * @return Array of the four column entries. + */ + [[nodiscard]] std::array column(std::size_t col) const; + + /** + * @brief Overwrites column @p col with @p values. + * @param col Column index in `[0, K_COLS)`. + * @param values New column entries, top to bottom. + */ + void setColumn(std::size_t col, const std::array& values); + + /** + * @brief Returns the element-wise real parts in row-major order. + * @return Real parts of all entries. + */ + [[nodiscard]] std::array realPart() const; + + /** + * @brief Returns the element-wise imaginary parts in row-major order. + * @return Imaginary parts of all entries. + */ + [[nodiscard]] std::array imagPart() const; + /** * @brief Checks approximate equality using an absolute entry-wise tolerance. * @@ -558,4 +624,54 @@ inline constexpr bool std::disjunction_v, std::is_same, std::is_same, std::is_same>; + +/** + * @brief Kronecker product `lhs (x) rhs` of two single-qubit matrices. + * + * Uses the computational-basis bit order where the first operand labels the + * high bit, matching `UnitaryOpInterface::getUnitaryMatrix4x4`. + * + * @param lhs Left factor (acts on the high bit / qubit 0). + * @param rhs Right factor (acts on the low bit / qubit 1). + * @return The `4x4` Kronecker product. + */ +[[nodiscard]] Matrix4x4 kron(const Matrix2x2& lhs, const Matrix2x2& rhs); + +/// Scalar-on-the-left multiply `scalar * matrix` (commutes with the member +/// `matrix * scalar`). Provided so generic code can scale a matrix from +/// either side. +[[nodiscard]] Matrix2x2 operator*(const Complex& scalar, + const Matrix2x2& matrix); +/// @copydoc operator*(const Complex&, const Matrix2x2&) +[[nodiscard]] Matrix4x4 operator*(const Complex& scalar, + const Matrix4x4& matrix); + +/** + * @brief Eigenvalues and eigenvectors of a real symmetric `4x4` matrix. + * + * `eigenvalues` are sorted ascending and `eigenvectors` holds the + * corresponding orthonormal eigenvectors as columns (column `j` is the + * eigenvector for `eigenvalues[j]`), matching the convention of + * `Eigen::SelfAdjointEigenSolver`. + */ +struct SymmetricEigen4 { + /// Eigenvalues sorted in ascending order. + std::array eigenvalues{}; + /// Orthonormal eigenvectors as columns (real-valued, zero imaginary part). + Matrix4x4 eigenvectors{}; +}; + +/** + * @brief Computes the eigendecomposition of a real symmetric `4x4` matrix. + * + * Implements the cyclic Jacobi eigenvalue algorithm, which is numerically + * robust for small symmetric matrices and yields orthonormal eigenvectors + * even for degenerate spectra. + * + * @param symmetric Row-major real symmetric `4x4` matrix. + * @return Ascending eigenvalues and matching eigenvectors (as columns). + */ +[[nodiscard]] SymmetricEigen4 +jacobiSymmetricEigen(const std::array& symmetric); + } // namespace mlir::qco diff --git a/mlir/lib/Compiler/CompilerPipeline.cpp b/mlir/lib/Compiler/CompilerPipeline.cpp index 480f0bfa2c..40846f2ef9 100644 --- a/mlir/lib/Compiler/CompilerPipeline.cpp +++ b/mlir/lib/Compiler/CompilerPipeline.cpp @@ -156,9 +156,6 @@ QuantumCompilerPipeline::runPipeline(ModuleOp module, pm.addPass( qco::createNativeGateSynthesisPass(qco::NativeGateSynthesisOptions{ .nativeGates = config_.nativeGates, - .scoreWeightTwoQ = config_.nativeGateScoreWeightTwoQ, - .scoreWeightOneQ = config_.nativeGateScoreWeightOneQ, - .scoreWeightDepth = config_.nativeGateScoreWeightDepth, })); }))) { return failure(); diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/ROp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/ROp.cpp index 1b742170fa..1baee612d9 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/ROp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/ROp.cpp @@ -115,9 +115,14 @@ std::optional ROp::getUnitaryMatrix() { return std::nullopt; } + using namespace std::complex_literals; const auto thetaSin = std::sin(*theta / 2); - const auto m01 = std::polar(thetaSin, -*phi - (std::numbers::pi / 2)); - const auto m10 = std::polar(thetaSin, *phi - (std::numbers::pi / 2)); + // `std::polar` has undefined behavior for negative magnitudes (libc++ returns + // NaN), and `sin(theta / 2)` is negative for negative `theta`. Build the + // phased entries via `sin * e^{i*phi}` instead, which is well-defined for any + // sign of the magnitude. + const auto m01 = thetaSin * std::exp(1i * (-*phi - (std::numbers::pi / 2))); + const auto m10 = thetaSin * std::exp(1i * (*phi - (std::numbers::pi / 2))); const auto thetaCos = std::cos(*theta / 2); return Matrix2x2::fromElements(thetaCos, m01, // row 0 m10, thetaCos); // row 1 diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/UOp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/UOp.cpp index f504ba3a49..4cbe633883 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/UOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/UOp.cpp @@ -133,12 +133,16 @@ std::optional UOp::getUnitaryMatrix() { return std::nullopt; } + using namespace std::complex_literals; const auto c = std::cos(*theta / 2); const auto s = std::sin(*theta / 2); - const auto m01 = std::polar(s, *lambda + std::numbers::pi); - const auto m10 = std::polar(s, *phi); - const auto m11 = std::polar(c, *phi + *lambda); + // `std::polar` has undefined behavior for negative magnitudes (libc++ returns + // NaN), and `sin`/`cos` of `theta / 2` can be negative. Build the phased + // entries via `mag * e^{i*phi}` instead, which is well-defined for any sign. + const auto m01 = s * std::exp(1i * (*lambda + std::numbers::pi)); + const auto m10 = s * std::exp(1i * (*phi)); + const auto m11 = c * std::exp(1i * (*phi + *lambda)); return Matrix2x2::fromElements(c, m01, // row 0 m10, m11); // row 1 } diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXMinusYYOp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXMinusYYOp.cpp index c75c45f26e..24e5fbf8c5 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXMinusYYOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXMinusYYOp.cpp @@ -79,10 +79,15 @@ std::optional XXMinusYYOp::getUnitaryMatrix() { return std::nullopt; } + using namespace std::complex_literals; const auto mc = std::cos(*theta / 2); const auto s = std::sin(*theta / 2); - const auto msp = std::polar(s, *beta - (std::numbers::pi / 2)); - const auto msm = std::polar(s, -*beta - (std::numbers::pi / 2)); + // `std::polar` has undefined behavior for negative magnitudes (libc++ returns + // NaN), and `s = sin(theta / 2)` is negative for negative `theta`. Build the + // phased entries via `s * e^{i*phi}` instead, which is well-defined for any + // sign of `s`. + const auto msp = s * std::exp(1i * (*beta - (std::numbers::pi / 2))); + const auto msm = s * std::exp(1i * (-*beta - (std::numbers::pi / 2))); return Matrix4x4::fromElements(mc, 0, 0, msm, // row 0 0, 1, 0, 0, // row 1 0, 0, 1, 0, // row 2 diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXPlusYYOp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXPlusYYOp.cpp index 6511b2344b..4cfb663658 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXPlusYYOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/XXPlusYYOp.cpp @@ -79,10 +79,15 @@ std::optional XXPlusYYOp::getUnitaryMatrix() { return std::nullopt; } + using namespace std::complex_literals; const auto mc = std::cos(*theta / 2); const auto s = std::sin(*theta / 2); - const auto msp = std::polar(s, *beta - (std::numbers::pi / 2)); - const auto msm = std::polar(s, -*beta - (std::numbers::pi / 2)); + // `std::polar` has undefined behavior for negative magnitudes (libc++ returns + // NaN), and `s = sin(theta / 2)` is negative for negative `theta`. Build the + // phased entries via `s * e^{i*phi}` instead, which is well-defined for any + // sign of `s`. + const auto msp = s * std::exp(1i * (*beta - (std::numbers::pi / 2))); + const auto msm = s * std::exp(1i * (-*beta - (std::numbers::pi / 2))); return Matrix4x4::fromElements(1, 0, 0, 0, // row 0 0, mc, msp, 0, // row 1 0, msm, mc, 0, // row 2 diff --git a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt index b056eb25e3..f02cdaf23b 100644 --- a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt +++ b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt @@ -12,8 +12,6 @@ add_mlir_library( MLIRQCOTransforms ${PASSES_SOURCES} LINK_LIBS - PUBLIC - Eigen3::Eigen PRIVATE MLIRQCODialect MLIRQCOUtils diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp index 250c1563a8..212c7e48ad 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp @@ -10,18 +10,15 @@ #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/Utils/Matrix.h" #include #include -#include #include #include #include @@ -34,18 +31,16 @@ #include namespace mlir::qco::decomposition { + +using namespace std::complex_literals; + 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 Matrix2x2 k12RArr = Matrix2x2::fromElements( + 1i * FRAC1_SQRT2, FRAC1_SQRT2, -FRAC1_SQRT2, -1i * FRAC1_SQRT2); + const Matrix2x2 k12LArr = + Matrix2x2::fromElements(Complex{0.5, 0.5}, Complex{0.5, 0.5}, + Complex{-0.5, 0.5}, Complex{0.5, -0.5}); // The Shende-Markov-Bullock 3-CX sandwich (and its 1/2-CX reductions) used // below is derived for a basis CX whose 4x4 matrix is the Qiskit/LSB form @@ -64,10 +59,8 @@ TwoQubitBasisDecomposer TwoQubitBasisDecomposer::create(const Gate& basisGate, // the emission boundary in `decomp{0,1,2,3}` below. This reproduces the // pre-flip gate counts without having to re-derive every SMB constant for // the MSB basis -- the two routes are algebraically equivalent. - const Eigen::Matrix4cd swap4{ - {1, 0, 0, 0}, {0, 0, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 1}}; - const Eigen::Matrix4cd basisMatrixLsb = - swap4 * getTwoQubitMatrix(basisGate) * swap4; + const Matrix4x4 basisMatrixLsb = + swapGate() * getTwoQubitMatrix(basisGate) * swapGate(); const auto basisDecomposer = decomposition::TwoQubitWeylDecomposition::create( basisMatrixLsb, basisFidelity); const auto isSuperControlled = @@ -77,47 +70,38 @@ TwoQubitBasisDecomposer TwoQubitBasisDecomposer::create(const Gate& basisGate, // 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)}, - }; - 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 + Complex temp{0.5, -0.5}; + const Matrix2x2 k11l = Matrix2x2::fromElements( + temp * (-1i * std::exp(-1i * b)), temp * std::exp(-1i * b), + temp * (-1i * std::exp(1i * b)), temp * -std::exp(1i * b)); + const Matrix2x2 k11r = Matrix2x2::fromElements( + 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 Matrix2x2 k32lK21l = + Matrix2x2::fromElements(FRAC1_SQRT2 * Complex{1., std::cos(2. * b)}, + FRAC1_SQRT2 * (1i * std::sin(2. * b)), + FRAC1_SQRT2 * (1i * std::sin(2. * b)), + FRAC1_SQRT2 * Complex{1., -std::cos(2. * b)}); + temp = Complex{0.5, 0.5}; + const Matrix2x2 k21r = Matrix2x2::fromElements( + temp * (-1i * std::exp(-2i * b)), temp * std::exp(-2i * b), + temp * (1i * std::exp(2i * b)), temp * std::exp(2i * b)); + const Matrix2x2 k22l = Matrix2x2::fromElements(FRAC1_SQRT2, -FRAC1_SQRT2, + FRAC1_SQRT2, FRAC1_SQRT2); + const Matrix2x2 k22r = Matrix2x2::fromElements(0, 1, -1, 0); + const Matrix2x2 k31l = Matrix2x2::fromElements( + 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 Matrix2x2 k31r = Matrix2x2::fromElements(1i * std::exp(1i * b), 0, 0, + -1i * std::exp(-1i * b)); + const Matrix2x2 k32r = Matrix2x2::fromElements( + 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().adjoint(); + auto k1rDagger = basisDecomposer.k1r().adjoint(); + auto k2lDagger = basisDecomposer.k2l().adjoint(); + auto k2rDagger = basisDecomposer.k2r().adjoint(); + // 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; @@ -129,13 +113,12 @@ TwoQubitBasisDecomposer TwoQubitBasisDecomposer::create(const Gate& basisGate, 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(); + // Pre-build the fixed parts of the matrices used in the 2-part decomposition + auto q0l = k12LArr.adjoint() * k1lDagger; + auto q0r = k12RArr.adjoint() * ipz() * k1rDagger; + auto q1la = k2lDagger * k11l.adjoint(); auto q1lb = k11l * k1lDagger; - auto q1ra = k2rDagger * IPZ * k11r.transpose().conjugate(); + auto q1ra = k2rDagger * ipz() * k11r.adjoint(); auto q1rb = k11r * k1rDagger; auto q2l = k2lDagger * k12LArr; auto q2r = k2rDagger * k12RArr; @@ -167,42 +150,19 @@ TwoQubitBasisDecomposer TwoQubitBasisDecomposer::create(const Gate& basisGate, }; } -std::optional TwoQubitBasisDecomposer::twoQubitDecompose( +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 { // Pick the number of basis gate uses `i ∈ {0, 1, 2, 3}` that maximizes - // expected_fidelity(i) = traceToFidelity(traces[i]) * basisFidelity^i - // i.e. "how well does using `i` basis gates approximate the target, - // assuming each basis gate has fidelity `basisFidelity`". With - // `basisFidelity == 1.0` (exact mode) the `pow` factor is constant and - // the larger `i` values tend to win because they can represent any - // SU(4); when `basisFidelity < 1.0` the `pow(...)^i` penalty lets - // shorter (lower-`i`) approximations win when the target is close - // enough. This is *not* a "smallest `i` above a threshold" rule. + // expected_fidelity(i) = traceToFidelity(traces[i]) * basisFidelity^i. 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); + auto value = + helpers::traceToFidelity(traces[i]) * std::pow(basisFidelity, i); if (std::isnan(value)) { continue; } @@ -242,72 +202,38 @@ std::optional TwoQubitBasisDecomposer::twoQubitDecompose( llvm::Twine(bestNbasis) + ")!"); llvm_unreachable(""); }; - auto decomposition = chooseDecomposition(); - llvm::SmallVector eulerDecompositions; - for (auto&& decomp : decomposition) { - assert(helpers::isUnitaryMatrix(decomp)); - eulerDecompositions.push_back( - unitaryToGateSequence(decomp, target1qEulerBases, true, std::nullopt)); + TwoQubitLocalUnitaryList factors = chooseDecomposition(); +#ifndef NDEBUG + for (const auto& factor : factors) { + assert(helpers::isUnitaryMatrix(factor)); } - 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 differs, but the - // worst case is a modest number of extra `Gate` slots; sequences are - // short-lived before lowering. - const auto twoQubitSequenceDefaultCapacity = - static_cast((11 * bestNbasis) + 10); - gates.gates.reserve(twoQubitSequenceDefaultCapacity); - gates.globalPhase -= bestNbasis * basisDecomposer.globalPhase(); +#endif + + double globalPhase = targetDecomposition.globalPhase(); + globalPhase -= bestNbasis * basisDecomposer.globalPhase(); if (bestNbasis == 2) { // The two-basis (2x CX/CZ) template in `decomp2Supercontrolled` produces // a sequence whose global phase is off by `pi` relative to the target; - // compensate here so the emitted sequence reproduces the target - // unitary exactly, not just up to sign. - gates.globalPhase += std::numbers::pi; + // compensate here so the emitted sequence reproduces the target unitary + // exactly, not just up to sign. + globalPhase += std::numbers::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 - // With q0 = MSB, `kron(K1l, K1r)` places the "l" factor on qubit 0 and the - // "r" factor on qubit 1; Weyl emits the "r" factor at even indices. - addEulerDecomposition(2 * i, 1); - addEulerDecomposition((2 * i) + 1, 0); - - // add basis gate - gates.gates.push_back(basisGate); - } - - // add single-qubit decompositions after basis gate - addEulerDecomposition(2UL * bestNbasis, 1); - addEulerDecomposition((2UL * bestNbasis) + 1UL, 0); - // 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, 2.0 * std::numbers::pi); + // it to [0, +2*pi) + globalPhase = helpers::remEuclid(globalPhase, 2.0 * std::numbers::pi); - return gates; + return TwoQubitNativeDecomposition{ + .numBasisUses = bestNbasis, + .singleQubitFactors = std::move(factors), + .globalPhase = globalPhase, + }; } // Ported SMB helpers assume Qiskit Weyl k-factor layout; QCO 4x4 input order // swaps l/r vs that port. Swap k1l<->k1r and k2l<->k2r when reading ``target``, -// and swap adjacent pairs in each return vector so ``addEulerDecomposition`` -// maps matrices to the same wires as the upstream decomposer. ``decomp0`` -// cancels to the unswapped formula. +// and swap adjacent pairs in each return vector so the emission boundary maps +// matrices to the same wires as the upstream decomposer. ``decomp0`` cancels to +// the unswapped formula. TwoQubitLocalUnitaryList TwoQubitBasisDecomposer::decomp0(const TwoQubitWeylDecomposition& target) { return TwoQubitLocalUnitaryList{ @@ -320,10 +246,10 @@ TwoQubitLocalUnitaryList TwoQubitBasisDecomposer::decomp1( const TwoQubitWeylDecomposition& target) const { // may not work for z != 0 and c != 0 (not always in Weyl chamber) return TwoQubitLocalUnitaryList{ - basisDecomposer.k2l().transpose().conjugate() * target.k2r(), - basisDecomposer.k2r().transpose().conjugate() * target.k2l(), - target.k1r() * basisDecomposer.k1l().transpose().conjugate(), - target.k1l() * basisDecomposer.k1r().transpose().conjugate(), + basisDecomposer.k2l().adjoint() * target.k2r(), + basisDecomposer.k2r().adjoint() * target.k2l(), + target.k1r() * basisDecomposer.k1l().adjoint(), + target.k1l() * basisDecomposer.k1r().adjoint(), }; } @@ -392,34 +318,6 @@ TwoQubitBasisDecomposer::traces(const TwoQubitWeylDecomposition& target) const { }; } -OneQubitGateSequence TwoQubitBasisDecomposer::unitaryToGateSequence( - const Eigen::Matrix2cd& unitaryMat, - const llvm::SmallVector& targetBasisList, 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); - // Sequence is on qubit 0; check against ``expandToTwoQubits(unitaryMat, - // 0)``. - assert((circuit.getUnitaryMatrix().isApprox( - expandToTwoQubits(unitaryMat, 0), 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 diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index 085d493abf..5f017109c5 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -102,21 +102,6 @@ static void emitGPhaseIfNeeded(OpBuilder& builder, Location loc, double phase) { // Euler decomposition (angles) //===----------------------------------------------------------------------===// -/** - * @brief Euler angles `(theta, phi, lambda)` and global phase for a 2x2 - * unitary. - */ -namespace { - -struct EulerAngles { - double theta = 0.0; ///< Middle rotation angle. - double phi = 0.0; ///< First outer rotation angle. - double lambda = 0.0; ///< Second outer rotation angle. - double phase = 0.0; ///< Global phase in radians. -}; - -} // namespace - /** * @brief Z-Y-Z Euler angles and global phase for a 2x2 unitary. * @@ -197,15 +182,7 @@ struct EulerAngles { .phase = phase - (0.5 * (phi + lambda))}; } -/** - * @brief Extracts `(theta, phi, lambda, phase)` for all Euler bases. - * - * @param matrix The single-qubit unitary to decompose. - * @param basis The target Euler basis. - * @return The extracted Euler angles and global phase. - */ -[[nodiscard]] static EulerAngles anglesFromUnitary(const Matrix2x2& matrix, - const EulerBasis basis) { +EulerAngles anglesFromUnitary(const Matrix2x2& matrix, const EulerBasis basis) { switch (basis) { case EulerBasis::ZYZ: case EulerBasis::ZSXX: @@ -215,6 +192,9 @@ struct EulerAngles { case EulerBasis::XZX: return paramsXZX(matrix); case EulerBasis::XYX: + case EulerBasis::R: + // The `R` basis reuses the X-Y-X angles and lowers `Rx`/`Ry` to the native + // `R(theta, phi)` gate (`Rx(a) == R(a, 0)`, `Ry(a) == R(a, pi/2)`). return paramsXYX(matrix); case EulerBasis::U: return paramsU(matrix); @@ -235,7 +215,7 @@ namespace { * `RZ`/`RY`/`RX` use @p theta as the rotation angle; `U` uses all three angles. */ struct SynthesisStep { - enum class Kind : std::uint8_t { RZ, RY, RX, SX, X, U }; + enum class Kind : std::uint8_t { RZ, RY, RX, SX, X, U, R }; Kind kind = Kind::RZ; double theta = 0.0; @@ -264,6 +244,19 @@ struct Unitary1QEulerPlan { } } + /** + * @brief Appends a native `R(angle, axis)` step for non-negligible angles. + * + * @param angle The rotation angle in radians. + * @param axis The rotation axis in the XY-plane (`0` for `Rx`, `pi/2` for + * `Ry`). + */ + void appendRStep(const double angle, const double axis) { + if (!isNearZeroRotationAngle(angle)) { + steps.emplace_back(SynthesisStep::Kind::R, angle, axis); + } + } + /** * @brief Appends the decomposition for @p basis based on @p angles. * @@ -290,6 +283,9 @@ struct Unitary1QEulerPlan { case EulerBasis::XYX: appendRotation(SynthesisStep::Kind::RX, angles.phi + angles.lambda); break; + case EulerBasis::R: + appendRStep(angles.phi + angles.lambda, 0.0); + break; case EulerBasis::U: steps.emplace_back(SynthesisStep::Kind::U, 0.0, angles.phi, angles.lambda); @@ -324,6 +320,14 @@ struct Unitary1QEulerPlan { appendRotation(SynthesisStep::Kind::RX, angles.phi); phase = angles.phase; break; + case EulerBasis::R: + // X-Y-X with `Rx(a) == R(a, 0)` and `Ry(a) == R(a, pi/2)`. + appendRStep(angles.lambda, 0.0); + steps.emplace_back(SynthesisStep::Kind::R, angles.theta, + std::numbers::pi / 2.0); + appendRStep(angles.phi, 0.0); + phase = angles.phase; + break; case EulerBasis::U: steps.emplace_back(SynthesisStep::Kind::U, angles.theta, angles.phi, angles.lambda); @@ -413,6 +417,9 @@ emitUnitary1QEulerPlan(OpBuilder& builder, Location loc, Value qubit, qubit = UOp::create(builder, loc, qubit, theta, phi, lambda).getQubitOut(); break; + case SynthesisStep::Kind::R: + qubit = ROp::create(builder, loc, qubit, theta, phi).getQubitOut(); + break; } } emitGPhaseIfNeeded(builder, loc, plan.phase); @@ -427,6 +434,7 @@ std::optional parseEulerBasis(StringRef basis) { .Case("xyx", EulerBasis::XYX) .Case("u", EulerBasis::U) .Case("zsxx", EulerBasis::ZSXX) + .Case("r", EulerBasis::R) .Default(std::nullopt); } diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerBasis.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerBasis.cpp deleted file mode 100644 index 7b11cf5c25..0000000000 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerBasis.cpp +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h" - -#include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" - -#include -#include - -namespace mlir::qco::decomposition { - -[[nodiscard]] llvm::SmallVector -getGateTypesForEulerBasis(GateEulerBasis eulerBasis) { - switch (eulerBasis) { - case GateEulerBasis::ZYZ: - // Z-Y-Z style decompositions only emit `rz` and `ry`. - return {GateKind::RZ, GateKind::RY}; - case GateEulerBasis::ZXZ: - // Z-X-Z and X-Z-X share the same two-axis alphabet with swapped roles. - return {GateKind::RZ, GateKind::RX}; - case GateEulerBasis::XZX: - return {GateKind::RX, GateKind::RZ}; - case GateEulerBasis::XYX: - return {GateKind::RX, GateKind::RY}; - case GateEulerBasis::U3: - [[fallthrough]]; - case GateEulerBasis::U321: - [[fallthrough]]; - case GateEulerBasis::U: - // All U variants collapse to a single `u` operation at emission time. - return {GateKind::U}; - case GateEulerBasis::ZSX: - // `ZSX` only emits `rz` and `sx`. - return {GateKind::RZ, GateKind::SX}; - case GateEulerBasis::ZSXX: - // `ZSXX` additionally allows a bare `X` when the middle rotation is - // +/- pi, staying within the `{rz, sx, x}` alphabet. - return {GateKind::RZ, GateKind::SX, GateKind::X}; - } - 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 deleted file mode 100644 index 6dd3ddf7dc..0000000000 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.cpp +++ /dev/null @@ -1,317 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "mlir/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.h" - -#include "mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.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(GateEulerBasis targetBasis, - const Eigen::Matrix2cd& unitaryMatrix, - bool simplify, std::optional atol) { - // First normalize the input into basis-specific Euler parameters, then map - // those parameters to the target gate alphabet. - auto [theta, phi, lambda, phase] = - anglesFromUnitary(unitaryMatrix, targetBasis); - - switch (targetBasis) { - case GateEulerBasis::ZYZ: - return decomposeKAK(theta, phi, lambda, phase, GateKind::RZ, GateKind::RY, - simplify, atol); - case GateEulerBasis::ZXZ: - return decomposeKAK(theta, phi, lambda, phase, GateKind::RZ, GateKind::RX, - simplify, atol); - case GateEulerBasis::XZX: - return decomposeKAK(theta, phi, lambda, phase, GateKind::RX, GateKind::RZ, - simplify, atol); - case GateEulerBasis::XYX: - return decomposeKAK(theta, phi, lambda, phase, GateKind::RX, GateKind::RY, - simplify, atol); - case GateEulerBasis::U: - [[fallthrough]]; - case GateEulerBasis::U3: - [[fallthrough]]; - case GateEulerBasis::U321: - return OneQubitGateSequence{ - .gates = {{.type = GateKind::U, .parameter = {theta, phi, lambda}}}, - .globalPhase = phase - ((phi + lambda) / 2.), - }; - case GateEulerBasis::ZSX: - return decomposePsxGen(theta, phi, lambda, phase, /*allowXShortcut=*/false, - simplify, atol); - case GateEulerBasis::ZSXX: - return decomposePsxGen(theta, phi, lambda, phase, /*allowXShortcut=*/true, - simplify, atol); - } - llvm::reportFatalInternalError( - "Unsupported euler basis for circuit generation in decomposition!"); -} - -std::array -EulerDecomposition::anglesFromUnitary(const Eigen::Matrix2cd& matrix, - GateEulerBasis basis) { - switch (basis) { - case GateEulerBasis::XYX: - return paramsXyx(matrix); - case GateEulerBasis::XZX: - return paramsXzx(matrix); - case GateEulerBasis::ZYZ: - return paramsZyz(matrix); - case GateEulerBasis::ZXZ: - return paramsZxz(matrix); - case GateEulerBasis::U: - case GateEulerBasis::U3: - case GateEulerBasis::U321: - // The `u` gate parameterization is derived from the standard Z-Y-Z form. - return paramsZyz(matrix); - case GateEulerBasis::ZSX: - case GateEulerBasis::ZSXX: - // Qiskit's `params_u1x_inner` reuses Z-Y-Z angles but shifts the global - // phase by `-0.5 * (theta + phi + lambda)` so that the decomposition - // matches an `rz`/`sx` emission exactly (not only up to global phase). - return paramsU1x(matrix); - } - llvm::reportFatalInternalError( - "Unsupported euler basis for angle computation in decomposition!"); -} - -std::array -EulerDecomposition::paramsZyz(const Eigen::Matrix2cd& matrix) { - // Split the matrix determinant into a scalar phase and an SU(2) part, then - // recover the canonical Z-Y-Z angles from the relative entry magnitudes and - // phases. - 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) { - // Convert from the Z-Y-Z parameterization via the standard basis-change - // identity RY(a) = RZ(pi/2) RX(a) RZ(-pi/2), i.e. - // RZ(phi) RY(theta) RZ(lambda) = - // RZ(phi + pi/2) RX(theta) RZ(lambda - pi/2). - const auto [theta, phi, lam, phase] = paramsZyz(matrix); - return {theta, phi + (std::numbers::pi / 2.0), lam - (std::numbers::pi / 2.0), - phase}; -} - -std::array -EulerDecomposition::paramsXyx(const Eigen::Matrix2cd& matrix) { - // Conjugating by Hadamards transforms an X-Y-X decomposition problem into a - // Z-Y-Z one, so we solve it there and map the angles back. - 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 + std::numbers::pi, 0.); - auto newLam = helpers::mod2pi(lam + std::numbers::pi, 0.); - return { - theta, - newPhi, - newLam, - phase + ((newPhi + newLam - phi - lam) / 2.), - }; -} - -std::array -EulerDecomposition::paramsU1x(const Eigen::Matrix2cd& matrix) { - // The determinant of the rz/sx emission depends on the Euler parameters. - // Shift the scalar phase so that `decomposePsxGen` can emit an exact - // (non-projective) decomposition in terms of `rz` and `sx`. - const auto [theta, phi, lambda, phase] = paramsZyz(matrix); - return {theta, phi, lambda, phase - (0.5 * (theta + phi + lambda))}; -} - -std::array -EulerDecomposition::paramsXzx(const Eigen::Matrix2cd& matrix) { - // Rewrite the matrix into a form where the residual SU(2) part can be - // interpreted as a Z-X-Z decomposition, then lift the resulting phase back - // to the original 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, GateKind kGate, GateKind aGate, - bool simplify, std::optional atol) { - // Treat tiny angles as zero when simplification is enabled. - 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 = {}, - // Track the scalar phase so emitted K-A-K rotations match the input - // unitary exactly (not only up to global phase). - .globalPhase = phase - ((phi + lambda) / 2.), - }; - if (std::abs(theta) <= angleZeroEpsilon) { - // A(0) vanishes, so K(lambda) A(0) K(phi) collapses to K(lambda + phi). - 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 - std::numbers::pi) <= angleZeroEpsilon) { - // At theta ~= pi, Euler parameters are non-unique. Rewrite into a stable - // equivalent form to keep emission deterministic. - sequence.globalPhase += phi; - lambda -= phi; - phi = 0.0; - } - if (std::abs(helpers::mod2pi(lambda + std::numbers::pi)) <= - angleZeroEpsilon || - std::abs(helpers::mod2pi(phi + std::numbers::pi)) <= angleZeroEpsilon) { - // Shift away from the -pi branch cut by an equivalent parameterization. - lambda += std::numbers::pi; - theta = -theta; - phi += std::numbers::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; -} - -OneQubitGateSequence -EulerDecomposition::decomposePsxGen(double theta, double phi, double lambda, - double phase, bool allowXShortcut, - bool simplify, std::optional atol) { - double angleZeroEpsilon = atol.value_or(DEFAULT_ATOL); - if (!simplify) { - // Disable all simplification checks by using a negative tolerance so that - // every `std::abs(...) < atol` comparison evaluates to false. - angleZeroEpsilon = -1.0; - } - - OneQubitGateSequence sequence{ - .gates = {}, - .globalPhase = phase, - }; - - // Append `RZ(angle)` and add `angle / 2` to `globalPhase` so the combined - // effect matches the `rz`/`sx` bookkeeping used here (RZ vs scalar phase). - // Small angles after `mod2pi` are dropped when simplification is enabled. - auto emitRzAsP = [&](double angle) { - const double canonicalAngle = helpers::mod2pi(angle); - if (std::abs(canonicalAngle) > angleZeroEpsilon) { - sequence.gates.push_back( - {.type = GateKind::RZ, .parameter = {canonicalAngle}}); - sequence.globalPhase += canonicalAngle / 2.0; - } - }; - - // Zero-`sx` decomposition: RZ(phi) . I . RZ(lambda) collapses to a single - // phase gate RZ(lambda + phi) (plus the matching phase correction). - if (std::abs(theta) < angleZeroEpsilon) { - emitRzAsP(lambda + phi); - return sequence; - } - - // Single-`sx` decomposition: - // RZ(phi) . RY(pi/2) . RZ(lambda) - // = P(phi + pi/2) . SX . P(lambda - pi/2) . e^{-i * pi / 4} - if (std::abs(theta - (std::numbers::pi / 2.0)) < angleZeroEpsilon) { - emitRzAsP(lambda - (std::numbers::pi / 2.0)); - sequence.gates.push_back({.type = GateKind::SX}); - emitRzAsP(phi + (std::numbers::pi / 2.0)); - return sequence; - } - - // General two-`sx` decomposition. - if (std::abs(theta - std::numbers::pi) < angleZeroEpsilon) { - sequence.globalPhase += lambda; - phi -= lambda; - lambda = 0.0; - } - if (std::abs(helpers::mod2pi(lambda + std::numbers::pi)) < angleZeroEpsilon || - std::abs(helpers::mod2pi(phi)) < angleZeroEpsilon) { - lambda += std::numbers::pi; - theta = -theta; - phi += std::numbers::pi; - sequence.globalPhase -= theta; - } - // Shift theta and phi to turn the decomposition from - // RZ(phi) . RY(theta) . RZ(lambda) - // = RZ(phi) . RX(-pi/2) . RZ(theta) . RX(+pi/2) . RZ(lambda) - // into P(phi + pi) . SX . P(theta + pi) . SX . P(lambda). - theta += std::numbers::pi; - phi += std::numbers::pi; - sequence.globalPhase -= std::numbers::pi / 2.0; - - emitRzAsP(lambda); - if (allowXShortcut && std::abs(helpers::mod2pi(theta)) < angleZeroEpsilon) { - // `SX . P(theta) . SX` with `theta` congruent to `+/- pi` simplifies to - // a bare `X` gate (up to the already-tracked global phase). - sequence.gates.push_back({.type = GateKind::X}); - } else { - sequence.gates.push_back({.type = GateKind::SX}); - emitRzAsP(theta); - sequence.gates.push_back({.type = GateKind::SX}); - } - emitRzAsP(phi); - return sequence; -} - -} // namespace mlir::qco::decomposition diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/GateSequence.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/GateSequence.cpp deleted file mode 100644 index 29db6a303e..0000000000 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/GateSequence.cpp +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h" - -#include "mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" - -#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()) { - // Count the same heuristic cost as an explicit global-phase gate. - c += helpers::getComplexity(GateKind::GPhase, 0); - } - return c; -} - -Eigen::Matrix4cd QubitGateSequence::getUnitaryMatrix() const { - Eigen::Matrix4cd unitaryMatrix = Eigen::Matrix4cd::Identity(); - for (auto&& gate : gates) { - // Left-multiply each gate matrix so the stored order matches execution - // order in the reconstructed unitary. - 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 index 99bea5fa83..278efa91ef 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp @@ -13,6 +13,7 @@ #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" #include #include @@ -57,6 +58,14 @@ decomposition::GateKind getGateKind(UnitaryOpInterface op) { }); } +bool isUnitaryMatrix(const Matrix2x2& matrix, double tolerance) { + return (matrix.adjoint() * matrix).isIdentity(tolerance); +} + +bool isUnitaryMatrix(const Matrix4x4& matrix, double tolerance) { + return (matrix.adjoint() * matrix).isIdentity(tolerance); +} + double remEuclid(double a, double b) { if (b == 0.0) { llvm::reportFatalInternalError( diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp index cf218ea3b1..e7d014a777 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp @@ -12,110 +12,141 @@ #include "mlir/Dialect/QCO/Transforms/Decomposition/Gate.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" -#include #include #include #include #include #include -#include namespace mlir::qco::decomposition { -Eigen::Matrix2cd uMatrix(double theta, double phi, double lambda) { - 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.)}}}}; +Matrix2x2 uMatrix(double theta, double phi, double lambda) { + const auto cosHalf = std::cos(theta / 2.); + const auto sinHalf = std::sin(theta / 2.); + return Matrix2x2::fromElements( + Complex{cosHalf, 0.}, + Complex{-std::cos(lambda) * sinHalf, -std::sin(lambda) * sinHalf}, + Complex{std::cos(phi) * sinHalf, std::sin(phi) * sinHalf}, + Complex{std::cos(lambda + phi) * cosHalf, + std::sin(lambda + phi) * cosHalf}); } -Eigen::Matrix2cd u2Matrix(double phi, double lambda) { - 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}}}; +Matrix2x2 u2Matrix(double phi, double lambda) { + return Matrix2x2::fromElements( + Complex{FRAC1_SQRT2, 0.}, + Complex{-std::cos(lambda) * FRAC1_SQRT2, -std::sin(lambda) * FRAC1_SQRT2}, + Complex{std::cos(phi) * FRAC1_SQRT2, std::sin(phi) * FRAC1_SQRT2}, + Complex{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}}; +Matrix2x2 rxMatrix(double theta) { + const auto halfTheta = theta / 2.; + const Complex cos{std::cos(halfTheta), 0.}; + const Complex isin{0., -std::sin(halfTheta)}; + return Matrix2x2::fromElements(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}}; +Matrix2x2 ryMatrix(double theta) { + const auto halfTheta = theta / 2.; + const Complex cos{std::cos(halfTheta), 0.}; + const Complex sin{std::sin(halfTheta), 0.}; + return Matrix2x2::fromElements(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.)}}}; +Matrix2x2 rzMatrix(double theta) { + return Matrix2x2::fromElements( + Complex{std::cos(theta / 2.), -std::sin(theta / 2.)}, 0., 0., + Complex{std::cos(theta / 2.), std::sin(theta / 2.)}); } -Eigen::Matrix4cd rxxMatrix(double theta) { +Matrix4x4 rxxMatrix(double theta) { const auto cosTheta = std::cos(theta / 2.); - const auto sinTheta = std::sin(theta / 2.); + const Complex misin{0., -std::sin(theta / 2.)}; + return Matrix4x4::fromElements(cosTheta, 0, 0, misin, // + 0, cosTheta, misin, 0, // + 0, misin, cosTheta, 0, // + misin, 0, 0, cosTheta); +} - return Eigen::Matrix4cd{{cosTheta, 0, 0, {0., -sinTheta}}, - {0, cosTheta, {0., -sinTheta}, 0}, - {0, {0., -sinTheta}, cosTheta, 0}, - {{0., -sinTheta}, 0, 0, cosTheta}}; +Matrix4x4 ryyMatrix(double theta) { + const auto cosTheta = std::cos(theta / 2.); + const Complex isin{0., std::sin(theta / 2.)}; + const Complex misin{0., -std::sin(theta / 2.)}; + return Matrix4x4::fromElements(cosTheta, 0, 0, isin, // + 0, cosTheta, misin, 0, // + 0, misin, cosTheta, 0, // + isin, 0, 0, cosTheta); } -Eigen::Matrix4cd ryyMatrix(double theta) { +Matrix4x4 rzzMatrix(double theta) { const auto cosTheta = std::cos(theta / 2.); const auto sinTheta = std::sin(theta / 2.); + const Complex em{cosTheta, -sinTheta}; + const Complex ep{cosTheta, sinTheta}; + return Matrix4x4::fromElements(em, 0, 0, 0, // + 0, ep, 0, 0, // + 0, 0, ep, 0, // + 0, 0, 0, em); +} - return Eigen::Matrix4cd{{{cosTheta, 0, 0, {0., sinTheta}}, - {0, cosTheta, {0., -sinTheta}, 0}, - {0, {0., -sinTheta}, cosTheta, 0}, - {{0., sinTheta}, 0, 0, cosTheta}}}; +Matrix2x2 pMatrix(double lambda) { + return Matrix2x2::fromElements(1., 0., 0., + Complex{std::cos(lambda), std::sin(lambda)}); } -Eigen::Matrix4cd rzzMatrix(double theta) { - const auto cosTheta = std::cos(theta / 2.); - const auto sinTheta = std::sin(theta / 2.); +const Matrix4x4& swapGate() { + static const Matrix4x4 matrix = Matrix4x4::fromElements(1, 0, 0, 0, // + 0, 0, 1, 0, // + 0, 1, 0, 0, // + 0, 0, 0, 1); + return matrix; +} + +const Matrix2x2& hGate() { + static const Matrix2x2 matrix = Matrix2x2::fromElements( + FRAC1_SQRT2, FRAC1_SQRT2, FRAC1_SQRT2, -FRAC1_SQRT2); + return matrix; +} - return Eigen::Matrix4cd{{{cosTheta, -sinTheta}, 0, 0, 0}, - {0, {cosTheta, sinTheta}, 0, 0}, - {0, 0, {cosTheta, sinTheta}, 0}, - {0, 0, 0, {cosTheta, -sinTheta}}}; +const Matrix2x2& ipz() { + static const Matrix2x2 matrix = + Matrix2x2::fromElements(Complex{0, 1}, 0, 0, Complex{0, -1}); + return matrix; } -Eigen::Matrix2cd pMatrix(double lambda) { - return Eigen::Matrix2cd{{1, 0}, {0, {std::cos(lambda), std::sin(lambda)}}}; +const Matrix2x2& ipy() { + static const Matrix2x2 matrix = Matrix2x2::fromElements(0, 1, -1, 0); + return matrix; } -Eigen::Matrix4cd expandToTwoQubits(const Eigen::Matrix2cd& singleQubitMatrix, - QubitId qubitId) { +const Matrix2x2& ipx() { + static const Matrix2x2 matrix = + Matrix2x2::fromElements(0, Complex{0, 1}, Complex{0, 1}, 0); + return matrix; +} + +Matrix4x4 expandToTwoQubits(const Matrix2x2& singleQubitMatrix, + QubitId qubitId) { if (qubitId == 0) { - return Eigen::kroneckerProduct(singleQubitMatrix, - Eigen::Matrix2cd::Identity()); + return kron(singleQubitMatrix, Matrix2x2::identity()); } if (qubitId == 1) { - return Eigen::kroneckerProduct(Eigen::Matrix2cd::Identity(), - singleQubitMatrix); + return kron(Matrix2x2::identity(), singleQubitMatrix); } llvm::reportFatalInternalError("Invalid qubit id for single-qubit expansion"); } -Eigen::Matrix4cd -fixTwoQubitMatrixQubitOrder(const Eigen::Matrix4cd& twoQubitMatrix, +Matrix4x4 +fixTwoQubitMatrixQubitOrder(const Matrix4x4& twoQubitMatrix, const llvm::SmallVector& qubitIds) { if (qubitIds == llvm::SmallVector{1, 0}) { // `UnitaryOpInterface::getUnitaryMatrix4x4` uses a fixed index order; // conjugate by SWAP when operand order is (1, 0) instead of (0, 1). - return decomposition::SWAP_GATE * twoQubitMatrix * decomposition::SWAP_GATE; + return swapGate() * twoQubitMatrix * swapGate(); } if (qubitIds == llvm::SmallVector{0, 1}) { return twoQubitMatrix; @@ -124,11 +155,10 @@ fixTwoQubitMatrixQubitOrder(const Eigen::Matrix4cd& twoQubitMatrix, "Invalid qubit IDs for fixing two-qubit matrix"); } -Eigen::Matrix2cd getSingleQubitMatrix(const Gate& gate) { +Matrix2x2 getSingleQubitMatrix(const Gate& gate) { if (gate.type == GateKind::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}}}; + return Matrix2x2::fromElements(Complex{0.5, 0.5}, Complex{0.5, -0.5}, + Complex{0.5, -0.5}, Complex{0.5, 0.5}); } if (gate.type == GateKind::RX) { assert(gate.parameter.size() == 1); @@ -143,10 +173,10 @@ Eigen::Matrix2cd getSingleQubitMatrix(const Gate& gate) { return rzMatrix(gate.parameter[0]); } if (gate.type == GateKind::X) { - return Eigen::Matrix2cd{{0, 1}, {1, 0}}; + return Matrix2x2::fromElements(0, 1, 1, 0); } if (gate.type == GateKind::I) { - return Eigen::Matrix2cd::Identity(); + return Matrix2x2::identity(); } if (gate.type == GateKind::P) { assert(gate.parameter.size() == 1); @@ -154,29 +184,23 @@ Eigen::Matrix2cd getSingleQubitMatrix(const Gate& gate) { } if (gate.type == GateKind::U) { assert(gate.parameter.size() == 3); - const double theta = gate.parameter[0]; - const double phi = gate.parameter[1]; - const double lambda = gate.parameter[2]; - return uMatrix(theta, phi, lambda); + return uMatrix(gate.parameter[0], gate.parameter[1], gate.parameter[2]); } if (gate.type == GateKind::U2) { assert(gate.parameter.size() == 2); - const double phi = gate.parameter[0]; - const double lambda = gate.parameter[1]; - return u2Matrix(phi, lambda); + return u2Matrix(gate.parameter[0], gate.parameter[1]); } if (gate.type == GateKind::H) { - return H_GATE; + return hGate(); } llvm::reportFatalInternalError( "unsupported gate type for single qubit matrix"); } // Reconstruct a two-qubit workspace matrix for a decomposition `Gate`. -// Used by sequence verification and `QubitGateSequence::getUnitaryMatrix()`. -Eigen::Matrix4cd getTwoQubitMatrix(const Gate& gate) { +Matrix4x4 getTwoQubitMatrix(const Gate& gate) { if (gate.qubitId.empty()) { - return Eigen::Matrix4cd::Identity(); + return Matrix4x4::identity(); } if (gate.qubitId.size() == 1) { return expandToTwoQubits(getSingleQubitMatrix(gate), gate.qubitId[0]); @@ -198,20 +222,23 @@ Eigen::Matrix4cd getTwoQubitMatrix(const Gate& gate) { // control/target wires produces a different basis-layout matrix. if (validPair01) { // control = wire 0 (MSB), target = wire 1. - return Eigen::Matrix4cd{ - {1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 1}, {0, 0, 1, 0}}; - } - if (validPair10) { - // control = wire 1, target = wire 0 (MSB). - return Eigen::Matrix4cd{ - {1, 0, 0, 0}, {0, 0, 0, 1}, {0, 0, 1, 0}, {0, 1, 0, 0}}; + return Matrix4x4::fromElements(1, 0, 0, 0, // + 0, 1, 0, 0, // + 0, 0, 0, 1, // + 0, 0, 1, 0); } - llvm::reportFatalInternalError("Invalid qubit IDs for CX gate"); + // control = wire 1, target = wire 0 (MSB). + return Matrix4x4::fromElements(1, 0, 0, 0, // + 0, 0, 0, 1, // + 0, 0, 1, 0, // + 0, 1, 0, 0); } if (gate.type == GateKind::Z) { // controlled Z (CZ) - return Eigen::Matrix4cd{ - {1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}, {0, 0, 0, -1}}; + return Matrix4x4::fromElements(1, 0, 0, 0, // + 0, 1, 0, 0, // + 0, 0, 1, 0, // + 0, 0, 0, -1); } if (gate.type == GateKind::RXX) { assert(gate.parameter.size() == 1); @@ -226,7 +253,7 @@ Eigen::Matrix4cd getTwoQubitMatrix(const Gate& gate) { return rzzMatrix(gate.parameter[0]); } if (gate.type == GateKind::I) { - return Eigen::Matrix4cd::Identity(); + return Matrix4x4::identity(); } llvm::reportFatalInternalError( "Unsupported gate type for two qubit matrix"); diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.cpp index dd227358fb..21d7c6cb59 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.cpp @@ -10,33 +10,33 @@ #include "mlir/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" -#include #include #include #include -#include // NOLINT(misc-include-cleaner) #include #include #include #include #include +#include #include #include #include #include -#include #include namespace mlir::qco::decomposition { + +using namespace std::complex_literals; + TwoQubitWeylDecomposition -TwoQubitWeylDecomposition::create(const Eigen::Matrix4cd& unitaryMatrix, +TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, std::optional fidelity) { auto u = unitaryMatrix; auto detU = u.determinant(); @@ -52,8 +52,7 @@ TwoQubitWeylDecomposition::create(const Eigen::Matrix4cd& unitaryMatrix, // Numerical drift can still leave tiny determinant errors after root // normalization. Re-normalize once more instead of aborting. auto detNormalized = u.determinant(); - if (std::abs(detNormalized - std::complex{1.0, 0.0}) > - SANITY_CHECK_PRECISION && + if (std::abs(detNormalized - Complex{1.0, 0.0}) > SANITY_CHECK_PRECISION && std::abs(detNormalized) > SANITY_CHECK_PRECISION) { u *= std::pow(detNormalized, -0.25); } @@ -63,7 +62,7 @@ TwoQubitWeylDecomposition::create(const Eigen::Matrix4cd& unitaryMatrix, // 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; + const Matrix4x4 m2 = uP.transpose() * uP; // diagonalization yields eigenvectors (p) and eigenvalues (d); // p is used to calculate K1/K2 (and thus the single-qubit gates @@ -72,135 +71,150 @@ TwoQubitWeylDecomposition::create(const Eigen::Matrix4cd& unitaryMatrix, 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, - (2.0 * std::numbers::pi)); + constexpr double pi = std::numbers::pi; + std::array dReal{}; + for (std::size_t i = 0; i < d.size(); ++i) { + dReal[i] = -std::arg(d[i]) / 2.0; + } + dReal[3] = -dReal[0] - dReal[1] - dReal[2]; + std::array cs{}; + for (std::size_t i = 0; i < cs.size(); ++i) { + cs[i] = helpers::remEuclid((dReal[i] + dReal[3]) / 2.0, 2.0 * pi); } // Reorder coordinates 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, (std::numbers::pi / 2.0)); - return std::min(tmp, (std::numbers::pi / 2.0) - 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])}; + std::array cstemp{}; + for (std::size_t i = 0; i < cs.size(); ++i) { + const auto tmp = helpers::remEuclid(cs[i], pi / 2.0); + cstemp[i] = std::min(tmp, (pi / 2.0) - tmp); + } + std::array order{0, 1, 2}; + std::stable_sort(order.begin(), order.end(), + [&](auto a, auto b) { return cstemp[a] < cstemp[b]; }); + order = {order[1], order[2], order[0]}; + cs = {cs[order[0]], cs[order[1]], cs[order[2]]}; + { + const std::array reordered{dReal[order[0]], dReal[order[1]], + dReal[order[2]]}; + dReal[0] = reordered[0]; + dReal[1] = reordered[1]; + dReal[2] = reordered[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]); + const Matrix4x4 pOrig = p; + for (std::size_t i = 0; i < order.size(); ++i) { + p.setColumn(i, pOrig.column(order[i])); } // apply correction for determinant if necessary if (p.determinant().real() < 0.0) { - auto lastColumnIndex = p.cols() - 1; - p.col(lastColumnIndex) *= -1.0; + auto lastColumn = p.column(3); + for (auto& entry : lastColumn) { + entry = -entry; + } + p.setColumn(3, lastColumn); } 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(); + // verification. Since the matrix is diagonal, the matrix exponential is + // equivalent to the element-wise exponential function. + std::array tempDiag{}; + for (std::size_t k = 0; k < tempDiag.size(); ++k) { + tempDiag[k] = std::exp(1i * dReal[k]); + } + const Matrix4x4 temp = Matrix4x4::fromDiagonal(tempDiag); // combined matrix k1 of 1q gates after canonical gate - Eigen::Matrix4cd k1 = uP * p * temp; - assert((k1.transpose() * k1).isIdentity()); // k1 must be orthogonal + Matrix4x4 k1 = uP * p * temp; + // k1 must be orthogonal; the tolerance matches the iterative diagonalization + // residual rather than the (much tighter) default matrix tolerance. + assert((k1.transpose() * k1).isIdentity(SANITY_CHECK_PRECISION)); 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 + Matrix4x4 k2 = p.adjoint(); + // k2 must be orthogonal; see the tolerance note on the k1 check above. + assert((k2.transpose() * k2).isIdentity(SANITY_CHECK_PRECISION)); 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) + std::array tempConjDiag{}; + for (std::size_t k = 0; k < tempConjDiag.size(); ++k) { + tempConjDiag[k] = std::conj(tempDiag[k]); + } assert((k1 * - magicBasisTransform(temp.conjugate(), MagicBasisTransform::Into) * k2) + magicBasisTransform(Matrix4x4::fromDiagonal(tempConjDiag), + MagicBasisTransform::Into) * + k2) .isApprox(u, SANITY_CHECK_PRECISION)); // calculate k1 = K1l ⊗ K1r - auto [K1l, K1r, phase_l] = decomposeTwoQubitProductGate(k1); + auto [K1l, K1r, phaseL] = 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)); + auto [K2l, K2r, phaseR] = decomposeTwoQubitProductGate(k2); + assert(kron(K1l, K1r).isApprox(k1, SANITY_CHECK_PRECISION)); + assert(kron(K2l, K2r).isApprox(k2, SANITY_CHECK_PRECISION)); // accumulate global phase - globalPhase += phase_l + phase_r; + globalPhase += phaseL + phaseR; // Flip into Weyl chamber - if (cs[0] > (std::numbers::pi / 2.0)) { - cs[0] -= 3.0 * (std::numbers::pi / 2.0); - K1l = K1l * IPY; - K1r = K1r * IPY; - globalPhase += (std::numbers::pi / 2.0); - } - if (cs[1] > (std::numbers::pi / 2.0)) { - cs[1] -= 3.0 * (std::numbers::pi / 2.0); - K1l = K1l * IPX; - K1r = K1r * IPX; - globalPhase += (std::numbers::pi / 2.0); + if (cs[0] > (pi / 2.0)) { + cs[0] -= 3.0 * (pi / 2.0); + K1l = K1l * ipy(); + K1r = K1r * ipy(); + globalPhase += (pi / 2.0); + } + if (cs[1] > (pi / 2.0)) { + cs[1] -= 3.0 * (pi / 2.0); + K1l = K1l * ipx(); + K1r = K1r * ipx(); + globalPhase += (pi / 2.0); } auto conjs = 0; - if (cs[0] > (std::numbers::pi / 4.0)) { - cs[0] = (std::numbers::pi / 2.0) - cs[0]; - K1l = K1l * IPY; - K2r = IPY * K2r; + if (cs[0] > (pi / 4.0)) { + cs[0] = (pi / 2.0) - cs[0]; + K1l = K1l * ipy(); + K2r = ipy() * K2r; conjs += 1; - globalPhase -= (std::numbers::pi / 2.0); + globalPhase -= (pi / 2.0); } - if (cs[1] > (std::numbers::pi / 4.0)) { - cs[1] = (std::numbers::pi / 2.0) - cs[1]; - K1l = K1l * IPX; - K2r = IPX * K2r; + if (cs[1] > (pi / 4.0)) { + cs[1] = (pi / 2.0) - cs[1]; + K1l = K1l * ipx(); + K2r = ipx() * K2r; conjs += 1; - globalPhase += (std::numbers::pi / 2.0); + globalPhase += (pi / 2.0); if (conjs == 1) { - globalPhase -= std::numbers::pi; + globalPhase -= pi; } } - if (cs[2] > (std::numbers::pi / 2.0)) { - cs[2] -= 3.0 * (std::numbers::pi / 2.0); - K1l = K1l * IPZ; - K1r = K1r * IPZ; - globalPhase += (std::numbers::pi / 2.0); + if (cs[2] > (pi / 2.0)) { + cs[2] -= 3.0 * (pi / 2.0); + K1l = K1l * ipz(); + K1r = K1r * ipz(); + globalPhase += (pi / 2.0); if (conjs == 1) { - globalPhase -= std::numbers::pi; + globalPhase -= pi; } } if (conjs == 1) { - cs[2] = (std::numbers::pi / 2.0) - cs[2]; - K1l = K1l * IPZ; - K2r = IPZ * K2r; - globalPhase += (std::numbers::pi / 2.0); + cs[2] = (pi / 2.0) - cs[2]; + K1l = K1l * ipz(); + K2r = ipz() * K2r; + globalPhase += (pi / 2.0); } - if (cs[2] > (std::numbers::pi / 4.0)) { - cs[2] -= (std::numbers::pi / 2.0); - K1l = K1l * IPZ; - K1r = K1r * IPZ; - globalPhase -= (std::numbers::pi / 2.0); + if (cs[2] > (pi / 4.0)) { + cs[2] -= (pi / 2.0); + K1l = K1l * ipz(); + K1r = K1r * ipz(); + globalPhase -= (pi / 2.0); } // bind weyl coordinates as parameters of canonical gate @@ -216,18 +230,15 @@ TwoQubitWeylDecomposition::create(const Eigen::Matrix4cd& unitaryMatrix, decomposition.k1r_ = K1r; decomposition.k2r_ = K2r; decomposition.specialization = Specialization::General; - decomposition.defaultEulerBasis = GateEulerBasis::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)); + assert((kron(K1l, K1r) * decomposition.getCanonicalMatrix() * kron(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 @@ -236,8 +247,8 @@ TwoQubitWeylDecomposition::create(const Eigen::Matrix4cd& unitaryMatrix, auto getTrace = [&]() { if (flippedFromOriginal) { return TwoQubitWeylDecomposition::getTrace( - (std::numbers::pi / 2.0) - a, b, -c, decomposition.a_, - decomposition.b_, decomposition.c_); + (pi / 2.0) - a, b, -c, decomposition.a_, decomposition.b_, + decomposition.c_); } return TwoQubitWeylDecomposition::getTrace( a, b, c, decomposition.a_, decomposition.b_, decomposition.c_); @@ -260,64 +271,48 @@ TwoQubitWeylDecomposition::create(const Eigen::Matrix4cd& unitaryMatrix, decomposition.globalPhase_ += std::arg(trace); // final check if decomposition is still valid after specialization - assert((Eigen::kroneckerProduct(decomposition.k1l_, decomposition.k1r_) * + assert((kron(decomposition.k1l_, decomposition.k1r_) * decomposition.getCanonicalMatrix() * - Eigen::kroneckerProduct(decomposition.k2l_, decomposition.k2r_) * + kron(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) { +Matrix4x4 TwoQubitWeylDecomposition::getCanonicalMatrix(double a, double b, + double c) { // Canonical gate `U_d(a, b, c) = exp(i * (a*XX + b*YY + c*ZZ))`. XX/YY/ZZ // commute pairwise, so any product order is equivalent; the order below is // chosen to match common Qiskit/QuantumFlow references. The negated rotation // angles (`-2 * a`, ...) compensate for the `RXX/RYY/RZZ` convention - // `exp(-i * theta/2 * XX)` used in `getTwoQubitMatrix`, so that the - // factored angles sum back to the intended `+a`, `+b`, `+c`. - auto xx = getTwoQubitMatrix({ - .type = GateKind::RXX, - .parameter = {-2.0 * a}, - .qubitId = {0, 1}, - }); - auto yy = getTwoQubitMatrix({ - .type = GateKind::RYY, - .parameter = {-2.0 * b}, - .qubitId = {0, 1}, - }); - auto zz = getTwoQubitMatrix({ - .type = GateKind::RZZ, - .parameter = {-2.0 * c}, - .qubitId = {0, 1}, - }); + // `exp(-i * theta/2 * XX)`, so that the factored angles sum back to the + // intended `+a`, `+b`, `+c`. + const auto xx = rxxMatrix(-2.0 * a); + const auto yy = ryyMatrix(-2.0 * b); + const auto zz = rzzMatrix(-2.0 * c); return zz * yy * xx; } -Eigen::Matrix4cd -TwoQubitWeylDecomposition::magicBasisTransform(const Eigen::Matrix4cd& unitary, +Matrix4x4 +TwoQubitWeylDecomposition::magicBasisTransform(const Matrix4x4& unitary, MagicBasisTransform direction) { - using namespace std::complex_literals; // Makhlin "magic basis" transform. Conjugating a 2-qubit unitary by // `bNonNormalized` maps SU(2) x SU(2) factors onto SO(4) and diagonalizes // the canonical (Weyl) gate. The matrices are stored unnormalized: the // `1/2` pre-factor that would normally appear in `B^dagger` is absorbed // into `bNonNormalizedDagger` directly so the product `Bd * B == I` // without an extra scalar. - 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}, - {std::complex{0.0, -0.5}, 0, 0, std::complex{0.0, 0.5}}, - {0, std::complex{0.0, -0.5}, std::complex{0.0, -0.5}, 0}, - {0, 0.5, -0.5, 0}, - }; + const Matrix4x4 bNonNormalized = Matrix4x4::fromElements( // + 1, 1i, 0, 0, // + 0, 0, 1i, 1, // + 0, 0, 1i, -1, // + 1, -1i, 0, 0); + const Matrix4x4 bNonNormalizedDagger = Matrix4x4::fromElements( // + 0.5, 0, 0, 0.5, // + Complex{0.0, -0.5}, 0, 0, Complex{0.0, 0.5}, // + 0, Complex{0.0, -0.5}, Complex{0.0, -0.5}, 0, // + 0, 0.5, -0.5, 0); if (direction == MagicBasisTransform::OutOf) { return bNonNormalizedDagger * unitary * bNonNormalized; } @@ -335,9 +330,9 @@ double TwoQubitWeylDecomposition::closestPartialSwap(double a, double b, return m + (am * bm * cm * (6. + (ab * ab) + (bc * bc) + (ca * ca)) / 18.); } -std::pair -TwoQubitWeylDecomposition::diagonalizeComplexSymmetric( - const Eigen::Matrix4cd& m, double precision) { +std::pair> +TwoQubitWeylDecomposition::diagonalizeComplexSymmetric(const Matrix4x4& 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, @@ -352,6 +347,10 @@ TwoQubitWeylDecomposition::diagonalizeComplexSymmetric( auto state = std::mt19937{2023}; std::normal_distribution dist; + const auto mReal = m.realPart(); + const auto mImag = m.imagPart(); + + double bestErr = 1e300; constexpr auto maxDiagonalizationAttempts = 100; for (int i = 0; i < maxDiagonalizationAttempts; ++i) { double randA{}; @@ -368,12 +367,23 @@ TwoQubitWeylDecomposition::diagonalizeComplexSymmetric( 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(); + std::array m2Real{}; + for (std::size_t k = 0; k < m2Real.size(); ++k) { + m2Real[k] = (randA * mReal[k]) + (randB * mImag[k]); + } + const Matrix4x4 p = jacobiSymmetricEigen(m2Real).eigenvectors; + const std::array d = (p.transpose() * m * p).diagonal(); + + const auto compare = p * Matrix4x4::fromDiagonal(d) * p.transpose(); + { + double err = 0.0; + for (std::size_t r = 0; r < 4; ++r) { + for (std::size_t cc = 0; cc < 4; ++cc) { + err = std::max(err, std::abs(compare(r, cc) - m(r, cc))); + } + } + bestErr = std::min(bestErr, err); + } if (compare.isApprox(m, precision)) { // p are the eigenvectors which are decomposed into the // single-qubit gates surrounding the canonical gate @@ -382,56 +392,57 @@ TwoQubitWeylDecomposition::diagonalizeComplexSymmetric( // 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) < + assert(std::abs(Matrix4x4::fromDiagonal(d).determinant() - 1.0) < SANITY_CHECK_PRECISION); return std::make_pair(p, d); } } - llvm::reportFatalInternalError( - "TwoQubitWeylDecomposition: failed to diagonalize M2 (" + - llvm::Twine(maxDiagonalizationAttempts) + " iterations)."); + llvm::reportFatalInternalError(llvm::formatv( + "TwoQubitWeylDecomposition: failed to diagonalize M2 ({0} iterations). " + "best error = {1:e}, precision = {2:e}", + maxDiagonalizationAttempts, bestErr, precision)); } -std::tuple +std::tuple TwoQubitWeylDecomposition::decomposeTwoQubitProductGate( - const Eigen::Matrix4cd& specialUnitary) { + const Matrix4x4& 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)}}; + Matrix2x2 r = + Matrix2x2::fromElements(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)}}; + r = Matrix2x2::fromElements(specialUnitary(2, 0), specialUnitary(2, 1), + specialUnitary(3, 0), specialUnitary(3, 1)); detR = r.determinant(); } if (std::abs(detR) < 0.1) { llvm::reportFatalInternalError( "decomposeTwoQubitProductGate: unable to decompose: det_r < 0.1"); } - r /= std::sqrt(detR); + r *= (1.0 / std::sqrt(detR)); // transpose with complex conjugate of each element - const Eigen::Matrix2cd rTConj = r.transpose().conjugate(); + const Matrix2x2 rTConj = r.adjoint(); - Eigen::Matrix4cd temp = - Eigen::kroneckerProduct(Eigen::Matrix2cd::Identity(), rTConj); - temp = specialUnitary * temp; + Matrix4x4 temp = specialUnitary * kron(Matrix2x2::identity(), rTConj); // [[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)}}; + Matrix2x2 l = + Matrix2x2::fromElements(temp(0, 0), temp(0, 2), temp(2, 0), temp(2, 2)); auto detL = l.determinant(); if (std::abs(detL) < 0.9) { llvm::reportFatalInternalError( "decomposeTwoQubitProductGate: unable to decompose: detL < 0.9"); } - l /= std::sqrt(detL); + l *= (1.0 / std::sqrt(detL)); auto phase = std::arg(detL) / 2.; return {l, r, phase}; @@ -529,9 +540,9 @@ bool TwoQubitWeylDecomposition::applySpecialization() { c_ = 0.; // unmodified global phase k1l_ = k1l_ * k2l_; - k2l_ = Eigen::Matrix2cd::Identity(); + k2l_ = Matrix2x2::identity(); k1r_ = k1r_ * k2r_; - k2r_ = Eigen::Matrix2cd::Identity(); + k2r_ = Matrix2x2::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}` @@ -543,16 +554,16 @@ bool TwoQubitWeylDecomposition::applySpecialization() { // unmodified global phase k1l_ = k1l_ * k2r_; k1r_ = k1r_ * k2l_; - k2l_ = Eigen::Matrix2cd::Identity(); - k2r_ = Eigen::Matrix2cd::Identity(); + k2l_ = Matrix2x2::identity(); + k2r_ = Matrix2x2::identity(); } else { flippedFromOriginal = true; globalPhase_ += (std::numbers::pi / 2.0); - k1l_ = k1l_ * IPZ * k2r_; - k1r_ = k1r_ * IPZ * k2l_; - k2l_ = Eigen::Matrix2cd::Identity(); - k2r_ = Eigen::Matrix2cd::Identity(); + k1l_ = k1l_ * ipz() * k2r_; + k1r_ = k1r_ * ipz() * k2l_; + k2l_ = Matrix2x2::identity(); + k2r_ = Matrix2x2::identity(); } a_ = (std::numbers::pi / 4.0); b_ = (std::numbers::pi / 4.0); @@ -565,7 +576,7 @@ bool TwoQubitWeylDecomposition::applySpecialization() { // // :math:`K2_l = Id`. auto closest = closestPartialSwap(a_, b_, c_); - auto k2lDagger = k2l_.transpose().conjugate(); + auto k2lDagger = k2l_.adjoint(); a_ = closest; b_ = closest; @@ -574,7 +585,7 @@ bool TwoQubitWeylDecomposition::applySpecialization() { k1l_ = k1l_ * k2l_; k1r_ = k1r_ * k2l_; k2r_ = k2lDagger * k2r_; - k2l_ = Eigen::Matrix2cd::Identity(); + k2l_ = Matrix2x2::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` @@ -586,16 +597,16 @@ bool TwoQubitWeylDecomposition::applySpecialization() { // // :math:`K2_l = Id` auto closest = closestPartialSwap(a_, b_, -c_); - auto k2lDagger = k2l_.transpose().conjugate(); + auto k2lDagger = k2l_.adjoint(); 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(); + k1r_ = k1r_ * ipz() * k2l_ * ipz(); + k2r_ = ipz() * k2lDagger * ipz() * k2r_; + k2l_ = Matrix2x2::identity(); } else if (newSpecialization == Specialization::ControlledEquiv) { // :math:`U \sim U_d(\alpha, 0, 0)` // Thus, :math:`U \sim \text{Ctrl-U}` @@ -604,12 +615,11 @@ bool TwoQubitWeylDecomposition::applySpecialization() { // // :math:`K2_l = Ry(\theta_l) Rx(\lambda_l)` // :math:`K2_r = Ry(\theta_r) Rx(\lambda_r)` - auto eulerBasis = GateEulerBasis::XYX; - auto [k2ltheta, k2lphi, k2llambda, k2lphase] = - EulerDecomposition::anglesFromUnitary(k2l_, eulerBasis); - auto [k2rtheta, k2rphi, k2rlambda, k2rphase] = - EulerDecomposition::anglesFromUnitary(k2r_, eulerBasis); - + const EulerBasis eulerBasis = EulerBasis::XYX; + const auto [k2ltheta, k2lphi, k2llambda, k2lphase] = + anglesFromUnitary(k2l_, eulerBasis); + const auto [k2rtheta, k2rphi, k2rlambda, k2rphase] = + anglesFromUnitary(k2r_, EulerBasis::XYX); // unmodified parameter a b_ = 0.; c_ = 0.; @@ -618,7 +628,6 @@ bool TwoQubitWeylDecomposition::applySpecialization() { 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}` @@ -627,11 +636,10 @@ bool TwoQubitWeylDecomposition::applySpecialization() { // // :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_, GateEulerBasis::ZYZ); - auto [k2rtheta, k2rphi, k2rlambda, k2rphase] = - EulerDecomposition::anglesFromUnitary(k2r_, GateEulerBasis::ZYZ); - + const auto [k2ltheta, k2lphi, k2llambda, k2lphase] = + anglesFromUnitary(k2l_, EulerBasis::ZYZ); + const auto [k2rtheta, k2rphi, k2rlambda, k2rphase] = + anglesFromUnitary(k2r_, EulerBasis::ZYZ); a_ = (std::numbers::pi / 4.0); b_ = (std::numbers::pi / 4.0); // unmodified parameter c @@ -647,7 +655,7 @@ bool TwoQubitWeylDecomposition::applySpecialization() { // // :math:`K2_l = Ry(\theta_l) \cdot Rz(\lambda_l)`. auto [k2ltheta, k2lphi, k2llambda, k2lphase] = - EulerDecomposition::anglesFromUnitary(k2l_, GateEulerBasis::ZYZ); + anglesFromUnitary(k2l_, EulerBasis::ZYZ); auto ab = (a_ + b_) / 2.; a_ = ab; @@ -664,9 +672,9 @@ bool TwoQubitWeylDecomposition::applySpecialization() { // This gate binds 5 parameters, we make it canonical by setting: // // :math:`K2_l = Ry(\theta_l) \cdot Rx(\lambda_l)` - auto eulerBasis = GateEulerBasis::XYX; + auto eulerBasis = EulerBasis::XYX; auto [k2ltheta, k2lphi, k2llambda, k2lphase] = - EulerDecomposition::anglesFromUnitary(k2l_, eulerBasis); + anglesFromUnitary(k2l_, eulerBasis); auto bc = (b_ + c_) / 2.; // unmodified parameter a @@ -677,16 +685,15 @@ bool TwoQubitWeylDecomposition::applySpecialization() { 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 = GateEulerBasis::XYX; + auto eulerBasis = EulerBasis::XYX; auto [k2ltheta, k2lphi, k2llambda, k2lphase] = - EulerDecomposition::anglesFromUnitary(k2l_, eulerBasis); + anglesFromUnitary(k2l_, eulerBasis); auto bc = (b_ - c_) / 2.; // unmodified parameter a @@ -695,9 +702,8 @@ bool TwoQubitWeylDecomposition::applySpecialization() { 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; + k1r_ = k1r_ * ipz() * rxMatrix(k2lphi) * ipz(); + k2r_ = ipz() * rxMatrix(-k2lphi) * ipz() * k2r_; } else { llvm::reportFatalInternalError( "Unknown specialization for Weyl decomposition!"); diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.cpp index 09ca1f2713..74c1e271d2 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.cpp @@ -10,7 +10,7 @@ #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" #include @@ -20,7 +20,6 @@ #include #include -#include namespace mlir::qco::native_synth { @@ -62,32 +61,12 @@ parseGateSet(llvm::StringRef nativeGates) { return gates; } -/// Build a fully-resolved `SingleQubitEmitterSpec` for `mode`, including the -/// list of Euler bases the matrix-fallback path is allowed to use. +/// Build a fully-resolved `SingleQubitEmitterSpec` for `mode`. static SingleQubitEmitterSpec makeEmitterSpec(SingleQubitMode mode, AxisPair axisPair = AxisPair::RxRz, bool supportsDirectRx = false) { - llvm::SmallVector bases; - switch (mode) { - case SingleQubitMode::ZSXX: - bases = {decomposition::GateEulerBasis::ZSXX}; - break; - case SingleQubitMode::U3: - bases = {decomposition::GateEulerBasis::U3}; - break; - case SingleQubitMode::R: - // XYX decomposes any 1Q unitary into Rx-Ry-Rx chains, all of which the - // R emitter lowers back into the native R(theta, phi) gate. - bases = {decomposition::GateEulerBasis::XYX}; - break; - case SingleQubitMode::AxisPair: - bases = getEulerBasesForAxisPair(axisPair); - break; - } - return {.mode = mode, - .axisPair = axisPair, - .eulerBases = std::move(bases), - .supportsDirectRx = supportsDirectRx}; + return { + .mode = mode, .axisPair = axisPair, .supportsDirectRx = supportsDirectRx}; } /// Append a new emitter for `(mode, axisPair, supportsDirectRx)` to @@ -166,19 +145,37 @@ static void populateAllowedGates(NativeProfileSpec& spec) { } } -llvm::SmallVector -getEulerBasesForAxisPair(AxisPair axisPair) { +/// Euler basis reconstructing a two-axis single-qubit unitary for `axisPair`. +static decomposition::EulerBasis eulerBasisForAxisPair(AxisPair axisPair) { switch (axisPair) { case AxisPair::RxRz: - return {decomposition::GateEulerBasis::XZX}; + return decomposition::EulerBasis::XZX; case AxisPair::RxRy: - return {decomposition::GateEulerBasis::XYX}; + return decomposition::EulerBasis::XYX; case AxisPair::RyRz: - return {decomposition::GateEulerBasis::ZYZ}; + return decomposition::EulerBasis::ZYZ; } llvm_unreachable("unknown axis pair"); } +decomposition::EulerBasis +emitterEulerBasis(const SingleQubitEmitterSpec& emitter) { + switch (emitter.mode) { + case SingleQubitMode::ZSXX: + return decomposition::EulerBasis::ZSXX; + case SingleQubitMode::U3: + return decomposition::EulerBasis::U; + case SingleQubitMode::R: + // The R basis decomposes any 1Q unitary into an X-Y-X chain emitted + // directly as native R(theta, phi) gates (`Rx(a) == R(a, 0)`, + // `Ry(a) == R(a, pi/2)`). + return decomposition::EulerBasis::R; + case SingleQubitMode::AxisPair: + return eulerBasisForAxisPair(emitter.axisPair); + } + llvm_unreachable("unknown single-qubit mode"); +} + std::optional resolveNativeGatesSpec(llvm::StringRef nativeGates) { const auto gates = parseGateSet(nativeGates); diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp index 6dea6e05bb..c380b3becc 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp @@ -11,11 +11,10 @@ #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Scoring.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" @@ -24,13 +23,9 @@ #include "mlir/Dialect/QCO/Utils/Matrix.h" #include "mlir/Dialect/Utils/Utils.h" -#include -#include -#include #include #include #include -#include #include #include #include @@ -41,13 +36,9 @@ #include #include -#include -#include #include -#include #include #include -#include namespace mlir::qco { #define GEN_PASS_DEF_NATIVEGATESYNTHESISPASS @@ -57,37 +48,28 @@ namespace mlir::qco { namespace mlir::qco { using native_synth::allowsSingleQubitOp; -using native_synth::areValidScoreWeights; -using native_synth::CandidateClass; -using native_synth::collectSingleQubitCandidates; -using native_synth::collectTwoQubitBasisCandidates; -using native_synth::collectTwoQubitBasisCandidatesFromMatrix; +using native_synth::canDirectlyDecomposeToAxisPair; +using native_synth::canDirectlyDecomposeToR; +using native_synth::canDirectlyDecomposeToU3; +using native_synth::canDirectlyDecomposeToZSXX; using native_synth::collectUnitaryOpsInPreOrder; using native_synth::decomposeToAxisPair; using native_synth::decomposeToR; using native_synth::decomposeToU3; using native_synth::decomposeToZSXX; -using native_synth::emitSynthesizedSingleQubitFromMatrix; -using native_synth::emitTwoQubitGateSequence; -using native_synth::eulerSequenceForMatrixSynthesis; +using native_synth::emitSingleQubitMatrix; +using native_synth::emitterEulerBasis; +using native_synth::emitTwoQubitNative; using native_synth::getBlockTwoQubitMatrix; using native_synth::NativeGateKind; using native_synth::NativeProfileSpec; using native_synth::resolveNativeGatesSpec; using native_synth::rewriteXXPlusMinusYYViaRzz; -using native_synth::ScoreWeights; -using native_synth::selectBestCandidate; using native_synth::SingleQubitEmitterSpec; using native_synth::SingleQubitMode; -using native_synth::SingleQubitRewritePlan; -using native_synth::SingleQubitRewriteStrategy; -using native_synth::SynthesisCandidate; -using native_synth::toEigen; -using native_synth::TwoQubitRewritePlan; using native_synth::TwoQubitWindowConsolidator; using native_synth::usesCxEntangler; using native_synth::usesCzEntangler; -using native_synth::xxPlusMinusYyRzzRewriteScoringMetrics; namespace { @@ -98,8 +80,11 @@ struct OneQubitRun { } // namespace -/// If profitable, replace the run with one synthesized single-qubit op. +/// If profitable, replace the run with one synthesized single-qubit op in +/// `basis` (mirrors `FuseSingleQubitUnitaryRuns`). Fuses when any op is +/// off-menu or when Euler resynthesis strictly shortens the run. static bool maybeFuseRun(IRRewriter& rewriter, OneQubitRun& run, + const decomposition::EulerBasis basis, const NativeProfileSpec& spec) { Matrix2x2 fused = Matrix2x2::identity(); for (UnitaryOpInterface u : run.ops) { @@ -109,67 +94,25 @@ static bool maybeFuseRun(IRRewriter& rewriter, OneQubitRun& run, } fused.premultiplyBy(m); } - const Eigen::Matrix2cd fusedEigen = toEigen(fused); const bool anyNonNative = llvm::any_of(run.ops, [&](UnitaryOpInterface u) { return !allowsSingleQubitOp(u, spec); }); - assert(!spec.singleQubitEmitters.empty() && "expected at least one emitter"); - - constexpr auto kInvalidLen = std::numeric_limits::max(); - const SingleQubitEmitterSpec* bestEmitter = nullptr; - std::size_t bestLen = kInvalidLen; - std::optional bestEuler; - for (const auto& emitter : spec.singleQubitEmitters) { - std::size_t len = 0; - std::optional euler; - if (emitter.mode == SingleQubitMode::U3) { - len = 1; - } else { - euler = eulerSequenceForMatrixSynthesis(fusedEigen, emitter); - if (!euler) { - continue; - } - len = euler->gates.size(); - } - if (bestEmitter == nullptr || len < bestLen) { - bestLen = len; - bestEmitter = &emitter; - bestEuler = std::move(euler); - } - } - if (bestEmitter == nullptr) { - return false; - } - - // Fully native runs: fuse only if some emitter strictly shortens the chain. - if (!anyNonNative && bestLen >= run.ops.size()) { - return false; - } - Operation* firstOp = run.ops.front().getOperation(); const Value inQubit = run.ops.front().getInputQubit(0); const Value outQubit = run.ops.back().getOutputQubit(0); rewriter.setInsertionPoint(firstOp); - Value replacement; - if (bestEmitter->mode == SingleQubitMode::U3) { - replacement = emitSynthesizedSingleQubitFromMatrix( - rewriter, firstOp->getLoc(), inQubit, fusedEigen, *bestEmitter); - } else { - assert(bestEuler.has_value()); - replacement = emitSynthesizedSingleQubitFromMatrix( - rewriter, firstOp->getLoc(), inQubit, fusedEigen, *bestEmitter, - &*bestEuler); - } + const auto replacement = decomposition::synthesizeUnitary1QEuler( + rewriter, firstOp->getLoc(), inQubit, fused, run.ops.size(), anyNonNative, + basis); if (!replacement) { return false; } - rewriter.replaceAllUsesWith(outQubit, replacement); + rewriter.replaceAllUsesWith(outQubit, *replacement); for (auto& op : llvm::reverse(run.ops)) { - Operation* toErase = op.getOperation(); - rewriter.eraseOp(toErase); + rewriter.eraseOp(op.getOperation()); } return true; } @@ -205,6 +148,23 @@ static UnitaryOpInterface fusibleSingleQubitOp(Operation* op) { return unitary; } +/// Whether `emitter` can lower the single-qubit `op` directly (used for ops +/// with non-constant angles, which have no constant `2×2` matrix). +static bool emitterHasDirectLowering(Operation* op, + const SingleQubitEmitterSpec& emitter) { + switch (emitter.mode) { + case SingleQubitMode::ZSXX: + return canDirectlyDecomposeToZSXX(op, emitter.supportsDirectRx); + case SingleQubitMode::U3: + return canDirectlyDecomposeToU3(op); + case SingleQubitMode::R: + return canDirectlyDecomposeToR(op); + case SingleQubitMode::AxisPair: + return canDirectlyDecomposeToAxisPair(op, emitter.axisPair); + } + return false; +} + /// Dispatch `op`'s direct (non-matrix) single-qubit lowering to the /// `decomposeTo*` helper for `emitter.mode`. Returns the output qubit value /// or a null `Value` if no direct rule applies for this op. @@ -226,9 +186,11 @@ applyDirectSingleQubitLowering(IRRewriter& rewriter, Operation* op, Value in, namespace { -/// Lowers unitary QCO ops to a comma-separated native gate menu (single-qubit -/// fuse, two-qubit windows, synthesis sweeps, seam single-qubit fuse, `rz` -/// through `ctrl` controls, another single-qubit fuse, optional cleanup sweeps. +/// Lowers unitary QCO ops to a comma-separated native gate menu using a +/// deterministic, matrix-driven synthesizer: single-qubit fuse, two-qubit +/// window consolidation, synthesis sweeps, seam single-qubit fuse, `rz` +/// through `ctrl` controls, another single-qubit fuse, optional cleanup +/// sweeps. struct NativeGateSynthesisPass : impl::NativeGateSynthesisPassBase { /// Default-construct the pass with the TableGen-generated option defaults. @@ -244,30 +206,15 @@ struct NativeGateSynthesisPass /// used by pipeline code that cannot include the TableGen-generated header. explicit NativeGateSynthesisPass(const NativeGateSynthesisOptions& options) { nativeGates = options.nativeGates; - scoreWeightTwoQ = options.scoreWeightTwoQ; - scoreWeightOneQ = options.scoreWeightOneQ; - scoreWeightDepth = options.scoreWeightDepth; } protected: - /// Top-level pass entry point. Validates the score weights and native-gate - /// menu, then drives the staged rewrite pipeline: one-qubit run fusion, - /// two-qubit window consolidation, synthesis sweeps until the single-qubit - /// surface is native, seam cleanup, `rz`-through-`ctrl` folding, and a - /// final fusion pass. Fails the pass on invalid input or non-convergence. + /// Top-level pass entry point. Resolves the native-gate menu, then drives + /// the staged rewrite pipeline: one-qubit run fusion, two-qubit window + /// consolidation, synthesis sweeps until the single-qubit surface is native, + /// seam cleanup, `rz`-through-`ctrl` folding, and a final fusion pass. Fails + /// the pass on invalid input or non-convergence. void runOnOperation() override { - const ScoreWeights weights{.twoQ = scoreWeightTwoQ, - .oneQ = scoreWeightOneQ, - .depth = scoreWeightDepth}; - if (!areValidScoreWeights(weights)) { - getOperation().emitError() - << "invalid native synthesis score weights (twoq=" << scoreWeightTwoQ - << ", oneq=" << scoreWeightOneQ << ", depth=" << scoreWeightDepth - << ")"; - signalPassFailure(); - return; - } - // Empty native-gates string: no-op. if (llvm::StringRef(nativeGates).trim().empty()) { return; @@ -281,11 +228,15 @@ struct NativeGateSynthesisPass return; } const auto& spec = *specOpt; + // Deterministic single-qubit basis: the first emitter drives all matrix + // synthesis and run fusion. + const decomposition::EulerBasis oneQubitBasis = + emitterEulerBasis(spec.singleQubitEmitters.front()); IRRewriter rewriter(&getContext()); - fuseOneQubitRuns(rewriter, spec); - if (failed(consolidateTwoQubitBlocks(rewriter, spec, weights))) { + fuseOneQubitRuns(rewriter, spec, oneQubitBasis); + if (failed(consolidateTwoQubitBlocks(rewriter, spec))) { signalPassFailure(); return; } @@ -293,7 +244,7 @@ struct NativeGateSynthesisPass // repeat until clean or hit the sweep cap before seam / `rz` cleanup. constexpr unsigned kMaxSynthesisSweeps = 4; for (unsigned i = 0; i < kMaxSynthesisSweeps; ++i) { - if (failed(synthesizeRemainingOps(rewriter, spec, weights))) { + if (failed(synthesizeRemainingOps(rewriter, spec, oneQubitBasis))) { signalPassFailure(); return; } @@ -310,19 +261,19 @@ struct NativeGateSynthesisPass return; } // Fuse single-qubit seams between two-qubit blocks. - fuseOneQubitRuns(rewriter, spec); + fuseOneQubitRuns(rewriter, spec, oneQubitBasis); // Fuse `rz` through control wires of `ctrl` (diagonal control phase). fuseRzAcrossCtrlControls(rewriter); - fuseOneQubitRuns(rewriter, spec); + fuseOneQubitRuns(rewriter, spec, oneQubitBasis); // Re-check full menu (single-qubit ops, native `ctrl`, allowed bare `rzz`). constexpr unsigned kPostMenuCleanupSweeps = 4; unsigned postMenuSweepsRemaining = kPostMenuCleanupSweeps; while (hasNonNativeMenuOps(spec) && postMenuSweepsRemaining-- > 0) { - if (failed(synthesizeRemainingOps(rewriter, spec, weights))) { + if (failed(synthesizeRemainingOps(rewriter, spec, oneQubitBasis))) { signalPassFailure(); return; } - fuseOneQubitRuns(rewriter, spec); + fuseOneQubitRuns(rewriter, spec, oneQubitBasis); } if (hasNonNativeMenuOps(spec)) { getOperation().emitError() @@ -419,7 +370,8 @@ struct NativeGateSynthesisPass private: /// Fuse adjacent single-qubit runs when the emitter wins on length or any op /// is off-menu. - void fuseOneQubitRuns(IRRewriter& rewriter, const NativeProfileSpec& spec) { + void fuseOneQubitRuns(IRRewriter& rewriter, const NativeProfileSpec& spec, + const decomposition::EulerBasis basis) { llvm::SmallVector runs; llvm::DenseMap tailOpToRun; @@ -454,7 +406,7 @@ struct NativeGateSynthesisPass if (run.ops.size() < 2) { continue; } - (void)maybeFuseRun(rewriter, run, spec); + (void)maybeFuseRun(rewriter, run, basis, spec); } } @@ -537,31 +489,14 @@ struct NativeGateSynthesisPass /// Two-qubit windows with absorbed single-qubit ops: replace when a cheaper /// native sequence exists. LogicalResult consolidateTwoQubitBlocks(IRRewriter& rewriter, - const NativeProfileSpec& spec, - const ScoreWeights& weights) { + const NativeProfileSpec& spec) { llvm::SmallVector ops; collectUnitaryOpsInPreOrder(getOperation(), ops); TwoQubitWindowConsolidator consolidator; for (Operation* op : ops) { consolidator.process(op, spec); } - return consolidator.materialize(rewriter, spec, weights); - } - - /// Lower one single-qubit rewrite plan; null `Value` on failure. - static Value emitSingleQCandidate(IRRewriter& rewriter, Operation* op, - UnitaryOpInterface unitary, - const SingleQubitRewritePlan& plan) { - const Value in = unitary.getInputQubit(0); - if (plan.strategy == SingleQubitRewriteStrategy::Direct) { - return applyDirectSingleQubitLowering(rewriter, op, in, plan.emitter); - } - Matrix2x2 matrix; - if (!unitary.isSingleQubit() || !unitary.getUnitaryMatrix2x2(matrix)) { - return {}; - } - return emitSynthesizedSingleQubitFromMatrix(rewriter, op->getLoc(), in, - toEigen(matrix), plan.emitter); + return consolidator.materialize(rewriter, spec); } /// One synthesis sweep over the whole function: rewrite every remaining @@ -571,7 +506,7 @@ struct NativeGateSynthesisPass /// `runOnOperation` iterates until convergence. LogicalResult synthesizeRemainingOps(IRRewriter& rewriter, const NativeProfileSpec& spec, - const ScoreWeights& weights) { + const decomposition::EulerBasis basis) { llvm::SmallVector ops; collectUnitaryOpsInPreOrder(getOperation(), ops); llvm::DenseSet erasedOps; @@ -597,8 +532,7 @@ struct NativeGateSynthesisPass if (unitary.isSingleQubit()) { if (!allowsSingleQubitOp(unitary, spec)) { - if (failed( - rewriteSingleQubit(rewriter, op, unitary, spec, weights))) { + if (failed(rewriteSingleQubit(rewriter, op, unitary, spec, basis))) { return failure(); } erasedOps.insert(op); @@ -608,7 +542,7 @@ struct NativeGateSynthesisPass if (auto ctrl = llvm::dyn_cast(op)) { const bool wasAlreadyNative = ctrlMatchesNativeMenu(ctrl, spec); - if (failed(rewriteControlled(rewriter, ctrl, spec, weights))) { + if (failed(rewriteControlled(rewriter, ctrl, spec))) { return failure(); } if (!wasAlreadyNative) { @@ -618,7 +552,7 @@ struct NativeGateSynthesisPass } if (unitary.isTwoQubit()) { - if (failed(rewriteTwoQubit(rewriter, op, unitary, spec, weights))) { + if (failed(rewriteTwoQubit(rewriter, op, unitary, spec))) { return failure(); } erasedOps.insert(op); @@ -628,35 +562,43 @@ struct NativeGateSynthesisPass return success(); } - /// Lower one off-menu single-qubit `op`: enumerate all valid rewrite - /// candidates for the active native profile, pick the best by `weights`, - /// emit it, and replace `op`. - static LogicalResult rewriteSingleQubit(IRRewriter& rewriter, Operation* op, - UnitaryOpInterface unitary, - const NativeProfileSpec& spec, - const ScoreWeights& weights) { + /// Lower one off-menu single-qubit `op`. Constant unitaries use the + /// matrix-driven Euler synthesizer in `basis`; ops with non-constant angles + /// fall back to the symbolic `decomposeTo*` lowering of the first emitter + /// that handles them. + static LogicalResult + rewriteSingleQubit(IRRewriter& rewriter, Operation* op, + UnitaryOpInterface unitary, const NativeProfileSpec& spec, + const decomposition::EulerBasis basis) { rewriter.setInsertionPoint(op); - const auto candidates = collectSingleQubitCandidates(unitary, spec); - const auto* best = selectBestCandidate(llvm::ArrayRef(candidates), weights); - const Value replaced = - best != nullptr - ? emitSingleQCandidate(rewriter, op, unitary, best->payload) - : Value{}; - if (!replaced) { - op->emitError("single-qubit operation not in selected native profile"); - return failure(); + const Value in = unitary.getInputQubit(0); + Matrix2x2 matrix; + if (unitary.isSingleQubit() && unitary.getUnitaryMatrix2x2(matrix)) { + const Value replaced = + emitSingleQubitMatrix(rewriter, op->getLoc(), in, matrix, basis); + rewriter.replaceOp(op, replaced); + return success(); } - rewriter.replaceOp(op, replaced); - return success(); + for (const auto& emitter : spec.singleQubitEmitters) { + if (!emitterHasDirectLowering(op, emitter)) { + continue; + } + if (const Value replaced = + applyDirectSingleQubitLowering(rewriter, op, in, emitter)) { + rewriter.replaceOp(op, replaced); + return success(); + } + } + op->emitError("single-qubit operation not in selected native profile"); + return failure(); } /// Lower a single-control, single-target `CtrlOp` to the native profile. /// Fast-path: already-native `CX`/`CZ` are kept as-is. Otherwise, lift the - /// controlled op to its 4x4 matrix (with SU(4) normalization), run the - /// Weyl-based basis-decomposer search, and emit the best candidate. + /// controlled op to its 4x4 matrix and run the deterministic two-qubit + /// synthesizer. static LogicalResult rewriteControlled(IRRewriter& rewriter, CtrlOp ctrl, - const NativeProfileSpec& spec, - const ScoreWeights& weights) { + const NativeProfileSpec& spec) { if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { ctrl.emitError("native synthesis currently only supports 1-control " "1-target controlled gates"); @@ -668,8 +610,7 @@ struct NativeGateSynthesisPass if ((usesCxEntangler(spec) && hasCX) || (usesCzEntangler(spec) && hasCZ)) { return success(); } - // Otherwise treat as a generic `4×4` (Weyl + basis decomposer + scorer). - Eigen::Matrix4cd matrix; + Matrix4x4 matrix; if (hasCX || hasCZ) { if (!getBlockTwoQubitMatrix(ctrl.getOperation(), matrix)) { ctrl.emitError("failed to compute 4x4 matrix for CtrlOp"); @@ -677,126 +618,62 @@ struct NativeGateSynthesisPass } } else { auto u = llvm::cast(ctrl.getOperation()); - Matrix4x4 raw; - if (!u.isTwoQubit() || !u.getUnitaryMatrix4x4(raw)) { + if (!u.isTwoQubit() || !u.getUnitaryMatrix4x4(matrix)) { ctrl.emitError( "native synthesis: cannot build a constant 4x4 matrix for this " "controlled gate (unsupported body or non-constant parameters)"); return failure(); } - matrix = toEigen(raw); - } - native_synth::normalizeToSU4(matrix); // SU(4) convention for Weyl - - const auto candidates = - collectTwoQubitBasisCandidatesFromMatrix(matrix, spec); - const auto* best = selectBestCandidate(llvm::ArrayRef(candidates), weights); - if (best == nullptr) { - ctrl.emitError("controlled gate not allowed by selected profile"); - return failure(); - } - if (!best->payload.sequence) { - ctrl.emitError("internal error: missing two-qubit rewrite sequence"); - return failure(); } rewriter.setInsertionPoint(ctrl); - if (failed(emitTwoQubitGateSequence( - rewriter, ctrl.getOperation(), ctrl.getInputControl(0), - ctrl.getInputTarget(0), *best->payload.sequence))) { - ctrl.emitError( - "failed to emit two-qubit gate sequence for selected candidate"); + Value out0; + Value out1; + if (failed(emitTwoQubitNative( + rewriter, ctrl.getLoc(), ctrl.getInputControl(0), + ctrl.getInputTarget(0), matrix, spec, out0, out1))) { + ctrl.emitError("controlled gate not allowed by selected profile"); return failure(); } + rewriter.replaceOp(ctrl, ValueRange{out0, out1}); return success(); } /// Lower an off-menu generic two-qubit op (`RZZ`, `XXPlusYY`, `XXMinusYY`, /// or any arbitrary 4x4 unitary). Handles the `Rzz`-native fast path; for - /// `XXPlusYY` / `XXMinusYY` with `rzz` on the menu, scores the dedicated - /// `XX±YY -> Rzz` rewrite against Weyl basis candidates and picks the - /// cheaper option under `weights`. + /// `XXPlusYY` / `XXMinusYY` with `rzz` on the menu, uses the dedicated + /// `XX±YY -> Rzz` rewrite. All other two-qubit unitaries go through the + /// deterministic KAK synthesizer. static LogicalResult rewriteTwoQubit(IRRewriter& rewriter, Operation* op, UnitaryOpInterface unitary, - const NativeProfileSpec& spec, - const ScoreWeights& weights) { + const NativeProfileSpec& spec) { if (spec.allowRzz && llvm::isa(op)) { return success(); } if (spec.allowRzz && (llvm::isa(op) || llvm::isa(op))) { - SmallVector>, 0> - combined; - unsigned nextIndex = 0; - combined.push_back(SynthesisCandidate>{ - .candidateClass = CandidateClass::XxPlusMinusViaRzz, - .metrics = xxPlusMinusYyRzzRewriteScoringMetrics(), - .enumerationIndex = nextIndex++, - .payload = std::nullopt, - }); - if (!spec.entanglerBases.empty()) { - for (const auto& cand : collectTwoQubitBasisCandidates(unitary, spec)) { - combined.push_back( - SynthesisCandidate>{ - .candidateClass = cand.candidateClass, - .metrics = cand.metrics, - .enumerationIndex = nextIndex++, - .payload = cand.payload, - }); - } - } - if (const auto* best = - selectBestCandidate(llvm::ArrayRef(combined), weights)) { - rewriter.setInsertionPoint(op); - if (best->candidateClass == CandidateClass::XxPlusMinusViaRzz) { - if (succeeded(rewriteXXPlusMinusYYViaRzz(rewriter, op))) { - return success(); - } - if (!spec.entanglerBases.empty()) { - const auto basisCandidates = - collectTwoQubitBasisCandidates(unitary, spec); - if (const auto* basisBest = selectBestCandidate( - llvm::ArrayRef(basisCandidates), weights)) { - if (!basisBest->payload.sequence) { - return failure(); - } - if (succeeded(emitTwoQubitGateSequence( - rewriter, op, unitary.getInputQubit(0), - unitary.getInputQubit(1), - *basisBest->payload.sequence))) { - return success(); - } - } - } - return failure(); - } - if (best->payload.has_value() && best->payload->sequence && - succeeded(emitTwoQubitGateSequence( - rewriter, op, unitary.getInputQubit(0), - unitary.getInputQubit(1), *best->payload->sequence))) { - return success(); - } + rewriter.setInsertionPoint(op); + if (succeeded(rewriteXXPlusMinusYYViaRzz(rewriter, op))) { + return success(); } + // Fall through to entangler-based synthesis when the dedicated rewrite + // could not be applied (e.g. no entangler-free realization). + } + Matrix4x4 matrix; + if (!getBlockTwoQubitMatrix(op, matrix)) { op->emitError("unsupported two-qubit operation for selected profile"); return failure(); } - if (!spec.entanglerBases.empty()) { - const auto candidates = collectTwoQubitBasisCandidates(unitary, spec); - if (const auto* best = - selectBestCandidate(llvm::ArrayRef(candidates), weights)) { - if (!best->payload.sequence) { - op->emitError("internal error: missing two-qubit rewrite sequence"); - return failure(); - } - rewriter.setInsertionPoint(op); - if (succeeded(emitTwoQubitGateSequence( - rewriter, op, unitary.getInputQubit(0), - unitary.getInputQubit(1), *best->payload.sequence))) { - return success(); - } - } + rewriter.setInsertionPoint(op); + Value out0; + Value out1; + if (failed(emitTwoQubitNative( + rewriter, op->getLoc(), unitary.getInputQubit(0), + unitary.getInputQubit(1), matrix, spec, out0, out1))) { + op->emitError("unsupported two-qubit operation for selected profile"); + return failure(); } - op->emitError("unsupported two-qubit operation for selected profile"); - return failure(); + rewriter.replaceOp(op, ValueRange{out0, out1}); + return success(); } }; diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp index c115f47ae6..d5f5c765a2 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp @@ -15,12 +15,11 @@ #include "mlir/Dialect/QCO/Transforms/Decomposition/Gate.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Scoring.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" -#include #include #include #include @@ -31,8 +30,8 @@ #include #include +#include #include -#include namespace mlir::qco::native_synth { @@ -57,32 +56,28 @@ static bool isNativeTwoQubitOp(Operation* op, const NativeProfileSpec& spec) { return spec.allowRzz && llvm::isa(op); } -/// Decide whether replacing a consolidated window with the candidate -/// described by `best` is worthwhile. Always replace a window that contains -/// any non-native op (we have to lower them anyway); otherwise only replace -/// when the candidate has strictly fewer two-qubit gates, or the same number -/// with strictly fewer one-qubit gates. +/// Decide whether replacing a consolidated window is worthwhile. Always +/// replace a window that contains any non-native op (we have to lower them +/// anyway); otherwise only replace when the deterministic synthesizer uses +/// strictly fewer entanglers than the window already contains. (Leftover +/// single-qubit gates are cleaned up by the surrounding fuse passes, so the +/// 1q count is not part of the decision.) static bool shouldApplyBlockReplacement(const TwoQubitBlock& block, - const CandidateMetrics& best) { + std::uint8_t numBasisUses) { if (block.anyNonNative) { return true; } - const bool shorterTwoQ = best.numTwoQ < block.numTwoQ; - const bool sameTwoQ = best.numTwoQ == block.numTwoQ; - const bool shorterOneQ = best.numOneQ < block.numOneQ; - return shorterTwoQ || (sameTwoQ && shorterOneQ); + return numBasisUses < block.numTwoQ; } -/// Emit the chosen synthesis sequence `best` at the location of the window's -/// first op, rewire the block's trailing SSA values (`wireA`, `wireB`) to -/// the newly emitted outputs, and erase the replaced ops in reverse order -/// so def-use edges are cleared before their defining ops disappear. -static LogicalResult materializeSingleTwoQubitBlock( - IRRewriter& rewriter, const TwoQubitBlock& block, - const SynthesisCandidate& best) { - if (!best.payload.sequence) { - return failure(); - } +/// Emit the deterministic native synthesis of `block.accum` at the location of +/// the window's first op, rewire the block's trailing SSA values (`wireA`, +/// `wireB`) to the newly emitted outputs, and erase the replaced ops in +/// reverse order so def-use edges are cleared before their defining ops +/// disappear. +static LogicalResult +materializeSingleTwoQubitBlock(IRRewriter& rewriter, const TwoQubitBlock& block, + const NativeProfileSpec& spec) { Operation* firstOp = block.ops.front(); auto firstUnitary = llvm::cast(firstOp); const Value inA = firstUnitary.getInputQubit(0); @@ -93,16 +88,11 @@ static LogicalResult materializeSingleTwoQubitBlock( rewriter.setInsertionPoint(firstOp); Value newA; Value newB; - if (failed(emitTwoQubitGateSequenceAtLoc(rewriter, firstOp->getLoc(), inA, - inB, *best.payload.sequence, newA, - newB))) { + if (failed(emitTwoQubitNative(rewriter, firstOp->getLoc(), inA, inB, + block.accum, spec, newA, newB))) { firstOp->emitError("failed to emit synthesized two-qubit gate sequence"); return failure(); } - if (best.payload.sequence->hasGlobalPhase()) { - emitGPhaseIfNonTrivial(rewriter, firstOp->getLoc(), - best.payload.sequence->globalPhase); - } rewriter.replaceAllUsesWith(outA, newA); rewriter.replaceAllUsesWith(outB, newB); for (auto* toErase : llvm::reverse(block.ops)) { @@ -194,7 +184,7 @@ void TwoQubitWindowConsolidator::process(Operation* op, if (unitary.isTwoQubit()) { // A two-qubit op for which we cannot build a 4x4 matrix is opaque to the // window model; close any blocks on its inputs and bail out. - Eigen::Matrix4cd opMatrix; + Matrix4x4 opMatrix; if (!getBlockTwoQubitMatrix(op, opMatrix)) { closeBlockOnWire(unitary.getInputQubit(0)); closeBlockOnWire(unitary.getInputQubit(1)); @@ -324,10 +314,9 @@ void TwoQubitWindowConsolidator::process(Operation* op, closeBlock(idx); return; } - const Eigen::Matrix2cd m = toEigen(raw); const auto pad = (v == block.wireA) - ? decomposition::expandToTwoQubits(m, 0) - : decomposition::expandToTwoQubits(m, 1); + ? decomposition::expandToTwoQubits(raw, 0) + : decomposition::expandToTwoQubits(raw, 1); block.accum = pad * block.accum; block.ops.push_back(op); ++block.numOneQ; @@ -355,8 +344,7 @@ void TwoQubitWindowConsolidator::process(Operation* op, LogicalResult TwoQubitWindowConsolidator::materialize(IRRewriter& rewriter, - const NativeProfileSpec& spec, - const ScoreWeights& weights) { + const NativeProfileSpec& spec) { llvm::DenseSet erasedOps; for (const auto& block : blocks) { if (block.ops.size() < 2) { @@ -369,18 +357,16 @@ TwoQubitWindowConsolidator::materialize(IRRewriter& rewriter, [&](Operation* op) { return erasedOps.contains(op); })) { continue; } - // Leave `block.accum` unnormalized: Weyl keeps stripped SU(4) phase in - // the candidate sequence's `globalPhase`. - const auto candidates = - collectTwoQubitBasisCandidatesFromMatrix(block.accum, spec); - const auto* best = selectBestCandidate(llvm::ArrayRef(candidates), weights); - if (best == nullptr) { + // Leave `block.accum` unnormalized: Weyl folds the stripped global phase + // into the synthesized `gphase`. + const auto numBasisUses = twoQubitEntanglerCount(block.accum, spec); + if (!numBasisUses) { continue; } - if (!shouldApplyBlockReplacement(block, best->metrics)) { + if (!shouldApplyBlockReplacement(block, *numBasisUses)) { continue; } - if (failed(materializeSingleTwoQubitBlock(rewriter, block, *best))) { + if (failed(materializeSingleTwoQubitBlock(rewriter, block, spec))) { return failure(); } for (Operation* op : block.ops) { diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Policy.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Policy.cpp index 5f077bb10d..cc58c207e5 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Policy.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Policy.cpp @@ -12,34 +12,17 @@ #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h" -#include "mlir/Dialect/QCO/Utils/Matrix.h" #include -#include #include #include #include -#include -#include -#include #include namespace mlir::qco::native_synth { -bool areValidScoreWeights(const ScoreWeights& weights) { - return std::isfinite(weights.twoQ) && std::isfinite(weights.oneQ) && - std::isfinite(weights.depth) && weights.twoQ >= 0.0 && - weights.oneQ >= 0.0 && weights.depth >= 0.0; -} - bool usesCxEntangler(const NativeProfileSpec& spec) { return llvm::is_contained(spec.entanglerBases, EntanglerBasis::Cx); } @@ -86,42 +69,6 @@ bool allowsSingleQubitOp(UnitaryOpInterface op, const NativeProfileSpec& spec) { return gate && spec.allowedGates.contains(*gate); } -CandidateMetrics -computeGateSequenceMetrics(const decomposition::QubitGateSequence& seq) { - CandidateMetrics metrics; - // Per-qubit depth counters used as a mini scheduler: single-qubit gates - // advance only their own wire's counter, while two-qubit gates act as a - // *sync barrier* and advance both wires to `1 + max(...)`. This mirrors a - // simple ASAP scheduling model where entangling gates force alignment of - // the two wires they touch. - llvm::SmallVector qubitDepths(2, 0); - for (const auto& gate : seq.gates) { - if (gate.qubitId.size() == 2) { - ++metrics.numTwoQ; - const unsigned q0 = gate.qubitId[0]; - const unsigned q1 = gate.qubitId[1]; - const unsigned neededSize = std::max(q0, q1) + 1; - if (neededSize > qubitDepths.size()) { - qubitDepths.resize(neededSize, 0); - } - const auto gateDepth = std::max(qubitDepths[q0], qubitDepths[q1]) + 1; - qubitDepths[q0] = gateDepth; - qubitDepths[q1] = gateDepth; - metrics.depth = std::max(metrics.depth, gateDepth); - } else if (gate.qubitId.size() == 1) { - ++metrics.numOneQ; - const unsigned q = gate.qubitId[0]; - if (q >= qubitDepths.size()) { - qubitDepths.resize(q + 1, 0); - } - const auto gateDepth = qubitDepths[q] + 1; - qubitDepths[q] = gateDepth; - metrics.depth = std::max(metrics.depth, gateDepth); - } - } - return metrics; -} - /// True when `decomposeTo*` should run instead of folding to a constant `2×2` /// matrix: trivial `Id`/`P`, dynamic-angle ops the matrix path cannot close /// over, and (for ZSXX with direct Rx) `Rx`/`Ry`/`R`. Static angles still use @@ -158,66 +105,4 @@ bool canDirectlyDecomposeToAxisPair(Operation* op, AxisPair axisPair) { llvm_unreachable("unknown axis pair"); } -CandidateMetrics -estimateDirectSingleQubitMetrics(Operation* op, - const SingleQubitEmitterSpec& emitter) { - if (llvm::isa(op)) { - return {}; - } - // ZSXX + direct Rx: `ry`/`r` use a three-gate `rz * rx * rz` sandwich; other - // direct paths emit a single native op. - const bool threeGate = emitter.mode == SingleQubitMode::ZSXX && - emitter.supportsDirectRx && llvm::isa(op); - const unsigned count = threeGate ? 3U : 1U; - return {.numOneQ = count, .numTwoQ = 0, .depth = count}; -} - -std::optional -estimateMatrixSingleQubitMetrics(UnitaryOpInterface unitary, - const SingleQubitEmitterSpec& emitter) { - if (!unitary.isSingleQubit()) { - return std::nullopt; - } - Matrix2x2 raw; - if (!unitary.getUnitaryMatrix2x2(raw)) { - return std::nullopt; - } - const Eigen::Matrix2cd matrix = toEigen(raw); - - const auto countNonIdentity = - [](const decomposition::QubitGateSequence& seq) { - CandidateMetrics metrics; - for (const auto& gate : seq.gates) { - if (gate.type != decomposition::GateKind::I) { - ++metrics.numOneQ; - } - } - metrics.depth = metrics.numOneQ; - return metrics; - }; - - switch (emitter.mode) { - case SingleQubitMode::ZSXX: - return computeGateSequenceMetrics( - decomposition::EulerDecomposition::generateCircuit( - decomposition::GateEulerBasis::ZSXX, matrix, /*simplify=*/true, - std::nullopt)); - case SingleQubitMode::U3: - return CandidateMetrics{.numOneQ = 1, .numTwoQ = 0, .depth = 1}; - case SingleQubitMode::R: - return countNonIdentity(decomposition::EulerDecomposition::generateCircuit( - decomposition::GateEulerBasis::XYX, matrix, /*simplify=*/true, - std::nullopt)); - case SingleQubitMode::AxisPair: { - const auto bases = getEulerBasesForAxisPair(emitter.axisPair); - if (bases.empty()) { - return std::nullopt; - } - return countNonIdentity(decomposition::EulerDecomposition::generateCircuit( - bases.front(), matrix, /*simplify=*/true, std::nullopt)); - } - } - llvm_unreachable("unknown single-qubit mode"); -} - } // namespace mlir::qco::native_synth diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp index aa8e7a22ec..32e26347d4 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp @@ -11,13 +11,10 @@ #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" #include #include @@ -27,13 +24,7 @@ #include #include -#include -#include -#include -#include -#include #include -#include namespace mlir::qco::native_synth { @@ -108,133 +99,6 @@ struct SingleQubitEmitter { } // namespace -/// Materialize an `GateEulerBasis::ZSXX` decomposition (`rz` / `sx` / `x`) into -/// QCO ops. -static Value -emitEulerSequenceZsxx(SingleQubitEmitter e, Value q, - const decomposition::QubitGateSequence& seq) { - for (const auto& gate : seq.gates) { - switch (gate.type) { - case decomposition::GateKind::RZ: - if (gate.parameter.size() != 1) { - return {}; - } - q = e.rz(q, gate.parameter[0]); - break; - case decomposition::GateKind::SX: - q = e.sx(q); - break; - case decomposition::GateKind::X: - q = e.x(q); - break; - case decomposition::GateKind::I: - break; - default: - return {}; - } - } - return q; -} - -/// Materialize an `GateEulerBasis::XYX` decomposition into `R(theta, phi)` ops -/// for the `R` emitter: `Rx(theta)` becomes `R(theta, 0)`, `Ry(theta)` -/// becomes `R(theta, pi/2)`, Pauli `X`/`Y` become `R(pi, *)`, `I` is a -/// no-op. -static Value emitEulerSequenceR(SingleQubitEmitter e, Value q, - const decomposition::QubitGateSequence& seq) { - for (const auto& gate : seq.gates) { - switch (gate.type) { - case decomposition::GateKind::RX: - if (gate.parameter.size() != 1) { - return {}; - } - q = e.r(q, gate.parameter[0], 0.0); - break; - case decomposition::GateKind::RY: - if (gate.parameter.size() != 1) { - return {}; - } - q = e.r(q, gate.parameter[0], HALF_PI); - break; - case decomposition::GateKind::X: - q = e.r(q, PI, 0.0); - break; - case decomposition::GateKind::Y: - q = e.r(q, PI, HALF_PI); - break; - case decomposition::GateKind::I: - break; - default: - return {}; - } - } - return q; -} - -/// Materialize an Euler decomposition in the two rotation axes named by -/// `axis` (e.g. `{Rx, Rz}`). Every gate kind that falls outside the two -/// chosen axes (or has the wrong parameter count) is rejected by returning -/// a null `Value`; the matrix-based fallback is expected to pick a -/// different basis in that case. Pauli gates are lowered to the -/// corresponding `R*(pi)` when their axis is available. -static Value -emitEulerSequenceAxisPair(SingleQubitEmitter e, Value q, AxisPair axis, - const decomposition::QubitGateSequence& seq) { - for (const auto& gate : seq.gates) { - switch (gate.type) { - case decomposition::GateKind::RX: - if (axis == AxisPair::RyRz || gate.parameter.size() != 1) { - return {}; - } - q = e.rx(q, gate.parameter[0]); - break; - case decomposition::GateKind::RY: - if (axis == AxisPair::RxRz || gate.parameter.size() != 1) { - return {}; - } - q = e.ry(q, gate.parameter[0]); - break; - case decomposition::GateKind::RZ: - if (axis == AxisPair::RxRy || gate.parameter.size() != 1) { - return {}; - } - q = e.rz(q, gate.parameter[0]); - break; - case decomposition::GateKind::X: - if (axis == AxisPair::RyRz) { - return {}; - } - q = e.rx(q, PI); - break; - case decomposition::GateKind::Y: - if (axis == AxisPair::RxRz) { - return {}; - } - q = e.ry(q, PI); - break; - case decomposition::GateKind::Z: - if (axis == AxisPair::RxRy) { - return {}; - } - q = e.rz(q, PI); - break; - case decomposition::GateKind::I: - break; - default: - return {}; - } - } - return q; -} - -/// Decompose `matrix` numerically into a gate sequence in `basis` with -/// zero-rotations pruned (`simplify=true`). -static decomposition::QubitGateSequence -runEuler(decomposition::GateEulerBasis basis, const Eigen::Matrix2cd& matrix) { - return decomposition::EulerDecomposition::generateCircuit( - basis, matrix, /*simplify=*/true, std::nullopt); -} - Value decomposeToZSXX(IRRewriter& rewriter, Operation* op, Value inQubit, bool supportsDirectRx) { if (llvm::isa(op)) { @@ -308,101 +172,16 @@ Value decomposeToU3(IRRewriter& rewriter, Operation* op, Value inQubit) { return {}; } -std::optional -eulerSequenceForMatrixSynthesis(const Eigen::Matrix2cd& matrix, - const SingleQubitEmitterSpec& emitter) { - switch (emitter.mode) { - case SingleQubitMode::U3: - return std::nullopt; - case SingleQubitMode::ZSXX: - return runEuler(decomposition::GateEulerBasis::ZSXX, matrix); - case SingleQubitMode::R: - return runEuler(decomposition::GateEulerBasis::XYX, matrix); - case SingleQubitMode::AxisPair: { - const auto bases = getEulerBasesForAxisPair(emitter.axisPair); - if (bases.empty()) { - return std::nullopt; - } - return runEuler(bases.front(), matrix); - } - } - llvm_unreachable("unknown single-qubit mode"); -} - -std::size_t -computeSynthesizedSingleQubitLength(const Eigen::Matrix2cd& matrix, - const SingleQubitEmitterSpec& emitter) { - if (emitter.mode == SingleQubitMode::U3) { - return 1; - } - const auto seq = eulerSequenceForMatrixSynthesis(matrix, emitter); - if (!seq) { - return std::numeric_limits::max(); - } - return seq->gates.size(); -} - -Value emitSynthesizedSingleQubitFromMatrix( - IRRewriter& rewriter, Location loc, Value inQubit, - const Eigen::Matrix2cd& matrix, const SingleQubitEmitterSpec& emitter, - const decomposition::QubitGateSequence* reuseEulerSeq) { - SingleQubitEmitter e{.rewriter = &rewriter, .loc = loc}; - switch (emitter.mode) { - case SingleQubitMode::ZSXX: { - if (reuseEulerSeq != nullptr) { - emitGPhaseIfNonTrivial(rewriter, loc, reuseEulerSeq->globalPhase); - return emitEulerSequenceZsxx(e, inQubit, *reuseEulerSeq); - } - const auto seq = runEuler(decomposition::GateEulerBasis::ZSXX, matrix); - emitGPhaseIfNonTrivial(rewriter, loc, seq.globalPhase); - return emitEulerSequenceZsxx(e, inQubit, seq); - } - case SingleQubitMode::U3: { - assert(reuseEulerSeq == nullptr && - "U3 matrix emission does not use a cached Euler sequence"); - using namespace std::complex_literals; - - // Project `matrix` into SU(2) before running the Euler decomposition. - // For a 2x2 unitary, det(U) sits on the unit circle, so dividing by the - // square root of det fixes det == 1. We use `arg(det) / 2` (not - // `/ 4` as in the 4x4 case) because `sqrt(det) = exp(i * arg(det) / 2)`. - // The removed global phase is re-emitted via `emitGPhaseIfNonTrivial` - // so the final sequence equals the original unitary, not just SU(2)-up - // to global phase. - Eigen::Matrix2cd m = matrix; - const auto det = m(0, 0) * m(1, 1) - m(0, 1) * m(1, 0); - const double phase = std::arg(det) / 2.0; - m *= std::exp(1i * (-phase)); - const auto angles = decomposition::EulerDecomposition::anglesFromUnitary( - m, decomposition::GateEulerBasis::ZYZ); - emitGPhaseIfNonTrivial(rewriter, loc, phase); - return e.u(inQubit, angles[0], angles[1], angles[2]); - } - case SingleQubitMode::R: { - if (reuseEulerSeq != nullptr) { - emitGPhaseIfNonTrivial(rewriter, loc, reuseEulerSeq->globalPhase); - return emitEulerSequenceR(e, inQubit, *reuseEulerSeq); - } - const auto seq = runEuler(decomposition::GateEulerBasis::XYX, matrix); - emitGPhaseIfNonTrivial(rewriter, loc, seq.globalPhase); - return emitEulerSequenceR(e, inQubit, seq); - } - case SingleQubitMode::AxisPair: { - const auto bases = getEulerBasesForAxisPair(emitter.axisPair); - if (bases.empty()) { - return {}; - } - if (reuseEulerSeq != nullptr) { - emitGPhaseIfNonTrivial(rewriter, loc, reuseEulerSeq->globalPhase); - return emitEulerSequenceAxisPair(e, inQubit, emitter.axisPair, - *reuseEulerSeq); - } - const auto seq = runEuler(bases.front(), matrix); - emitGPhaseIfNonTrivial(rewriter, loc, seq.globalPhase); - return emitEulerSequenceAxisPair(e, inQubit, emitter.axisPair, seq); - } - } - llvm_unreachable("unknown single-qubit mode"); +Value emitSingleQubitMatrix(IRRewriter& rewriter, Location loc, Value inQubit, + const Matrix2x2& matrix, + const decomposition::EulerBasis basis) { + // Force emission (`hasNonBasisGate = true`, `runSize = 0`) so the matrix is + // always lowered into native gates of `basis`, including any residual + // `qco.gphase`. With these arguments `synthesizeUnitary1QEuler` never + // returns `std::nullopt`. + return *decomposition::synthesizeUnitary1QEuler( + rewriter, loc, inQubit, matrix, /*runSize=*/0, + /*hasNonBasisGate=*/true, basis); } Value decomposeToR(IRRewriter& rewriter, Operation* op, Value inQubit) { diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp index 3bd6b1daef..ee79e614f7 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp @@ -10,17 +10,17 @@ #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h" -#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/Gate.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" #include #include @@ -30,12 +30,10 @@ #include #include -#include +#include #include -#include #include #include -#include #include namespace mlir::qco::native_synth { @@ -43,233 +41,106 @@ namespace mlir::qco::native_synth { constexpr double PI = std::numbers::pi; constexpr double HALF_PI = PI / 2.0; -/// Whether the given single-qubit emitter can lower a decomposition-IR gate -/// of `kind` (an intermediate from Euler/Weyl, *not* a `NativeGateKind`) to a -/// native output sequence. -static bool -emitterHandlesDecompositionGate(const SingleQubitEmitterSpec& emitter, - decomposition::GateKind kind) { - if (kind == decomposition::GateKind::I) { - return true; +/// Deterministic entangler choice: prefer CX over CZ. Returns `std::nullopt` +/// when the menu has no entangler basis. +static std::optional +selectEntangler(const NativeProfileSpec& spec) { + if (usesCxEntangler(spec)) { + return EntanglerBasis::Cx; } - switch (emitter.mode) { - case SingleQubitMode::ZSXX: - return kind == decomposition::GateKind::RZ || - kind == decomposition::GateKind::SX || - kind == decomposition::GateKind::X; - case SingleQubitMode::U3: - return kind == decomposition::GateKind::U; - case SingleQubitMode::R: - return kind == decomposition::GateKind::RX || - kind == decomposition::GateKind::RY || - kind == decomposition::GateKind::X || - kind == decomposition::GateKind::Y; - case SingleQubitMode::AxisPair: - switch (emitter.axisPair) { - case AxisPair::RxRz: - return kind == decomposition::GateKind::RX || - kind == decomposition::GateKind::RZ || - kind == decomposition::GateKind::X || - kind == decomposition::GateKind::Z; - case AxisPair::RxRy: - return kind == decomposition::GateKind::RX || - kind == decomposition::GateKind::RY || - kind == decomposition::GateKind::X || - kind == decomposition::GateKind::Y; - case AxisPair::RyRz: - return kind == decomposition::GateKind::RY || - kind == decomposition::GateKind::RZ || - kind == decomposition::GateKind::Y || - kind == decomposition::GateKind::Z; - } - break; + if (usesCzEntangler(spec)) { + return EntanglerBasis::Cz; } - return false; + return std::nullopt; } -/// Check that a single decomposition gate is allowed by the profile menu. -static bool menuAllows(const decomposition::Gate& gate, - const NativeProfileSpec& spec) { - if (gate.qubitId.size() == 1) { - return std::ranges::any_of(spec.singleQubitEmitters, - [&gate](const SingleQubitEmitterSpec& emitter) { - return emitterHandlesDecompositionGate( - emitter, gate.type); - }); - } - if (gate.qubitId.size() == 2) { - switch (gate.type) { - case decomposition::GateKind::X: - return usesCxEntangler(spec); - case decomposition::GateKind::Z: - return usesCzEntangler(spec); - case decomposition::GateKind::RZZ: - return spec.allowRzz; - default: - return false; - } - } - return false; -} - -/// Whether `emitter` can lower the single-qubit `op` directly. -static bool emitterHasDirectLowering(Operation* op, - const SingleQubitEmitterSpec& emitter) { - switch (emitter.mode) { - case SingleQubitMode::ZSXX: - return canDirectlyDecomposeToZSXX(op, emitter.supportsDirectRx); - case SingleQubitMode::U3: - return canDirectlyDecomposeToU3(op); - case SingleQubitMode::R: - return canDirectlyDecomposeToR(op); - case SingleQubitMode::AxisPair: - return canDirectlyDecomposeToAxisPair(op, emitter.axisPair); - } - return false; -} - -bool gateSequenceFitsMenu(const decomposition::TwoQubitGateSequence& seq, - const NativeProfileSpec& spec) { - return std::ranges::all_of(seq.gates, - [&spec](const decomposition::Gate& gate) { - return menuAllows(gate, spec); - }); -} - -std::optional -decomposeTwoQubitFromMatrix(const Eigen::Matrix4cd& matrix, - EntanglerBasis entangler, - decomposition::GateEulerBasis eulerBasis, - std::optional numBasisUses) { - // Basis-gate qubit ids align with `getBlockTwoQubitMatrix` / CX layout. - const decomposition::Gate basisGate{ +/// Build the decomposition-layer basis gate for `entangler`. The qubit ids +/// align with `getBlockTwoQubitMatrix` / CX layout (control on qubit 0). +static decomposition::Gate entanglerGate(EntanglerBasis entangler) { + return decomposition::Gate{ .type = entangler == EntanglerBasis::Cz ? decomposition::GateKind::Z : decomposition::GateKind::X, .qubitId = {0, 1}, }; +} + +/// Run the Weyl + basis decomposer for `target` against `entangler`, returning +/// the raw single-qubit factors and entangler count (or `std::nullopt`). +static std::optional +decomposeWithEntangler(const Matrix4x4& target, EntanglerBasis entangler) { + const auto basisGate = entanglerGate(entangler); auto decomposer = decomposition::TwoQubitBasisDecomposer::create(basisGate, 1.0); auto weyl = - decomposition::TwoQubitWeylDecomposition::create(matrix, std::nullopt); - return decomposer.twoQubitDecompose( - weyl, llvm::SmallVector{eulerBasis}, - std::nullopt, /*approximate=*/false, numBasisUses); + decomposition::TwoQubitWeylDecomposition::create(target, std::nullopt); + return decomposer.twoQubitDecompose(weyl, std::nullopt); } -llvm::SmallVector, 0> -collectSingleQubitCandidates(UnitaryOpInterface unitary, - const NativeProfileSpec& spec) { - llvm::SmallVector, 0> candidates; - Operation* op = unitary.getOperation(); - unsigned enumerationIndex = 0; - const auto addCandidate = [&](CandidateClass klass, CandidateMetrics metrics, - SingleQubitRewriteStrategy strategy, - const SingleQubitEmitterSpec& emitter) { - candidates.push_back(SynthesisCandidate{ - .candidateClass = klass, - .metrics = metrics, - .enumerationIndex = enumerationIndex++, - .payload = - SingleQubitRewritePlan{.strategy = strategy, .emitter = emitter}, - }); - }; - for (const auto& emitter : spec.singleQubitEmitters) { - if (emitterHasDirectLowering(op, emitter)) { - addCandidate(CandidateClass::DirectSingleQ, - estimateDirectSingleQubitMetrics(op, emitter), - SingleQubitRewriteStrategy::Direct, emitter); - } - if (auto matrixMetrics = - estimateMatrixSingleQubitMetrics(unitary, emitter)) { - addCandidate(CandidateClass::MatrixSingleQ, *matrixMetrics, - SingleQubitRewriteStrategy::MatrixFallback, emitter); - } +std::optional +twoQubitEntanglerCount(const Matrix4x4& target, const NativeProfileSpec& spec) { + const auto entangler = selectEntangler(spec); + if (!entangler) { + return std::nullopt; } - return candidates; -} - -/// Try every `numBasisUses` in `{0, 1, 2, 3}` for the `(entangler, emitter, -/// basis)` triple, running the Weyl-based basis decomposer for each. Any -/// resulting gate sequence that both matches `targetMatrix` up to global -/// phase AND stays inside the native menu is appended to `candidates`. -static void tryAddTwoQubitBasisCandidatesForEmitterBasis( - llvm::SmallVector, 0>& candidates, - unsigned& enumerationIndex, const Eigen::Matrix4cd& targetMatrix, - const NativeProfileSpec& spec, EntanglerBasis entangler, - const SingleQubitEmitterSpec& emitter, - decomposition::GateEulerBasis basis) { - // An arbitrary 2-qubit unitary can always be realized using at most three - // copies of any fixed (non-diagonal) entangler plus local gates -- this is - // a consequence of the KAK/Weyl decomposition. Trying all four candidate - // counts (0..3) and scoring them with the gate-sequence metric lets the - // outer pass pick the cheapest realization for the particular target - // unitary (e.g. local unitaries collapse to 0 entanglers, SWAP uses 3). - for (std::uint8_t numBasisUses = 0; numBasisUses <= 3; ++numBasisUses) { - auto seq = decomposeTwoQubitFromMatrix(targetMatrix, entangler, basis, - numBasisUses); - // Two independent checks: `isEquivalentUpToGlobalPhase` verifies the - // numerical decomposition actually reproduces the target; `fitsMenu` - // verifies every emitted gate kind is in the backend native set. Both - // are required because the decomposer can legitimately produce an - // accurate sequence that still contains non-native gates (e.g. when the - // requested emitter supports fewer axes than the target unitary needs). - if (!seq || - !isEquivalentUpToGlobalPhase(seq->getUnitaryMatrix(), targetMatrix) || - !gateSequenceFitsMenu(*seq, spec)) { - continue; - } - candidates.push_back(SynthesisCandidate{ - .candidateClass = CandidateClass::TwoQubitBasisRewrite, - .metrics = computeGateSequenceMetrics(*seq), - .enumerationIndex = enumerationIndex++, - .payload = {.sequence = - std::make_shared( - std::move(*seq)), - .emitter = emitter, - .entanglerBasis = entangler}, - }); + const auto native = decomposeWithEntangler(target, *entangler); + if (!native) { + return std::nullopt; } + return native->numBasisUses; } -llvm::SmallVector, 0> -collectTwoQubitBasisCandidatesFromMatrix(const Eigen::Matrix4cd& targetMatrix, - const NativeProfileSpec& spec) { - llvm::SmallVector, 0> candidates; - if (spec.entanglerBases.empty()) { - return candidates; +LogicalResult emitTwoQubitNative(IRRewriter& rewriter, Location loc, + Value qubit0, Value qubit1, + const Matrix4x4& target, + const NativeProfileSpec& spec, + Value& outQubit0, Value& outQubit1) { + const auto entangler = selectEntangler(spec); + if (!entangler) { + return failure(); } - unsigned enumerationIndex = 0; - for (const auto entangler : spec.entanglerBases) { - for (const auto& emitter : spec.singleQubitEmitters) { - for (const auto basis : emitter.eulerBases) { - tryAddTwoQubitBasisCandidatesForEmitterBasis( - candidates, enumerationIndex, targetMatrix, spec, entangler, - emitter, basis); - } - } + const auto native = decomposeWithEntangler(target, *entangler); + if (!native) { + return failure(); } - return candidates; -} + const auto basis = emitterEulerBasis(spec.singleQubitEmitters.front()); -CandidateMetrics xxPlusMinusYyRzzRewriteScoringMetrics() { - // Tallies for `rewriteXXPlusMinusYYViaRzz` (identical for `XXPlusYY` and - // `XXMinusYY`): leading/final `rz` on `q0` (2) + `ryy` via `rzz` (four 1q + - // one `rzz`) + `rxx` via `rzz` (four `(rz, sx, rz)` per wire around each - // `rzz`, i.e. twelve 1q + one `rzz`). - constexpr unsigned numOneQ = 18; - constexpr unsigned numTwoQ = 2; - constexpr unsigned depth = 12; - return {.numOneQ = numOneQ, .numTwoQ = numTwoQ, .depth = depth}; -} + // Residual global phase not represented by the factors / entanglers. + emitGPhaseIfNonTrivial(rewriter, loc, native->globalPhase); + + Value wire0 = qubit0; + Value wire1 = qubit1; + const auto& factors = native->singleQubitFactors; + const std::uint8_t numBasisUses = native->numBasisUses; + const auto emitFactor = [&](Value& wire, std::size_t index) { + wire = emitSingleQubitMatrix(rewriter, loc, wire, factors[index], basis); + }; + const auto emitEntangler = [&]() { + // The entangler acts with its control on wire 0 and target on wire 1. + auto ctrlOp = CtrlOp::create( + rewriter, loc, ValueRange{wire0}, ValueRange{wire1}, + [&](ValueRange targetArgs) -> llvm::SmallVector { + if (*entangler == EntanglerBasis::Cz) { + return { + ZOp::create(rewriter, loc, targetArgs[0]).getOutputQubit(0)}; + } + return {XOp::create(rewriter, loc, targetArgs[0]).getOutputQubit(0)}; + }); + wire0 = ctrlOp.getOutputControl(0); + wire1 = ctrlOp.getOutputTarget(0); + }; -llvm::SmallVector, 0> -collectTwoQubitBasisCandidates(UnitaryOpInterface unitary, - const NativeProfileSpec& spec) { - Eigen::Matrix4cd target; - if (!getNormalizedTwoQubitMatrix(unitary, target)) { - return {}; + // factor[2i] on wire 1, factor[2i + 1] on wire 0, then one entangler. + for (std::uint8_t i = 0; i < numBasisUses; ++i) { + emitFactor(wire1, static_cast(2 * i)); + emitFactor(wire0, static_cast((2 * i) + 1)); + emitEntangler(); } - return collectTwoQubitBasisCandidatesFromMatrix(target, spec); + emitFactor(wire1, static_cast(2 * numBasisUses)); + emitFactor(wire0, static_cast((2 * numBasisUses) + 1)); + + outQubit0 = wire0; + outQubit1 = wire1; + return success(); } LogicalResult rewriteXXPlusMinusYYViaRzz(IRRewriter& rewriter, Operation* op) { diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp index 07dce3f028..37bed956f4 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp @@ -12,14 +12,9 @@ #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/Gate.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" #include -#include -#include #include #include #include @@ -27,37 +22,14 @@ #include #include #include -#include -#include #include #include +#include #include namespace mlir::qco::native_synth { -Eigen::Matrix2cd toEigen(const Matrix2x2& matrix) { - Eigen::Matrix2cd out; - for (std::size_t row = 0; row < 2; ++row) { - for (std::size_t col = 0; col < 2; ++col) { - out(static_cast(row), static_cast(col)) = - matrix(row, col); - } - } - return out; -} - -Eigen::Matrix4cd toEigen(const Matrix4x4& matrix) { - Eigen::Matrix4cd out; - for (std::size_t row = 0; row < 4; ++row) { - for (std::size_t col = 0; col < 4; ++col) { - out(static_cast(row), static_cast(col)) = - matrix(row, col); - } - } - return out; -} - Value createF64Const(IRRewriter& rewriter, Location loc, double value) { return arith::ConstantFloatOp::create(rewriter, loc, rewriter.getF64Type(), llvm::APFloat(value)) @@ -80,19 +52,19 @@ void emitGPhaseIfNonTrivial(IRRewriter& rewriter, Location loc, double phase) { } } -bool isEquivalentUpToGlobalPhase(const Eigen::Matrix4cd& lhs, - const Eigen::Matrix4cd& rhs, double atol) { - const auto overlap = (rhs.adjoint() * lhs).trace(); +bool isEquivalentUpToGlobalPhase(const Matrix4x4& lhs, const Matrix4x4& rhs, + double atol) { + const Complex overlap = (rhs.adjoint() * lhs).trace(); if (std::abs(overlap) <= atol) { return false; } - const auto factor = overlap / std::abs(overlap); - return lhs.isApprox(factor * rhs, atol); + const Complex factor = overlap / std::abs(overlap); + return lhs.isApprox(rhs * factor, atol); } -void normalizeToSU4(Eigen::Matrix4cd& matrix) { +void normalizeToSU4(Matrix4x4& matrix) { using namespace std::complex_literals; - const std::complex det = matrix.determinant(); + const Complex det = matrix.determinant(); // Project `matrix` into SU(4) by dividing out the fourth root of its // determinant (det(SU(N)) == 1). `|det|^{-1/4}` fixes the magnitude and // `exp(-i * arg(det) / 4)` removes the global phase so the Weyl @@ -104,17 +76,17 @@ void normalizeToSU4(Eigen::Matrix4cd& matrix) { } bool getNormalizedTwoQubitMatrix(UnitaryOpInterface unitary, - Eigen::Matrix4cd& matrix) { + Matrix4x4& matrix) { Matrix4x4 raw; if (!unitary.getUnitaryMatrix4x4(raw)) { return false; } - matrix = toEigen(raw); + matrix = raw; normalizeToSU4(matrix); return true; } -bool getBlockTwoQubitMatrix(Operation* op, Eigen::Matrix4cd& matrix) { +bool getBlockTwoQubitMatrix(Operation* op, Matrix4x4& matrix) { if (llvm::isa(op)) { return false; } @@ -125,11 +97,14 @@ bool getBlockTwoQubitMatrix(Operation* op, Eigen::Matrix4cd& matrix) { auto* body = ctrl.getBodyUnitary(0).getOperation(); if (llvm::isa(body)) { // CX matrix in the same 4x4 basis layout as ``getUnitaryMatrix4x4``. - matrix << 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0; + matrix = Matrix4x4::fromElements(1, 0, 0, 0, // + 0, 1, 0, 0, // + 0, 0, 0, 1, // + 0, 0, 1, 0); return true; } if (llvm::isa(body)) { - matrix = Eigen::Matrix4cd::Identity(); + matrix = Matrix4x4::identity(); matrix(3, 3) = -1.0; return true; } @@ -143,203 +118,8 @@ bool getBlockTwoQubitMatrix(Operation* op, Eigen::Matrix4cd& matrix) { if (!unitary.getUnitaryMatrix4x4(raw)) { return false; } - matrix = toEigen(raw); + matrix = raw; return true; } -/// Emit a single-qubit gate from a decomposition gate, threading `target` and -/// recording the inserted op (if any) in `insertedOps` so the caller can roll -/// back on failure. -static LogicalResult -emitSingleQubitStep(IRRewriter& rewriter, Location loc, - const decomposition::Gate& gate, Value& target, - llvm::SmallVectorImpl& insertedOps) { - const auto emitConst = [&](double v) { - auto constant = arith::ConstantFloatOp::create( - rewriter, loc, rewriter.getF64Type(), llvm::APFloat(v)); - insertedOps.push_back(constant); - return constant.getResult(); - }; - const auto record = [&](auto op) { - insertedOps.push_back(op.getOperation()); - return op; - }; - switch (gate.type) { - case decomposition::GateKind::I: - return success(); - case decomposition::GateKind::U: - if (gate.parameter.size() != 3) { - return failure(); - } - target = - record(UOp::create(rewriter, loc, target, emitConst(gate.parameter[0]), - emitConst(gate.parameter[1]), - emitConst(gate.parameter[2]))) - .getOutputQubit(0); - return success(); - case decomposition::GateKind::U2: - if (gate.parameter.size() != 2) { - return failure(); - } - target = - record(U2Op::create(rewriter, loc, target, emitConst(gate.parameter[0]), - emitConst(gate.parameter[1]))) - .getOutputQubit(0); - return success(); - case decomposition::GateKind::SX: - target = record(SXOp::create(rewriter, loc, target)).getOutputQubit(0); - return success(); - case decomposition::GateKind::X: - target = record(XOp::create(rewriter, loc, target)).getOutputQubit(0); - return success(); - case decomposition::GateKind::RX: - if (gate.parameter.size() != 1) { - return failure(); - } - target = record(RXOp::create(rewriter, loc, target, - emitConst(gate.parameter[0]))) - .getOutputQubit(0); - return success(); - case decomposition::GateKind::RY: - if (gate.parameter.size() != 1) { - return failure(); - } - target = record(RYOp::create(rewriter, loc, target, - emitConst(gate.parameter[0]))) - .getOutputQubit(0); - return success(); - case decomposition::GateKind::RZ: - if (gate.parameter.size() != 1) { - return failure(); - } - target = record(RZOp::create(rewriter, loc, target, - emitConst(gate.parameter[0]))) - .getOutputQubit(0); - return success(); - default: - return failure(); - } -} - -/// Erase all ops tracked in `insertedOps` in reverse insertion order. -static void -rollbackInsertedOps(IRRewriter& rewriter, - llvm::SmallVectorImpl& insertedOps) { - for (Operation* op : llvm::reverse(insertedOps)) { - rewriter.eraseOp(op); - } - insertedOps.clear(); -} - -LogicalResult -emitTwoQubitGateSequenceAtLoc(IRRewriter& rewriter, Location loc, Value qubit0, - Value qubit1, - const decomposition::TwoQubitGateSequence& seq, - Value& outQubit0, Value& outQubit1) { - llvm::SmallVector insertedOps; - for (const auto& gate : seq.gates) { - if (gate.qubitId.size() == 1) { - Value& target = (gate.qubitId[0] == 0) ? qubit0 : qubit1; - if (failed( - emitSingleQubitStep(rewriter, loc, gate, target, insertedOps))) { - rollbackInsertedOps(rewriter, insertedOps); - return failure(); - } - continue; - } - - if (gate.qubitId.size() != 2) { - rollbackInsertedOps(rewriter, insertedOps); - return failure(); - } - - if (gate.type == decomposition::GateKind::RZZ) { - if (gate.parameter.size() != 1) { - rollbackInsertedOps(rewriter, insertedOps); - return failure(); - } - const decomposition::QubitId a = gate.qubitId[0]; - const decomposition::QubitId b = gate.qubitId[1]; - if (a + b != 1) { - rollbackInsertedOps(rewriter, insertedOps); - return failure(); - } - const Value va = (a == 0) ? qubit0 : qubit1; - const Value vb = (b == 0) ? qubit0 : qubit1; - Value thetaVal = createF64Const(rewriter, loc, gate.parameter[0]); - insertedOps.push_back(thetaVal.getDefiningOp()); - auto rzz = RZZOp::create(rewriter, loc, va, vb, thetaVal); - insertedOps.push_back(rzz.getOperation()); - qubit0 = (gate.qubitId[0] == 0) ? rzz.getOutputQubit(0) - : rzz.getOutputQubit(1); - qubit1 = (gate.qubitId[0] == 1) ? rzz.getOutputQubit(0) - : rzz.getOutputQubit(1); - continue; - } - - if (gate.type != decomposition::GateKind::X && - gate.type != decomposition::GateKind::Z) { - rollbackInsertedOps(rewriter, insertedOps); - return failure(); - } - - const decomposition::QubitId controlId = gate.qubitId[0]; - const decomposition::QubitId targetId = gate.qubitId[1]; - const Value controlIn = (controlId == 0) ? qubit0 : qubit1; - const Value targetIn = (targetId == 0) ? qubit0 : qubit1; - - auto ctrlOp = CtrlOp::create( - rewriter, loc, ValueRange{controlIn}, ValueRange{targetIn}, - [&](ValueRange targetArgs) -> llvm::SmallVector { - if (gate.type == decomposition::GateKind::X) { - return { - XOp::create(rewriter, loc, targetArgs[0]).getOutputQubit(0)}; - } - return {ZOp::create(rewriter, loc, targetArgs[0]).getOutputQubit(0)}; - }); - // Erasing the `CtrlOp` also removes its nested body op. - insertedOps.push_back(ctrlOp.getOperation()); - const Value controlOut = ctrlOp.getOutputControl(0); - const Value targetOut = ctrlOp.getOutputTarget(0); - Value next0 = qubit0; - Value next1 = qubit1; - if (controlId == 0) { - next0 = controlOut; - } else { - next1 = controlOut; - } - if (targetId == 0) { - next0 = targetOut; - } else { - next1 = targetOut; - } - qubit0 = next0; - qubit1 = next1; - } - - outQubit0 = qubit0; - outQubit1 = qubit1; - return success(); -} - -LogicalResult -emitTwoQubitGateSequence(IRRewriter& rewriter, Operation* op, Value qubit0, - Value qubit1, - const decomposition::TwoQubitGateSequence& seq) { - Value outQubit0; - Value outQubit1; - if (failed(emitTwoQubitGateSequenceAtLoc( - rewriter, op->getLoc(), qubit0, qubit1, seq, outQubit0, outQubit1))) { - return failure(); - } - // Match `seq.getUnitaryMatrix()` / `PassTwoQubitWindows` materialization: - // residual phase from Weyl + basis decomposition is not represented as 2q - // ops in `seq.gates`. - if (seq.hasGlobalPhase()) { - emitGPhaseIfNonTrivial(rewriter, op->getLoc(), seq.globalPhase); - } - rewriter.replaceOp(op, ValueRange{outQubit0, outQubit1}); - return success(); -} - } // namespace mlir::qco::native_synth diff --git a/mlir/lib/Dialect/QCO/Utils/Matrix.cpp b/mlir/lib/Dialect/QCO/Utils/Matrix.cpp index 8df45840a3..bb5eedc63d 100644 --- a/mlir/lib/Dialect/QCO/Utils/Matrix.cpp +++ b/mlir/lib/Dialect/QCO/Utils/Matrix.cpp @@ -231,6 +231,10 @@ Matrix2x2 Matrix2x2::adjoint() const { std::conj(data[1]), std::conj(data[3])); } +Matrix2x2 Matrix2x2::transpose() const { + return fromElements(data[0], data[2], data[1], data[3]); +} + Complex Matrix2x2::trace() const { return data[0] + data[3]; } Complex Matrix2x2::determinant() const { @@ -241,6 +245,10 @@ bool Matrix2x2::isApprox(const Matrix2x2& other, const double tol) const { return entriesAreApprox(data, other.data, tol); } +bool Matrix2x2::isIdentity(const double tol) const { + return isApprox(fromElements(1.0, 0.0, 0.0, 1.0), tol); +} + bool Matrix2x2::assignFrom(const DynamicMatrix& src) { return assignFromDynamicImpl(src, data); } @@ -296,6 +304,16 @@ Matrix4x4 Matrix4x4::adjoint() const { return out; } +Matrix4x4 Matrix4x4::transpose() const { + Matrix4x4 out{}; + for (std::size_t row = 0; row < K_ROWS; ++row) { + for (std::size_t col = 0; col < K_COLS; ++col) { + out.data[(col * K_COLS) + row] = data[(row * K_COLS) + col]; + } + } + return out; +} + Complex Matrix4x4::trace() const { return data[0] + data[5] + data[10] + data[15]; } @@ -321,6 +339,58 @@ bool Matrix4x4::isApprox(const Matrix4x4& other, const double tol) const { return entriesAreApprox(data, other.data, tol); } +bool Matrix4x4::isIdentity(const double tol) const { + Matrix4x4 id{}; + for (std::size_t i = 0; i < K_ROWS; ++i) { + id.data[(i * K_COLS) + i] = 1.0; + } + return isApprox(id, tol); +} + +std::array Matrix4x4::diagonal() const { + return {data[0], data[5], data[10], data[15]}; +} + +Matrix4x4 +Matrix4x4::fromDiagonal(const std::array& diagonalEntries) { + Matrix4x4 out{}; + for (std::size_t i = 0; i < K_ROWS; ++i) { + out.data[(i * K_COLS) + i] = diagonalEntries[i]; + } + return out; +} + +std::array +Matrix4x4::column(const std::size_t col) const { + return {data[col], data[K_COLS + col], data[(2 * K_COLS) + col], + data[(3 * K_COLS) + col]}; +} + +void Matrix4x4::setColumn(const std::size_t col, + const std::array& values) { + for (std::size_t row = 0; row < K_ROWS; ++row) { + data[(row * K_COLS) + col] = values[row]; + } +} + +std::array +Matrix4x4::realPart() const { + std::array out{}; + for (std::size_t i = 0; i < K_SIZE_AT_COMPILE_TIME; ++i) { + out[i] = data[i].real(); + } + return out; +} + +std::array +Matrix4x4::imagPart() const { + std::array out{}; + for (std::size_t i = 0; i < K_SIZE_AT_COMPILE_TIME; ++i) { + out[i] = data[i].imag(); + } + return out; +} + bool Matrix4x4::assignFrom(const DynamicMatrix& src) { return assignFromDynamicImpl(src, data); } @@ -453,4 +523,103 @@ bool DynamicMatrix::isApprox(const DynamicMatrix& other, return entriesAreApprox(impl_->data, other.impl_->data, tol); } +Matrix2x2 operator*(const Complex& scalar, const Matrix2x2& matrix) { + return matrix * scalar; +} + +Matrix4x4 operator*(const Complex& scalar, const Matrix4x4& matrix) { + return matrix * scalar; +} + +Matrix4x4 kron(const Matrix2x2& lhs, const Matrix2x2& rhs) { + Matrix4x4 out{}; + for (std::size_t i = 0; i < Matrix2x2::K_ROWS; ++i) { + for (std::size_t j = 0; j < Matrix2x2::K_COLS; ++j) { + const Complex a = lhs(i, j); + for (std::size_t k = 0; k < Matrix2x2::K_ROWS; ++k) { + for (std::size_t l = 0; l < Matrix2x2::K_COLS; ++l) { + out((2 * i) + k, (2 * j) + l) = a * rhs(k, l); + } + } + } + } + return out; +} + +SymmetricEigen4 jacobiSymmetricEigen(const std::array& symmetric) { + constexpr std::size_t n = 4; + constexpr int maxSweeps = 100; + + std::array a = symmetric; + std::array v{}; + for (std::size_t i = 0; i < n; ++i) { + v[(i * n) + i] = 1.0; + } + + for (int sweep = 0; sweep < maxSweeps; ++sweep) { + double off = 0.0; + for (std::size_t p = 0; p < n; ++p) { + for (std::size_t q = p + 1; q < n; ++q) { + off += a[(p * n) + q] * a[(p * n) + q]; + } + } + if (off <= 1e-30) { + break; + } + + for (std::size_t p = 0; p < n; ++p) { + for (std::size_t q = p + 1; q < n; ++q) { + const double apq = a[(p * n) + q]; + if (std::abs(apq) <= 1e-300) { + continue; + } + const double app = a[(p * n) + p]; + const double aqq = a[(q * n) + q]; + // Rotation angle that annihilates the (p, q) off-diagonal entry. + const double phi = 0.5 * std::atan2(2.0 * apq, aqq - app); + const double c = std::cos(phi); + const double s = std::sin(phi); + + // Right-multiply by the Givens rotation: columns p and q. + for (std::size_t k = 0; k < n; ++k) { + const double akp = a[(k * n) + p]; + const double akq = a[(k * n) + q]; + a[(k * n) + p] = (c * akp) - (s * akq); + a[(k * n) + q] = (s * akp) + (c * akq); + } + // Left-multiply by the transposed rotation: rows p and q. + for (std::size_t k = 0; k < n; ++k) { + const double apk = a[(p * n) + k]; + const double aqk = a[(q * n) + k]; + a[(p * n) + k] = (c * apk) - (s * aqk); + a[(q * n) + k] = (s * apk) + (c * aqk); + } + // Accumulate the rotation into the eigenvector matrix. + for (std::size_t k = 0; k < n; ++k) { + const double vkp = v[(k * n) + p]; + const double vkq = v[(k * n) + q]; + v[(k * n) + p] = (c * vkp) - (s * vkq); + v[(k * n) + q] = (s * vkp) + (c * vkq); + } + } + } + } + + std::array evals{a[0], a[5], a[10], a[15]}; + std::array order{0, 1, 2, 3}; + std::ranges::sort(order, [&evals](const std::size_t x, const std::size_t y) { + return evals[x] < evals[y]; + }); + + SymmetricEigen4 result; + for (std::size_t j = 0; j < n; ++j) { + const std::size_t src = order[j]; + result.eigenvalues[j] = evals[src]; + for (std::size_t i = 0; i < n; ++i) { + result.eigenvectors(i, j) = Complex{v[(i * n) + src], 0.0}; + } + } + return result; +} + } // namespace mlir::qco diff --git a/mlir/tools/mqt-cc/mqt-cc.cpp b/mlir/tools/mqt-cc/mqt-cc.cpp index 7cc3ea26db..43625aa73a 100644 --- a/mlir/tools/mqt-cc/mqt-cc.cpp +++ b/mlir/tools/mqt-cc/mqt-cc.cpp @@ -91,28 +91,12 @@ static llvm::cl::opt enableHadamardLifting( llvm::cl::desc("Apply Hadamard lifting during optimization"), llvm::cl::init(false)); -static cl::opt nativeGates( +static llvm::cl::opt nativeGates( "native-gates", - cl::desc("Comma-separated native gate menu for the native-gate-synthesis " - "pass (empty or whitespace-only disables synthesis)"), - cl::value_desc("csv"), cl::init("")); - -static cl::opt nativeGateScoreWeightTwoQ( - "score-weight-twoq", - cl::desc( - "Weight for two-qubit gates in native synthesis candidate scoring"), - cl::init(1.0)); - -static cl::opt nativeGateScoreWeightOneQ( - "score-weight-oneq", - cl::desc("Weight for single-qubit gates in native synthesis candidate " - "scoring"), - cl::init(0.1)); - -static cl::opt nativeGateScoreWeightDepth( - "score-weight-depth", - cl::desc("Weight for local depth in native synthesis candidate scoring"), - cl::init(0.01)); + llvm::cl::desc( + "Comma-separated native gate menu for the native-gate-synthesis " + "pass (empty or whitespace-only disables synthesis)"), + llvm::cl::value_desc("csv"), llvm::cl::init("")); /** * @brief Load and parse a .qasm file @@ -211,9 +195,6 @@ int main(int argc, char** argv) { disableMergeSingleQubitRotationGates; config.enableHadamardLifting = enableHadamardLifting; config.nativeGates = nativeGates.getValue(); - config.nativeGateScoreWeightTwoQ = nativeGateScoreWeightTwoQ.getValue(); - config.nativeGateScoreWeightOneQ = nativeGateScoreWeightOneQ.getValue(); - config.nativeGateScoreWeightDepth = nativeGateScoreWeightDepth.getValue(); // Run the compilation pipeline CompilationRecord record; diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 70fd7dd1d9..2c60e931f8 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -48,7 +48,6 @@ #include #include -#include #include #include #include @@ -757,13 +756,13 @@ using mqt::test::isEquivalentUpToGlobalPhase; /// `qco.x`, `qco.p`, `qco.u`; and `qco.gphase`, which is skipped). Returns /// `std::nullopt` if the IR contains an unsupported op or non-constant /// parameters. -static std::optional +static std::optional computeStaticTwoQubitUnitary(mlir::ModuleOp module) { if (module == nullptr) { return std::nullopt; } - Eigen::Matrix4cd unitary = Eigen::Matrix4cd::Identity(); + mlir::qco::Matrix4x4 unitary = mlir::qco::Matrix4x4::identity(); llvm::DenseMap qubitIds; const auto getQubitId = [&](mlir::Value qubit) -> std::optional { @@ -800,12 +799,10 @@ computeStaticTwoQubitUnitary(mlir::ModuleOp module) { if (!qid) { return std::nullopt; } - mlir::qco::Matrix2x2 rawOneQ; - if (!op.getUnitaryMatrix2x2(rawOneQ)) { + mlir::qco::Matrix2x2 oneQ; + if (!op.getUnitaryMatrix2x2(oneQ)) { return std::nullopt; } - const Eigen::Matrix2cd oneQ = - mlir::qco::native_synth::toEigen(rawOneQ); unitary = mlir::qco::decomposition::expandToTwoQubits(oneQ, *qid) * unitary; qubitIds[op.getOutputQubit(0)] = *qid; @@ -818,7 +815,7 @@ computeStaticTwoQubitUnitary(mlir::ModuleOp module) { if (!q0 || !q1) { return std::nullopt; } - Eigen::Matrix4cd twoQ; + mlir::qco::Matrix4x4 twoQ; if (auto ctrl = llvm::dyn_cast(&rawOp)) { if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { return std::nullopt; @@ -826,19 +823,17 @@ computeStaticTwoQubitUnitary(mlir::ModuleOp module) { auto* body = ctrl.getBodyUnitary(0).getOperation(); if (llvm::isa(body)) { // CX matrix (same 4×4 layout as QCO unitary interface). - twoQ << 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0; + twoQ = mlir::qco::Matrix4x4::fromElements(1, 0, 0, 0, 0, 1, 0, 0, + 0, 0, 0, 1, 0, 0, 1, 0); } else if (llvm::isa(body)) { - twoQ = Eigen::Matrix4cd::Identity(); - twoQ(3, 3) = -1.0; + // CZ matrix: identity with a `-1` phase on the `|11>` entry. + twoQ = mlir::qco::Matrix4x4::fromElements( + 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, -1); } else { return std::nullopt; } - } else { - mlir::qco::Matrix4x4 rawTwoQ; - if (!op.getUnitaryMatrix4x4(rawTwoQ)) { - return std::nullopt; - } - twoQ = mlir::qco::native_synth::toEigen(rawTwoQ); + } else if (!op.getUnitaryMatrix4x4(twoQ)) { + return std::nullopt; } const llvm::SmallVector ids{ static_cast(*q0), @@ -892,14 +887,6 @@ TEST_F(CompilerPipelineNativeSynthesisConfigTest, EXPECT_EQ(record.afterOptimization.find("qco.h"), std::string::npos); } -TEST_F(CompilerPipelineNativeSynthesisConfigTest, - RejectsInvalidNativeSynthesisScoreWeightsInStage5) { - config.nativeGates = "u,cx"; - config.nativeGateScoreWeightTwoQ = -1.0; - - runPipelineAndExpectFailure(); -} - TEST_F(CompilerPipelineNativeSynthesisConfigTest, RejectsUnderSpecifiedNativeSynthesisMenuInStage5) { // A menu with only two-qubit entanglers cannot synthesize any single-qubit diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt index 1b231420ec..78bdafbc48 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt @@ -10,7 +10,7 @@ set(target_name mqt-core-mlir-unittest-decomposition) add_executable( ${target_name} test_basis_decomposer.cpp test_decomposition_get_gate_kind.cpp test_decomposition_helpers.cpp - test_euler_decomposition.cpp test_matrix_euler_decomposition.cpp test_weyl_decomposition.cpp) + test_euler_decomposition.cpp test_weyl_decomposition.cpp) target_link_libraries( ${target_name} @@ -24,8 +24,7 @@ target_link_libraries( MLIRIR MLIRSupport MLIRQTensorDialect - LLVMSupport - Eigen3::Eigen) + LLVMSupport) mqt_mlir_configure_unittest_target(${target_name}) diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/decomposition_test_utils.h b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/decomposition_test_utils.h index 4bf8d176af..a5a5091523 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/decomposition_test_utils.h +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/decomposition_test_utils.h @@ -13,13 +13,14 @@ #include "TestCaseUtils.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" - -#include -#include +#include "mlir/Dialect/QCO/Utils/Matrix.h" #include +#include #include +#include #include +#include namespace mlir::qco::decomposition_test { @@ -27,37 +28,73 @@ using mqt::test::isEquivalentUpToGlobalPhase; /// Standard `U3(theta, phi, lambda)` matrix. Thin wrapper over the library /// `uMatrix` so every test uses the same implementation. -[[nodiscard]] inline Eigen::Matrix2cd u3Matrix(double theta, double phi, - double lambda) { +[[nodiscard]] inline Matrix2x2 u3Matrix(double theta, double phi, + double lambda) { return decomposition::uMatrix(theta, phi, lambda); } -template -[[nodiscard]] MatrixType randomUnitaryMatrix(std::mt19937& rng) { - static_assert(MatrixType::RowsAtCompileTime != Eigen::Dynamic && - MatrixType::ColsAtCompileTime != Eigen::Dynamic, - "randomUnitaryMatrix requires fixed-size matrices"); +namespace detail { + +/// Generate a Haar-ish random unitary as a row-major `dim x dim` buffer via +/// modified Gram-Schmidt on Gaussian-random complex columns. +[[nodiscard]] inline std::vector> +randomUnitaryData(std::size_t dim, std::mt19937& rng) { std::normal_distribution normalDist(0.0, 1.0); - MatrixType randomMatrix; - for (auto& x : randomMatrix.reshaped()) { - x = std::complex(normalDist(rng), normalDist(rng)); + std::vector>> columns( + dim, std::vector>(dim)); + for (auto& column : columns) { + for (auto& entry : column) { + entry = std::complex(normalDist(rng), normalDist(rng)); + } } - Eigen::HouseholderQR qr{}; - qr.compute(randomMatrix); - const MatrixType qMatrix = qr.householderQ(); - const MatrixType rMatrix = - qr.matrixQR().template triangularView(); - MatrixType dMatrix = MatrixType::Identity(); - constexpr Eigen::Index dim = MatrixType::RowsAtCompileTime; - for (Eigen::Index i = 0; i < dim; ++i) { - const auto rii = rMatrix(i, i); - const auto absRii = std::abs(rii); - dMatrix(i, i) = - absRii > 0.0 ? (rii / absRii) : std::complex{1.0, 0.0}; + for (std::size_t j = 0; j < dim; ++j) { + for (std::size_t k = 0; k < j; ++k) { + std::complex projection{0.0, 0.0}; + for (std::size_t i = 0; i < dim; ++i) { + projection += std::conj(columns[k][i]) * columns[j][i]; + } + for (std::size_t i = 0; i < dim; ++i) { + columns[j][i] -= projection * columns[k][i]; + } + } + double norm = 0.0; + for (std::size_t i = 0; i < dim; ++i) { + norm += std::norm(columns[j][i]); + } + norm = std::sqrt(norm); + for (std::size_t i = 0; i < dim; ++i) { + columns[j][i] /= norm; + } } - const MatrixType unitaryMatrix = qMatrix * dMatrix; - assert(helpers::isUnitaryMatrix(unitaryMatrix)); - return unitaryMatrix; + std::vector> data(dim * dim); + for (std::size_t row = 0; row < dim; ++row) { + for (std::size_t col = 0; col < dim; ++col) { + data[(row * dim) + col] = columns[col][row]; + } + } + return data; +} + +} // namespace detail + +/// Random `2×2` unitary matrix. +[[nodiscard]] inline Matrix2x2 randomUnitary2x2(std::mt19937& rng) { + const auto data = detail::randomUnitaryData(2, rng); + const Matrix2x2 unitary = + Matrix2x2::fromElements(data[0], data[1], data[2], data[3]); + assert(helpers::isUnitaryMatrix(unitary)); + return unitary; +} + +/// Random `4×4` unitary matrix. +[[nodiscard]] inline Matrix4x4 randomUnitary4x4(std::mt19937& rng) { + const auto data = detail::randomUnitaryData(4, rng); + const Matrix4x4 unitary = Matrix4x4::fromElements( + data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], + data[8], data[9], data[10], data[11], data[12], data[13], data[14], + data[15]); + assert(helpers::isUnitaryMatrix(unitary)); + return unitary; } } // namespace mlir::qco::decomposition_test diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_basis_decomposer.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_basis_decomposer.cpp index 637621bc11..811a4ed7e7 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_basis_decomposer.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_basis_decomposer.cpp @@ -10,16 +10,14 @@ #include "decomposition_test_utils.h" #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/GateKind.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/Utils/Matrix.h" #include -#include #include #include @@ -28,7 +26,6 @@ #include #include #include -#include using namespace mlir::qco; using namespace mlir::qco::decomposition; @@ -36,63 +33,67 @@ using namespace mlir::qco::decomposition_test; // NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_P` at global scope class BasisDecomposerTest - : public testing::TestWithParam, Eigen::Matrix4cd (*)()>> { + : public testing::TestWithParam> { public: - [[nodiscard]] static Eigen::Matrix4cd - restore(const TwoQubitGateSequence& sequence) { - Eigen::Matrix4cd matrix = Eigen::Matrix4cd::Identity(); - for (auto&& gate : sequence.gates) { - matrix = getTwoQubitMatrix(gate) * matrix; + /// Reconstruct the 4x4 unitary realized by a native two-qubit decomposition. + /// + /// The factors come in `(r, l)` pairs: `factors[2i]` acts on qubit 1 (LSB) + /// and `factors[2i + 1]` on qubit 0 (MSB), mirroring `emitTwoQubitNative`. + /// Each interior pair is followed by one entangler, with a trailing pair + /// after the last entangler. + [[nodiscard]] static Matrix4x4 + restore(const TwoQubitNativeDecomposition& decomposition, + const Gate& basisGate) { + const Matrix4x4 entangler = getTwoQubitMatrix(basisGate); + const auto& factors = decomposition.singleQubitFactors; + const auto layer = [&](std::size_t i) { + return kron(factors[(2 * i) + 1], factors[2 * i]); + }; + Matrix4x4 matrix = layer(0); + for (std::uint8_t i = 0; i < decomposition.numBasisUses; ++i) { + matrix = entangler * matrix; + matrix = layer(static_cast(i) + 1) * matrix; } - - matrix *= helpers::globalPhaseFactor(sequence.globalPhase); - return matrix; + return matrix * helpers::globalPhaseFactor(decomposition.globalPhase); } protected: void SetUp() override { basisGate = std::get<0>(GetParam()); - eulerBases = std::get<1>(GetParam()); - target = std::get<2>(GetParam())(); + target = std::get<1>(GetParam())(); targetDecomposition = std::make_unique( TwoQubitWeylDecomposition::create(target, std::optional{1.0})); } - Eigen::Matrix4cd target; + Matrix4x4 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); + auto decomposed = + decomposer.twoQubitDecompose(*targetDecomposition, std::nullopt); - ASSERT_TRUE(decomposedSequence.has_value()); + ASSERT_TRUE(decomposed.has_value()); - auto restoredMatrix = restore(*decomposedSequence); + auto restoredMatrix = restore(*decomposed, basisGate); - EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix)) - << "RESULT:\n" - << restoredMatrix << '\n'; + EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix)); } 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); + auto decomposed = + decomposer.twoQubitDecompose(*targetDecomposition, std::nullopt); - ASSERT_TRUE(decomposedSequence.has_value()); + ASSERT_TRUE(decomposed.has_value()); - auto restoredMatrix = restore(*decomposedSequence); + auto restoredMatrix = restore(*decomposed, basisGate); - EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix)) - << "RESULT:\n" - << restoredMatrix << '\n'; + EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix)); } TEST(BasisDecomposerTest, Random) { @@ -102,55 +103,41 @@ TEST(BasisDecomposerTest, Random) { const llvm::SmallVector basisGates{ {.type = GateKind::X, .parameter = {}, .qubitId = {0, 1}}, {.type = GateKind::X, .parameter = {}, .qubitId = {1, 0}}}; - const llvm::SmallVector eulerBases = { - GateEulerBasis::XYX, GateEulerBasis::ZXZ, GateEulerBasis::ZYZ, - GateEulerBasis::XZX}; std::uniform_int_distribution distBasisGate{ 0, basisGates.size() - 1}; - std::uniform_int_distribution distEulerBases{1, - eulerBases.size()}; - - 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 originalMatrix = randomUnitary4x4(rng); auto targetDecomposition = TwoQubitWeylDecomposition::create( originalMatrix, std::optional{1.0}); - auto decomposer = - TwoQubitBasisDecomposer::create(selectRandomBasisGate(), 1.0); - auto decomposedSequence = decomposer.twoQubitDecompose( - targetDecomposition, selectRandomEulerBases(), 1.0, true, std::nullopt); + const auto basisGate = selectRandomBasisGate(); + auto decomposer = TwoQubitBasisDecomposer::create(basisGate, 1.0); + auto decomposed = + decomposer.twoQubitDecompose(targetDecomposition, std::nullopt); - ASSERT_TRUE(decomposedSequence.has_value()); + ASSERT_TRUE(decomposed.has_value()); - auto restoredMatrix = BasisDecomposerTest::restore(*decomposedSequence); + auto restoredMatrix = BasisDecomposerTest::restore(*decomposed, basisGate); - EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix)) - << "ORIGINAL:\n" - << originalMatrix << '\n' - << "RESULT:\n" - << restoredMatrix << '\n'; + // Reconstruction accumulates the Weyl diagonalization residual through up + // to three entangler layers, so allow a correspondingly relaxed tolerance. + EXPECT_TRUE( + restoredMatrix.isApprox(originalMatrix, SANITY_CHECK_PRECISION)); } } TEST(BasisDecomposerNumBasisTest, ForcesZeroBasisUsesForIdentityTarget) { const Gate basis{.type = GateKind::X, .parameter = {}, .qubitId = {0, 1}}; const auto decomposer = TwoQubitBasisDecomposer::create(basis, 1.0); - const Eigen::Matrix4cd target = Eigen::Matrix4cd::Identity(); + const Matrix4x4 target = Matrix4x4::identity(); const auto weyl = TwoQubitWeylDecomposition::create(target, std::optional{1.0}); - const llvm::SmallVector eulerBases{GateEulerBasis::ZYZ}; - const auto decomposed = decomposer.twoQubitDecompose(weyl, eulerBases, 1.0, - false, std::uint8_t{0}); + const auto decomposed = decomposer.twoQubitDecompose(weyl, std::uint8_t{0}); ASSERT_TRUE(decomposed.has_value()); - const Eigen::Matrix4cd restored = BasisDecomposerTest::restore(*decomposed); + EXPECT_EQ(decomposed->numBasisUses, 0); + const Matrix4x4 restored = BasisDecomposerTest::restore(*decomposed, basis); EXPECT_TRUE(restored.isApprox(target)); } @@ -161,22 +148,14 @@ INSTANTIATE_TEST_SUITE_P( testing::Values( Gate{.type = GateKind::X, .parameter = {}, .qubitId = {0, 1}}, Gate{.type = GateKind::X, .parameter = {}, .qubitId = {1, 0}}), - // sets of Euler bases - testing::Values(llvm::SmallVector{GateEulerBasis::ZYZ}, - llvm::SmallVector{ - GateEulerBasis::ZYZ, GateEulerBasis::ZXZ, - GateEulerBasis::XYX, GateEulerBasis::XZX}, - llvm::SmallVector{GateEulerBasis::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)); - }))); + testing::Values([]() -> Matrix4x4 { return Matrix4x4::identity(); }, + []() -> Matrix4x4 { + return kron(rzMatrix(1.0), ryMatrix(3.1)); + }, + []() -> Matrix4x4 { + return kron(Matrix2x2::identity(), rxMatrix(0.1)); + }))); INSTANTIATE_TEST_SUITE_P( TwoQubitMatrices, BasisDecomposerTest, @@ -185,36 +164,27 @@ INSTANTIATE_TEST_SUITE_P( testing::Values( Gate{.type = GateKind::X, .parameter = {}, .qubitId = {0, 1}}, Gate{.type = GateKind::X, .parameter = {}, .qubitId = {1, 0}}), - // sets of Euler bases - testing::Values(llvm::SmallVector{GateEulerBasis::ZYZ}, - llvm::SmallVector{ - GateEulerBasis::ZYZ, GateEulerBasis::ZXZ, - GateEulerBasis::XYX, GateEulerBasis::XZX}, - llvm::SmallVector{GateEulerBasis::XZX, - GateEulerBasis::XYX}), // targets to be decomposed ::testing::Values( - []() -> Eigen::Matrix4cd { return rzzMatrix(2.0); }, - []() -> Eigen::Matrix4cd { + []() -> Matrix4x4 { return rzzMatrix(2.0); }, + []() -> Matrix4x4 { return ryyMatrix(1.0) * rzzMatrix(3.0) * rxxMatrix(2.0); }, - []() -> Eigen::Matrix4cd { + []() -> Matrix4x4 { return TwoQubitWeylDecomposition::getCanonicalMatrix(1.5, -0.2, 0.0) * - Eigen::kroneckerProduct(rxMatrix(1.0), - Eigen::Matrix2cd::Identity()); + kron(rxMatrix(1.0), Matrix2x2::identity()); }, - []() -> Eigen::Matrix4cd { - return Eigen::kroneckerProduct(rxMatrix(1.0), ryMatrix(1.0)) * + []() -> Matrix4x4 { + return kron(rxMatrix(1.0), ryMatrix(1.0)) * TwoQubitWeylDecomposition::getCanonicalMatrix(1.1, 0.2, 3.0) * - Eigen::kroneckerProduct(rxMatrix(1.0), - Eigen::Matrix2cd::Identity()); + kron(rxMatrix(1.0), Matrix2x2::identity()); }, - []() -> Eigen::Matrix4cd { - return Eigen::kroneckerProduct(H_GATE, IPZ) * + []() -> Matrix4x4 { + return kron(hGate(), ipz()) * getTwoQubitMatrix({.type = GateKind::X, .parameter = {}, .qubitId = {0, 1}}) * - Eigen::kroneckerProduct(IPX, IPY); + kron(ipx(), ipy()); }))); diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_helpers.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_helpers.cpp index 283dfeaa8e..7dc67d549f 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_helpers.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_helpers.cpp @@ -10,14 +10,15 @@ #include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" -#include #include #include #include #include +using namespace mlir::qco; using namespace mlir::qco::helpers; using namespace mlir::qco::decomposition; @@ -54,12 +55,11 @@ TEST(DecompositionHelpersTest, GlobalPhaseFactorUnitMagnitude) { } TEST(DecompositionHelpersTest, IsUnitaryMatrixRejectsNonUnitary) { - Eigen::Matrix2cd m; - m << 2.0, 0.0, 0.0, 2.0; + const Matrix2x2 m = Matrix2x2::fromElements(2.0, 0.0, 0.0, 2.0); EXPECT_FALSE(isUnitaryMatrix(m)); } TEST(DecompositionHelpersTest, IsUnitaryMatrixAcceptsUnitary) { - const Eigen::Matrix2cd m = Eigen::Matrix2cd::Identity(); + const Matrix2x2 m = Matrix2x2::identity(); EXPECT_TRUE(isUnitaryMatrix(m)); } diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp index fa9ba1d4ea..67755f2d1f 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -264,6 +264,8 @@ template return countOps(funcOp); case ZSXX: return countZSXXGates(funcOp); + case R: + return countOps(funcOp); } return 0; } @@ -472,6 +474,8 @@ TEST(EulerSynthesisTest, RandomReconstructionAllBases) { return isa(op); case ZSXX: return isa(op); + case R: + return isa(op); } return false; } diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_matrix_euler_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_matrix_euler_decomposition.cpp deleted file mode 100644 index 306fafcfcf..0000000000 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_matrix_euler_decomposition.cpp +++ /dev/null @@ -1,240 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "decomposition_test_utils.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.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 - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace mlir::qco; -using namespace mlir::qco::decomposition; -using namespace mlir::qco::decomposition_test; - -static std::size_t countGatesOfType(const OneQubitGateSequence& seq, - GateKind kind) { - std::size_t count = 0; - for (const auto& gate : seq.gates) { - if (gate.type == kind) { - ++count; - } - } - return count; -} - -/// Compare ``seq.getUnitaryMatrix()`` to ``u`` embedded on qubit 0 (4×4 -/// layout). -static bool sequenceMatchesSingleQubitMatrix(const Eigen::Matrix2cd& u, - const OneQubitGateSequence& seq, - double tol = 1e-10) { - const Eigen::Matrix4cd expanded = expandToTwoQubits(u, 0); - return expanded.isApprox(seq.getUnitaryMatrix(), tol); -} - -// NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_P` at global scope -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; - } - -protected: - void SetUp() override { - eulerBasis = std::get<0>(GetParam()); - originalMatrix = std::get<1>(GetParam())(); - } - - Eigen::Matrix2cd originalMatrix; - GateEulerBasis eulerBasis{}; -}; - -TEST_P(EulerDecompositionTest, TestExact) { - auto decomposition = EulerDecomposition::generateCircuit( - eulerBasis, originalMatrix, false, std::nullopt); - 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{GateEulerBasis::XYX, GateEulerBasis::XZX, - GateEulerBasis::ZYZ, GateEulerBasis::ZXZ}; - std::size_t currentEulerBasis = 0; - for (int i = 0; i < maxIterations; ++i) { - auto originalMatrix = randomUnitaryMatrix(rng); - auto eulerBasis = eulerBases[currentEulerBasis++ % 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'; - } -} - -TEST(EulerDecompositionTest, ZyzAnglesFromUnitaryReconstructHadamard) { - Eigen::Matrix2cd hadamard; - hadamard << 1.0 / std::numbers::sqrt2, 1.0 / std::numbers::sqrt2, - 1.0 / std::numbers::sqrt2, -1.0 / std::numbers::sqrt2; - - const auto angles = - EulerDecomposition::anglesFromUnitary(hadamard, GateEulerBasis::ZYZ); - const Eigen::Matrix2cd reconstructed = - u3Matrix(angles[0], angles[1], angles[2]); - - EXPECT_TRUE(isEquivalentUpToGlobalPhase(hadamard, reconstructed)); -} - -TEST(EulerDecompositionTest, NativeEulerBasesRandomReconstruction) { - std::mt19937 rng(424242); - std::uniform_real_distribution angleDist(-std::numbers::pi, - std::numbers::pi); - for (int i = 0; i < 24; ++i) { - const double theta = angleDist(rng); - const double phi = angleDist(rng); - const double lambda = angleDist(rng); - const double phase = angleDist(rng); - const Eigen::Matrix2cd unitary = - std::exp(std::complex(0.0, phase)) * - u3Matrix(theta, phi, lambda); - const Eigen::Matrix4cd expanded = expandToTwoQubits(unitary, 0); - - const auto u3Seq = EulerDecomposition::generateCircuit( - GateEulerBasis::U3, unitary, true, std::nullopt); - const auto zsxSeq = EulerDecomposition::generateCircuit( - GateEulerBasis::ZSX, unitary, true, std::nullopt); - const auto zsxxSeq = EulerDecomposition::generateCircuit( - GateEulerBasis::ZSXX, unitary, true, std::nullopt); - - EXPECT_TRUE( - isEquivalentUpToGlobalPhase(expanded, u3Seq.getUnitaryMatrix())); - EXPECT_TRUE( - isEquivalentUpToGlobalPhase(expanded, zsxSeq.getUnitaryMatrix())); - EXPECT_TRUE(sequenceMatchesSingleQubitMatrix(unitary, zsxSeq)); - EXPECT_TRUE(sequenceMatchesSingleQubitMatrix(unitary, zsxxSeq)); - - const std::size_t zsxSx = countGatesOfType(zsxSeq, GateKind::SX); - const std::size_t zsxxSx = countGatesOfType(zsxxSeq, GateKind::SX); - const std::size_t zsxxX = countGatesOfType(zsxxSeq, GateKind::X); - EXPECT_EQ(countGatesOfType(zsxSeq, GateKind::X), 0U); - EXPECT_LE(zsxxX, 1U); - if (zsxxX == 0U) { - EXPECT_EQ(zsxSx, zsxxSx); - } else { - EXPECT_EQ(zsxSx, zsxxSx + 2U); - } - } -} - -TEST(EulerDecompositionTest, ZsxxPauliXUsesSingleXGate) { - Eigen::Matrix2cd pauliX; - pauliX << 0.0, 1.0, 1.0, 0.0; - const auto seq = EulerDecomposition::generateCircuit( - GateEulerBasis::ZSXX, pauliX, true, std::nullopt); - EXPECT_EQ(countGatesOfType(seq, GateKind::X), 1U); - EXPECT_EQ(countGatesOfType(seq, GateKind::SX), 0U); - EXPECT_TRUE(sequenceMatchesSingleQubitMatrix(pauliX, seq)); -} - -TEST(EulerDecompositionTest, GetGateTypesForEulerBasis) { - const auto zyz = getGateTypesForEulerBasis(GateEulerBasis::ZYZ); - ASSERT_EQ(zyz.size(), 2U); - EXPECT_EQ(zyz[0], GateKind::RZ); - EXPECT_EQ(zyz[1], GateKind::RY); - - const auto uFamily = getGateTypesForEulerBasis(GateEulerBasis::U321); - ASSERT_EQ(uFamily.size(), 1U); - EXPECT_EQ(uFamily[0], GateKind::U); - - const auto zsxx = getGateTypesForEulerBasis(GateEulerBasis::ZSXX); - ASSERT_EQ(zsxx.size(), 3U); - EXPECT_EQ(zsxx[0], GateKind::RZ); - EXPECT_EQ(zsxx[1], GateKind::SX); - EXPECT_EQ(zsxx[2], GateKind::X); -} - -TEST(EulerDecompositionTest, UAndU321MatchU3Reconstruction) { - std::mt19937 rng(99991); - for (int i = 0; i < 32; ++i) { - const auto u = randomUnitaryMatrix(rng); - const auto seqU3 = EulerDecomposition::generateCircuit( - GateEulerBasis::U3, u, true, std::nullopt); - const auto seqU = EulerDecomposition::generateCircuit(GateEulerBasis::U, u, - true, std::nullopt); - const auto seqU321 = EulerDecomposition::generateCircuit( - GateEulerBasis::U321, u, true, std::nullopt); - EXPECT_TRUE(EulerDecompositionTest::restore(seqU3).isApprox(u)); - EXPECT_TRUE(EulerDecompositionTest::restore(seqU).isApprox(u)); - EXPECT_TRUE(EulerDecompositionTest::restore(seqU321).isApprox(u)); - } -} - -TEST(EulerDecompositionTest, AnglesFromUnitaryXZXReconstructsRx) { - const Eigen::Matrix2cd u = rxMatrix(0.7); - (void)EulerDecomposition::anglesFromUnitary(u, GateEulerBasis::XZX); - const auto seq = EulerDecomposition::generateCircuit(GateEulerBasis::XZX, u, - false, std::nullopt); - EXPECT_TRUE(EulerDecompositionTest::restore(seq).isApprox(u)); -} - -TEST(EulerDecompositionTest, GateSequenceComplexityAndGlobalPhase) { - OneQubitGateSequence seq; - seq.gates.push_back( - {.type = GateKind::RZ, .parameter = {0.2}, .qubitId = {0}}); - seq.globalPhase = 0.5; - EXPECT_TRUE(seq.hasGlobalPhase()); - EXPECT_GE(seq.complexity(), 1U); - seq.globalPhase = 0.0; - EXPECT_FALSE(seq.hasGlobalPhase()); -} - -INSTANTIATE_TEST_SUITE_P( - SingleQubitMatrices, EulerDecompositionTest, - testing::Combine(testing::Values(GateEulerBasis::XYX, GateEulerBasis::XZX, - GateEulerBasis::ZYZ, GateEulerBasis::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 index 6c71ff5502..bec1e1fb23 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -13,23 +13,22 @@ #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/Utils/Matrix.h" #include #include #include #include -#include using namespace mlir::qco; using namespace mlir::qco::decomposition; using namespace mlir::qco::decomposition_test; // NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_P` at global scope -class WeylDecompositionTest - : public testing::TestWithParam { +class WeylDecompositionTest : public testing::TestWithParam { public: - [[nodiscard]] static Eigen::Matrix4cd + [[nodiscard]] static Matrix4x4 restore(const TwoQubitWeylDecomposition& decomposition) { return k1(decomposition) * can(decomposition) * k2(decomposition) * globalPhaseFactor(decomposition); @@ -39,17 +38,17 @@ class WeylDecompositionTest globalPhaseFactor(const TwoQubitWeylDecomposition& decomposition) { return helpers::globalPhaseFactor(decomposition.globalPhase()); } - [[nodiscard]] static Eigen::Matrix4cd + [[nodiscard]] static Matrix4x4 can(const TwoQubitWeylDecomposition& decomposition) { return decomposition.getCanonicalMatrix(); } - [[nodiscard]] static Eigen::Matrix4cd + [[nodiscard]] static Matrix4x4 k1(const TwoQubitWeylDecomposition& decomposition) { - return Eigen::kroneckerProduct(decomposition.k1l(), decomposition.k1r()); + return kron(decomposition.k1l(), decomposition.k1r()); } - [[nodiscard]] static Eigen::Matrix4cd + [[nodiscard]] static Matrix4x4 k2(const TwoQubitWeylDecomposition& decomposition) { - return Eigen::kroneckerProduct(decomposition.k2l(), decomposition.k2r()); + return kron(decomposition.k2l(), decomposition.k2r()); } }; @@ -59,9 +58,7 @@ TEST_P(WeylDecompositionTest, TestExact) { originalMatrix, std::optional{1.0}); auto restoredMatrix = restore(decomposition); - EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix)) - << "RESULT:\n" - << restoredMatrix << '\n'; + EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix)); } TEST_P(WeylDecompositionTest, TestApproximation) { @@ -70,18 +67,15 @@ TEST_P(WeylDecompositionTest, TestApproximation) { originalMatrix, std::optional{1.0 - 1e-12}); auto restoredMatrix = restore(decomposition); - EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix)) - << "RESULT:\n" - << restoredMatrix << '\n'; + EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix)); } TEST(WeylDecompositionStandalone, CnotProducesValidWeylParametersAndUnitaryLocals) { - Eigen::Matrix4cd cnot = Eigen::Matrix4cd::Zero(); - cnot(0, 0) = 1.0; - cnot(1, 1) = 1.0; - cnot(2, 3) = 1.0; - cnot(3, 2) = 1.0; + const Matrix4x4 cnot = Matrix4x4::fromElements(1, 0, 0, 0, // row 0 + 0, 1, 0, 0, // row 1 + 0, 0, 0, 1, // row 2 + 0, 0, 1, 0); const auto decomp = TwoQubitWeylDecomposition::create(cnot, std::nullopt); EXPECT_GE(decomp.a(), -1e-10); @@ -102,63 +96,59 @@ TEST(WeylDecompositionStandalone, Random) { std::mt19937 rng{1234567UL}; for (int i = 0; i < maxIterations; ++i) { - auto originalMatrix = randomUnitaryMatrix(rng); + auto originalMatrix = randomUnitary4x4(rng); auto decomposition = TwoQubitWeylDecomposition::create( originalMatrix, std::optional{1.0 - 1e-12}); auto restoredMatrix = WeylDecompositionTest::restore(decomposition); - EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix)) - << "ORIGINAL:\n" - << originalMatrix << '\n' - << "RESULT:\n" - << restoredMatrix << '\n'; + // The reconstruction accuracy is bounded by the iterative diagonalization + // residual rather than the (much tighter) default matrix tolerance. + EXPECT_TRUE( + restoredMatrix.isApprox(originalMatrix, SANITY_CHECK_PRECISION)); } } INSTANTIATE_TEST_SUITE_P( ProductTwoQubitMatrices, 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)); - })); + ::testing::Values([]() -> Matrix4x4 { return Matrix4x4::identity(); }, + []() -> Matrix4x4 { + return kron(rzMatrix(1.0), ryMatrix(3.1)); + }, + []() -> Matrix4x4 { + return kron(Matrix2x2::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 = GateKind::X, - .parameter = {}, - .qubitId = {0, 1}}) * - Eigen::kroneckerProduct(IPX, IPY); - })); + ::testing::Values([]() -> Matrix4x4 { return rzzMatrix(2.0); }, + []() -> Matrix4x4 { + return ryyMatrix(1.0) * rzzMatrix(3.0) * rxxMatrix(2.0); + }, + []() -> Matrix4x4 { + return TwoQubitWeylDecomposition::getCanonicalMatrix( + 1.5, -0.2, 0.0) * + kron(rxMatrix(1.0), Matrix2x2::identity()); + }, + []() -> Matrix4x4 { + return kron(rxMatrix(1.0), ryMatrix(1.0)) * + TwoQubitWeylDecomposition::getCanonicalMatrix( + 1.1, 0.2, 3.0) * + kron(rxMatrix(1.0), Matrix2x2::identity()); + }, + []() -> Matrix4x4 { + return kron(hGate(), ipz()) * + getTwoQubitMatrix({.type = GateKind::X, + .parameter = {}, + .qubitId = {0, 1}}) * + kron(ipx(), ipy()); + })); INSTANTIATE_TEST_SUITE_P( SpecializedMatrices, WeylDecompositionTest, ::testing::Values( // id + controlled + general already covered by other parametrizations // swap equiv - []() -> Eigen::Matrix4cd { + []() -> Matrix4x4 { return getTwoQubitMatrix({.type = GateKind::X, .parameter = {}, .qubitId = {0, 1}}) * @@ -169,15 +159,15 @@ INSTANTIATE_TEST_SUITE_P( {.type = GateKind::X, .parameter = {}, .qubitId = {0, 1}}); }, // partial swap equiv - []() -> Eigen::Matrix4cd { + []() -> Matrix4x4 { return TwoQubitWeylDecomposition::getCanonicalMatrix(0.5, 0.5, 0.5); }, // partial swap equiv (flipped) - []() -> Eigen::Matrix4cd { + []() -> Matrix4x4 { return TwoQubitWeylDecomposition::getCanonicalMatrix(0.5, 0.5, -0.5); }, // mirror controlled equiv - []() -> Eigen::Matrix4cd { + []() -> Matrix4x4 { return getTwoQubitMatrix({.type = GateKind::X, .parameter = {}, .qubitId = {0, 1}}) * @@ -185,14 +175,14 @@ INSTANTIATE_TEST_SUITE_P( {.type = GateKind::X, .parameter = {}, .qubitId = {1, 0}}); }, // sim aab equiv - []() -> Eigen::Matrix4cd { + []() -> Matrix4x4 { return TwoQubitWeylDecomposition::getCanonicalMatrix(0.5, 0.5, 0.1); }, // sim abb equiv - []() -> Eigen::Matrix4cd { + []() -> Matrix4x4 { return TwoQubitWeylDecomposition::getCanonicalMatrix(0.5, 0.1, 0.1); }, // sim ab-b equiv - []() -> Eigen::Matrix4cd { + []() -> Matrix4x4 { return TwoQubitWeylDecomposition::getCanonicalMatrix(0.5, 0.1, -0.1); })); diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt index 1e04b0f07b..b42643d889 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt @@ -15,8 +15,7 @@ add_executable( test_native_synthesis_pass_custom_menus.cpp test_native_synthesis_pass_fusion.cpp test_native_synthesis_pass_multi_qubit.cpp - test_native_synthesis_pass_profiles.cpp - test_native_synthesis_pass_scoring.cpp) + test_native_synthesis_pass_profiles.cpp) target_link_libraries( ${target_name} diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_pass_test_fixture.h b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_pass_test_fixture.h index b8e1a4124c..8eba08db14 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_pass_test_fixture.h +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_pass_test_fixture.h @@ -205,18 +205,12 @@ class NativeSynthesisPassTest : public testing::Test { } static void runNativeSynthesis(mlir::OwningOpRef& moduleOp, - const std::string& nativeGates, - const double scoreWeightTwoQ = 1.0, - const double scoreWeightOneQ = 0.1, - const double scoreWeightDepth = 0.01) { + const std::string& nativeGates) { mlir::PassManager pm(moduleOp->getContext()); pm.addPass(mlir::createQCToQCO()); pm.addPass(mlir::qco::createNativeGateSynthesisPass( mlir::qco::NativeGateSynthesisOptions{ .nativeGates = nativeGates, - .scoreWeightTwoQ = scoreWeightTwoQ, - .scoreWeightOneQ = scoreWeightOneQ, - .scoreWeightDepth = scoreWeightDepth, })); ASSERT_TRUE(mlir::succeeded(pm.run(*moduleOp))); } @@ -238,48 +232,36 @@ class NativeSynthesisPassTest : public testing::Test { template void expectNativeAfterSynthesis(BuildFn buildFn, const std::string& nativeGates, - PredicateFn isNative, - const double scoreWeightTwoQ = 1.0, - const double scoreWeightOneQ = 0.1, - const double scoreWeightDepth = 0.01) { + PredicateFn isNative) { auto moduleOp = buildFn(); - runNativeSynthesis(moduleOp, nativeGates, scoreWeightTwoQ, scoreWeightOneQ, - scoreWeightDepth); + runNativeSynthesis(moduleOp, nativeGates); EXPECT_TRUE(isNative(moduleOp)); } template - void expectSynthesisFailure(BuildFn buildFn, const std::string& nativeGates, - const double scoreWeightTwoQ = 1.0, - const double scoreWeightOneQ = 0.1, - const double scoreWeightDepth = 0.01) { + void expectSynthesisFailure(BuildFn buildFn, const std::string& nativeGates) { auto moduleOp = buildFn(); mlir::PassManager pm(moduleOp->getContext()); pm.addPass(mlir::createQCToQCO()); pm.addPass(mlir::qco::createNativeGateSynthesisPass( mlir::qco::NativeGateSynthesisOptions{ .nativeGates = nativeGates, - .scoreWeightTwoQ = scoreWeightTwoQ, - .scoreWeightOneQ = scoreWeightOneQ, - .scoreWeightDepth = scoreWeightDepth, })); EXPECT_TRUE(mlir::failed(pm.run(*moduleOp))); } template - void expectEquivalentAndNativeAfterSynthesis( - BuildFn buildFn, const std::string& nativeGates, PredicateFn isNative, - UnitaryFn computeUnitary, const double scoreWeightTwoQ = 1.0, - const double scoreWeightOneQ = 0.1, - const double scoreWeightDepth = 0.01) { + void expectEquivalentAndNativeAfterSynthesis(BuildFn buildFn, + const std::string& nativeGates, + PredicateFn isNative, + UnitaryFn computeUnitary) { auto expectedModule = buildFn(); runQcToQco(expectedModule); const auto expectedUnitary = computeUnitary(expectedModule); ASSERT_TRUE(expectedUnitary.has_value()); auto synthesizedModule = buildFn(); - runNativeSynthesis(synthesizedModule, nativeGates, scoreWeightTwoQ, - scoreWeightOneQ, scoreWeightDepth); + runNativeSynthesis(synthesizedModule, nativeGates); EXPECT_TRUE(isNative(synthesizedModule)); const auto synthesizedUnitary = computeUnitary(synthesizedModule); ASSERT_TRUE(synthesizedUnitary.has_value()); diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp index 9d8c12779b..d04cc600f6 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp @@ -13,11 +13,12 @@ #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Gate.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" #include +#include #include #include #include @@ -35,19 +36,93 @@ using namespace mlir; -namespace { +namespace mlir::qco::native_synth_test { -Eigen::Matrix2cd matrixToEigen(const qco::Matrix2x2& matrix) { - return qco::native_synth::toEigen(matrix); +TestMatrix TestMatrix::identity(std::size_t dim) { + TestMatrix result(dim); + for (std::size_t i = 0; i < dim; ++i) { + result(i, i) = std::complex{1.0, 0.0}; + } + return result; +} + +TestMatrix TestMatrix::fromMatrix2x2(const Matrix2x2& matrix) { + TestMatrix result(2); + for (std::size_t row = 0; row < 2; ++row) { + for (std::size_t col = 0; col < 2; ++col) { + result(row, col) = matrix(row, col); + } + } + return result; } -Eigen::Matrix4cd matrixToEigen(const qco::Matrix4x4& matrix) { - return qco::native_synth::toEigen(matrix); +TestMatrix TestMatrix::fromMatrix4x4(const Matrix4x4& matrix) { + TestMatrix result(4); + for (std::size_t row = 0; row < 4; ++row) { + for (std::size_t col = 0; col < 4; ++col) { + result(row, col) = matrix(row, col); + } + } + return result; } -} // namespace +TestMatrix TestMatrix::operator*(const TestMatrix& rhs) const { + TestMatrix result(dim_); + for (std::size_t row = 0; row < dim_; ++row) { + for (std::size_t k = 0; k < dim_; ++k) { + const std::complex a = (*this)(row, k); + if (a == std::complex{0.0, 0.0}) { + continue; + } + for (std::size_t col = 0; col < dim_; ++col) { + result(row, col) += a * rhs(k, col); + } + } + } + return result; +} -namespace mlir::qco::native_synth_test { +TestMatrix TestMatrix::operator*(std::complex scalar) const { + TestMatrix result(dim_); + for (std::size_t row = 0; row < dim_; ++row) { + for (std::size_t col = 0; col < dim_; ++col) { + result(row, col) = (*this)(row, col) * scalar; + } + } + return result; +} + +TestMatrix TestMatrix::adjoint() const { + TestMatrix result(dim_); + for (std::size_t row = 0; row < dim_; ++row) { + for (std::size_t col = 0; col < dim_; ++col) { + result(col, row) = std::conj((*this)(row, col)); + } + } + return result; +} + +std::complex TestMatrix::trace() const { + std::complex sum{0.0, 0.0}; + for (std::size_t i = 0; i < dim_; ++i) { + sum += (*this)(i, i); + } + return sum; +} + +bool TestMatrix::isApprox(const TestMatrix& other, double tol) const { + if (dim_ != other.dim_) { + return false; + } + for (std::size_t row = 0; row < dim_; ++row) { + for (std::size_t col = 0; col < dim_; ++col) { + if (std::abs((*this)(row, col) - other(row, col)) > tol) { + return false; + } + } + } + return true; +} [[nodiscard]] static std::optional getUnitaryQubitOperand(qco::UnitaryOpInterface op, std::size_t index) { @@ -79,14 +154,13 @@ std::complex phasedAmplitude(const double magnitude, std::exp(std::complex(0.0, phase)); } -Eigen::Matrix2cd u3Matrix(double theta, double phi, double lambda) { +Matrix2x2 u3Matrix(double theta, double phi, double lambda) { return decomposition::uMatrix(theta, phi, lambda); } -bool isUnitary(const Eigen::Matrix2cd& m, const double atol) { - const auto identity = Eigen::Matrix2cd::Identity(); - return (m * m.adjoint()).isApprox(identity, atol) && - (m.adjoint() * m).isApprox(identity, atol); +bool isUnitary(const Matrix2x2& matrix, const double atol) { + return (matrix * matrix.adjoint()).isIdentity(atol) && + (matrix.adjoint() * matrix).isIdentity(atol); } std::optional evaluateConstF64(Value value) { @@ -141,8 +215,7 @@ std::optional evaluateConstF64(Value value) { } /// Extract the 2x2 unitary matrix associated with a single-qubit op. -bool extractSingleQubitMatrix(qco::UnitaryOpInterface op, - Eigen::Matrix2cd& out) { +bool extractSingleQubitMatrix(qco::UnitaryOpInterface op, Matrix2x2& out) { if (llvm::isa(op.getOperation())) { auto* raw = op.getOperation(); if (raw->getNumOperands() < 2) { @@ -220,11 +293,11 @@ bool extractSingleQubitMatrix(qco::UnitaryOpInterface op, phasedAmplitude(thetaSin, -*phi - (std::numbers::pi / 2.0)); const auto m10 = phasedAmplitude(thetaSin, *phi - (std::numbers::pi / 2.0)); const std::complex thetaCos = std::cos(*theta / 2.0); - out = Eigen::Matrix2cd{{thetaCos, m01}, {m10, thetaCos}}; + out = Matrix2x2::fromElements(thetaCos, m01, m10, thetaCos); return true; } - if (qco::Matrix2x2 raw; op.getUnitaryMatrix2x2(raw)) { - out = matrixToEigen(raw); + if (Matrix2x2 raw; op.getUnitaryMatrix2x2(raw)) { + out = raw; return true; } qco::DynamicMatrix dynamic; @@ -232,35 +305,32 @@ bool extractSingleQubitMatrix(qco::UnitaryOpInterface op, dynamic.cols() != 2) { return false; } - for (std::size_t row = 0; row < 2; ++row) { - for (std::size_t col = 0; col < 2; ++col) { - out(static_cast(row), static_cast(col)) = - dynamic(row, col); - } - } + out = Matrix2x2::fromElements(dynamic(0, 0), dynamic(0, 1), dynamic(1, 0), + dynamic(1, 1)); return true; } /// 4×4 unitary for a two-qubit op (same layout as ``getUnitaryMatrix4x4``). -bool extractTwoQubitMatrix(qco::UnitaryOpInterface op, Eigen::Matrix4cd& out) { +bool extractTwoQubitMatrix(qco::UnitaryOpInterface op, Matrix4x4& out) { if (auto ctrl = llvm::dyn_cast(op.getOperation())) { if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { return false; } auto* body = ctrl.getBodyUnitary(0).getOperation(); if (llvm::isa(body)) { - out = Eigen::Matrix4cd::Identity(); + out = Matrix4x4::identity(); out(3, 3) = -1.0; return true; } if (llvm::isa(body)) { - out << 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0; + out = Matrix4x4::fromElements(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, + 0); return true; } return false; } - if (qco::Matrix4x4 raw; op.getUnitaryMatrix4x4(raw)) { - out = matrixToEigen(raw); + if (Matrix4x4 raw; op.getUnitaryMatrix4x4(raw)) { + out = raw; return true; } qco::DynamicMatrix dynamic; @@ -270,20 +340,20 @@ bool extractTwoQubitMatrix(qco::UnitaryOpInterface op, Eigen::Matrix4cd& out) { } for (std::size_t row = 0; row < 4; ++row) { for (std::size_t col = 0; col < 4; ++col) { - out(static_cast(row), static_cast(col)) = - dynamic(row, col); + out(row, col) = dynamic(static_cast(row), + static_cast(col)); } } return true; } -std::optional +std::optional computeTwoQubitUnitaryFromModule(const OwningOpRef& moduleOp) { ModuleOp module = moduleOp.get(); if (!module) { return std::nullopt; } - Eigen::Matrix4cd unitary = Eigen::Matrix4cd::Identity(); + Matrix4x4 unitary = Matrix4x4::identity(); llvm::DenseMap qubitIds; std::size_t nextQubitId = 0; @@ -328,11 +398,13 @@ computeTwoQubitUnitaryFromModule(const OwningOpRef& moduleOp) { if (!qid) { return std::nullopt; } - Eigen::Matrix2cd oneQ; + Matrix2x2 oneQ; if (!extractSingleQubitMatrix(op, oneQ)) { return std::nullopt; } - unitary = qco::decomposition::expandToTwoQubits(oneQ, *qid) * unitary; + unitary = decomposition::expandToTwoQubits( + oneQ, static_cast(*qid)) * + unitary; const auto qOut = getUnitaryQubitResult(op, 0); if (!qOut) { return std::nullopt; @@ -352,12 +424,17 @@ computeTwoQubitUnitaryFromModule(const OwningOpRef& moduleOp) { if (!q0id || !q1id) { return std::nullopt; } - Eigen::Matrix4cd twoQ; + Matrix4x4 twoQ; if (!extractTwoQubitMatrix(op, twoQ)) { return std::nullopt; } + // Reorder the gate's (operand0, operand1) layout into the canonical + // (qubit 0, qubit 1) order used by `unitary`. + const llvm::SmallVector ids{ + static_cast(*q0id), + static_cast(*q1id)}; unitary = - expandTwoQToN(twoQ, *q0id, *q1id, /*numQubits=*/2) * unitary; + decomposition::fixTwoQubitMatrixQubitOrder(twoQ, ids) * unitary; const auto q0Out = getUnitaryQubitResult(op, 0); const auto q1Out = getUnitaryQubitResult(op, 1); if (!q0Out || !q1Out) { @@ -377,51 +454,46 @@ computeTwoQubitUnitaryFromModule(const OwningOpRef& moduleOp) { return unitary; } -/// Kronecker-embed ``m`` on wire ``q`` into a ``2^N``-dim unitary (same index -/// bit order as QCO 4×4 matrices: wire 0 is the high bit). -Eigen::MatrixXcd expandOneQToN(const Eigen::Matrix2cd& m, std::size_t q, - std::size_t numQubits) { - const auto dim = static_cast(1ULL << numQubits); - Eigen::MatrixXcd full = Eigen::MatrixXcd::Zero(dim, dim); +/// Kronecker-embed ``matrix`` on wire ``q`` into a ``2^N``-dim unitary (same +/// index bit order as QCO 4×4 matrices: wire 0 is the high bit). +TestMatrix expandOneQToN(const Matrix2x2& matrix, std::size_t q, + std::size_t numQubits) { + const std::size_t dim = 1ULL << numQubits; + TestMatrix full(dim); const auto bit = numQubits - 1 - q; const std::size_t mask = 1ULL << bit; - for (Eigen::Index col = 0; col < dim; ++col) { - const auto colIdx = static_cast(col); - const std::size_t sIn = (colIdx >> bit) & 1ULL; - const std::size_t rest = colIdx & ~mask; + for (std::size_t col = 0; col < dim; ++col) { + const std::size_t sIn = (col >> bit) & 1ULL; + const std::size_t rest = col & ~mask; for (std::size_t sOut = 0; sOut < 2; ++sOut) { - const auto row = static_cast(rest | (sOut << bit)); - full(row, col) = - m(static_cast(sOut), static_cast(sIn)); + const std::size_t row = rest | (sOut << bit); + full(row, col) = matrix(sOut, sIn); } } return full; } -/// Embed ``m`` on wires ``q0``, ``q1`` into a ``2^N``-dim unitary. -Eigen::MatrixXcd expandTwoQToN(const Eigen::Matrix4cd& m, std::size_t q0, - std::size_t q1, std::size_t numQubits) { - const auto dim = static_cast(1ULL << numQubits); - Eigen::MatrixXcd full = Eigen::MatrixXcd::Zero(dim, dim); +/// Embed ``matrix`` on wires ``q0``, ``q1`` into a ``2^N``-dim unitary. +TestMatrix expandTwoQToN(const Matrix4x4& matrix, std::size_t q0, + std::size_t q1, std::size_t numQubits) { + const std::size_t dim = 1ULL << numQubits; + TestMatrix full(dim); const auto bit0 = numQubits - 1 - q0; const auto bit1 = numQubits - 1 - q1; const std::size_t mask0 = 1ULL << bit0; const std::size_t mask1 = 1ULL << bit1; const std::size_t maskBoth = mask0 | mask1; - for (Eigen::Index col = 0; col < dim; ++col) { - const auto colIdx = static_cast(col); - const std::size_t s0In = (colIdx >> bit0) & 1ULL; - const std::size_t s1In = (colIdx >> bit1) & 1ULL; + for (std::size_t col = 0; col < dim; ++col) { + const std::size_t s0In = (col >> bit0) & 1ULL; + const std::size_t s1In = (col >> bit1) & 1ULL; // 2-bit index for the pair matches QCO 4×4 row/column layout. const std::size_t smallIn = (s0In << 1) | s1In; - const std::size_t rest = colIdx & ~maskBoth; + const std::size_t rest = col & ~maskBoth; for (std::size_t smallOut = 0; smallOut < 4; ++smallOut) { const std::size_t s0Out = (smallOut >> 1) & 1ULL; const std::size_t s1Out = smallOut & 1ULL; - const auto row = - static_cast(rest | (s0Out << bit0) | (s1Out << bit1)); - full(row, col) = m(static_cast(smallOut), - static_cast(smallIn)); + const std::size_t row = rest | (s0Out << bit0) | (s1Out << bit1); + full(row, col) = matrix(smallOut, smallIn); } } return full; @@ -430,7 +502,7 @@ Eigen::MatrixXcd expandTwoQToN(const Eigen::Matrix4cd& m, std::size_t q0, /// Full ``2^N`` unitary from a QCO module (``alloc`` / ``static``, 1q/2q /// unitaries, ``ctrl`` with X/Z body). ``std::nullopt`` on unsupported ops or /// if ``N`` exceeds ``maxQubits``. -std::optional +std::optional computeNQubitUnitaryFromModule(const OwningOpRef& moduleOp, std::size_t maxQubits) { ModuleOp module = moduleOp.get(); @@ -465,8 +537,7 @@ computeNQubitUnitaryFromModule(const OwningOpRef& moduleOp, return std::nullopt; } - const auto dim = static_cast(1ULL << numQubits); - Eigen::MatrixXcd unitary = Eigen::MatrixXcd::Identity(dim, dim); + TestMatrix unitary = TestMatrix::identity(1ULL << numQubits); auto getQubitId = [&](Value qubit) -> std::optional { auto it = qubitIds.find(qubit); @@ -496,7 +567,7 @@ computeNQubitUnitaryFromModule(const OwningOpRef& moduleOp, if (!qid) { return std::nullopt; } - Eigen::Matrix2cd oneQ; + Matrix2x2 oneQ; if (!extractSingleQubitMatrix(op, oneQ)) { return std::nullopt; } @@ -520,7 +591,7 @@ computeNQubitUnitaryFromModule(const OwningOpRef& moduleOp, if (!q0id || !q1id) { return std::nullopt; } - Eigen::Matrix4cd twoQ; + Matrix4x4 twoQ; if (!extractTwoQubitMatrix(op, twoQ)) { return std::nullopt; } diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.h b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.h index 6cda461ac8..3a1b58c58c 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.h +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.h @@ -12,8 +12,8 @@ #include "TestCaseUtils.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" -#include #include #include #include @@ -21,28 +21,80 @@ #include #include #include +#include namespace mlir::qco::native_synth_test { using mqt::test::isEquivalentUpToGlobalPhase; +/// Minimal dense, row-major, square complex matrix with runtime dimension. +/// +/// Used by the multi-qubit equivalence checks (the synthesized circuits may +/// span more than two wires, so the fixed-size `Matrix2x2`/`Matrix4x4` are not +/// enough). Provides exactly the surface +/// `mqt::test::isEquivalentUpToGlobalPhase` needs: `adjoint()`, `operator*`, +/// scalar multiply, `trace()`, and `isApprox()`. +class TestMatrix { +public: + TestMatrix() = default; + explicit TestMatrix(std::size_t dim) + : dim_(dim), data_(dim * dim, std::complex{0.0, 0.0}) {} + + /// Identity matrix of dimension @p dim. + [[nodiscard]] static TestMatrix identity(std::size_t dim); + /// Promote a fixed `2×2` matrix to a `TestMatrix`. + [[nodiscard]] static TestMatrix fromMatrix2x2(const Matrix2x2& matrix); + /// Promote a fixed `4×4` matrix to a `TestMatrix`. + [[nodiscard]] static TestMatrix fromMatrix4x4(const Matrix4x4& matrix); + + [[nodiscard]] std::size_t dim() const { return dim_; } + + [[nodiscard]] std::complex& operator()(std::size_t row, + std::size_t col) { + return data_[(row * dim_) + col]; + } + [[nodiscard]] std::complex operator()(std::size_t row, + std::size_t col) const { + return data_[(row * dim_) + col]; + } + + /// Matrix product (dimensions must match). + [[nodiscard]] TestMatrix operator*(const TestMatrix& rhs) const; + /// Element-wise scaling by a complex scalar. + [[nodiscard]] TestMatrix operator*(std::complex scalar) const; + /// Conjugate transpose. + [[nodiscard]] TestMatrix adjoint() const; + /// Sum of diagonal entries. + [[nodiscard]] std::complex trace() const; + /// Entry-wise approximate equality (false on dimension mismatch). + [[nodiscard]] bool isApprox(const TestMatrix& other, + double tol = 1e-10) const; + +private: + std::size_t dim_ = 0; + std::vector> data_; +}; + +/// Left scalar multiply, mirroring the right multiply above. +[[nodiscard]] inline TestMatrix operator*(std::complex scalar, + const TestMatrix& matrix) { + return matrix * scalar; +} + [[nodiscard]] std::complex phasedAmplitude(double magnitude, double phase); -[[nodiscard]] Eigen::Matrix2cd u3Matrix(double theta, double phi, - double lambda); -[[nodiscard]] bool isUnitary(const Eigen::Matrix2cd& m, double atol = 1e-10); +[[nodiscard]] Matrix2x2 u3Matrix(double theta, double phi, double lambda); +[[nodiscard]] bool isUnitary(const Matrix2x2& matrix, double atol = 1e-10); [[nodiscard]] std::optional evaluateConstF64(Value value); -bool extractSingleQubitMatrix(qco::UnitaryOpInterface op, - Eigen::Matrix2cd& out); -bool extractTwoQubitMatrix(qco::UnitaryOpInterface op, Eigen::Matrix4cd& out); -[[nodiscard]] std::optional +bool extractSingleQubitMatrix(qco::UnitaryOpInterface op, Matrix2x2& out); +bool extractTwoQubitMatrix(qco::UnitaryOpInterface op, Matrix4x4& out); +[[nodiscard]] std::optional computeTwoQubitUnitaryFromModule(const OwningOpRef& moduleOp); -[[nodiscard]] Eigen::MatrixXcd -expandOneQToN(const Eigen::Matrix2cd& m, std::size_t q, std::size_t numQubits); -[[nodiscard]] Eigen::MatrixXcd expandTwoQToN(const Eigen::Matrix4cd& m, - std::size_t q0, std::size_t q1, - std::size_t numQubits); -[[nodiscard]] std::optional +[[nodiscard]] TestMatrix expandOneQToN(const Matrix2x2& matrix, std::size_t q, + std::size_t numQubits); +[[nodiscard]] TestMatrix expandTwoQToN(const Matrix4x4& matrix, std::size_t q0, + std::size_t q1, std::size_t numQubits); +[[nodiscard]] std::optional computeNQubitUnitaryFromModule(const OwningOpRef& moduleOp, std::size_t maxQubits = 6); diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_policy.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_policy.cpp index 079e56677c..29c195fa17 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_policy.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_policy.cpp @@ -12,8 +12,6 @@ #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" @@ -34,20 +32,6 @@ using namespace mlir::qco; using namespace mlir::qco::decomposition; using namespace mlir::qco::native_synth; -TEST(NativePolicyTest, ComputeGateSequenceMetricsDepth) { - QubitGateSequence seq; - seq.gates.push_back( - {.type = GateKind::RZ, .parameter = {0.1}, .qubitId = {0}}); - seq.gates.push_back( - {.type = GateKind::RZ, .parameter = {0.2}, .qubitId = {0}}); - seq.gates.push_back( - {.type = GateKind::RZZ, .parameter = {0.3}, .qubitId = {0, 1}}); - const CandidateMetrics m = computeGateSequenceMetrics(seq); - EXPECT_EQ(m.numOneQ, 2U); - EXPECT_EQ(m.numTwoQ, 1U); - EXPECT_EQ(m.depth, 3U); -} - TEST(NativePolicyTest, UsesCxAndCzFromResolvedSpec) { const auto cxOnly = resolveNativeGatesSpec("u,cx"); ASSERT_TRUE(cxOnly); diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_spec.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_spec.cpp index 129ea4b961..1ccaf6c29a 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_spec.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_spec.cpp @@ -8,7 +8,7 @@ * Licensed under the MIT License */ -#include "mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" @@ -44,18 +44,28 @@ TEST(NativeSpecTest, PhaseAliasPMatchesRzInIbmStyleMenu) { EXPECT_EQ(pMenu->allowedGates, rzMenu->allowedGates); } -TEST(NativeSpecTest, GetEulerBasesForAxisPair) { - const auto rxRz = getEulerBasesForAxisPair(AxisPair::RxRz); - ASSERT_EQ(rxRz.size(), 1U); - EXPECT_EQ(rxRz[0], GateEulerBasis::XZX); - - const auto rxRy = getEulerBasesForAxisPair(AxisPair::RxRy); - ASSERT_EQ(rxRy.size(), 1U); - EXPECT_EQ(rxRy[0], GateEulerBasis::XYX); +TEST(NativeSpecTest, EmitterEulerBasisForAxisPair) { + EXPECT_EQ(emitterEulerBasis(SingleQubitEmitterSpec{ + .mode = SingleQubitMode::AxisPair, .axisPair = AxisPair::RxRz}), + EulerBasis::XZX); + EXPECT_EQ(emitterEulerBasis(SingleQubitEmitterSpec{ + .mode = SingleQubitMode::AxisPair, .axisPair = AxisPair::RxRy}), + EulerBasis::XYX); + EXPECT_EQ(emitterEulerBasis(SingleQubitEmitterSpec{ + .mode = SingleQubitMode::AxisPair, .axisPair = AxisPair::RyRz}), + EulerBasis::ZYZ); +} - const auto ryRz = getEulerBasesForAxisPair(AxisPair::RyRz); - ASSERT_EQ(ryRz.size(), 1U); - EXPECT_EQ(ryRz[0], GateEulerBasis::ZYZ); +TEST(NativeSpecTest, EmitterEulerBasisForPrimaryModes) { + EXPECT_EQ( + emitterEulerBasis(SingleQubitEmitterSpec{.mode = SingleQubitMode::U3}), + EulerBasis::U); + EXPECT_EQ( + emitterEulerBasis(SingleQubitEmitterSpec{.mode = SingleQubitMode::ZSXX}), + EulerBasis::ZSXX); + EXPECT_EQ( + emitterEulerBasis(SingleQubitEmitterSpec{.mode = SingleQubitMode::R}), + EulerBasis::R); } TEST(NativeSpecTest, RzzSetsAllowRzzFlag) { diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_custom_menus.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_custom_menus.cpp index ddb51bb545..b38be63ede 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_custom_menus.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_custom_menus.cpp @@ -458,9 +458,6 @@ TEST_F(NativeSynthesisPassTest, RandomizedCustomMenusAndCircuitsAreEquivalent) { pm.addPass( qco::createNativeGateSynthesisPass(qco::NativeGateSynthesisOptions{ .nativeGates = menuCsv, - .scoreWeightTwoQ = 1.0, - .scoreWeightOneQ = 0.1, - .scoreWeightDepth = 0.01, })); if (failed(pm.run(*synthesized))) { ADD_FAILURE() << "Native synthesis failed for menu=" << menuCsv diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_fusion.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_fusion.cpp index 0cee7aa2da..637537204f 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_fusion.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_fusion.cpp @@ -86,12 +86,17 @@ TEST_P(NativeSynthesisOneQFusionU3GPhaseTest, FusesAdjacentNativeUChain) { INSTANTIATE_TEST_SUITE_P( OneQRunMergingU3GPhaseMatrix, NativeSynthesisOneQFusionU3GPhaseTest, - testing::Values(OneQU3FusionGPhaseRow{"EmitsGlobalPhaseOnU3", + // `T * S = diag(1, e^{i*3pi/4})` is captured exactly by `U(0, 0, 3pi/4)`, + // so no residual `gphase` is needed. A generic `SU(2)` run (two det-1 `U` + // gates) cannot be written as a single `U` gate without a residual phase, + // because `U(theta, phi, lambda)` has determinant `e^{i*(phi + lambda)}`; + // the leftover `-(phi + lambda) / 2` global phase is emitted as `gphase`. + testing::Values(OneQU3FusionGPhaseRow{"OmitsGPhaseWhenU3IsExact", mlir::qc::nativeSynthFusionTS, - /*expectGPhaseCount=*/1U}, - OneQU3FusionGPhaseRow{"OmitsGPhaseWhenResidualIsTrivial", + /*expectGPhaseCount=*/0U}, + OneQU3FusionGPhaseRow{"EmitsGlobalPhaseForSu2ViaU3", mlir::qc::nativeSynthFusionUUTwoQDet1, - /*expectGPhaseCount=*/0U}), + /*expectGPhaseCount=*/1U}), [](const testing::TestParamInfo& info) { return info.param.name; }); @@ -130,9 +135,8 @@ TEST_P(NativeSynthesisTwoQBlockEquivGenericU3CxTest, TEST(NativeSynthesisFusionTest, IsEquivalentUpToGlobalPhaseRejectsNearZeroOverlap) { - const Eigen::Matrix2cd lhs = Eigen::Matrix2cd::Identity(); - const Eigen::Matrix2cd rhs = - (Eigen::Matrix2cd() << 1.0, 0.0, 0.0, -1.0).finished(); + const Matrix2x2 lhs = Matrix2x2::identity(); + const Matrix2x2 rhs = Matrix2x2::fromElements(1.0, 0.0, 0.0, -1.0); // overlap = trace(rhs^H * lhs) = trace(Z) = 0 -> early false branch. EXPECT_FALSE(isEquivalentUpToGlobalPhase(lhs, rhs, 1e-10)); } diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_profiles.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_profiles.cpp index 3ee930d447..395f62376e 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_profiles.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_profiles.cpp @@ -366,14 +366,6 @@ TEST_F(NativeSynthesisPassTest, FailsForNativeGateMenuWithoutSingleQEmitter) { "cx,cz"); } -TEST_F(NativeSynthesisPassTest, FailsForNegativeScoreWeight) { - expectSynthesisFailure( - [&] { - return mlir::qc::QCProgramBuilder::build(context.get(), mlir::qc::h); - }, - "u,cx", -1.0, 0.1, 0.01); -} - TEST_F(NativeSynthesisPassTest, CandidateSelectionIsDeterministicAcrossRuns) { auto buildFn = [&] { return mlir::qc::QCProgramBuilder::build( @@ -389,22 +381,19 @@ TEST_F(NativeSynthesisPassTest, CandidateSelectionIsDeterministicAcrossRuns) { } TEST_F(NativeSynthesisPassTest, - RichCustomMenuSelectionRemainsDeterministicAcrossWeightsAndRuns) { + RichCustomMenuSelectionRemainsDeterministicAcrossRuns) { auto buildFn = [&] { return mlir::qc::QCProgramBuilder::build( context.get(), mlir::qc::nativeSynthDeterminismTwoQubitSwap); }; auto firstModule = buildFn(); - runNativeSynthesis(firstModule, "u,rx,rz,cx,cz", 1.0, 0.1, 0.01); + runNativeSynthesis(firstModule, "u,rx,rz,cx,cz"); auto secondModule = buildFn(); - runNativeSynthesis(secondModule, "u,rx,rz,cx,cz", 1.0, 0.1, 0.01); + runNativeSynthesis(secondModule, "u,rx,rz,cx,cz"); EXPECT_EQ(moduleToString(firstModule), moduleToString(secondModule)); - - auto alternateWeightsModule = buildFn(); - runNativeSynthesis(alternateWeightsModule, "u,rx,rz,cx,cz", 3.0, 0.5, 0.0); - EXPECT_TRUE(onlyUOrAxisPairRxRzCxOps(alternateWeightsModule) || - onlyGenericU3CxOrCzOps(alternateWeightsModule)); + EXPECT_TRUE(onlyUOrAxisPairRxRzCxOps(firstModule) || + onlyGenericU3CxOrCzOps(firstModule)); } TEST_F(NativeSynthesisPassTest, FailsForMultiControlledGateStructure) { diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_scoring.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_scoring.cpp deleted file mode 100644 index 2c5fe9aa3e..0000000000 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_scoring.cpp +++ /dev/null @@ -1,289 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -// Scoring helpers for native-gate synthesis plus ``XXPlusYY`` / ``XXMinusYY`` -// rewrite metric checks. - -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Scoring.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" -#include "native_synthesis_pass_test_fixture.h" -#include "qc_programs.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -using namespace mlir; -using namespace mlir::qco; -using namespace mlir::qco::native_synth_test; - -namespace { - -/// Dummy payload: scoring helpers do not inspect the type. -struct ScoringTag {}; - -} // namespace - -static std::pair -countSingleAndTwoQubitUnitariesForXxRzzMetrics(ModuleOp module) { - unsigned numOneQ = 0; - unsigned numTwoQ = 0; - module.walk([&](Operation* op) { - if (llvm::isa(op)) { - return; - } - if (llvm::isa_and_present(op->getParentOp())) { - return; - } - auto unitary = llvm::dyn_cast(op); - if (!unitary) { - return; - } - if (unitary.isSingleQubit()) { - ++numOneQ; - return; - } - if (unitary.isTwoQubit()) { - ++numTwoQ; - } - }); - return {numOneQ, numTwoQ}; -} - -TEST(NativeSynthesisScoringTest, ValidScoreWeights) { - using namespace mlir::qco::native_synth; - EXPECT_TRUE(areValidScoreWeights(ScoreWeights{})); - EXPECT_TRUE(areValidScoreWeights( - ScoreWeights{.twoQ = 0.0, .oneQ = 0.0, .depth = 0.0})); - EXPECT_TRUE(areValidScoreWeights( - ScoreWeights{.twoQ = 5.0, .oneQ = 2.5, .depth = 0.1})); - - EXPECT_FALSE(areValidScoreWeights(ScoreWeights{.twoQ = -1.0})); - EXPECT_FALSE(areValidScoreWeights(ScoreWeights{.oneQ = -0.1})); - EXPECT_FALSE(areValidScoreWeights(ScoreWeights{.depth = -0.01})); - - const double inf = std::numeric_limits::infinity(); - const double nan = std::numeric_limits::quiet_NaN(); - EXPECT_FALSE(areValidScoreWeights(ScoreWeights{.twoQ = inf})); - EXPECT_FALSE(areValidScoreWeights(ScoreWeights{.oneQ = inf})); - EXPECT_FALSE(areValidScoreWeights(ScoreWeights{.depth = inf})); - EXPECT_FALSE(areValidScoreWeights(ScoreWeights{.twoQ = nan})); - EXPECT_FALSE(areValidScoreWeights(ScoreWeights{.oneQ = nan})); - EXPECT_FALSE(areValidScoreWeights(ScoreWeights{.depth = nan})); -} - -TEST(NativeSynthesisScoringTest, ScoreCandidateAppliesWeightsLinearly) { - using namespace mlir::qco::native_synth; - SynthesisCandidate candidate; - candidate.metrics.numTwoQ = 3; - candidate.metrics.numOneQ = 5; - candidate.metrics.depth = 7; - candidate.candidateClass = CandidateClass::DirectSingleQ; - candidate.enumerationIndex = 11; - - const ScoreWeights weights{.twoQ = 2.0, .oneQ = 0.5, .depth = 0.1}; - const auto score = scoreCandidate(candidate, weights); - - EXPECT_DOUBLE_EQ(score.weighted, (2.0 * 3.0) + (0.5 * 5.0) + (0.1 * 7.0)); - EXPECT_EQ(score.numTwoQ, 3U); - EXPECT_EQ(score.numOneQ, 5U); - EXPECT_EQ(score.depth, 7U); - EXPECT_EQ(score.tieBreakClass, - static_cast(CandidateClass::DirectSingleQ)); - EXPECT_EQ(score.enumerationIndex, 11U); -} - -TEST(NativeSynthesisScoringTest, IsBetterScoreComparesWeightedFirst) { - using namespace mlir::qco::native_synth; - const CandidateScore lower{ - .weighted = 1.0, .numTwoQ = 10, .depth = 100, .numOneQ = 1000}; - const CandidateScore higher{.weighted = 2.0}; - EXPECT_TRUE(isBetterScore(lower, higher)); - EXPECT_FALSE(isBetterScore(higher, lower)); - EXPECT_FALSE(isBetterScore(lower, lower)); -} - -TEST(NativeSynthesisScoringTest, IsBetterScoreTieBreaksInDeclaredOrder) { - using namespace mlir::qco::native_synth; - const CandidateScore anchor{.weighted = 1.0, - .numTwoQ = 5, - .depth = 5, - .numOneQ = 5, - .tieBreakClass = 5, - .enumerationIndex = 5}; - - const CandidateScore fewerTwoQ{.weighted = 1.0, - .numTwoQ = 4, - .depth = 99, - .numOneQ = 99, - .tieBreakClass = 99, - .enumerationIndex = 99}; - EXPECT_TRUE(isBetterScore(fewerTwoQ, anchor)); - - const CandidateScore lowerDepth{.weighted = 1.0, - .numTwoQ = 5, - .depth = 4, - .numOneQ = 99, - .tieBreakClass = 99, - .enumerationIndex = 99}; - EXPECT_TRUE(isBetterScore(lowerDepth, anchor)); - - const CandidateScore fewerOneQ{.weighted = 1.0, - .numTwoQ = 5, - .depth = 5, - .numOneQ = 4, - .tieBreakClass = 99, - .enumerationIndex = 99}; - EXPECT_TRUE(isBetterScore(fewerOneQ, anchor)); - - const CandidateScore lowerClass{.weighted = 1.0, - .numTwoQ = 5, - .depth = 5, - .numOneQ = 5, - .tieBreakClass = 0, - .enumerationIndex = 99}; - EXPECT_TRUE(isBetterScore(lowerClass, anchor)); - - const CandidateScore lowerEnum{.weighted = 1.0, - .numTwoQ = 5, - .depth = 5, - .numOneQ = 5, - .tieBreakClass = 5, - .enumerationIndex = 0}; - EXPECT_TRUE(isBetterScore(lowerEnum, anchor)); -} - -TEST(NativeSynthesisScoringTest, IsBetterScoreTreatsCloseWeightedAsTie) { - using namespace mlir::qco::native_synth; - const CandidateScore a{.weighted = 1.0, .numTwoQ = 1}; - const CandidateScore b{.weighted = 1.0 + 1e-13, .numTwoQ = 0}; - EXPECT_TRUE(isBetterScore(b, a)); -} - -TEST(NativeSynthesisScoringTest, IsBetterScoreFallsBackToTupleTieBreak) { - using namespace mlir::qco::native_synth; - // Within tolerance: force the lexicographic tuple comparison path. - const CandidateScore lhs{.weighted = 2.0 + 1e-13, - .numTwoQ = 3, - .depth = 4, - .numOneQ = 5, - .tieBreakClass = 6, - .enumerationIndex = 7}; - const CandidateScore rhs{.weighted = 2.0, - .numTwoQ = 3, - .depth = 4, - .numOneQ = 5, - .tieBreakClass = 6, - .enumerationIndex = 8}; - EXPECT_TRUE(isBetterScore(lhs, rhs)); -} - -TEST(NativeSynthesisScoringTest, SelectBestCandidateReturnsNullForEmptyInput) { - using namespace mlir::qco::native_synth; - const llvm::SmallVector, 0> empty; - EXPECT_EQ(selectBestCandidate(llvm::ArrayRef(empty), ScoreWeights{}), - nullptr); -} - -TEST(NativeSynthesisScoringTest, SelectBestCandidatePicksLowestWeighted) { - using namespace mlir::qco::native_synth; - llvm::SmallVector, 3> candidates(3); - candidates[0].metrics.numTwoQ = 4U; - candidates[1].metrics.numTwoQ = 1U; - candidates[2].metrics.numTwoQ = 2U; - - const auto* best = - selectBestCandidate(llvm::ArrayRef(candidates), ScoreWeights{}); - ASSERT_NE(best, nullptr); - EXPECT_EQ(best, &candidates[1]); -} - -TEST(NativeSynthesisScoringTest, SelectBestCandidateHonoursWeightPreferences) { - using namespace mlir::qco::native_synth; - llvm::SmallVector, 2> candidates(2); - candidates[0].metrics.numTwoQ = 2U; - candidates[0].metrics.numOneQ = 0U; - candidates[1].metrics.numTwoQ = 1U; - candidates[1].metrics.numOneQ = 20U; - - EXPECT_EQ(selectBestCandidate(llvm::ArrayRef(candidates), ScoreWeights{}), - candidates.data()); - - const ScoreWeights heavyTwoQ{.twoQ = 10.0, .oneQ = 0.01, .depth = 0.0}; - EXPECT_EQ(selectBestCandidate(llvm::ArrayRef(candidates), heavyTwoQ), - &candidates[1]); -} - -TEST(NativeSynthesisScoringTest, - SelectBestCandidateTieBreaksByEnumerationOrder) { - using namespace mlir::qco::native_synth; - llvm::SmallVector, 3> candidates(3); - candidates[0].enumerationIndex = 2U; - candidates[1].enumerationIndex = 0U; - candidates[2].enumerationIndex = 1U; - - const auto* best = - selectBestCandidate(llvm::ArrayRef(candidates), ScoreWeights{}); - ASSERT_NE(best, nullptr); - EXPECT_EQ(best, &candidates[1]); -} - -TEST_F(NativeSynthesisPassTest, XxPlusMinusYyEmittedCountsMatchScoringMetrics) { - using namespace mlir::qco::native_synth; - - const auto runRewriteCase = - [&](void (*emitProgram)(mlir::qc::QCProgramBuilder&)) { - OwningOpRef module = - mlir::qc::QCProgramBuilder::build(context.get(), emitProgram); - - PassManager pm(context.get()); - pm.addPass(createQCToQCO()); - ASSERT_TRUE(succeeded(pm.run(*module))); - - Operation* twoQOp = nullptr; - module->walk([&](Operation* op) { - if (llvm::isa(op)) { - twoQOp = op; - return WalkResult::interrupt(); - } - return WalkResult::advance(); - }); - ASSERT_NE(twoQOp, nullptr); - - IRRewriter rewriter(context.get()); - ASSERT_TRUE(succeeded(rewriteXXPlusMinusYYViaRzz(rewriter, twoQOp))); - - const auto expected = xxPlusMinusYyRzzRewriteScoringMetrics(); - const auto [numOneQ, numTwoQ] = - countSingleAndTwoQubitUnitariesForXxRzzMetrics(*module); - EXPECT_EQ(numOneQ, expected.numOneQ); - EXPECT_EQ(numTwoQ, expected.numTwoQ); - }; - - runRewriteCase(mlir::qc::nativeSynthScoringXxPlusYyOnly); - runRewriteCase(mlir::qc::nativeSynthScoringXxMinusYyOnly); -} diff --git a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp index afa0792415..cb12365bd5 100644 --- a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp +++ b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp @@ -12,8 +12,11 @@ #include +#include #include #include +#include +#include #include using namespace mlir::qco; @@ -303,6 +306,140 @@ TEST(Matrix4x4, AssignFromDynamicMatrix) { EXPECT_FALSE(out.assignFrom(DynamicMatrix::identity(2))); } +TEST(UnitaryMatrix2x2, TransposeAndIsIdentity) { + const Matrix2x2 m = Matrix2x2::fromElements(1, 2i, 3, 4); + EXPECT_TRUE(m.transpose().isApprox(Matrix2x2::fromElements(1, 3, 2i, 4))); + EXPECT_TRUE(Matrix2x2::identity().isIdentity()); + EXPECT_FALSE(pauliX().isIdentity()); +} + +TEST(UnitaryMatrix4x4, TransposeAndIsIdentity) { + Matrix4x4 m = Matrix4x4::identity(); + m(0, 3) = 2i; + m(3, 0) = 5.0; + const Matrix4x4 t = m.transpose(); + EXPECT_EQ(t(3, 0), 2i); + EXPECT_EQ(t(0, 3), 5.0); + EXPECT_TRUE(Matrix4x4::identity().isIdentity()); + EXPECT_FALSE(swapMatrix().isIdentity()); +} + +TEST(UnitaryMatrix4x4, DiagonalColumnsAndParts) { + Matrix4x4 m = + Matrix4x4::fromElements(Complex{1, 1}, 0, 0, 0, 0, Complex{2, 2}, 0, 0, 0, + 0, Complex{3, 3}, 0, 0, 0, 0, Complex{4, 4}); + const auto diag = m.diagonal(); + EXPECT_EQ(diag[0], (Complex{1, 1})); + EXPECT_EQ(diag[3], (Complex{4, 4})); + EXPECT_TRUE(Matrix4x4::fromDiagonal(diag).isApprox(m)); + + const auto col1 = m.column(1); + EXPECT_EQ(col1[1], (Complex{2, 2})); + Matrix4x4 n = Matrix4x4::identity(); + n.setColumn(2, {1i, 2i, 3i, 4i}); + EXPECT_EQ(n(0, 2), 1i); + EXPECT_EQ(n(3, 2), 4i); + + const auto re = m.realPart(); + const auto im = m.imagPart(); + EXPECT_EQ(re[0], 1.0); + EXPECT_EQ(im[0], 1.0); + EXPECT_EQ(re[15], 4.0); + EXPECT_EQ(im[15], 4.0); +} + +TEST(UnitaryMatrix4x4, KroneckerProduct) { + const Matrix2x2 x = pauliX(); + // X (x) I should swap the high bit. + const Matrix4x4 xi = kron(x, Matrix2x2::identity()); + EXPECT_TRUE(xi.isApprox(Matrix4x4::fromElements(0, 0, 1, 0, // row 0 + 0, 0, 0, 1, // row 1 + 1, 0, 0, 0, // row 2 + 0, 1, 0, 0))); + // I (x) X swaps the low bit. + const Matrix4x4 ix = kron(Matrix2x2::identity(), x); + EXPECT_TRUE(ix.isApprox(Matrix4x4::fromElements(0, 1, 0, 0, // row 0 + 1, 0, 0, 0, // row 1 + 0, 0, 0, 1, // row 2 + 0, 0, 1, 0))); +} + +TEST(UnitaryMatrix2x2, ScalarLeftMultiply) { + const Matrix2x2 x = pauliX(); + const Complex scalar = std::exp(1i * 0.5); + EXPECT_TRUE((scalar * x).isApprox(x * scalar)); +} + +TEST(UnitaryMatrix4x4, ScalarLeftMultiply) { + const Matrix4x4 swap = swapMatrix(); + const Complex scalar = std::exp(1i * 0.25); + EXPECT_TRUE((scalar * swap).isApprox(swap * scalar)); +} + +TEST(JacobiEigensolver, DiagonalMatrix) { + std::array a{}; + a[0] = 3.0; + a[5] = 1.0; + a[10] = 4.0; + a[15] = 2.0; + const SymmetricEigen4 result = jacobiSymmetricEigen(a); + EXPECT_NEAR(result.eigenvalues[0], 1.0, 1e-12); + EXPECT_NEAR(result.eigenvalues[1], 2.0, 1e-12); + EXPECT_NEAR(result.eigenvalues[2], 3.0, 1e-12); + EXPECT_NEAR(result.eigenvalues[3], 4.0, 1e-12); +} + +TEST(JacobiEigensolver, ReconstructsRandomSymmetric) { + std::mt19937 rng(0xC0FFEE); + std::uniform_real_distribution dist(-2.0, 2.0); + for (int trial = 0; trial < 50; ++trial) { + std::array a{}; + for (std::size_t i = 0; i < 4; ++i) { + for (std::size_t j = i; j < 4; ++j) { + const double value = dist(rng); + a[(i * 4) + j] = value; + a[(j * 4) + i] = value; + } + } + const SymmetricEigen4 result = jacobiSymmetricEigen(a); + + // Eigenvalues are ascending. + for (std::size_t i = 0; i + 1 < 4; ++i) { + EXPECT_LE(result.eigenvalues[i], result.eigenvalues[i + 1] + 1e-12); + } + + // Eigenvectors are orthonormal: V^T V == I. + const Matrix4x4& v = result.eigenvectors; + EXPECT_TRUE((v.transpose() * v).isIdentity(1e-9)); + + // Reconstruction: V D V^T == A. + const Matrix4x4 d = + Matrix4x4::fromDiagonal({result.eigenvalues[0], result.eigenvalues[1], + result.eigenvalues[2], result.eigenvalues[3]}); + const Matrix4x4 reconstructed = v * d * v.transpose(); + Matrix4x4 original{}; + for (std::size_t k = 0; k < 16; ++k) { + original(k / 4, k % 4) = a[k]; + } + EXPECT_TRUE(reconstructed.isApprox(original, 1e-9)); + } +} + +TEST(JacobiEigensolver, HandlesDegenerateSpectrum) { + // A scalar multiple of the identity: every vector is an eigenvector, but the + // returned basis must still be orthonormal. + std::array a{}; + for (std::size_t i = 0; i < 4; ++i) { + a[(i * 4) + i] = 2.5; + } + const SymmetricEigen4 result = jacobiSymmetricEigen(a); + for (const double value : result.eigenvalues) { + EXPECT_NEAR(value, 2.5, 1e-12); + } + const Matrix4x4& v = result.eigenvectors; + EXPECT_TRUE((v.transpose() * v).isIdentity(1e-9)); +} + TEST(DynamicMatrix, IsApproxOverloads) { const Matrix1x1 phase = Matrix1x1::fromElements(Complex{0.25, 0.5}); const Matrix2x2 x = pauliX(); From 4d195517964f5476ca9555107ac88a0151d99bf4 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 18 Jun 2026 15:35:35 +0200 Subject: [PATCH 043/122] =?UTF-8?q?=F0=9F=94=A5=20Remove=20`fuseRzAcrossCt?= =?UTF-8?q?rlControls`=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mlir/Dialect/QCO/Transforms/Passes.td | 3 +- .../QCO/Transforms/NativeSynthesis/Pass.cpp | 92 +------------------ 2 files changed, 6 insertions(+), 89 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index a633fa3244..6cd6d8f811 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -184,8 +184,7 @@ def NativeGateSynthesisPass : Pass<"native-gate-synthesis", "mlir::ModuleOp"> { non-native unitaries until every single-qubit op matches the menu (two-qubit lowering may temporarily emit off-menu 1q ops that later sweeps absorb—if any remain after that cap, the pass fails); fuse 1q seams between two-qubit - blocks; merge `rz` through eligible `qco.ctrl` control wires; fuse 1q runs - again; then up to four further synthesis + fusion rounds until the full menu + blocks; then up to four further synthesis + fusion rounds until the full menu holds (including native `qco.ctrl` shells and bare `rzz` when allowed). If anything is still off-menu, the pass fails. diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp index c380b3becc..013be38032 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp @@ -21,12 +21,10 @@ #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" -#include "mlir/Dialect/Utils/Utils.h" #include #include #include -#include #include #include #include @@ -188,9 +186,8 @@ namespace { /// Lowers unitary QCO ops to a comma-separated native gate menu using a /// deterministic, matrix-driven synthesizer: single-qubit fuse, two-qubit -/// window consolidation, synthesis sweeps, seam single-qubit fuse, `rz` -/// through `ctrl` controls, another single-qubit fuse, optional cleanup -/// sweeps. +/// window consolidation, synthesis sweeps, seam single-qubit fuse, and +/// optional cleanup sweeps. struct NativeGateSynthesisPass : impl::NativeGateSynthesisPassBase { /// Default-construct the pass with the TableGen-generated option defaults. @@ -212,8 +209,8 @@ struct NativeGateSynthesisPass /// Top-level pass entry point. Resolves the native-gate menu, then drives /// the staged rewrite pipeline: one-qubit run fusion, two-qubit window /// consolidation, synthesis sweeps until the single-qubit surface is native, - /// seam cleanup, `rz`-through-`ctrl` folding, and a final fusion pass. Fails - /// the pass on invalid input or non-convergence. + /// seam cleanup, and a final fusion pass. Fails the pass on invalid input or + /// non-convergence. void runOnOperation() override { // Empty native-gates string: no-op. if (llvm::StringRef(nativeGates).trim().empty()) { @@ -241,7 +238,7 @@ struct NativeGateSynthesisPass return; } // Two-qubit lowering can emit off-menu single-qubit ops (e.g. `rx`/`ry`); - // repeat until clean or hit the sweep cap before seam / `rz` cleanup. + // repeat until clean or hit the sweep cap before seam cleanup. constexpr unsigned kMaxSynthesisSweeps = 4; for (unsigned i = 0; i < kMaxSynthesisSweeps; ++i) { if (failed(synthesizeRemainingOps(rewriter, spec, oneQubitBasis))) { @@ -262,9 +259,6 @@ struct NativeGateSynthesisPass } // Fuse single-qubit seams between two-qubit blocks. fuseOneQubitRuns(rewriter, spec, oneQubitBasis); - // Fuse `rz` through control wires of `ctrl` (diagonal control phase). - fuseRzAcrossCtrlControls(rewriter); - fuseOneQubitRuns(rewriter, spec, oneQubitBasis); // Re-check full menu (single-qubit ops, native `ctrl`, allowed bare `rzz`). constexpr unsigned kPostMenuCleanupSweeps = 4; unsigned postMenuSweepsRemaining = kPostMenuCleanupSweeps; @@ -410,82 +404,6 @@ struct NativeGateSynthesisPass } } - /// If `rz1` can reach another `rz` through at least one `ctrl` control hop, - /// merge angles into `rz1` and erase the partner. - /// - /// `Rz` commutes with a `ctrl` operation acting on the same wire when the - /// wire is a *control* line (controls only diagonalize the computational - /// basis and are invariant under Z-rotations). We walk the def-use chain - /// forward from `rz1`'s output, hopping through `ctrl`s where the wire is - /// used as a control, and fold into the next `rz` we find. The `hops == 0` - /// guard intentionally rejects two adjacent `rz`s with nothing in between - /// -- that case is handled by `fuseOneQubitRuns` above. - static bool tryFuseRzForwardThroughCtrls(IRRewriter& rewriter, RZOp rz1) { - Value v = rz1->getResult(0); - if (!llvm::isa(v.getType())) { - return false; - } - RZOp partner; - unsigned hops = 0; - while (v.hasOneUse()) { - Operation* user = *v.getUsers().begin(); - if (auto rz2 = llvm::dyn_cast(user); - rz2 && rz2->getOperand(0) == v) { - partner = rz2; - break; - } - auto ctrl = llvm::dyn_cast(user); - if (!ctrl) { - return false; - } - // Only control wires commute through `ctrl` here. - if (!llvm::is_contained(ctrl.getControlsIn(), v)) { - return false; - } - v = ctrl.getOutputForInput(v); - ++hops; - } - if (!partner || hops == 0) { - return false; - } - - // Fold angles; use a scalar constant when both inputs are constant. - const Location loc = rz1.getLoc(); - const Value theta1 = rz1.getTheta(); - const Value theta2 = partner.getTheta(); - const auto c1 = mlir::utils::valueToDouble(theta1); - const auto c2 = mlir::utils::valueToDouble(theta2); - rewriter.setInsertionPoint(rz1); - Value newTheta; - if (c1.has_value() && c2.has_value()) { - newTheta = mlir::utils::constantFromScalar(rewriter, loc, *c1 + *c2); - } else { - newTheta = arith::AddFOp::create(rewriter, loc, theta1, theta2); - } - rewriter.modifyOpInPlace(rz1, - [&] { rz1.getThetaMutable().assign(newTheta); }); - rewriter.replaceOp(partner, partner->getOperand(0)); - return true; - } - - /// Fixpoint: merge `rz` through `ctrl` control chains into the next `rz`. - void fuseRzAcrossCtrlControls(IRRewriter& rewriter) { - bool changed = true; - while (changed) { - changed = false; - llvm::SmallVector rzOps; - getOperation()->walk([&](RZOp rz) { rzOps.push_back(rz); }); - for (RZOp rz : rzOps) { - if (rz->getBlock() == nullptr) { - continue; - } - if (tryFuseRzForwardThroughCtrls(rewriter, rz)) { - changed = true; - } - } - } - } - /// Two-qubit windows with absorbed single-qubit ops: replace when a cheaper /// native sequence exists. LogicalResult consolidateTwoQubitBlocks(IRRewriter& rewriter, From 8a2d90e3c1261bb13e0cf7abaf649d085ef401bb Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 18 Jun 2026 15:48:42 +0200 Subject: [PATCH 044/122] =?UTF-8?q?=F0=9F=94=A5=20Remove=20`Gate.h`=20and?= =?UTF-8?q?=20`GateKind.h`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Decomposition/BasisDecomposer.h | 30 ++-- .../QCO/Transforms/Decomposition/Gate.h | 41 ------ .../QCO/Transforms/Decomposition/GateKind.h | 44 ------ .../QCO/Transforms/Decomposition/Helpers.h | 20 +-- .../Decomposition/UnitaryMatrices.h | 20 ++- .../Decomposition/BasisDecomposer.cpp | 15 +- .../QCO/Transforms/Decomposition/Helpers.cpp | 51 ------- .../Decomposition/UnitaryMatrices.cpp | 133 ++++-------------- .../NativeSynthesis/PassTwoQubitWindows.cpp | 1 - .../Transforms/NativeSynthesis/TwoQubit.cpp | 21 ++- .../Compiler/test_compiler_pipeline.cpp | 1 - .../Transforms/Decomposition/CMakeLists.txt | 6 +- .../Decomposition/test_basis_decomposer.cpp | 60 ++++---- .../test_decomposition_get_gate_kind.cpp | 84 ----------- .../test_decomposition_helpers.cpp | 11 -- .../Decomposition/test_weyl_decomposition.cpp | 59 +++----- 16 files changed, 112 insertions(+), 485 deletions(-) delete mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Gate.h delete mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h delete mode 100644 mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_get_gate_kind.cpp diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.h index 643f78cfe6..a2cc1eb753 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.h @@ -10,7 +10,7 @@ #pragma once -#include "Gate.h" +#include "UnitaryMatrices.h" #include "WeylDecomposition.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" @@ -68,15 +68,12 @@ 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 decomposition. - * 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). + * specified entangler matrix. + * This entangler will appear between 0 and 3 times in each decomposition. + * The 4x4 matrix must be in MQT operand order (qubit 0 = MSB). */ - [[nodiscard]] static TwoQubitBasisDecomposer create(const Gate& basisGate, - double basisFidelity); + [[nodiscard]] static TwoQubitBasisDecomposer + create(const Matrix4x4& basisMatrix, double basisFidelity); /** * Perform decomposition using the basis gate of this decomposer. @@ -100,7 +97,7 @@ class TwoQubitBasisDecomposer { * Constructs decomposer instance. */ TwoQubitBasisDecomposer( - Gate basisGate, double basisFidelity, + double basisFidelity, const decomposition::TwoQubitWeylDecomposition& basisDecomposer, bool isSuperControlled, const Matrix2x2& u0l, const Matrix2x2& u0r, const Matrix2x2& u1l, const Matrix2x2& u1ra, const Matrix2x2& u1rb, @@ -109,12 +106,11 @@ class TwoQubitBasisDecomposer { const Matrix2x2& q0l, const Matrix2x2& q0r, const Matrix2x2& q1la, const Matrix2x2& q1lb, const Matrix2x2& q1ra, const Matrix2x2& q1rb, const Matrix2x2& q2l, const Matrix2x2& q2r) - : basisGate{std::move(basisGate)}, basisFidelity{basisFidelity}, - basisDecomposer{basisDecomposer}, isSuperControlled{isSuperControlled}, - u0l{u0l}, u0r{u0r}, u1l{u1l}, u1ra{u1ra}, u1rb{u1rb}, u2la{u2la}, - u2lb{u2lb}, u2ra{u2ra}, u2rb{u2rb}, u3l{u3l}, u3r{u3r}, q0l{q0l}, - q0r{q0r}, q1la{q1la}, q1lb{q1lb}, q1ra{q1ra}, q1rb{q1rb}, q2l{q2l}, - q2r{q2r} {} + : 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) /** @@ -201,8 +197,6 @@ class TwoQubitBasisDecomposer { 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 diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Gate.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Gate.h deleted file mode 100644 index 429f10482f..0000000000 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Gate.h +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#pragma once - -#include "GateKind.h" - -#include - -#include - -namespace mlir::qco::decomposition { - -using QubitId = std::size_t; - -/** - * Lightweight decomposition-time gate record. - * - * This struct is intentionally independent from MLIR operations so helper code - * can build and manipulate abstract one- and two-qubit circuits before they - * are materialized back into the IR. - */ -struct Gate { - /// Operation kind represented by this gate. - GateKind type{GateKind::I}; - - /// Gate parameters in operation-specific order. - llvm::SmallVector parameter; - - /// Logical qubit ids used by the gate, in operand order. - llvm::SmallVector qubitId = {0}; -}; - -} // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h deleted file mode 100644 index 3ba8b148cb..0000000000 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#pragma once - -#include - -namespace mlir::qco::decomposition { - -/** - * Lightweight gate identifiers used by decomposition utilities. - * - * These kinds intentionally stay independent from the core IR `qc::OpType` - * enum so the MLIR/QCO decomposition layer does not depend on the `ir` - * package. - */ -enum class GateKind : std::uint8_t { - I = 0, - H, - P, - U, - U2, - X, - Y, - Z, - SX, - RX, - RY, - RZ, - R, - RXX, - RYY, - RZZ, - GPhase, -}; - -} // 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 index 56d7dd176f..bd1bb58daf 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h @@ -10,26 +10,14 @@ #pragma once -#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" #include -/// Numeric + classification helpers used by the decomposition passes. -/// Lives in `mlir::qco::helpers` (not `decomposition`) because some helpers -/// map IR ops back to decomposition kinds. +/// Numeric helpers used by the decomposition passes. namespace mlir::qco::helpers { -/** - * Map a QCO unitary operation to the corresponding decomposition `GateKind`. - * - * For controlled operations, this returns the wrapped body operation type - * rather than the outer `ctrl` marker. - */ -[[nodiscard]] decomposition::GateKind getGateKind(UnitaryOpInterface op); - /// Check whether `matrix` is unitary within `tolerance` (i.e. `M^H M` is /// approximately the identity). [[nodiscard]] bool isUnitaryMatrix(const Matrix2x2& matrix, @@ -58,12 +46,6 @@ namespace mlir::qco::helpers { */ [[nodiscard]] double traceToFidelity(const std::complex& x); -/** - * Return the heuristic cost assigned to a gate acting on `numOfQubits`. - */ -[[nodiscard]] std::size_t getComplexity(decomposition::GateKind type, - std::size_t numOfQubits); - /** * Return the scalar `e^(i * globalPhase)` factor for a stored global phase. */ diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h index 1077705d01..3eca091001 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h @@ -10,17 +10,22 @@ #pragma once -#include "Gate.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" #include +#include + /// Standard-basis matrix factories for the decomposition layer. Two-qubit /// matrices use the same computational-basis index bit order as /// ``UnitaryOpInterface::getUnitaryMatrix4x4`` (qubit 0 labels the high bit). namespace mlir::qco::decomposition { +/// Logical qubit index used by ``expandToTwoQubits`` / +/// ``fixTwoQubitMatrixQubitOrder``. +using QubitId = std::size_t; + inline constexpr double FRAC1_SQRT2 = 0.707106781186547524400844362104849039284835937688474036588L; @@ -50,6 +55,13 @@ inline constexpr double FRAC1_SQRT2 = /// `i * sigma_x`. [[nodiscard]] const Matrix2x2& ipx(); +/// CX entangler with control on qubit 0 (MSB) and target on qubit 1. +[[nodiscard]] const Matrix4x4& cxGate01(); +/// CX entangler with control on qubit 1 and target on qubit 0 (MSB). +[[nodiscard]] const Matrix4x4& cxGate10(); +/// CZ entangler (wire-order invariant). +[[nodiscard]] const Matrix4x4& czGate(); + /// Kronecker-embed a 2x2 on wire ``qubitId`` (identity on the other wire). [[nodiscard]] Matrix4x4 expandToTwoQubits(const Matrix2x2& singleQubitMatrix, QubitId qubitId); @@ -61,10 +73,4 @@ inline constexpr double FRAC1_SQRT2 = fixTwoQubitMatrixQubitOrder(const Matrix4x4& twoQubitMatrix, const llvm::SmallVector& qubitIds); -/// Construct the 2x2 / 4x4 matrix described by `gate`. Two-qubit gates are -/// returned in the convention matching `expandToTwoQubits` + the gate's own -/// operand order. -[[nodiscard]] Matrix2x2 getSingleQubitMatrix(const Gate& gate); -[[nodiscard]] Matrix4x4 getTwoQubitMatrix(const Gate& gate); - } // namespace mlir::qco::decomposition diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp index 212c7e48ad..d8e837c7ba 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp @@ -10,12 +10,12 @@ #include "mlir/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.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 "mlir/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" +#include #include #include @@ -34,8 +34,9 @@ namespace mlir::qco::decomposition { using namespace std::complex_literals; -TwoQubitBasisDecomposer TwoQubitBasisDecomposer::create(const Gate& basisGate, - double basisFidelity) { +TwoQubitBasisDecomposer +TwoQubitBasisDecomposer::create(const Matrix4x4& basisMatrix, + double basisFidelity) { const Matrix2x2 k12RArr = Matrix2x2::fromElements( 1i * FRAC1_SQRT2, FRAC1_SQRT2, -FRAC1_SQRT2, -1i * FRAC1_SQRT2); const Matrix2x2 k12LArr = @@ -46,8 +47,8 @@ TwoQubitBasisDecomposer TwoQubitBasisDecomposer::create(const Gate& basisGate, // below is derived for a basis CX whose 4x4 matrix is the Qiskit/LSB form // `[[1,0,0,0],[0,0,0,1],[0,0,1,0],[0,1,0,0]]`, i.e. "control on the LSB // factor, target on the MSB factor" of the tensor product. MQT's wider - // convention places operand 0 on the MSB factor, so `getTwoQubitMatrix` for - // the same logical CX gives the SWAP-conjugate + // convention places operand 0 on the MSB factor, so the CX/CZ matrix for + // control-on-wire-0 gives the SWAP-conjugate // `[[1,0,0,0],[0,1,0,0],[0,0,0,1],[0,0,1,0]]`. // // Because `SWAP * C(a,b,c) * SWAP = C(a,b,c)` but @@ -59,8 +60,7 @@ TwoQubitBasisDecomposer TwoQubitBasisDecomposer::create(const Gate& basisGate, // the emission boundary in `decomp{0,1,2,3}` below. This reproduces the // pre-flip gate counts without having to re-derive every SMB constant for // the MSB basis -- the two routes are algebraically equivalent. - const Matrix4x4 basisMatrixLsb = - swapGate() * getTwoQubitMatrix(basisGate) * swapGate(); + const Matrix4x4 basisMatrixLsb = swapGate() * basisMatrix * swapGate(); const auto basisDecomposer = decomposition::TwoQubitWeylDecomposition::create( basisMatrixLsb, basisFidelity); const auto isSuperControlled = @@ -124,7 +124,6 @@ TwoQubitBasisDecomposer TwoQubitBasisDecomposer::create(const Gate& basisGate, auto q2r = k2rDagger * k12RArr; return TwoQubitBasisDecomposer{ - basisGate, basisFidelity, basisDecomposer, isSuperControlled, diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp index 278efa91ef..752cb083c1 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp @@ -10,54 +10,16 @@ #include "mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h" -#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" -#include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" -#include -#include #include -#include #include #include -#include #include namespace mlir::qco::helpers { -decomposition::GateKind getGateKind(UnitaryOpInterface op) { - Operation* raw = op.getOperation(); - if (auto ctrl = llvm::dyn_cast(raw)) { - // Controlled operations encode the physical gate in the body region. - raw = ctrl.getBodyUnitary(0).getOperation(); - } - return llvm::TypeSwitch(raw) - .Case([](auto) { return decomposition::GateKind::I; }) - .Case([](auto) { return decomposition::GateKind::H; }) - .Case([](auto) { return decomposition::GateKind::P; }) - .Case([](auto) { return decomposition::GateKind::U; }) - .Case([](auto) { return decomposition::GateKind::U2; }) - .Case([](auto) { return decomposition::GateKind::X; }) - .Case([](auto) { return decomposition::GateKind::Y; }) - .Case([](auto) { return decomposition::GateKind::Z; }) - .Case([](auto) { return decomposition::GateKind::SX; }) - .Case([](auto) { return decomposition::GateKind::RX; }) - .Case([](auto) { return decomposition::GateKind::RY; }) - .Case([](auto) { return decomposition::GateKind::RZ; }) - .Case([](auto) { return decomposition::GateKind::R; }) - .Case([](auto) { return decomposition::GateKind::RXX; }) - .Case([](auto) { return decomposition::GateKind::RYY; }) - .Case([](auto) { return decomposition::GateKind::RZZ; }) - .Case([](auto) { return decomposition::GateKind::GPhase; }) - .Default([](Operation*) -> decomposition::GateKind { - llvm::reportFatalInternalError( - "Unsupported QCO unitary operation kind"); - llvm_unreachable("unsupported gate kind"); - }); -} - bool isUnitaryMatrix(const Matrix2x2& matrix, double tolerance) { return (matrix.adjoint() * matrix).isIdentity(tolerance); } @@ -99,19 +61,6 @@ double traceToFidelity(const std::complex& x) { return (4.0 + (xAbs * xAbs)) / 20.0; } -std::size_t getComplexity(decomposition::GateKind type, - std::size_t numOfQubits) { - if (numOfQubits > 1) { - // Multi-qubit operations dominate the heuristic cost model. - constexpr std::size_t multiQubitFactor = 10; - return (numOfQubits - 1) * multiQubitFactor; - } - if (type == decomposition::GateKind::GPhase) { - return 0; - } - return 1; -} - std::complex globalPhaseFactor(double globalPhase) { return std::exp(std::complex{0, 1} * globalPhase); } diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp index e7d014a777..95d9bccf6b 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp @@ -10,8 +10,6 @@ #include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/Gate.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" #include @@ -129,6 +127,30 @@ const Matrix2x2& ipx() { return matrix; } +const Matrix4x4& cxGate01() { + static const Matrix4x4 matrix = Matrix4x4::fromElements(1, 0, 0, 0, // + 0, 1, 0, 0, // + 0, 0, 0, 1, // + 0, 0, 1, 0); + return matrix; +} + +const Matrix4x4& cxGate10() { + static const Matrix4x4 matrix = Matrix4x4::fromElements(1, 0, 0, 0, // + 0, 0, 0, 1, // + 0, 0, 1, 0, // + 0, 1, 0, 0); + return matrix; +} + +const Matrix4x4& czGate() { + static const Matrix4x4 matrix = Matrix4x4::fromElements(1, 0, 0, 0, // + 0, 1, 0, 0, // + 0, 0, 1, 0, // + 0, 0, 0, -1); + return matrix; +} + Matrix4x4 expandToTwoQubits(const Matrix2x2& singleQubitMatrix, QubitId qubitId) { if (qubitId == 0) { @@ -155,111 +177,4 @@ fixTwoQubitMatrixQubitOrder(const Matrix4x4& twoQubitMatrix, "Invalid qubit IDs for fixing two-qubit matrix"); } -Matrix2x2 getSingleQubitMatrix(const Gate& gate) { - if (gate.type == GateKind::SX) { - return Matrix2x2::fromElements(Complex{0.5, 0.5}, Complex{0.5, -0.5}, - Complex{0.5, -0.5}, Complex{0.5, 0.5}); - } - if (gate.type == GateKind::RX) { - assert(gate.parameter.size() == 1); - return rxMatrix(gate.parameter[0]); - } - if (gate.type == GateKind::RY) { - assert(gate.parameter.size() == 1); - return ryMatrix(gate.parameter[0]); - } - if (gate.type == GateKind::RZ) { - assert(gate.parameter.size() == 1); - return rzMatrix(gate.parameter[0]); - } - if (gate.type == GateKind::X) { - return Matrix2x2::fromElements(0, 1, 1, 0); - } - if (gate.type == GateKind::I) { - return Matrix2x2::identity(); - } - if (gate.type == GateKind::P) { - assert(gate.parameter.size() == 1); - return pMatrix(gate.parameter[0]); - } - if (gate.type == GateKind::U) { - assert(gate.parameter.size() == 3); - return uMatrix(gate.parameter[0], gate.parameter[1], gate.parameter[2]); - } - if (gate.type == GateKind::U2) { - assert(gate.parameter.size() == 2); - return u2Matrix(gate.parameter[0], gate.parameter[1]); - } - if (gate.type == GateKind::H) { - return hGate(); - } - llvm::reportFatalInternalError( - "unsupported gate type for single qubit matrix"); -} - -// Reconstruct a two-qubit workspace matrix for a decomposition `Gate`. -Matrix4x4 getTwoQubitMatrix(const Gate& gate) { - if (gate.qubitId.empty()) { - return Matrix4x4::identity(); - } - if (gate.qubitId.size() == 1) { - return expandToTwoQubits(getSingleQubitMatrix(gate), gate.qubitId[0]); - } - if (gate.qubitId.size() == 2) { - const bool validPair01 = - gate.qubitId == llvm::SmallVector{0, 1}; - const bool validPair10 = - gate.qubitId == llvm::SmallVector{1, 0}; - if (!validPair01 && !validPair10) { - llvm::reportFatalInternalError( - "Invalid two-qubit gate qubit IDs: expected {0,1} or {1,0}"); - } - if (gate.type == GateKind::X) { - // Controlled-X. The two matrices below are the *same* CX gate written in - // the two possible operand orderings used by `Gate::qubitId`: qubit 0 is - // the MSB of the 4x4 computational basis (matching - // `UnitaryOpInterface::getUnitaryMatrix4x4`), so swapping - // control/target wires produces a different basis-layout matrix. - if (validPair01) { - // control = wire 0 (MSB), target = wire 1. - return Matrix4x4::fromElements(1, 0, 0, 0, // - 0, 1, 0, 0, // - 0, 0, 0, 1, // - 0, 0, 1, 0); - } - // control = wire 1, target = wire 0 (MSB). - return Matrix4x4::fromElements(1, 0, 0, 0, // - 0, 0, 0, 1, // - 0, 0, 1, 0, // - 0, 1, 0, 0); - } - if (gate.type == GateKind::Z) { - // controlled Z (CZ) - return Matrix4x4::fromElements(1, 0, 0, 0, // - 0, 1, 0, 0, // - 0, 0, 1, 0, // - 0, 0, 0, -1); - } - if (gate.type == GateKind::RXX) { - assert(gate.parameter.size() == 1); - return rxxMatrix(gate.parameter[0]); - } - if (gate.type == GateKind::RYY) { - assert(gate.parameter.size() == 1); - return ryyMatrix(gate.parameter[0]); - } - if (gate.type == GateKind::RZZ) { - assert(gate.parameter.size() == 1); - return rzzMatrix(gate.parameter[0]); - } - if (gate.type == GateKind::I) { - return Matrix4x4::identity(); - } - llvm::reportFatalInternalError( - "Unsupported gate type for two qubit matrix"); - } - llvm::reportFatalInternalError( - "Invalid number of qubit IDs for two-qubit matrix construction"); -} - } // namespace mlir::qco::decomposition diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp index d5f5c765a2..44b902d13a 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp @@ -12,7 +12,6 @@ #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/Gate.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h" diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp index ee79e614f7..226d14303b 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp @@ -12,8 +12,7 @@ #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/Gate.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h" @@ -54,23 +53,19 @@ selectEntangler(const NativeProfileSpec& spec) { return std::nullopt; } -/// Build the decomposition-layer basis gate for `entangler`. The qubit ids -/// align with `getBlockTwoQubitMatrix` / CX layout (control on qubit 0). -static decomposition::Gate entanglerGate(EntanglerBasis entangler) { - return decomposition::Gate{ - .type = entangler == EntanglerBasis::Cz ? decomposition::GateKind::Z - : decomposition::GateKind::X, - .qubitId = {0, 1}, - }; +/// 4x4 entangler matrix for `entangler` in MQT operand order (control on qubit +/// 0 = MSB), matching `getBlockTwoQubitMatrix` / CX layout. +static Matrix4x4 entanglerMatrix(EntanglerBasis entangler) { + return entangler == EntanglerBasis::Cz ? decomposition::czGate() + : decomposition::cxGate01(); } /// Run the Weyl + basis decomposer for `target` against `entangler`, returning /// the raw single-qubit factors and entangler count (or `std::nullopt`). static std::optional decomposeWithEntangler(const Matrix4x4& target, EntanglerBasis entangler) { - const auto basisGate = entanglerGate(entangler); - auto decomposer = - decomposition::TwoQubitBasisDecomposer::create(basisGate, 1.0); + auto decomposer = decomposition::TwoQubitBasisDecomposer::create( + entanglerMatrix(entangler), 1.0); auto weyl = decomposition::TwoQubitWeylDecomposition::create(target, std::nullopt); return decomposer.twoQubitDecompose(weyl, std::nullopt); diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 2c60e931f8..70a0d54620 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -17,7 +17,6 @@ #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/Gate.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt index 78bdafbc48..c7fd4cf7ec 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt @@ -7,10 +7,8 @@ # Licensed under the MIT License set(target_name mqt-core-mlir-unittest-decomposition) -add_executable( - ${target_name} - test_basis_decomposer.cpp test_decomposition_get_gate_kind.cpp test_decomposition_helpers.cpp - test_euler_decomposition.cpp test_weyl_decomposition.cpp) +add_executable(${target_name} test_basis_decomposer.cpp test_decomposition_helpers.cpp + test_euler_decomposition.cpp test_weyl_decomposition.cpp) target_link_libraries( ${target_name} diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_basis_decomposer.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_basis_decomposer.cpp index 811a4ed7e7..c012e24b23 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_basis_decomposer.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_basis_decomposer.cpp @@ -10,8 +10,6 @@ #include "decomposition_test_utils.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/Gate.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.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" @@ -32,8 +30,8 @@ using namespace mlir::qco::decomposition; using namespace mlir::qco::decomposition_test; // NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_P` at global scope -class BasisDecomposerTest - : public testing::TestWithParam> { +class BasisDecomposerTest : public testing::TestWithParam< + std::tuple> { public: /// Reconstruct the 4x4 unitary realized by a native two-qubit decomposition. /// @@ -43,8 +41,7 @@ class BasisDecomposerTest /// after the last entangler. [[nodiscard]] static Matrix4x4 restore(const TwoQubitNativeDecomposition& decomposition, - const Gate& basisGate) { - const Matrix4x4 entangler = getTwoQubitMatrix(basisGate); + const Matrix4x4& entangler) { const auto& factors = decomposition.singleQubitFactors; const auto layer = [&](std::size_t i) { return kron(factors[(2 * i) + 1], factors[2 * i]); @@ -59,39 +56,39 @@ class BasisDecomposerTest protected: void SetUp() override { - basisGate = std::get<0>(GetParam()); + basisMatrix = std::get<0>(GetParam())(); target = std::get<1>(GetParam())(); targetDecomposition = std::make_unique( TwoQubitWeylDecomposition::create(target, std::optional{1.0})); } Matrix4x4 target; - Gate basisGate; + Matrix4x4 basisMatrix; std::unique_ptr targetDecomposition; }; TEST_P(BasisDecomposerTest, TestExact) { const auto& originalMatrix = target; - auto decomposer = TwoQubitBasisDecomposer::create(basisGate, 1.0); + auto decomposer = TwoQubitBasisDecomposer::create(basisMatrix, 1.0); auto decomposed = decomposer.twoQubitDecompose(*targetDecomposition, std::nullopt); ASSERT_TRUE(decomposed.has_value()); - auto restoredMatrix = restore(*decomposed, basisGate); + auto restoredMatrix = restore(*decomposed, basisMatrix); EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix)); } TEST_P(BasisDecomposerTest, TestApproximation) { const auto& originalMatrix = target; - auto decomposer = TwoQubitBasisDecomposer::create(basisGate, 1.0 - 1e-12); + auto decomposer = TwoQubitBasisDecomposer::create(basisMatrix, 1.0 - 1e-12); auto decomposed = decomposer.twoQubitDecompose(*targetDecomposition, std::nullopt); ASSERT_TRUE(decomposed.has_value()); - auto restoredMatrix = restore(*decomposed, basisGate); + auto restoredMatrix = restore(*decomposed, basisMatrix); EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix)); } @@ -100,26 +97,27 @@ TEST(BasisDecomposerTest, Random) { constexpr auto maxIterations = 2000; std::mt19937 rng{123456UL}; - const llvm::SmallVector basisGates{ - {.type = GateKind::X, .parameter = {}, .qubitId = {0, 1}}, - {.type = GateKind::X, .parameter = {}, .qubitId = {1, 0}}}; + const llvm::SmallVector basisMatrices{cxGate01(), cxGate10()}; std::uniform_int_distribution distBasisGate{ - 0, basisGates.size() - 1}; - auto selectRandomBasisGate = [&]() { return basisGates[distBasisGate(rng)]; }; + 0, basisMatrices.size() - 1}; + auto selectRandomBasisMatrix = [&]() { + return basisMatrices[distBasisGate(rng)]; + }; for (int i = 0; i < maxIterations; ++i) { auto originalMatrix = randomUnitary4x4(rng); auto targetDecomposition = TwoQubitWeylDecomposition::create( originalMatrix, std::optional{1.0}); - const auto basisGate = selectRandomBasisGate(); - auto decomposer = TwoQubitBasisDecomposer::create(basisGate, 1.0); + const auto basisMatrix = selectRandomBasisMatrix(); + auto decomposer = TwoQubitBasisDecomposer::create(basisMatrix, 1.0); auto decomposed = decomposer.twoQubitDecompose(targetDecomposition, std::nullopt); ASSERT_TRUE(decomposed.has_value()); - auto restoredMatrix = BasisDecomposerTest::restore(*decomposed, basisGate); + auto restoredMatrix = + BasisDecomposerTest::restore(*decomposed, basisMatrix); // Reconstruction accumulates the Weyl diagonalization residual through up // to three entangler layers, so allow a correspondingly relaxed tolerance. @@ -129,7 +127,7 @@ TEST(BasisDecomposerTest, Random) { } TEST(BasisDecomposerNumBasisTest, ForcesZeroBasisUsesForIdentityTarget) { - const Gate basis{.type = GateKind::X, .parameter = {}, .qubitId = {0, 1}}; + const auto basis = cxGate01(); const auto decomposer = TwoQubitBasisDecomposer::create(basis, 1.0); const Matrix4x4 target = Matrix4x4::identity(); const auto weyl = @@ -144,10 +142,9 @@ TEST(BasisDecomposerNumBasisTest, ForcesZeroBasisUsesForIdentityTarget) { INSTANTIATE_TEST_SUITE_P( ProductTwoQubitMatrices, BasisDecomposerTest, testing::Combine( - // basis gates - testing::Values( - Gate{.type = GateKind::X, .parameter = {}, .qubitId = {0, 1}}, - Gate{.type = GateKind::X, .parameter = {}, .qubitId = {1, 0}}), + // basis entanglers + testing::Values([]() -> Matrix4x4 { return cxGate01(); }, + []() -> Matrix4x4 { return cxGate10(); }), // targets to be decomposed testing::Values([]() -> Matrix4x4 { return Matrix4x4::identity(); }, []() -> Matrix4x4 { @@ -160,10 +157,9 @@ INSTANTIATE_TEST_SUITE_P( INSTANTIATE_TEST_SUITE_P( TwoQubitMatrices, BasisDecomposerTest, testing::Combine( - // basis gates - testing::Values( - Gate{.type = GateKind::X, .parameter = {}, .qubitId = {0, 1}}, - Gate{.type = GateKind::X, .parameter = {}, .qubitId = {1, 0}}), + // basis entanglers + testing::Values([]() -> Matrix4x4 { return cxGate01(); }, + []() -> Matrix4x4 { return cxGate10(); }), // targets to be decomposed ::testing::Values( []() -> Matrix4x4 { return rzzMatrix(2.0); }, @@ -182,9 +178,5 @@ INSTANTIATE_TEST_SUITE_P( kron(rxMatrix(1.0), Matrix2x2::identity()); }, []() -> Matrix4x4 { - return kron(hGate(), ipz()) * - getTwoQubitMatrix({.type = GateKind::X, - .parameter = {}, - .qubitId = {0, 1}}) * - kron(ipx(), ipy()); + return kron(hGate(), ipz()) * cxGate01() * kron(ipx(), ipy()); }))); diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_get_gate_kind.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_get_gate_kind.cpp deleted file mode 100644 index 2247bd474d..0000000000 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_get_gate_kind.cpp +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h" -#include "mlir/Dialect/QCO/IR/QCODialect.h" -#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" -#include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace mlir; -using namespace mlir::qco; - -// NOLINTNEXTLINE(misc-use-internal-linkage) -class DecompositionGetGateKindTest : public ::testing::Test { -protected: - MLIRContext context; - QCOProgramBuilder builder{&context}; - - void SetUp() override { - context.loadDialect(); - context.loadDialect(); - context.loadDialect(); - context.loadDialect(); - builder.initialize(); - } -}; - -TEST_F(DecompositionGetGateKindTest, MapsBareSingleQubitOps) { - Value q = builder.staticQubit(0); - q = builder.rx(0.25, q); - auto mod = builder.finalize(); - ASSERT_TRUE(mod); - RXOp rx; - mod->walk([&](RXOp op) { - rx = op; - return WalkResult::interrupt(); - }); - ASSERT_TRUE(rx); - EXPECT_EQ( - helpers::getGateKind(llvm::cast(rx.getOperation())), - decomposition::GateKind::RX); -} - -TEST_F(DecompositionGetGateKindTest, MapsCtrlBodyNotWrapper) { - Value c = builder.staticQubit(0); - Value t = builder.staticQubit(1); - auto [cOut, tOut] = - builder.ctrl(ValueRange{c}, ValueRange{t}, - [&](ValueRange targets) -> llvm::SmallVector { - return {builder.z(targets[0])}; - }); - (void)cOut; - (void)tOut; - auto mod = builder.finalize(); - ASSERT_TRUE(mod); - CtrlOp ctrl; - mod->walk([&](CtrlOp op) { - ctrl = op; - return WalkResult::interrupt(); - }); - ASSERT_TRUE(ctrl); - EXPECT_EQ( - helpers::getGateKind(llvm::cast(ctrl.getOperation())), - decomposition::GateKind::Z); -} diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_helpers.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_helpers.cpp index 7dc67d549f..e3f863e3cb 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_helpers.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_helpers.cpp @@ -8,7 +8,6 @@ * Licensed under the MIT License */ -#include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" @@ -20,7 +19,6 @@ using namespace mlir::qco; using namespace mlir::qco::helpers; -using namespace mlir::qco::decomposition; TEST(DecompositionHelpersTest, RemEuclidNeverNegative) { EXPECT_DOUBLE_EQ(remEuclid(-1.0, 3.0), 2.0); @@ -40,15 +38,6 @@ TEST(DecompositionHelpersTest, TraceToFidelityMatchesFormula) { EXPECT_DOUBLE_EQ(traceToFidelity(x), (4.0 + (absx * absx)) / 20.0); } -TEST(DecompositionHelpersTest, GetComplexitySingleQubitAndGphase) { - EXPECT_EQ(getComplexity(GateKind::X, 1), 1U); - EXPECT_EQ(getComplexity(GateKind::GPhase, 1), 0U); -} - -TEST(DecompositionHelpersTest, GetComplexityMultiQubitUsesFactorModel) { - EXPECT_EQ(getComplexity(GateKind::RZZ, 2), 10U); -} - TEST(DecompositionHelpersTest, GlobalPhaseFactorUnitMagnitude) { const auto z = globalPhaseFactor(1.25); EXPECT_NEAR(std::abs(z), 1.0, 1e-14); diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index bec1e1fb23..3b17bea882 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -9,7 +9,6 @@ */ #include "decomposition_test_utils.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.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" @@ -120,44 +119,30 @@ INSTANTIATE_TEST_SUITE_P( INSTANTIATE_TEST_SUITE_P( TwoQubitMatrices, WeylDecompositionTest, - ::testing::Values([]() -> Matrix4x4 { return rzzMatrix(2.0); }, - []() -> Matrix4x4 { - return ryyMatrix(1.0) * rzzMatrix(3.0) * rxxMatrix(2.0); - }, - []() -> Matrix4x4 { - return TwoQubitWeylDecomposition::getCanonicalMatrix( - 1.5, -0.2, 0.0) * - kron(rxMatrix(1.0), Matrix2x2::identity()); - }, - []() -> Matrix4x4 { - return kron(rxMatrix(1.0), ryMatrix(1.0)) * - TwoQubitWeylDecomposition::getCanonicalMatrix( - 1.1, 0.2, 3.0) * - kron(rxMatrix(1.0), Matrix2x2::identity()); - }, - []() -> Matrix4x4 { - return kron(hGate(), ipz()) * - getTwoQubitMatrix({.type = GateKind::X, - .parameter = {}, - .qubitId = {0, 1}}) * - kron(ipx(), ipy()); - })); + ::testing::Values( + []() -> Matrix4x4 { return rzzMatrix(2.0); }, + []() -> Matrix4x4 { + return ryyMatrix(1.0) * rzzMatrix(3.0) * rxxMatrix(2.0); + }, + []() -> Matrix4x4 { + return TwoQubitWeylDecomposition::getCanonicalMatrix(1.5, -0.2, 0.0) * + kron(rxMatrix(1.0), Matrix2x2::identity()); + }, + []() -> Matrix4x4 { + return kron(rxMatrix(1.0), ryMatrix(1.0)) * + TwoQubitWeylDecomposition::getCanonicalMatrix(1.1, 0.2, 3.0) * + kron(rxMatrix(1.0), Matrix2x2::identity()); + }, + []() -> Matrix4x4 { + return kron(hGate(), ipz()) * cxGate01() * kron(ipx(), ipy()); + })); INSTANTIATE_TEST_SUITE_P( SpecializedMatrices, WeylDecompositionTest, ::testing::Values( // id + controlled + general already covered by other parametrizations // swap equiv - []() -> Matrix4x4 { - return getTwoQubitMatrix({.type = GateKind::X, - .parameter = {}, - .qubitId = {0, 1}}) * - getTwoQubitMatrix({.type = GateKind::X, - .parameter = {}, - .qubitId = {1, 0}}) * - getTwoQubitMatrix( - {.type = GateKind::X, .parameter = {}, .qubitId = {0, 1}}); - }, + []() -> Matrix4x4 { return cxGate01() * cxGate10() * cxGate01(); }, // partial swap equiv []() -> Matrix4x4 { return TwoQubitWeylDecomposition::getCanonicalMatrix(0.5, 0.5, 0.5); @@ -167,13 +152,7 @@ INSTANTIATE_TEST_SUITE_P( return TwoQubitWeylDecomposition::getCanonicalMatrix(0.5, 0.5, -0.5); }, // mirror controlled equiv - []() -> Matrix4x4 { - return getTwoQubitMatrix({.type = GateKind::X, - .parameter = {}, - .qubitId = {0, 1}}) * - getTwoQubitMatrix( - {.type = GateKind::X, .parameter = {}, .qubitId = {1, 0}}); - }, + []() -> Matrix4x4 { return cxGate01() * cxGate10(); }, // sim aab equiv []() -> Matrix4x4 { return TwoQubitWeylDecomposition::getCanonicalMatrix(0.5, 0.5, 0.1); From 5aa7660e17207f258bec1e50425134bf21139be1 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 18 Jun 2026 16:25:52 +0200 Subject: [PATCH 045/122] =?UTF-8?q?=F0=9F=94=A5=20Remove=20dead=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/Decomposition/Helpers.h | 11 ------ .../Decomposition/UnitaryMatrices.h | 2 -- .../Decomposition/WeylDecomposition.h | 2 -- .../QCO/Transforms/NativeSynthesis/Utils.h | 13 +------ .../mlir/Dialect/QCO/Transforms/Passes.td | 3 +- .../QCO/Transforms/Decomposition/Helpers.cpp | 23 +------------ .../Decomposition/UnitaryMatrices.cpp | 9 ----- .../Decomposition/WeylDecomposition.cpp | 10 ++---- .../QCO/Transforms/NativeSynthesis/Utils.cpp | 34 ------------------- .../Decomposition/decomposition_test_utils.h | 11 +----- .../test_decomposition_helpers.cpp | 7 ---- .../native_synthesis_test_helpers.cpp | 6 ---- .../native_synthesis_test_helpers.h | 1 - mlir/unittests/programs/qc_programs.cpp | 10 ------ mlir/unittests/programs/qc_programs.h | 8 +---- 15 files changed, 8 insertions(+), 142 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h index bd1bb58daf..4d45854321 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h @@ -23,23 +23,12 @@ namespace mlir::qco::helpers { [[nodiscard]] bool isUnitaryMatrix(const Matrix2x2& matrix, double tolerance = 1e-12); -/// Check whether `matrix` is unitary within `tolerance` (i.e. `M^H M` is -/// approximately the identity). -[[nodiscard]] bool isUnitaryMatrix(const Matrix4x4& matrix, - double tolerance = 1e-12); - /** * Euclidean remainder of a modulo b. * The returned value is never negative. */ [[nodiscard]] double remEuclid(double a, double b); -/** - * Wrap angle into interval [-pi, pi). If within atol of the endpoint, clamp to - * -pi. - */ -[[nodiscard]] double mod2pi(double angle, double angleZeroEpsilon = 1e-13); - /** * Convert a two-qubit trace overlap into the average gate fidelity metric used * by the decomposition cost code. diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h index 3eca091001..cb6fd56389 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h @@ -31,8 +31,6 @@ inline constexpr double FRAC1_SQRT2 = /// Generic 3-parameter single-qubit unitary `U(theta, phi, lambda)`. [[nodiscard]] Matrix2x2 uMatrix(double theta, double phi, double lambda); -/// `U2(phi, lambda) == U(pi/2, phi, lambda)`. -[[nodiscard]] Matrix2x2 u2Matrix(double phi, double lambda); /// Axis rotations `exp(-i theta/2 * sigma_{x,y,z})`. [[nodiscard]] Matrix2x2 rxMatrix(double theta); [[nodiscard]] Matrix2x2 ryMatrix(double theta); diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.h index 896279ffb9..7f9bda7a30 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.h @@ -235,7 +235,5 @@ class TwoQubitWeylDecomposition { Specialization specialization{Specialization::General}; /// Optional `traceToFidelity` floor for specialization; unset disables it. std::optional requestedFidelity; - double calculatedFidelity{}; - Matrix4x4 unitaryMatrix; }; } // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h index ac05307042..c399ff3eb4 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h @@ -17,7 +17,7 @@ #include -/// F64 helpers, global phase, and SU(4) normalization for two-qubit synthesis. +/// F64 helpers and block unitary extraction for native gate synthesis. namespace mlir::qco::native_synth { @@ -30,17 +30,6 @@ std::optional getConstantF64(Value value); /// Emit a `qco.gphase` if `phase` is non-negligible. void emitGPhaseIfNonTrivial(IRRewriter& rewriter, Location loc, double phase); -/// Matrix equality up to a unit-modulus global phase. -bool isEquivalentUpToGlobalPhase(const Matrix4x4& lhs, const Matrix4x4& rhs, - double atol = 1e-10); - -/// Rescale `matrix` to determinant 1 (SU(4)) for Weyl / basis decomposers. -/// No-op if det is numerically zero. -void normalizeToSU4(Matrix4x4& matrix); - -/// ``getUnitaryMatrix4x4`` then rescale to SU(4). -bool getNormalizedTwoQubitMatrix(UnitaryOpInterface unitary, Matrix4x4& matrix); - /// 4x4 for a 2q block member (plain 2q, ``CtrlOp`` CX/CZ, or lifted 1q). Fails /// for barriers, ``gphase``, multi-control, or non-constant matrix parameters. bool getBlockTwoQubitMatrix(Operation* op, Matrix4x4& matrix); diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index 6cd6d8f811..4cf63e8ff9 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -166,8 +166,7 @@ def NativeGateSynthesisPass : Pass<"native-gate-synthesis", "mlir::ModuleOp"> { Recognised tokens: `u`, `x`, `sx`, `rz` (or `p`), `rx`, `ry`, `r`, `cx`, `cz`, `rzz`. An empty or whitespace-only menu is a no-op, which is the intended pipeline default when synthesis is not needed. An unrecognised - token or an invalid score weight (non-finite or negative) causes the pass - to fail. + token causes the pass to fail. Example menus (each line is one illustrative menu; pick either `cx` or `cz` as the entangler, or list both if both are native): diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp index 752cb083c1..9a449f3902 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp @@ -16,7 +16,6 @@ #include #include -#include namespace mlir::qco::helpers { @@ -24,34 +23,14 @@ bool isUnitaryMatrix(const Matrix2x2& matrix, double tolerance) { return (matrix.adjoint() * matrix).isIdentity(tolerance); } -bool isUnitaryMatrix(const Matrix4x4& matrix, double tolerance) { - return (matrix.adjoint() * matrix).isIdentity(tolerance); -} - double remEuclid(double a, double b) { if (b == 0.0) { - llvm::reportFatalInternalError( - "remEuclid expects non-zero divisor; callers like mod2pi pass positive " - "constants"); + llvm::reportFatalInternalError("remEuclid expects non-zero divisor"); } 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) { - // Canonicalize the upper endpoint back to -pi so callers always receive a - // half-open interval [-pi, pi). - return -std::numbers::pi; - } - return wrapped; -} - double traceToFidelity(const std::complex& x) { // Average two-qubit process fidelity given the Hilbert-Schmidt overlap // `x = tr(U_target^dag * U_actual)`. For a 4x4 unitary the general formula is diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp index 95d9bccf6b..4b2f88137b 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp @@ -32,15 +32,6 @@ Matrix2x2 uMatrix(double theta, double phi, double lambda) { std::sin(lambda + phi) * cosHalf}); } -Matrix2x2 u2Matrix(double phi, double lambda) { - return Matrix2x2::fromElements( - Complex{FRAC1_SQRT2, 0.}, - Complex{-std::cos(lambda) * FRAC1_SQRT2, -std::sin(lambda) * FRAC1_SQRT2}, - Complex{std::cos(phi) * FRAC1_SQRT2, std::sin(phi) * FRAC1_SQRT2}, - Complex{std::cos(lambda + phi) * FRAC1_SQRT2, - std::sin(lambda + phi) * FRAC1_SQRT2}); -} - Matrix2x2 rxMatrix(double theta) { const auto halfTheta = theta / 2.; const Complex cos{std::cos(halfTheta), 0.}; diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.cpp index 21d7c6cb59..4a1c5798e9 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.cpp @@ -231,9 +231,6 @@ TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, decomposition.k2r_ = K2r; decomposition.specialization = Specialization::General; 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((kron(K1l, K1r) * decomposition.getCanonicalMatrix() * kron(K2l, K2r) * @@ -256,17 +253,16 @@ TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, // use trace to calculate fidelity of applied specialization and // adjust global phase auto trace = getTrace(); - decomposition.calculatedFidelity = helpers::traceToFidelity(trace); + const double 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) { + 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)); + calculatedFidelity, *decomposition.requestedFidelity)); } decomposition.globalPhase_ += std::arg(trace); diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp index 37bed956f4..770fc738d1 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp @@ -52,40 +52,6 @@ void emitGPhaseIfNonTrivial(IRRewriter& rewriter, Location loc, double phase) { } } -bool isEquivalentUpToGlobalPhase(const Matrix4x4& lhs, const Matrix4x4& rhs, - double atol) { - const Complex overlap = (rhs.adjoint() * lhs).trace(); - if (std::abs(overlap) <= atol) { - return false; - } - const Complex factor = overlap / std::abs(overlap); - return lhs.isApprox(rhs * factor, atol); -} - -void normalizeToSU4(Matrix4x4& matrix) { - using namespace std::complex_literals; - const Complex det = matrix.determinant(); - // Project `matrix` into SU(4) by dividing out the fourth root of its - // determinant (det(SU(N)) == 1). `|det|^{-1/4}` fixes the magnitude and - // `exp(-i * arg(det) / 4)` removes the global phase so the Weyl - // decomposition downstream operates on a special-unitary input. - if (std::abs(det) > 1e-16) { - matrix *= - std::pow(std::abs(det), -0.25) * std::exp(1i * (-std::arg(det) / 4.0)); - } -} - -bool getNormalizedTwoQubitMatrix(UnitaryOpInterface unitary, - Matrix4x4& matrix) { - Matrix4x4 raw; - if (!unitary.getUnitaryMatrix4x4(raw)) { - return false; - } - matrix = raw; - normalizeToSU4(matrix); - return true; -} - bool getBlockTwoQubitMatrix(Operation* op, Matrix4x4& matrix) { if (llvm::isa(op)) { return false; diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/decomposition_test_utils.h b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/decomposition_test_utils.h index a5a5091523..e81b725308 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/decomposition_test_utils.h +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/decomposition_test_utils.h @@ -77,15 +77,6 @@ randomUnitaryData(std::size_t dim, std::mt19937& rng) { } // namespace detail -/// Random `2×2` unitary matrix. -[[nodiscard]] inline Matrix2x2 randomUnitary2x2(std::mt19937& rng) { - const auto data = detail::randomUnitaryData(2, rng); - const Matrix2x2 unitary = - Matrix2x2::fromElements(data[0], data[1], data[2], data[3]); - assert(helpers::isUnitaryMatrix(unitary)); - return unitary; -} - /// Random `4×4` unitary matrix. [[nodiscard]] inline Matrix4x4 randomUnitary4x4(std::mt19937& rng) { const auto data = detail::randomUnitaryData(4, rng); @@ -93,7 +84,7 @@ randomUnitaryData(std::size_t dim, std::mt19937& rng) { data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], data[8], data[9], data[10], data[11], data[12], data[13], data[14], data[15]); - assert(helpers::isUnitaryMatrix(unitary)); + assert((unitary.adjoint() * unitary).isIdentity(1e-12)); return unitary; } diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_helpers.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_helpers.cpp index e3f863e3cb..e868e1d1a9 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_helpers.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_helpers.cpp @@ -15,7 +15,6 @@ #include #include -#include using namespace mlir::qco; using namespace mlir::qco::helpers; @@ -26,12 +25,6 @@ TEST(DecompositionHelpersTest, RemEuclidNeverNegative) { EXPECT_DOUBLE_EQ(remEuclid(0.0, 2.5), 0.0); } -TEST(DecompositionHelpersTest, Mod2piWrapsIntoHalfOpenInterval) { - EXPECT_NEAR(mod2pi(0.0), 0.0, 1e-14); - EXPECT_NEAR(mod2pi(std::numbers::pi), -std::numbers::pi, 1e-12); - EXPECT_NEAR(mod2pi(3.0 * std::numbers::pi), -std::numbers::pi, 1e-12); -} - TEST(DecompositionHelpersTest, TraceToFidelityMatchesFormula) { const std::complex x{3.0, 4.0}; const double absx = 5.0; diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp index d04cc600f6..851ddffce3 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp @@ -13,7 +13,6 @@ #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/Gate.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" @@ -158,11 +157,6 @@ Matrix2x2 u3Matrix(double theta, double phi, double lambda) { return decomposition::uMatrix(theta, phi, lambda); } -bool isUnitary(const Matrix2x2& matrix, const double atol) { - return (matrix * matrix.adjoint()).isIdentity(atol) && - (matrix.adjoint() * matrix).isIdentity(atol); -} - std::optional evaluateConstF64(Value value) { if (!value) { return std::nullopt; diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.h b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.h index 3a1b58c58c..e1b362a298 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.h +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.h @@ -84,7 +84,6 @@ class TestMatrix { [[nodiscard]] std::complex phasedAmplitude(double magnitude, double phase); [[nodiscard]] Matrix2x2 u3Matrix(double theta, double phi, double lambda); -[[nodiscard]] bool isUnitary(const Matrix2x2& matrix, double atol = 1e-10); [[nodiscard]] std::optional evaluateConstF64(Value value); bool extractSingleQubitMatrix(qco::UnitaryOpInterface op, Matrix2x2& out); bool extractTwoQubitMatrix(qco::UnitaryOpInterface op, Matrix4x4& out); diff --git a/mlir/unittests/programs/qc_programs.cpp b/mlir/unittests/programs/qc_programs.cpp index bff40cccfe..e91e734cc0 100644 --- a/mlir/unittests/programs/qc_programs.cpp +++ b/mlir/unittests/programs/qc_programs.cpp @@ -2116,16 +2116,6 @@ void nativeSynthCustomMenusXxMinusYyOnly(QCProgramBuilder& b) { b.xx_minus_yy(-0.37, 0.26, q0, q1); } -void nativeSynthScoringXxPlusYyOnly(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.xx_plus_yy(0.52, -0.14, q0, q1); -} - -void nativeSynthScoringXxMinusYyOnly(QCProgramBuilder& b) { - nativeSynthCustomMenusXxMinusYyOnly(b); -} - void nativeSynthDeterminismTwoQubitSwap(QCProgramBuilder& b) { const auto q0 = b.allocQubit(); const auto q1 = b.allocQubit(); diff --git a/mlir/unittests/programs/qc_programs.h b/mlir/unittests/programs/qc_programs.h index 88fb012d0f..793c829d0d 100644 --- a/mlir/unittests/programs/qc_programs.h +++ b/mlir/unittests/programs/qc_programs.h @@ -1069,15 +1069,9 @@ void nativeSynthCustomMenusIbmFractionalTwoQStress(QCProgramBuilder& b); /// ``H``, ``SX``, ``XX+YY``, ``RZ``; custom-menu ``XX+YY`` chain behaviour. void nativeSynthCustomMenusXxPlusYyChain(QCProgramBuilder& b); -/// Single ``XX-YY`` on a pair; custom menu / scoring delegate shape. +/// Single ``XX-YY`` on a pair; custom-menu delegate shape. void nativeSynthCustomMenusXxMinusYyOnly(QCProgramBuilder& b); -/// Single ``XX+YY`` on a pair; scoring metrics on emitted counts. -void nativeSynthScoringXxPlusYyOnly(QCProgramBuilder& b); - -/// Forwards to ``nativeSynthCustomMenusXxMinusYyOnly``; scoring-only alias. -void nativeSynthScoringXxMinusYyOnly(QCProgramBuilder& b); - /// Two-qubit ``swap`` with explicit ``allocQubit`` / ``dealloc`` ordering. void nativeSynthDeterminismTwoQubitSwap(QCProgramBuilder& b); From bd07e69570a0ba167d549c3f1cfabf293ca5bf07 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 18 Jun 2026 16:48:59 +0200 Subject: [PATCH 046/122] =?UTF-8?q?=E2=9C=A8=20Add=20`fuse-two-qubit-unita?= =?UTF-8?q?ry-runs`=20pass=20for=20fusing=20compile-time=20two-qubit=20uni?= =?UTF-8?q?tary=20windows=20via=20Weyl/KAK=20resynthesis.=20Update=20chang?= =?UTF-8?q?elog=20and=20remove=20obsolete=20files=20related=20to=20two-qub?= =?UTF-8?q?it=20window=20consolidation.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 4 + .../NativeSynthesis/FuseTwoQubitUnitaryRuns.h | 30 +++ .../NativeSynthesis/PassTwoQubitWindows.h | 78 ------- .../QCO/Transforms/NativeSynthesis/Utils.h | 7 + .../mlir/Dialect/QCO/Transforms/Passes.td | 32 ++- ...indows.cpp => FuseTwoQubitUnitaryRuns.cpp} | 193 +++++++++--------- .../QCO/Transforms/NativeSynthesis/Pass.cpp | 13 +- .../QCO/Transforms/NativeSynthesis/Utils.cpp | 17 ++ .../Transforms/NativeSynthesis/CMakeLists.txt | 1 + .../test_fuse_two_qubit_unitary_runs.cpp | 105 ++++++++++ 10 files changed, 289 insertions(+), 191 deletions(-) create mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.h delete mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.h rename mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/{PassTwoQubitWindows.cpp => FuseTwoQubitUnitaryRuns.cpp} (58%) create mode 100644 mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d0330315a..f99a2dfeaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ with the exception that minor releases may include breaking changes. ### Added +- ✨ Add a `fuse-two-qubit-unitary-runs` pass + for fusing compile-time two-qubit unitary windows via Weyl/KAK resynthesis + ([#1655]) ([**@simon1hofmann**]) - ✨ Add a `fuse-single-qubit-unitary-runs` pass for fusing compile-time single-qubit unitary runs via Euler resynthesis ([#1672]) ([**@simon1hofmann**], [**@burgholzer**]) @@ -631,6 +634,7 @@ changelogs._ [#1664]: https://github.com/munich-quantum-toolkit/core/pull/1664 [#1662]: https://github.com/munich-quantum-toolkit/core/pull/1662 [#1660]: https://github.com/munich-quantum-toolkit/core/pull/1660 +[#1655]: https://github.com/munich-quantum-toolkit/core/pull/1655 [#1652]: https://github.com/munich-quantum-toolkit/core/pull/1652 [#1638]: https://github.com/munich-quantum-toolkit/core/pull/1638 [#1637]: https://github.com/munich-quantum-toolkit/core/pull/1637 diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.h new file mode 100644 index 0000000000..a09f980139 --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.h @@ -0,0 +1,30 @@ +/* + * 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 + */ + +/// \file +/// Fuse maximal two-qubit unitary windows (with absorbed single-qubit padding). + +#pragma once + +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" + +#include +#include +#include + +namespace mlir::qco::native_synth { + +/// Scan `root` for maximal two-qubit windows (including absorbed single-qubit +/// ops on the same wire pair) and replace each window when Weyl/KAK +/// resynthesis to the native profile is profitable. +LogicalResult fuseTwoQubitUnitaryRuns(IRRewriter& rewriter, Operation* root, + const NativeProfileSpec& spec); + +} // namespace mlir::qco::native_synth diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.h deleted file mode 100644 index 2aa9437c3e..0000000000 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.h +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -/// \file -/// Helpers for `NativeGateSynthesisPass` two-qubit window consolidation. - -#pragma once - -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" -#include "mlir/Dialect/QCO/Utils/Matrix.h" - -#include -#include -#include -#include -#include -#include - -#include -#include - -namespace mlir::qco::native_synth { - -/// State for one maximal two-qubit window (plus absorbed one-qubit ops) -/// during consolidation. -struct TwoQubitBlock { - Value wireA; - Value wireB; - llvm::SmallVector ops; - Matrix4x4 accum = Matrix4x4::identity(); - unsigned numTwoQ = 0; - unsigned numOneQ = 0; - bool anyNonNative = false; - bool open = true; -}; - -/// Pre-order walk: every op implementing `UnitaryOpInterface` under `root`. -void collectUnitaryOpsInPreOrder(Operation* root, - llvm::SmallVectorImpl& ops); - -/// Tracks overlapping two-qubit windows on a module slice. -struct TwoQubitWindowConsolidator { - /// Append-only list of windows discovered so far; closed windows are kept - /// so `materialize()` can still rewrite them. - std::vector blocks; - /// Maps each currently-open SSA qubit value to the index of the block - /// that owns its trailing wire. - llvm::DenseMap wireToBlock; - - /// Mark block `idx` as closed and remove its tracked wires from - /// `wireToBlock`. - void closeBlock(size_t idx); - - /// If `v` is currently tracked, close the block that owns it; otherwise - /// do nothing. Used at synchronization points (barriers, fan-out, etc.). - void closeBlockOnWire(Value v); - - /// State-machine step for one IR op, called in pre-order walk order. - /// Extends an existing window, starts a fresh one, or closes conflicting - /// windows depending on the op's kind and operand use pattern. - void process(Operation* op, const NativeProfileSpec& spec); - - /// Rewrite each collected window whose accumulated unitary can be realized - /// with strictly fewer entanglers (or that contains non-native ops). The - /// deterministic two-qubit synthesizer emits the replacement through - /// `rewriter`. - LogicalResult materialize(IRRewriter& rewriter, - const NativeProfileSpec& spec); -}; - -} // namespace mlir::qco::native_synth diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h index c399ff3eb4..fbf70131b8 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h @@ -13,6 +13,8 @@ #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" +#include +#include #include #include @@ -34,4 +36,9 @@ void emitGPhaseIfNonTrivial(IRRewriter& rewriter, Location loc, double phase); /// for barriers, ``gphase``, multi-control, or non-constant matrix parameters. bool getBlockTwoQubitMatrix(Operation* op, Matrix4x4& matrix); +/// Pre-order walk: every op implementing `UnitaryOpInterface` under `root`, +/// excluding bodies nested under `ctrl` / `inv`. +void collectUnitaryOpsInPreOrder(Operation* root, + llvm::SmallVectorImpl& ops); + } // namespace mlir::qco::native_synth diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index 4cf63e8ff9..0b66c68578 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -66,6 +66,34 @@ def FuseSingleQubitUnitaryRuns "Target Euler basis (zyz, zxz, xzx, xyx, u, zsxx).">]; } +def FuseTwoQubitUnitaryRuns + : Pass<"fuse-two-qubit-unitary-runs", "mlir::ModuleOp"> { + let dependentDialects = ["mlir::qco::QCODialect"]; + let summary = "Fuse two-qubit unitary runs using Weyl/KAK resynthesis"; + let description = [{ + Scans the module for maximal two-qubit windows: contiguous sequences of + two-qubit unitaries on the same wire pair, with single-qubit gates on those + wires absorbed into the window's accumulated `4×4` unitary when they have + a single use. Each window with at least two ops is replaced when beneficial: + when the window contains any gate outside the `native-gates` menu, or when + deterministic Weyl/KAK resynthesis to that menu uses strictly fewer + entanglers than the window already contains. + + The `native-gates` option uses the same comma-separated token list as + `native-gate-synthesis` (e.g. `u,cx`, `x,sx,rz,cx`). An empty or + whitespace-only menu is a no-op. An unrecognised token causes the pass to + fail. + + Barriers, global phase, fan-out, and ops on more than two qubits close + open windows. Bodies nested under `qco.ctrl` or `qco.inv` are not tracked + independently. + }]; + let options = [Option< + "nativeGates", "native-gates", "std::string", "\"\"", + "Comma-separated native gate menu. Empty or whitespace-only is " + "a no-op. Tokens: u, x, sx, rz (or p), rx, ry, r, cx, cz, rzz.">]; +} + def QuantumLoopUnroll : InterfacePass<"quantum-loop-unroll", "FunctionOpInterface"> { let dependentDialects = ["mlir::qco::QCODialect", "mlir::scf::SCFDialect"]; @@ -178,8 +206,8 @@ def NativeGateSynthesisPass : Pass<"native-gate-synthesis", "mlir::ModuleOp"> { Supported pairs are exactly `rx`+`rz`, `rx`+`ry`, and `ry`+`rz`. Execution order (mirrors the implementation): fuse consecutive - single-qubit runs; consolidate two-qubit windows (including absorbed - single-qubit padding); run up to four synthesis sweeps over remaining + single-qubit runs; fuse two-qubit windows (including absorbed + single-qubit padding) via `fuse-two-qubit-unitary-runs`; run up to four synthesis sweeps over remaining non-native unitaries until every single-qubit op matches the menu (two-qubit lowering may temporarily emit off-menu 1q ops that later sweeps absorb—if any remain after that cap, the pass fails); fuse 1q seams between two-qubit diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp similarity index 58% rename from mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp rename to mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp index 44b902d13a..6ddbfb9580 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp @@ -8,20 +8,23 @@ * Licensed under the MIT License */ -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h" +#include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" +#include #include #include #include +#include #include #include #include @@ -31,14 +34,50 @@ #include #include #include +#include +#include + +namespace mlir::qco { + +#define GEN_PASS_DEF_FUSETWOQUBITUNITARYRUNS +#include "mlir/Dialect/QCO/Transforms/Passes.h.inc" + +} // namespace mlir::qco namespace mlir::qco::native_synth { +namespace { + +/// State for one maximal two-qubit window (plus absorbed one-qubit ops) +/// during consolidation. +struct TwoQubitBlock { + Value wireA; + Value wireB; + llvm::SmallVector ops; + Matrix4x4 accum = Matrix4x4::identity(); + unsigned numTwoQ = 0; + unsigned numOneQ = 0; + bool anyNonNative = false; + bool open = true; +}; + +/// Tracks overlapping two-qubit windows on a module slice. +struct TwoQubitWindowConsolidator { + std::vector blocks; + llvm::DenseMap wireToBlock; + + void closeBlock(size_t idx); + void closeBlockOnWire(Value v); + void process(Operation* op, const NativeProfileSpec& spec); + LogicalResult materialize(IRRewriter& rewriter, + const NativeProfileSpec& spec); +}; + /// Check whether a two-qubit op `op` is already expressible by the resolved /// native menu: a single-control `CX`/`CZ` consistent with the active /// entangler, or `Rzz` when `spec.allowRzz` is set. Multi-control and other /// two-qubit ops are considered non-native. -static bool isNativeTwoQubitOp(Operation* op, const NativeProfileSpec& spec) { +bool isNativeTwoQubitOp(Operation* op, const NativeProfileSpec& spec) { if (auto ctrl = llvm::dyn_cast(op)) { if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { return false; @@ -58,25 +97,18 @@ static bool isNativeTwoQubitOp(Operation* op, const NativeProfileSpec& spec) { /// Decide whether replacing a consolidated window is worthwhile. Always /// replace a window that contains any non-native op (we have to lower them /// anyway); otherwise only replace when the deterministic synthesizer uses -/// strictly fewer entanglers than the window already contains. (Leftover -/// single-qubit gates are cleaned up by the surrounding fuse passes, so the -/// 1q count is not part of the decision.) -static bool shouldApplyBlockReplacement(const TwoQubitBlock& block, - std::uint8_t numBasisUses) { +/// strictly fewer entanglers than the window already contains. +bool shouldApplyBlockReplacement(const TwoQubitBlock& block, + std::uint8_t numBasisUses) { if (block.anyNonNative) { return true; } return numBasisUses < block.numTwoQ; } -/// Emit the deterministic native synthesis of `block.accum` at the location of -/// the window's first op, rewire the block's trailing SSA values (`wireA`, -/// `wireB`) to the newly emitted outputs, and erase the replaced ops in -/// reverse order so def-use edges are cleared before their defining ops -/// disappear. -static LogicalResult -materializeSingleTwoQubitBlock(IRRewriter& rewriter, const TwoQubitBlock& block, - const NativeProfileSpec& spec) { +LogicalResult materializeSingleTwoQubitBlock(IRRewriter& rewriter, + const TwoQubitBlock& block, + const NativeProfileSpec& spec) { Operation* firstOp = block.ops.front(); auto firstUnitary = llvm::cast(firstOp); const Value inA = firstUnitary.getInputQubit(0); @@ -100,21 +132,6 @@ materializeSingleTwoQubitBlock(IRRewriter& rewriter, const TwoQubitBlock& block, return success(); } -void collectUnitaryOpsInPreOrder(Operation* root, - llvm::SmallVectorImpl& ops) { - root->walk([&](Operation* op) { - if (op->getParentOfType()) { - return; - } - if (!llvm::isa(op) && op->getParentOfType()) { - return; - } - if (llvm::isa(op)) { - ops.push_back(op); - } - }); -} - void TwoQubitWindowConsolidator::closeBlock(size_t idx) { auto& block = blocks[idx]; if (!block.open) { @@ -133,33 +150,8 @@ void TwoQubitWindowConsolidator::closeBlockOnWire(Value v) { } } -/// State-machine step for one IR op, invoked in walk order over the module. -/// -/// The consolidator tracks a set of *maximal two-qubit windows* -- contiguous -/// slices of the dataflow where at most two qubit wires interact -- so a -/// later pass can re-synthesize each window as a single 4x4 unitary. For -/// each op we update two pieces of state: -/// -/// * `blocks` -- append-only list of `TwoQubitBlock`s. Closed -/// blocks are kept so `materialize()` can rewrite -/// them later. -/// * `wireToBlock` -- maps each *currently-open* SSA qubit Value to the -/// index of the block that still owns it. -/// Re-keyed whenever an op produces a new output -/// Value on a tracked wire. -/// -/// Because `process` is called in pre-order over the IR, when we see an op -/// its input Values have already been processed (or were function -/// arguments). A block stays open for a wire as long as every op consuming -/// that wire is either (a) a single-qubit op absorbable into the block, or -/// (b) another two-qubit op on the *same* pair of wires. Any other -/// consumer -- a barrier, a control, a different pair of wires, a -/// multi-use fork -- closes the block. void TwoQubitWindowConsolidator::process(Operation* op, const NativeProfileSpec& spec) { - // Skip ops nested under `ctrl` / `inv` (e.g. `ctrl { inv { ... } }`, - // `inv { ... }`): handled via the shell op, not as independent gates for - // window tracking. if (op->getParentOfType()) { return; } @@ -170,9 +162,6 @@ void TwoQubitWindowConsolidator::process(Operation* op, if (!unitary) { return; } - // Barriers and stand-alone global-phase ops are not unitaries we can - // absorb; they act as synchronization points that force any block - // touching their operand wires to close. if (llvm::isa(op)) { for (Value v : op->getOperands()) { closeBlockOnWire(v); @@ -181,8 +170,6 @@ void TwoQubitWindowConsolidator::process(Operation* op, } if (unitary.isTwoQubit()) { - // A two-qubit op for which we cannot build a 4x4 matrix is opaque to the - // window model; close any blocks on its inputs and bail out. Matrix4x4 opMatrix; if (!getBlockTwoQubitMatrix(op, opMatrix)) { closeBlockOnWire(unitary.getInputQubit(0)); @@ -191,9 +178,6 @@ void TwoQubitWindowConsolidator::process(Operation* op, } const Value v0 = unitary.getInputQubit(0); const Value v1 = unitary.getInputQubit(1); - // Defensive guard: malformed/degenerated two-qubit ops with identical - // input wires cannot be represented by this window model. Treat them as - // synchronization points and avoid map-iterator aliasing UB below. if (v0 == v1) { closeBlockOnWire(v0); return; @@ -206,27 +190,13 @@ void TwoQubitWindowConsolidator::process(Operation* op, tracked0 ? std::optional(it0->second) : std::nullopt; const std::optional idx1 = tracked1 ? std::optional(it1->second) : std::nullopt; - // "Same block" means the two input wires are currently the (wireA, - // wireB) pair of one existing block -- i.e. this op operates on the - // same pair as the previous two-qubit op in that block. Otherwise the - // op either extends into a *new* pair (merging two blocks, which we - // don't support) or starts a fresh block. const bool sameBlock = idx0.has_value() && idx1.has_value() && *idx0 == *idx1; const bool singleUse = v0.hasOneUse() && v1.hasOneUse(); - // ---- Case A: extend the existing block --------------------------- - // Both inputs belong to the same open block and nothing else uses - // them. Absorb the new gate into the block's accumulated unitary and - // advance the tracked wires to this op's outputs. if (sameBlock && singleUse) { const size_t idx = *idx0; auto& block = blocks[idx]; - // `block.accum` is the composite 4x4 unitary of the gates absorbed so - // far, with qubit 0 == `wireA` and qubit 1 == `wireB`. The incoming - // op's `opMatrix` is in the (v0, v1) operand order, so we reorder it - // to the block's (wireA, wireB) convention before left-multiplying - // (newest gate on the left, matching matrix-times-column-state order). llvm::SmallVector ids; if (v0 == block.wireA && v1 == block.wireB) { ids = {0, 1}; @@ -265,13 +235,6 @@ void TwoQubitWindowConsolidator::process(Operation* op, return; } - // ---- Case B: close overlapping blocks, start a new one ---------- - // The inputs do not form a clean pair on an existing block (fan-out, - // straddling two different blocks, or only one wire tracked). Closing - // the affected blocks prevents wire-to-block aliasing from becoming - // inconsistent -- note the second `if` guards against double-closing - // the same block when both inputs happened to live in it but `sameBlock - // && singleUse` was false (e.g. only fan-out violated). if (idx0.has_value()) { closeBlock(*idx0); } @@ -292,10 +255,6 @@ void TwoQubitWindowConsolidator::process(Operation* op, return; } - // ---- Case C: single-qubit op on a tracked wire ------------------- - // Absorbable into the block's accumulated 4x4 by lifting the 2x2 to the - // appropriate tensor slot. If the wire is not tracked, the op simply - // does not interact with any open block and is left for other passes. if (unitary.isSingleQubit()) { const Value v = unitary.getInputQubit(0); auto it = wireToBlock.find(v); @@ -305,10 +264,6 @@ void TwoQubitWindowConsolidator::process(Operation* op, const size_t idx = it->second; auto& block = blocks[idx]; Matrix2x2 raw; - // `!v.hasOneUse()` is the fan-out guard: if any other op also consumes - // this wire, we cannot soundly absorb this single-qubit gate into the - // block (the sibling user would see the pre-gate state). Close the - // block and let the outer pass rewrite the op individually. if (!unitary.getUnitaryMatrix2x2(raw) || !v.hasOneUse()) { closeBlock(idx); return; @@ -333,9 +288,6 @@ void TwoQubitWindowConsolidator::process(Operation* op, return; } - // ---- Case D: any other unitary (e.g. >2-qubit ops) --------------- - // We can neither absorb nor continue a window through an op of unknown - // arity, so close every block that touches one of its operand wires. for (Value v : op->getOperands()) { closeBlockOnWire(v); } @@ -349,15 +301,10 @@ TwoQubitWindowConsolidator::materialize(IRRewriter& rewriter, if (block.ops.size() < 2) { continue; } - // Rewriting earlier windows can erase ops captured in later windows. - // Track erased op pointers and skip such windows without dereferencing - // potentially dangling `Operation*`. if (llvm::any_of(block.ops, [&](Operation* op) { return erasedOps.contains(op); })) { continue; } - // Leave `block.accum` unnormalized: Weyl folds the stripped global phase - // into the synthesized `gphase`. const auto numBasisUses = twoQubitEntanglerCount(block.accum, spec); if (!numBasisUses) { continue; @@ -375,4 +322,48 @@ TwoQubitWindowConsolidator::materialize(IRRewriter& rewriter, return success(); } +} // namespace + +LogicalResult fuseTwoQubitUnitaryRuns(IRRewriter& rewriter, Operation* root, + const NativeProfileSpec& spec) { + llvm::SmallVector ops; + collectUnitaryOpsInPreOrder(root, ops); + TwoQubitWindowConsolidator consolidator; + for (Operation* op : ops) { + consolidator.process(op, spec); + } + return consolidator.materialize(rewriter, spec); +} + +namespace { + +struct FuseTwoQubitUnitaryRunsPass final + : impl::FuseTwoQubitUnitaryRunsBase { + using Base::Base; + + explicit FuseTwoQubitUnitaryRunsPass(FuseTwoQubitUnitaryRunsOptions options) + : Base(std::move(options)) {} + +protected: + void runOnOperation() override { + if (llvm::StringRef(nativeGates).trim().empty()) { + return; + } + auto specOpt = resolveNativeGatesSpec(nativeGates); + if (!specOpt) { + getOperation().emitError() + << "unsupported native gate menu (native-gates='" << nativeGates + << "')"; + signalPassFailure(); + return; + } + IRRewriter rewriter(&getContext()); + if (failed(fuseTwoQubitUnitaryRuns(rewriter, getOperation(), *specOpt))) { + signalPassFailure(); + } + } +}; + +} // namespace + } // namespace mlir::qco::native_synth diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp index 013be38032..7ab87c95c1 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp @@ -12,12 +12,11 @@ #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/PassTwoQubitWindows.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" #include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" @@ -58,6 +57,7 @@ using native_synth::decomposeToZSXX; using native_synth::emitSingleQubitMatrix; using native_synth::emitterEulerBasis; using native_synth::emitTwoQubitNative; +using native_synth::fuseTwoQubitUnitaryRuns; using native_synth::getBlockTwoQubitMatrix; using native_synth::NativeGateKind; using native_synth::NativeProfileSpec; @@ -65,7 +65,6 @@ using native_synth::resolveNativeGatesSpec; using native_synth::rewriteXXPlusMinusYYViaRzz; using native_synth::SingleQubitEmitterSpec; using native_synth::SingleQubitMode; -using native_synth::TwoQubitWindowConsolidator; using native_synth::usesCxEntangler; using native_synth::usesCzEntangler; @@ -408,13 +407,7 @@ struct NativeGateSynthesisPass /// native sequence exists. LogicalResult consolidateTwoQubitBlocks(IRRewriter& rewriter, const NativeProfileSpec& spec) { - llvm::SmallVector ops; - collectUnitaryOpsInPreOrder(getOperation(), ops); - TwoQubitWindowConsolidator consolidator; - for (Operation* op : ops) { - consolidator.process(op, spec); - } - return consolidator.materialize(rewriter, spec); + return fuseTwoQubitUnitaryRuns(rewriter, getOperation(), spec); } /// One synthesis sweep over the whole function: rewrite every remaining diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp index 770fc738d1..8c7a6ce523 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp @@ -15,6 +15,7 @@ #include "mlir/Dialect/QCO/Utils/Matrix.h" #include +#include #include #include #include @@ -22,6 +23,7 @@ #include #include #include +#include #include #include @@ -88,4 +90,19 @@ bool getBlockTwoQubitMatrix(Operation* op, Matrix4x4& matrix) { return true; } +void collectUnitaryOpsInPreOrder(Operation* root, + llvm::SmallVectorImpl& ops) { + root->walk([&](Operation* op) { + if (op->getParentOfType()) { + return; + } + if (!llvm::isa(op) && op->getParentOfType()) { + return; + } + if (llvm::isa(op)) { + ops.push_back(op); + } + }); +} + } // namespace mlir::qco::native_synth diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt index b42643d889..621ad6c994 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt @@ -10,6 +10,7 @@ set(target_name mqt-core-mlir-unittest-native-synthesis) add_executable( ${target_name} native_synthesis_test_helpers.cpp + test_fuse_two_qubit_unitary_runs.cpp test_native_policy.cpp test_native_spec.cpp test_native_synthesis_pass_custom_menus.cpp diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp new file mode 100644 index 0000000000..7dfe23302b --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp @@ -0,0 +1,105 @@ +/* + * 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 + */ + +// Standalone ``fuse-two-qubit-unitary-runs`` pass tests. + +#include "native_synthesis_pass_test_fixture.h" +#include "native_synthesis_test_helpers.h" +#include "qc_programs.h" + +#include +#include +#include +#include + +using namespace mlir; +using namespace mlir::qco; +using namespace mlir::qco::native_synth_test; + +namespace { + +static void runFuseTwoQubitUnitaryRuns(OwningOpRef& moduleOp, + const std::string& nativeGates) { + PassManager pm(moduleOp->getContext()); + FuseTwoQubitUnitaryRunsOptions opts; + opts.nativeGates = nativeGates; + pm.addPass(createFuseTwoQubitUnitaryRuns(opts)); + ASSERT_TRUE(succeeded(pm.run(*moduleOp))); +} + +template +static std::size_t +countOpsOfTypeInModule(const OwningOpRef& moduleOp) { + std::size_t count = 0; + moduleOp.get()->walk([&](Operation* op) { + if (llvm::isa(op)) { + ++count; + } + }); + return count; +} + +} // namespace + +class FuseTwoQubitUnitaryRunsTest : public NativeSynthesisPassTest {}; + +TEST_F(FuseTwoQubitUnitaryRunsTest, InvalidMenuFailsPass) { + auto moduleOp = mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::nativeSynthFusionCxCx); + runQcToQco(moduleOp); + PassManager pm(moduleOp->getContext()); + FuseTwoQubitUnitaryRunsOptions opts; + opts.nativeGates = "not-a-real-menu"; + pm.addPass(createFuseTwoQubitUnitaryRuns(opts)); + EXPECT_TRUE(failed(pm.run(*moduleOp))); +} + +TEST_F(FuseTwoQubitUnitaryRunsTest, EmptyMenuIsNoOp) { + auto moduleOp = mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::nativeSynthFusionCxCx); + runQcToQco(moduleOp); + const auto before = countOpsOfTypeInModule(moduleOp); + runFuseTwoQubitUnitaryRuns(moduleOp, ""); + EXPECT_EQ(countOpsOfTypeInModule(moduleOp), before); +} + +TEST_F(FuseTwoQubitUnitaryRunsTest, CancelsAdjacentCxPair) { + auto moduleOp = mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::nativeSynthFusionCxCx); + runQcToQco(moduleOp); + runFuseTwoQubitUnitaryRuns(moduleOp, "u,cx"); + EXPECT_EQ(countOpsOfTypeInModule(moduleOp), 0U); +} + +TEST_F(FuseTwoQubitUnitaryRunsTest, PreservesSingleCx) { + auto moduleOp = mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::nativeSynthFusionHadamardCxHadamard); + runQcToQco(moduleOp); + runFuseTwoQubitUnitaryRuns(moduleOp, "u,cx"); + EXPECT_EQ(countOpsOfTypeInModule(moduleOp), 1U); +} + +TEST_F(FuseTwoQubitUnitaryRunsTest, FusesCxThroughInterleavedOneQOps) { + auto buildFn = [&] { + return mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::nativeSynthFusionHCxInterleavedTCx); + }; + auto expected = buildFn(); + runQcToQco(expected); + const auto expectedUnitary = computeTwoQubitUnitaryFromModule(expected); + ASSERT_TRUE(expectedUnitary.has_value()); + + auto moduleOp = buildFn(); + runQcToQco(moduleOp); + runFuseTwoQubitUnitaryRuns(moduleOp, "u,cx"); + const auto fusedUnitary = computeTwoQubitUnitaryFromModule(moduleOp); + ASSERT_TRUE(fusedUnitary.has_value()); + EXPECT_TRUE(isEquivalentUpToGlobalPhase(*expectedUnitary, *fusedUnitary)); +} From fc42ea84130a5d762faf61ddbaf8da092c46c663 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 18 Jun 2026 17:39:31 +0200 Subject: [PATCH 047/122] =?UTF-8?q?=F0=9F=94=A5=20Remove=20and=20merge=20t?= =?UTF-8?q?ests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Compiler/test_compiler_pipeline.cpp | 264 ---- .../Transforms/Decomposition/CMakeLists.txt | 5 +- .../Decomposition/test_basis_decomposer.cpp | 182 --- .../test_decomposition_helpers.cpp | 47 - .../Decomposition/test_weyl_decomposition.cpp | 567 +++++++- .../Transforms/NativeSynthesis/CMakeLists.txt | 11 +- .../native_synthesis_pass_test_fixture.h | 273 ---- .../native_synthesis_test_helpers.cpp | 609 --------- .../native_synthesis_test_helpers.h | 100 -- .../test_fuse_two_qubit_unitary_runs.cpp | 105 -- .../NativeSynthesis/test_native_policy.cpp | 122 -- .../NativeSynthesis/test_native_spec.cpp | 76 -- .../NativeSynthesis/test_native_synthesis.cpp | 1148 +++++++++++++++++ ...est_native_synthesis_pass_custom_menus.cpp | 519 -------- .../test_native_synthesis_pass_fusion.cpp | 445 ------- ...test_native_synthesis_pass_multi_qubit.cpp | 118 -- .../test_native_synthesis_pass_profiles.cpp | 486 ------- mlir/unittests/programs/qc_programs.cpp | 535 -------- mlir/unittests/programs/qc_programs.h | 168 --- 19 files changed, 1692 insertions(+), 4088 deletions(-) delete mode 100644 mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_basis_decomposer.cpp delete mode 100644 mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_helpers.cpp delete mode 100644 mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_pass_test_fixture.h delete mode 100644 mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp delete mode 100644 mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.h delete mode 100644 mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp delete mode 100644 mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_policy.cpp delete mode 100644 mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_spec.cpp create mode 100644 mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis.cpp delete mode 100644 mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_custom_menus.cpp delete mode 100644 mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_fusion.cpp delete mode 100644 mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_multi_qubit.cpp delete mode 100644 mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_profiles.cpp diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 70a0d54620..8dab83823f 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -950,268 +950,4 @@ TEST_F(CompilerPipelineNativeSynthesisConfigTest, EXPECT_TRUE(isEquivalentUpToGlobalPhase(*preU, *postU)); } -namespace { - -struct NativeSynthesisProgramTestCase { - std::string name; - QCProgramBuilderFn qcProgramBuilder; -}; - -struct NativeSynthesisProfileTestCase { - std::string name; - std::string nativeGates; - bool expectUInStage5 = false; - llvm::SmallVector nonNativeOpsToEliminate; -}; - -struct NativeSynthesisStage5TestCase { - NativeSynthesisProgramTestCase program; - NativeSynthesisProfileTestCase profile; -}; - -} // namespace - -static std::ostream& operator<<(std::ostream& os, - const NativeSynthesisProgramTestCase& info) { - return os << "NativeSynthesisProgram{" << info.name << "}"; -} - -static std::ostream& operator<<(std::ostream& os, - const NativeSynthesisProfileTestCase& info) { - return os << "NativeSynthesisProfile{" << info.name << "}"; -} - -static std::ostream& operator<<(std::ostream& os, - const NativeSynthesisStage5TestCase& info) { - return os << info.profile << " / " << info.program; -} - -static mlir::OwningOpRef -buildQCModuleForNativeSynthesisProgram(mlir::MLIRContext* context, - const QCProgramBuilderFn builder) { - auto module = mlir::qc::QCProgramBuilder::build(context, builder.fn); - EXPECT_TRUE(module) << "failed to build QC module"; - return module; -} - -static mlir::CompilationRecord -runPipelineWithNativeSynthesisConfig(mlir::ModuleOp module, - const std::string& nativeGates) { - mlir::QuantumCompilerConfig config; - config.recordIntermediates = true; - config.nativeGates = nativeGates; - - mlir::CompilationRecord record; - mlir::QuantumCompilerPipeline pipeline(config); - EXPECT_TRUE(pipeline.runPipeline(module, &record).succeeded()); - return record; -} - -namespace { - -class CompilerPipelineNativeSynthesisProgramsTest - : public testing::TestWithParam { -protected: - std::unique_ptr context; - - void SetUp() override { - mlir::DialectRegistry registry; - registry.insert(); - context = std::make_unique(); - context->appendDialectRegistry(registry); - context->loadAllAvailableDialects(); - } -}; - -} // namespace - -TEST_P(CompilerPipelineNativeSynthesisProgramsTest, - SynthesizesHOperationsInStage5) { - const auto& testCase = GetParam(); - auto module = buildQCModuleForNativeSynthesisProgram( - context.get(), testCase.program.qcProgramBuilder); - ASSERT_TRUE(module); - - const auto record = runPipelineWithNativeSynthesisConfig( - module.get(), testCase.profile.nativeGates); - - for (const auto& opName : testCase.profile.nonNativeOpsToEliminate) { - ASSERT_NE(record.afterQCOCanon.find(opName), std::string::npos) - << "Program must contain " << opName << " after QCO canonicalization"; - EXPECT_EQ(record.afterOptimization.find(opName), std::string::npos); - } - if (testCase.profile.expectUInStage5) { - EXPECT_NE(record.afterOptimization.find("qco.u"), std::string::npos); - } -} - -INSTANTIATE_TEST_SUITE_P( - NativeSynthesisStage5Programs, CompilerPipelineNativeSynthesisProgramsTest, - testing::Values( - NativeSynthesisStage5TestCase{ - .program = - NativeSynthesisProgramTestCase{ - "StaticQubitsWithOps", - MQT_NAMED_BUILDER(mlir::qc::staticQubitsWithOps), - }, - .profile = - NativeSynthesisProfileTestCase{ - .name = "IbmBasicCx", - .nativeGates = "x,sx,rz,cx", - .nonNativeOpsToEliminate = {"qco.h"}, - }, - }, - NativeSynthesisStage5TestCase{ - .program = - NativeSynthesisProgramTestCase{ - "ResetMultipleQubitsAfterSingleOp", - MQT_NAMED_BUILDER( - mlir::qc::resetMultipleQubitsAfterSingleOp), - }, - .profile = - NativeSynthesisProfileTestCase{ - .name = "IbmBasicCx", - .nativeGates = "x,sx,rz,cx", - .nonNativeOpsToEliminate = {"qco.h"}, - }, - }, - NativeSynthesisStage5TestCase{ - .program = - NativeSynthesisProgramTestCase{ - "S", - MQT_NAMED_BUILDER(mlir::qc::s), - }, - .profile = - NativeSynthesisProfileTestCase{ - .name = "IbmBasicCx", - .nativeGates = "x,sx,rz,cx", - .nonNativeOpsToEliminate = {"qco.s"}, - }, - }, - NativeSynthesisStage5TestCase{ - .program = - NativeSynthesisProgramTestCase{ - "T", - MQT_NAMED_BUILDER(mlir::qc::t_), - }, - .profile = - NativeSynthesisProfileTestCase{ - .name = "IbmBasicCx", - .nativeGates = "x,sx,rz,cx", - .nonNativeOpsToEliminate = {"qco.t"}, - }, - }, - NativeSynthesisStage5TestCase{ - .program = - NativeSynthesisProgramTestCase{ - "Y", - MQT_NAMED_BUILDER(mlir::qc::y), - }, - .profile = - NativeSynthesisProfileTestCase{ - .name = "IbmBasicCx", - .nativeGates = "x,sx,rz,cx", - .nonNativeOpsToEliminate = {"qco.y"}, - }, - }, - NativeSynthesisStage5TestCase{ - .program = - NativeSynthesisProgramTestCase{ - "StaticQubitsWithOps", - MQT_NAMED_BUILDER(mlir::qc::staticQubitsWithOps), - }, - .profile = - NativeSynthesisProfileTestCase{ - .name = "U3Cx", - .nativeGates = "u,cx", - .expectUInStage5 = true, - .nonNativeOpsToEliminate = {"qco.h"}, - }, - }, - NativeSynthesisStage5TestCase{ - .program = - NativeSynthesisProgramTestCase{ - "ResetMultipleQubitsAfterSingleOp", - MQT_NAMED_BUILDER( - mlir::qc::resetMultipleQubitsAfterSingleOp), - }, - .profile = - NativeSynthesisProfileTestCase{ - .name = "U3Cx", - .nativeGates = "u,cx", - .expectUInStage5 = true, - .nonNativeOpsToEliminate = {"qco.h"}, - }, - }, - NativeSynthesisStage5TestCase{ - .program = - NativeSynthesisProgramTestCase{ - "StaticQubitsWithOps", - MQT_NAMED_BUILDER(mlir::qc::staticQubitsWithOps), - }, - .profile = - NativeSynthesisProfileTestCase{ - .name = "IbmBasicCz", - .nativeGates = "x,sx,rz,cz", - .nonNativeOpsToEliminate = {"qco.h"}, - }, - }, - NativeSynthesisStage5TestCase{ - .program = - NativeSynthesisProgramTestCase{ - "StaticQubitsWithOps", - MQT_NAMED_BUILDER(mlir::qc::staticQubitsWithOps), - }, - .profile = - NativeSynthesisProfileTestCase{ - .name = "IbmFractional", - .nativeGates = "x,sx,rz,rx,rzz,cz", - .nonNativeOpsToEliminate = {"qco.h"}, - }, - }, - NativeSynthesisStage5TestCase{ - .program = - NativeSynthesisProgramTestCase{ - "StaticQubitsWithOps", - MQT_NAMED_BUILDER(mlir::qc::staticQubitsWithOps), - }, - .profile = - NativeSynthesisProfileTestCase{ - .name = "IqmDefault", - .nativeGates = "r,cz", - .nonNativeOpsToEliminate = {"qco.h"}, - }, - }, - NativeSynthesisStage5TestCase{ - .program = - NativeSynthesisProgramTestCase{ - "StaticQubitsWithOps", - MQT_NAMED_BUILDER(mlir::qc::staticQubitsWithOps), - }, - .profile = - NativeSynthesisProfileTestCase{ - .name = "AxisPairRxRzCx", - .nativeGates = "rx,rz,cx", - .nonNativeOpsToEliminate = {"qco.h"}, - }, - }, - NativeSynthesisStage5TestCase{ - .program = - NativeSynthesisProgramTestCase{ - "StaticQubitsWithOps", - MQT_NAMED_BUILDER(mlir::qc::staticQubitsWithOps), - }, - .profile = - NativeSynthesisProfileTestCase{ - .name = "U3Cz", - .nativeGates = "u,cz", - .expectUInStage5 = true, - .nonNativeOpsToEliminate = {"qco.h"}, - }, - })); - } // namespace mqt::test::compiler diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt index c7fd4cf7ec..21985a2967 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt @@ -7,13 +7,14 @@ # Licensed under the MIT License set(target_name mqt-core-mlir-unittest-decomposition) -add_executable(${target_name} test_basis_decomposer.cpp test_decomposition_helpers.cpp - test_euler_decomposition.cpp test_weyl_decomposition.cpp) +add_executable(${target_name} test_euler_decomposition.cpp test_weyl_decomposition.cpp) target_link_libraries( ${target_name} PRIVATE GTest::gtest_main + MLIRQCPrograms MLIRQCOProgramBuilder + MLIRQCToQCO MLIRQCOTransforms MLIRQCOUtils MLIRPass diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_basis_decomposer.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_basis_decomposer.cpp deleted file mode 100644 index c012e24b23..0000000000 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_basis_decomposer.cpp +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "decomposition_test_utils.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.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/Utils/Matrix.h" - -#include -#include - -#include -#include -#include -#include -#include -#include - -using namespace mlir::qco; -using namespace mlir::qco::decomposition; -using namespace mlir::qco::decomposition_test; - -// NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_P` at global scope -class BasisDecomposerTest : public testing::TestWithParam< - std::tuple> { -public: - /// Reconstruct the 4x4 unitary realized by a native two-qubit decomposition. - /// - /// The factors come in `(r, l)` pairs: `factors[2i]` acts on qubit 1 (LSB) - /// and `factors[2i + 1]` on qubit 0 (MSB), mirroring `emitTwoQubitNative`. - /// Each interior pair is followed by one entangler, with a trailing pair - /// after the last entangler. - [[nodiscard]] static Matrix4x4 - restore(const TwoQubitNativeDecomposition& decomposition, - const Matrix4x4& entangler) { - const auto& factors = decomposition.singleQubitFactors; - const auto layer = [&](std::size_t i) { - return kron(factors[(2 * i) + 1], factors[2 * i]); - }; - Matrix4x4 matrix = layer(0); - for (std::uint8_t i = 0; i < decomposition.numBasisUses; ++i) { - matrix = entangler * matrix; - matrix = layer(static_cast(i) + 1) * matrix; - } - return matrix * helpers::globalPhaseFactor(decomposition.globalPhase); - } - -protected: - void SetUp() override { - basisMatrix = std::get<0>(GetParam())(); - target = std::get<1>(GetParam())(); - targetDecomposition = std::make_unique( - TwoQubitWeylDecomposition::create(target, std::optional{1.0})); - } - - Matrix4x4 target; - Matrix4x4 basisMatrix; - std::unique_ptr targetDecomposition; -}; - -TEST_P(BasisDecomposerTest, TestExact) { - const auto& originalMatrix = target; - auto decomposer = TwoQubitBasisDecomposer::create(basisMatrix, 1.0); - auto decomposed = - decomposer.twoQubitDecompose(*targetDecomposition, std::nullopt); - - ASSERT_TRUE(decomposed.has_value()); - - auto restoredMatrix = restore(*decomposed, basisMatrix); - - EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix)); -} - -TEST_P(BasisDecomposerTest, TestApproximation) { - const auto& originalMatrix = target; - auto decomposer = TwoQubitBasisDecomposer::create(basisMatrix, 1.0 - 1e-12); - auto decomposed = - decomposer.twoQubitDecompose(*targetDecomposition, std::nullopt); - - ASSERT_TRUE(decomposed.has_value()); - - auto restoredMatrix = restore(*decomposed, basisMatrix); - - EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix)); -} - -TEST(BasisDecomposerTest, Random) { - constexpr auto maxIterations = 2000; - std::mt19937 rng{123456UL}; - - const llvm::SmallVector basisMatrices{cxGate01(), cxGate10()}; - std::uniform_int_distribution distBasisGate{ - 0, basisMatrices.size() - 1}; - auto selectRandomBasisMatrix = [&]() { - return basisMatrices[distBasisGate(rng)]; - }; - - for (int i = 0; i < maxIterations; ++i) { - auto originalMatrix = randomUnitary4x4(rng); - - auto targetDecomposition = TwoQubitWeylDecomposition::create( - originalMatrix, std::optional{1.0}); - const auto basisMatrix = selectRandomBasisMatrix(); - auto decomposer = TwoQubitBasisDecomposer::create(basisMatrix, 1.0); - auto decomposed = - decomposer.twoQubitDecompose(targetDecomposition, std::nullopt); - - ASSERT_TRUE(decomposed.has_value()); - - auto restoredMatrix = - BasisDecomposerTest::restore(*decomposed, basisMatrix); - - // Reconstruction accumulates the Weyl diagonalization residual through up - // to three entangler layers, so allow a correspondingly relaxed tolerance. - EXPECT_TRUE( - restoredMatrix.isApprox(originalMatrix, SANITY_CHECK_PRECISION)); - } -} - -TEST(BasisDecomposerNumBasisTest, ForcesZeroBasisUsesForIdentityTarget) { - const auto basis = cxGate01(); - const auto decomposer = TwoQubitBasisDecomposer::create(basis, 1.0); - const Matrix4x4 target = Matrix4x4::identity(); - const auto weyl = - TwoQubitWeylDecomposition::create(target, std::optional{1.0}); - const auto decomposed = decomposer.twoQubitDecompose(weyl, std::uint8_t{0}); - ASSERT_TRUE(decomposed.has_value()); - EXPECT_EQ(decomposed->numBasisUses, 0); - const Matrix4x4 restored = BasisDecomposerTest::restore(*decomposed, basis); - EXPECT_TRUE(restored.isApprox(target)); -} - -INSTANTIATE_TEST_SUITE_P( - ProductTwoQubitMatrices, BasisDecomposerTest, - testing::Combine( - // basis entanglers - testing::Values([]() -> Matrix4x4 { return cxGate01(); }, - []() -> Matrix4x4 { return cxGate10(); }), - // targets to be decomposed - testing::Values([]() -> Matrix4x4 { return Matrix4x4::identity(); }, - []() -> Matrix4x4 { - return kron(rzMatrix(1.0), ryMatrix(3.1)); - }, - []() -> Matrix4x4 { - return kron(Matrix2x2::identity(), rxMatrix(0.1)); - }))); - -INSTANTIATE_TEST_SUITE_P( - TwoQubitMatrices, BasisDecomposerTest, - testing::Combine( - // basis entanglers - testing::Values([]() -> Matrix4x4 { return cxGate01(); }, - []() -> Matrix4x4 { return cxGate10(); }), - // targets to be decomposed - ::testing::Values( - []() -> Matrix4x4 { return rzzMatrix(2.0); }, - []() -> Matrix4x4 { - return ryyMatrix(1.0) * rzzMatrix(3.0) * rxxMatrix(2.0); - }, - []() -> Matrix4x4 { - return TwoQubitWeylDecomposition::getCanonicalMatrix(1.5, -0.2, - 0.0) * - kron(rxMatrix(1.0), Matrix2x2::identity()); - }, - []() -> Matrix4x4 { - return kron(rxMatrix(1.0), ryMatrix(1.0)) * - TwoQubitWeylDecomposition::getCanonicalMatrix(1.1, 0.2, - 3.0) * - kron(rxMatrix(1.0), Matrix2x2::identity()); - }, - []() -> Matrix4x4 { - return kron(hGate(), ipz()) * cxGate01() * kron(ipx(), ipy()); - }))); diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_helpers.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_helpers.cpp deleted file mode 100644 index e868e1d1a9..0000000000 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_helpers.cpp +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h" -#include "mlir/Dialect/QCO/Utils/Matrix.h" - -#include - -#include -#include - -using namespace mlir::qco; -using namespace mlir::qco::helpers; - -TEST(DecompositionHelpersTest, RemEuclidNeverNegative) { - EXPECT_DOUBLE_EQ(remEuclid(-1.0, 3.0), 2.0); - EXPECT_DOUBLE_EQ(remEuclid(7.0, 3.0), 1.0); - EXPECT_DOUBLE_EQ(remEuclid(0.0, 2.5), 0.0); -} - -TEST(DecompositionHelpersTest, TraceToFidelityMatchesFormula) { - const std::complex x{3.0, 4.0}; - const double absx = 5.0; - EXPECT_DOUBLE_EQ(traceToFidelity(x), (4.0 + (absx * absx)) / 20.0); -} - -TEST(DecompositionHelpersTest, GlobalPhaseFactorUnitMagnitude) { - const auto z = globalPhaseFactor(1.25); - EXPECT_NEAR(std::abs(z), 1.0, 1e-14); -} - -TEST(DecompositionHelpersTest, IsUnitaryMatrixRejectsNonUnitary) { - const Matrix2x2 m = Matrix2x2::fromElements(2.0, 0.0, 0.0, 2.0); - EXPECT_FALSE(isUnitaryMatrix(m)); -} - -TEST(DecompositionHelpersTest, IsUnitaryMatrixAcceptsUnitary) { - const Matrix2x2 m = Matrix2x2::identity(); - EXPECT_TRUE(isUnitaryMatrix(m)); -} diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index 3b17bea882..252ecdd9e1 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -9,20 +9,83 @@ */ #include "decomposition_test_utils.h" +#include "mlir/Conversion/QCToQCO/QCToQCO.h" +#include "mlir/Dialect/QC/Builder/QCProgramBuilder.h" +#include "mlir/Dialect/QCO/IR/QCODialect.h" +#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/Helpers.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h" +#include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include +#include #include #include +#include +using namespace mlir; using namespace mlir::qco; using namespace mlir::qco::decomposition; using namespace mlir::qco::decomposition_test; +using namespace mlir::qco::helpers; +using namespace mlir::qco::native_synth; + +// Weyl / basis / helpers. + +TEST(DecompositionHelpersTest, RemEuclidNeverNegative) { + EXPECT_DOUBLE_EQ(remEuclid(-1.0, 3.0), 2.0); + EXPECT_DOUBLE_EQ(remEuclid(7.0, 3.0), 1.0); + EXPECT_DOUBLE_EQ(remEuclid(0.0, 2.5), 0.0); +} + +TEST(DecompositionHelpersTest, TraceToFidelityMatchesFormula) { + const std::complex x{3.0, 4.0}; + const double absx = 5.0; + EXPECT_DOUBLE_EQ(traceToFidelity(x), (4.0 + (absx * absx)) / 20.0); +} + +TEST(DecompositionHelpersTest, GlobalPhaseFactorUnitMagnitude) { + const auto z = globalPhaseFactor(1.25); + EXPECT_NEAR(std::abs(z), 1.0, 1e-14); +} + +TEST(DecompositionHelpersTest, IsUnitaryMatrixRejectsNonUnitary) { + const Matrix2x2 m = Matrix2x2::fromElements(2.0, 0.0, 0.0, 2.0); + EXPECT_FALSE(isUnitaryMatrix(m)); +} + +TEST(DecompositionHelpersTest, IsUnitaryMatrixAcceptsUnitary) { + const Matrix2x2 m = Matrix2x2::identity(); + EXPECT_TRUE(isUnitaryMatrix(m)); +} + +//===----------------------------------------------------------------------===// +// Weyl decomposition +//===----------------------------------------------------------------------===// // NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_P` at global scope class WeylDecompositionTest : public testing::TestWithParam { @@ -137,31 +200,481 @@ INSTANTIATE_TEST_SUITE_P( return kron(hGate(), ipz()) * cxGate01() * kron(ipx(), ipy()); })); +//===----------------------------------------------------------------------===// +// Basis decomposer +//===----------------------------------------------------------------------===// + +// NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_P` at global scope +class BasisDecomposerTest : public testing::TestWithParam< + std::tuple> { +public: + [[nodiscard]] static Matrix4x4 + restore(const TwoQubitNativeDecomposition& decomposition, + const Matrix4x4& entangler) { + const auto& factors = decomposition.singleQubitFactors; + const auto layer = [&](std::size_t i) { + return kron(factors[(2 * i) + 1], factors[2 * i]); + }; + Matrix4x4 matrix = layer(0); + for (std::uint8_t i = 0; i < decomposition.numBasisUses; ++i) { + matrix = entangler * matrix; + matrix = layer(static_cast(i) + 1) * matrix; + } + return matrix * helpers::globalPhaseFactor(decomposition.globalPhase); + } + +protected: + void SetUp() override { + basisMatrix = std::get<0>(GetParam())(); + target = std::get<1>(GetParam())(); + targetDecomposition = std::make_unique( + TwoQubitWeylDecomposition::create(target, std::optional{1.0})); + } + + Matrix4x4 target; + Matrix4x4 basisMatrix; + std::unique_ptr targetDecomposition; +}; + +TEST_P(BasisDecomposerTest, TestExact) { + const auto& originalMatrix = target; + auto decomposer = TwoQubitBasisDecomposer::create(basisMatrix, 1.0); + auto decomposed = + decomposer.twoQubitDecompose(*targetDecomposition, std::nullopt); + + ASSERT_TRUE(decomposed.has_value()); + + auto restoredMatrix = restore(*decomposed, basisMatrix); + + EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix)); +} + +TEST_P(BasisDecomposerTest, TestApproximation) { + const auto& originalMatrix = target; + auto decomposer = TwoQubitBasisDecomposer::create(basisMatrix, 1.0 - 1e-12); + auto decomposed = + decomposer.twoQubitDecompose(*targetDecomposition, std::nullopt); + + ASSERT_TRUE(decomposed.has_value()); + + auto restoredMatrix = restore(*decomposed, basisMatrix); + + EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix)); +} + +TEST(BasisDecomposerTest, Random) { + constexpr auto maxIterations = 2000; + std::mt19937 rng{123456UL}; + + const llvm::SmallVector basisMatrices{cxGate01(), cxGate10()}; + std::uniform_int_distribution distBasisGate{ + 0, basisMatrices.size() - 1}; + auto selectRandomBasisMatrix = [&]() { + return basisMatrices[distBasisGate(rng)]; + }; + + for (int i = 0; i < maxIterations; ++i) { + auto originalMatrix = randomUnitary4x4(rng); + + auto targetDecomposition = TwoQubitWeylDecomposition::create( + originalMatrix, std::optional{1.0}); + const auto basisMatrix = selectRandomBasisMatrix(); + auto decomposer = TwoQubitBasisDecomposer::create(basisMatrix, 1.0); + auto decomposed = + decomposer.twoQubitDecompose(targetDecomposition, std::nullopt); + + ASSERT_TRUE(decomposed.has_value()); + + auto restoredMatrix = + BasisDecomposerTest::restore(*decomposed, basisMatrix); + + // Reconstruction accumulates the Weyl diagonalization residual through up + // to three entangler layers, so allow a correspondingly relaxed tolerance. + EXPECT_TRUE( + restoredMatrix.isApprox(originalMatrix, SANITY_CHECK_PRECISION)); + } +} + +TEST(BasisDecomposerNumBasisTest, ForcesZeroBasisUsesForIdentityTarget) { + const auto basis = cxGate01(); + const auto decomposer = TwoQubitBasisDecomposer::create(basis, 1.0); + const Matrix4x4 target = Matrix4x4::identity(); + const auto weyl = + TwoQubitWeylDecomposition::create(target, std::optional{1.0}); + const auto decomposed = decomposer.twoQubitDecompose(weyl, std::uint8_t{0}); + ASSERT_TRUE(decomposed.has_value()); + EXPECT_EQ(decomposed->numBasisUses, 0); + const Matrix4x4 restored = BasisDecomposerTest::restore(*decomposed, basis); + EXPECT_TRUE(restored.isApprox(target)); +} + INSTANTIATE_TEST_SUITE_P( - SpecializedMatrices, WeylDecompositionTest, - ::testing::Values( - // id + controlled + general already covered by other parametrizations - // swap equiv - []() -> Matrix4x4 { return cxGate01() * cxGate10() * cxGate01(); }, - // partial swap equiv - []() -> Matrix4x4 { - return TwoQubitWeylDecomposition::getCanonicalMatrix(0.5, 0.5, 0.5); - }, - // partial swap equiv (flipped) - []() -> Matrix4x4 { - return TwoQubitWeylDecomposition::getCanonicalMatrix(0.5, 0.5, -0.5); - }, - // mirror controlled equiv - []() -> Matrix4x4 { return cxGate01() * cxGate10(); }, - // sim aab equiv - []() -> Matrix4x4 { - return TwoQubitWeylDecomposition::getCanonicalMatrix(0.5, 0.5, 0.1); - }, - // sim abb equiv - []() -> Matrix4x4 { - return TwoQubitWeylDecomposition::getCanonicalMatrix(0.5, 0.1, 0.1); - }, - // sim ab-b equiv - []() -> Matrix4x4 { - return TwoQubitWeylDecomposition::getCanonicalMatrix(0.5, 0.1, -0.1); - })); + ProductTwoQubitMatrices, BasisDecomposerTest, + testing::Combine( + // basis entanglers + testing::Values([]() -> Matrix4x4 { return cxGate01(); }, + []() -> Matrix4x4 { return cxGate10(); }), + // targets to be decomposed + testing::Values([]() -> Matrix4x4 { return Matrix4x4::identity(); }, + []() -> Matrix4x4 { + return kron(rzMatrix(1.0), ryMatrix(3.1)); + }, + []() -> Matrix4x4 { + return kron(Matrix2x2::identity(), rxMatrix(0.1)); + }))); + +INSTANTIATE_TEST_SUITE_P( + TwoQubitMatrices, BasisDecomposerTest, + testing::Combine( + // basis entanglers + testing::Values([]() -> Matrix4x4 { return cxGate01(); }, + []() -> Matrix4x4 { return cxGate10(); }), + // targets to be decomposed + ::testing::Values( + []() -> Matrix4x4 { return rzzMatrix(2.0); }, + []() -> Matrix4x4 { + return ryyMatrix(1.0) * rzzMatrix(3.0) * rxxMatrix(2.0); + }, + []() -> Matrix4x4 { + return TwoQubitWeylDecomposition::getCanonicalMatrix(1.5, -0.2, + 0.0) * + kron(rxMatrix(1.0), Matrix2x2::identity()); + }, + []() -> Matrix4x4 { + return kron(rxMatrix(1.0), ryMatrix(1.0)) * + TwoQubitWeylDecomposition::getCanonicalMatrix(1.1, 0.2, + 3.0) * + kron(rxMatrix(1.0), Matrix2x2::identity()); + }, + []() -> Matrix4x4 { + return kron(hGate(), ipz()) * cxGate01() * kron(ipx(), ipy()); + }))); + +namespace { + +[[nodiscard]] static std::optional +getUnitaryQubitOperand(qco::UnitaryOpInterface op, std::size_t index) { + if (index >= op.getNumQubits()) { + return std::nullopt; + } + Value v = op->getOperand(index); + if (!llvm::isa(v.getType())) { + return std::nullopt; + } + return v; +} + +[[nodiscard]] static std::optional +getUnitaryQubitResult(qco::UnitaryOpInterface op, std::size_t index) { + if (index >= op.getNumQubits()) { + return std::nullopt; + } + Value v = op->getResult(index); + if (!llvm::isa(v.getType())) { + return std::nullopt; + } + return v; +} + +static bool extractSingleQubitMatrix(qco::UnitaryOpInterface op, + Matrix2x2& out) { + if (op.getUnitaryMatrix2x2(out)) { + return true; + } + qco::DynamicMatrix dynamic; + if (!op.getUnitaryMatrixDynamic(dynamic) || dynamic.rows() != 2 || + dynamic.cols() != 2) { + return false; + } + out = Matrix2x2::fromElements(dynamic(0, 0), dynamic(0, 1), dynamic(1, 0), + dynamic(1, 1)); + return true; +} + +static bool extractTwoQubitMatrix(qco::UnitaryOpInterface op, Matrix4x4& out) { + if (getBlockTwoQubitMatrix(op.getOperation(), out)) { + return true; + } + return op.getUnitaryMatrix4x4(out); +} + +static std::optional +computeTwoQubitUnitaryFromModule(const OwningOpRef& moduleOp) { + ModuleOp module = moduleOp.get(); + if (!module) { + return std::nullopt; + } + Matrix4x4 unitary = Matrix4x4::identity(); + llvm::DenseMap qubitIds; + std::size_t nextQubitId = 0; + + for (auto func : module.getOps()) { + for (auto& block : func.getBlocks()) { + for (auto& rawOp : block.getOperations()) { + if (auto alloc = llvm::dyn_cast(&rawOp)) { + if (nextQubitId >= 2) { + return std::nullopt; + } + qubitIds.try_emplace(alloc.getResult(), nextQubitId++); + } + } + } + } + + auto getQubitId = [&](Value qubit) -> std::optional { + auto it = qubitIds.find(qubit); + if (it == qubitIds.end()) { + return std::nullopt; + } + return it->second; + }; + + for (auto func : module.getOps()) { + for (auto& block : func.getBlocks()) { + for (auto& rawOp : block.getOperations()) { + auto op = llvm::dyn_cast(&rawOp); + if (!op) { + continue; + } + if (llvm::isa(op.getOperation())) { + continue; + } + + if (op.isSingleQubit()) { + const auto qIn = getUnitaryQubitOperand(op, 0); + if (!qIn) { + return std::nullopt; + } + auto qid = getQubitId(*qIn); + if (!qid) { + return std::nullopt; + } + Matrix2x2 oneQ; + if (!extractSingleQubitMatrix(op, oneQ)) { + return std::nullopt; + } + unitary = decomposition::expandToTwoQubits( + oneQ, static_cast(*qid)) * + unitary; + const auto qOut = getUnitaryQubitResult(op, 0); + if (!qOut) { + return std::nullopt; + } + qubitIds[*qOut] = *qid; + continue; + } + + if (op.isTwoQubit()) { + const auto q0In = getUnitaryQubitOperand(op, 0); + const auto q1In = getUnitaryQubitOperand(op, 1); + if (!q0In || !q1In) { + return std::nullopt; + } + auto q0id = getQubitId(*q0In); + auto q1id = getQubitId(*q1In); + if (!q0id || !q1id) { + return std::nullopt; + } + Matrix4x4 twoQ; + if (!extractTwoQubitMatrix(op, twoQ)) { + return std::nullopt; + } + const llvm::SmallVector ids{ + static_cast(*q0id), + static_cast(*q1id)}; + unitary = + decomposition::fixTwoQubitMatrixQubitOrder(twoQ, ids) * unitary; + const auto q0Out = getUnitaryQubitResult(op, 0); + const auto q1Out = getUnitaryQubitResult(op, 1); + if (!q0Out || !q1Out) { + return std::nullopt; + } + qubitIds[*q0Out] = *q0id; + qubitIds[*q1Out] = *q1id; + continue; + } + } + } + } + + if (nextQubitId != 2) { + return std::nullopt; + } + return unitary; +} + +struct TwoQFuseFixture { + std::unique_ptr context; + + void setUp() { + DialectRegistry registry; + registry.insert(); + context = std::make_unique(); + context->appendDialectRegistry(registry); + context->loadAllAvailableDialects(); + } + + [[nodiscard]] MLIRContext* ctx() const { return context.get(); } +}; + +static std::size_t countCtrlOps(const OwningOpRef& moduleOp) { + std::size_t count = 0; + moduleOp.get()->walk([&](qco::CtrlOp) { ++count; }); + return count; +} + +static LogicalResult runQcToQco(ModuleOp moduleOp) { + PassManager pm(moduleOp.getContext()); + pm.addPass(mlir::createQCToQCO()); + return pm.run(moduleOp); +} + +static LogicalResult runTwoQFuse(ModuleOp moduleOp, StringRef nativeGates) { + PassManager pm(moduleOp.getContext()); + pm.addPass(mlir::qco::createFuseTwoQubitUnitaryRuns( + mlir::qco::FuseTwoQubitUnitaryRunsOptions{ + .nativeGates = nativeGates.str(), + })); + return pm.run(moduleOp); +} + +template +static OwningOpRef buildProgram(MLIRContext* ctx, ProgramT program) { + return mlir::qc::QCProgramBuilder::build(ctx, program); +} + +static void fusionCxCx(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.cx(q0, q1); + b.cx(q0, q1); +} + +static void fusionHCxInterleavedTCx(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.cx(q0, q1); + b.t(q1); + b.s(q0); + b.cx(q0, q1); +} + +static void fusionThreeLineCx(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + const auto q2 = b.allocQubit(); + b.cx(q0, q1); + b.cx(q1, q2); + b.cx(q0, q1); +} + +static void fusionCxBarrierCx(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.cx(q0, q1); + b.barrier({q0, q1}); + b.cx(q0, q1); +} + +static void fusionSwapCxPattern(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.cx(q0, q1); + b.cx(q1, q0); + b.cx(q0, q1); +} + +static void fusionHRzzSRzz(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.rzz(-0.29, q0, q1); + b.s(q1); + b.rzz(0.17, q0, q1); +} + +template +static void expectTwoQFusePreservesUnitary(MLIRContext* ctx, ProgramT program, + StringRef nativeGates) { + auto expected = buildProgram(ctx, program); + ASSERT_TRUE(expected); + ASSERT_TRUE(succeeded(runQcToQco(*expected))); + const auto expectedUnitary = computeTwoQubitUnitaryFromModule(expected); + ASSERT_TRUE(expectedUnitary.has_value()); + + auto fused = buildProgram(ctx, program); + ASSERT_TRUE(fused); + ASSERT_TRUE(succeeded(runQcToQco(*fused))); + ASSERT_TRUE(succeeded(runTwoQFuse(*fused, nativeGates))); + ASSERT_TRUE(succeeded(verify(*fused))); + const auto fusedUnitary = computeTwoQubitUnitaryFromModule(fused); + ASSERT_TRUE(fusedUnitary.has_value()); + EXPECT_TRUE(isEquivalentUpToGlobalPhase(*expectedUnitary, *fusedUnitary)); +} + +} // namespace + +//===----------------------------------------------------------------------===// +// FuseTwoQubitUnitaryRuns tests +//===----------------------------------------------------------------------===// + +TEST(FuseTwoQubitUnitaryRunsTest, InvalidNativeGatesFailsPass) { + TwoQFuseFixture fx; + fx.setUp(); + auto module = buildProgram(fx.ctx(), fusionCxCx); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(runQcToQco(*module))); + EXPECT_TRUE(failed(runTwoQFuse(*module, "not-a-gate"))); +} + +TEST(FuseTwoQubitUnitaryRunsTest, AdjacentCxCancel) { + TwoQFuseFixture fx; + fx.setUp(); + expectTwoQFusePreservesUnitary(fx.ctx(), fusionCxCx, "u,cx"); + + auto module = buildProgram(fx.ctx(), fusionCxCx); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(runQcToQco(*module))); + ASSERT_TRUE(succeeded(runTwoQFuse(*module, "u,cx"))); + EXPECT_EQ(countCtrlOps(module), 0U); +} + +TEST(FuseTwoQubitUnitaryRunsTest, FusesCxThroughInterleavedOneQOps) { + TwoQFuseFixture fx; + fx.setUp(); + expectTwoQFusePreservesUnitary(fx.ctx(), fusionHCxInterleavedTCx, "u,cx"); +} + +TEST(FuseTwoQubitUnitaryRunsTest, StopsAtDifferentPairBoundary) { + TwoQFuseFixture fx; + fx.setUp(); + auto module = buildProgram(fx.ctx(), fusionThreeLineCx); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(runQcToQco(*module))); + ASSERT_TRUE(succeeded(runTwoQFuse(*module, "u,cx"))); + EXPECT_GE(countCtrlOps(module), 1U); +} + +TEST(FuseTwoQubitUnitaryRunsTest, DoesNotFuseAcrossBarrier) { + TwoQFuseFixture fx; + fx.setUp(); + auto module = buildProgram(fx.ctx(), fusionCxBarrierCx); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(runQcToQco(*module))); + ASSERT_TRUE(succeeded(runTwoQFuse(*module, "u,cx"))); + EXPECT_EQ(countCtrlOps(module), 2U); +} + +TEST(FuseTwoQubitUnitaryRunsTest, HandlesSwappedWireOrder) { + TwoQFuseFixture fx; + fx.setUp(); + expectTwoQFusePreservesUnitary(fx.ctx(), fusionSwapCxPattern, "u,cx"); +} + +TEST(FuseTwoQubitUnitaryRunsTest, HandlesRzzBlock) { + TwoQFuseFixture fx; + fx.setUp(); + expectTwoQFusePreservesUnitary(fx.ctx(), fusionHRzzSRzz, "x,sx,rz,rx,rzz,cz"); +} diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt index 621ad6c994..35463ee646 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt @@ -7,16 +7,7 @@ # Licensed under the MIT License set(target_name mqt-core-mlir-unittest-native-synthesis) -add_executable( - ${target_name} - native_synthesis_test_helpers.cpp - test_fuse_two_qubit_unitary_runs.cpp - test_native_policy.cpp - test_native_spec.cpp - test_native_synthesis_pass_custom_menus.cpp - test_native_synthesis_pass_fusion.cpp - test_native_synthesis_pass_multi_qubit.cpp - test_native_synthesis_pass_profiles.cpp) +add_executable(${target_name} test_native_synthesis.cpp) target_link_libraries( ${target_name} diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_pass_test_fixture.h b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_pass_test_fixture.h deleted file mode 100644 index 8eba08db14..0000000000 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_pass_test_fixture.h +++ /dev/null @@ -1,273 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#pragma once - -#include "mlir/Conversion/QCToQCO/QCToQCO.h" -#include "mlir/Dialect/QC/Builder/QCProgramBuilder.h" -#include "mlir/Dialect/QC/IR/QCDialect.h" -#include "mlir/Dialect/QCO/IR/QCODialect.h" -#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" -#include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/Transforms/Passes.h" -#include "native_synthesis_test_helpers.h" -#include "qc_programs.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -/// One row of the standard multi-profile equivalence sweeps in tests. -struct NativeSynthesisProfileSweepCase { - const char* nativeGates; - bool (*isNative)(mlir::OwningOpRef&); -}; - -class NativeSynthesisPassTest : public testing::Test { -protected: - void SetUp() override { - mlir::DialectRegistry registry; - registry.insert(); - context = std::make_unique(); - context->appendDialectRegistry(registry); - context->loadAllAvailableDialects(); - } - - template - static bool onlyTheseOps(mlir::OwningOpRef& moduleOp, - const bool allowCx, const bool allowCz) { - bool ok = true; - std::ignore = moduleOp->walk([&](mlir::qco::UnitaryOpInterface op) { - mlir::Operation* raw = op.getOperation(); - if (llvm::isa_and_present(raw->getParentOp())) { - return mlir::WalkResult::advance(); - } - if (llvm::isa(raw)) { - return mlir::WalkResult::advance(); - } - if (auto ctrl = llvm::dyn_cast(raw)) { - if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { - ok = false; - return mlir::WalkResult::interrupt(); - } - mlir::Operation* body = ctrl.getBodyUnitary(0).getOperation(); - const bool isCx = llvm::isa(body); - const bool isCz = llvm::isa(body); - if ((isCx && allowCx) || (isCz && allowCz)) { - return mlir::WalkResult::advance(); - } - ok = false; - return mlir::WalkResult::interrupt(); - } - - if (!llvm::isa(raw)) { - ok = false; - return mlir::WalkResult::interrupt(); - } - return mlir::WalkResult::advance(); - }); - return ok; - } - - static bool onlyIbmBasicCxOps(mlir::OwningOpRef& moduleOp) { - return onlyTheseOps(moduleOp, /*allowCx=*/true, - /*allowCz=*/false); - } - - static bool onlyIbmBasicCzOps(mlir::OwningOpRef& moduleOp) { - return onlyTheseOps(moduleOp, /*allowCx=*/false, - /*allowCz=*/true); - } - - static bool onlyGenericU3CxOps(mlir::OwningOpRef& moduleOp) { - return onlyTheseOps(moduleOp, /*allowCx=*/true, - /*allowCz=*/false); - } - - static bool onlyGenericU3CzOps(mlir::OwningOpRef& moduleOp) { - return onlyTheseOps(moduleOp, /*allowCx=*/false, - /*allowCz=*/true); - } - - static bool onlyIqmDefaultOps(mlir::OwningOpRef& moduleOp) { - return onlyTheseOps(moduleOp, /*allowCx=*/false, - /*allowCz=*/true); - } - - static bool - onlyIbmFractionalOps(mlir::OwningOpRef& moduleOp) { - return onlyTheseOps( - moduleOp, /*allowCx=*/false, /*allowCz=*/true); - } - - static bool - onlyAxisPairRxRzCxOps(mlir::OwningOpRef& moduleOp) { - return onlyTheseOps( - moduleOp, /*allowCx=*/true, /*allowCz=*/false); - } - - static bool - onlyAxisPairRxRyCxOps(mlir::OwningOpRef& moduleOp) { - return onlyTheseOps( - moduleOp, /*allowCx=*/true, /*allowCz=*/false); - } - - static bool - onlyAxisPairRyRzCzOps(mlir::OwningOpRef& moduleOp) { - return onlyTheseOps( - moduleOp, /*allowCx=*/false, /*allowCz=*/true); - } - - static bool - onlyUOrAxisPairRxRzCxOps(mlir::OwningOpRef& moduleOp) { - return onlyTheseOps(moduleOp, /*allowCx=*/true, - /*allowCz=*/false); - } - - static bool - onlyGenericU3CxOrCzOps(mlir::OwningOpRef& moduleOp) { - return onlyTheseOps(moduleOp, /*allowCx=*/true, - /*allowCz=*/true); - } - - /// The nine built-in reference profiles (IBM basic, U3, fractional, IQM, - /// axis pairs including ``rx,rz,cx``). Used by 2q / multi-qubit equivalence - /// sweeps. - static std::array - allNineEquivalenceProfiles() { - return {{{.nativeGates = "x,sx,rz,cx", .isNative = &onlyIbmBasicCxOps}, - {.nativeGates = "x,sx,rz,cz", .isNative = &onlyIbmBasicCzOps}, - {.nativeGates = "u,cx", .isNative = &onlyGenericU3CxOps}, - {.nativeGates = "u,cz", .isNative = &onlyGenericU3CzOps}, - {.nativeGates = "x,sx,rz,rx,rzz,cz", - .isNative = &onlyIbmFractionalOps}, - {.nativeGates = "r,cz", .isNative = &onlyIqmDefaultOps}, - {.nativeGates = "rx,ry,cx", .isNative = &onlyAxisPairRxRyCxOps}, - {.nativeGates = "ry,rz,cz", .isNative = &onlyAxisPairRyRzCzOps}, - {.nativeGates = "rx,rz,cx", .isNative = &onlyAxisPairRxRzCxOps}}}; - } - - /// CX-friendly profiles excluding IQM-default (CZ-only entangler), for - /// circuits that use a ``cx`` two-qubit primitive in the source. - static std::array - fiveCxEntanglerEquivalenceProfiles() { - return {{{.nativeGates = "x,sx,rz,cx", .isNative = &onlyIbmBasicCxOps}, - {.nativeGates = "u,cx", .isNative = &onlyGenericU3CxOps}, - {.nativeGates = "x,sx,rz,rx,rzz,cz", - .isNative = &onlyIbmFractionalOps}, - {.nativeGates = "rx,ry,cx", .isNative = &onlyAxisPairRxRyCxOps}, - {.nativeGates = "rx,rz,cx", .isNative = &onlyAxisPairRxRzCxOps}}}; - } - - [[nodiscard]] mlir::OwningOpRef - buildBroadOneQCanonicalizationCircuit() const { - return mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::nativeSynthBroadOneQCanonicalization); - } - - [[nodiscard]] mlir::OwningOpRef - buildZeroAngleCanonicalizationCircuit() const { - return mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::nativeSynthZeroAngleCanonicalization); - } - - [[nodiscard]] mlir::OwningOpRef - buildIbmFractionalAllGateFamiliesCircuit() const { - return mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::nativeSynthIbmFractionalAllGateFamilies); - } - - static void runNativeSynthesis(mlir::OwningOpRef& moduleOp, - const std::string& nativeGates) { - mlir::PassManager pm(moduleOp->getContext()); - pm.addPass(mlir::createQCToQCO()); - pm.addPass(mlir::qco::createNativeGateSynthesisPass( - mlir::qco::NativeGateSynthesisOptions{ - .nativeGates = nativeGates, - })); - ASSERT_TRUE(mlir::succeeded(pm.run(*moduleOp))); - } - - static void runQcToQco(mlir::OwningOpRef& moduleOp) { - mlir::PassManager pm(moduleOp->getContext()); - pm.addPass(mlir::createQCToQCO()); - ASSERT_TRUE(mlir::succeeded(pm.run(*moduleOp))); - } - - static std::string - moduleToString(const mlir::OwningOpRef& moduleOp) { - std::string text; - llvm::raw_string_ostream os(text); - moduleOp.get()->print(os); - return text; - } - - template - void expectNativeAfterSynthesis(BuildFn buildFn, - const std::string& nativeGates, - PredicateFn isNative) { - auto moduleOp = buildFn(); - runNativeSynthesis(moduleOp, nativeGates); - EXPECT_TRUE(isNative(moduleOp)); - } - - template - void expectSynthesisFailure(BuildFn buildFn, const std::string& nativeGates) { - auto moduleOp = buildFn(); - mlir::PassManager pm(moduleOp->getContext()); - pm.addPass(mlir::createQCToQCO()); - pm.addPass(mlir::qco::createNativeGateSynthesisPass( - mlir::qco::NativeGateSynthesisOptions{ - .nativeGates = nativeGates, - })); - EXPECT_TRUE(mlir::failed(pm.run(*moduleOp))); - } - - template - void expectEquivalentAndNativeAfterSynthesis(BuildFn buildFn, - const std::string& nativeGates, - PredicateFn isNative, - UnitaryFn computeUnitary) { - auto expectedModule = buildFn(); - runQcToQco(expectedModule); - const auto expectedUnitary = computeUnitary(expectedModule); - ASSERT_TRUE(expectedUnitary.has_value()); - - auto synthesizedModule = buildFn(); - runNativeSynthesis(synthesizedModule, nativeGates); - EXPECT_TRUE(isNative(synthesizedModule)); - const auto synthesizedUnitary = computeUnitary(synthesizedModule); - ASSERT_TRUE(synthesizedUnitary.has_value()); - EXPECT_TRUE(mlir::qco::native_synth_test::isEquivalentUpToGlobalPhase( - *expectedUnitary, *synthesizedUnitary)); - } - - std::unique_ptr context; -}; diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp deleted file mode 100644 index 851ddffce3..0000000000 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.cpp +++ /dev/null @@ -1,609 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "native_synthesis_test_helpers.h" - -#include "mlir/Dialect/QCO/IR/QCODialect.h" -#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" -#include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" -#include "mlir/Dialect/QCO/Utils/Matrix.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -using namespace mlir; - -namespace mlir::qco::native_synth_test { - -TestMatrix TestMatrix::identity(std::size_t dim) { - TestMatrix result(dim); - for (std::size_t i = 0; i < dim; ++i) { - result(i, i) = std::complex{1.0, 0.0}; - } - return result; -} - -TestMatrix TestMatrix::fromMatrix2x2(const Matrix2x2& matrix) { - TestMatrix result(2); - for (std::size_t row = 0; row < 2; ++row) { - for (std::size_t col = 0; col < 2; ++col) { - result(row, col) = matrix(row, col); - } - } - return result; -} - -TestMatrix TestMatrix::fromMatrix4x4(const Matrix4x4& matrix) { - TestMatrix result(4); - for (std::size_t row = 0; row < 4; ++row) { - for (std::size_t col = 0; col < 4; ++col) { - result(row, col) = matrix(row, col); - } - } - return result; -} - -TestMatrix TestMatrix::operator*(const TestMatrix& rhs) const { - TestMatrix result(dim_); - for (std::size_t row = 0; row < dim_; ++row) { - for (std::size_t k = 0; k < dim_; ++k) { - const std::complex a = (*this)(row, k); - if (a == std::complex{0.0, 0.0}) { - continue; - } - for (std::size_t col = 0; col < dim_; ++col) { - result(row, col) += a * rhs(k, col); - } - } - } - return result; -} - -TestMatrix TestMatrix::operator*(std::complex scalar) const { - TestMatrix result(dim_); - for (std::size_t row = 0; row < dim_; ++row) { - for (std::size_t col = 0; col < dim_; ++col) { - result(row, col) = (*this)(row, col) * scalar; - } - } - return result; -} - -TestMatrix TestMatrix::adjoint() const { - TestMatrix result(dim_); - for (std::size_t row = 0; row < dim_; ++row) { - for (std::size_t col = 0; col < dim_; ++col) { - result(col, row) = std::conj((*this)(row, col)); - } - } - return result; -} - -std::complex TestMatrix::trace() const { - std::complex sum{0.0, 0.0}; - for (std::size_t i = 0; i < dim_; ++i) { - sum += (*this)(i, i); - } - return sum; -} - -bool TestMatrix::isApprox(const TestMatrix& other, double tol) const { - if (dim_ != other.dim_) { - return false; - } - for (std::size_t row = 0; row < dim_; ++row) { - for (std::size_t col = 0; col < dim_; ++col) { - if (std::abs((*this)(row, col) - other(row, col)) > tol) { - return false; - } - } - } - return true; -} - -[[nodiscard]] static std::optional -getUnitaryQubitOperand(qco::UnitaryOpInterface op, std::size_t index) { - if (index >= op.getNumQubits()) { - return std::nullopt; - } - Value v = op->getOperand(index); - if (!llvm::isa(v.getType())) { - return std::nullopt; - } - return v; -} - -[[nodiscard]] static std::optional -getUnitaryQubitResult(qco::UnitaryOpInterface op, std::size_t index) { - if (index >= op.getNumQubits()) { - return std::nullopt; - } - Value v = op->getResult(index); - if (!llvm::isa(v.getType())) { - return std::nullopt; - } - return v; -} - -std::complex phasedAmplitude(const double magnitude, - const double phase) { - return std::complex(magnitude, 0.0) * - std::exp(std::complex(0.0, phase)); -} - -Matrix2x2 u3Matrix(double theta, double phi, double lambda) { - return decomposition::uMatrix(theta, phi, lambda); -} - -std::optional evaluateConstF64(Value value) { - if (!value) { - return std::nullopt; - } - if (auto cst = value.getDefiningOp()) { - if (auto attr = llvm::dyn_cast(cst.getValue())) { - return attr.getValueAsDouble(); - } - return std::nullopt; - } - if (auto neg = value.getDefiningOp()) { - if (auto v = evaluateConstF64(neg.getOperand())) { - return -*v; - } - return std::nullopt; - } - if (auto add = value.getDefiningOp()) { - auto lhs = evaluateConstF64(add.getLhs()); - auto rhs = evaluateConstF64(add.getRhs()); - if (lhs && rhs) { - return *lhs + *rhs; - } - return std::nullopt; - } - if (auto sub = value.getDefiningOp()) { - auto lhs = evaluateConstF64(sub.getLhs()); - auto rhs = evaluateConstF64(sub.getRhs()); - if (lhs && rhs) { - return *lhs - *rhs; - } - return std::nullopt; - } - if (auto mul = value.getDefiningOp()) { - auto lhs = evaluateConstF64(mul.getLhs()); - auto rhs = evaluateConstF64(mul.getRhs()); - if (lhs && rhs) { - return *lhs * *rhs; - } - return std::nullopt; - } - if (auto div = value.getDefiningOp()) { - auto lhs = evaluateConstF64(div.getLhs()); - auto rhs = evaluateConstF64(div.getRhs()); - if (lhs && rhs) { - return *lhs / *rhs; - } - return std::nullopt; - } - return std::nullopt; -} - -/// Extract the 2x2 unitary matrix associated with a single-qubit op. -bool extractSingleQubitMatrix(qco::UnitaryOpInterface op, Matrix2x2& out) { - if (llvm::isa(op.getOperation())) { - auto* raw = op.getOperation(); - if (raw->getNumOperands() < 2) { - return false; - } - auto theta = evaluateConstF64(raw->getOperand(1)); - if (!theta) { - return false; - } - out = qco::decomposition::rzMatrix(*theta); - return true; - } - if (llvm::isa(op.getOperation())) { - auto* raw = op.getOperation(); - if (raw->getNumOperands() < 2) { - return false; - } - auto theta = evaluateConstF64(raw->getOperand(1)); - if (!theta) { - return false; - } - out = qco::decomposition::rxMatrix(*theta); - return true; - } - if (llvm::isa(op.getOperation())) { - auto* raw = op.getOperation(); - if (raw->getNumOperands() < 2) { - return false; - } - auto theta = evaluateConstF64(raw->getOperand(1)); - if (!theta) { - return false; - } - out = qco::decomposition::ryMatrix(*theta); - return true; - } - if (llvm::isa(op.getOperation())) { - auto* raw = op.getOperation(); - if (raw->getNumOperands() < 4) { - return false; - } - auto theta = evaluateConstF64(raw->getOperand(1)); - auto phi = evaluateConstF64(raw->getOperand(2)); - auto lambda = evaluateConstF64(raw->getOperand(3)); - if (!theta || !phi || !lambda) { - return false; - } - out = u3Matrix(*theta, *phi, *lambda); - return true; - } - if (llvm::isa(op.getOperation())) { - auto* raw = op.getOperation(); - if (raw->getNumOperands() < 2) { - return false; - } - auto lambda = evaluateConstF64(raw->getOperand(1)); - if (!lambda) { - return false; - } - out = qco::decomposition::pMatrix(*lambda); - return true; - } - if (llvm::isa(op.getOperation())) { - auto* raw = op.getOperation(); - if (raw->getNumOperands() < 3) { - return false; - } - auto theta = evaluateConstF64(raw->getOperand(1)); - auto phi = evaluateConstF64(raw->getOperand(2)); - if (!theta || !phi) { - return false; - } - const auto thetaSin = std::sin(*theta / 2.0); - const auto m01 = - phasedAmplitude(thetaSin, -*phi - (std::numbers::pi / 2.0)); - const auto m10 = phasedAmplitude(thetaSin, *phi - (std::numbers::pi / 2.0)); - const std::complex thetaCos = std::cos(*theta / 2.0); - out = Matrix2x2::fromElements(thetaCos, m01, m10, thetaCos); - return true; - } - if (Matrix2x2 raw; op.getUnitaryMatrix2x2(raw)) { - out = raw; - return true; - } - qco::DynamicMatrix dynamic; - if (!op.getUnitaryMatrixDynamic(dynamic) || dynamic.rows() != 2 || - dynamic.cols() != 2) { - return false; - } - out = Matrix2x2::fromElements(dynamic(0, 0), dynamic(0, 1), dynamic(1, 0), - dynamic(1, 1)); - return true; -} - -/// 4×4 unitary for a two-qubit op (same layout as ``getUnitaryMatrix4x4``). -bool extractTwoQubitMatrix(qco::UnitaryOpInterface op, Matrix4x4& out) { - if (auto ctrl = llvm::dyn_cast(op.getOperation())) { - if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { - return false; - } - auto* body = ctrl.getBodyUnitary(0).getOperation(); - if (llvm::isa(body)) { - out = Matrix4x4::identity(); - out(3, 3) = -1.0; - return true; - } - if (llvm::isa(body)) { - out = Matrix4x4::fromElements(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, - 0); - return true; - } - return false; - } - if (Matrix4x4 raw; op.getUnitaryMatrix4x4(raw)) { - out = raw; - return true; - } - qco::DynamicMatrix dynamic; - if (!op.getUnitaryMatrixDynamic(dynamic) || dynamic.rows() != 4 || - dynamic.cols() != 4) { - return false; - } - for (std::size_t row = 0; row < 4; ++row) { - for (std::size_t col = 0; col < 4; ++col) { - out(row, col) = dynamic(static_cast(row), - static_cast(col)); - } - } - return true; -} - -std::optional -computeTwoQubitUnitaryFromModule(const OwningOpRef& moduleOp) { - ModuleOp module = moduleOp.get(); - if (!module) { - return std::nullopt; - } - Matrix4x4 unitary = Matrix4x4::identity(); - llvm::DenseMap qubitIds; - std::size_t nextQubitId = 0; - - for (auto func : module.getOps()) { - for (auto& block : func.getBlocks()) { - for (auto& rawOp : block.getOperations()) { - if (auto alloc = llvm::dyn_cast(&rawOp)) { - if (nextQubitId >= 2) { - return std::nullopt; - } - qubitIds.try_emplace(alloc.getResult(), nextQubitId++); - } - } - } - } - - auto getQubitId = [&](Value qubit) -> std::optional { - auto it = qubitIds.find(qubit); - if (it == qubitIds.end()) { - return std::nullopt; - } - return it->second; - }; - - for (auto func : module.getOps()) { - for (auto& block : func.getBlocks()) { - for (auto& rawOp : block.getOperations()) { - auto op = llvm::dyn_cast(&rawOp); - if (!op) { - continue; - } - if (llvm::isa(op.getOperation())) { - continue; - } - - if (op.isSingleQubit()) { - const auto qIn = getUnitaryQubitOperand(op, 0); - if (!qIn) { - return std::nullopt; - } - auto qid = getQubitId(*qIn); - if (!qid) { - return std::nullopt; - } - Matrix2x2 oneQ; - if (!extractSingleQubitMatrix(op, oneQ)) { - return std::nullopt; - } - unitary = decomposition::expandToTwoQubits( - oneQ, static_cast(*qid)) * - unitary; - const auto qOut = getUnitaryQubitResult(op, 0); - if (!qOut) { - return std::nullopt; - } - qubitIds[*qOut] = *qid; - continue; - } - - if (op.isTwoQubit()) { - const auto q0In = getUnitaryQubitOperand(op, 0); - const auto q1In = getUnitaryQubitOperand(op, 1); - if (!q0In || !q1In) { - return std::nullopt; - } - auto q0id = getQubitId(*q0In); - auto q1id = getQubitId(*q1In); - if (!q0id || !q1id) { - return std::nullopt; - } - Matrix4x4 twoQ; - if (!extractTwoQubitMatrix(op, twoQ)) { - return std::nullopt; - } - // Reorder the gate's (operand0, operand1) layout into the canonical - // (qubit 0, qubit 1) order used by `unitary`. - const llvm::SmallVector ids{ - static_cast(*q0id), - static_cast(*q1id)}; - unitary = - decomposition::fixTwoQubitMatrixQubitOrder(twoQ, ids) * unitary; - const auto q0Out = getUnitaryQubitResult(op, 0); - const auto q1Out = getUnitaryQubitResult(op, 1); - if (!q0Out || !q1Out) { - return std::nullopt; - } - qubitIds[*q0Out] = *q0id; - qubitIds[*q1Out] = *q1id; - continue; - } - } - } - } - - if (nextQubitId != 2) { - return std::nullopt; - } - return unitary; -} - -/// Kronecker-embed ``matrix`` on wire ``q`` into a ``2^N``-dim unitary (same -/// index bit order as QCO 4×4 matrices: wire 0 is the high bit). -TestMatrix expandOneQToN(const Matrix2x2& matrix, std::size_t q, - std::size_t numQubits) { - const std::size_t dim = 1ULL << numQubits; - TestMatrix full(dim); - const auto bit = numQubits - 1 - q; - const std::size_t mask = 1ULL << bit; - for (std::size_t col = 0; col < dim; ++col) { - const std::size_t sIn = (col >> bit) & 1ULL; - const std::size_t rest = col & ~mask; - for (std::size_t sOut = 0; sOut < 2; ++sOut) { - const std::size_t row = rest | (sOut << bit); - full(row, col) = matrix(sOut, sIn); - } - } - return full; -} - -/// Embed ``matrix`` on wires ``q0``, ``q1`` into a ``2^N``-dim unitary. -TestMatrix expandTwoQToN(const Matrix4x4& matrix, std::size_t q0, - std::size_t q1, std::size_t numQubits) { - const std::size_t dim = 1ULL << numQubits; - TestMatrix full(dim); - const auto bit0 = numQubits - 1 - q0; - const auto bit1 = numQubits - 1 - q1; - const std::size_t mask0 = 1ULL << bit0; - const std::size_t mask1 = 1ULL << bit1; - const std::size_t maskBoth = mask0 | mask1; - for (std::size_t col = 0; col < dim; ++col) { - const std::size_t s0In = (col >> bit0) & 1ULL; - const std::size_t s1In = (col >> bit1) & 1ULL; - // 2-bit index for the pair matches QCO 4×4 row/column layout. - const std::size_t smallIn = (s0In << 1) | s1In; - const std::size_t rest = col & ~maskBoth; - for (std::size_t smallOut = 0; smallOut < 4; ++smallOut) { - const std::size_t s0Out = (smallOut >> 1) & 1ULL; - const std::size_t s1Out = smallOut & 1ULL; - const std::size_t row = rest | (s0Out << bit0) | (s1Out << bit1); - full(row, col) = matrix(smallOut, smallIn); - } - } - return full; -} - -/// Full ``2^N`` unitary from a QCO module (``alloc`` / ``static``, 1q/2q -/// unitaries, ``ctrl`` with X/Z body). ``std::nullopt`` on unsupported ops or -/// if ``N`` exceeds ``maxQubits``. -std::optional -computeNQubitUnitaryFromModule(const OwningOpRef& moduleOp, - std::size_t maxQubits) { - ModuleOp module = moduleOp.get(); - if (!module) { - return std::nullopt; - } - - llvm::DenseMap qubitIds; - std::size_t numQubits = 0; - - for (auto func : module.getOps()) { - for (auto& block : func.getBlocks()) { - for (auto& rawOp : block.getOperations()) { - if (auto alloc = llvm::dyn_cast(&rawOp)) { - if (numQubits >= maxQubits) { - return std::nullopt; - } - qubitIds.try_emplace(alloc.getResult(), numQubits++); - } else if (auto staticOp = llvm::dyn_cast(&rawOp)) { - const auto idx = static_cast(staticOp.getIndex()); - if (idx >= maxQubits) { - return std::nullopt; - } - qubitIds.try_emplace(staticOp.getResult(), idx); - numQubits = std::max(numQubits, idx + 1); - } - } - } - } - - if (numQubits == 0) { - return std::nullopt; - } - - TestMatrix unitary = TestMatrix::identity(1ULL << numQubits); - - auto getQubitId = [&](Value qubit) -> std::optional { - auto it = qubitIds.find(qubit); - if (it == qubitIds.end()) { - return std::nullopt; - } - return it->second; - }; - - for (auto func : module.getOps()) { - for (auto& block : func.getBlocks()) { - for (auto& rawOp : block.getOperations()) { - auto op = llvm::dyn_cast(&rawOp); - if (!op) { - continue; - } - if (llvm::isa(op.getOperation())) { - continue; - } - - if (op.isSingleQubit()) { - const auto qIn = getUnitaryQubitOperand(op, 0); - if (!qIn) { - return std::nullopt; - } - auto qid = getQubitId(*qIn); - if (!qid) { - return std::nullopt; - } - Matrix2x2 oneQ; - if (!extractSingleQubitMatrix(op, oneQ)) { - return std::nullopt; - } - unitary = expandOneQToN(oneQ, *qid, numQubits) * unitary; - const auto qOut = getUnitaryQubitResult(op, 0); - if (!qOut) { - return std::nullopt; - } - qubitIds[*qOut] = *qid; - continue; - } - - if (op.isTwoQubit()) { - const auto q0In = getUnitaryQubitOperand(op, 0); - const auto q1In = getUnitaryQubitOperand(op, 1); - if (!q0In || !q1In) { - return std::nullopt; - } - auto q0id = getQubitId(*q0In); - auto q1id = getQubitId(*q1In); - if (!q0id || !q1id) { - return std::nullopt; - } - Matrix4x4 twoQ; - if (!extractTwoQubitMatrix(op, twoQ)) { - return std::nullopt; - } - unitary = expandTwoQToN(twoQ, *q0id, *q1id, numQubits) * unitary; - const auto q0Out = getUnitaryQubitResult(op, 0); - const auto q1Out = getUnitaryQubitResult(op, 1); - if (!q0Out || !q1Out) { - return std::nullopt; - } - qubitIds[*q0Out] = *q0id; - qubitIds[*q1Out] = *q1id; - continue; - } - } - } - } - - return unitary; -} - -} // namespace mlir::qco::native_synth_test diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.h b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.h deleted file mode 100644 index e1b362a298..0000000000 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/native_synthesis_test_helpers.h +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#pragma once - -#include "TestCaseUtils.h" -#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" -#include "mlir/Dialect/QCO/Utils/Matrix.h" - -#include -#include -#include - -#include -#include -#include -#include - -namespace mlir::qco::native_synth_test { - -using mqt::test::isEquivalentUpToGlobalPhase; - -/// Minimal dense, row-major, square complex matrix with runtime dimension. -/// -/// Used by the multi-qubit equivalence checks (the synthesized circuits may -/// span more than two wires, so the fixed-size `Matrix2x2`/`Matrix4x4` are not -/// enough). Provides exactly the surface -/// `mqt::test::isEquivalentUpToGlobalPhase` needs: `adjoint()`, `operator*`, -/// scalar multiply, `trace()`, and `isApprox()`. -class TestMatrix { -public: - TestMatrix() = default; - explicit TestMatrix(std::size_t dim) - : dim_(dim), data_(dim * dim, std::complex{0.0, 0.0}) {} - - /// Identity matrix of dimension @p dim. - [[nodiscard]] static TestMatrix identity(std::size_t dim); - /// Promote a fixed `2×2` matrix to a `TestMatrix`. - [[nodiscard]] static TestMatrix fromMatrix2x2(const Matrix2x2& matrix); - /// Promote a fixed `4×4` matrix to a `TestMatrix`. - [[nodiscard]] static TestMatrix fromMatrix4x4(const Matrix4x4& matrix); - - [[nodiscard]] std::size_t dim() const { return dim_; } - - [[nodiscard]] std::complex& operator()(std::size_t row, - std::size_t col) { - return data_[(row * dim_) + col]; - } - [[nodiscard]] std::complex operator()(std::size_t row, - std::size_t col) const { - return data_[(row * dim_) + col]; - } - - /// Matrix product (dimensions must match). - [[nodiscard]] TestMatrix operator*(const TestMatrix& rhs) const; - /// Element-wise scaling by a complex scalar. - [[nodiscard]] TestMatrix operator*(std::complex scalar) const; - /// Conjugate transpose. - [[nodiscard]] TestMatrix adjoint() const; - /// Sum of diagonal entries. - [[nodiscard]] std::complex trace() const; - /// Entry-wise approximate equality (false on dimension mismatch). - [[nodiscard]] bool isApprox(const TestMatrix& other, - double tol = 1e-10) const; - -private: - std::size_t dim_ = 0; - std::vector> data_; -}; - -/// Left scalar multiply, mirroring the right multiply above. -[[nodiscard]] inline TestMatrix operator*(std::complex scalar, - const TestMatrix& matrix) { - return matrix * scalar; -} - -[[nodiscard]] std::complex phasedAmplitude(double magnitude, - double phase); -[[nodiscard]] Matrix2x2 u3Matrix(double theta, double phi, double lambda); -[[nodiscard]] std::optional evaluateConstF64(Value value); -bool extractSingleQubitMatrix(qco::UnitaryOpInterface op, Matrix2x2& out); -bool extractTwoQubitMatrix(qco::UnitaryOpInterface op, Matrix4x4& out); -[[nodiscard]] std::optional -computeTwoQubitUnitaryFromModule(const OwningOpRef& moduleOp); -[[nodiscard]] TestMatrix expandOneQToN(const Matrix2x2& matrix, std::size_t q, - std::size_t numQubits); -[[nodiscard]] TestMatrix expandTwoQToN(const Matrix4x4& matrix, std::size_t q0, - std::size_t q1, std::size_t numQubits); -[[nodiscard]] std::optional -computeNQubitUnitaryFromModule(const OwningOpRef& moduleOp, - std::size_t maxQubits = 6); - -} // namespace mlir::qco::native_synth_test diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp deleted file mode 100644 index 7dfe23302b..0000000000 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -// Standalone ``fuse-two-qubit-unitary-runs`` pass tests. - -#include "native_synthesis_pass_test_fixture.h" -#include "native_synthesis_test_helpers.h" -#include "qc_programs.h" - -#include -#include -#include -#include - -using namespace mlir; -using namespace mlir::qco; -using namespace mlir::qco::native_synth_test; - -namespace { - -static void runFuseTwoQubitUnitaryRuns(OwningOpRef& moduleOp, - const std::string& nativeGates) { - PassManager pm(moduleOp->getContext()); - FuseTwoQubitUnitaryRunsOptions opts; - opts.nativeGates = nativeGates; - pm.addPass(createFuseTwoQubitUnitaryRuns(opts)); - ASSERT_TRUE(succeeded(pm.run(*moduleOp))); -} - -template -static std::size_t -countOpsOfTypeInModule(const OwningOpRef& moduleOp) { - std::size_t count = 0; - moduleOp.get()->walk([&](Operation* op) { - if (llvm::isa(op)) { - ++count; - } - }); - return count; -} - -} // namespace - -class FuseTwoQubitUnitaryRunsTest : public NativeSynthesisPassTest {}; - -TEST_F(FuseTwoQubitUnitaryRunsTest, InvalidMenuFailsPass) { - auto moduleOp = mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::nativeSynthFusionCxCx); - runQcToQco(moduleOp); - PassManager pm(moduleOp->getContext()); - FuseTwoQubitUnitaryRunsOptions opts; - opts.nativeGates = "not-a-real-menu"; - pm.addPass(createFuseTwoQubitUnitaryRuns(opts)); - EXPECT_TRUE(failed(pm.run(*moduleOp))); -} - -TEST_F(FuseTwoQubitUnitaryRunsTest, EmptyMenuIsNoOp) { - auto moduleOp = mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::nativeSynthFusionCxCx); - runQcToQco(moduleOp); - const auto before = countOpsOfTypeInModule(moduleOp); - runFuseTwoQubitUnitaryRuns(moduleOp, ""); - EXPECT_EQ(countOpsOfTypeInModule(moduleOp), before); -} - -TEST_F(FuseTwoQubitUnitaryRunsTest, CancelsAdjacentCxPair) { - auto moduleOp = mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::nativeSynthFusionCxCx); - runQcToQco(moduleOp); - runFuseTwoQubitUnitaryRuns(moduleOp, "u,cx"); - EXPECT_EQ(countOpsOfTypeInModule(moduleOp), 0U); -} - -TEST_F(FuseTwoQubitUnitaryRunsTest, PreservesSingleCx) { - auto moduleOp = mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::nativeSynthFusionHadamardCxHadamard); - runQcToQco(moduleOp); - runFuseTwoQubitUnitaryRuns(moduleOp, "u,cx"); - EXPECT_EQ(countOpsOfTypeInModule(moduleOp), 1U); -} - -TEST_F(FuseTwoQubitUnitaryRunsTest, FusesCxThroughInterleavedOneQOps) { - auto buildFn = [&] { - return mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::nativeSynthFusionHCxInterleavedTCx); - }; - auto expected = buildFn(); - runQcToQco(expected); - const auto expectedUnitary = computeTwoQubitUnitaryFromModule(expected); - ASSERT_TRUE(expectedUnitary.has_value()); - - auto moduleOp = buildFn(); - runQcToQco(moduleOp); - runFuseTwoQubitUnitaryRuns(moduleOp, "u,cx"); - const auto fusedUnitary = computeTwoQubitUnitaryFromModule(moduleOp); - ASSERT_TRUE(fusedUnitary.has_value()); - EXPECT_TRUE(isEquivalentUpToGlobalPhase(*expectedUnitary, *fusedUnitary)); -} diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_policy.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_policy.cpp deleted file mode 100644 index 29c195fa17..0000000000 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_policy.cpp +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h" -#include "mlir/Dialect/QCO/IR/QCODialect.h" -#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" -#include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace mlir; -using namespace mlir::qco; -using namespace mlir::qco::decomposition; -using namespace mlir::qco::native_synth; - -TEST(NativePolicyTest, UsesCxAndCzFromResolvedSpec) { - const auto cxOnly = resolveNativeGatesSpec("u,cx"); - ASSERT_TRUE(cxOnly); - EXPECT_TRUE(usesCxEntangler(*cxOnly)); - EXPECT_FALSE(usesCzEntangler(*cxOnly)); - - const auto both = resolveNativeGatesSpec("u,cx,cz"); - ASSERT_TRUE(both); - EXPECT_TRUE(usesCxEntangler(*both)); - EXPECT_TRUE(usesCzEntangler(*both)); -} - -// NOLINTNEXTLINE(misc-use-internal-linkage) -class NativePolicyAllowsOpTest : public ::testing::Test { -protected: - MLIRContext context; - QCOProgramBuilder builder{&context}; - - void SetUp() override { - context.loadDialect(); - context.loadDialect(); - context.loadDialect(); - context.loadDialect(); - builder.initialize(); - } -}; - -TEST_F(NativePolicyAllowsOpTest, AllowsSingleQubitOpRespectsMenu) { - const auto spec = resolveNativeGatesSpec("x,sx,rz,cx"); - ASSERT_TRUE(spec); - Value q = builder.staticQubit(0); - q = builder.x(q); - auto mod = builder.finalize(); - ASSERT_TRUE(mod); - XOp xop; - mod->walk([&](XOp op) { - xop = op; - return WalkResult::interrupt(); - }); - ASSERT_TRUE(xop); - EXPECT_TRUE(allowsSingleQubitOp( - llvm::cast(xop.getOperation()), *spec)); -} - -TEST_F(NativePolicyAllowsOpTest, RejectsSingleQubitOpNotInMenu) { - const auto spec = resolveNativeGatesSpec("u,cx"); - ASSERT_TRUE(spec); - Value q = builder.staticQubit(0); - q = builder.x(q); - auto mod = builder.finalize(); - ASSERT_TRUE(mod); - XOp xop; - mod->walk([&](XOp op) { - xop = op; - return WalkResult::interrupt(); - }); - ASSERT_TRUE(xop); - EXPECT_FALSE(allowsSingleQubitOp( - llvm::cast(xop.getOperation()), *spec)); -} - -TEST_F(NativePolicyAllowsOpTest, CanDirectlyDecomposeToU3OnRxInCircuit) { - Value q = builder.staticQubit(0); - q = builder.rx(0.1, q); - auto mod = builder.finalize(); - ASSERT_TRUE(mod); - RXOp rx; - mod->walk([&](RXOp op) { - rx = op; - return WalkResult::interrupt(); - }); - ASSERT_TRUE(rx); - EXPECT_TRUE(canDirectlyDecomposeToU3(rx.getOperation())); -} - -TEST_F(NativePolicyAllowsOpTest, CannotDirectlyDecomposeHToU3) { - Value q = builder.staticQubit(0); - q = builder.h(q); - auto mod = builder.finalize(); - ASSERT_TRUE(mod); - HOp hop; - mod->walk([&](HOp op) { - hop = op; - return WalkResult::interrupt(); - }); - ASSERT_TRUE(hop); - EXPECT_FALSE(canDirectlyDecomposeToU3(hop.getOperation())); -} diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_spec.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_spec.cpp deleted file mode 100644 index 1ccaf6c29a..0000000000 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_spec.cpp +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" - -#include -#include - -using namespace mlir::qco::decomposition; -using namespace mlir::qco::native_synth; - -TEST(NativeSpecTest, ResolveIbmBasicCx) { - const auto spec = resolveNativeGatesSpec("x,sx,rz,cx"); - ASSERT_TRUE(spec); - EXPECT_TRUE(spec->allowedGates.contains(NativeGateKind::Cx)); - EXPECT_TRUE(spec->allowedGates.contains(NativeGateKind::X)); - EXPECT_FALSE(spec->allowRzz); -} - -TEST(NativeSpecTest, ResolveRejectsUnknownToken) { - EXPECT_FALSE(resolveNativeGatesSpec("x,sx,rz,not-a-gate").has_value()); -} - -TEST(NativeSpecTest, ResolveEmptyOrWhitespaceOnlyReturnsNullopt) { - EXPECT_FALSE(resolveNativeGatesSpec("").has_value()); - EXPECT_FALSE(resolveNativeGatesSpec(" \t ").has_value()); - EXPECT_FALSE(resolveNativeGatesSpec(",,,").has_value()); -} - -TEST(NativeSpecTest, PhaseAliasPMatchesRzInIbmStyleMenu) { - const auto pMenu = resolveNativeGatesSpec("x,sx,p,cx"); - const auto rzMenu = resolveNativeGatesSpec("x,sx,rz,cx"); - ASSERT_TRUE(pMenu); - ASSERT_TRUE(rzMenu); - EXPECT_EQ(pMenu->allowedGates, rzMenu->allowedGates); -} - -TEST(NativeSpecTest, EmitterEulerBasisForAxisPair) { - EXPECT_EQ(emitterEulerBasis(SingleQubitEmitterSpec{ - .mode = SingleQubitMode::AxisPair, .axisPair = AxisPair::RxRz}), - EulerBasis::XZX); - EXPECT_EQ(emitterEulerBasis(SingleQubitEmitterSpec{ - .mode = SingleQubitMode::AxisPair, .axisPair = AxisPair::RxRy}), - EulerBasis::XYX); - EXPECT_EQ(emitterEulerBasis(SingleQubitEmitterSpec{ - .mode = SingleQubitMode::AxisPair, .axisPair = AxisPair::RyRz}), - EulerBasis::ZYZ); -} - -TEST(NativeSpecTest, EmitterEulerBasisForPrimaryModes) { - EXPECT_EQ( - emitterEulerBasis(SingleQubitEmitterSpec{.mode = SingleQubitMode::U3}), - EulerBasis::U); - EXPECT_EQ( - emitterEulerBasis(SingleQubitEmitterSpec{.mode = SingleQubitMode::ZSXX}), - EulerBasis::ZSXX); - EXPECT_EQ( - emitterEulerBasis(SingleQubitEmitterSpec{.mode = SingleQubitMode::R}), - EulerBasis::R); -} - -TEST(NativeSpecTest, RzzSetsAllowRzzFlag) { - const auto spec = resolveNativeGatesSpec("u,cx,rzz"); - ASSERT_TRUE(spec); - EXPECT_TRUE(spec->allowRzz); - EXPECT_TRUE(spec->allowedGates.contains(NativeGateKind::Rzz)); -} diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis.cpp new file mode 100644 index 0000000000..63ff826c61 --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis.cpp @@ -0,0 +1,1148 @@ +/* + * 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 "TestCaseUtils.h" +#include "mlir/Conversion/QCToQCO/QCToQCO.h" +#include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h" +#include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" +#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h" +#include "mlir/Dialect/QCO/Transforms/Passes.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" +#include "qc_programs.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace mlir; +using namespace mlir::qco; +using namespace mlir::qco::decomposition; +using namespace mlir::qco::native_synth; + +namespace mlir::qco::native_synth_test { + +using mqt::test::isEquivalentUpToGlobalPhase; + +/// Minimal dense, row-major, square complex matrix with runtime dimension. +/// +/// Used by the multi-qubit equivalence checks (the synthesized circuits may +/// span more than two wires, so the fixed-size `Matrix2x2`/`Matrix4x4` are not +/// enough). Provides exactly the surface +/// `mqt::test::isEquivalentUpToGlobalPhase` needs: `adjoint()`, `operator*`, +/// scalar multiply, `trace()`, and `isApprox()`. +class TestMatrix { +public: + TestMatrix() = default; + explicit TestMatrix(std::size_t dim) + : dim_(dim), data_(dim * dim, std::complex{0.0, 0.0}) {} + + /// Identity matrix of dimension @p dim. + [[nodiscard]] static TestMatrix identity(std::size_t dim); + /// Promote a fixed `2×2` matrix to a `TestMatrix`. + [[nodiscard]] static TestMatrix fromMatrix2x2(const Matrix2x2& matrix); + /// Promote a fixed `4×4` matrix to a `TestMatrix`. + [[nodiscard]] static TestMatrix fromMatrix4x4(const Matrix4x4& matrix); + + [[nodiscard]] std::size_t dim() const { return dim_; } + + [[nodiscard]] std::complex& operator()(std::size_t row, + std::size_t col) { + return data_[(row * dim_) + col]; + } + [[nodiscard]] std::complex operator()(std::size_t row, + std::size_t col) const { + return data_[(row * dim_) + col]; + } + + /// Matrix product (dimensions must match). + [[nodiscard]] TestMatrix operator*(const TestMatrix& rhs) const; + /// Element-wise scaling by a complex scalar. + [[nodiscard]] TestMatrix operator*(std::complex scalar) const; + /// Conjugate transpose. + [[nodiscard]] TestMatrix adjoint() const; + /// Sum of diagonal entries. + [[nodiscard]] std::complex trace() const; + /// Entry-wise approximate equality (false on dimension mismatch). + [[nodiscard]] bool isApprox(const TestMatrix& other, + double tol = 1e-10) const; + +private: + std::size_t dim_ = 0; + std::vector> data_; +}; + +/// Left scalar multiply, mirroring the right multiply above. +[[nodiscard]] inline TestMatrix operator*(std::complex scalar, + const TestMatrix& matrix) { + return matrix * scalar; +} + +bool extractSingleQubitMatrix(qco::UnitaryOpInterface op, Matrix2x2& out); +bool extractTwoQubitMatrix(qco::UnitaryOpInterface op, Matrix4x4& out); +[[nodiscard]] std::optional +computeTwoQubitUnitaryFromModule(const OwningOpRef& moduleOp); +[[nodiscard]] TestMatrix expandOneQToN(const Matrix2x2& matrix, std::size_t q, + std::size_t numQubits); +[[nodiscard]] TestMatrix expandTwoQToN(const Matrix4x4& matrix, std::size_t q0, + std::size_t q1, std::size_t numQubits); +[[nodiscard]] std::optional +computeNQubitUnitaryFromModule(const OwningOpRef& moduleOp, + std::size_t maxQubits = 6); + +/// One row of the standard multi-profile equivalence sweeps in tests. +struct NativeSynthesisProfileSweepCase { + const char* nativeGates; + bool (*isNative)(mlir::OwningOpRef&); +}; + +/// Shared gtest fixture for native-gate synthesis pass tests. +class NativeSynthesisPassTest : public testing::Test { +protected: + void SetUp() override { + mlir::DialectRegistry registry; + registry.insert(); + context = std::make_unique(); + context->appendDialectRegistry(registry); + context->loadAllAvailableDialects(); + } + + template + static bool onlyTheseOps(mlir::OwningOpRef& moduleOp, + const bool allowCx, const bool allowCz) { + bool ok = true; + std::ignore = moduleOp->walk([&](mlir::qco::UnitaryOpInterface op) { + mlir::Operation* raw = op.getOperation(); + if (llvm::isa_and_present(raw->getParentOp())) { + return mlir::WalkResult::advance(); + } + if (llvm::isa(raw)) { + return mlir::WalkResult::advance(); + } + if (auto ctrl = llvm::dyn_cast(raw)) { + if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { + ok = false; + return mlir::WalkResult::interrupt(); + } + mlir::Operation* body = ctrl.getBodyUnitary(0).getOperation(); + const bool isCx = llvm::isa(body); + const bool isCz = llvm::isa(body); + if ((isCx && allowCx) || (isCz && allowCz)) { + return mlir::WalkResult::advance(); + } + ok = false; + return mlir::WalkResult::interrupt(); + } + + if (!llvm::isa(raw)) { + ok = false; + return mlir::WalkResult::interrupt(); + } + return mlir::WalkResult::advance(); + }); + return ok; + } + + static bool onlyIbmBasicCxOps(mlir::OwningOpRef& moduleOp) { + return onlyTheseOps(moduleOp, /*allowCx=*/true, + /*allowCz=*/false); + } + + static bool onlyIbmBasicCzOps(mlir::OwningOpRef& moduleOp) { + return onlyTheseOps(moduleOp, /*allowCx=*/false, + /*allowCz=*/true); + } + + static bool onlyGenericU3CxOps(mlir::OwningOpRef& moduleOp) { + return onlyTheseOps(moduleOp, /*allowCx=*/true, + /*allowCz=*/false); + } + + static bool onlyGenericU3CzOps(mlir::OwningOpRef& moduleOp) { + return onlyTheseOps(moduleOp, /*allowCx=*/false, + /*allowCz=*/true); + } + + static bool onlyIqmDefaultOps(mlir::OwningOpRef& moduleOp) { + return onlyTheseOps(moduleOp, /*allowCx=*/false, + /*allowCz=*/true); + } + + static bool + onlyIbmFractionalOps(mlir::OwningOpRef& moduleOp) { + return onlyTheseOps( + moduleOp, /*allowCx=*/false, /*allowCz=*/true); + } + + static bool + onlyAxisPairRxRzCxOps(mlir::OwningOpRef& moduleOp) { + return onlyTheseOps( + moduleOp, /*allowCx=*/true, /*allowCz=*/false); + } + + static bool + onlyAxisPairRxRyCxOps(mlir::OwningOpRef& moduleOp) { + return onlyTheseOps( + moduleOp, /*allowCx=*/true, /*allowCz=*/false); + } + + static bool + onlyAxisPairRyRzCzOps(mlir::OwningOpRef& moduleOp) { + return onlyTheseOps( + moduleOp, /*allowCx=*/false, /*allowCz=*/true); + } + + static bool + onlyUOrAxisPairRxRzCxOps(mlir::OwningOpRef& moduleOp) { + return onlyTheseOps(moduleOp, /*allowCx=*/true, + /*allowCz=*/false); + } + + static bool + onlyGenericU3CxOrCzOps(mlir::OwningOpRef& moduleOp) { + return onlyTheseOps(moduleOp, /*allowCx=*/true, + /*allowCz=*/true); + } + + static std::array + coreEquivalenceProfiles() { + return {{{.nativeGates = "x,sx,rz,cx", .isNative = &onlyIbmBasicCxOps}, + {.nativeGates = "u,cx", .isNative = &onlyGenericU3CxOps}, + {.nativeGates = "r,cz", .isNative = &onlyIqmDefaultOps}}}; + } + + static void runNativeSynthesis(mlir::OwningOpRef& moduleOp, + const std::string& nativeGates) { + mlir::PassManager pm(moduleOp->getContext()); + pm.addPass(mlir::createQCToQCO()); + pm.addPass(mlir::qco::createNativeGateSynthesisPass( + mlir::qco::NativeGateSynthesisOptions{ + .nativeGates = nativeGates, + })); + ASSERT_TRUE(mlir::succeeded(pm.run(*moduleOp))); + } + + static void runQcToQco(mlir::OwningOpRef& moduleOp) { + mlir::PassManager pm(moduleOp->getContext()); + pm.addPass(mlir::createQCToQCO()); + ASSERT_TRUE(mlir::succeeded(pm.run(*moduleOp))); + } + + static std::string + moduleToString(const mlir::OwningOpRef& moduleOp) { + std::string text; + llvm::raw_string_ostream os(text); + moduleOp.get()->print(os); + return text; + } + + template + void expectNativeAfterSynthesis(BuildFn buildFn, + const std::string& nativeGates, + PredicateFn isNative) { + auto moduleOp = buildFn(); + runNativeSynthesis(moduleOp, nativeGates); + EXPECT_TRUE(isNative(moduleOp)); + } + + template + void expectSynthesisFailure(BuildFn buildFn, const std::string& nativeGates) { + auto moduleOp = buildFn(); + mlir::PassManager pm(moduleOp->getContext()); + pm.addPass(mlir::createQCToQCO()); + pm.addPass(mlir::qco::createNativeGateSynthesisPass( + mlir::qco::NativeGateSynthesisOptions{ + .nativeGates = nativeGates, + })); + EXPECT_TRUE(mlir::failed(pm.run(*moduleOp))); + } + + template + void expectEquivalentAndNativeAfterSynthesis(BuildFn buildFn, + const std::string& nativeGates, + PredicateFn isNative, + UnitaryFn computeUnitary) { + auto expectedModule = buildFn(); + runQcToQco(expectedModule); + const auto expectedUnitary = computeUnitary(expectedModule); + ASSERT_TRUE(expectedUnitary.has_value()); + + auto synthesizedModule = buildFn(); + runNativeSynthesis(synthesizedModule, nativeGates); + EXPECT_TRUE(isNative(synthesizedModule)); + const auto synthesizedUnitary = computeUnitary(synthesizedModule); + ASSERT_TRUE(synthesizedUnitary.has_value()); + EXPECT_TRUE( + isEquivalentUpToGlobalPhase(*expectedUnitary, *synthesizedUnitary)); + } + + std::unique_ptr context; +}; + +TestMatrix TestMatrix::identity(std::size_t dim) { + TestMatrix result(dim); + for (std::size_t i = 0; i < dim; ++i) { + result(i, i) = std::complex{1.0, 0.0}; + } + return result; +} + +TestMatrix TestMatrix::fromMatrix2x2(const Matrix2x2& matrix) { + TestMatrix result(2); + for (std::size_t row = 0; row < 2; ++row) { + for (std::size_t col = 0; col < 2; ++col) { + result(row, col) = matrix(row, col); + } + } + return result; +} + +TestMatrix TestMatrix::fromMatrix4x4(const Matrix4x4& matrix) { + TestMatrix result(4); + for (std::size_t row = 0; row < 4; ++row) { + for (std::size_t col = 0; col < 4; ++col) { + result(row, col) = matrix(row, col); + } + } + return result; +} + +TestMatrix TestMatrix::operator*(const TestMatrix& rhs) const { + TestMatrix result(dim_); + for (std::size_t row = 0; row < dim_; ++row) { + for (std::size_t k = 0; k < dim_; ++k) { + const std::complex a = (*this)(row, k); + if (a == std::complex{0.0, 0.0}) { + continue; + } + for (std::size_t col = 0; col < dim_; ++col) { + result(row, col) += a * rhs(k, col); + } + } + } + return result; +} + +TestMatrix TestMatrix::operator*(std::complex scalar) const { + TestMatrix result(dim_); + for (std::size_t row = 0; row < dim_; ++row) { + for (std::size_t col = 0; col < dim_; ++col) { + result(row, col) = (*this)(row, col) * scalar; + } + } + return result; +} + +TestMatrix TestMatrix::adjoint() const { + TestMatrix result(dim_); + for (std::size_t row = 0; row < dim_; ++row) { + for (std::size_t col = 0; col < dim_; ++col) { + result(col, row) = std::conj((*this)(row, col)); + } + } + return result; +} + +std::complex TestMatrix::trace() const { + std::complex sum{0.0, 0.0}; + for (std::size_t i = 0; i < dim_; ++i) { + sum += (*this)(i, i); + } + return sum; +} + +bool TestMatrix::isApprox(const TestMatrix& other, double tol) const { + if (dim_ != other.dim_) { + return false; + } + for (std::size_t row = 0; row < dim_; ++row) { + for (std::size_t col = 0; col < dim_; ++col) { + if (std::abs((*this)(row, col) - other(row, col)) > tol) { + return false; + } + } + } + return true; +} + +[[nodiscard]] static std::optional +getUnitaryQubitOperand(qco::UnitaryOpInterface op, std::size_t index) { + if (index >= op.getNumQubits()) { + return std::nullopt; + } + Value v = op->getOperand(index); + if (!llvm::isa(v.getType())) { + return std::nullopt; + } + return v; +} + +[[nodiscard]] static std::optional +getUnitaryQubitResult(qco::UnitaryOpInterface op, std::size_t index) { + if (index >= op.getNumQubits()) { + return std::nullopt; + } + Value v = op->getResult(index); + if (!llvm::isa(v.getType())) { + return std::nullopt; + } + return v; +} + +/// Extract the 2x2 unitary matrix associated with a single-qubit op. +bool extractSingleQubitMatrix(qco::UnitaryOpInterface op, Matrix2x2& out) { + if (op.getUnitaryMatrix2x2(out)) { + return true; + } + qco::DynamicMatrix dynamic; + if (!op.getUnitaryMatrixDynamic(dynamic) || dynamic.rows() != 2 || + dynamic.cols() != 2) { + return false; + } + out = Matrix2x2::fromElements(dynamic(0, 0), dynamic(0, 1), dynamic(1, 0), + dynamic(1, 1)); + return true; +} + +/// 4×4 unitary for a two-qubit op (same layout as ``getUnitaryMatrix4x4``). +bool extractTwoQubitMatrix(qco::UnitaryOpInterface op, Matrix4x4& out) { + if (native_synth::getBlockTwoQubitMatrix(op.getOperation(), out)) { + return true; + } + return op.getUnitaryMatrix4x4(out); +} + +std::optional +computeTwoQubitUnitaryFromModule(const OwningOpRef& moduleOp) { + ModuleOp module = moduleOp.get(); + if (!module) { + return std::nullopt; + } + Matrix4x4 unitary = Matrix4x4::identity(); + llvm::DenseMap qubitIds; + std::size_t nextQubitId = 0; + + for (auto func : module.getOps()) { + for (auto& block : func.getBlocks()) { + for (auto& rawOp : block.getOperations()) { + if (auto alloc = llvm::dyn_cast(&rawOp)) { + if (nextQubitId >= 2) { + return std::nullopt; + } + qubitIds.try_emplace(alloc.getResult(), nextQubitId++); + } + } + } + } + + auto getQubitId = [&](Value qubit) -> std::optional { + auto it = qubitIds.find(qubit); + if (it == qubitIds.end()) { + return std::nullopt; + } + return it->second; + }; + + for (auto func : module.getOps()) { + for (auto& block : func.getBlocks()) { + for (auto& rawOp : block.getOperations()) { + auto op = llvm::dyn_cast(&rawOp); + if (!op) { + continue; + } + if (llvm::isa(op.getOperation())) { + continue; + } + + if (op.isSingleQubit()) { + const auto qIn = getUnitaryQubitOperand(op, 0); + if (!qIn) { + return std::nullopt; + } + auto qid = getQubitId(*qIn); + if (!qid) { + return std::nullopt; + } + Matrix2x2 oneQ; + if (!extractSingleQubitMatrix(op, oneQ)) { + return std::nullopt; + } + unitary = decomposition::expandToTwoQubits( + oneQ, static_cast(*qid)) * + unitary; + const auto qOut = getUnitaryQubitResult(op, 0); + if (!qOut) { + return std::nullopt; + } + qubitIds[*qOut] = *qid; + continue; + } + + if (op.isTwoQubit()) { + const auto q0In = getUnitaryQubitOperand(op, 0); + const auto q1In = getUnitaryQubitOperand(op, 1); + if (!q0In || !q1In) { + return std::nullopt; + } + auto q0id = getQubitId(*q0In); + auto q1id = getQubitId(*q1In); + if (!q0id || !q1id) { + return std::nullopt; + } + Matrix4x4 twoQ; + if (!extractTwoQubitMatrix(op, twoQ)) { + return std::nullopt; + } + // Reorder the gate's (operand0, operand1) layout into the canonical + // (qubit 0, qubit 1) order used by `unitary`. + const llvm::SmallVector ids{ + static_cast(*q0id), + static_cast(*q1id)}; + unitary = + decomposition::fixTwoQubitMatrixQubitOrder(twoQ, ids) * unitary; + const auto q0Out = getUnitaryQubitResult(op, 0); + const auto q1Out = getUnitaryQubitResult(op, 1); + if (!q0Out || !q1Out) { + return std::nullopt; + } + qubitIds[*q0Out] = *q0id; + qubitIds[*q1Out] = *q1id; + continue; + } + } + } + } + + if (nextQubitId != 2) { + return std::nullopt; + } + return unitary; +} + +/// Kronecker-embed ``matrix`` on wire ``q`` into a ``2^N``-dim unitary (same +/// index bit order as QCO 4×4 matrices: wire 0 is the high bit). +TestMatrix expandOneQToN(const Matrix2x2& matrix, std::size_t q, + std::size_t numQubits) { + const std::size_t dim = 1ULL << numQubits; + TestMatrix full(dim); + const auto bit = numQubits - 1 - q; + const std::size_t mask = 1ULL << bit; + for (std::size_t col = 0; col < dim; ++col) { + const std::size_t sIn = (col >> bit) & 1ULL; + const std::size_t rest = col & ~mask; + for (std::size_t sOut = 0; sOut < 2; ++sOut) { + const std::size_t row = rest | (sOut << bit); + full(row, col) = matrix(sOut, sIn); + } + } + return full; +} + +/// Embed ``matrix`` on wires ``q0``, ``q1`` into a ``2^N``-dim unitary. +TestMatrix expandTwoQToN(const Matrix4x4& matrix, std::size_t q0, + std::size_t q1, std::size_t numQubits) { + const std::size_t dim = 1ULL << numQubits; + TestMatrix full(dim); + const auto bit0 = numQubits - 1 - q0; + const auto bit1 = numQubits - 1 - q1; + const std::size_t mask0 = 1ULL << bit0; + const std::size_t mask1 = 1ULL << bit1; + const std::size_t maskBoth = mask0 | mask1; + for (std::size_t col = 0; col < dim; ++col) { + const std::size_t s0In = (col >> bit0) & 1ULL; + const std::size_t s1In = (col >> bit1) & 1ULL; + // 2-bit index for the pair matches QCO 4×4 row/column layout. + const std::size_t smallIn = (s0In << 1) | s1In; + const std::size_t rest = col & ~maskBoth; + for (std::size_t smallOut = 0; smallOut < 4; ++smallOut) { + const std::size_t s0Out = (smallOut >> 1) & 1ULL; + const std::size_t s1Out = smallOut & 1ULL; + const std::size_t row = rest | (s0Out << bit0) | (s1Out << bit1); + full(row, col) = matrix(smallOut, smallIn); + } + } + return full; +} + +/// Full ``2^N`` unitary from a QCO module (``alloc`` / ``static``, 1q/2q +/// unitaries, ``ctrl`` with X/Z body). ``std::nullopt`` on unsupported ops or +/// if ``N`` exceeds ``maxQubits``. +std::optional +computeNQubitUnitaryFromModule(const OwningOpRef& moduleOp, + std::size_t maxQubits) { + ModuleOp module = moduleOp.get(); + if (!module) { + return std::nullopt; + } + + llvm::DenseMap qubitIds; + std::size_t numQubits = 0; + + for (auto func : module.getOps()) { + for (auto& block : func.getBlocks()) { + for (auto& rawOp : block.getOperations()) { + if (auto alloc = llvm::dyn_cast(&rawOp)) { + if (numQubits >= maxQubits) { + return std::nullopt; + } + qubitIds.try_emplace(alloc.getResult(), numQubits++); + } else if (auto staticOp = llvm::dyn_cast(&rawOp)) { + const auto idx = static_cast(staticOp.getIndex()); + if (idx >= maxQubits) { + return std::nullopt; + } + qubitIds.try_emplace(staticOp.getResult(), idx); + numQubits = std::max(numQubits, idx + 1); + } + } + } + } + + if (numQubits == 0) { + return std::nullopt; + } + + TestMatrix unitary = TestMatrix::identity(1ULL << numQubits); + + auto getQubitId = [&](Value qubit) -> std::optional { + auto it = qubitIds.find(qubit); + if (it == qubitIds.end()) { + return std::nullopt; + } + return it->second; + }; + + for (auto func : module.getOps()) { + for (auto& block : func.getBlocks()) { + for (auto& rawOp : block.getOperations()) { + auto op = llvm::dyn_cast(&rawOp); + if (!op) { + continue; + } + if (llvm::isa(op.getOperation())) { + continue; + } + + if (op.isSingleQubit()) { + const auto qIn = getUnitaryQubitOperand(op, 0); + if (!qIn) { + return std::nullopt; + } + auto qid = getQubitId(*qIn); + if (!qid) { + return std::nullopt; + } + Matrix2x2 oneQ; + if (!extractSingleQubitMatrix(op, oneQ)) { + return std::nullopt; + } + unitary = expandOneQToN(oneQ, *qid, numQubits) * unitary; + const auto qOut = getUnitaryQubitResult(op, 0); + if (!qOut) { + return std::nullopt; + } + qubitIds[*qOut] = *qid; + continue; + } + + if (op.isTwoQubit()) { + const auto q0In = getUnitaryQubitOperand(op, 0); + const auto q1In = getUnitaryQubitOperand(op, 1); + if (!q0In || !q1In) { + return std::nullopt; + } + auto q0id = getQubitId(*q0In); + auto q1id = getQubitId(*q1In); + if (!q0id || !q1id) { + return std::nullopt; + } + Matrix4x4 twoQ; + if (!extractTwoQubitMatrix(op, twoQ)) { + return std::nullopt; + } + unitary = expandTwoQToN(twoQ, *q0id, *q1id, numQubits) * unitary; + const auto q0Out = getUnitaryQubitResult(op, 0); + const auto q1Out = getUnitaryQubitResult(op, 1); + if (!q0Out || !q1Out) { + return std::nullopt; + } + qubitIds[*q0Out] = *q0id; + qubitIds[*q1Out] = *q1id; + continue; + } + } + } + } + + return unitary; +} + +} // namespace mlir::qco::native_synth_test + +using namespace mlir::qco::native_synth_test; + +namespace { + +struct NativeSynthMenuRow { + const char* name; + const char* nativeGates; + bool (*isNative)(OwningOpRef&); +}; + +// --- Inline circuit builders --- + +static void broadOneQThenCz(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.x(q0); + b.y(q1); + b.h(q0); + b.sx(q1); + b.rx(0.13, q0); + b.ry(-0.47, q1); + b.rz(0.29, q0); + b.cz(q0, q1); +} + +static void zeroAngleThenCz(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.rx(0.0, q0); + b.ry(0.0, q1); + b.rz(0.0, q0); + b.p(0.0, q1); + b.cz(q0, q1); +} + +static void ibmFractionalGateFamilies(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.rx(0.13, q1); + b.cx(q0, q1); + b.cz(q1, q0); + b.swap(q0, q1); + b.rzz(-0.33, q0, q1); + b.rzx(0.41, q0, q1); +} + +static void hstycxTwoQ(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.s(q0); + b.t(q0); + b.y(q0); + b.cx(q0, q1); +} + +static void cxYOnQ1(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.cx(q0, q1); + b.y(q1); +} + +static void hCxTOnQ1(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q1); + b.cx(q0, q1); + b.t(q1); +} + +static void xYSXCz(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.x(q0); + b.y(q0); + b.sx(q0); + b.cz(q0, q1); +} + +static void hYCx(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.y(q0); + b.cx(q0, q1); +} + +static void zCx(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.z(q0); + b.cx(q0, q1); +} + +static void xHCz(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.x(q0); + b.h(q0); + b.cz(q0, q1); +} + +static void hq0Yq1CxSq0(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.y(q1); + b.cx(q0, q1); + b.s(q0); +} + +static void hCxSq1(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.cx(q0, q1); + b.s(q1); +} + +static void threeQGhz(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + const auto q2 = b.allocQubit(); + b.h(q0); + b.cx(q0, q1); + b.cx(q1, q2); +} + +static void determinismSwap(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.swap(q0, q1); + b.dealloc(q0); + b.dealloc(q1); +} + +} // namespace + +// --- NativeSpec / NativePolicy --- + +TEST(NativeSpecTest, ResolveIbmBasicCx) { + const auto spec = resolveNativeGatesSpec("x,sx,rz,cx"); + ASSERT_TRUE(spec); + EXPECT_TRUE(spec->allowedGates.contains(NativeGateKind::Cx)); + EXPECT_TRUE(spec->allowedGates.contains(NativeGateKind::X)); + EXPECT_FALSE(spec->allowRzz); +} + +TEST(NativeSpecTest, ResolveRejectsUnknownToken) { + EXPECT_FALSE(resolveNativeGatesSpec("x,sx,rz,not-a-gate").has_value()); +} + +TEST(NativeSpecTest, PhaseAliasPMatchesRzInIbmStyleMenu) { + const auto pMenu = resolveNativeGatesSpec("x,sx,p,cx"); + const auto rzMenu = resolveNativeGatesSpec("x,sx,rz,cx"); + ASSERT_TRUE(pMenu); + ASSERT_TRUE(rzMenu); + EXPECT_EQ(pMenu->allowedGates, rzMenu->allowedGates); +} + +TEST(NativeSpecTest, EmitterEulerBasisForAxisPair) { + EXPECT_EQ(emitterEulerBasis(SingleQubitEmitterSpec{ + .mode = SingleQubitMode::AxisPair, .axisPair = AxisPair::RxRz}), + EulerBasis::XZX); + EXPECT_EQ(emitterEulerBasis(SingleQubitEmitterSpec{ + .mode = SingleQubitMode::AxisPair, .axisPair = AxisPair::RyRz}), + EulerBasis::ZYZ); +} + +TEST(NativePolicyTest, UsesCxAndCzFromResolvedSpec) { + const auto cxOnly = resolveNativeGatesSpec("u,cx"); + ASSERT_TRUE(cxOnly); + EXPECT_TRUE(usesCxEntangler(*cxOnly)); + EXPECT_FALSE(usesCzEntangler(*cxOnly)); + + const auto both = resolveNativeGatesSpec("u,cx,cz"); + ASSERT_TRUE(both); + EXPECT_TRUE(usesCxEntangler(*both)); + EXPECT_TRUE(usesCzEntangler(*both)); +} + +// NOLINTNEXTLINE(misc-use-internal-linkage) +class NativePolicyAllowsOpTest : public ::testing::Test { +protected: + MLIRContext context; + QCOProgramBuilder builder{&context}; + + void SetUp() override { + context.loadDialect(); + context.loadDialect(); + context.loadDialect(); + context.loadDialect(); + builder.initialize(); + } +}; + +TEST_F(NativePolicyAllowsOpTest, AllowsSingleQubitOpRespectsMenu) { + const auto spec = resolveNativeGatesSpec("x,sx,rz,cx"); + ASSERT_TRUE(spec); + Value q = builder.staticQubit(0); + q = builder.x(q); + auto mod = builder.finalize(); + ASSERT_TRUE(mod); + XOp xop; + mod->walk([&](XOp op) { + xop = op; + return WalkResult::interrupt(); + }); + ASSERT_TRUE(xop); + EXPECT_TRUE(allowsSingleQubitOp( + llvm::cast(xop.getOperation()), *spec)); +} + +TEST_F(NativePolicyAllowsOpTest, RejectsSingleQubitOpNotInMenu) { + const auto spec = resolveNativeGatesSpec("u,cx"); + ASSERT_TRUE(spec); + Value q = builder.staticQubit(0); + q = builder.x(q); + auto mod = builder.finalize(); + ASSERT_TRUE(mod); + XOp xop; + mod->walk([&](XOp op) { + xop = op; + return WalkResult::interrupt(); + }); + ASSERT_TRUE(xop); + EXPECT_FALSE(allowsSingleQubitOp( + llvm::cast(xop.getOperation()), *spec)); +} + +TEST_F(NativePolicyAllowsOpTest, CanDirectlyDecomposeToU3OnRxInCircuit) { + Value q = builder.staticQubit(0); + q = builder.rx(0.1, q); + auto mod = builder.finalize(); + ASSERT_TRUE(mod); + RXOp rx; + mod->walk([&](RXOp op) { + rx = op; + return WalkResult::interrupt(); + }); + ASSERT_TRUE(rx); + EXPECT_TRUE(canDirectlyDecomposeToU3(rx.getOperation())); +} + +// --- Pass profile coverage --- + +// NOLINTNEXTLINE(misc-use-internal-linkage) +class NativeSynthesisSwapProfileTest + : public NativeSynthesisPassTest, + public testing::WithParamInterface { +public: + using NativeSynthesisPassTest::onlyGenericU3CxOps; + using NativeSynthesisPassTest::onlyIbmBasicCxOps; + using NativeSynthesisPassTest::onlyIqmDefaultOps; +}; + +TEST_P(NativeSynthesisSwapProfileTest, DecomposesSwapToProfile) { + const NativeSynthMenuRow& param = GetParam(); + expectNativeAfterSynthesis( + [&] { + return mlir::qc::QCProgramBuilder::build(context.get(), mlir::qc::swap); + }, + param.nativeGates, param.isNative); +} + +INSTANTIATE_TEST_SUITE_P( + SwapMenuMatrix, NativeSynthesisSwapProfileTest, + testing::Values( + NativeSynthMenuRow{"IbmBasicCx", "x,sx,rz,cx", + &NativeSynthesisSwapProfileTest::onlyIbmBasicCxOps}, + NativeSynthMenuRow{"GenericU3Cx", "u,cx", + &NativeSynthesisSwapProfileTest::onlyGenericU3CxOps}, + NativeSynthMenuRow{"IqmDefault", "r,cz", + &NativeSynthesisSwapProfileTest::onlyIqmDefaultOps}), + [](const testing::TestParamInfo& info) { + return info.param.name; + }); + +TEST_F(NativeSynthesisPassTest, DecomposesHstycxToIbmBasicCx) { + expectNativeAfterSynthesis( + [&] { + return mlir::qc::QCProgramBuilder::build(context.get(), hstycxTwoQ); + }, + "x,sx,rz,cx", &NativeSynthesisPassTest::onlyIbmBasicCxOps); +} + +TEST_F(NativeSynthesisPassTest, DecomposesCxYOnQ1ToIqmDefault) { + expectNativeAfterSynthesis( + [&] { return mlir::qc::QCProgramBuilder::build(context.get(), cxYOnQ1); }, + "r,cz", &NativeSynthesisPassTest::onlyIqmDefaultOps); +} + +TEST_F(NativeSynthesisPassTest, BroadOneQCanonicalizationOnIqmDefault) { + auto moduleOp = + mlir::qc::QCProgramBuilder::build(context.get(), broadOneQThenCz); + runNativeSynthesis(moduleOp, "r,cz"); + EXPECT_TRUE(onlyIqmDefaultOps(moduleOp)); +} + +TEST_F(NativeSynthesisPassTest, ZeroAngleCanonicalizationOnRyRzCz) { + auto moduleOp = + mlir::qc::QCProgramBuilder::build(context.get(), zeroAngleThenCz); + runNativeSynthesis(moduleOp, "ry,rz,cz"); + EXPECT_TRUE(onlyAxisPairRyRzCzOps(moduleOp)); +} + +TEST_F(NativeSynthesisPassTest, DecomposesCxToCzForIbmBasicCzProfile) { + expectNativeAfterSynthesis( + [&] { + return mlir::qc::QCProgramBuilder::build(context.get(), hCxTOnQ1); + }, + "x,sx,rz,cz", &NativeSynthesisPassTest::onlyIbmBasicCzOps); +} + +TEST_F(NativeSynthesisPassTest, DecomposesToIqmDefaultProfile) { + expectNativeAfterSynthesis( + [&] { return mlir::qc::QCProgramBuilder::build(context.get(), xYSXCz); }, + "r,cz", &NativeSynthesisPassTest::onlyIqmDefaultOps); +} + +TEST_F(NativeSynthesisPassTest, DecomposesToIbmFractionalProfile) { + expectNativeAfterSynthesis( + [&] { + return mlir::qc::QCProgramBuilder::build(context.get(), + ibmFractionalGateFamilies); + }, + "x,sx,rz,rx,rzz,cz", &NativeSynthesisPassTest::onlyIbmFractionalOps); +} + +TEST_F(NativeSynthesisPassTest, DecomposesToAxisPairRxRzCxProfile) { + expectNativeAfterSynthesis( + [&] { return mlir::qc::QCProgramBuilder::build(context.get(), hYCx); }, + "rx,rz,cx", &NativeSynthesisPassTest::onlyAxisPairRxRzCxOps); +} + +TEST_F(NativeSynthesisPassTest, DecomposesRzToAxisPairRxRyCxProfile) { + expectNativeAfterSynthesis( + [&] { return mlir::qc::QCProgramBuilder::build(context.get(), zCx); }, + "rx,ry,cx", &NativeSynthesisPassTest::onlyAxisPairRxRyCxOps); +} + +TEST_F(NativeSynthesisPassTest, GenericProfileMatchesGenericU3CxBehavior) { + expectEquivalentAndNativeAfterSynthesis( + [&] { + return mlir::qc::QCProgramBuilder::build(context.get(), hq0Yq1CxSq0); + }, + "u,cx", &NativeSynthesisPassTest::onlyGenericU3CxOps, + computeTwoQubitUnitaryFromModule); +} + +TEST_F(NativeSynthesisPassTest, GenericProfileMatchesAxisPairRyRzCzBehavior) { + expectEquivalentAndNativeAfterSynthesis( + [&] { return mlir::qc::QCProgramBuilder::build(context.get(), xHCz); }, + "ry,rz,cz", &NativeSynthesisPassTest::onlyAxisPairRyRzCzOps, + computeTwoQubitUnitaryFromModule); +} + +TEST_F(NativeSynthesisPassTest, CustomProfileAcceptsMultipleEntanglersMenu) { + expectEquivalentAndNativeAfterSynthesis( + [&] { return mlir::qc::QCProgramBuilder::build(context.get(), hCxSq1); }, + "u,cx,cz", &NativeSynthesisPassTest::onlyGenericU3CxOrCzOps, + computeTwoQubitUnitaryFromModule); +} + +TEST_F(NativeSynthesisPassTest, FailsForUnsupportedNativeGateMenu) { + expectSynthesisFailure( + [&] { + return mlir::qc::QCProgramBuilder::build(context.get(), mlir::qc::h); + }, + "not-a-gate"); +} + +TEST_F(NativeSynthesisPassTest, FailsForNativeGateMenuWithoutSingleQEmitter) { + expectSynthesisFailure( + [&] { + return mlir::qc::QCProgramBuilder::build(context.get(), + mlir::qc::singleControlledX); + }, + "cx,cz"); +} + +TEST_F(NativeSynthesisPassTest, FailsForMultiControlledGateStructure) { + expectSynthesisFailure( + [&] { + return mlir::qc::QCProgramBuilder::build(context.get(), + mlir::qc::multipleControlledX); + }, + "x,sx,rz,cx"); +} + +TEST_F(NativeSynthesisPassTest, CandidateSelectionIsDeterministicAcrossRuns) { + auto buildFn = [&] { + return mlir::qc::QCProgramBuilder::build(context.get(), determinismSwap); + }; + auto firstModule = buildFn(); + runNativeSynthesis(firstModule, "u,cx"); + auto secondModule = buildFn(); + runNativeSynthesis(secondModule, "u,cx"); + EXPECT_EQ(moduleToString(firstModule), moduleToString(secondModule)); +} + +TEST_F(NativeSynthesisPassTest, ThreeQubitGhzEquivalentOnCoreProfiles) { + for (const auto& profileCase : coreEquivalenceProfiles()) { + auto expected = mlir::qc::QCProgramBuilder::build(context.get(), threeQGhz); + runQcToQco(expected); + const auto expectedUnitary = computeNQubitUnitaryFromModule(expected); + ASSERT_TRUE(expectedUnitary.has_value()); + + auto synthesized = + mlir::qc::QCProgramBuilder::build(context.get(), threeQGhz); + runNativeSynthesis(synthesized, profileCase.nativeGates); + EXPECT_TRUE(profileCase.isNative(synthesized)); + const auto synthesizedUnitary = computeNQubitUnitaryFromModule(synthesized); + ASSERT_TRUE(synthesizedUnitary.has_value()); + EXPECT_TRUE( + isEquivalentUpToGlobalPhase(*expectedUnitary, *synthesizedUnitary)); + } +} diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_custom_menus.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_custom_menus.cpp deleted file mode 100644 index b38be63ede..0000000000 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_custom_menus.cpp +++ /dev/null @@ -1,519 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -// Custom native-gate menus, randomized equivalence, and IBM-fractional stress -// circuits for the native-gate synthesis pass. - -#include "native_synthesis_pass_test_fixture.h" -#include "native_synthesis_test_helpers.h" -#include "qc_programs.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace mlir; -using namespace mlir::qco; -using namespace mlir::qco::native_synth_test; - -namespace { - -struct CustomMenuSpec { - std::string menuCsv; - bool allowCx = false; - bool allowCz = false; - bool allowU = false; - bool allowX = false; - bool allowSX = false; - bool allowRZ = false; - bool allowRX = false; - bool allowRY = false; - bool allowR = false; - bool allowRzz = false; -}; - -} // namespace - -static std::vector splitCSV(const std::string& s) { - std::vector out; - std::size_t tokenStart = 0; - while (tokenStart <= s.size()) { - const auto tokenEnd = s.find(',', tokenStart); - const auto end = (tokenEnd == std::string::npos) ? s.size() : tokenEnd; - std::size_t left = tokenStart; - while (left < end && - std::isspace(static_cast(s[left])) != 0) { - ++left; - } - std::size_t right = end; - while (right > left && - std::isspace(static_cast(s[right - 1])) != 0) { - --right; - } - if (left < right) { - std::string token = s.substr(left, right - left); - for (char& ch : token) { - ch = static_cast(std::tolower(static_cast(ch))); - } - out.push_back(std::move(token)); - } - if (tokenEnd == std::string::npos) { - break; - } - tokenStart = tokenEnd + 1; - } - return out; -} - -static CustomMenuSpec parseCustomMenu(const std::string& csv) { - CustomMenuSpec spec; - spec.menuCsv = csv; - for (const auto& tok : splitCSV(csv)) { - if (tok == "u") { - spec.allowU = true; - } else if (tok == "x") { - spec.allowX = true; - } else if (tok == "sx") { - spec.allowSX = true; - } else if (tok == "rz" || tok == "p") { - // ``p`` is an alias for Z-axis rotation in ``native-gates`` (see pass - // docs). - spec.allowRZ = true; - } else if (tok == "rx") { - spec.allowRX = true; - } else if (tok == "ry") { - spec.allowRY = true; - } else if (tok == "r") { - spec.allowR = true; - } else if (tok == "cx") { - spec.allowCx = true; - } else if (tok == "cz") { - spec.allowCz = true; - } else if (tok == "rzz") { - spec.allowRzz = true; - } - } - return spec; -} - -static bool onlyAllowsMenuNativeOps(ModuleOp moduleOp, - const CustomMenuSpec& spec) { - bool ok = true; - moduleOp.walk([&](Operation* op) { - if (!ok) { - return; - } - if (!llvm::isa(op)) { - return; - } - // Non-synthesized helper ops are allowed to remain. - if (llvm::isa(op)) { - return; - } - if (llvm::isa(op)) { - return; - } - - // Treat `p` as a phase/Z-rotation alias when `rz` is allowed. - if (llvm::isa(op)) { - ok = spec.allowRZ; - return; - } - - if (llvm::isa(op)) { - ok = spec.allowU; - return; - } - if (llvm::isa(op)) { - // `cx` is represented as a `qco.ctrl` with a `qco.x` in the body region. - if (llvm::isa_and_present(op->getParentOp())) { - ok = spec.allowCx; - } else { - ok = spec.allowX; - } - return; - } - if (llvm::isa(op)) { - ok = spec.allowSX; - return; - } - if (llvm::isa(op)) { - ok = spec.allowRZ; - return; - } - if (llvm::isa(op)) { - if (spec.allowRX) { - ok = true; - return; - } - // If `rx` is not native, only the `rx(±pi)` case is accepted as an - // X-equivalent under the IBM-basic family fallback. - if (!(spec.allowX && spec.allowSX && spec.allowRZ)) { - ok = false; - return; - } - auto rx = llvm::cast(op); - const auto theta = evaluateConstF64(rx.getTheta()); - if (!theta.has_value()) { - ok = false; - return; - } - const double rem = std::remainder(*theta, 2.0 * std::numbers::pi); - ok = std::abs(std::abs(rem) - std::numbers::pi) <= 1e-10; - return; - } - if (llvm::isa(op)) { - ok = spec.allowRY; - return; - } - if (llvm::isa(op)) { - // `cz` is represented as a `qco.ctrl` with a `qco.z` in the body region. - if (llvm::isa_and_present(op->getParentOp())) { - ok = spec.allowCz; - } else { - ok = false; - } - return; - } - if (llvm::isa(op)) { - ok = spec.allowR; - return; - } - if (llvm::isa(op)) { - ok = spec.allowRzz; - return; - } - if (auto ctrl = llvm::dyn_cast(op)) { - if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { - ok = false; - return; - } - Operation* body = ctrl.getBodyUnitary(0).getOperation(); - if (llvm::isa(body)) { - ok = spec.allowCx; - return; - } - if (llvm::isa(body)) { - ok = spec.allowCz; - return; - } - ok = false; - return; - } - ok = false; - }); - return ok; -} - -TEST_F(NativeSynthesisPassTest, RandomizedCustomMenusAndCircuitsAreEquivalent) { - // Sample many valid custom menus and generate matching random input circuits. - // For each case, we assert that native synthesis (a) succeeds, (b) emits only - // ops allowed by the menu, and (c) preserves the 2-qubit unitary up to global - // phase. - std::mt19937 rng(0xC0FFEE); - std::uniform_real_distribution angle(-1.0, 1.0); - std::uniform_int_distribution stepsDist(4, 14); - std::uniform_int_distribution gateDist(0, 9); - std::uniform_int_distribution whichQubit(0, 1); - - // Menus are chosen from known-valid families that the pass supports. - const std::vector menuPool = { - "u,cx", "u,cz", "x,sx,rz,rx,cx", "rx,rz,cx", "rx,ry,cx", - "ry,rz,cz", "r,cz", "u,rx,rz,cx,cz", "u,rx,rz,cx", - }; - std::uniform_int_distribution menuDist(0, menuPool.size() - 1); - - constexpr int numCases = 18; - for (int caseIdx = 0; caseIdx < numCases; ++caseIdx) { - const std::string& menuCsv = menuPool[menuDist(rng)]; - const auto menuSpec = parseCustomMenu(menuCsv); - - // Build an input circuit that uses only two qubits and (if present) only - // the entangler types allowed by the menu. Use a mix of operations that the - // pass is expected to rewrite into the menu. - auto buildCircuit = [&]() { - mlir::qc::QCProgramBuilder builder(context.get()); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - - const int steps = stepsDist(rng); - for (int i = 0; i < steps; ++i) { - const auto q = (whichQubit(rng) == 0) ? q0 : q1; - - // Choose operations based on the menu family to avoid generating inputs - // that are not exactly synthesizable with the configured gateset. - if (menuSpec.allowU) { - // Keep input gates within the robust unitary evaluator set. - switch (gateDist(rng) % 5) { - case 0: - builder.rz(angle(rng), q); - break; - case 1: - builder.rx(angle(rng), q); - break; - case 2: - builder.ry(angle(rng), q); - break; - case 3: - builder.p(angle(rng), q); - break; - case 4: - if (menuSpec.allowCz) { - builder.cz(q0, q1); - } else if (menuSpec.allowCx) { - builder.cx(q0, q1); - } else { - builder.rz(angle(rng), q); - } - break; - default: - break; - } - } else if (menuSpec.allowR && menuSpec.allowCz && !menuSpec.allowCx) { - // Minimal r/cz menu: generate only operations directly expressible in - // that gateset so synthesis is required to succeed. - switch (gateDist(rng) % 4) { - case 0: - builder.r(angle(rng), angle(rng), q); - break; - case 1: - // X/Y-like rotations expressed via r(theta, phi). - builder.r(std::numbers::pi, angle(rng), q); - break; - case 2: - builder.r(angle(rng), angle(rng), q); - break; - case 3: - builder.cz(q0, q1); - break; - default: - break; - } - } else if (menuSpec.allowRX && menuSpec.allowRY && menuSpec.allowCx && - !menuSpec.allowRZ) { - // Axis-pair RX/RY with CX: avoid Z-axis primitives. - switch (gateDist(rng) % 6) { - case 0: - builder.rx(angle(rng), q); - break; - case 1: - builder.ry(angle(rng), q); - break; - case 2: - builder.rx(std::numbers::pi, q); - break; - case 3: - builder.ry(std::numbers::pi, q); - break; - case 4: - builder.ry(angle(rng), q); - break; - case 5: - builder.cx(q0, q1); - break; - default: - break; - } - } else if (menuSpec.allowRX && menuSpec.allowRZ && menuSpec.allowCx) { - // Axis-pair RX/RZ with CX. - switch (gateDist(rng) % 6) { - case 0: - builder.rx(angle(rng), q); - break; - case 1: - builder.rz(angle(rng), q); - break; - case 2: - builder.rx(std::numbers::pi, q); - break; - case 3: - builder.rz(std::numbers::pi, q); - break; - case 4: - builder.rz(angle(rng), q); - break; - case 5: - builder.cx(q0, q1); - break; - default: - break; - } - } else if (menuSpec.allowRY && menuSpec.allowRZ && menuSpec.allowCz) { - // Axis-pair RY/RZ with CZ. - switch (gateDist(rng) % 6) { - case 0: - builder.ry(angle(rng), q); - break; - case 1: - builder.rz(angle(rng), q); - break; - case 2: - builder.ry(std::numbers::pi, q); - break; - case 3: - builder.rz(std::numbers::pi, q); - break; - case 4: - builder.rz(angle(rng), q); - break; - case 5: - builder.cz(q0, q1); - break; - default: - break; - } - } else { - // IBM-basic-ish menus (x,sx,rz[,rx],cx): use Z/SX patterns + CX. - switch (gateDist(rng) % 7) { - case 0: - builder.rz(angle(rng), q); - break; - case 1: - builder.p(angle(rng), q); - break; - case 2: - builder.rz(angle(rng), q); - break; - case 3: - builder.rx(menuSpec.allowRX ? angle(rng) : std::numbers::pi, q); - break; - case 4: - builder.rz(angle(rng), q); - break; - case 5: - if (menuSpec.allowRX) { - builder.rx(angle(rng), q); - } else { - builder.p(angle(rng), q); - } - break; - case 6: - if (menuSpec.allowCx) { - builder.cx(q0, q1); - } else { - builder.rz(angle(rng), q); - } - break; - default: - break; - } - } - } - - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); - }; - - // Build the random circuit exactly once, then clone it for the expected and - // synthesized paths so the unitary comparison is meaningful. - auto input = buildCircuit(); - const auto inputText = moduleToString(input); - - auto expected = - mlir::parseSourceString(inputText, context.get()); - ASSERT_TRUE(expected) << "case=" << caseIdx; - runQcToQco(expected); - const auto expectedUnitary = computeTwoQubitUnitaryFromModule(expected); - if (!expectedUnitary.has_value()) { - ADD_FAILURE() << "Failed to reconstruct expected unitary for case=" - << caseIdx << " menu=" << menuCsv << "\nIR:\n" - << moduleToString(expected); - continue; - } - - auto synthesized = - mlir::parseSourceString(inputText, context.get()); - ASSERT_TRUE(synthesized) << "case=" << caseIdx; - { - PassManager pm(synthesized->getContext()); - pm.addPass(createQCToQCO()); - pm.addPass( - qco::createNativeGateSynthesisPass(qco::NativeGateSynthesisOptions{ - .nativeGates = menuCsv, - })); - if (failed(pm.run(*synthesized))) { - ADD_FAILURE() << "Native synthesis failed for menu=" << menuCsv - << " case=" << caseIdx << "\nQC/QCO IR:\n" - << moduleToString(synthesized); - continue; - } - } - - EXPECT_TRUE(onlyAllowsMenuNativeOps(synthesized.get(), menuSpec)) - << "menu=" << menuCsv << "\nIR:\n" - << moduleToString(synthesized); - - const auto synthesizedUnitary = - computeTwoQubitUnitaryFromModule(synthesized); - ASSERT_TRUE(synthesizedUnitary.has_value()) << "case=" << caseIdx; - EXPECT_TRUE( - isEquivalentUpToGlobalPhase(*expectedUnitary, *synthesizedUnitary)) - << "menu=" << menuCsv << " case=" << caseIdx; - } -} - -TEST_F(NativeSynthesisPassTest, - LargeCircuitEquivalentAndNativeGatesIbmFractional) { - auto buildStressCircuit = [&](MLIRContext* ctx) { - return mlir::qc::QCProgramBuilder::build( - ctx, mlir::qc::nativeSynthCustomMenusIbmFractionalTwoQStress); - }; - expectEquivalentAndNativeAfterSynthesis( - [&] { return buildStressCircuit(context.get()); }, "x,sx,rz,rx,rzz,cz", - &NativeSynthesisPassTest::onlyIbmFractionalOps, - computeTwoQubitUnitaryFromModule); -} - -TEST_F(NativeSynthesisPassTest, - AllGateFamiliesEquivalentAndNativeIbmFractional) { - expectEquivalentAndNativeAfterSynthesis( - [&] { return buildIbmFractionalAllGateFamiliesCircuit(); }, - "x,sx,rz,rx,rzz,cz", &NativeSynthesisPassTest::onlyIbmFractionalOps, - computeTwoQubitUnitaryFromModule); -} - -TEST_F(NativeSynthesisPassTest, XXPlusMinusYYEquivalentAndNativeIbmFractional) { - constexpr const char* kIbmFrac = "x,sx,rz,rx,rzz,cz"; - expectEquivalentAndNativeAfterSynthesis( - [&] { - return mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::nativeSynthCustomMenusXxPlusYyChain); - }, - kIbmFrac, &NativeSynthesisPassTest::onlyIbmFractionalOps, - computeTwoQubitUnitaryFromModule); - expectEquivalentAndNativeAfterSynthesis( - [&] { - return mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::nativeSynthCustomMenusXxMinusYyOnly); - }, - kIbmFrac, &NativeSynthesisPassTest::onlyIbmFractionalOps, - computeTwoQubitUnitaryFromModule); -} diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_fusion.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_fusion.cpp deleted file mode 100644 index 637537204f..0000000000 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_fusion.cpp +++ /dev/null @@ -1,445 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -// 1q run merging, 2q block consolidation, and RZX profile sweeps for the -// native-gate synthesis pass. Includes a few ``TEST_P`` matrices (U3 GPhase -// pair, generic-u3-cx two-qubit equivalence rows). Linked with sibling -// ``test_native_synthesis_*.cpp`` sources into -// ``mqt-core-mlir-unittest-native-synthesis``. - -#include "native_synthesis_pass_test_fixture.h" -#include "native_synthesis_test_helpers.h" -#include "qc_programs.h" - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -using namespace mlir; -using namespace mlir::qco; -using namespace mlir::qco::native_synth_test; - -namespace { - -struct OneQU3FusionGPhaseRow { - const char* name; - void (*program)(mlir::qc::QCProgramBuilder&); - unsigned expectGPhaseCount; -}; - -struct TwoQBlockEquivGenericU3CxRow { - const char* name; - void (*program)(mlir::qc::QCProgramBuilder&); - std::optional expectExactCtrlOpCount; -}; - -} // namespace - -// Count ops of a given MLIR op type across a module; used to assert the -// effects of the 1q-run-merging pre-synthesis step on concrete programs. -template -static std::size_t -countOpsOfTypeInModule(const OwningOpRef& moduleOp) { - std::size_t count = 0; - moduleOp.get()->walk([&](Operation* op) { - if (llvm::isa(op)) { - ++count; - } - }); - return count; -} - -// NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_P` at global scope -class NativeSynthesisOneQFusionU3GPhaseTest - : public NativeSynthesisPassTest, - public testing::WithParamInterface { -public: - using NativeSynthesisPassTest::onlyGenericU3CxOps; -}; - -TEST_P(NativeSynthesisOneQFusionU3GPhaseTest, FusesAdjacentNativeUChain) { - const OneQU3FusionGPhaseRow& param = GetParam(); - auto moduleOp = - mlir::qc::QCProgramBuilder::build(context.get(), param.program); - runNativeSynthesis(moduleOp, "u,cx"); - EXPECT_TRUE(onlyGenericU3CxOps(moduleOp)); - EXPECT_EQ(countOpsOfTypeInModule(moduleOp), 1U); - EXPECT_EQ(countOpsOfTypeInModule(moduleOp), - param.expectGPhaseCount); -} - -INSTANTIATE_TEST_SUITE_P( - OneQRunMergingU3GPhaseMatrix, NativeSynthesisOneQFusionU3GPhaseTest, - // `T * S = diag(1, e^{i*3pi/4})` is captured exactly by `U(0, 0, 3pi/4)`, - // so no residual `gphase` is needed. A generic `SU(2)` run (two det-1 `U` - // gates) cannot be written as a single `U` gate without a residual phase, - // because `U(theta, phi, lambda)` has determinant `e^{i*(phi + lambda)}`; - // the leftover `-(phi + lambda) / 2` global phase is emitted as `gphase`. - testing::Values(OneQU3FusionGPhaseRow{"OmitsGPhaseWhenU3IsExact", - mlir::qc::nativeSynthFusionTS, - /*expectGPhaseCount=*/0U}, - OneQU3FusionGPhaseRow{"EmitsGlobalPhaseForSu2ViaU3", - mlir::qc::nativeSynthFusionUUTwoQDet1, - /*expectGPhaseCount=*/1U}), - [](const testing::TestParamInfo& info) { - return info.param.name; - }); - -// NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_P` at global scope -class NativeSynthesisTwoQBlockEquivGenericU3CxTest - : public NativeSynthesisPassTest, - public testing::WithParamInterface { -public: - using NativeSynthesisPassTest::onlyGenericU3CxOps; -}; - -TEST_P(NativeSynthesisTwoQBlockEquivGenericU3CxTest, - EquivalentUnderConsolidation) { - const TwoQBlockEquivGenericU3CxRow& param = GetParam(); - auto buildFn = [&] { - return mlir::qc::QCProgramBuilder::build(context.get(), param.program); - }; - - auto expected = buildFn(); - runQcToQco(expected); - const auto expectedUnitary = computeTwoQubitUnitaryFromModule(expected); - ASSERT_TRUE(expectedUnitary.has_value()); - - auto synth = buildFn(); - runNativeSynthesis(synth, "u,cx"); - EXPECT_TRUE(onlyGenericU3CxOps(synth)); - if (param.expectExactCtrlOpCount.has_value()) { - EXPECT_EQ(countOpsOfTypeInModule(synth), - *param.expectExactCtrlOpCount); - } - const auto synthUnitary = computeTwoQubitUnitaryFromModule(synth); - ASSERT_TRUE(synthUnitary.has_value()); - EXPECT_TRUE(isEquivalentUpToGlobalPhase(*expectedUnitary, *synthUnitary)); -} - -TEST(NativeSynthesisFusionTest, - IsEquivalentUpToGlobalPhaseRejectsNearZeroOverlap) { - const Matrix2x2 lhs = Matrix2x2::identity(); - const Matrix2x2 rhs = Matrix2x2::fromElements(1.0, 0.0, 0.0, -1.0); - // overlap = trace(rhs^H * lhs) = trace(Z) = 0 -> early false branch. - EXPECT_FALSE(isEquivalentUpToGlobalPhase(lhs, rhs, 1e-10)); -} - -INSTANTIATE_TEST_SUITE_P( - TwoQBlockEquivGenericU3CxMatrix, - NativeSynthesisTwoQBlockEquivGenericU3CxTest, - testing::Values( - TwoQBlockEquivGenericU3CxRow{"AdjacentCxCancel", - mlir::qc::nativeSynthFusionCxCx, - /*expectExactCtrlOpCount=*/0U}, - TwoQBlockEquivGenericU3CxRow{ - "FusesCxThroughInterleavedOneQOps", - mlir::qc::nativeSynthFusionHCxInterleavedTCx, std::nullopt}, - TwoQBlockEquivGenericU3CxRow{"HandlesSwappedWireOrder", - mlir::qc::nativeSynthFusionSwapCxPattern, - std::nullopt}, - TwoQBlockEquivGenericU3CxRow{"EquivalentWhenBlockContainsDcx", - mlir::qc::nativeSynthFusionHDcxSCx, - std::nullopt}, - TwoQBlockEquivGenericU3CxRow{"EquivalentWhenBlockContainsRzx", - mlir::qc::nativeSynthFusionXRzxTCx, - std::nullopt}), - [](const testing::TestParamInfo& info) { - return info.param.name; - }); - -// --- 1q-run-merging pre-synthesis step --- -// -// The tests below exercise the in-pass run merging that fuses adjacent -// single-qubit `UnitaryOpInterface` ops on the same wire before per-op -// native-gate emission. They cover (a) the reductions unlocked by fusion, -// (b) that the fusion respects boundaries (CX, barrier, multi-use), and -// (c) unitary equivalence over longer mixed chains. - -TEST_F(NativeSynthesisPassTest, OneQRunMergingCollapsesHadamardZHadamardToX) { - // H * Z * H = X (up to global phase). With fusion enabled, the ibm-basic - // emitter hits the ZSXX X-shortcut and emits a single X, whereas without - // fusion we would expect at least 3 RZ gates from two H decompositions and - // the Z. - auto buildFn = [&] { - return mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::nativeSynthFusionHadamardZHadamard); - }; - - auto moduleOp = buildFn(); - runNativeSynthesis(moduleOp, "x,sx,rz,cx"); - EXPECT_TRUE(onlyIbmBasicCxOps(moduleOp)); - EXPECT_EQ(countOpsOfTypeInModule(moduleOp), 1U); - EXPECT_EQ(countOpsOfTypeInModule(moduleOp), 0U); - EXPECT_EQ(countOpsOfTypeInModule(moduleOp), 0U); -} - -TEST_F(NativeSynthesisPassTest, OneQRunMergingCancelsAdjacentSelfInverses) { - // H * H = I. Fusion collapses the run to no 1q ops at all. - auto buildFn = [&] { - return mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::nativeSynthFusionHadamardHadamard); - }; - - auto moduleOp = buildFn(); - runNativeSynthesis(moduleOp, "x,sx,rz,cx"); - EXPECT_TRUE(onlyIbmBasicCxOps(moduleOp)); - EXPECT_EQ(countOpsOfTypeInModule(moduleOp), 0U); - EXPECT_EQ(countOpsOfTypeInModule(moduleOp), 0U); - EXPECT_EQ(countOpsOfTypeInModule(moduleOp), 0U); -} - -TEST_F(NativeSynthesisPassTest, OneQRunMergingReducesMixedChainToSingleU) { - // A long chain of distinct 1q ops on a single wire still collapses to a - // single UOp on the generic-u3-cx profile via fusion, regardless of the - // mix of non-native ops in the input. - auto buildFn = [&] { - return mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::nativeSynthFusionMixedChainHSTYSX); - }; - - auto moduleOp = buildFn(); - runNativeSynthesis(moduleOp, "u,cx"); - EXPECT_TRUE(onlyGenericU3CxOps(moduleOp)); - EXPECT_EQ(countOpsOfTypeInModule(moduleOp), 1U); -} - -TEST_F(NativeSynthesisPassTest, OneQRunMergingDoesNotFuseAcrossCX) { - // H(q0); CX(q0,q1); H(q0) must NOT be fused because CX breaks the run - // on q0. Equivalence still holds; to witness that fusion did not happen - // we assert we still see >=2 SX gates (one from each Hadamard expansion). - expectEquivalentAndNativeAfterSynthesis( - [&] { - return mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::nativeSynthFusionHadamardCxHadamard); - }, - "x,sx,rz,cx", &NativeSynthesisPassTest::onlyIbmBasicCxOps, - computeTwoQubitUnitaryFromModule); - - auto moduleOp = mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::nativeSynthFusionHadamardCxHadamard); - runNativeSynthesis(moduleOp, "x,sx,rz,cx"); - // Each H decomposes to rz(pi/2) sx rz(pi/2); without fusion we get two - // separate decompositions => at least 2 SX gates total. - EXPECT_GE(countOpsOfTypeInModule(moduleOp), 2U); -} - -TEST_F(NativeSynthesisPassTest, OneQRunMergingDoesNotFuseAcrossBarrier) { - // A barrier between two 1q ops on the same wire interrupts the run: - // `BarrierOp` is explicitly excluded from fusibility and its use of the - // qubit breaks the single-use precondition on the intermediate value. - auto moduleOp = mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::nativeSynthFusionHadamardBarrierHadamard); - runNativeSynthesis(moduleOp, "x,sx,rz,cx"); - EXPECT_TRUE(onlyIbmBasicCxOps(moduleOp)); - // Two separate H decompositions survive => at least 2 SX gates. - EXPECT_GE(countOpsOfTypeInModule(moduleOp), 2U); -} - -TEST_F(NativeSynthesisPassTest, OneQRunMergingSkipsFullyNativeRuns) { - // A run consisting entirely of ops that are already native to the - // ibm-basic-cx profile (rz; sx; rz) is pass-through: the cost gate only - // fuses a fully-native run when fusion would produce strictly fewer ops - // than the original run. For `rz; sx; rz` the ZSXX decomposition of the - // fused matrix is itself three ops, so the run is left untouched. - auto moduleOp = mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::nativeSynthFusionRzSxRz); - runNativeSynthesis(moduleOp, "x,sx,rz,cx"); - EXPECT_TRUE(onlyIbmBasicCxOps(moduleOp)); - EXPECT_EQ(countOpsOfTypeInModule(moduleOp), 2U); - EXPECT_EQ(countOpsOfTypeInModule(moduleOp), 1U); -} - -TEST_F(NativeSynthesisPassTest, - OneQRunMergingCostGateFusesFullyNativeUChainGenericU3) { - // Phase-A cost-gate refinement (fully-native path): two adjacent native - // `u` ops on the same wire fuse into a single `u` because U3 mode always - // emits exactly one gate per fused 2x2 unitary. Without the cost gate, - // the fully-native run would be skipped; without fusion, the run would - // survive as two ops because there is no `MergeSubsequentU` canonicalizer. - auto moduleOp = mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::nativeSynthFusionUUTwoQGenericU3); - runNativeSynthesis(moduleOp, "u,cx"); - EXPECT_TRUE(onlyGenericU3CxOps(moduleOp)); - EXPECT_EQ(countOpsOfTypeInModule(moduleOp), 1U); -} - -// GPhase expectations for adjacent native ``U`` fusion are covered by -// ``OneQRunMergingU3GPhaseMatrix`` (``EmitsGlobalPhaseOnU3`` / -// ``OmitsGPhaseWhenResidualIsTrivial``). - -TEST_F(NativeSynthesisPassTest, - OneQRunMergingLongMixedChainEquivalentAcrossProfiles) { - // A ten-op mixed chain on a single wire must fuse to the correct unitary - // on every CX-friendly reference profile (see - // ``fiveCxEntanglerEquivalenceProfiles``), excluding IQM-default ``r,cz``, - // which uses a different two-qubit path. - auto buildFn = [&] { - return mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::nativeSynthFusionLongMixedTenOpCx); - }; - - const auto profiles = - NativeSynthesisPassTest::fiveCxEntanglerEquivalenceProfiles(); - for (const auto& pc : profiles) { - // Expected and synthesized unitaries both come from the permissive - // default helper, which understands the full alphabet the builder emits - // and the R/RX/RY/RZ/U/P gates produced by synthesis. - auto expected = buildFn(); - runQcToQco(expected); - const auto expectedUnitary = computeTwoQubitUnitaryFromModule(expected); - ASSERT_TRUE(expectedUnitary.has_value()) - << "native-gates=" << pc.nativeGates; - - auto synth = buildFn(); - runNativeSynthesis(synth, pc.nativeGates); - EXPECT_TRUE(pc.isNative(synth)) << "native-gates=" << pc.nativeGates; - const auto synthUnitary = computeTwoQubitUnitaryFromModule(synth); - ASSERT_TRUE(synthUnitary.has_value()) << "native-gates=" << pc.nativeGates; - EXPECT_TRUE(isEquivalentUpToGlobalPhase(*expectedUnitary, *synthUnitary)) - << "native-gates=" << pc.nativeGates; - } -} - -// --- 2q-block-consolidation pre-synthesis step (Phase B) --- -// -// These tests exercise the in-pass 2q block consolidation that collects -// adjacent two-qubit ops (plus interleaved single-qubit ops) acting on the -// same pair of wires, composes a 4x4 unitary, and re-synthesizes the block -// via `TwoQubitBasisDecomposer`. They cover (a) reductions unlocked by -// consolidation, (b) fully-native blocks that are only rewritten when -// strictly shorter, and (c) boundary conditions such as wire swaps and -// interleaved barriers. - -// Generic-u3-cx two-qubit block equivalence rows (including adjacent-CX -// cancellation) live in ``TwoQBlockEquivGenericU3CxMatrix``. - -TEST_F(NativeSynthesisPassTest, - TwoQBlockConsolidationStopsAtDifferentPairBoundary) { - // Consolidation must not cross a 2q op that touches a different pair of - // wires. We arrange two back-to-back `cx(q0, q1)` separated by a - // `cx(q1, q2)` so block consolidation cannot fuse the outer pair into a - // single identity; equivalence still has to hold. - auto buildFn = [&] { - return mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::nativeSynthFusionThreeLineCx01Cx12Cx01); - }; - - auto synth = buildFn(); - runNativeSynthesis(synth, "u,cx"); - EXPECT_TRUE(onlyGenericU3CxOps(synth)); - // At least the middle CX(q1,q2) must survive because its pair differs - // from the outer CX(q0,q1) block; consolidation cannot eliminate it. - EXPECT_GE(countOpsOfTypeInModule(synth), 1U); -} - -TEST_F(NativeSynthesisPassTest, - TwoQBlockConsolidationDoesNotFuseAcrossBarrier) { - // A barrier between two CX(q0,q1) blocks must prevent them from being - // fused into a single block. Each CX stays an individual entangler. - auto buildFn = [&] { - return mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::nativeSynthFusionCxBarrierCx); - }; - - auto synth = buildFn(); - runNativeSynthesis(synth, "u,cx"); - EXPECT_TRUE(onlyGenericU3CxOps(synth)); - // The barrier prevents block consolidation from cancelling the pair, so - // both CX ops survive as separate entanglers. - EXPECT_EQ(countOpsOfTypeInModule(synth), 2U); -} - -TEST_F(NativeSynthesisPassTest, - TwoQBlockConsolidationHandlesRzzOnIbmFractional) { - // Explicitly exercise a non-CX/CZ two-qubit gate inside a block on a - // profile that supports it natively. Consolidation may keep/reshape the - // block, but equivalence and profile validity must hold. - auto buildFn = [&] { - return mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::nativeSynthFusionHRzzSRzz); - }; - - auto expected = buildFn(); - runQcToQco(expected); - const auto expectedUnitary = computeTwoQubitUnitaryFromModule(expected); - ASSERT_TRUE(expectedUnitary.has_value()); - - auto synth = buildFn(); - runNativeSynthesis(synth, "x,sx,rz,rx,rzz,cz"); - EXPECT_TRUE(onlyIbmFractionalOps(synth)); - const auto synthUnitary = computeTwoQubitUnitaryFromModule(synth); - ASSERT_TRUE(synthUnitary.has_value()); - EXPECT_TRUE(isEquivalentUpToGlobalPhase(*expectedUnitary, *synthUnitary)); -} - -TEST_F(NativeSynthesisPassTest, - RzxStandaloneSynthesisEquivalentAcrossProfiles) { - // Directed RZX tests (asymmetric 2q); both operand orders. - const auto profiles = NativeSynthesisPassTest::allNineEquivalenceProfiles(); - - // Four directed RZX fixtures: two angles × two operand orders. - struct RzxStandaloneRow { - double theta; - bool swapOperands; - void (*program)(mlir::qc::QCProgramBuilder&); - }; - const std::array rzxRows{{ - RzxStandaloneRow{.theta = 0.41, - .swapOperands = false, - .program = mlir::qc::nativeSynthFusionRzx041Q0First}, - RzxStandaloneRow{.theta = 0.41, - .swapOperands = true, - .program = mlir::qc::nativeSynthFusionRzx041Q1First}, - RzxStandaloneRow{.theta = std::numbers::pi / 2.0, - .swapOperands = false, - .program = mlir::qc::nativeSynthFusionRzxPiHalfQ0First}, - RzxStandaloneRow{.theta = std::numbers::pi / 2.0, - .swapOperands = true, - .program = mlir::qc::nativeSynthFusionRzxPiHalfQ1First}, - }}; - - for (const auto& profileCase : profiles) { - for (const RzxStandaloneRow& row : rzxRows) { - auto buildFn = [&] { - return mlir::qc::QCProgramBuilder::build(context.get(), row.program); - }; - - auto expected = buildFn(); - runQcToQco(expected); - const auto expectedUnitary = computeTwoQubitUnitaryFromModule(expected); - ASSERT_TRUE(expectedUnitary.has_value()) - << "native-gates=" << profileCase.nativeGates - << " theta=" << row.theta << " swapped=" << row.swapOperands; - - auto synth = buildFn(); - runNativeSynthesis(synth, profileCase.nativeGates); - EXPECT_TRUE(profileCase.isNative(synth)) - << "native-gates=" << profileCase.nativeGates - << " theta=" << row.theta << " swapped=" << row.swapOperands; - const auto synthUnitary = computeTwoQubitUnitaryFromModule(synth); - ASSERT_TRUE(synthUnitary.has_value()) - << "native-gates=" << profileCase.nativeGates - << " theta=" << row.theta << " swapped=" << row.swapOperands; - EXPECT_TRUE(isEquivalentUpToGlobalPhase(*expectedUnitary, *synthUnitary)) - << "native-gates=" << profileCase.nativeGates - << " theta=" << row.theta << " swapped=" << row.swapOperands; - } - } -} diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_multi_qubit.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_multi_qubit.cpp deleted file mode 100644 index b9fa9b14f1..0000000000 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_multi_qubit.cpp +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -// Multi-qubit equivalence sweeps (3q circuit families, 5q stress) for the -// native-gate synthesis pass. - -#include "native_synthesis_pass_test_fixture.h" -#include "native_synthesis_test_helpers.h" -#include "qc_programs.h" - -#include -#include -#include -#include -#include - -#include -#include - -using namespace mlir; -using namespace mlir::qco; -using namespace mlir::qco::native_synth_test; - -static OwningOpRef buildThreeQGhzCircuit(MLIRContext* context) { - return mlir::qc::QCProgramBuilder::build( - context, mlir::qc::nativeSynthMultiQThreeQGhz); -} - -static OwningOpRef buildThreeQToffoliCircuit(MLIRContext* context) { - return mlir::qc::QCProgramBuilder::build( - context, mlir::qc::nativeSynthMultiQThreeQToffoli); -} - -static OwningOpRef buildThreeQQftCircuit(MLIRContext* context) { - return mlir::qc::QCProgramBuilder::build( - context, mlir::qc::nativeSynthMultiQThreeQQft); -} - -static OwningOpRef -buildThreeQCliffordTMixCircuit(MLIRContext* context) { - return mlir::qc::QCProgramBuilder::build( - context, mlir::qc::nativeSynthMultiQThreeQCliffordTMix); -} - -namespace { - -struct ThreeQubitCircuitCase { - const char* name; - OwningOpRef (*build)(MLIRContext*); -}; - -const std::array THREE_QUBIT_CIRCUIT_CASES{{ - {.name = "ghz-3", .build = &buildThreeQGhzCircuit}, - {.name = "toffoli-3", .build = &buildThreeQToffoliCircuit}, - {.name = "qft-3", .build = &buildThreeQQftCircuit}, - {.name = "clifford-t-3", .build = &buildThreeQCliffordTMixCircuit}, -}}; - -} // namespace - -// NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest fixture at global scope -class NativeSynthesisPassMultiQubitTest : public NativeSynthesisPassTest { -protected: - template - void verifyEquivalentAcrossProfiles(BuildFn buildFn, - const char* circuitName = nullptr) { - const auto profiles = allNineEquivalenceProfiles(); - for (const auto& profileCase : profiles) { - const std::string prefix = - circuitName != nullptr ? std::string("circuit=") + circuitName + " " - : ""; - auto expected = buildFn(); - runQcToQco(expected); - const auto expectedUnitary = computeNQubitUnitaryFromModule(expected); - ASSERT_TRUE(expectedUnitary.has_value()) - << prefix << "native-gates=" << profileCase.nativeGates; - - auto synthesized = buildFn(); - runNativeSynthesis(synthesized, profileCase.nativeGates); - EXPECT_TRUE(profileCase.isNative(synthesized)) - << prefix << "native-gates=" << profileCase.nativeGates; - - const auto synthesizedUnitary = - computeNQubitUnitaryFromModule(synthesized); - ASSERT_TRUE(synthesizedUnitary.has_value()) - << prefix << "native-gates=" << profileCase.nativeGates; - EXPECT_TRUE( - isEquivalentUpToGlobalPhase(*expectedUnitary, *synthesizedUnitary)) - << prefix << "native-gates=" << profileCase.nativeGates; - } - } -}; - -TEST_F(NativeSynthesisPassMultiQubitTest, - ThreeQubitCircuitsEquivalentAcrossProfiles) { - for (const auto& circuitCase : THREE_QUBIT_CIRCUIT_CASES) { - verifyEquivalentAcrossProfiles( - [&] { return circuitCase.build(context.get()); }, circuitCase.name); - } -} - -static OwningOpRef buildFiveQubitStressCircuit(MLIRContext* context) { - return mlir::qc::QCProgramBuilder::build( - context, mlir::qc::nativeSynthMultiQFiveQStressFourLayers); -} - -TEST_F(NativeSynthesisPassMultiQubitTest, - FiveQubitStressCircuitEquivalentAcrossProfiles) { - verifyEquivalentAcrossProfiles( - [&] { return buildFiveQubitStressCircuit(context.get()); }); -} diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_profiles.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_profiles.cpp deleted file mode 100644 index 395f62376e..0000000000 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis_pass_profiles.cpp +++ /dev/null @@ -1,486 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "native_synthesis_pass_test_fixture.h" -#include "native_synthesis_test_helpers.h" -#include "qc_programs.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace mlir; -using namespace mlir::qco; -using namespace mlir::qco::native_synth_test; - -namespace { - -/// Row for ``native-gates`` menu + IR predicate used by several profile -/// matrices. -struct NativeSynthMenuRow { - const char* name; - const char* nativeGates; - bool (*isNative)(OwningOpRef&); -}; - -} // namespace - -// NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_P` at global scope -class NativeSynthesisSwapProfileTest - : public NativeSynthesisPassTest, - public testing::WithParamInterface { -public: - using NativeSynthesisPassTest::onlyAxisPairRxRzCxOps; - using NativeSynthesisPassTest::onlyAxisPairRyRzCzOps; - using NativeSynthesisPassTest::onlyGenericU3CxOps; - using NativeSynthesisPassTest::onlyGenericU3CzOps; - using NativeSynthesisPassTest::onlyIbmBasicCxOps; - using NativeSynthesisPassTest::onlyIbmBasicCzOps; - using NativeSynthesisPassTest::onlyIbmFractionalOps; -}; - -TEST_P(NativeSynthesisSwapProfileTest, DecomposesSwapToProfile) { - const NativeSynthMenuRow& param = GetParam(); - expectNativeAfterSynthesis( - [&] { - return mlir::qc::QCProgramBuilder::build(context.get(), mlir::qc::swap); - }, - param.nativeGates, param.isNative); -} - -INSTANTIATE_TEST_SUITE_P( - SwapMenuMatrix, NativeSynthesisSwapProfileTest, - testing::Values( - NativeSynthMenuRow{"IbmBasicCx", "x,sx,rz,cx", - &NativeSynthesisSwapProfileTest::onlyIbmBasicCxOps}, - NativeSynthMenuRow{"GenericU3Cx", "u,cx", - &NativeSynthesisSwapProfileTest::onlyGenericU3CxOps}, - NativeSynthMenuRow{"IbmBasicCz", "x,sx,rz,cz", - &NativeSynthesisSwapProfileTest::onlyIbmBasicCzOps}, - NativeSynthMenuRow{"GenericU3Cz", "u,cz", - &NativeSynthesisSwapProfileTest::onlyGenericU3CzOps}, - NativeSynthMenuRow{ - "IbmFractional", "x,sx,rz,rx,rzz,cz", - &NativeSynthesisSwapProfileTest::onlyIbmFractionalOps}, - NativeSynthMenuRow{ - "AxisPairRxRzCx", "rx,rz,cx", - &NativeSynthesisSwapProfileTest::onlyAxisPairRxRzCxOps}, - NativeSynthMenuRow{ - "AxisPairRyRzCz", "ry,rz,cz", - &NativeSynthesisSwapProfileTest::onlyAxisPairRyRzCzOps}), - [](const testing::TestParamInfo& info) { - return info.param.name; - }); - -// NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_P` at global scope -class NativeSynthesisHstycxMenuTest - : public NativeSynthesisPassTest, - public testing::WithParamInterface { -public: - using NativeSynthesisPassTest::onlyGenericU3CxOps; - using NativeSynthesisPassTest::onlyIbmBasicCxOps; -}; - -TEST_P(NativeSynthesisHstycxMenuTest, DecomposesHstycxTwoQToProfile) { - const NativeSynthMenuRow& param = GetParam(); - expectNativeAfterSynthesis( - [&] { - return mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::nativeSynthProfilesHstycxTwoQ); - }, - param.nativeGates, param.isNative); -} - -INSTANTIATE_TEST_SUITE_P( - HstycxTwoQMenuMatrix, NativeSynthesisHstycxMenuTest, - testing::Values( - NativeSynthMenuRow{"IbmBasicCx", "x,sx,rz,cx", - &NativeSynthesisHstycxMenuTest::onlyIbmBasicCxOps}, - NativeSynthMenuRow{"GenericU3Cx", "u,cx", - &NativeSynthesisHstycxMenuTest::onlyGenericU3CxOps}), - [](const testing::TestParamInfo& info) { - return info.param.name; - }); - -// NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_P` at global scope -class NativeSynthesisCxYOnQ1MenuTest - : public NativeSynthesisPassTest, - public testing::WithParamInterface { -public: - using NativeSynthesisPassTest::onlyAxisPairRyRzCzOps; - using NativeSynthesisPassTest::onlyIqmDefaultOps; -}; - -TEST_P(NativeSynthesisCxYOnQ1MenuTest, ConvertsCxToCzForProfile) { - const NativeSynthMenuRow& param = GetParam(); - expectNativeAfterSynthesis( - [&] { - return mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::nativeSynthProfilesCxYOnQ1); - }, - param.nativeGates, param.isNative); -} - -INSTANTIATE_TEST_SUITE_P( - CxYOnQ1MenuMatrix, NativeSynthesisCxYOnQ1MenuTest, - testing::Values( - NativeSynthMenuRow{ - "AxisPairRyRzCz", "ry,rz,cz", - &NativeSynthesisCxYOnQ1MenuTest::onlyAxisPairRyRzCzOps}, - NativeSynthMenuRow{"IqmDefault", "r,cz", - &NativeSynthesisCxYOnQ1MenuTest::onlyIqmDefaultOps}), - [](const testing::TestParamInfo& info) { - return info.param.name; - }); - -// NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_P` at global scope -class NativeSynthesisBroadOneQMenuTest - : public NativeSynthesisPassTest, - public testing::WithParamInterface { -public: - using NativeSynthesisPassTest::onlyAxisPairRyRzCzOps; - using NativeSynthesisPassTest::onlyGenericU3CzOps; - using NativeSynthesisPassTest::onlyIqmDefaultOps; -}; - -TEST_P(NativeSynthesisBroadOneQMenuTest, CanonicalizationNoLeakage) { - const NativeSynthMenuRow& param = GetParam(); - auto moduleOp = buildBroadOneQCanonicalizationCircuit(); - runNativeSynthesis(moduleOp, param.nativeGates); - EXPECT_TRUE(param.isNative(moduleOp)); -} - -INSTANTIATE_TEST_SUITE_P( - BroadOneQMenuMatrix, NativeSynthesisBroadOneQMenuTest, - testing::Values( - NativeSynthMenuRow{ - "IqmDefault", "r,cz", - &NativeSynthesisBroadOneQMenuTest::onlyIqmDefaultOps}, - NativeSynthMenuRow{ - "AxisPairRyRzCz", "ry,rz,cz", - &NativeSynthesisBroadOneQMenuTest::onlyAxisPairRyRzCzOps}, - NativeSynthMenuRow{ - "GenericU3Cz", "u,cz", - &NativeSynthesisBroadOneQMenuTest::onlyGenericU3CzOps}), - [](const testing::TestParamInfo& info) { - return info.param.name; - }); - -// NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_P` at global scope -class NativeSynthesisZeroAngleMenuTest - : public NativeSynthesisPassTest, - public testing::WithParamInterface { -public: - using NativeSynthesisPassTest::onlyAxisPairRyRzCzOps; - using NativeSynthesisPassTest::onlyIqmDefaultOps; -}; - -TEST_P(NativeSynthesisZeroAngleMenuTest, CanonicalizationNoLeakage) { - const NativeSynthMenuRow& param = GetParam(); - auto moduleOp = buildZeroAngleCanonicalizationCircuit(); - runNativeSynthesis(moduleOp, param.nativeGates); - EXPECT_TRUE(param.isNative(moduleOp)); -} - -INSTANTIATE_TEST_SUITE_P( - ZeroAngleMenuMatrix, NativeSynthesisZeroAngleMenuTest, - testing::Values( - NativeSynthMenuRow{ - "IqmDefault", "r,cz", - &NativeSynthesisZeroAngleMenuTest::onlyIqmDefaultOps}, - NativeSynthMenuRow{ - "AxisPairRyRzCz", "ry,rz,cz", - &NativeSynthesisZeroAngleMenuTest::onlyAxisPairRyRzCzOps}), - [](const testing::TestParamInfo& info) { - return info.param.name; - }); - -TEST_F(NativeSynthesisPassTest, DecomposesCxToCzForIbmBasicCzProfile) { - expectNativeAfterSynthesis( - [&] { - return mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::nativeSynthProfilesHCxTOnQ1); - }, - "x,sx,rz,cz", &NativeSynthesisPassTest::onlyIbmBasicCzOps); -} - -TEST_F(NativeSynthesisPassTest, DecomposesToIqmDefaultProfile) { - expectNativeAfterSynthesis( - [&] { - return mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::nativeSynthProfilesXYSXCz); - }, - "r,cz", &NativeSynthesisPassTest::onlyIqmDefaultOps); -} - -TEST_F(NativeSynthesisPassTest, DecomposesToIbmFractionalProfile) { - expectNativeAfterSynthesis( - [&] { - return mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::nativeSynthProfilesFractionalChain); - }, - "x,sx,rz,rx,rzz,cz", &NativeSynthesisPassTest::onlyIbmFractionalOps); -} - -TEST_F(NativeSynthesisPassTest, DecomposesToAxisPairRxRzCxProfile) { - expectNativeAfterSynthesis( - [&] { - return mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::nativeSynthProfilesHYcx); - }, - "rx,rz,cx", &NativeSynthesisPassTest::onlyAxisPairRxRzCxOps); -} - -TEST_F(NativeSynthesisPassTest, DecomposesRzToAxisPairRxRyCxProfile) { - expectNativeAfterSynthesis( - [&] { - return mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::nativeSynthProfilesZCx); - }, - "rx,ry,cx", &NativeSynthesisPassTest::onlyAxisPairRxRyCxOps); -} - -/// Single-control / single-target QC→QCO ``ctrl`` shells from -/// ``allSingleControlledGateFamiliesOneCtrlOneTarget`` must reach the generic -/// ``u,cx`` menu. -TEST_F(NativeSynthesisPassTest, - AllSingleControlledOneCtrlOneTargetFamiliesReachesU3Cx) { - expectNativeAfterSynthesis( - [&] { - return mlir::qc::QCProgramBuilder::build( - context.get(), - mlir::qc:: - nativeSynthAllSingleControlledGateFamiliesOneCtrlOneTarget); - }, - "u,cx", &NativeSynthesisPassTest::onlyGenericU3CxOps); -} - -TEST_F(NativeSynthesisPassTest, GenericProfileMatchesGenericU3CxBehavior) { - expectEquivalentAndNativeAfterSynthesis( - [&] { - return mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::nativeSynthProfilesHq0Yq1CxSq0); - }, - "u,cx", &NativeSynthesisPassTest::onlyGenericU3CxOps, - computeTwoQubitUnitaryFromModule); -} - -TEST_F(NativeSynthesisPassTest, GenericProfileMatchesAxisPairRyRzCzBehavior) { - expectEquivalentAndNativeAfterSynthesis( - [&] { - return mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::nativeSynthProfilesXHCz); - }, - "ry,rz,cz", &NativeSynthesisPassTest::onlyAxisPairRyRzCzOps, - computeTwoQubitUnitaryFromModule); -} - -TEST_F(NativeSynthesisPassTest, FailsForUnsupportedNativeGateMenu) { - expectSynthesisFailure( - [&] { - return mlir::qc::QCProgramBuilder::build(context.get(), mlir::qc::h); - }, - "not-a-gate"); -} - -TEST_F(NativeSynthesisPassTest, - CustomProfileAcceptsOverlappingOneQSupersetMenu) { - expectEquivalentAndNativeAfterSynthesis( - [&] { - return mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::nativeSynthProfilesHYSameWireCxSq1); - }, - "u,rx,rz,cx", &NativeSynthesisPassTest::onlyUOrAxisPairRxRzCxOps, - computeTwoQubitUnitaryFromModule); -} - -TEST_F(NativeSynthesisPassTest, CustomProfileMatchesIbmFractionalBehavior) { - expectEquivalentAndNativeAfterSynthesis( - [&] { return buildIbmFractionalAllGateFamiliesCircuit(); }, - "x,sx,rz,rx,cz,rzz", &NativeSynthesisPassTest::onlyIbmFractionalOps, - computeTwoQubitUnitaryFromModule); -} - -TEST_F(NativeSynthesisPassTest, CustomProfileAcceptsMultipleEntanglersMenu) { - expectEquivalentAndNativeAfterSynthesis( - [&] { - return mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::nativeSynthProfilesHCxSq1); - }, - "u,cx,cz", &NativeSynthesisPassTest::onlyGenericU3CxOrCzOps, - computeTwoQubitUnitaryFromModule); -} - -TEST_F(NativeSynthesisPassTest, - FailsForUnsupportedNativeGateMenuWithoutEmitter) { - expectSynthesisFailure( - [&] { - return mlir::qc::QCProgramBuilder::build(context.get(), mlir::qc::h); - }, - "rz,cx"); -} - -TEST_F(NativeSynthesisPassTest, MinimalIbmBasicCustomMenuAcceptsPhaseAlias) { - // `x,sx,rz,cx` is the minimal IBM-basic style menu. The synthesis pass may - // represent Z-axis phases using `p`, which should be accepted as an alias of - // `rz` for custom menus. - expectNativeAfterSynthesis( - [&] { - return mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::nativeSynthProfilesPhaseHCxPhase); - }, - "x,sx,rz,cx", &NativeSynthesisPassTest::onlyIbmBasicCxOps); -} - -TEST_F(NativeSynthesisPassTest, LargeMultiQubitCircuitStaysWithinMinimalMenu) { - // Stress-test: larger circuit (>2 qubits) with many 1Q/2Q ops that should - // still synthesize into the minimal IBM-basic custom menu. - expectNativeAfterSynthesis( - [&] { - return mlir::qc::QCProgramBuilder::build( - context.get(), - mlir::qc::nativeSynthProfilesLargeFiveQStressEightLayers); - }, - "x,sx,rz,cx", &NativeSynthesisPassTest::onlyIbmBasicCxOps); -} - -TEST_F(NativeSynthesisPassTest, FailsForNativeGateMenuWithoutSingleQEmitter) { - expectSynthesisFailure( - [&] { - return mlir::qc::QCProgramBuilder::build(context.get(), - mlir::qc::singleControlledX); - }, - "cx,cz"); -} - -TEST_F(NativeSynthesisPassTest, CandidateSelectionIsDeterministicAcrossRuns) { - auto buildFn = [&] { - return mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::nativeSynthDeterminismTwoQubitSwap); - }; - - auto firstModule = buildFn(); - runNativeSynthesis(firstModule, "u,cx"); - auto secondModule = buildFn(); - runNativeSynthesis(secondModule, "u,cx"); - - EXPECT_EQ(moduleToString(firstModule), moduleToString(secondModule)); -} - -TEST_F(NativeSynthesisPassTest, - RichCustomMenuSelectionRemainsDeterministicAcrossRuns) { - auto buildFn = [&] { - return mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::nativeSynthDeterminismTwoQubitSwap); - }; - - auto firstModule = buildFn(); - runNativeSynthesis(firstModule, "u,rx,rz,cx,cz"); - auto secondModule = buildFn(); - runNativeSynthesis(secondModule, "u,rx,rz,cx,cz"); - EXPECT_EQ(moduleToString(firstModule), moduleToString(secondModule)); - EXPECT_TRUE(onlyUOrAxisPairRxRzCxOps(firstModule) || - onlyGenericU3CxOrCzOps(firstModule)); -} - -TEST_F(NativeSynthesisPassTest, FailsForMultiControlledGateStructure) { - expectSynthesisFailure( - [&] { - return mlir::qc::QCProgramBuilder::build(context.get(), - mlir::qc::multipleControlledX); - }, - "x,sx,rz,cx"); -} - -TEST_F(NativeSynthesisPassTest, FailsForControlledTwoTargetGateStructure) { - expectSynthesisFailure( - [&] { - return mlir::qc::QCProgramBuilder::build( - context.get(), mlir::qc::singleControlledSwap); - }, - "x,sx,rz,cx"); -} - -TEST_F(NativeSynthesisPassTest, - RandomizedEquivalentAcrossProfilesWithFixedSeed) { - auto buildStressCircuit = [&](MLIRContext* ctx, const char* nativeGates) { - mlir::qc::QCProgramBuilder builder(ctx); - builder.initialize(); - const auto q0 = builder.allocQubit(); - const auto q1 = builder.allocQubit(); - const std::string menu(nativeGates); - if (menu == "r,cz") { - builder.r(0.37, -0.42, q0); - builder.cz(q0, q1); - builder.r(-0.11, 0.21, q1); - } else if (menu == "ry,rz,cz") { - builder.ry(0.37, q0); - builder.rz(-0.42, q1); - builder.cz(q0, q1); - builder.rz(0.21, q0); - } else if (menu == "rx,ry,cx") { - builder.rx(0.37, q0); - builder.ry(-0.42, q1); - builder.cx(q0, q1); - builder.ry(0.21, q0); - } else if (menu == "rx,rz,cx") { - builder.rx(0.37, q0); - builder.rz(-0.42, q1); - builder.cx(q0, q1); - builder.rz(0.21, q0); - } else { - builder.h(q0); - builder.y(q1); - builder.cx(q0, q1); - builder.s(q0); - builder.cx(q1, q0); - } - - builder.dealloc(q0); - builder.dealloc(q1); - return builder.finalize(); - }; - - const auto profiles = NativeSynthesisPassTest::allNineEquivalenceProfiles(); - - for (const auto& profileCase : profiles) { - auto synthesizedModule = - buildStressCircuit(context.get(), profileCase.nativeGates); - PassManager prePm(synthesizedModule->getContext()); - prePm.addPass(createQCToQCO()); - ASSERT_TRUE(succeeded(prePm.run(*synthesizedModule))); - const auto expectedUnitary = - computeTwoQubitUnitaryFromModule(synthesizedModule); - ASSERT_TRUE(expectedUnitary.has_value()); - - PassManager synthPm(synthesizedModule->getContext()); - synthPm.addPass( - qco::createNativeGateSynthesisPass(qco::NativeGateSynthesisOptions{ - .nativeGates = profileCase.nativeGates, - })); - ASSERT_TRUE(succeeded(synthPm.run(*synthesizedModule))) - << "native-gates=" << profileCase.nativeGates; - EXPECT_TRUE(profileCase.isNative(synthesizedModule)) - << "native-gates=" << profileCase.nativeGates; - - const auto synthesizedUnitary = - computeTwoQubitUnitaryFromModule(synthesizedModule); - ASSERT_TRUE(synthesizedUnitary.has_value()); - EXPECT_TRUE( - isEquivalentUpToGlobalPhase(*expectedUnitary, *synthesizedUnitary)) - << "native-gates=" << profileCase.nativeGates; - } -} diff --git a/mlir/unittests/programs/qc_programs.cpp b/mlir/unittests/programs/qc_programs.cpp index e91e734cc0..e3d0e81b65 100644 --- a/mlir/unittests/programs/qc_programs.cpp +++ b/mlir/unittests/programs/qc_programs.cpp @@ -1617,539 +1617,4 @@ void nestedForLoopCtrlOpWithExtractedQubit(QCProgramBuilder& b) { b.cx(reg[0], q0); }); } - -static void emitNativeSynthControlledPhase(QCProgramBuilder& b, - const double theta, mlir::Value ctrl, - mlir::Value tgt) { - b.p(theta / 2.0, ctrl); - b.cx(ctrl, tgt); - b.p(-theta / 2.0, tgt); - b.cx(ctrl, tgt); - b.p(theta / 2.0, tgt); -} - -static void emitNativeSynthToffoli(QCProgramBuilder& b, mlir::Value c1, - mlir::Value c2, mlir::Value t) { - b.h(t); - b.cx(c2, t); - b.tdg(t); - b.cx(c1, t); - b.t(t); - b.cx(c2, t); - b.tdg(t); - b.cx(c1, t); - b.t(c2); - b.t(t); - b.h(t); - b.cx(c1, c2); - b.t(c1); - b.tdg(c2); - b.cx(c1, c2); -} - -/// Shared by ``nativeSynthBroadOneQCanonicalization`` and -/// ``nativeSynthIbmFractionalAllGateFamilies``: wide 1q sweep on two qubits, -/// ending before any two-qubit primitive. -static void emitNativeSynthFixtureBroad1qPrefix(QCProgramBuilder& b, - mlir::Value q0, - mlir::Value q1) { - b.id(q0); - b.x(q0); - b.y(q1); - b.z(q0); - b.h(q1); - b.s(q0); - b.sdg(q1); - b.t(q0); - b.tdg(q1); - b.sx(q0); - b.sxdg(q1); - b.rx(0.13, q0); - b.ry(-0.47, q1); - b.rz(0.29, q0); - b.p(-0.38, q1); - b.r(0.61, -0.22, q0); -} - -static void emitNativeSynthFiveQStressLayers(QCProgramBuilder& b, - const int numLayers) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - const auto q2 = b.allocQubit(); - const auto q3 = b.allocQubit(); - const auto q4 = b.allocQubit(); - b.h(q0); - b.s(q1); - b.t(q2); - b.y(q3); - b.h(q4); - b.cx(q0, q1); - b.cz(q1, q2); - b.swap(q2, q3); - b.cx(q3, q4); - for (int layer = 0; layer < numLayers; ++layer) { - b.h(q0); - b.s(q0); - b.t(q0); - b.y(q1); - b.h(q2); - b.s(q3); - b.t(q4); - b.cx(q0, q2); - b.cz(q1, q3); - b.cx(q2, q4); - if ((layer % 2) == 0) { - b.swap(q0, q1); - b.swap(q3, q4); - } else { - b.cx(q4, q0); - b.cz(q2, q1); - } - } - b.p(0.25, q0); - b.p(-0.5, q2); - b.p(0.75, q4); -} - -static void emitNativeSynthTwoQRzx(QCProgramBuilder& b, const double theta, - const bool controlOnFirstWire) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - if (controlOnFirstWire) { - b.rzx(theta, q0, q1); - } else { - b.rzx(theta, q1, q0); - } -} - -void nativeSynthBroadOneQCanonicalization(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - emitNativeSynthFixtureBroad1qPrefix(b, q0, q1); - b.cz(q0, q1); -} - -void nativeSynthZeroAngleCanonicalization(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.rx(0.0, q0); - b.ry(0.0, q1); - b.rz(0.0, q0); - b.p(0.0, q1); - b.r(0.0, 0.0, q0); - b.cz(q0, q1); -} - -void nativeSynthIbmFractionalAllGateFamilies(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - emitNativeSynthFixtureBroad1qPrefix(b, q0, q1); - b.cx(q0, q1); - b.cz(q1, q0); - b.swap(q0, q1); - b.iswap(q0, q1); - b.dcx(q0, q1); - b.ecr(q0, q1); - b.rxx(0.17, q0, q1); - b.ryy(-0.21, q0, q1); - b.rzx(0.41, q0, q1); - b.rzz(-0.33, q0, q1); - b.xx_plus_yy(0.52, -0.14, q0, q1); - b.xx_minus_yy(-0.37, 0.26, q0, q1); -} - -void nativeSynthFusionHadamardZHadamard(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - b.h(q0); - b.z(q0); - b.h(q0); -} - -void nativeSynthFusionHadamardHadamard(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - b.h(q0); - b.h(q0); -} - -void nativeSynthFusionMixedChainHSTYSX(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - b.h(q0); - b.s(q0); - b.t(q0); - b.y(q0); - b.sx(q0); -} - -void nativeSynthFusionHadamardCxHadamard(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.h(q0); - b.cx(q0, q1); - b.h(q0); -} - -void nativeSynthFusionHadamardBarrierHadamard(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - b.h(q0); - b.barrier({q0}); - b.h(q0); -} - -void nativeSynthFusionRzSxRz(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - b.rz(0.4, q0); - b.sx(q0); - b.rz(-0.9, q0); -} - -void nativeSynthFusionUUTwoQGenericU3(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - b.u(0.3, 0.1, -0.2, q0); - b.u(-0.5, 0.7, 0.4, q0); -} - -void nativeSynthFusionTS(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - b.t(q0); - b.s(q0); -} - -void nativeSynthFusionUUTwoQDet1(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - b.u(0.3, 0.2, -0.2, q0); - b.u(0.5, 0.4, -0.4, q0); -} - -void nativeSynthFusionLongMixedTenOpCx(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.h(q0); - b.t(q0); - b.rx(0.37, q0); - b.s(q0); - b.ry(-0.21, q0); - b.h(q0); - b.z(q0); - b.rz(0.52, q0); - b.sx(q0); - b.y(q0); - b.cx(q0, q1); -} - -void nativeSynthFusionCxCx(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.cx(q0, q1); - b.cx(q0, q1); -} - -void nativeSynthFusionHCxInterleavedTCx(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.h(q0); - b.cx(q0, q1); - b.t(q1); - b.s(q0); - b.cx(q0, q1); -} - -void nativeSynthFusionThreeLineCx01Cx12Cx01(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - const auto q2 = b.allocQubit(); - b.cx(q0, q1); - b.cx(q1, q2); - b.cx(q0, q1); -} - -void nativeSynthFusionCxBarrierCx(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.cx(q0, q1); - b.barrier({q0, q1}); - b.cx(q0, q1); -} - -void nativeSynthFusionSwapCxPattern(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.cx(q0, q1); - b.cx(q1, q0); - b.cx(q0, q1); -} - -void nativeSynthFusionHDcxSCx(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.h(q0); - b.dcx(q0, q1); - b.s(q1); - b.cx(q0, q1); -} - -void nativeSynthFusionXRzxTCx(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.x(q0); - b.rzx(0.41, q0, q1); - b.t(q1); - b.cx(q0, q1); -} - -void nativeSynthFusionHRzzSRzz(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.h(q0); - b.rzz(-0.29, q0, q1); - b.s(q1); - b.rzz(0.17, q0, q1); -} - -void nativeSynthFusionRzx041Q0First(QCProgramBuilder& b) { - emitNativeSynthTwoQRzx(b, 0.41, /*controlOnFirstWire=*/true); -} - -void nativeSynthFusionRzx041Q1First(QCProgramBuilder& b) { - emitNativeSynthTwoQRzx(b, 0.41, /*controlOnFirstWire=*/false); -} - -void nativeSynthFusionRzxPiHalfQ0First(QCProgramBuilder& b) { - emitNativeSynthTwoQRzx(b, std::numbers::pi / 2.0, - /*controlOnFirstWire=*/true); -} - -void nativeSynthFusionRzxPiHalfQ1First(QCProgramBuilder& b) { - emitNativeSynthTwoQRzx(b, std::numbers::pi / 2.0, - /*controlOnFirstWire=*/false); -} - -void nativeSynthProfilesHstycxTwoQ(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.h(q0); - b.s(q0); - b.t(q0); - b.y(q0); - b.cx(q0, q1); -} - -void nativeSynthProfilesHCxTOnQ1(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.h(q1); - b.cx(q0, q1); - b.t(q1); -} - -void nativeSynthProfilesXYSXCz(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.x(q0); - b.y(q0); - b.sx(q0); - b.cz(q0, q1); -} - -void nativeSynthProfilesFractionalChain(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.h(q0); - b.ry(0.37, q0); - b.sxdg(q0); - b.cx(q0, q1); - b.rzz(0.23, q0, q1); -} - -void nativeSynthProfilesHYcx(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.h(q0); - b.y(q0); - b.cx(q0, q1); -} - -void nativeSynthProfilesZCx(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.z(q0); - b.cx(q0, q1); -} - -void nativeSynthProfilesXHCz(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.x(q0); - b.h(q0); - b.cz(q0, q1); -} - -void nativeSynthProfilesCxYOnQ1(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.cx(q0, q1); - b.y(q1); -} - -void nativeSynthProfilesHq0Yq1CxSq0(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.h(q0); - b.y(q1); - b.cx(q0, q1); - b.s(q0); -} - -void nativeSynthProfilesHCxSq1(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.h(q0); - b.cx(q0, q1); - b.s(q1); -} - -void nativeSynthProfilesHYSameWireCxSq1(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.h(q0); - b.y(q0); - b.cx(q0, q1); - b.s(q1); -} - -void nativeSynthProfilesPhaseHCxPhase(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.p(0.13, q0); - b.h(q0); - b.cx(q0, q1); - b.p(-0.27, q1); -} - -void nativeSynthProfilesLargeFiveQStressEightLayers(QCProgramBuilder& b) { - emitNativeSynthFiveQStressLayers(b, /*numLayers=*/8); -} - -void nativeSynthMultiQThreeQGhz(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - const auto q2 = b.allocQubit(); - b.h(q0); - b.cx(q0, q1); - b.cx(q1, q2); -} - -void nativeSynthMultiQThreeQToffoli(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - const auto q2 = b.allocQubit(); - emitNativeSynthToffoli(b, q0, q1, q2); -} - -void nativeSynthMultiQThreeQQft(QCProgramBuilder& b) { - using std::numbers::pi; - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - const auto q2 = b.allocQubit(); - b.h(q2); - emitNativeSynthControlledPhase(b, pi / 2.0, q1, q2); - b.h(q1); - emitNativeSynthControlledPhase(b, pi / 4.0, q0, q2); - emitNativeSynthControlledPhase(b, pi / 2.0, q0, q1); - b.h(q0); - b.cx(q0, q2); - b.cx(q2, q0); - b.cx(q0, q2); -} - -void nativeSynthMultiQThreeQCliffordTMix(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - const auto q2 = b.allocQubit(); - b.h(q0); - b.t(q1); - b.x(q2); - b.cx(q0, q1); - b.rz(0.37, q2); - b.cz(q1, q2); - b.sdg(q0); - b.ry(-0.42, q1); - b.cx(q2, q0); - b.y(q1); - b.tdg(q2); - b.cx(q0, q1); - b.p(0.21, q2); - b.h(q2); - b.cz(q0, q2); - b.rx(-0.13, q1); - b.s(q0); -} - -void nativeSynthMultiQFiveQStressFourLayers(QCProgramBuilder& b) { - emitNativeSynthFiveQStressLayers(b, /*numLayers=*/4); -} - -void nativeSynthCustomMenusIbmFractionalTwoQStress(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.h(q0); - b.sxdg(q0); - b.ry(-0.22, q1); - b.swap(q0, q1); - b.rxx(0.53, q0, q1); - b.ecr(q0, q1); - b.p(0.31, q0); - b.rzz(-0.44, q0, q1); -} - -void nativeSynthCustomMenusXxPlusYyChain(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.h(q0); - b.sx(q1); - b.xx_plus_yy(0.52, -0.14, q0, q1); - b.rz(0.31, q0); -} - -void nativeSynthCustomMenusXxMinusYyOnly(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.xx_minus_yy(-0.37, 0.26, q0, q1); -} - -void nativeSynthDeterminismTwoQubitSwap(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.swap(q0, q1); - b.dealloc(q0); - b.dealloc(q1); -} - -void nativeSynthAllSingleControlledGateFamiliesOneCtrlOneTarget( - QCProgramBuilder& b) { - auto q = b.allocQubitRegister(2); - const mlir::Value c = q[0]; - const mlir::Value t = q[1]; - - b.cgphase(0.07, c); - - b.cid(c, t); - b.cx(c, t); - b.cy(c, t); - b.cz(c, t); - b.ch(c, t); - b.cs(c, t); - b.csdg(c, t); - b.ct(c, t); - b.ctdg(c, t); - b.csx(c, t); - b.csxdg(c, t); - - b.crx(0.11, c, t); - b.cry(0.12, c, t); - b.crz(0.13, c, t); - b.cp(0.14, c, t); - b.cr(0.15, 0.16, c, t); - b.cu2(0.17, 0.18, c, t); - b.cu(0.19, 0.2, 0.21, c, t); -} } // namespace mlir::qc diff --git a/mlir/unittests/programs/qc_programs.h b/mlir/unittests/programs/qc_programs.h index 793c829d0d..25d76718cc 100644 --- a/mlir/unittests/programs/qc_programs.h +++ b/mlir/unittests/programs/qc_programs.h @@ -912,172 +912,4 @@ void nestedForLoopCtrlOpWithSeparateQubit(QCProgramBuilder& b); /// nested ctrl operation where the qubit is extracted from the register. void nestedForLoopCtrlOpWithExtractedQubit(QCProgramBuilder& b); -// --- Native gate synthesis (mlir/unittests/.../NativeSynthesis) ----------- // - -/// Wide single-qubit sweep on two qubits, then ``cz``; exercises broad 1q -/// canonicalization without entangler-specific shortcuts. -void nativeSynthBroadOneQCanonicalization(QCProgramBuilder& b); - -/// Degenerate rotations (all zero angles) on two qubits then ``cz``; checks -/// that trivial phases collapse cleanly on phase-sensitive menus. -void nativeSynthZeroAngleCanonicalization(QCProgramBuilder& b); - -/// Same 1q prefix as ``nativeSynthBroadOneQCanonicalization`` followed by a -/// representative mix of two-qubit primitives (CX, CZ, SWAP, iSWAP, DCX, ECR, -/// Pauli rotations, RZZ, XX±YY); used for IBM-fractional-style coverage. -void nativeSynthIbmFractionalAllGateFamilies(QCProgramBuilder& b); - -/// Single wire: ``H Z H`` (fusion should collapse toward ``X`` on IBM-style -/// menus). -void nativeSynthFusionHadamardZHadamard(QCProgramBuilder& b); - -/// Single wire: adjacent ``H H`` (identity run; fusion should remove 1q ops). -void nativeSynthFusionHadamardHadamard(QCProgramBuilder& b); - -/// Single wire: ``H S T Y SX`` mixed non-native chain for generic-U3 fusion. -void nativeSynthFusionMixedChainHSTYSX(QCProgramBuilder& b); - -/// Two qubits: ``H`` on control, ``CX``, ``H`` on control; 1q runs must not -/// fuse across the entangler. -void nativeSynthFusionHadamardCxHadamard(QCProgramBuilder& b); - -/// ``H``, barrier, ``H`` on one wire; barrier must break 1q-run merging. -void nativeSynthFusionHadamardBarrierHadamard(QCProgramBuilder& b); - -/// Fully native IBM-style ``rz; sx; rz`` triple on one wire (cost-gate / -/// skip-fully-native path). -void nativeSynthFusionRzSxRz(QCProgramBuilder& b); - -/// Two adjacent native ``U`` on one wire (generic ``u,cx`` profile; cost-gate -/// fuses to one ``U``). -void nativeSynthFusionUUTwoQGenericU3(QCProgramBuilder& b); - -/// ``T S`` on one wire; fused SU(2) normalisation emits a non-trivial -/// ``qco.gphase`` on the generic-U3 path. -void nativeSynthFusionTS(QCProgramBuilder& b); - -/// Two ``U`` with ``lambda = -phi`` each (det=1); fused result must omit -/// ``gphase`` (trivial residual phase). -void nativeSynthFusionUUTwoQDet1(QCProgramBuilder& b); - -/// Long mixed 1q chain on ``q0`` then ``CX(q0,q1)``; profile-sweep equivalence -/// for CX-friendly menus. -void nativeSynthFusionLongMixedTenOpCx(QCProgramBuilder& b); - -/// Two identical ``CX`` on the same pair (block consolidation / cancellation). -void nativeSynthFusionCxCx(QCProgramBuilder& b); - -/// ``H``, ``CX``, interleaved 1q on both wires, ``CX``; consolidation to one -/// 4×4 block on ``u,cx``. -void nativeSynthFusionHCxInterleavedTCx(QCProgramBuilder& b); - -/// Three ``CX`` on lines ``0-1``, ``1-2``, ``0-1``; consolidation must not -/// merge across the middle pair. -void nativeSynthFusionThreeLineCx01Cx12Cx01(QCProgramBuilder& b); - -/// Two ``CX`` separated by a barrier on the pair; consolidation must not fuse -/// across the barrier. -void nativeSynthFusionCxBarrierCx(QCProgramBuilder& b); - -/// Alternating-direction ``CX`` triple (SWAP pattern) on two qubits. -void nativeSynthFusionSwapCxPattern(QCProgramBuilder& b); - -/// ``H``, ``DCX``, ``S`` on target, ``CX``; asymmetric DCX inside a block. -void nativeSynthFusionHDcxSCx(QCProgramBuilder& b); - -/// ``X``, ``RZX``, ``T`` on target, ``CX``; directional RZX inside a block. -void nativeSynthFusionXRzxTCx(QCProgramBuilder& b); - -/// ``H``, two ``RZZ`` with ``S`` on target; IBM-fractional RZZ consolidation. -void nativeSynthFusionHRzzSRzz(QCProgramBuilder& b); - -/// Standalone ``RZX(0.41)`` with control on the first allocated qubit. -void nativeSynthFusionRzx041Q0First(QCProgramBuilder& b); - -/// Standalone ``RZX(0.41)`` with control on the second allocated qubit. -void nativeSynthFusionRzx041Q1First(QCProgramBuilder& b); - -/// Standalone ``RZX(pi/2)`` with control on the first allocated qubit. -void nativeSynthFusionRzxPiHalfQ0First(QCProgramBuilder& b); - -/// Standalone ``RZX(pi/2)`` with control on the second allocated qubit. -void nativeSynthFusionRzxPiHalfQ1First(QCProgramBuilder& b); - -/// Two qubits: ``H,S,T,Y`` on ``q0`` then ``CX(q0,q1)``; profile decomposition -/// (HSTY + CX) smoke shape. -void nativeSynthProfilesHstycxTwoQ(QCProgramBuilder& b); - -/// ``H`` on target, ``CX``, ``T`` on target; CX→CZ style menus on IBM-basic -/// CZ. -void nativeSynthProfilesHCxTOnQ1(QCProgramBuilder& b); - -/// ``X``, ``Y``, ``SX`` on control, ``CZ``; IQM-style ``r,cz`` profile fixture. -void nativeSynthProfilesXYSXCz(QCProgramBuilder& b); - -/// ``H``, ``RY``, ``SXdg``, ``CX``, ``RZZ``; IBM-fractional chain profile. -void nativeSynthProfilesFractionalChain(QCProgramBuilder& b); - -/// ``H``, ``Y``, ``CX``; axis-pair ``rx,rz,cx`` profile fixture. -void nativeSynthProfilesHYcx(QCProgramBuilder& b); - -/// ``Z``, ``CX``; axis-pair ``rx,ry,cx`` (Rz decomposition) fixture. -void nativeSynthProfilesZCx(QCProgramBuilder& b); - -/// ``X``, ``H``, ``CZ``; axis-pair ``ry,rz,cz`` / generic overlap checks. -void nativeSynthProfilesXHCz(QCProgramBuilder& b); - -/// ``CX`` then ``Y`` on target; Cx→Cz / ``r,cz`` conversion on a fixed pair. -void nativeSynthProfilesCxYOnQ1(QCProgramBuilder& b); - -/// ``H(q0)``, ``Y(q1)``, ``CX``, ``S(q0)``; generic ``u,cx`` equivalence menu. -void nativeSynthProfilesHq0Yq1CxSq0(QCProgramBuilder& b); - -/// ``H``, ``CX``, ``S`` on target; custom menu with multiple entanglers -/// (``u,cx,cz``). -void nativeSynthProfilesHCxSq1(QCProgramBuilder& b); - -/// ``H``, ``Y`` on same wire as control, ``CX``, ``S`` on target; overlapping -/// one-qubit superset custom menu. -void nativeSynthProfilesHYSameWireCxSq1(QCProgramBuilder& b); - -/// Phase before/after ``H`` and ``CX``; minimal IBM-basic menu with ``p`` as -/// phase alias. -void nativeSynthProfilesPhaseHCxPhase(QCProgramBuilder& b); - -/// Five-qubit stress circuit with eight repeated layers; large multi-qubit -/// minimal-menu synthesis. -void nativeSynthProfilesLargeFiveQStressEightLayers(QCProgramBuilder& b); - -/// Three-qubit GHZ preparation (``H``, ``CX`` chain). -void nativeSynthMultiQThreeQGhz(QCProgramBuilder& b); - -/// Three-qubit Toffoli (decomposed) for multi-profile equivalence. -void nativeSynthMultiQThreeQToffoli(QCProgramBuilder& b); - -/// Three-qubit QFT-style controlled phases and permutations. -void nativeSynthMultiQThreeQQft(QCProgramBuilder& b); - -/// Three-qubit Clifford+``T``+rotations mix; moderate-depth multi-qubit sweep. -void nativeSynthMultiQThreeQCliffordTMix(QCProgramBuilder& b); - -/// Same five-qubit layer template as the eight-layer profile, but four layers. -void nativeSynthMultiQFiveQStressFourLayers(QCProgramBuilder& b); - -/// Two-qubit stress: IBM-fractional primitives (SWAP, RXX, ECR, RZZ, …). -void nativeSynthCustomMenusIbmFractionalTwoQStress(QCProgramBuilder& b); - -/// ``H``, ``SX``, ``XX+YY``, ``RZ``; custom-menu ``XX+YY`` chain behaviour. -void nativeSynthCustomMenusXxPlusYyChain(QCProgramBuilder& b); - -/// Single ``XX-YY`` on a pair; custom-menu delegate shape. -void nativeSynthCustomMenusXxMinusYyOnly(QCProgramBuilder& b); - -/// Two-qubit ``swap`` with explicit ``allocQubit`` / ``dealloc`` ordering. -void nativeSynthDeterminismTwoQubitSwap(QCProgramBuilder& b); - -/// Single-control ops whose QCO lowering uses only 1-control / 1-target -/// ``ctrl`` shells (native gate synthesis supports these today). -void nativeSynthAllSingleControlledGateFamiliesOneCtrlOneTarget( - QCProgramBuilder& b); - } // namespace mlir::qc From 742632d0969e0810651dd4718cc6594df8686f0f Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Fri, 19 Jun 2026 12:00:13 +0200 Subject: [PATCH 048/122] =?UTF-8?q?=F0=9F=94=A5=20Remove=20and=20merge=20f?= =?UTF-8?q?iles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Decomposition/BasisDecomposer.h | 231 --- .../QCO/Transforms/Decomposition/Helpers.h | 43 - .../Decomposition/UnitaryMatrices.h | 74 - .../QCO/Transforms/Decomposition/Weyl.h | 506 ++++++ .../Decomposition/WeylDecomposition.h | 239 --- .../NativeSynthesis/FuseTwoQubitUnitaryRuns.h | 30 - .../Transforms/NativeSynthesis/NativeSpec.h | 40 - .../QCO/Transforms/NativeSynthesis/Policy.h | 40 - .../Transforms/NativeSynthesis/SingleQubit.h | 61 - .../QCO/Transforms/NativeSynthesis/TwoQubit.h | 52 - .../QCO/Transforms/NativeSynthesis/Types.h | 80 - .../QCO/Transforms/NativeSynthesis/Utils.h | 44 - .../mlir/Dialect/QCO/Transforms/Passes.h | 11 - .../mlir/Dialect/QCO/Transforms/Passes.td | 117 +- mlir/lib/Compiler/CompilerPipeline.cpp | 4 +- .../lib/Dialect/QCO/Transforms/CMakeLists.txt | 2 +- .../Decomposition/BasisDecomposer.cpp | 347 ---- .../QCO/Transforms/Decomposition/Helpers.cpp | 47 - .../Decomposition/UnitaryMatrices.cpp | 171 -- .../QCO/Transforms/Decomposition/Weyl.cpp | 1479 +++++++++++++++++ .../Decomposition/WeylDecomposition.cpp | 710 -------- .../FuseTwoQubitUnitaryRuns.cpp | 1078 +++++++++++- .../Transforms/NativeSynthesis/NativeSpec.cpp | 246 --- .../QCO/Transforms/NativeSynthesis/Pass.cpp | 598 ------- .../QCO/Transforms/NativeSynthesis/Policy.cpp | 108 -- .../NativeSynthesis/SingleQubit.cpp | 255 --- .../Transforms/NativeSynthesis/TwoQubit.cpp | 230 --- .../QCO/Transforms/NativeSynthesis/Utils.cpp | 108 -- mlir/tools/mqt-cc/mqt-cc.cpp | 4 +- .../Compiler/test_compiler_pipeline.cpp | 24 +- .../Decomposition/decomposition_test_utils.h | 91 - .../test_euler_decomposition.cpp | 120 +- .../Decomposition/test_weyl_decomposition.cpp | 169 +- .../NativeSynthesis/test_native_synthesis.cpp | 94 +- mlir/unittests/TestCaseUtils.h | 59 + 35 files changed, 3401 insertions(+), 4111 deletions(-) delete mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.h delete mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h delete mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h create mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h delete mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.h delete mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.h delete mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h delete mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h delete mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h delete mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h delete mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h delete mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h delete mode 100644 mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp delete mode 100644 mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp delete mode 100644 mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp create mode 100644 mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp delete mode 100644 mlir/lib/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.cpp delete mode 100644 mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.cpp delete mode 100644 mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp delete mode 100644 mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Policy.cpp delete mode 100644 mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp delete mode 100644 mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp delete mode 100644 mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp delete mode 100644 mlir/unittests/Dialect/QCO/Transforms/Decomposition/decomposition_test_utils.h diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.h deleted file mode 100644 index a2cc1eb753..0000000000 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.h +++ /dev/null @@ -1,231 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#pragma once - -#include "UnitaryMatrices.h" -#include "WeylDecomposition.h" -#include "mlir/Dialect/QCO/Utils/Matrix.h" - -#include - -#include -#include -#include -#include -#include - -namespace mlir::qco::decomposition { - -/// Intermediate single-qubit ``2×2`` unitaries produced while expanding a -/// two-qubit basis decomposition. -using TwoQubitLocalUnitaryList = llvm::SmallVector; - -/** - * Result of a two-qubit basis decomposition expressed as raw single-qubit - * factors interleaved with a fixed number of basis-gate (entangler) uses. - * - * The factors are stored in emission order. For `i` in `[0, numBasisUses)` the - * pair `(singleQubitFactors[2*i], singleQubitFactors[2*i + 1])` is applied to - * qubits `1` and `0` respectively, followed by one entangler. The final pair - * `(singleQubitFactors[2*numBasisUses], singleQubitFactors[2*numBasisUses+1])` - * is applied after the last entangler. The list therefore has length - * `2 * (numBasisUses + 1)`. - */ -struct TwoQubitNativeDecomposition { - /// Number of basis-gate (entangler) uses. - std::uint8_t numBasisUses = 0; - /// Single-qubit factors in emission order (see struct comment). - TwoQubitLocalUnitaryList singleQubitFactors; - /// Residual global phase (radians) not represented by factors/entanglers. - double globalPhase = 0.0; -}; - -/** - * 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 - * https://www.apache.org/licenses/LICENSE-2.0. - * - * Any modifications or derivative works of this code must retain this - * copyright notice, and modified files need to carry a notice - * indicating that they have been altered from the originals. - */ -class TwoQubitBasisDecomposer { -public: - /** - * Create decomposer that allows two-qubit decompositions based on the - * specified entangler matrix. - * This entangler will appear between 0 and 3 times in each decomposition. - * The 4x4 matrix must be in MQT operand order (qubit 0 = MSB). - */ - [[nodiscard]] static TwoQubitBasisDecomposer - create(const Matrix4x4& basisMatrix, double basisFidelity); - - /** - * Perform decomposition using the basis gate of this decomposer. - * - * @param targetDecomposition Prepared Weyl decomposition of unitary matrix - * to be decomposed. - * @param numBasisGateUses Force use of given number of basis gates. When - * unset, the optimal count is selected from the - * Hilbert-Schmidt traces. - * @return The single-qubit factors and entangler count, or `std::nullopt` - * when more than one basis gate would be required but the basis gate - * is not super-controlled. - */ - [[nodiscard]] std::optional twoQubitDecompose( - const decomposition::TwoQubitWeylDecomposition& targetDecomposition, - std::optional numBasisGateUses) const; - -protected: - // NOLINTBEGIN(modernize-pass-by-value) - /** - * Constructs decomposer instance. - */ - TwoQubitBasisDecomposer( - double basisFidelity, - const decomposition::TwoQubitWeylDecomposition& basisDecomposer, - bool isSuperControlled, const Matrix2x2& u0l, const Matrix2x2& u0r, - const Matrix2x2& u1l, const Matrix2x2& u1ra, const Matrix2x2& u1rb, - const Matrix2x2& u2la, const Matrix2x2& u2lb, const Matrix2x2& u2ra, - const Matrix2x2& u2rb, const Matrix2x2& u3l, const Matrix2x2& u3r, - const Matrix2x2& q0l, const Matrix2x2& q0r, const Matrix2x2& q1la, - const Matrix2x2& q1lb, const Matrix2x2& q1ra, const Matrix2x2& q1rb, - const Matrix2x2& q2l, const Matrix2x2& q2r) - : basisFidelity{basisFidelity}, basisDecomposer{basisDecomposer}, - isSuperControlled{isSuperControlled}, u0l{u0l}, u0r{u0r}, u1l{u1l}, - u1ra{u1ra}, u1rb{u1rb}, u2la{u2la}, u2lb{u2lb}, u2ra{u2ra}, u2rb{u2rb}, - u3l{u3l}, u3r{u3r}, q0l{q0l}, q0r{q0r}, q1la{q1la}, q1lb{q1lb}, - q1ra{q1ra}, q1rb{q1rb}, q2l{q2l}, q2r{q2r} {} - // NOLINTEND(modernize-pass-by-value) - - /** - * Calculate decompositions when no basis gate is required. - * - * Decompose target :math:`\sim U_d(x, y, z)` with 0 uses of the - * basis gate. Result :math:`U_r` has trace: - * - * .. math:: - * - * \Big\vert\text{Tr}(U_r\cdot U_\text{target}^{\dag})\Big\vert = - * 4\Big\vert (\cos(x)\cos(y)\cos(z)+ j \sin(x)\sin(y)\sin(z)\Big\vert - * - * which is optimal for all targets and bases - */ - [[nodiscard]] static TwoQubitLocalUnitaryList - 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``. - */ - [[nodiscard]] TwoQubitLocalUnitaryList - 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. - */ - [[nodiscard]] TwoQubitLocalUnitaryList 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. - */ - [[nodiscard]] TwoQubitLocalUnitaryList 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; - - [[nodiscard]] static bool relativeEq(double lhs, double rhs, double epsilon, - double maxRelative); - -private: - // 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 - Matrix2x2 u0l; - Matrix2x2 u0r; - Matrix2x2 u1l; - Matrix2x2 u1ra; - Matrix2x2 u1rb; - Matrix2x2 u2la; - Matrix2x2 u2lb; - Matrix2x2 u2ra; - Matrix2x2 u2rb; - Matrix2x2 u3l; - Matrix2x2 u3r; - - // pre-built components for decomposition with 2 basis gates - Matrix2x2 q0l; - Matrix2x2 q0r; - Matrix2x2 q1la; - Matrix2x2 q1lb; - Matrix2x2 q1ra; - Matrix2x2 q1rb; - Matrix2x2 q2l; - Matrix2x2 q2r; -}; - -} // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h deleted file mode 100644 index 4d45854321..0000000000 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#pragma once - -#include "mlir/Dialect/QCO/Utils/Matrix.h" - -#include - -/// Numeric helpers used by the decomposition passes. - -namespace mlir::qco::helpers { - -/// Check whether `matrix` is unitary within `tolerance` (i.e. `M^H M` is -/// approximately the identity). -[[nodiscard]] bool isUnitaryMatrix(const Matrix2x2& matrix, - double tolerance = 1e-12); - -/** - * Euclidean remainder of a modulo b. - * The returned value is never negative. - */ -[[nodiscard]] double remEuclid(double a, double b); - -/** - * Convert a two-qubit trace overlap into the average gate fidelity metric used - * by the decomposition cost code. - */ -[[nodiscard]] double traceToFidelity(const std::complex& x); - -/** - * Return the scalar `e^(i * globalPhase)` factor for a stored global phase. - */ -[[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 deleted file mode 100644 index cb6fd56389..0000000000 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#pragma once - -#include "mlir/Dialect/QCO/Utils/Matrix.h" - -#include - -#include - -/// Standard-basis matrix factories for the decomposition layer. Two-qubit -/// matrices use the same computational-basis index bit order as -/// ``UnitaryOpInterface::getUnitaryMatrix4x4`` (qubit 0 labels the high bit). - -namespace mlir::qco::decomposition { - -/// Logical qubit index used by ``expandToTwoQubits`` / -/// ``fixTwoQubitMatrixQubitOrder``. -using QubitId = std::size_t; - -inline constexpr double FRAC1_SQRT2 = - 0.707106781186547524400844362104849039284835937688474036588L; - -/// Generic 3-parameter single-qubit unitary `U(theta, phi, lambda)`. -[[nodiscard]] Matrix2x2 uMatrix(double theta, double phi, double lambda); -/// Axis rotations `exp(-i theta/2 * sigma_{x,y,z})`. -[[nodiscard]] Matrix2x2 rxMatrix(double theta); -[[nodiscard]] Matrix2x2 ryMatrix(double theta); -[[nodiscard]] Matrix2x2 rzMatrix(double theta); -/// Two-qubit Ising-style rotations on the `XX`, `YY`, `ZZ` generators. -[[nodiscard]] Matrix4x4 rxxMatrix(double theta); -[[nodiscard]] Matrix4x4 ryyMatrix(double theta); -[[nodiscard]] Matrix4x4 rzzMatrix(double theta); -/// Phase gate `diag(1, exp(i lambda))`. -[[nodiscard]] Matrix2x2 pMatrix(double lambda); - -/// `SWAP` gate (4x4). -[[nodiscard]] const Matrix4x4& swapGate(); -/// Hadamard gate (2x2). -[[nodiscard]] const Matrix2x2& hGate(); -/// `i * sigma_z`; useful when factoring Pauli rotations out of a 2x2. -[[nodiscard]] const Matrix2x2& ipz(); -/// `i * sigma_y`. -[[nodiscard]] const Matrix2x2& ipy(); -/// `i * sigma_x`. -[[nodiscard]] const Matrix2x2& ipx(); - -/// CX entangler with control on qubit 0 (MSB) and target on qubit 1. -[[nodiscard]] const Matrix4x4& cxGate01(); -/// CX entangler with control on qubit 1 and target on qubit 0 (MSB). -[[nodiscard]] const Matrix4x4& cxGate10(); -/// CZ entangler (wire-order invariant). -[[nodiscard]] const Matrix4x4& czGate(); - -/// Kronecker-embed a 2x2 on wire ``qubitId`` (identity on the other wire). -[[nodiscard]] Matrix4x4 expandToTwoQubits(const Matrix2x2& singleQubitMatrix, - QubitId qubitId); - -/// Reorder a 4x4 two-qubit matrix so its qubits match the canonical -/// `(low, high)` order given the operand-order `qubitIds`. No-op when the -/// operand order already matches. -[[nodiscard]] Matrix4x4 -fixTwoQubitMatrixQubitOrder(const Matrix4x4& twoQubitMatrix, - const llvm::SmallVector& qubitIds); - -} // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h new file mode 100644 index 0000000000..b071e8ab0e --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h @@ -0,0 +1,506 @@ +/* + * 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/Utils/Matrix.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace mlir::qco::decomposition { + +/** + * @brief Native gate kinds that may appear in a two-qubit synthesis menu. + */ +enum class NativeGateKind : std::uint8_t { + U, + X, + Sx, + Rz, + Rx, + Ry, + R, + Cx, + Cz, + Rzz, +}; + +/** + * @brief Single-qubit emission strategy resolved from a native-gate menu. + */ +enum class SingleQubitMode : std::uint8_t { + ZSXX, ///< `RZ` / `SX` / `X` via ZYZ decomposition. + U3, ///< Generic `U(theta, phi, lambda)`. + R, ///< `R(theta, phi)` chain (`Rx`/`Ry` as `R`). + AxisPair, ///< Two fixed rotation axes (see @ref AxisPair). +}; + +/** + * @brief Rotation-axis pair for @ref SingleQubitMode::AxisPair emitters. + */ +enum class AxisPair : std::uint8_t { + RxRz, + RxRy, + RyRz, +}; + +/** + * @brief Entangling basis gate for two-qubit Weyl synthesis. + */ +enum class EntanglerBasis : std::uint8_t { + None, + Cx, + Cz, +}; + +/// Resolved single-qubit emitter entry in a @ref NativeProfileSpec. +struct SingleQubitEmitterSpec { + SingleQubitMode mode = SingleQubitMode::U3; + AxisPair axisPair = AxisPair::RxRz; + bool supportsDirectRx = false; +}; + +/** + * @brief Fully resolved native-gate menu for two-qubit Weyl synthesis. + * + * Produced by @ref parseNativeSpec from a comma-separated gate list such as + * `"x,sx,rz,cx"`. + */ +struct NativeProfileSpec { + bool allowRzz = false; + llvm::DenseSet allowedGates; + llvm::SmallVector singleQubitEmitters; + llvm::SmallVector entanglerBases; +}; + +/** + * @brief Weyl decomposition of a 2-qubit unitary matrix (4x4). + * + * The result consists of four 2x2 single-qubit matrices (`k1l`, `k2l`, + * `k1r`, `k2r`) and three parameters for a canonical gate (`a`, `b`, `c`). + * 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 + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Any modifications or derivative works of this code must retain this + * copyright notice, and modified files need to carry a notice + * indicating that they have been altered from the originals. + */ +class TwoQubitWeylDecomposition { +public: + /** + * @brief Create Weyl decomposition. + * + * @param unitaryMatrix Matrix of the two-qubit operation/series to be + * decomposed. + * @param fidelity Tolerance to assume a specialization which is used to + * reduce the number of parameters required by the canonical + * gate and thus potentially decreasing the number of basis + * gates. + */ + static TwoQubitWeylDecomposition create(const Matrix4x4& unitaryMatrix, + std::optional fidelity); + + ~TwoQubitWeylDecomposition() = default; + TwoQubitWeylDecomposition(const TwoQubitWeylDecomposition&) = default; + TwoQubitWeylDecomposition(TwoQubitWeylDecomposition&&) = default; + TwoQubitWeylDecomposition& + operator=(const TwoQubitWeylDecomposition&) = default; + TwoQubitWeylDecomposition& operator=(TwoQubitWeylDecomposition&&) = default; + + /** @brief Matrix of the canonical gate from its parameters `a`, `b`, `c`. */ + [[nodiscard]] Matrix4x4 getCanonicalMatrix() const { + return getCanonicalMatrix(a_, b_, c_); + } + + /** + * @brief First parameter of the canonical gate. + * + * @note Multiply by `-2.0` for the `RXX` rotation angle. + */ + [[nodiscard]] double a() const { return a_; } + /** + * @brief Second parameter of the canonical gate. + * + * @note Multiply by `-2.0` for the `RYY` rotation angle. + */ + [[nodiscard]] double b() const { return b_; } + /** + * @brief Third parameter of the canonical gate. + * + * @note Multiply by `-2.0` for the `RZZ` rotation angle. + */ + [[nodiscard]] double c() const { return c_; } + /** @brief Global phase adjustment after applying the decomposition. */ + [[nodiscard]] double globalPhase() const { return globalPhase_; } + + /** + * @brief Left single-qubit factor after the canonical gate. + * + * ``` + * q1 - k2r - C - k1r - + * A + * q0 - k2l - N - *k1l* - + * ``` + */ + [[nodiscard]] const Matrix2x2& k1l() const { return k1l_; } + /** + * @brief Left single-qubit factor before the canonical gate. + * + * ``` + * q1 - k2r - C - k1r - + * A + * q0 - *k2l* - N - k1l - + * ``` + */ + [[nodiscard]] const Matrix2x2& k2l() const { return k2l_; } + /** + * @brief Right single-qubit factor after the canonical gate. + * + * ``` + * q1 - k2r - C - *k1r* - + * A + * q0 - k2l - N - k1l - + * ``` + */ + [[nodiscard]] const Matrix2x2& k1r() const { return k1r_; } + /** + * @brief Right single-qubit factor before the canonical gate. + * + * ``` + * q1 - *k2r* - C - k1r - + * A + * q0 - k2l - N - k1l - + * ``` + */ + [[nodiscard]] const Matrix2x2& k2r() const { return k2r_; } + + /** @brief Canonical gate matrix for parameters `a`, `b`, `c`. */ + [[nodiscard]] static Matrix4x4 getCanonicalMatrix(double a, double b, + double c); + +protected: + enum class Specialization : std::uint8_t { + General, + IdEquiv, + SWAPEquiv, + PartialSWAPEquiv, + PartialSWAPFlipEquiv, + ControlledEquiv, + MirrorControlledEquiv, + FSimaabEquiv, + FSimabbEquiv, + FSimabmbEquiv, + }; + + enum class MagicBasisTransform : std::uint8_t { + Into, + OutOf, + }; + + static constexpr auto DIAGONALIZATION_PRECISION = 1e-13; + + TwoQubitWeylDecomposition() = default; + + [[nodiscard]] static Matrix4x4 + magicBasisTransform(const Matrix4x4& unitary, MagicBasisTransform direction); + + [[nodiscard]] static double closestPartialSwap(double a, double b, double c); + + [[nodiscard]] static std::pair> + diagonalizeComplexSymmetric(const Matrix4x4& m, double precision); + + static std::tuple + decomposeTwoQubitProductGate(const Matrix4x4& specialUnitary); + + [[nodiscard]] static std::complex + getTrace(double a, double b, double c, double ap, double bp, double cp); + + [[nodiscard]] Specialization bestSpecialization() const; + + bool applySpecialization(); + +private: + double a_{}; + double b_{}; + double c_{}; + double globalPhase_{}; + Matrix2x2 k1l_; + Matrix2x2 k2l_; + Matrix2x2 k1r_; + Matrix2x2 k2r_; + Specialization specialization{Specialization::General}; + std::optional requestedFidelity; +}; + +using TwoQubitLocalUnitaryList = llvm::SmallVector; + +/** + * @brief Result of a two-qubit basis decomposition as single-qubit factors and + * entangler uses. + * + * Factors are stored in emission order. For `i` in `[0, numBasisUses)` the + * pair `(singleQubitFactors[2*i], singleQubitFactors[2*i + 1])` is applied to + * qubits `1` and `0` respectively, followed by one entangler. The final pair + * `(singleQubitFactors[2*numBasisUses], singleQubitFactors[2*numBasisUses+1])` + * is applied after the last entangler. The list therefore has length + * `2 * (numBasisUses + 1)`. + */ +struct TwoQubitNativeDecomposition { + /// Number of basis-gate (entangler) uses. + std::uint8_t numBasisUses = 0; + /// Single-qubit factors in emission order (see struct comment). + TwoQubitLocalUnitaryList singleQubitFactors; + /// Residual global phase (radians) not represented by factors/entanglers. + double globalPhase = 0.0; +}; + +/** + * @brief Decomposer initialized with a two-qubit basis gate for canonical-gate + * (RXX+RYY+RZZ) synthesis. + * + * @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 + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Any modifications or derivative works of this code must retain this + * copyright notice, and modified files need to carry a notice + * indicating that they have been altered from the originals. + */ +class TwoQubitBasisDecomposer { +public: + /** + * @brief Create decomposer for the specified entangler matrix. + * + * The entangler appears between 0 and 3 times in each decomposition. The 4x4 + * matrix must be in MQT operand order (qubit 0 = MSB). + */ + [[nodiscard]] static TwoQubitBasisDecomposer + create(const Matrix4x4& basisMatrix, double basisFidelity); + + /** + * @brief Perform decomposition using this decomposer's basis gate. + * + * @param targetDecomposition Prepared Weyl decomposition of the unitary to + * decompose. + * @param numBasisGateUses Force a given number of basis gates; when unset, + * the optimal count is selected from the + * Hilbert-Schmidt traces. + * @return Single-qubit factors and entangler count, or `std::nullopt` when + * more than one basis gate would be required but the basis gate is + * not super-controlled. + */ + [[nodiscard]] std::optional + twoQubitDecompose(const TwoQubitWeylDecomposition& targetDecomposition, + std::optional numBasisGateUses) const; + +protected: + // NOLINTBEGIN(modernize-pass-by-value) + /** @brief Constructs decomposer instance. */ + TwoQubitBasisDecomposer( + double basisFidelity, const TwoQubitWeylDecomposition& basisDecomposer, + bool isSuperControlled, const Matrix2x2& u0l, const Matrix2x2& u0r, + const Matrix2x2& u1l, const Matrix2x2& u1ra, const Matrix2x2& u1rb, + const Matrix2x2& u2la, const Matrix2x2& u2lb, const Matrix2x2& u2ra, + const Matrix2x2& u2rb, const Matrix2x2& u3l, const Matrix2x2& u3r, + const Matrix2x2& q0l, const Matrix2x2& q0r, const Matrix2x2& q1la, + const Matrix2x2& q1lb, const Matrix2x2& q1ra, const Matrix2x2& q1rb, + const Matrix2x2& q2l, const Matrix2x2& q2r) + : 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) + + /** + * @brief Decomposition with 0 basis gates. + * + * Decompose target :math:`\sim U_d(x, y, z)` with 0 uses of the basis gate. + * Result :math:`U_r` has trace: + * + * .. math:: + * + * \Big\vert\text{Tr}(U_r\cdot U_\text{target}^{\dag})\Big\vert = + * 4\Big\vert (\cos(x)\cos(y)\cos(z)+ j \sin(x)\sin(y)\sin(z)\Big\vert + * + * which is optimal for all targets and bases. + */ + [[nodiscard]] static TwoQubitLocalUnitaryList + decomp0(const TwoQubitWeylDecomposition& target); + + /** + * @brief Decomposition with 1 basis gate. + * + * Decompose target :math:`\sim U_d(x, y, z)` with 1 use of the basis gate + * :math:`\sim U_d(a, b, c)`. Result :math:`U_r` has trace: + * + * .. math:: + * + * \Big\vert\text{Tr}(U_r \cdot U_\text{target}^{\dag})\Big\vert = + * 4\Big\vert \cos(x-a)\cos(y-b)\cos(z-c) + j + * \sin(x-a)\sin(y-b)\sin(z-c)\Big\vert + * + * which is optimal for all targets and bases with ``z==0`` or ``c==0``. + */ + [[nodiscard]] TwoQubitLocalUnitaryList + decomp1(const TwoQubitWeylDecomposition& target) const; + + /** + * @brief Decomposition with 2 basis gates. + * + * Decompose target :math:`\sim U_d(x, y, z)` with 2 uses of the basis gate. + * + * For supercontrolled basis :math:`\sim U_d(\pi/4, b, 0)`, all b, result + * :math:`U_r` has trace + * + * .. math:: + * + * \Big\vert\text{Tr}(U_r \cdot U_\text{target}^\dag) \Big\vert = + * 4\cos(z) + * + * which is the optimal approximation for basis of CNOT-class + * :math:`\sim U_d(\pi/4, 0, 0)` or DCNOT-class + * :math:`\sim U_d(\pi/4, \pi/4, 0)` and any target. It may be sub-optimal + * for :math:`b \neq 0` (i.e. there exists an exact decomposition for any + * target using :math:`B \sim U_d(\pi/4, \pi/8, 0)`, but it may not be this + * decomposition). This is an exact decomposition for supercontrolled basis + * and target :math:`\sim U_d(x, y, 0)`. No guarantees for + * non-supercontrolled basis. + */ + [[nodiscard]] TwoQubitLocalUnitaryList + decomp2Supercontrolled(const TwoQubitWeylDecomposition& target) const; + + /** + * @brief Decomposition with 3 basis gates. + * + * Exact decomposition for supercontrolled basis :math:`\sim U_d(\pi/4, b, + * 0)`, all b, and any target. No guarantees for non-supercontrolled basis. + */ + [[nodiscard]] TwoQubitLocalUnitaryList + decomp3Supercontrolled(const TwoQubitWeylDecomposition& target) const; + + /** + * @brief Traces for canonical-gate parameter combinations of target and + * basis. + * + * Used to determine the smallest number of basis gates needed for an + * equivalent canonical gate. + */ + [[nodiscard]] std::array, 4> + traces(const TwoQubitWeylDecomposition& target) const; + + [[nodiscard]] static bool relativeEq(double lhs, double rhs, double epsilon, + double maxRelative); + +private: + double basisFidelity; + TwoQubitWeylDecomposition basisDecomposer; + bool isSuperControlled; + Matrix2x2 u0l; + Matrix2x2 u0r; + Matrix2x2 u1l; + Matrix2x2 u1ra; + Matrix2x2 u1rb; + Matrix2x2 u2la; + Matrix2x2 u2lb; + Matrix2x2 u2ra; + Matrix2x2 u2rb; + Matrix2x2 u3l; + Matrix2x2 u3r; + Matrix2x2 q0l; + Matrix2x2 q0r; + Matrix2x2 q1la; + Matrix2x2 q1lb; + Matrix2x2 q1ra; + Matrix2x2 q1rb; + Matrix2x2 q2l; + Matrix2x2 q2r; +}; + +/** + * @brief Parses a comma-separated native-gate menu (e.g. `"u,cx,rzz"`). + * + * @param nativeGates Comma-separated gate names (case-insensitive). + * @return The resolved profile, or `std::nullopt` if any token is unknown or + * no legal single-qubit emitter can be derived. + */ +[[nodiscard]] std::optional +parseNativeSpec(StringRef nativeGates); + +/** + * @brief Synthesizes a composed two-qubit unitary as gates in @p spec. + * + * Emits single-qubit factors from the first resolved emitter in @p spec and + * entanglers from the preferred basis (`CX` over `CZ`). Returns `failure()` + * when @p spec has no entangler or the Weyl decomposition cannot be realized. + * + * @param builder Builder for the emitted operations. + * @param loc Location for the emitted operations. + * @param qubit0 First qubit (control wire for entanglers). + * @param qubit1 Second qubit (target wire for entanglers). + * @param target Composed unitary to synthesize (MQT operand order). + * @param spec Resolved native-gate menu. + * @param outQubit0 Output value for qubit 0 after synthesis. + * @param outQubit1 Output value for qubit 1 after synthesis. + * @return `success()` when gates were emitted, `failure()` otherwise. + */ +[[nodiscard]] LogicalResult +synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, Value qubit0, + Value qubit1, const Matrix4x4& target, + const NativeProfileSpec& spec, Value& outQubit0, + Value& outQubit1); + +/** + * @brief Number of entangling basis gates required to synthesize @p target. + * + * @return Entangler count for @p spec, or `std::nullopt` if synthesis fails. + */ +[[nodiscard]] std::optional +twoQubitEntanglerCount(const Matrix4x4& target, const NativeProfileSpec& spec); + +/// Common constant `1/sqrt(2)` used by gate-matrix factories. +inline constexpr double FRAC1_SQRT2 = + 0.707106781186547524400844362104849039284835937688474036588L; + +/// Axis rotations `exp(-i theta/2 * sigma_{x,y,z})`. +[[nodiscard]] Matrix2x2 rxMatrix(double theta); +[[nodiscard]] Matrix2x2 ryMatrix(double theta); +[[nodiscard]] Matrix2x2 rzMatrix(double theta); +/// `i * sigma_z`; useful when factoring Pauli rotations out of a 2x2. +[[nodiscard]] const Matrix2x2& ipz(); +/// `i * sigma_y`. +[[nodiscard]] const Matrix2x2& ipy(); +/// `i * sigma_x`. +[[nodiscard]] const Matrix2x2& ipx(); +} // 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 deleted file mode 100644 index 7f9bda7a30..0000000000 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.h +++ /dev/null @@ -1,239 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#pragma once - -#include "mlir/Dialect/QCO/Utils/Matrix.h" - -#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 - * https://www.apache.org/licenses/LICENSE-2.0. - * - * Any modifications or derivative works of this code must retain this - * copyright notice, and modified files need to carry a notice - * indicating that they have been altered from the originals. - */ -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 Matrix4x4& 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]] Matrix4x4 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 Matrix2x2& k1l() const { return k1l_; } - /** - * "Left" qubit before canonical gate. - * - * q1 - k2r - C - k1r - - * A - * q0 - *k2l* - N - k1l - - */ - [[nodiscard]] const Matrix2x2& k2l() const { return k2l_; } - /** - * "Right" qubit after canonical gate. - * - * q1 - k2r - C - *k1r* - - * A - * q0 - k2l - N - k1l - - */ - [[nodiscard]] const Matrix2x2& k1r() const { return k1r_; } - /** - * "Right" qubit before canonical gate. - * - * q1 - *k2r* - C - k1r - - * A - * q0 - k2l - N - k1l - - */ - [[nodiscard]] const Matrix2x2& k2r() const { return k2r_; } - - /** - * Calculate matrix of canonical gate based on given parameters a, b, c. - */ - [[nodiscard]] static Matrix4x4 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 Matrix4x4 - magicBasisTransform(const Matrix4x4& 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 in SO(4). - * d is a vector containing sqrt(eigenvalues) of M with unit-magnitude - * elements (for each element, complex magnitude is 1.0). - * D is d as a diagonal matrix. - * - * M = P * D * P^T - * - * @return pair of (P, D.diagonal()) - */ - [[nodiscard]] static std::pair> - diagonalizeComplexSymmetric(const Matrix4x4& m, 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 Matrix4x4& 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: - // Canonical gate parameters `(a, b, c)`; documented on the public accessors. - double a_{}; - double b_{}; - double c_{}; - double globalPhase_{}; - // Single-qubit factors surrounding the canonical gate; see the accessors - // for the per-field wiring diagram. - Matrix2x2 k1l_; - Matrix2x2 k2l_; - Matrix2x2 k1r_; - Matrix2x2 k2r_; - Specialization specialization{Specialization::General}; - /// Optional `traceToFidelity` floor for specialization; unset disables it. - std::optional requestedFidelity; -}; -} // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.h deleted file mode 100644 index a09f980139..0000000000 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.h +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -/// \file -/// Fuse maximal two-qubit unitary windows (with absorbed single-qubit padding). - -#pragma once - -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" - -#include -#include -#include - -namespace mlir::qco::native_synth { - -/// Scan `root` for maximal two-qubit windows (including absorbed single-qubit -/// ops on the same wire pair) and replace each window when Weyl/KAK -/// resynthesis to the native profile is profitable. -LogicalResult fuseTwoQubitUnitaryRuns(IRRewriter& rewriter, Operation* root, - const NativeProfileSpec& spec); - -} // namespace mlir::qco::native_synth diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h deleted file mode 100644 index 4993a85758..0000000000 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#pragma once - -#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" - -#include - -#include - -namespace mlir::qco::native_synth { - -/// Euler basis used to synthesize an arbitrary single-qubit unitary into the -/// gates emitted by `emitter`. This is the deterministic replacement for the -/// scored multi-basis search. -[[nodiscard]] decomposition::EulerBasis -emitterEulerBasis(const SingleQubitEmitterSpec& emitter); - -/// Resolve a comma-separated native gate menu (e.g. `"x,sx,rz,cx"`) into a -/// full `NativeProfileSpec`. -/// -/// Parses the pass `native-gates` string into a `NativeProfileSpec` -/// (single-qubit emitters, entangler bases, and `allowedGates`). Token set -/// matches `Passes.td` on this pass. -/// -/// Recognised tokens: `u`, `x`, `sx`, `rz` (or `p`), `rx`, `ry`, `r`, -/// `cx`, `cz`, `rzz`. -std::optional -resolveNativeGatesSpec(llvm::StringRef nativeGates); - -} // namespace mlir::qco::native_synth diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h deleted file mode 100644 index 7b7b00fb9b..0000000000 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#pragma once - -#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" - -#include - -/// Menu membership checks for native synthesis (no IR rewrites). - -namespace mlir::qco::native_synth { - -/// Whether the menu contains the corresponding two-qubit entangler. Used by -/// the 2q rewrite path to pick between CX and CZ emission. -bool usesCxEntangler(const NativeProfileSpec& spec); -bool usesCzEntangler(const NativeProfileSpec& spec); - -/// Whether an already-lowered single-qubit op is in the menu (i.e. no -/// further rewrite needed). -bool allowsSingleQubitOp(UnitaryOpInterface op, const NativeProfileSpec& spec); - -/// Whether `op` has a direct (non-matrix) lowering via the corresponding -/// `decomposeTo*` helper in `SingleQubit.h`. These are used for ops whose -/// angles are not compile-time constants, so no constant ``2×2`` matrix is -/// available for the matrix-driven path. -bool canDirectlyDecomposeToZSXX(Operation* op, bool supportsDirectRx); -bool canDirectlyDecomposeToU3(Operation* op); -bool canDirectlyDecomposeToR(Operation* op); -bool canDirectlyDecomposeToAxisPair(Operation* op, AxisPair axisPair); - -} // namespace mlir::qco::native_synth diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h deleted file mode 100644 index b09c48b4dd..0000000000 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -/// \file -/// Single-qubit native-synthesis lowering helpers. -/// Covers symbolic `decomposeTo*` rewrites (used for dynamic-angle ops) plus -/// the matrix-driven `emitSingleQubitMatrix` synthesizer that lowers any -/// constant ``2×2`` unitary via the shared `Euler.h` synthesis. - -#pragma once - -#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" -#include "mlir/Dialect/QCO/Utils/Matrix.h" - -#include -#include - -namespace mlir::qco::native_synth { - -/// Direct (non-matrix) single-qubit lowering to the `ZSXX` emitter -/// (`{Rz, Sx, X}`). Returns the output qubit value, or a null `Value` if no -/// direct rule applies and a matrix-based fallback must be tried. -/// -/// When `supportsDirectRx` is true, the emitter also passes `Rx` through -/// unchanged and lowers `Ry` / `R` via an `rz * rx * rz` sandwich. -Value decomposeToZSXX(IRRewriter& rewriter, Operation* op, Value inQubit, - bool supportsDirectRx); - -/// Direct (non-matrix) single-qubit lowering to a `U(theta, phi, lambda)` -/// output. Returns the output qubit value, or a null `Value` if no direct -/// rule applies and a matrix-based fallback must be tried. -Value decomposeToU3(IRRewriter& rewriter, Operation* op, Value inQubit); - -/// Direct (non-matrix) single-qubit lowering to the `R(theta, phi)` emitter. -/// Returns the output qubit value, or a null `Value` if no direct rule -/// applies and a matrix-based fallback must be tried. -Value decomposeToR(IRRewriter& rewriter, Operation* op, Value inQubit); - -/// Direct (non-matrix) single-qubit lowering to a two-axis emitter -/// identified by `axisPair` (e.g. `{Rx, Rz}`, `{Ry, Rz}`). Returns the -/// output qubit value, or a null `Value` if no direct rule applies and a -/// matrix-based fallback must be tried. -Value decomposeToAxisPair(IRRewriter& rewriter, Operation* op, Value inQubit, - AxisPair axisPair); - -/// Synthesize a constant ``2×2`` unitary `matrix` into native gates of `basis` -/// (including a `qco.gphase` when the residual phase is non-trivial) and -/// return the resulting output qubit. Wraps `decomposition::Euler`. -Value emitSingleQubitMatrix(IRRewriter& rewriter, Location loc, Value inQubit, - const Matrix2x2& matrix, - decomposition::EulerBasis basis); - -} // namespace mlir::qco::native_synth diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h deleted file mode 100644 index cd90b21861..0000000000 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#pragma once - -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" -#include "mlir/Dialect/QCO/Utils/Matrix.h" - -#include -#include -#include -#include - -#include - -/// Deterministic two-qubit lowering: Weyl decomposition + the -/// `TwoQubitBasisDecomposer` with a fixed entangler (CX before CZ) and the -/// first emitter's Euler basis for the surrounding single-qubit factors. - -namespace mlir::qco::native_synth { - -/// Number of entanglers (basis-gate uses) the minimal KAK decomposition of -/// `target` requires for the entangler selected by `spec` (CX before CZ). -/// Returns `std::nullopt` when `spec` has no usable entangler basis. -std::optional -twoQubitEntanglerCount(const Matrix4x4& target, const NativeProfileSpec& spec); - -/// Synthesize the two-qubit unitary `target` (raw `4×4`, any global phase) at -/// `(qubit0, qubit1)` into native entanglers and single-qubit gates of `spec`. -/// The entangler is chosen deterministically (CX before CZ) and the -/// single-qubit factors use the first emitter's Euler basis. Writes the output -/// qubit values to `outQubit0` / `outQubit1`. -/// -/// Returns `failure()` when the profile has no usable entangler basis or the -/// KAK decomposition is not realizable with that entangler. -LogicalResult emitTwoQubitNative(IRRewriter& rewriter, Location loc, - Value qubit0, Value qubit1, - const Matrix4x4& target, - const NativeProfileSpec& spec, - Value& outQubit0, Value& outQubit1); - -/// Rewrite `XXPlusYY` / `XXMinusYY` via two `RZZ` blocks (menus with `rzz`). -LogicalResult rewriteXXPlusMinusYYViaRzz(IRRewriter& rewriter, Operation* op); - -} // namespace mlir::qco::native_synth diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h deleted file mode 100644 index cd32596bb9..0000000000 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#pragma once - -#include -#include - -#include - -/// Types for native gate synthesis: the resolved menu and its emitters. - -namespace mlir::qco::native_synth { - -/// Two-axis token pairs (`rx`+`rz`, `rx`+`ry`, `ry`+`rz`) that can be selected -/// as the single-qubit menu in a `NativeProfileSpec`. -enum class AxisPair : std::uint8_t { RxRz, RxRy, RyRz }; - -/// Single-qubit emission strategy. -enum class SingleQubitMode : std::uint8_t { - /// Emit `{X, Sx, Rz}` via the ZSXX Euler decomposition. When the spec's - /// `supportsDirectRx` is set, the emitter additionally passes Rx through - /// unchanged and expands Ry / R via an `rz * rx * rz` sandwich. - ZSXX, - /// Emit a single `u(theta, phi, lambda)` op. - U3, - /// Emit `R(theta, phi)` via the XYX Euler decomposition. - R, - /// Emit one of the three two-axis rotation pairs selected by `axisPair`. - AxisPair, -}; - -/// Two-qubit entangling basis selected by a profile. -enum class EntanglerBasis : std::uint8_t { None, Cx, Cz }; - -/// Profile-level classification of a native gate. Used both to describe the -/// menu (`NativeProfileSpec::allowedGates`) and to classify already-lowered -/// output ops in policy checks. -enum class NativeGateKind : std::uint8_t { - U, - X, - Sx, - Rz, - Rx, - Ry, - R, - Cx, - Cz, - Rzz, -}; - -/// Single-qubit emitter specification: the target mode plus any modifiers -/// (axis pair, whether direct Rx emission is permitted). -struct SingleQubitEmitterSpec { - SingleQubitMode mode = SingleQubitMode::U3; - AxisPair axisPair = AxisPair::RxRz; - /// Only meaningful for `SingleQubitMode::ZSXX`: when set, the emitter may - /// emit Rx / Ry / R directly (via an `rz * rx * rz` sandwich for the latter - /// two) instead of falling back to the ZSXX Euler sequence. - bool supportsDirectRx = false; -}; - -/// Resolved menu: emitters to try for 1q synthesis and entangler bases for 2q. -/// Built by `resolveNativeGatesSpec`. Single-qubit synthesis is deterministic: -/// the first emitter is preferred and its Euler basis drives matrix synthesis. -struct NativeProfileSpec { - bool allowRzz = false; - llvm::DenseSet allowedGates; - llvm::SmallVector singleQubitEmitters; - llvm::SmallVector entanglerBases; -}; - -} // namespace mlir::qco::native_synth diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h b/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h deleted file mode 100644 index fbf70131b8..0000000000 --- a/mlir/include/mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#pragma once - -#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" -#include "mlir/Dialect/QCO/Utils/Matrix.h" - -#include -#include -#include - -#include - -/// F64 helpers and block unitary extraction for native gate synthesis. - -namespace mlir::qco::native_synth { - -/// Create an ``arith.constant`` F64. -Value createF64Const(IRRewriter& rewriter, Location loc, double value); - -/// If ``value`` is an F64 ``arith.constant``, return its value. -std::optional getConstantF64(Value value); - -/// Emit a `qco.gphase` if `phase` is non-negligible. -void emitGPhaseIfNonTrivial(IRRewriter& rewriter, Location loc, double phase); - -/// 4x4 for a 2q block member (plain 2q, ``CtrlOp`` CX/CZ, or lifted 1q). Fails -/// for barriers, ``gphase``, multi-control, or non-constant matrix parameters. -bool getBlockTwoQubitMatrix(Operation* op, Matrix4x4& matrix); - -/// Pre-order walk: every op implementing `UnitaryOpInterface` under `root`, -/// excluding bodies nested under `ctrl` / `inv`. -void collectUnitaryOpsInPreOrder(Operation* root, - llvm::SmallVectorImpl& ops); - -} // namespace mlir::qco::native_synth diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h index 39289a1062..1919b50b0c 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h @@ -29,15 +29,4 @@ namespace mlir::qco { #define GEN_PASS_REGISTRATION #include "mlir/Dialect/QCO/Transforms/Passes.h.inc" // IWYU pragma: export -/// Options for the native gate synthesis pass. -/// -/// @p nativeGates is a comma-separated list of gate tokens (see `Passes.td` -/// for recognised tokens). -struct NativeGateSynthesisOptions { - std::string nativeGates; -}; - -std::unique_ptr -createNativeGateSynthesisPass(const NativeGateSynthesisOptions& options); - } // namespace mlir::qco diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index 0b66c68578..f76f225278 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -69,31 +69,57 @@ def FuseSingleQubitUnitaryRuns def FuseTwoQubitUnitaryRuns : Pass<"fuse-two-qubit-unitary-runs", "mlir::ModuleOp"> { let dependentDialects = ["mlir::qco::QCODialect"]; - let summary = "Fuse two-qubit unitary runs using Weyl/KAK resynthesis"; + let summary = "Lower QCO unitary gates to a user-specified native gate menu."; let description = [{ - Scans the module for maximal two-qubit windows: contiguous sequences of - two-qubit unitaries on the same wire pair, with single-qubit gates on those - wires absorbed into the window's accumulated `4×4` unitary when they have - a single use. Each window with at least two ops is replaced when beneficial: - when the window contains any gate outside the `native-gates` menu, or when - deterministic Weyl/KAK resynthesis to that menu uses strictly fewer - entanglers than the window already contains. - - The `native-gates` option uses the same comma-separated token list as - `native-gate-synthesis` (e.g. `u,cx`, `x,sx,rz,cx`). An empty or - whitespace-only menu is a no-op. An unrecognised token causes the pass to - fail. - - Barriers, global phase, fan-out, and ops on more than two qubits close - open windows. Bodies nested under `qco.ctrl` or `qco.inv` are not tracked - independently. + This pass rewrites a module so that every remaining unitary operation is + allowed by the `native-gates` menu. `qco.barrier` and `qco.gphase` are + preserved; controlled gates (`qco.ctrl`) must have a single control and a + single target. + + The menu is a comma-separated list of gate tokens (order not significant) + from which the pass builds a profile: a single-qubit synthesis mode + (generic `qco.u` when `u` is present; IBM-style surface gates when all of + `x`, `sx`, and `rz`/`p` are present; IQM-style `qco.r` when `r` is present; + or a supported rotation pair chosen from `rx`, `ry`, `rz`) plus optional + two-qubit entanglers `cx`, `cz`, and optional `rzz`. + + Recognised tokens: `u`, `x`, `sx`, `rz` (or `p`), `rx`, `ry`, `r`, `cx`, + `cz`, `rzz`. An empty or whitespace-only menu is a no-op, which is the + intended pipeline default when synthesis is not needed. An unrecognised + token causes the pass to fail. + + Example menus (each line is one illustrative menu; pick either `cx` or + `cz` as the entangler, or list both if both are native): + - IBM basic (no fractional two-qubit): `x,sx,rz,cx` or `x,sx,rz,cz` + - IBM fractional: `x,sx,rz,rx,rzz,cx` or `x,sx,rz,rx,rzz,cz` + - Generic single-qubit U: `u,cx` or `u,cz` + - IQM default: `r,cz` (or `r,cx` if CX is the native entangler) + - Rotation pair + entangler: `rx,rz,cx`, `rx,ry,cz`, `ry,rz,cx`, etc. + Supported pairs are exactly `rx`+`rz`, `rx`+`ry`, and `ry`+`rz`. + + Execution order (mirrors the implementation): fuse consecutive + single-qubit runs; fuse two-qubit windows (including absorbed + single-qubit padding); run up to four synthesis sweeps over remaining + non-native unitaries until every single-qubit op matches the menu (two-qubit + lowering may temporarily emit off-menu 1q ops that later sweeps absorb—if + any remain after that cap, the pass fails); fuse 1q seams between two-qubit + blocks; then up to four further synthesis + fusion rounds until the full menu + holds (including native `qco.ctrl` shells and bare `rzz` when allowed). If + anything is still off-menu, the pass fails. + + Lowering is deterministic: the entangler is chosen as `cx` before `cz`, the + single-qubit factors use the first emitter's Euler basis, and the minimal + KAK entangler count drives two-qubit window replacement. }]; let options = [Option< "nativeGates", "native-gates", "std::string", "\"\"", "Comma-separated native gate menu. Empty or whitespace-only is " - "a no-op. Tokens: u, x, sx, rz (or p), rx, ry, r, cx, cz, rzz.">]; + "a no-op. Tokens: u, x, sx, rz (or p), rx, ry, r, cx, cz, rzz. " + "Examples: x,sx,rz,cx; x,sx,rz,rx,rzz,cz; u,cx; r,cz; rx,rz,cx.">]; } +//===----------------------------------------------------------------------===// + def QuantumLoopUnroll : InterfacePass<"quantum-loop-unroll", "FunctionOpInterface"> { let dependentDialects = ["mlir::qco::QCODialect", "mlir::scf::SCFDialect"]; @@ -171,61 +197,6 @@ def MappingPass : Pass<"place-and-route", "mlir::ModuleOp"> { "The number of inserted SWAPs">]; } -//===----------------------------------------------------------------------===// -// Native gate synthesis -//===----------------------------------------------------------------------===// - -def NativeGateSynthesisPass : Pass<"native-gate-synthesis", "mlir::ModuleOp"> { - let dependentDialects = ["mlir::qco::QCODialect"]; - let summary = "Lower QCO unitary gates to a user-specified native gate menu."; - let description = [{ - This pass rewrites a module so that every remaining unitary operation is - allowed by the `native-gates` menu. `qco.barrier` and `qco.gphase` are - preserved; controlled gates (`qco.ctrl`) must have a single control and a - single target. - - The menu is a comma-separated list of gate tokens (order not significant) - from which the pass builds a profile: a single-qubit synthesis mode - (generic `qco.u` when `u` is present; IBM-style surface gates when all of - `x`, `sx`, and `rz`/`p` are present; IQM-style `qco.r` when `r` is present; - or a supported rotation pair chosen from `rx`, `ry`, `rz`) plus optional - two-qubit entanglers `cx`, `cz`, and optional `rzz`. - - Recognised tokens: `u`, `x`, `sx`, `rz` (or `p`), `rx`, `ry`, `r`, `cx`, - `cz`, `rzz`. An empty or whitespace-only menu is a no-op, which is the - intended pipeline default when synthesis is not needed. An unrecognised - token causes the pass to fail. - - Example menus (each line is one illustrative menu; pick either `cx` or - `cz` as the entangler, or list both if both are native): - - IBM basic (no fractional two-qubit): `x,sx,rz,cx` or `x,sx,rz,cz` - - IBM fractional: `x,sx,rz,rx,rzz,cx` or `x,sx,rz,rx,rzz,cz` - - Generic single-qubit U: `u,cx` or `u,cz` - - IQM default: `r,cz` (or `r,cx` if CX is the native entangler) - - Rotation pair + entangler: `rx,rz,cx`, `rx,ry,cz`, `ry,rz,cx`, etc. - Supported pairs are exactly `rx`+`rz`, `rx`+`ry`, and `ry`+`rz`. - - Execution order (mirrors the implementation): fuse consecutive - single-qubit runs; fuse two-qubit windows (including absorbed - single-qubit padding) via `fuse-two-qubit-unitary-runs`; run up to four synthesis sweeps over remaining - non-native unitaries until every single-qubit op matches the menu (two-qubit - lowering may temporarily emit off-menu 1q ops that later sweeps absorb—if - any remain after that cap, the pass fails); fuse 1q seams between two-qubit - blocks; then up to four further synthesis + fusion rounds until the full menu - holds (including native `qco.ctrl` shells and bare `rzz` when allowed). If - anything is still off-menu, the pass fails. - - Lowering is deterministic: the entangler is chosen as `cx` before `cz`, the - single-qubit factors use the first emitter's Euler basis, and the minimal - KAK entangler count drives two-qubit window replacement. - }]; - let options = [Option< - "nativeGates", "native-gates", "std::string", "\"\"", - "Comma-separated native gate menu. Empty or whitespace-only is " - "a no-op. Tokens: u, x, sx, rz (or p), rx, ry, r, cx, cz, rzz. " - "Examples: x,sx,rz,cx; x,sx,rz,rx,rzz,cz; u,cx; r,cz; rx,rz,cx.">]; -} - //===----------------------------------------------------------------------===// // Optimization Passes //===----------------------------------------------------------------------===// diff --git a/mlir/lib/Compiler/CompilerPipeline.cpp b/mlir/lib/Compiler/CompilerPipeline.cpp index 40846f2ef9..ca1aae19f5 100644 --- a/mlir/lib/Compiler/CompilerPipeline.cpp +++ b/mlir/lib/Compiler/CompilerPipeline.cpp @@ -153,8 +153,8 @@ QuantumCompilerPipeline::runPipeline(ModuleOp module, if (config_.enableHadamardLifting) { pm.addPass(qco::createHadamardLifting()); } - pm.addPass( - qco::createNativeGateSynthesisPass(qco::NativeGateSynthesisOptions{ + pm.addPass(qco::createFuseTwoQubitUnitaryRuns( + qco::FuseTwoQubitUnitaryRunsOptions{ .nativeGates = config_.nativeGates, })); }))) { diff --git a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt index f02cdaf23b..d56388d402 100644 --- a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt +++ b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt @@ -22,7 +22,7 @@ add_mlir_library( DEPENDS MLIRQCOTransformsIncGen) -# collect header files (subdirs: NativeSynthesis/, Decomposition/, …) +# collect header files (subdirs: Decomposition/, …) file(GLOB_RECURSE PASSES_HEADERS_SOURCE CONFIGURE_DEPENDS "${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Dialect/QCO/Transforms/*.h") file(GLOB_RECURSE PASSES_HEADERS_BUILD CONFIGURE_DEPENDS diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp deleted file mode 100644 index d8e837c7ba..0000000000 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp +++ /dev/null @@ -1,347 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "mlir/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.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/Utils/Matrix.h" - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace mlir::qco::decomposition { - -using namespace std::complex_literals; - -TwoQubitBasisDecomposer -TwoQubitBasisDecomposer::create(const Matrix4x4& basisMatrix, - double basisFidelity) { - const Matrix2x2 k12RArr = Matrix2x2::fromElements( - 1i * FRAC1_SQRT2, FRAC1_SQRT2, -FRAC1_SQRT2, -1i * FRAC1_SQRT2); - const Matrix2x2 k12LArr = - Matrix2x2::fromElements(Complex{0.5, 0.5}, Complex{0.5, 0.5}, - Complex{-0.5, 0.5}, Complex{0.5, -0.5}); - - // The Shende-Markov-Bullock 3-CX sandwich (and its 1/2-CX reductions) used - // below is derived for a basis CX whose 4x4 matrix is the Qiskit/LSB form - // `[[1,0,0,0],[0,0,0,1],[0,0,1,0],[0,1,0,0]]`, i.e. "control on the LSB - // factor, target on the MSB factor" of the tensor product. MQT's wider - // convention places operand 0 on the MSB factor, so the CX/CZ matrix for - // control-on-wire-0 gives the SWAP-conjugate - // `[[1,0,0,0],[0,1,0,0],[0,0,0,1],[0,0,1,0]]`. - // - // Because `SWAP * C(a,b,c) * SWAP = C(a,b,c)` but - // `SWAP * (K1l ⊗ K1r) * SWAP = (K1r ⊗ K1l)`, feeding the MSB matrix directly - // into the Weyl decomposer would swap the roles of `k1l`/`k1r` (and `k2l`/ - // `k2r`) relative to the hard-coded constants above. To keep the SMB algebra - // self-consistent we SWAP-conjugate the basis matrix here (restoring the - // Qiskit/LSB 4x4) and then absorb the resulting "left/right" relabeling at - // the emission boundary in `decomp{0,1,2,3}` below. This reproduces the - // pre-flip gate counts without having to re-derive every SMB constant for - // the MSB basis -- the two routes are algebraically equivalent. - const Matrix4x4 basisMatrixLsb = swapGate() * basisMatrix * swapGate(); - const auto basisDecomposer = decomposition::TwoQubitWeylDecomposition::create( - basisMatrixLsb, basisFidelity); - const auto isSuperControlled = - relativeEq(basisDecomposer.a(), std::numbers::pi / 4.0, 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(); - Complex temp{0.5, -0.5}; - const Matrix2x2 k11l = Matrix2x2::fromElements( - temp * (-1i * std::exp(-1i * b)), temp * std::exp(-1i * b), - temp * (-1i * std::exp(1i * b)), temp * -std::exp(1i * b)); - const Matrix2x2 k11r = Matrix2x2::fromElements( - 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 Matrix2x2 k32lK21l = - Matrix2x2::fromElements(FRAC1_SQRT2 * Complex{1., std::cos(2. * b)}, - FRAC1_SQRT2 * (1i * std::sin(2. * b)), - FRAC1_SQRT2 * (1i * std::sin(2. * b)), - FRAC1_SQRT2 * Complex{1., -std::cos(2. * b)}); - temp = Complex{0.5, 0.5}; - const Matrix2x2 k21r = Matrix2x2::fromElements( - temp * (-1i * std::exp(-2i * b)), temp * std::exp(-2i * b), - temp * (1i * std::exp(2i * b)), temp * std::exp(2i * b)); - const Matrix2x2 k22l = Matrix2x2::fromElements(FRAC1_SQRT2, -FRAC1_SQRT2, - FRAC1_SQRT2, FRAC1_SQRT2); - const Matrix2x2 k22r = Matrix2x2::fromElements(0, 1, -1, 0); - const Matrix2x2 k31l = Matrix2x2::fromElements( - 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 Matrix2x2 k31r = Matrix2x2::fromElements(1i * std::exp(1i * b), 0, 0, - -1i * std::exp(-1i * b)); - const Matrix2x2 k32r = Matrix2x2::fromElements( - 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().adjoint(); - auto k1rDagger = basisDecomposer.k1r().adjoint(); - auto k2lDagger = basisDecomposer.k2l().adjoint(); - auto k2rDagger = basisDecomposer.k2r().adjoint(); - // 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.adjoint() * k1lDagger; - auto q0r = k12RArr.adjoint() * ipz() * k1rDagger; - auto q1la = k2lDagger * k11l.adjoint(); - auto q1lb = k11l * k1lDagger; - auto q1ra = k2rDagger * ipz() * k11r.adjoint(); - auto q1rb = k11r * k1rDagger; - auto q2l = k2lDagger * k12LArr; - auto q2r = k2rDagger * k12RArr; - - return TwoQubitBasisDecomposer{ - 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, - std::optional numBasisGateUses) const { - auto traces = this->traces(targetDecomposition); - auto getDefaultNbasis = [&]() -> std::uint8_t { - // Pick the number of basis gate uses `i ∈ {0, 1, 2, 3}` that maximizes - // expected_fidelity(i) = traceToFidelity(traces[i]) * basisFidelity^i. - auto bestValue = std::numeric_limits::lowest(); - auto bestIndex = -1; - for (int i = 0; std::cmp_less(i, traces.size()); ++i) { - auto value = - helpers::traceToFidelity(traces[i]) * std::pow(basisFidelity, i); - if (std::isnan(value)) { - continue; - } - if (value > bestValue) { - bestIndex = i; - bestValue = value; - } - } - if (bestIndex < 0) { - llvm::reportFatalInternalError("Unable to select basis-gate count: all " - "candidate fidelities are NaN"); - } - 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(""); - }; - TwoQubitLocalUnitaryList factors = chooseDecomposition(); -#ifndef NDEBUG - for (const auto& factor : factors) { - assert(helpers::isUnitaryMatrix(factor)); - } -#endif - - double globalPhase = targetDecomposition.globalPhase(); - globalPhase -= bestNbasis * basisDecomposer.globalPhase(); - if (bestNbasis == 2) { - // The two-basis (2x CX/CZ) template in `decomp2Supercontrolled` produces - // a sequence whose global phase is off by `pi` relative to the target; - // compensate here so the emitted sequence reproduces the target unitary - // exactly, not just up to sign. - globalPhase += std::numbers::pi; - } - // large global phases can be generated by the decomposition, thus limit - // it to [0, +2*pi) - globalPhase = helpers::remEuclid(globalPhase, 2.0 * std::numbers::pi); - - return TwoQubitNativeDecomposition{ - .numBasisUses = bestNbasis, - .singleQubitFactors = std::move(factors), - .globalPhase = globalPhase, - }; -} - -// Ported SMB helpers assume Qiskit Weyl k-factor layout; QCO 4x4 input order -// swaps l/r vs that port. Swap k1l<->k1r and k2l<->k2r when reading ``target``, -// and swap adjacent pairs in each return vector so the emission boundary maps -// matrices to the same wires as the upstream decomposer. ``decomp0`` cancels to -// the unswapped formula. -TwoQubitLocalUnitaryList -TwoQubitBasisDecomposer::decomp0(const TwoQubitWeylDecomposition& target) { - return TwoQubitLocalUnitaryList{ - target.k1r() * target.k2r(), - target.k1l() * target.k2l(), - }; -} - -TwoQubitLocalUnitaryList TwoQubitBasisDecomposer::decomp1( - const TwoQubitWeylDecomposition& target) const { - // may not work for z != 0 and c != 0 (not always in Weyl chamber) - return TwoQubitLocalUnitaryList{ - basisDecomposer.k2l().adjoint() * target.k2r(), - basisDecomposer.k2r().adjoint() * target.k2l(), - target.k1r() * basisDecomposer.k1l().adjoint(), - target.k1l() * basisDecomposer.k1r().adjoint(), - }; -} - -TwoQubitLocalUnitaryList 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 TwoQubitLocalUnitaryList{ - q2l * target.k2r(), - q2r * target.k2l(), - q1la * rzMatrix(-2. * target.a()) * q1lb, - q1ra * rzMatrix(2. * target.b()) * q1rb, - target.k1r() * q0l, - target.k1l() * q0r, - }; -} - -TwoQubitLocalUnitaryList 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 TwoQubitLocalUnitaryList{ - u3l * target.k2r(), - u3r * target.k2l(), - u2la * rzMatrix(-2. * target.a()) * u2lb, - u2ra * rzMatrix(2. * target.b()) * u2rb, - u1l, - u1ra * rzMatrix(-2. * target.c()) * u1rb, - target.k1r() * u0l, - target.k1l() * u0r, - }; -} - -std::array, 4> -TwoQubitBasisDecomposer::traces(const TwoQubitWeylDecomposition& target) const { - // Returns the Hilbert-Schmidt traces between the target canonical gate and - // the best candidate reachable with `0, 1, 2, 3` uses of the basis gate, - // respectively. Fed into `traceToFidelity` by `getDefaultNbasis` to pick - // the best basis-gate count. The closed-form expressions specialize - // `TwoQubitWeylDecomposition::getTrace(a, b, c, ap, bp, cp)` for: - // i == 0: no basis gate (ap == bp == cp == 0) - // i == 1: one basis use (ap == pi/4, bp == basis.b, cp == 0) - // i == 2: two basis uses (ap == 0, bp == 0, cp == -target.c) - // i == 3: three basis uses (target reachable exactly -> trace == 4) - // so the array has length 4 and is indexed by the number of basis uses. - 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((std::numbers::pi / 4.0) - target.a()) * - std::cos(basisDecomposer.b() - target.b()) * - std::cos(target.c()), - std::sin((std::numbers::pi / 4.0) - target.a()) * - std::sin(basisDecomposer.b() - target.b()) * - std::sin(target.c())}, - std::complex{4. * std::cos(target.c()), 0.}, - std::complex{4., 0.}, - }; -} - -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/Helpers.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp deleted file mode 100644 index 9a449f3902..0000000000 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h" - -#include "mlir/Dialect/QCO/Utils/Matrix.h" - -#include - -#include -#include - -namespace mlir::qco::helpers { - -bool isUnitaryMatrix(const Matrix2x2& matrix, double tolerance) { - return (matrix.adjoint() * matrix).isIdentity(tolerance); -} - -double remEuclid(double a, double b) { - if (b == 0.0) { - llvm::reportFatalInternalError("remEuclid expects non-zero divisor"); - } - auto r = std::fmod(a, b); - return (r < 0.0) ? r + std::abs(b) : r; -} - -double traceToFidelity(const std::complex& x) { - // Average two-qubit process fidelity given the Hilbert-Schmidt overlap - // `x = tr(U_target^dag * U_actual)`. For a 4x4 unitary the general formula is - // `F_avg = (d + |tr|^2) / (d * (d + 1))` with `d = 4`, which reduces to the - // `(4 + |x|^2) / 20` expression below. See e.g. Horodecki/Nielsen. - auto xAbs = std::abs(x); - return (4.0 + (xAbs * xAbs)) / 20.0; -} - -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 deleted file mode 100644 index 4b2f88137b..0000000000 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" - -#include "mlir/Dialect/QCO/Utils/Matrix.h" - -#include -#include - -#include -#include -#include - -namespace mlir::qco::decomposition { - -Matrix2x2 uMatrix(double theta, double phi, double lambda) { - const auto cosHalf = std::cos(theta / 2.); - const auto sinHalf = std::sin(theta / 2.); - return Matrix2x2::fromElements( - Complex{cosHalf, 0.}, - Complex{-std::cos(lambda) * sinHalf, -std::sin(lambda) * sinHalf}, - Complex{std::cos(phi) * sinHalf, std::sin(phi) * sinHalf}, - Complex{std::cos(lambda + phi) * cosHalf, - std::sin(lambda + phi) * cosHalf}); -} - -Matrix2x2 rxMatrix(double theta) { - const auto halfTheta = theta / 2.; - const Complex cos{std::cos(halfTheta), 0.}; - const Complex isin{0., -std::sin(halfTheta)}; - return Matrix2x2::fromElements(cos, isin, isin, cos); -} - -Matrix2x2 ryMatrix(double theta) { - const auto halfTheta = theta / 2.; - const Complex cos{std::cos(halfTheta), 0.}; - const Complex sin{std::sin(halfTheta), 0.}; - return Matrix2x2::fromElements(cos, -sin, sin, cos); -} - -Matrix2x2 rzMatrix(double theta) { - return Matrix2x2::fromElements( - Complex{std::cos(theta / 2.), -std::sin(theta / 2.)}, 0., 0., - Complex{std::cos(theta / 2.), std::sin(theta / 2.)}); -} - -Matrix4x4 rxxMatrix(double theta) { - const auto cosTheta = std::cos(theta / 2.); - const Complex misin{0., -std::sin(theta / 2.)}; - return Matrix4x4::fromElements(cosTheta, 0, 0, misin, // - 0, cosTheta, misin, 0, // - 0, misin, cosTheta, 0, // - misin, 0, 0, cosTheta); -} - -Matrix4x4 ryyMatrix(double theta) { - const auto cosTheta = std::cos(theta / 2.); - const Complex isin{0., std::sin(theta / 2.)}; - const Complex misin{0., -std::sin(theta / 2.)}; - return Matrix4x4::fromElements(cosTheta, 0, 0, isin, // - 0, cosTheta, misin, 0, // - 0, misin, cosTheta, 0, // - isin, 0, 0, cosTheta); -} - -Matrix4x4 rzzMatrix(double theta) { - const auto cosTheta = std::cos(theta / 2.); - const auto sinTheta = std::sin(theta / 2.); - const Complex em{cosTheta, -sinTheta}; - const Complex ep{cosTheta, sinTheta}; - return Matrix4x4::fromElements(em, 0, 0, 0, // - 0, ep, 0, 0, // - 0, 0, ep, 0, // - 0, 0, 0, em); -} - -Matrix2x2 pMatrix(double lambda) { - return Matrix2x2::fromElements(1., 0., 0., - Complex{std::cos(lambda), std::sin(lambda)}); -} - -const Matrix4x4& swapGate() { - static const Matrix4x4 matrix = Matrix4x4::fromElements(1, 0, 0, 0, // - 0, 0, 1, 0, // - 0, 1, 0, 0, // - 0, 0, 0, 1); - return matrix; -} - -const Matrix2x2& hGate() { - static const Matrix2x2 matrix = Matrix2x2::fromElements( - FRAC1_SQRT2, FRAC1_SQRT2, FRAC1_SQRT2, -FRAC1_SQRT2); - return matrix; -} - -const Matrix2x2& ipz() { - static const Matrix2x2 matrix = - Matrix2x2::fromElements(Complex{0, 1}, 0, 0, Complex{0, -1}); - return matrix; -} - -const Matrix2x2& ipy() { - static const Matrix2x2 matrix = Matrix2x2::fromElements(0, 1, -1, 0); - return matrix; -} - -const Matrix2x2& ipx() { - static const Matrix2x2 matrix = - Matrix2x2::fromElements(0, Complex{0, 1}, Complex{0, 1}, 0); - return matrix; -} - -const Matrix4x4& cxGate01() { - static const Matrix4x4 matrix = Matrix4x4::fromElements(1, 0, 0, 0, // - 0, 1, 0, 0, // - 0, 0, 0, 1, // - 0, 0, 1, 0); - return matrix; -} - -const Matrix4x4& cxGate10() { - static const Matrix4x4 matrix = Matrix4x4::fromElements(1, 0, 0, 0, // - 0, 0, 0, 1, // - 0, 0, 1, 0, // - 0, 1, 0, 0); - return matrix; -} - -const Matrix4x4& czGate() { - static const Matrix4x4 matrix = Matrix4x4::fromElements(1, 0, 0, 0, // - 0, 1, 0, 0, // - 0, 0, 1, 0, // - 0, 0, 0, -1); - return matrix; -} - -Matrix4x4 expandToTwoQubits(const Matrix2x2& singleQubitMatrix, - QubitId qubitId) { - if (qubitId == 0) { - return kron(singleQubitMatrix, Matrix2x2::identity()); - } - if (qubitId == 1) { - return kron(Matrix2x2::identity(), singleQubitMatrix); - } - llvm::reportFatalInternalError("Invalid qubit id for single-qubit expansion"); -} - -Matrix4x4 -fixTwoQubitMatrixQubitOrder(const Matrix4x4& twoQubitMatrix, - const llvm::SmallVector& qubitIds) { - if (qubitIds == llvm::SmallVector{1, 0}) { - // `UnitaryOpInterface::getUnitaryMatrix4x4` uses a fixed index order; - // conjugate by SWAP when operand order is (1, 0) instead of (0, 1). - return swapGate() * twoQubitMatrix * swapGate(); - } - if (qubitIds == llvm::SmallVector{0, 1}) { - return twoQubitMatrix; - } - llvm::reportFatalInternalError( - "Invalid qubit IDs for fixing two-qubit matrix"); -} - -} // namespace mlir::qco::decomposition diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp new file mode 100644 index 0000000000..d8b222b656 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp @@ -0,0 +1,1479 @@ +/* + * 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/Weyl.h" + +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr double SANITY_CHECK_PRECISION = 1e-12; +/// Common constant `1/sqrt(2)` used by gate-matrix factories. +inline constexpr double FRAC1_SQRT2 = + 0.707106781186547524400844362104849039284835937688474036588L; + +using mlir::qco::Complex; +using mlir::qco::Matrix2x2; +using mlir::qco::Matrix4x4; + +Matrix2x2 rxMatrix(double theta) { + const auto halfTheta = theta / 2.; + const Complex cos{std::cos(halfTheta), 0.}; + const Complex isin{0., -std::sin(halfTheta)}; + return Matrix2x2::fromElements(cos, isin, isin, cos); +} + +Matrix2x2 ryMatrix(double theta) { + const auto halfTheta = theta / 2.; + const Complex cos{std::cos(halfTheta), 0.}; + const Complex sin{std::sin(halfTheta), 0.}; + return Matrix2x2::fromElements(cos, -sin, sin, cos); +} + +Matrix2x2 rzMatrix(double theta) { + return Matrix2x2::fromElements( + Complex{std::cos(theta / 2.), -std::sin(theta / 2.)}, 0., 0., + Complex{std::cos(theta / 2.), std::sin(theta / 2.)}); +} + +const Matrix2x2& ipz() { + static const Matrix2x2 matrix = + Matrix2x2::fromElements(Complex{0, 1}, 0, 0, Complex{0, -1}); + return matrix; +} + +const Matrix2x2& ipy() { + static const Matrix2x2 matrix = Matrix2x2::fromElements(0, 1, -1, 0); + return matrix; +} + +const Matrix2x2& ipx() { + static const Matrix2x2 matrix = + Matrix2x2::fromElements(0, Complex{0, 1}, Complex{0, 1}, 0); + return matrix; +} + +[[nodiscard]] bool isUnitaryMatrix(const Matrix2x2& matrix, + double tolerance = 1e-12) { + return (matrix.adjoint() * matrix).isIdentity(tolerance); +} + +[[nodiscard]] double remEuclid(double a, double b) { + if (b == 0.0) { + llvm::reportFatalInternalError("remEuclid expects non-zero divisor"); + } + const auto r = std::fmod(a, b); + return (r < 0.0) ? r + std::abs(b) : r; +} + +[[nodiscard]] double traceToFidelity(const std::complex& x) { + const auto xAbs = std::abs(x); + return (4.0 + (xAbs * xAbs)) / 20.0; +} + +[[nodiscard]] std::complex globalPhaseFactor(double globalPhase) { + return std::exp(std::complex{0, 1} * globalPhase); +} + +[[nodiscard]] Matrix4x4 rxxMatrix(double theta) { + const auto cosTheta = std::cos(theta / 2.); + const Complex misin{0., -std::sin(theta / 2.)}; + return Matrix4x4::fromElements(cosTheta, 0, 0, misin, // + 0, cosTheta, misin, 0, // + 0, misin, cosTheta, 0, // + misin, 0, 0, cosTheta); +} + +[[nodiscard]] Matrix4x4 ryyMatrix(double theta) { + const auto cosTheta = std::cos(theta / 2.); + const Complex isin{0., std::sin(theta / 2.)}; + const Complex misin{0., -std::sin(theta / 2.)}; + return Matrix4x4::fromElements(cosTheta, 0, 0, isin, // + 0, cosTheta, misin, 0, // + 0, misin, cosTheta, 0, // + isin, 0, 0, cosTheta); +} + +[[nodiscard]] Matrix4x4 rzzMatrix(double theta) { + const auto cosTheta = std::cos(theta / 2.); + const auto sinTheta = std::sin(theta / 2.); + const Complex em{cosTheta, -sinTheta}; + const Complex ep{cosTheta, sinTheta}; + return Matrix4x4::fromElements(em, 0, 0, 0, // + 0, ep, 0, 0, // + 0, 0, ep, 0, // + 0, 0, 0, em); +} + +[[nodiscard]] const Matrix4x4& swapGate() { + static const Matrix4x4 matrix = Matrix4x4::fromElements(1, 0, 0, 0, // + 0, 0, 1, 0, // + 0, 1, 0, 0, // + 0, 0, 0, 1); + return matrix; +} + +[[nodiscard]] const Matrix4x4& cxGate01() { + static const Matrix4x4 matrix = Matrix4x4::fromElements(1, 0, 0, 0, // + 0, 1, 0, 0, // + 0, 0, 0, 1, // + 0, 0, 1, 0); + return matrix; +} + +[[nodiscard]] const Matrix4x4& cxGate10() { + static const Matrix4x4 matrix = Matrix4x4::fromElements(1, 0, 0, 0, // + 0, 0, 0, 1, // + 0, 0, 1, 0, // + 0, 1, 0, 0); + return matrix; +} + +[[nodiscard]] const Matrix4x4& czGate() { + static const Matrix4x4 matrix = Matrix4x4::fromElements(1, 0, 0, 0, // + 0, 1, 0, 0, // + 0, 0, 1, 0, // + 0, 0, 0, -1); + return matrix; +} + +} // namespace + +namespace mlir::qco::decomposition { + +using namespace std::complex_literals; + +TwoQubitWeylDecomposition +TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, + std::optional fidelity) { + auto u = unitaryMatrix; + auto detU = u.determinant(); + // Project into SU(4) by dividing out the fourth root of det(U): for a 4x4 + // unitary, |det(U)| == 1 so `det^{-1/4}` both enforces det == 1 and removes + // the global phase. The extracted phase is tracked separately in + // `globalPhase` (quarter of arg(det) to match the fourth-root choice) so the + // caller can reconstruct the original matrix exactly if needed. + auto detPow = std::pow(detU, -0.25); + u *= detPow; // remove global phase from unitary matrix + auto globalPhase = std::arg(detU) / 4.; + + // Numerical drift can still leave tiny determinant errors after root + // normalization. Re-normalize once more instead of aborting. + auto detNormalized = u.determinant(); + if (std::abs(detNormalized - Complex{1.0, 0.0}) > SANITY_CHECK_PRECISION && + std::abs(detNormalized) > SANITY_CHECK_PRECISION) { + u *= std::pow(detNormalized, -0.25); + } + + // transform unitary matrix to magic basis; this enables two properties: + // 1. if uP ∈ SO(4), V = A ⊗ B (SO(4) → SU(2) ⊗ SU(2)) + // 2. magic basis diagonalizes canonical gate, allowing calculation of + // canonical gate parameters later on + auto uP = magicBasisTransform(u, MagicBasisTransform::OutOf); + const Matrix4x4 m2 = uP.transpose() * uP; + + // diagonalization yields eigenvectors (p) and eigenvalues (d); + // p is used to calculate K1/K2 (and thus the single-qubit gates + // surrounding the canonical gate); d is 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) + constexpr double pi = std::numbers::pi; + std::array dReal{}; + for (std::size_t i = 0; i < d.size(); ++i) { + dReal[i] = -std::arg(d[i]) / 2.0; + } + dReal[3] = -dReal[0] - dReal[1] - dReal[2]; + std::array cs{}; + for (std::size_t i = 0; i < cs.size(); ++i) { + cs[i] = remEuclid((dReal[i] + dReal[3]) / 2.0, 2.0 * pi); + } + + // Reorder coordinates according to min(a, pi/2 - a) with + // a = x mod pi/2 for each Weyl coordinate x + std::array cstemp{}; + for (std::size_t i = 0; i < cs.size(); ++i) { + const auto tmp = remEuclid(cs[i], pi / 2.0); + cstemp[i] = std::min(tmp, (pi / 2.0) - tmp); + } + std::array order{0, 1, 2}; + std::stable_sort(order.begin(), order.end(), + [&](auto a, auto b) { return cstemp[a] < cstemp[b]; }); + order = {order[1], order[2], order[0]}; + cs = {cs[order[0]], cs[order[1]], cs[order[2]]}; + { + const std::array reordered{dReal[order[0]], dReal[order[1]], + dReal[order[2]]}; + dReal[0] = reordered[0]; + dReal[1] = reordered[1]; + dReal[2] = reordered[2]; + } + + // update eigenvectors (columns of p) according to new order of + // weyl coordinates + const Matrix4x4 pOrig = p; + for (std::size_t i = 0; i < order.size(); ++i) { + p.setColumn(i, pOrig.column(order[i])); + } + // apply correction for determinant if necessary + if (p.determinant().real() < 0.0) { + auto lastColumn = p.column(3); + for (auto& entry : lastColumn) { + entry = -entry; + } + p.setColumn(3, lastColumn); + } + 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. Since the matrix is diagonal, the matrix exponential is + // equivalent to the element-wise exponential function. + std::array tempDiag{}; + for (std::size_t k = 0; k < tempDiag.size(); ++k) { + tempDiag[k] = std::exp(1i * dReal[k]); + } + const Matrix4x4 temp = Matrix4x4::fromDiagonal(tempDiag); + + // combined matrix k1 of 1q gates after canonical gate + Matrix4x4 k1 = uP * p * temp; + // k1 must be orthogonal; the tolerance matches the iterative diagonalization + // residual rather than the (much tighter) default matrix tolerance. + assert((k1.transpose() * k1).isIdentity(SANITY_CHECK_PRECISION)); + assert(k1.determinant().real() > 0.0); + k1 = magicBasisTransform(k1, MagicBasisTransform::Into); + + // combined matrix k2 of 1q gates before canonical gate + Matrix4x4 k2 = p.adjoint(); + // k2 must be orthogonal; see the tolerance note on the k1 check above. + assert((k2.transpose() * k2).isIdentity(SANITY_CHECK_PRECISION)); + 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) + std::array tempConjDiag{}; + for (std::size_t k = 0; k < tempConjDiag.size(); ++k) { + tempConjDiag[k] = std::conj(tempDiag[k]); + } + assert((k1 * + magicBasisTransform(Matrix4x4::fromDiagonal(tempConjDiag), + MagicBasisTransform::Into) * + k2) + .isApprox(u, SANITY_CHECK_PRECISION)); + + // calculate k1 = K1l ⊗ K1r + auto [K1l, K1r, phaseL] = decomposeTwoQubitProductGate(k1); + // decompose k2 = K2l ⊗ K2r + auto [K2l, K2r, phaseR] = decomposeTwoQubitProductGate(k2); + assert(kron(K1l, K1r).isApprox(k1, SANITY_CHECK_PRECISION)); + assert(kron(K2l, K2r).isApprox(k2, SANITY_CHECK_PRECISION)); + // accumulate global phase + globalPhase += phaseL + phaseR; + + // Flip into Weyl chamber + if (cs[0] > (pi / 2.0)) { + cs[0] -= 3.0 * (pi / 2.0); + K1l = K1l * ipy(); + K1r = K1r * ipy(); + globalPhase += (pi / 2.0); + } + if (cs[1] > (pi / 2.0)) { + cs[1] -= 3.0 * (pi / 2.0); + K1l = K1l * ipx(); + K1r = K1r * ipx(); + globalPhase += (pi / 2.0); + } + auto conjs = 0; + if (cs[0] > (pi / 4.0)) { + cs[0] = (pi / 2.0) - cs[0]; + K1l = K1l * ipy(); + K2r = ipy() * K2r; + conjs += 1; + globalPhase -= (pi / 2.0); + } + if (cs[1] > (pi / 4.0)) { + cs[1] = (pi / 2.0) - cs[1]; + K1l = K1l * ipx(); + K2r = ipx() * K2r; + conjs += 1; + globalPhase += (pi / 2.0); + if (conjs == 1) { + globalPhase -= pi; + } + } + if (cs[2] > (pi / 2.0)) { + cs[2] -= 3.0 * (pi / 2.0); + K1l = K1l * ipz(); + K1r = K1r * ipz(); + globalPhase += (pi / 2.0); + if (conjs == 1) { + globalPhase -= pi; + } + } + if (conjs == 1) { + cs[2] = (pi / 2.0) - cs[2]; + K1l = K1l * ipz(); + K2r = ipz() * K2r; + globalPhase += (pi / 2.0); + } + if (cs[2] > (pi / 4.0)) { + cs[2] -= (pi / 2.0); + K1l = K1l * ipz(); + K1r = K1r * ipz(); + globalPhase -= (pi / 2.0); + } + + // 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.requestedFidelity = fidelity; + + // make sure decomposition is equal to input + assert((kron(K1l, K1r) * decomposition.getCanonicalMatrix() * kron(K2l, K2r) * + 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( + (pi / 2.0) - 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(); + const double calculatedFidelity = 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 && + 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})!", + calculatedFidelity, *decomposition.requestedFidelity)); + } + decomposition.globalPhase_ += std::arg(trace); + + // final check if decomposition is still valid after specialization + assert((kron(decomposition.k1l_, decomposition.k1r_) * + decomposition.getCanonicalMatrix() * + kron(decomposition.k2l_, decomposition.k2r_) * + globalPhaseFactor(decomposition.globalPhase_)) + .isApprox(unitaryMatrix, SANITY_CHECK_PRECISION)); + + return decomposition; +} + +Matrix4x4 TwoQubitWeylDecomposition::getCanonicalMatrix(double a, double b, + double c) { + // Canonical gate `U_d(a, b, c) = exp(i * (a*XX + b*YY + c*ZZ))`. XX/YY/ZZ + // commute pairwise, so any product order is equivalent; the order below is + // chosen to match common Qiskit/QuantumFlow references. The negated rotation + // angles (`-2 * a`, ...) compensate for the `RXX/RYY/RZZ` convention + // `exp(-i * theta/2 * XX)`, so that the factored angles sum back to the + // intended `+a`, `+b`, `+c`. + const auto xx = rxxMatrix(-2.0 * a); + const auto yy = ryyMatrix(-2.0 * b); + const auto zz = rzzMatrix(-2.0 * c); + return zz * yy * xx; +} + +Matrix4x4 +TwoQubitWeylDecomposition::magicBasisTransform(const Matrix4x4& unitary, + MagicBasisTransform direction) { + // Makhlin "magic basis" transform. Conjugating a 2-qubit unitary by + // `bNonNormalized` maps SU(2) x SU(2) factors onto SO(4) and diagonalizes + // the canonical (Weyl) gate. The matrices are stored unnormalized: the + // `1/2` pre-factor that would normally appear in `B^dagger` is absorbed + // into `bNonNormalizedDagger` directly so the product `Bd * B == I` + // without an extra scalar. + const Matrix4x4 bNonNormalized = Matrix4x4::fromElements( // + 1, 1i, 0, 0, // + 0, 0, 1i, 1, // + 0, 0, 1i, -1, // + 1, -1i, 0, 0); + const Matrix4x4 bNonNormalizedDagger = Matrix4x4::fromElements( // + 0.5, 0, 0, 0.5, // + Complex{0.0, -0.5}, 0, 0, Complex{0.0, 0.5}, // + 0, Complex{0.0, -0.5}, Complex{0.0, -0.5}, 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.); +} + +std::pair> +TwoQubitWeylDecomposition::diagonalizeComplexSymmetric(const Matrix4x4& 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; + + const auto mReal = m.realPart(); + const auto mImag = m.imagPart(); + + double bestErr = 1e300; + 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); + } + std::array m2Real{}; + for (std::size_t k = 0; k < m2Real.size(); ++k) { + m2Real[k] = (randA * mReal[k]) + (randB * mImag[k]); + } + const Matrix4x4 p = jacobiSymmetricEigen(m2Real).eigenvectors; + const std::array d = (p.transpose() * m * p).diagonal(); + + const auto compare = p * Matrix4x4::fromDiagonal(d) * p.transpose(); + { + double err = 0.0; + for (std::size_t r = 0; r < 4; ++r) { + for (std::size_t cc = 0; cc < 4; ++cc) { + err = std::max(err, std::abs(compare(r, cc) - m(r, cc))); + } + } + bestErr = std::min(bestErr, err); + } + if (compare.isApprox(m, precision)) { + // p are the eigenvectors which are decomposed into the + // single-qubit gates surrounding the canonical gate + // d is the sqrt of the eigenvalues that are used to determine the + // weyl coordinates and thus the parameters of the canonical gate + // check that p is in SO(4) + assert((p.transpose() * p).isIdentity(SANITY_CHECK_PRECISION)); + // make sure determinant of eigenvalues is 1.0 + assert(std::abs(Matrix4x4::fromDiagonal(d).determinant() - 1.0) < + SANITY_CHECK_PRECISION); + return std::make_pair(p, d); + } + } + llvm::reportFatalInternalError(llvm::formatv( + "TwoQubitWeylDecomposition: failed to diagonalize M2 ({0} iterations). " + "best error = {1:e}, precision = {2:e}", + maxDiagonalizationAttempts, bestErr, precision)); +} + +std::tuple +TwoQubitWeylDecomposition::decomposeTwoQubitProductGate( + const Matrix4x4& specialUnitary) { + // for alternative approaches, see + // pennylane's math.decomposition.su2su2_to_tensor_products + // or quantumflow.kronecker_decomposition + + // first quadrant + Matrix2x2 r = + Matrix2x2::fromElements(specialUnitary(0, 0), specialUnitary(0, 1), + specialUnitary(1, 0), specialUnitary(1, 1)); + auto detR = r.determinant(); + if (std::abs(detR) < 0.1) { + // third quadrant + r = Matrix2x2::fromElements(specialUnitary(2, 0), specialUnitary(2, 1), + specialUnitary(3, 0), specialUnitary(3, 1)); + detR = r.determinant(); + } + if (std::abs(detR) < 0.1) { + llvm::reportFatalInternalError( + "decomposeTwoQubitProductGate: unable to decompose: det_r < 0.1"); + } + r *= (1.0 / std::sqrt(detR)); + // transpose with complex conjugate of each element + const Matrix2x2 rTConj = r.adjoint(); + + Matrix4x4 temp = specialUnitary * kron(Matrix2x2::identity(), rTConj); + + // [[a, b, c, d], + // [e, f, g, h], => [[a, c], + // [i, j, k, l], [i, k]] + // [m, n, o, p]] + Matrix2x2 l = + Matrix2x2::fromElements(temp(0, 0), temp(0, 2), temp(2, 0), temp(2, 2)); + auto detL = l.determinant(); + if (std::abs(detL) < 0.9) { + llvm::reportFatalInternalError( + "decomposeTwoQubitProductGate: unable to decompose: detL < 0.9"); + } + l *= (1.0 / 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) { + // Closed-form Hilbert-Schmidt overlap `tr(U_d(a,b,c)^dag * U_d(ap,bp,cp))` + // between two canonical (Weyl) gates, expressed in terms of the coordinate + // differences. Feeding the result into `traceToFidelity` gives the average + // two-qubit gate fidelity between the two canonical gates, which + // `bestSpecialization` uses to rank candidate specializations. + // Reference: Zhang et al., "Geometric theory of nonlocal two-qubit + // operations", Phys. Rev. A 67, 042313 (2003), Eq. (20). + 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 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((std::numbers::pi / 4.0), (std::numbers::pi / 4.0), + (std::numbers::pi / 4.0)) || + isClose((std::numbers::pi / 4.0), (std::numbers::pi / 4.0), + -(std::numbers::pi / 4.0))) { + 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((std::numbers::pi / 4.0), (std::numbers::pi / 4.0), 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_ = Matrix2x2::identity(); + k1r_ = k1r_ * k2r_; + k2r_ = Matrix2x2::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_ = Matrix2x2::identity(); + k2r_ = Matrix2x2::identity(); + } else { + flippedFromOriginal = true; + + globalPhase_ += (std::numbers::pi / 2.0); + k1l_ = k1l_ * ipz() * k2r_; + k1r_ = k1r_ * ipz() * k2l_; + k2l_ = Matrix2x2::identity(); + k2r_ = Matrix2x2::identity(); + } + a_ = (std::numbers::pi / 4.0); + b_ = (std::numbers::pi / 4.0); + c_ = (std::numbers::pi / 4.0); + } 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_.adjoint(); + + a_ = closest; + b_ = closest; + c_ = closest; + // unmodified global phase + k1l_ = k1l_ * k2l_; + k1r_ = k1r_ * k2l_; + k2r_ = k2lDagger * k2r_; + k2l_ = Matrix2x2::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_.adjoint(); + + a_ = closest; + b_ = closest; + c_ = -closest; + // unmodified global phase + k1l_ = k1l_ * k2l_; + k1r_ = k1r_ * ipz() * k2l_ * ipz(); + k2r_ = ipz() * k2lDagger * ipz() * k2r_; + k2l_ = Matrix2x2::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)` + const EulerBasis eulerBasis = EulerBasis::XYX; + const auto [k2ltheta, k2lphi, k2llambda, k2lphase] = + anglesFromUnitary(k2l_, eulerBasis); + const auto [k2rtheta, k2rphi, k2rlambda, k2rphase] = + anglesFromUnitary(k2r_, EulerBasis::XYX); + // 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); + } 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)` + const auto [k2ltheta, k2lphi, k2llambda, k2lphase] = + anglesFromUnitary(k2l_, EulerBasis::ZYZ); + const auto [k2rtheta, k2rphi, k2rlambda, k2rphase] = + anglesFromUnitary(k2r_, EulerBasis::ZYZ); + a_ = (std::numbers::pi / 4.0); + b_ = (std::numbers::pi / 4.0); + // 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] = + 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] = + 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_; + } 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] = + 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_; + } else { + llvm::reportFatalInternalError( + "Unknown specialization for Weyl decomposition!"); + } + return flippedFromOriginal; +} + +TwoQubitBasisDecomposer +TwoQubitBasisDecomposer::create(const Matrix4x4& basisMatrix, + double basisFidelity) { + const Matrix2x2 k12RArr = Matrix2x2::fromElements( + 1i * FRAC1_SQRT2, FRAC1_SQRT2, -FRAC1_SQRT2, -1i * FRAC1_SQRT2); + const Matrix2x2 k12LArr = + Matrix2x2::fromElements(Complex{0.5, 0.5}, Complex{0.5, 0.5}, + Complex{-0.5, 0.5}, Complex{0.5, -0.5}); + + // The Shende-Markov-Bullock 3-CX sandwich (and its 1/2-CX reductions) used + // below is derived for a basis CX whose 4x4 matrix is the Qiskit/LSB form + // `[[1,0,0,0],[0,0,0,1],[0,0,1,0],[0,1,0,0]]`, i.e. "control on the LSB + // factor, target on the MSB factor" of the tensor product. MQT's wider + // convention places operand 0 on the MSB factor, so the CX/CZ matrix for + // control-on-wire-0 gives the SWAP-conjugate + // `[[1,0,0,0],[0,1,0,0],[0,0,0,1],[0,0,1,0]]`. + // + // Because `SWAP * C(a,b,c) * SWAP = C(a,b,c)` but + // `SWAP * (K1l ⊗ K1r) * SWAP = (K1r ⊗ K1l)`, feeding the MSB matrix directly + // into the Weyl decomposer would swap the roles of `k1l`/`k1r` (and `k2l`/ + // `k2r`) relative to the hard-coded constants above. To keep the SMB algebra + // self-consistent we SWAP-conjugate the basis matrix here (restoring the + // Qiskit/LSB 4x4) and then absorb the resulting "left/right" relabeling at + // the emission boundary in `decomp{0,1,2,3}` below. This reproduces the + // pre-flip gate counts without having to re-derive every SMB constant for + // the MSB basis -- the two routes are algebraically equivalent. + const Matrix4x4 basisMatrixLsb = swapGate() * basisMatrix * swapGate(); + const auto basisDecomposer = + TwoQubitWeylDecomposition::create(basisMatrixLsb, basisFidelity); + const auto isSuperControlled = + relativeEq(basisDecomposer.a(), std::numbers::pi / 4.0, 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(); + Complex temp{0.5, -0.5}; + const Matrix2x2 k11l = Matrix2x2::fromElements( + temp * (-1i * std::exp(-1i * b)), temp * std::exp(-1i * b), + temp * (-1i * std::exp(1i * b)), temp * -std::exp(1i * b)); + const Matrix2x2 k11r = Matrix2x2::fromElements( + 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 Matrix2x2 k32lK21l = + Matrix2x2::fromElements(FRAC1_SQRT2 * Complex{1., std::cos(2. * b)}, + FRAC1_SQRT2 * (1i * std::sin(2. * b)), + FRAC1_SQRT2 * (1i * std::sin(2. * b)), + FRAC1_SQRT2 * Complex{1., -std::cos(2. * b)}); + temp = Complex{0.5, 0.5}; + const Matrix2x2 k21r = Matrix2x2::fromElements( + temp * (-1i * std::exp(-2i * b)), temp * std::exp(-2i * b), + temp * (1i * std::exp(2i * b)), temp * std::exp(2i * b)); + const Matrix2x2 k22l = Matrix2x2::fromElements(FRAC1_SQRT2, -FRAC1_SQRT2, + FRAC1_SQRT2, FRAC1_SQRT2); + const Matrix2x2 k22r = Matrix2x2::fromElements(0, 1, -1, 0); + const Matrix2x2 k31l = Matrix2x2::fromElements( + 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 Matrix2x2 k31r = Matrix2x2::fromElements(1i * std::exp(1i * b), 0, 0, + -1i * std::exp(-1i * b)); + const Matrix2x2 k32r = Matrix2x2::fromElements( + 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().adjoint(); + auto k1rDagger = basisDecomposer.k1r().adjoint(); + auto k2lDagger = basisDecomposer.k2l().adjoint(); + auto k2rDagger = basisDecomposer.k2r().adjoint(); + // 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.adjoint() * k1lDagger; + auto q0r = k12RArr.adjoint() * ipz() * k1rDagger; + auto q1la = k2lDagger * k11l.adjoint(); + auto q1lb = k11l * k1lDagger; + auto q1ra = k2rDagger * ipz() * k11r.adjoint(); + auto q1rb = k11r * k1rDagger; + auto q2l = k2lDagger * k12LArr; + auto q2r = k2rDagger * k12RArr; + + return TwoQubitBasisDecomposer{ + 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 TwoQubitWeylDecomposition& targetDecomposition, + std::optional numBasisGateUses) const { + auto traces = this->traces(targetDecomposition); + auto getDefaultNbasis = [&]() -> std::uint8_t { + // Pick the number of basis gate uses `i ∈ {0, 1, 2, 3}` that maximizes + // expected_fidelity(i) = traceToFidelity(traces[i]) * basisFidelity^i. + auto bestValue = std::numeric_limits::lowest(); + auto bestIndex = -1; + for (int i = 0; std::cmp_less(i, traces.size()); ++i) { + auto value = traceToFidelity(traces[i]) * std::pow(basisFidelity, i); + if (std::isnan(value)) { + continue; + } + if (value > bestValue) { + bestIndex = i; + bestValue = value; + } + } + if (bestIndex < 0) { + llvm::reportFatalInternalError("Unable to select basis-gate count: all " + "candidate fidelities are NaN"); + } + 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(""); + }; + TwoQubitLocalUnitaryList factors = chooseDecomposition(); +#ifndef NDEBUG + for (const auto& factor : factors) { + assert(isUnitaryMatrix(factor)); + } +#endif + + double globalPhase = targetDecomposition.globalPhase(); + globalPhase -= bestNbasis * basisDecomposer.globalPhase(); + if (bestNbasis == 2) { + // The two-basis (2x CX/CZ) template in `decomp2Supercontrolled` produces + // a sequence whose global phase is off by `pi` relative to the target; + // compensate here so the emitted sequence reproduces the target unitary + // exactly, not just up to sign. + globalPhase += std::numbers::pi; + } + // large global phases can be generated by the decomposition, thus limit + // it to [0, +2*pi) + globalPhase = remEuclid(globalPhase, 2.0 * std::numbers::pi); + + return TwoQubitNativeDecomposition{ + .numBasisUses = bestNbasis, + .singleQubitFactors = std::move(factors), + .globalPhase = globalPhase, + }; +} + +// Ported SMB helpers assume Qiskit Weyl k-factor layout; QCO 4x4 input order +// swaps l/r vs that port. Swap k1l<->k1r and k2l<->k2r when reading ``target``, +// and swap adjacent pairs in each return vector so the emission boundary maps +// matrices to the same wires as the upstream decomposer. ``decomp0`` cancels to +// the unswapped formula. +TwoQubitLocalUnitaryList +TwoQubitBasisDecomposer::decomp0(const TwoQubitWeylDecomposition& target) { + return TwoQubitLocalUnitaryList{ + target.k1r() * target.k2r(), + target.k1l() * target.k2l(), + }; +} + +TwoQubitLocalUnitaryList TwoQubitBasisDecomposer::decomp1( + const TwoQubitWeylDecomposition& target) const { + // may not work for z != 0 and c != 0 (not always in Weyl chamber) + return TwoQubitLocalUnitaryList{ + basisDecomposer.k2l().adjoint() * target.k2r(), + basisDecomposer.k2r().adjoint() * target.k2l(), + target.k1r() * basisDecomposer.k1l().adjoint(), + target.k1l() * basisDecomposer.k1r().adjoint(), + }; +} + +TwoQubitLocalUnitaryList 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 TwoQubitLocalUnitaryList{ + q2l * target.k2r(), + q2r * target.k2l(), + q1la * rzMatrix(-2. * target.a()) * q1lb, + q1ra * rzMatrix(2. * target.b()) * q1rb, + target.k1r() * q0l, + target.k1l() * q0r, + }; +} + +TwoQubitLocalUnitaryList 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 TwoQubitLocalUnitaryList{ + u3l * target.k2r(), + u3r * target.k2l(), + u2la * rzMatrix(-2. * target.a()) * u2lb, + u2ra * rzMatrix(2. * target.b()) * u2rb, + u1l, + u1ra * rzMatrix(-2. * target.c()) * u1rb, + target.k1r() * u0l, + target.k1l() * u0r, + }; +} + +std::array, 4> +TwoQubitBasisDecomposer::traces(const TwoQubitWeylDecomposition& target) const { + // Returns the Hilbert-Schmidt traces between the target canonical gate and + // the best candidate reachable with `0, 1, 2, 3` uses of the basis gate, + // respectively. Fed into `traceToFidelity` by `getDefaultNbasis` to pick + // the best basis-gate count. The closed-form expressions specialize + // `TwoQubitWeylDecomposition::getTrace(a, b, c, ap, bp, cp)` for: + // i == 0: no basis gate (ap == bp == cp == 0) + // i == 1: one basis use (ap == pi/4, bp == basis.b, cp == 0) + // i == 2: two basis uses (ap == 0, bp == 0, cp == -target.c) + // i == 3: three basis uses (target reachable exactly -> trace == 4) + // so the array has length 4 and is indexed by the number of basis uses. + 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((std::numbers::pi / 4.0) - target.a()) * + std::cos(basisDecomposer.b() - target.b()) * + std::cos(target.c()), + std::sin((std::numbers::pi / 4.0) - target.a()) * + std::sin(basisDecomposer.b() - target.b()) * + std::sin(target.c())}, + std::complex{4. * std::cos(target.c()), 0.}, + std::complex{4., 0.}, + }; +} + +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; +} + +//===----------------------------------------------------------------------===// +// Native-spec parsing and two-qubit synthesis +//===----------------------------------------------------------------------===// + +namespace { + +constexpr double PI = std::numbers::pi; + +[[nodiscard]] static std::optional +parseGateToken(llvm::StringRef name) { + return llvm::StringSwitch>(name) + .Case("u", NativeGateKind::U) + .Case("x", NativeGateKind::X) + .Case("sx", NativeGateKind::Sx) + .Cases("rz", "p", NativeGateKind::Rz) + .Case("rx", NativeGateKind::Rx) + .Case("ry", NativeGateKind::Ry) + .Case("r", NativeGateKind::R) + .Case("cx", NativeGateKind::Cx) + .Case("cz", NativeGateKind::Cz) + .Case("rzz", NativeGateKind::Rzz) + .Default(std::nullopt); +} + +[[nodiscard]] static std::optional> +parseGateSet(llvm::StringRef nativeGates) { + llvm::DenseSet gates; + llvm::SmallVector parts; + nativeGates.split(parts, ',', /*MaxSplit=*/-1, /*KeepEmpty=*/false); + for (llvm::StringRef part : parts) { + const auto token = part.trim().lower(); + if (token.empty()) { + continue; + } + const auto gate = parseGateToken(token); + if (!gate) { + return std::nullopt; + } + gates.insert(*gate); + } + return gates; +} + +[[nodiscard]] static SingleQubitEmitterSpec +makeEmitterSpec(SingleQubitMode mode, AxisPair axisPair = AxisPair::RxRz, + bool supportsDirectRx = false) { + return { + .mode = mode, .axisPair = axisPair, .supportsDirectRx = supportsDirectRx}; +} + +static void +addEmitterIfAbsent(llvm::SmallVectorImpl& emitters, + SingleQubitMode mode, AxisPair axisPair = AxisPair::RxRz, + bool supportsDirectRx = false) { + const bool present = llvm::any_of(emitters, [&](const auto& e) { + return e.mode == mode && e.axisPair == axisPair && + e.supportsDirectRx == supportsDirectRx; + }); + if (!present) { + emitters.push_back(makeEmitterSpec(mode, axisPair, supportsDirectRx)); + } +} + +[[nodiscard]] static llvm::SmallVector +allowedGatesForEmitter(const SingleQubitEmitterSpec& emitter) { + switch (emitter.mode) { + case SingleQubitMode::ZSXX: { + llvm::SmallVector gates{ + NativeGateKind::X, NativeGateKind::Sx, NativeGateKind::Rz}; + if (emitter.supportsDirectRx) { + gates.push_back(NativeGateKind::Rx); + } + return gates; + } + case SingleQubitMode::U3: + return {NativeGateKind::U}; + case SingleQubitMode::R: + return {NativeGateKind::R}; + case SingleQubitMode::AxisPair: + switch (emitter.axisPair) { + case AxisPair::RxRz: + return {NativeGateKind::Rx, NativeGateKind::Rz}; + case AxisPair::RxRy: + return {NativeGateKind::Rx, NativeGateKind::Ry}; + case AxisPair::RyRz: + return {NativeGateKind::Ry, NativeGateKind::Rz}; + } + break; + } + llvm_unreachable("unknown single-qubit mode"); +} + +[[nodiscard]] static llvm::SmallVector +allowedGatesForEntangler(EntanglerBasis entangler) { + switch (entangler) { + case EntanglerBasis::None: + return {}; + case EntanglerBasis::Cx: + return {NativeGateKind::Cx}; + case EntanglerBasis::Cz: + return {NativeGateKind::Cz}; + } + llvm_unreachable("unknown entangler basis"); +} + +static void populateAllowedGates(NativeProfileSpec& spec) { + spec.allowedGates.clear(); + for (const auto& emitter : spec.singleQubitEmitters) { + const auto allowed = allowedGatesForEmitter(emitter); + spec.allowedGates.insert(allowed.begin(), allowed.end()); + } + for (const auto entangler : spec.entanglerBases) { + const auto allowed = allowedGatesForEntangler(entangler); + spec.allowedGates.insert(allowed.begin(), allowed.end()); + } + if (spec.allowRzz) { + spec.allowedGates.insert(NativeGateKind::Rzz); + } +} + +[[nodiscard]] static EulerBasis eulerBasisForAxisPair(AxisPair axisPair) { + switch (axisPair) { + case AxisPair::RxRz: + return EulerBasis::XZX; + case AxisPair::RxRy: + return EulerBasis::XYX; + case AxisPair::RyRz: + return EulerBasis::ZYZ; + } + llvm_unreachable("unknown axis pair"); +} + +[[nodiscard]] static EulerBasis +emitterEulerBasis(const SingleQubitEmitterSpec& emitter) { + switch (emitter.mode) { + case SingleQubitMode::ZSXX: + return EulerBasis::ZSXX; + case SingleQubitMode::U3: + return EulerBasis::U; + case SingleQubitMode::R: + return EulerBasis::R; + case SingleQubitMode::AxisPair: + return eulerBasisForAxisPair(emitter.axisPair); + } + llvm_unreachable("unknown single-qubit mode"); +} + +[[nodiscard]] static bool usesCxEntangler(const NativeProfileSpec& spec) { + return llvm::is_contained(spec.entanglerBases, EntanglerBasis::Cx); +} + +[[nodiscard]] static bool usesCzEntangler(const NativeProfileSpec& spec) { + return llvm::is_contained(spec.entanglerBases, EntanglerBasis::Cz); +} + +[[nodiscard]] static std::optional +selectEntangler(const NativeProfileSpec& spec) { + if (usesCxEntangler(spec)) { + return EntanglerBasis::Cx; + } + if (usesCzEntangler(spec)) { + return EntanglerBasis::Cz; + } + return std::nullopt; +} + +[[nodiscard]] static Matrix4x4 entanglerMatrix(EntanglerBasis entangler) { + return entangler == EntanglerBasis::Cz ? czGate() : cxGate01(); +} + +[[nodiscard]] static std::optional +decomposeWithEntangler(const Matrix4x4& target, EntanglerBasis entangler) { + auto decomposer = + TwoQubitBasisDecomposer::create(entanglerMatrix(entangler), 1.0); + auto weyl = TwoQubitWeylDecomposition::create(target, std::nullopt); + return decomposer.twoQubitDecompose(weyl, std::nullopt); +} + +static void emitGPhaseIfNonTrivial(OpBuilder& builder, Location loc, + double phase) { + constexpr double epsilon = 1e-12; + if (std::abs(phase) > epsilon) { + GPhaseOp::create(builder, loc, phase); + } +} + +[[nodiscard]] static Value emitSingleQubitMatrix(OpBuilder& builder, + Location loc, Value inQubit, + const Matrix2x2& matrix, + EulerBasis basis) { + return *synthesizeUnitary1QEuler(builder, loc, inQubit, matrix, + /*runSize=*/0, /*hasNonBasisGate=*/true, + basis); +} + +} // namespace + +std::optional parseNativeSpec(llvm::StringRef nativeGates) { + const auto gates = parseGateSet(nativeGates); + if (!gates || gates->empty()) { + return std::nullopt; + } + const auto has = [&](NativeGateKind kind) { return gates->contains(kind); }; + + NativeProfileSpec spec; + + if (has(NativeGateKind::U)) { + addEmitterIfAbsent(spec.singleQubitEmitters, SingleQubitMode::U3); + } + const bool hasXSxRz = has(NativeGateKind::X) && has(NativeGateKind::Sx) && + has(NativeGateKind::Rz); + if (hasXSxRz) { + addEmitterIfAbsent(spec.singleQubitEmitters, SingleQubitMode::ZSXX, + AxisPair::RxRz, + /*supportsDirectRx=*/has(NativeGateKind::Rx)); + } + if (has(NativeGateKind::R)) { + addEmitterIfAbsent(spec.singleQubitEmitters, SingleQubitMode::R); + } + struct AxisPairRule { + AxisPair axis; + NativeGateKind left; + NativeGateKind right; + }; + for (const auto& rule : { + AxisPairRule{.axis = AxisPair::RxRz, + .left = NativeGateKind::Rx, + .right = NativeGateKind::Rz}, + AxisPairRule{.axis = AxisPair::RxRy, + .left = NativeGateKind::Rx, + .right = NativeGateKind::Ry}, + AxisPairRule{.axis = AxisPair::RyRz, + .left = NativeGateKind::Ry, + .right = NativeGateKind::Rz}, + }) { + if (has(rule.left) && has(rule.right)) { + addEmitterIfAbsent(spec.singleQubitEmitters, SingleQubitMode::AxisPair, + rule.axis); + } + } + if (spec.singleQubitEmitters.empty()) { + return std::nullopt; + } + + if (has(NativeGateKind::Cx)) { + spec.entanglerBases.push_back(EntanglerBasis::Cx); + } + if (has(NativeGateKind::Cz)) { + spec.entanglerBases.push_back(EntanglerBasis::Cz); + } + spec.allowRzz = has(NativeGateKind::Rzz); + + populateAllowedGates(spec); + return spec; +} + +LogicalResult synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, + Value qubit0, Value qubit1, + const Matrix4x4& target, + const NativeProfileSpec& spec, + Value& outQubit0, Value& outQubit1) { + const auto entangler = selectEntangler(spec); + if (!entangler) { + return failure(); + } + const auto native = decomposeWithEntangler(target, *entangler); + if (!native) { + return failure(); + } + const auto basis = emitterEulerBasis(spec.singleQubitEmitters.front()); + + emitGPhaseIfNonTrivial(builder, loc, native->globalPhase); + + Value wire0 = qubit0; + Value wire1 = qubit1; + const auto& factors = native->singleQubitFactors; + const std::uint8_t numBasisUses = native->numBasisUses; + const auto emitFactor = [&](Value& wire, std::size_t index) { + wire = emitSingleQubitMatrix(builder, loc, wire, factors[index], basis); + }; + const auto emitEntangler = [&]() { + auto ctrlOp = CtrlOp::create( + builder, loc, ValueRange{wire0}, ValueRange{wire1}, + [&](ValueRange targetArgs) -> llvm::SmallVector { + if (*entangler == EntanglerBasis::Cz) { + return {ZOp::create(builder, loc, targetArgs[0]).getOutputQubit(0)}; + } + return {XOp::create(builder, loc, targetArgs[0]).getOutputQubit(0)}; + }); + wire0 = ctrlOp.getOutputControl(0); + wire1 = ctrlOp.getOutputTarget(0); + }; + + for (std::uint8_t i = 0; i < numBasisUses; ++i) { + emitFactor(wire1, static_cast(2 * i)); + emitFactor(wire0, static_cast((2 * i) + 1)); + emitEntangler(); + } + emitFactor(wire1, static_cast(2 * numBasisUses)); + emitFactor(wire0, static_cast((2 * numBasisUses) + 1)); + + outQubit0 = wire0; + outQubit1 = wire1; + return success(); +} + +std::optional +twoQubitEntanglerCount(const Matrix4x4& target, const NativeProfileSpec& spec) { + const auto entangler = selectEntangler(spec); + if (!entangler) { + return std::nullopt; + } + const auto native = decomposeWithEntangler(target, *entangler); + if (!native) { + return std::nullopt; + } + return native->numBasisUses; +} + +} // namespace mlir::qco::decomposition diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.cpp deleted file mode 100644 index 4a1c5798e9..0000000000 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.cpp +++ /dev/null @@ -1,710 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "mlir/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.h" - -#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" -#include "mlir/Dialect/QCO/Utils/Matrix.h" - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace mlir::qco::decomposition { - -using namespace std::complex_literals; - -TwoQubitWeylDecomposition -TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, - std::optional fidelity) { - auto u = unitaryMatrix; - auto detU = u.determinant(); - // Project into SU(4) by dividing out the fourth root of det(U): for a 4x4 - // unitary, |det(U)| == 1 so `det^{-1/4}` both enforces det == 1 and removes - // the global phase. The extracted phase is tracked separately in - // `globalPhase` (quarter of arg(det) to match the fourth-root choice) so the - // caller can reconstruct the original matrix exactly if needed. - auto detPow = std::pow(detU, -0.25); - u *= detPow; // remove global phase from unitary matrix - auto globalPhase = std::arg(detU) / 4.; - - // Numerical drift can still leave tiny determinant errors after root - // normalization. Re-normalize once more instead of aborting. - auto detNormalized = u.determinant(); - if (std::abs(detNormalized - Complex{1.0, 0.0}) > SANITY_CHECK_PRECISION && - std::abs(detNormalized) > SANITY_CHECK_PRECISION) { - u *= std::pow(detNormalized, -0.25); - } - - // transform unitary matrix to magic basis; this enables two properties: - // 1. if uP ∈ SO(4), V = A ⊗ B (SO(4) → SU(2) ⊗ SU(2)) - // 2. magic basis diagonalizes canonical gate, allowing calculation of - // canonical gate parameters later on - auto uP = magicBasisTransform(u, MagicBasisTransform::OutOf); - const Matrix4x4 m2 = uP.transpose() * uP; - - // diagonalization yields eigenvectors (p) and eigenvalues (d); - // p is used to calculate K1/K2 (and thus the single-qubit gates - // surrounding the canonical gate); d is 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) - constexpr double pi = std::numbers::pi; - std::array dReal{}; - for (std::size_t i = 0; i < d.size(); ++i) { - dReal[i] = -std::arg(d[i]) / 2.0; - } - dReal[3] = -dReal[0] - dReal[1] - dReal[2]; - std::array cs{}; - for (std::size_t i = 0; i < cs.size(); ++i) { - cs[i] = helpers::remEuclid((dReal[i] + dReal[3]) / 2.0, 2.0 * pi); - } - - // Reorder coordinates according to min(a, pi/2 - a) with - // a = x mod pi/2 for each Weyl coordinate x - std::array cstemp{}; - for (std::size_t i = 0; i < cs.size(); ++i) { - const auto tmp = helpers::remEuclid(cs[i], pi / 2.0); - cstemp[i] = std::min(tmp, (pi / 2.0) - tmp); - } - std::array order{0, 1, 2}; - std::stable_sort(order.begin(), order.end(), - [&](auto a, auto b) { return cstemp[a] < cstemp[b]; }); - order = {order[1], order[2], order[0]}; - cs = {cs[order[0]], cs[order[1]], cs[order[2]]}; - { - const std::array reordered{dReal[order[0]], dReal[order[1]], - dReal[order[2]]}; - dReal[0] = reordered[0]; - dReal[1] = reordered[1]; - dReal[2] = reordered[2]; - } - - // update eigenvectors (columns of p) according to new order of - // weyl coordinates - const Matrix4x4 pOrig = p; - for (std::size_t i = 0; i < order.size(); ++i) { - p.setColumn(i, pOrig.column(order[i])); - } - // apply correction for determinant if necessary - if (p.determinant().real() < 0.0) { - auto lastColumn = p.column(3); - for (auto& entry : lastColumn) { - entry = -entry; - } - p.setColumn(3, lastColumn); - } - 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. Since the matrix is diagonal, the matrix exponential is - // equivalent to the element-wise exponential function. - std::array tempDiag{}; - for (std::size_t k = 0; k < tempDiag.size(); ++k) { - tempDiag[k] = std::exp(1i * dReal[k]); - } - const Matrix4x4 temp = Matrix4x4::fromDiagonal(tempDiag); - - // combined matrix k1 of 1q gates after canonical gate - Matrix4x4 k1 = uP * p * temp; - // k1 must be orthogonal; the tolerance matches the iterative diagonalization - // residual rather than the (much tighter) default matrix tolerance. - assert((k1.transpose() * k1).isIdentity(SANITY_CHECK_PRECISION)); - assert(k1.determinant().real() > 0.0); - k1 = magicBasisTransform(k1, MagicBasisTransform::Into); - - // combined matrix k2 of 1q gates before canonical gate - Matrix4x4 k2 = p.adjoint(); - // k2 must be orthogonal; see the tolerance note on the k1 check above. - assert((k2.transpose() * k2).isIdentity(SANITY_CHECK_PRECISION)); - 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) - std::array tempConjDiag{}; - for (std::size_t k = 0; k < tempConjDiag.size(); ++k) { - tempConjDiag[k] = std::conj(tempDiag[k]); - } - assert((k1 * - magicBasisTransform(Matrix4x4::fromDiagonal(tempConjDiag), - MagicBasisTransform::Into) * - k2) - .isApprox(u, SANITY_CHECK_PRECISION)); - - // calculate k1 = K1l ⊗ K1r - auto [K1l, K1r, phaseL] = decomposeTwoQubitProductGate(k1); - // decompose k2 = K2l ⊗ K2r - auto [K2l, K2r, phaseR] = decomposeTwoQubitProductGate(k2); - assert(kron(K1l, K1r).isApprox(k1, SANITY_CHECK_PRECISION)); - assert(kron(K2l, K2r).isApprox(k2, SANITY_CHECK_PRECISION)); - // accumulate global phase - globalPhase += phaseL + phaseR; - - // Flip into Weyl chamber - if (cs[0] > (pi / 2.0)) { - cs[0] -= 3.0 * (pi / 2.0); - K1l = K1l * ipy(); - K1r = K1r * ipy(); - globalPhase += (pi / 2.0); - } - if (cs[1] > (pi / 2.0)) { - cs[1] -= 3.0 * (pi / 2.0); - K1l = K1l * ipx(); - K1r = K1r * ipx(); - globalPhase += (pi / 2.0); - } - auto conjs = 0; - if (cs[0] > (pi / 4.0)) { - cs[0] = (pi / 2.0) - cs[0]; - K1l = K1l * ipy(); - K2r = ipy() * K2r; - conjs += 1; - globalPhase -= (pi / 2.0); - } - if (cs[1] > (pi / 4.0)) { - cs[1] = (pi / 2.0) - cs[1]; - K1l = K1l * ipx(); - K2r = ipx() * K2r; - conjs += 1; - globalPhase += (pi / 2.0); - if (conjs == 1) { - globalPhase -= pi; - } - } - if (cs[2] > (pi / 2.0)) { - cs[2] -= 3.0 * (pi / 2.0); - K1l = K1l * ipz(); - K1r = K1r * ipz(); - globalPhase += (pi / 2.0); - if (conjs == 1) { - globalPhase -= pi; - } - } - if (conjs == 1) { - cs[2] = (pi / 2.0) - cs[2]; - K1l = K1l * ipz(); - K2r = ipz() * K2r; - globalPhase += (pi / 2.0); - } - if (cs[2] > (pi / 4.0)) { - cs[2] -= (pi / 2.0); - K1l = K1l * ipz(); - K1r = K1r * ipz(); - globalPhase -= (pi / 2.0); - } - - // 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.requestedFidelity = fidelity; - - // make sure decomposition is equal to input - assert((kron(K1l, K1r) * decomposition.getCanonicalMatrix() * kron(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( - (pi / 2.0) - 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(); - const double 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 && - 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})!", - calculatedFidelity, *decomposition.requestedFidelity)); - } - decomposition.globalPhase_ += std::arg(trace); - - // final check if decomposition is still valid after specialization - assert((kron(decomposition.k1l_, decomposition.k1r_) * - decomposition.getCanonicalMatrix() * - kron(decomposition.k2l_, decomposition.k2r_) * - helpers::globalPhaseFactor(decomposition.globalPhase_)) - .isApprox(unitaryMatrix, SANITY_CHECK_PRECISION)); - - return decomposition; -} - -Matrix4x4 TwoQubitWeylDecomposition::getCanonicalMatrix(double a, double b, - double c) { - // Canonical gate `U_d(a, b, c) = exp(i * (a*XX + b*YY + c*ZZ))`. XX/YY/ZZ - // commute pairwise, so any product order is equivalent; the order below is - // chosen to match common Qiskit/QuantumFlow references. The negated rotation - // angles (`-2 * a`, ...) compensate for the `RXX/RYY/RZZ` convention - // `exp(-i * theta/2 * XX)`, so that the factored angles sum back to the - // intended `+a`, `+b`, `+c`. - const auto xx = rxxMatrix(-2.0 * a); - const auto yy = ryyMatrix(-2.0 * b); - const auto zz = rzzMatrix(-2.0 * c); - return zz * yy * xx; -} - -Matrix4x4 -TwoQubitWeylDecomposition::magicBasisTransform(const Matrix4x4& unitary, - MagicBasisTransform direction) { - // Makhlin "magic basis" transform. Conjugating a 2-qubit unitary by - // `bNonNormalized` maps SU(2) x SU(2) factors onto SO(4) and diagonalizes - // the canonical (Weyl) gate. The matrices are stored unnormalized: the - // `1/2` pre-factor that would normally appear in `B^dagger` is absorbed - // into `bNonNormalizedDagger` directly so the product `Bd * B == I` - // without an extra scalar. - const Matrix4x4 bNonNormalized = Matrix4x4::fromElements( // - 1, 1i, 0, 0, // - 0, 0, 1i, 1, // - 0, 0, 1i, -1, // - 1, -1i, 0, 0); - const Matrix4x4 bNonNormalizedDagger = Matrix4x4::fromElements( // - 0.5, 0, 0, 0.5, // - Complex{0.0, -0.5}, 0, 0, Complex{0.0, 0.5}, // - 0, Complex{0.0, -0.5}, Complex{0.0, -0.5}, 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.); -} - -std::pair> -TwoQubitWeylDecomposition::diagonalizeComplexSymmetric(const Matrix4x4& 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; - - const auto mReal = m.realPart(); - const auto mImag = m.imagPart(); - - double bestErr = 1e300; - 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); - } - std::array m2Real{}; - for (std::size_t k = 0; k < m2Real.size(); ++k) { - m2Real[k] = (randA * mReal[k]) + (randB * mImag[k]); - } - const Matrix4x4 p = jacobiSymmetricEigen(m2Real).eigenvectors; - const std::array d = (p.transpose() * m * p).diagonal(); - - const auto compare = p * Matrix4x4::fromDiagonal(d) * p.transpose(); - { - double err = 0.0; - for (std::size_t r = 0; r < 4; ++r) { - for (std::size_t cc = 0; cc < 4; ++cc) { - err = std::max(err, std::abs(compare(r, cc) - m(r, cc))); - } - } - bestErr = std::min(bestErr, err); - } - if (compare.isApprox(m, precision)) { - // p are the eigenvectors which are decomposed into the - // single-qubit gates surrounding the canonical gate - // d is the sqrt of the eigenvalues that are used to determine the - // weyl coordinates and thus the parameters of the canonical gate - // check that p is in SO(4) - assert((p.transpose() * p).isIdentity(SANITY_CHECK_PRECISION)); - // make sure determinant of eigenvalues is 1.0 - assert(std::abs(Matrix4x4::fromDiagonal(d).determinant() - 1.0) < - SANITY_CHECK_PRECISION); - return std::make_pair(p, d); - } - } - llvm::reportFatalInternalError(llvm::formatv( - "TwoQubitWeylDecomposition: failed to diagonalize M2 ({0} iterations). " - "best error = {1:e}, precision = {2:e}", - maxDiagonalizationAttempts, bestErr, precision)); -} - -std::tuple -TwoQubitWeylDecomposition::decomposeTwoQubitProductGate( - const Matrix4x4& specialUnitary) { - // for alternative approaches, see - // pennylane's math.decomposition.su2su2_to_tensor_products - // or quantumflow.kronecker_decomposition - - // first quadrant - Matrix2x2 r = - Matrix2x2::fromElements(specialUnitary(0, 0), specialUnitary(0, 1), - specialUnitary(1, 0), specialUnitary(1, 1)); - auto detR = r.determinant(); - if (std::abs(detR) < 0.1) { - // third quadrant - r = Matrix2x2::fromElements(specialUnitary(2, 0), specialUnitary(2, 1), - specialUnitary(3, 0), specialUnitary(3, 1)); - detR = r.determinant(); - } - if (std::abs(detR) < 0.1) { - llvm::reportFatalInternalError( - "decomposeTwoQubitProductGate: unable to decompose: det_r < 0.1"); - } - r *= (1.0 / std::sqrt(detR)); - // transpose with complex conjugate of each element - const Matrix2x2 rTConj = r.adjoint(); - - Matrix4x4 temp = specialUnitary * kron(Matrix2x2::identity(), rTConj); - - // [[a, b, c, d], - // [e, f, g, h], => [[a, c], - // [i, j, k, l], [i, k]] - // [m, n, o, p]] - Matrix2x2 l = - Matrix2x2::fromElements(temp(0, 0), temp(0, 2), temp(2, 0), temp(2, 2)); - auto detL = l.determinant(); - if (std::abs(detL) < 0.9) { - llvm::reportFatalInternalError( - "decomposeTwoQubitProductGate: unable to decompose: detL < 0.9"); - } - l *= (1.0 / 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) { - // Closed-form Hilbert-Schmidt overlap `tr(U_d(a,b,c)^dag * U_d(ap,bp,cp))` - // between two canonical (Weyl) gates, expressed in terms of the coordinate - // differences. Feeding the result into `traceToFidelity` gives the average - // two-qubit gate fidelity between the two canonical gates, which - // `bestSpecialization` uses to rank candidate specializations. - // Reference: Zhang et al., "Geometric theory of nonlocal two-qubit - // operations", Phys. Rev. A 67, 042313 (2003), Eq. (20). - 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((std::numbers::pi / 4.0), (std::numbers::pi / 4.0), - (std::numbers::pi / 4.0)) || - isClose((std::numbers::pi / 4.0), (std::numbers::pi / 4.0), - -(std::numbers::pi / 4.0))) { - 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((std::numbers::pi / 4.0), (std::numbers::pi / 4.0), 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_ = Matrix2x2::identity(); - k1r_ = k1r_ * k2r_; - k2r_ = Matrix2x2::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_ = Matrix2x2::identity(); - k2r_ = Matrix2x2::identity(); - } else { - flippedFromOriginal = true; - - globalPhase_ += (std::numbers::pi / 2.0); - k1l_ = k1l_ * ipz() * k2r_; - k1r_ = k1r_ * ipz() * k2l_; - k2l_ = Matrix2x2::identity(); - k2r_ = Matrix2x2::identity(); - } - a_ = (std::numbers::pi / 4.0); - b_ = (std::numbers::pi / 4.0); - c_ = (std::numbers::pi / 4.0); - } 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_.adjoint(); - - a_ = closest; - b_ = closest; - c_ = closest; - // unmodified global phase - k1l_ = k1l_ * k2l_; - k1r_ = k1r_ * k2l_; - k2r_ = k2lDagger * k2r_; - k2l_ = Matrix2x2::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_.adjoint(); - - a_ = closest; - b_ = closest; - c_ = -closest; - // unmodified global phase - k1l_ = k1l_ * k2l_; - k1r_ = k1r_ * ipz() * k2l_ * ipz(); - k2r_ = ipz() * k2lDagger * ipz() * k2r_; - k2l_ = Matrix2x2::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)` - const EulerBasis eulerBasis = EulerBasis::XYX; - const auto [k2ltheta, k2lphi, k2llambda, k2lphase] = - anglesFromUnitary(k2l_, eulerBasis); - const auto [k2rtheta, k2rphi, k2rlambda, k2rphase] = - anglesFromUnitary(k2r_, EulerBasis::XYX); - // 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); - } 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)` - const auto [k2ltheta, k2lphi, k2llambda, k2lphase] = - anglesFromUnitary(k2l_, EulerBasis::ZYZ); - const auto [k2rtheta, k2rphi, k2rlambda, k2rphase] = - anglesFromUnitary(k2r_, EulerBasis::ZYZ); - a_ = (std::numbers::pi / 4.0); - b_ = (std::numbers::pi / 4.0); - // 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] = - 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] = - 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_; - } 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] = - 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_; - } else { - llvm::reportFatalInternalError( - "Unknown specialization for Weyl decomposition!"); - } - return flippedFromOriginal; -} - -} // namespace mlir::qco::decomposition diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp index 6ddbfb9580..2005017452 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp @@ -8,31 +8,38 @@ * Licensed under the MIT License */ -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.h" - +#include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" +#include #include #include #include #include -#include +#include #include +#include +#include +#include +#include #include #include #include +#include +#include #include +#include +#include #include #include +#include +#include #include #include #include @@ -44,10 +51,554 @@ namespace mlir::qco { } // namespace mlir::qco +// The following three functions are part of this pass's internal logic but are +// also exercised directly by unit tests, so they live in a named namespace that +// the tests can forward-declare. Everything else is file-local (see the +// anonymous namespace below). namespace mlir::qco::native_synth { +using decomposition::NativeGateKind; +using decomposition::NativeProfileSpec; + +/// Map a single-qubit `UnitaryOpInterface` op to the `NativeGateKind` that +/// must appear in the menu for the op to be a no-op. +static std::optional +singleQubitNativeGateKind(UnitaryOpInterface op) { + Operation* raw = op.getOperation(); + if (llvm::isa(raw)) { + return NativeGateKind::U; + } + if (llvm::isa(raw)) { + return NativeGateKind::X; + } + if (llvm::isa(raw)) { + return NativeGateKind::Sx; + } + if (llvm::isa(raw)) { + // `p` is a Z-rotation primitive for menu purposes. + return NativeGateKind::Rz; + } + if (llvm::isa(raw)) { + return NativeGateKind::Rx; + } + if (llvm::isa(raw)) { + return NativeGateKind::Ry; + } + if (llvm::isa(raw)) { + return NativeGateKind::R; + } + return std::nullopt; +} + +// NOLINTNEXTLINE(misc-use-internal-linkage): test-visible (see comment above). +bool allowsSingleQubitOp(UnitaryOpInterface op, const NativeProfileSpec& spec) { + if (llvm::isa(op.getOperation())) { + return true; + } + const auto gate = singleQubitNativeGateKind(op); + return gate && spec.allowedGates.contains(*gate); +} + +// NOLINTNEXTLINE(misc-use-internal-linkage): test-visible (see comment above). +bool getBlockTwoQubitMatrix(Operation* op, Matrix4x4& matrix) { + if (llvm::isa(op)) { + return false; + } + if (auto ctrl = llvm::dyn_cast(op)) { + if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { + return false; + } + auto* body = ctrl.getBodyUnitary(0).getOperation(); + if (llvm::isa(body)) { + matrix = Matrix4x4::fromElements(1, 0, 0, 0, // + 0, 1, 0, 0, // + 0, 0, 0, 1, // + 0, 0, 1, 0); + return true; + } + if (llvm::isa(body)) { + matrix = Matrix4x4::identity(); + matrix(3, 3) = -1.0; + return true; + } + return false; + } + auto unitary = llvm::dyn_cast(op); + if (!unitary || !unitary.isTwoQubit()) { + return false; + } + Matrix4x4 raw; + if (!unitary.getUnitaryMatrix4x4(raw)) { + return false; + } + matrix = raw; + return true; +} + +} // namespace mlir::qco::native_synth + +namespace mlir::qco { namespace { +using decomposition::AxisPair; +using decomposition::EntanglerBasis; +using decomposition::NativeGateKind; +using decomposition::NativeProfileSpec; +using decomposition::parseNativeSpec; +using decomposition::SingleQubitEmitterSpec; +using decomposition::SingleQubitMode; +using decomposition::synthesizeUnitary2QWeyl; +using decomposition::twoQubitEntanglerCount; +using native_synth::allowsSingleQubitOp; +using native_synth::getBlockTwoQubitMatrix; + +constexpr double PI = std::numbers::pi; +constexpr double HALF_PI = PI / 2.0; + +using QubitId = std::size_t; + +[[nodiscard]] const Matrix4x4& swapGate() { + static const Matrix4x4 matrix = Matrix4x4::fromElements(1, 0, 0, 0, // + 0, 0, 1, 0, // + 0, 1, 0, 0, // + 0, 0, 0, 1); + return matrix; +} + +[[nodiscard]] Matrix4x4 expandToTwoQubits(const Matrix2x2& singleQubitMatrix, + QubitId qubitId) { + if (qubitId == 0) { + return kron(singleQubitMatrix, Matrix2x2::identity()); + } + if (qubitId == 1) { + return kron(Matrix2x2::identity(), singleQubitMatrix); + } + llvm::reportFatalInternalError("Invalid qubit id for single-qubit expansion"); +} + +[[nodiscard]] Matrix4x4 +fixTwoQubitMatrixQubitOrder(const Matrix4x4& twoQubitMatrix, + const llvm::SmallVector& qubitIds) { + if (qubitIds == llvm::SmallVector{1, 0}) { + return swapGate() * twoQubitMatrix * swapGate(); + } + if (qubitIds == llvm::SmallVector{0, 1}) { + return twoQubitMatrix; + } + llvm::reportFatalInternalError( + "Invalid qubit IDs for fixing two-qubit matrix"); +} + +[[nodiscard]] decomposition::EulerBasis +emitterEulerBasis(const SingleQubitEmitterSpec& emitter) { + switch (emitter.mode) { + case SingleQubitMode::ZSXX: + return decomposition::EulerBasis::ZSXX; + case SingleQubitMode::U3: + return decomposition::EulerBasis::U; + case SingleQubitMode::R: + return decomposition::EulerBasis::R; + case SingleQubitMode::AxisPair: + switch (emitter.axisPair) { + case AxisPair::RxRz: + return decomposition::EulerBasis::XZX; + case AxisPair::RxRy: + return decomposition::EulerBasis::XYX; + case AxisPair::RyRz: + return decomposition::EulerBasis::ZYZ; + } + break; + } + llvm_unreachable("unknown single-qubit mode"); +} + +[[nodiscard]] bool usesCxEntangler(const NativeProfileSpec& spec) { + return llvm::is_contained(spec.entanglerBases, EntanglerBasis::Cx); +} + +[[nodiscard]] bool usesCzEntangler(const NativeProfileSpec& spec) { + return llvm::is_contained(spec.entanglerBases, EntanglerBasis::Cz); +} + +/// True when `decomposeTo*` should run instead of folding to a constant `2×2` +/// matrix: trivial `Id`/`P`, dynamic-angle ops the matrix path cannot close +/// over, and (for ZSXX with direct Rx) `Rx`/`Ry`/`R`. Static angles still use +/// matrix + Euler. +bool canDirectlyDecomposeToZSXX(Operation* op, bool supportsDirectRx) { + if (llvm::isa(op)) { + return true; + } + return supportsDirectRx && llvm::isa(op); +} + +bool canDirectlyDecomposeToU3(Operation* op) { + return llvm::isa(op); +} + +bool canDirectlyDecomposeToR(Operation* op) { + return llvm::isa(op); +} + +bool canDirectlyDecomposeToAxisPair(Operation* op, AxisPair axisPair) { + if (llvm::isa(op)) { + return true; + } + switch (axisPair) { + case AxisPair::RxRz: + // `p` on an Rx/Rz axis pair folds directly to `rz(theta)`. + return llvm::isa(op); + case AxisPair::RxRy: + // No cheap symbolic lowering of `p` without `rz` available. + return llvm::isa(op); + case AxisPair::RyRz: + return llvm::isa(op); + } + llvm_unreachable("unknown axis pair"); +} + +Value createF64Const(IRRewriter& rewriter, Location loc, double value) { + return arith::ConstantFloatOp::create(rewriter, loc, rewriter.getF64Type(), + llvm::APFloat(value)) + .getResult(); +} + +std::optional getConstantF64(Value value) { + if (auto constant = value.getDefiningOp()) { + if (auto floatAttr = llvm::dyn_cast(constant.getValue())) { + return floatAttr.getValueAsDouble(); + } + } + return std::nullopt; +} + +void emitGPhaseIfNonTrivial(IRRewriter& rewriter, Location loc, double phase) { + constexpr double epsilon = 1e-12; + if (std::abs(phase) > epsilon) { + GPhaseOp::create(rewriter, loc, createF64Const(rewriter, loc, phase)); + } +} + +void collectUnitaryOpsInPreOrder(Operation* root, + llvm::SmallVectorImpl& ops) { + root->walk([&](Operation* op) { + if (op->getParentOfType()) { + return; + } + if (!llvm::isa(op) && op->getParentOfType()) { + return; + } + if (llvm::isa(op)) { + ops.push_back(op); + } + }); +} + +/// Small convenience wrapper to avoid passing rewriter/loc everywhere. Each +/// method creates the corresponding QCO op threaded through `q` and returns +/// its new output qubit value. +struct SingleQubitEmitter { + IRRewriter* rewriter; + Location loc; + + /// Create an `arith.constant` `f64` of value `v` at `loc`. + [[nodiscard]] Value constF(double v) const { + return createF64Const(*rewriter, loc, v); + } + + /// Emit `rx(theta)` with a compile-time scalar angle. + [[nodiscard]] Value rx(Value q, double theta) const { + return RXOp::create(*rewriter, loc, q, constF(theta)).getOutputQubit(0); + } + /// Emit `rx(theta)` with a runtime `f64` angle value. + [[nodiscard]] Value rx(Value q, Value theta) const { + return RXOp::create(*rewriter, loc, q, theta).getOutputQubit(0); + } + /// Emit `ry(theta)` with a compile-time scalar angle. + [[nodiscard]] Value ry(Value q, double theta) const { + return RYOp::create(*rewriter, loc, q, constF(theta)).getOutputQubit(0); + } + /// Emit `ry(theta)` with a runtime `f64` angle value. + [[nodiscard]] Value ry(Value q, Value theta) const { + return RYOp::create(*rewriter, loc, q, theta).getOutputQubit(0); + } + /// Emit `rz(theta)` with a compile-time scalar angle. + [[nodiscard]] Value rz(Value q, double theta) const { + return RZOp::create(*rewriter, loc, q, constF(theta)).getOutputQubit(0); + } + /// Emit `rz(theta)` with a runtime `f64` angle value. + [[nodiscard]] Value rz(Value q, Value theta) const { + return RZOp::create(*rewriter, loc, q, theta).getOutputQubit(0); + } + /// Emit `sx` (square-root-of-X). + [[nodiscard]] Value sx(Value q) const { + return SXOp::create(*rewriter, loc, q).getOutputQubit(0); + } + /// Emit a Pauli `x`. + [[nodiscard]] Value x(Value q) const { + return XOp::create(*rewriter, loc, q).getOutputQubit(0); + } + /// Emit `r(theta, phi)` with compile-time scalar angles. + [[nodiscard]] Value r(Value q, double theta, double phi) const { + return ROp::create(*rewriter, loc, q, constF(theta), constF(phi)) + .getOutputQubit(0); + } + /// Emit `r(theta, phi)` with runtime `f64` angle values. + [[nodiscard]] Value r(Value q, Value theta, Value phi) const { + return ROp::create(*rewriter, loc, q, theta, phi).getOutputQubit(0); + } + /// Emit `u(theta, phi, lambda)` with runtime `f64` angle values. + [[nodiscard]] Value u(Value q, Value theta, Value phi, Value lambda) const { + return UOp::create(*rewriter, loc, q, theta, phi, lambda).getOutputQubit(0); + } + /// Emit `u(theta, phi, lambda)` with compile-time scalar angles. + [[nodiscard]] Value u(Value q, double theta, double phi, + double lambda) const { + return u(q, constF(theta), constF(phi), constF(lambda)); + } +}; + +Value decomposeToZSXX(IRRewriter& rewriter, Operation* op, Value inQubit, + bool supportsDirectRx) { + if (llvm::isa(op)) { + return inQubit; + } + SingleQubitEmitter e{.rewriter = &rewriter, .loc = op->getLoc()}; + if (auto p = llvm::dyn_cast(op)) { + auto q = e.rz(inQubit, p.getTheta()); + auto halfTheta = arith::MulFOp::create(rewriter, op->getLoc(), p.getTheta(), + e.constF(0.5)) + .getResult(); + GPhaseOp::create(rewriter, op->getLoc(), halfTheta); + return q; + } + if (!supportsDirectRx) { + return {}; + } + if (auto rx = llvm::dyn_cast(op)) { + return rx.getOutputQubit(0); + } + if (auto ry = llvm::dyn_cast(op)) { + return e.rz(e.rx(e.rz(inQubit, -HALF_PI), ry.getTheta()), HALF_PI); + } + if (auto r = llvm::dyn_cast(op)) { + auto negPhi = + arith::NegFOp::create(rewriter, op->getLoc(), r.getPhi()).getResult(); + return e.rz(e.rx(e.rz(inQubit, negPhi), r.getTheta()), r.getPhi()); + } + return {}; +} + +Value decomposeToU3(IRRewriter& rewriter, Operation* op, Value inQubit) { + if (llvm::isa(op)) { + return inQubit; + } + SingleQubitEmitter e{.rewriter = &rewriter, .loc = op->getLoc()}; + if (auto u = llvm::dyn_cast(op)) { + return u.getOutputQubit(0); + } + if (auto rx = llvm::dyn_cast(op)) { + return e.u(inQubit, rx.getTheta(), e.constF(-HALF_PI), e.constF(HALF_PI)); + } + if (auto ry = llvm::dyn_cast(op)) { + return e.u(inQubit, ry.getTheta(), e.constF(0.0), e.constF(0.0)); + } + if (auto rz = llvm::dyn_cast(op)) { + auto out = e.u(inQubit, e.constF(0.0), e.constF(0.0), rz.getTheta()); + auto halfTheta = arith::MulFOp::create(rewriter, op->getLoc(), + rz.getTheta(), e.constF(-0.5)) + .getResult(); + GPhaseOp::create(rewriter, op->getLoc(), halfTheta); + return out; + } + if (auto p = llvm::dyn_cast(op)) { + return e.u(inQubit, e.constF(0.0), e.constF(0.0), p.getTheta()); + } + if (auto u2 = llvm::dyn_cast(op)) { + return e.u(inQubit, e.constF(HALF_PI), u2.getPhi(), u2.getLambda()); + } + if (auto r = llvm::dyn_cast(op)) { + auto loc = op->getLoc(); + auto phiMinus = + arith::AddFOp::create(rewriter, loc, r.getPhi(), e.constF(-HALF_PI)) + .getResult(); + auto negPhi = arith::NegFOp::create(rewriter, loc, r.getPhi()).getResult(); + auto minusPlus = + arith::AddFOp::create(rewriter, loc, negPhi, e.constF(HALF_PI)) + .getResult(); + return e.u(inQubit, r.getTheta(), phiMinus, minusPlus); + } + return {}; +} + +Value decomposeToR(IRRewriter& rewriter, Operation* op, Value inQubit) { + if (llvm::isa(op)) { + return inQubit; + } + SingleQubitEmitter e{.rewriter = &rewriter, .loc = op->getLoc()}; + if (auto r = llvm::dyn_cast(op)) { + return r.getOutputQubit(0); + } + if (auto rx = llvm::dyn_cast(op)) { + return e.r(inQubit, rx.getTheta(), e.constF(0.0)); + } + if (auto ry = llvm::dyn_cast(op)) { + return e.r(inQubit, ry.getTheta(), e.constF(HALF_PI)); + } + return {}; +} + +Value decomposeToAxisPair(IRRewriter& rewriter, Operation* op, Value inQubit, + AxisPair axisPair) { + if (llvm::isa(op)) { + return inQubit; + } + SingleQubitEmitter e{.rewriter = &rewriter, .loc = op->getLoc()}; + switch (axisPair) { + case AxisPair::RxRz: + if (auto rx = llvm::dyn_cast(op)) { + return rx.getOutputQubit(0); + } + if (auto rz = llvm::dyn_cast(op)) { + return rz.getOutputQubit(0); + } + if (auto p = llvm::dyn_cast(op)) { + auto q = e.rz(inQubit, p.getTheta()); + auto halfTheta = arith::MulFOp::create(rewriter, op->getLoc(), + p.getTheta(), e.constF(0.5)) + .getResult(); + GPhaseOp::create(rewriter, op->getLoc(), halfTheta); + return q; + } + return {}; + case AxisPair::RxRy: + if (auto rx = llvm::dyn_cast(op)) { + return rx.getOutputQubit(0); + } + if (auto ry = llvm::dyn_cast(op)) { + return ry.getOutputQubit(0); + } + return {}; + case AxisPair::RyRz: + if (auto ry = llvm::dyn_cast(op)) { + return ry.getOutputQubit(0); + } + if (auto rz = llvm::dyn_cast(op)) { + return rz.getOutputQubit(0); + } + if (auto p = llvm::dyn_cast(op)) { + auto q = e.rz(inQubit, p.getTheta()); + auto halfTheta = arith::MulFOp::create(rewriter, op->getLoc(), + p.getTheta(), e.constF(0.5)) + .getResult(); + GPhaseOp::create(rewriter, op->getLoc(), halfTheta); + return q; + } + return {}; + } + llvm_unreachable("unknown axis pair"); +} + +Value emitSingleQubitMatrix(IRRewriter& rewriter, Location loc, Value inQubit, + const Matrix2x2& matrix, + const decomposition::EulerBasis basis) { + // Force emission (`hasNonBasisGate = true`, `runSize = 0`) so the matrix is + // always lowered into native gates of `basis`, including any residual + // `qco.gphase`. With these arguments `synthesizeUnitary1QEuler` never + // returns `std::nullopt`. + return *decomposition::synthesizeUnitary1QEuler( + rewriter, loc, inQubit, matrix, /*runSize=*/0, + /*hasNonBasisGate=*/true, basis); +} + +LogicalResult rewriteXXPlusMinusYYViaRzz(IRRewriter& rewriter, Operation* op) { + rewriter.setInsertionPoint(op); + const auto loc = op->getLoc(); + const auto constF = [&](double v) { + return createF64Const(rewriter, loc, v); + }; + const auto half = [&](Value v) -> Value { + if (auto c = getConstantF64(v)) { + return constF(*c * 0.5); + } + return arith::MulFOp::create(rewriter, loc, v, constF(0.5)).getResult(); + }; + const auto neg = [&](Value v) -> Value { + if (auto c = getConstantF64(v)) { + return constF(-*c); + } + return arith::NegFOp::create(rewriter, loc, v).getResult(); + }; + const auto emitH = [&](Value q) -> Value { + auto rz0 = RZOp::create(rewriter, loc, q, constF(HALF_PI)); + auto sx = SXOp::create(rewriter, loc, rz0.getOutputQubit(0)); + return RZOp::create(rewriter, loc, sx.getOutputQubit(0), constF(HALF_PI)) + .getOutputQubit(0); + }; + // Realize `Rxx(theta)` as `(H ⊗ H) * Rzz(theta) * (H ⊗ H)`: Hadamard + // conjugation maps the Z axis to X on each qubit, and the tensor-product + // identity `(H ⊗ H) * ZZ * (H ⊗ H) == XX` lifts that to the entangler. + const auto emitRxxViaRzz = [&](Value q0, Value q1, + Value theta) -> std::pair { + q0 = emitH(q0); + q1 = emitH(q1); + auto rzz = RZZOp::create(rewriter, loc, q0, q1, theta); + q0 = rzz.getOutputQubit(0); + q1 = rzz.getOutputQubit(1); + return {emitH(q0), emitH(q1)}; + }; + // Realize `Ryy(theta)` as `(Rx(-pi/2) ⊗ Rx(-pi/2)) * Rzz(theta) * + // (Rx(pi/2) ⊗ Rx(pi/2))`: Rx(pi/2) maps Z to Y on each qubit, so the + // conjugation transports `ZZ` to `YY` just like the Hadamard sandwich + // above maps it to `XX`. + const auto emitRyyViaRzz = [&](Value q0, Value q1, + Value theta) -> std::pair { + auto rx0 = RXOp::create(rewriter, loc, q0, constF(HALF_PI)); + auto rx1 = RXOp::create(rewriter, loc, q1, constF(HALF_PI)); + auto rzz = RZZOp::create(rewriter, loc, rx0.getOutputQubit(0), + rx1.getOutputQubit(0), theta); + auto rxb0 = + RXOp::create(rewriter, loc, rzz.getOutputQubit(0), constF(-HALF_PI)); + auto rxb1 = + RXOp::create(rewriter, loc, rzz.getOutputQubit(1), constF(-HALF_PI)); + return {rxb0.getOutputQubit(0), rxb1.getOutputQubit(0)}; + }; + + // `XXPlusYY(theta, beta)` and `XXMinusYY(theta, beta)` both act as + // Rz(-beta) on q0 -> entangling core -> Rz(+beta) on q0, + // but differ in the entangling core: + // XXPlusYY: exp(-i * theta/4 * (XX + YY)) == Ryy(theta/2) * Rxx(theta/2) + // XXMinusYY: exp(-i * theta/4 * (XX - YY)) == Rxx(theta/2) * Ryy(-theta/2) + // (XX and YY commute, so the two multiplication orders produce identical + // unitaries; the distinct order and sign below are what makes `XXMinusYY` + // the "minus" variant and must be preserved even though an order flip + // alone would also compile.) + if (auto xxPlus = llvm::dyn_cast(op)) { + Value q0 = xxPlus.getInputQubit(0); + Value q1 = xxPlus.getInputQubit(1); + q0 = RZOp::create(rewriter, loc, q0, neg(xxPlus.getBeta())) + .getOutputQubit(0); + const auto halfTheta = half(xxPlus.getTheta()); + std::tie(q0, q1) = emitRyyViaRzz(q0, q1, halfTheta); + std::tie(q0, q1) = emitRxxViaRzz(q0, q1, halfTheta); + q0 = RZOp::create(rewriter, loc, q0, xxPlus.getBeta()).getOutputQubit(0); + rewriter.replaceOp(op, ValueRange{q0, q1}); + return success(); + } + if (auto xxMinus = llvm::dyn_cast(op)) { + Value q0 = xxMinus.getInputQubit(0); + Value q1 = xxMinus.getInputQubit(1); + q0 = RZOp::create(rewriter, loc, q0, neg(xxMinus.getBeta())) + .getOutputQubit(0); + const auto halfTheta = half(xxMinus.getTheta()); + std::tie(q0, q1) = emitRxxViaRzz(q0, q1, halfTheta); + std::tie(q0, q1) = emitRyyViaRzz(q0, q1, neg(halfTheta)); + q0 = RZOp::create(rewriter, loc, q0, xxMinus.getBeta()).getOutputQubit(0); + rewriter.replaceOp(op, ValueRange{q0, q1}); + return success(); + } + return failure(); +} + /// State for one maximal two-qubit window (plus absorbed one-qubit ops) /// during consolidation. struct TwoQubitBlock { @@ -84,10 +635,10 @@ bool isNativeTwoQubitOp(Operation* op, const NativeProfileSpec& spec) { } auto* body = ctrl.getBodyUnitary(0).getOperation(); if (llvm::isa(body)) { - return usesCxEntangler(spec); + return llvm::is_contained(spec.entanglerBases, EntanglerBasis::Cx); } if (llvm::isa(body)) { - return usesCzEntangler(spec); + return llvm::is_contained(spec.entanglerBases, EntanglerBasis::Cz); } return false; } @@ -119,8 +670,8 @@ LogicalResult materializeSingleTwoQubitBlock(IRRewriter& rewriter, rewriter.setInsertionPoint(firstOp); Value newA; Value newB; - if (failed(emitTwoQubitNative(rewriter, firstOp->getLoc(), inA, inB, - block.accum, spec, newA, newB))) { + if (failed(synthesizeUnitary2QWeyl(rewriter, firstOp->getLoc(), inA, inB, + block.accum, spec, newA, newB))) { firstOp->emitError("failed to emit synthesized two-qubit gate sequence"); return failure(); } @@ -197,7 +748,7 @@ void TwoQubitWindowConsolidator::process(Operation* op, if (sameBlock && singleUse) { const size_t idx = *idx0; auto& block = blocks[idx]; - llvm::SmallVector ids; + llvm::SmallVector ids; if (v0 == block.wireA && v1 == block.wireB) { ids = {0, 1}; } else if (v0 == block.wireB && v1 == block.wireA) { @@ -206,8 +757,7 @@ void TwoQubitWindowConsolidator::process(Operation* op, closeBlock(idx); return; } - block.accum = decomposition::fixTwoQubitMatrixQubitOrder(opMatrix, ids) * - block.accum; + block.accum = fixTwoQubitMatrixQubitOrder(opMatrix, ids) * block.accum; block.ops.push_back(op); ++block.numTwoQ; if (!isNativeTwoQubitOp(op, spec)) { @@ -268,9 +818,8 @@ void TwoQubitWindowConsolidator::process(Operation* op, closeBlock(idx); return; } - const auto pad = (v == block.wireA) - ? decomposition::expandToTwoQubits(raw, 0) - : decomposition::expandToTwoQubits(raw, 1); + const auto pad = (v == block.wireA) ? expandToTwoQubits(raw, 0) + : expandToTwoQubits(raw, 1); block.accum = pad * block.accum; block.ops.push_back(op); ++block.numOneQ; @@ -322,8 +871,6 @@ TwoQubitWindowConsolidator::materialize(IRRewriter& rewriter, return success(); } -} // namespace - LogicalResult fuseTwoQubitUnitaryRuns(IRRewriter& rewriter, Operation* root, const NativeProfileSpec& spec) { llvm::SmallVector ops; @@ -335,21 +882,139 @@ LogicalResult fuseTwoQubitUnitaryRuns(IRRewriter& rewriter, Operation* root, return consolidator.materialize(rewriter, spec); } -namespace { +/// Adjacent single-qubit unitaries on one wire considered for fusion. +struct OneQubitRun { + llvm::SmallVector ops; +}; + +/// If profitable, replace the run with one synthesized single-qubit op in +/// `basis` (mirrors `FuseSingleQubitUnitaryRuns`). Fuses when any op is +/// off-menu or when Euler resynthesis strictly shortens the run. +bool maybeFuseRun(IRRewriter& rewriter, OneQubitRun& run, + const decomposition::EulerBasis basis, + const NativeProfileSpec& spec) { + Matrix2x2 fused = Matrix2x2::identity(); + for (UnitaryOpInterface u : run.ops) { + Matrix2x2 m; + if (!u.getUnitaryMatrix2x2(m)) { + return false; + } + fused.premultiplyBy(m); + } + + const bool anyNonNative = llvm::any_of(run.ops, [&](UnitaryOpInterface u) { + return !allowsSingleQubitOp(u, spec); + }); + + Operation* firstOp = run.ops.front().getOperation(); + const Value inQubit = run.ops.front().getInputQubit(0); + const Value outQubit = run.ops.back().getOutputQubit(0); + + rewriter.setInsertionPoint(firstOp); + const auto replacement = decomposition::synthesizeUnitary1QEuler( + rewriter, firstOp->getLoc(), inQubit, fused, run.ops.size(), anyNonNative, + basis); + if (!replacement) { + return false; + } + rewriter.replaceAllUsesWith(outQubit, *replacement); + for (auto& op : llvm::reverse(run.ops)) { + rewriter.eraseOp(op.getOperation()); + } + return true; +} + +/// True when `op` lives in a `ctrl`/`inv` region body (not the shell op). +/// Skips nested unitaries so they are handled via the enclosing modifier. +bool isHiddenInsideCtrlOrInvBody(Operation* op) { + if (op->getParentOfType()) { + return true; + } + if (!llvm::isa(op) && op->getParentOfType()) { + return true; + } + return false; +} + +/// Single-qubit op eligible for fusion (constant `2×2`, not under `ctrl`). +UnitaryOpInterface fusibleSingleQubitOp(Operation* op) { + auto unitary = llvm::dyn_cast(op); + if (!unitary || !unitary.isSingleQubit()) { + return {}; + } + if (llvm::isa(op)) { + return {}; + } + if (isHiddenInsideCtrlOrInvBody(op)) { + return {}; + } + Matrix2x2 matrix; + if (!unitary.getUnitaryMatrix2x2(matrix)) { + return {}; + } + return unitary; +} + +/// Whether `emitter` can lower the single-qubit `op` directly (used for ops +/// with non-constant angles, which have no constant `2×2` matrix). +bool emitterHasDirectLowering(Operation* op, + const SingleQubitEmitterSpec& emitter) { + switch (emitter.mode) { + case SingleQubitMode::ZSXX: + return canDirectlyDecomposeToZSXX(op, emitter.supportsDirectRx); + case SingleQubitMode::U3: + return canDirectlyDecomposeToU3(op); + case SingleQubitMode::R: + return canDirectlyDecomposeToR(op); + case SingleQubitMode::AxisPair: + return canDirectlyDecomposeToAxisPair(op, emitter.axisPair); + } + return false; +} + +/// Dispatch `op`'s direct (non-matrix) single-qubit lowering to the +/// `decomposeTo*` helper for `emitter.mode`. Returns the output qubit value +/// or a null `Value` if no direct rule applies for this op. +Value applyDirectSingleQubitLowering(IRRewriter& rewriter, Operation* op, + Value in, + const SingleQubitEmitterSpec& emitter) { + switch (emitter.mode) { + case SingleQubitMode::ZSXX: + return decomposeToZSXX(rewriter, op, in, emitter.supportsDirectRx); + case SingleQubitMode::U3: + return decomposeToU3(rewriter, op, in); + case SingleQubitMode::R: + return decomposeToR(rewriter, op, in); + case SingleQubitMode::AxisPair: + return decomposeToAxisPair(rewriter, op, in, emitter.axisPair); + } + llvm_unreachable("unknown SingleQubitMode"); +} -struct FuseTwoQubitUnitaryRunsPass final +/// Lowers unitary QCO ops to a comma-separated native gate menu using a +/// deterministic, matrix-driven synthesizer: single-qubit fuse, two-qubit +/// window consolidation, synthesis sweeps, seam single-qubit fuse, and +/// optional cleanup sweeps. +struct FuseTwoQubitUnitaryRunsPass : impl::FuseTwoQubitUnitaryRunsBase { - using Base::Base; + /// Default-construct the pass with the TableGen-generated option defaults. + FuseTwoQubitUnitaryRunsPass() = default; explicit FuseTwoQubitUnitaryRunsPass(FuseTwoQubitUnitaryRunsOptions options) - : Base(std::move(options)) {} + : FuseTwoQubitUnitaryRunsBase(std::move(options)) {} protected: + /// Top-level pass entry point. Resolves the native-gate menu, then drives + /// the staged rewrite pipeline: one-qubit run fusion, two-qubit window + /// consolidation, synthesis sweeps until the single-qubit surface is native, + /// seam cleanup, and a final fusion pass. Fails the pass on invalid input or + /// non-convergence. void runOnOperation() override { + // Empty native-gates string: no-op. if (llvm::StringRef(nativeGates).trim().empty()) { return; } - auto specOpt = resolveNativeGatesSpec(nativeGates); + auto specOpt = parseNativeSpec(nativeGates); if (!specOpt) { getOperation().emitError() << "unsupported native gate menu (native-gates='" << nativeGates @@ -357,13 +1022,370 @@ struct FuseTwoQubitUnitaryRunsPass final signalPassFailure(); return; } + const auto& spec = *specOpt; + // Deterministic single-qubit basis: the first emitter drives all matrix + // synthesis and run fusion. + const decomposition::EulerBasis oneQubitBasis = + emitterEulerBasis(spec.singleQubitEmitters.front()); + IRRewriter rewriter(&getContext()); - if (failed(fuseTwoQubitUnitaryRuns(rewriter, getOperation(), *specOpt))) { + + fuseOneQubitRuns(rewriter, spec, oneQubitBasis); + if (failed(consolidateTwoQubitBlocks(rewriter, spec))) { signalPassFailure(); + return; + } + // Two-qubit lowering can emit off-menu single-qubit ops (e.g. `rx`/`ry`); + // repeat until clean or hit the sweep cap before seam cleanup. + constexpr unsigned kMaxSynthesisSweeps = 4; + for (unsigned i = 0; i < kMaxSynthesisSweeps; ++i) { + if (failed(synthesizeRemainingOps(rewriter, spec, oneQubitBasis))) { + signalPassFailure(); + return; + } + if (!hasNonNativeSingleQubitOps(spec)) { + break; + } + } + if (hasNonNativeSingleQubitOps(spec)) { + getOperation().emitError() + << "native gate synthesis did not converge within " + << kMaxSynthesisSweeps + << " sweeps (single-qubit ops remain outside the native menu)"; + signalPassFailure(); + return; + } + // Fuse single-qubit seams between two-qubit blocks. + fuseOneQubitRuns(rewriter, spec, oneQubitBasis); + // Re-check full menu (single-qubit ops, native `ctrl`, allowed bare `rzz`). + constexpr unsigned kPostMenuCleanupSweeps = 4; + unsigned postMenuSweepsRemaining = kPostMenuCleanupSweeps; + while (hasNonNativeMenuOps(spec) && postMenuSweepsRemaining-- > 0) { + if (failed(synthesizeRemainingOps(rewriter, spec, oneQubitBasis))) { + signalPassFailure(); + return; + } + fuseOneQubitRuns(rewriter, spec, oneQubitBasis); + } + if (hasNonNativeMenuOps(spec)) { + getOperation().emitError() + << "native gate synthesis: operations remain outside the native menu " + "after final cleanup"; + signalPassFailure(); + return; } } + + /// `CtrlOp` is already on-menu when the body is `X`/`Z` and the profile + /// supplies `cx` / `cz` entanglers. + static bool ctrlMatchesNativeMenu(CtrlOp ctrl, + const NativeProfileSpec& spec) { + if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { + return false; + } + Operation* body = ctrl.getBodyUnitary(0).getOperation(); + const bool hasCX = llvm::isa(body); + const bool hasCZ = llvm::isa(body); + if (!hasCX && !hasCZ) { + return false; + } + return (usesCxEntangler(spec) && hasCX) || (usesCzEntangler(spec) && hasCZ); + } + + /// Bare two-qubit on-menu: `rzz` when the profile allows it. + static bool bareTwoQubitMatchesNativeMenu(Operation* op, + const NativeProfileSpec& spec) { + return llvm::isa(op) && spec.allowRzz && + spec.allowedGates.contains(NativeGateKind::Rzz); + } + + /// True if any unitary is outside `spec` (single-qubit, `ctrl`, or bare + /// `rzz`). + bool hasNonNativeMenuOps(const NativeProfileSpec& spec) { + const mlir::WalkResult walkResult = + getOperation()->walk([&](Operation* op) { + if (llvm::isa(op)) { + return mlir::WalkResult::advance(); + } + if (isHiddenInsideCtrlOrInvBody(op)) { + return mlir::WalkResult::advance(); + } + if (auto ctrl = llvm::dyn_cast(op)) { + if (!ctrlMatchesNativeMenu(ctrl, spec)) { + return mlir::WalkResult::interrupt(); + } + return mlir::WalkResult::advance(); + } + auto unitary = llvm::dyn_cast(op); + if (!unitary) { + return mlir::WalkResult::advance(); + } + if (unitary.isSingleQubit()) { + if (!allowsSingleQubitOp(unitary, spec)) { + return mlir::WalkResult::interrupt(); + } + return mlir::WalkResult::advance(); + } + if (unitary.isTwoQubit()) { + if (!bareTwoQubitMatchesNativeMenu(op, spec)) { + return mlir::WalkResult::interrupt(); + } + return mlir::WalkResult::advance(); + } + return mlir::WalkResult::interrupt(); + }); + return walkResult.wasInterrupted(); + } + + /// Any off-menu single-qubit unitary (ignores `ctrl` region bodies). + bool hasNonNativeSingleQubitOps(const NativeProfileSpec& spec) { + const mlir::WalkResult walkResult = + getOperation()->walk([&](Operation* op) { + if (llvm::isa(op)) { + return mlir::WalkResult::advance(); + } + if (isHiddenInsideCtrlOrInvBody(op)) { + return mlir::WalkResult::advance(); + } + auto unitary = llvm::dyn_cast(op); + if (!unitary || !unitary.isSingleQubit()) { + return mlir::WalkResult::advance(); + } + if (!allowsSingleQubitOp(unitary, spec)) { + return mlir::WalkResult::interrupt(); + } + return mlir::WalkResult::advance(); + }); + return walkResult.wasInterrupted(); + } + +private: + /// Fuse adjacent single-qubit runs when the emitter wins on length or any op + /// is off-menu. + void fuseOneQubitRuns(IRRewriter& rewriter, const NativeProfileSpec& spec, + const decomposition::EulerBasis basis) { + llvm::SmallVector runs; + llvm::DenseMap tailOpToRun; + + // Extend the current run only when this op consumes the run's *tail* + // output with no other uses: both the `tailOpToRun` lookup and + // `inQubit.hasOneUse()` are required. Without the single-use check a run + // could fuse gates on a wire that also feeds another path (fan-out), + // which would silently drop the sibling user. + getOperation()->walk([&](Operation* op) { + auto unitary = fusibleSingleQubitOp(op); + if (!unitary) { + return; + } + Value inQubit = unitary.getInputQubit(0); + Operation* defOp = inQubit.getDefiningOp(); + auto it = + (defOp != nullptr) ? tailOpToRun.find(defOp) : tailOpToRun.end(); + const bool canExtend = it != tailOpToRun.end() && inQubit.hasOneUse(); + if (canExtend) { + const size_t runIdx = it->second; + runs[runIdx].ops.push_back(unitary); + tailOpToRun.erase(it); + tailOpToRun[op] = runIdx; + } else { + runs.push_back(OneQubitRun{}); + runs.back().ops.push_back(unitary); + tailOpToRun[op] = runs.size() - 1; + } + }); + + for (auto& run : runs) { + if (run.ops.size() < 2) { + continue; + } + (void)maybeFuseRun(rewriter, run, basis, spec); + } + } + + /// Two-qubit windows with absorbed single-qubit ops: replace when a cheaper + /// native sequence exists. + LogicalResult consolidateTwoQubitBlocks(IRRewriter& rewriter, + const NativeProfileSpec& spec) { + return fuseTwoQubitUnitaryRuns(rewriter, getOperation(), spec); + } + + /// One synthesis sweep over the whole function: rewrite every remaining + /// off-menu unitary by dispatching to `rewriteSingleQubit` / + /// `rewriteControlled` / `rewriteTwoQubit`. Returns `failure()` as soon as + /// any op cannot be lowered to the native menu. Safe to call repeatedly; + /// `runOnOperation` iterates until convergence. + LogicalResult synthesizeRemainingOps(IRRewriter& rewriter, + const NativeProfileSpec& spec, + const decomposition::EulerBasis basis) { + llvm::SmallVector ops; + collectUnitaryOpsInPreOrder(getOperation(), ops); + llvm::DenseSet erasedOps; + + for (Operation* op : ops) { + // Pointers were collected before this loop; avoid dereferencing ops + // erased by earlier rewrites in this same sweep. + if (erasedOps.contains(op)) { + continue; + } + // Nested regions under `ctrl` / `inv` are handled on the shell op + // (e.g. `ctrl { inv { ... } }`, `inv { ... }`). + if (isHiddenInsideCtrlOrInvBody(op)) { + continue; + } + if (llvm::isa(op)) { + continue; + } + auto unitary = llvm::dyn_cast(op); + if (!unitary) { + continue; + } + + if (unitary.isSingleQubit()) { + if (!allowsSingleQubitOp(unitary, spec)) { + if (failed(rewriteSingleQubit(rewriter, op, unitary, spec, basis))) { + return failure(); + } + erasedOps.insert(op); + } + continue; + } + + if (auto ctrl = llvm::dyn_cast(op)) { + const bool wasAlreadyNative = ctrlMatchesNativeMenu(ctrl, spec); + if (failed(rewriteControlled(rewriter, ctrl, spec))) { + return failure(); + } + if (!wasAlreadyNative) { + erasedOps.insert(op); + } + continue; + } + + if (unitary.isTwoQubit()) { + if (failed(rewriteTwoQubit(rewriter, op, unitary, spec))) { + return failure(); + } + erasedOps.insert(op); + continue; + } + } + return success(); + } + + /// Lower one off-menu single-qubit `op`. Constant unitaries use the + /// matrix-driven Euler synthesizer in `basis`; ops with non-constant angles + /// fall back to the symbolic `decomposeTo*` lowering of the first emitter + /// that handles them. + static LogicalResult + rewriteSingleQubit(IRRewriter& rewriter, Operation* op, + UnitaryOpInterface unitary, const NativeProfileSpec& spec, + const decomposition::EulerBasis basis) { + rewriter.setInsertionPoint(op); + const Value in = unitary.getInputQubit(0); + Matrix2x2 matrix; + if (unitary.isSingleQubit() && unitary.getUnitaryMatrix2x2(matrix)) { + const Value replaced = + emitSingleQubitMatrix(rewriter, op->getLoc(), in, matrix, basis); + rewriter.replaceOp(op, replaced); + return success(); + } + for (const auto& emitter : spec.singleQubitEmitters) { + if (!emitterHasDirectLowering(op, emitter)) { + continue; + } + if (const Value replaced = + applyDirectSingleQubitLowering(rewriter, op, in, emitter)) { + rewriter.replaceOp(op, replaced); + return success(); + } + } + op->emitError("single-qubit operation not in selected native profile"); + return failure(); + } + + /// Lower a single-control, single-target `CtrlOp` to the native profile. + /// Fast-path: already-native `CX`/`CZ` are kept as-is. Otherwise, lift the + /// controlled op to its 4x4 matrix and run the deterministic two-qubit + /// synthesizer. + static LogicalResult rewriteControlled(IRRewriter& rewriter, CtrlOp ctrl, + const NativeProfileSpec& spec) { + if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { + ctrl.emitError("native synthesis currently only supports 1-control " + "1-target controlled gates"); + return failure(); + } + auto* body = ctrl.getBodyUnitary(0).getOperation(); + const bool hasCX = llvm::isa(body); + const bool hasCZ = llvm::isa(body); + if ((usesCxEntangler(spec) && hasCX) || (usesCzEntangler(spec) && hasCZ)) { + return success(); + } + Matrix4x4 matrix; + if (hasCX || hasCZ) { + if (!getBlockTwoQubitMatrix(ctrl.getOperation(), matrix)) { + ctrl.emitError("failed to compute 4x4 matrix for CtrlOp"); + return failure(); + } + } else { + auto u = llvm::cast(ctrl.getOperation()); + if (!u.isTwoQubit() || !u.getUnitaryMatrix4x4(matrix)) { + ctrl.emitError( + "native synthesis: cannot build a constant 4x4 matrix for this " + "controlled gate (unsupported body or non-constant parameters)"); + return failure(); + } + } + rewriter.setInsertionPoint(ctrl); + Value out0; + Value out1; + if (failed(synthesizeUnitary2QWeyl( + rewriter, ctrl.getLoc(), ctrl.getInputControl(0), + ctrl.getInputTarget(0), matrix, spec, out0, out1))) { + ctrl.emitError("controlled gate not allowed by selected profile"); + return failure(); + } + rewriter.replaceOp(ctrl, ValueRange{out0, out1}); + return success(); + } + + /// Lower an off-menu generic two-qubit op (`RZZ`, `XXPlusYY`, `XXMinusYY`, + /// or any arbitrary 4x4 unitary). Handles the `Rzz`-native fast path; for + /// `XXPlusYY` / `XXMinusYY` with `rzz` on the menu, uses the dedicated + /// `XX±YY -> Rzz` rewrite. All other two-qubit unitaries go through the + /// deterministic KAK synthesizer. + static LogicalResult rewriteTwoQubit(IRRewriter& rewriter, Operation* op, + UnitaryOpInterface unitary, + const NativeProfileSpec& spec) { + if (spec.allowRzz && llvm::isa(op)) { + return success(); + } + if (spec.allowRzz && + (llvm::isa(op) || llvm::isa(op))) { + rewriter.setInsertionPoint(op); + if (succeeded(rewriteXXPlusMinusYYViaRzz(rewriter, op))) { + return success(); + } + // Fall through to entangler-based synthesis when the dedicated rewrite + // could not be applied (e.g. no entangler-free realization). + } + Matrix4x4 matrix; + if (!getBlockTwoQubitMatrix(op, matrix)) { + op->emitError("unsupported two-qubit operation for selected profile"); + return failure(); + } + rewriter.setInsertionPoint(op); + Value out0; + Value out1; + if (failed(synthesizeUnitary2QWeyl( + rewriter, op->getLoc(), unitary.getInputQubit(0), + unitary.getInputQubit(1), matrix, spec, out0, out1))) { + op->emitError("unsupported two-qubit operation for selected profile"); + return failure(); + } + rewriter.replaceOp(op, ValueRange{out0, out1}); + return success(); + } }; } // namespace - -} // namespace mlir::qco::native_synth +} // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.cpp deleted file mode 100644 index 74c1e271d2..0000000000 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.cpp +++ /dev/null @@ -1,246 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h" - -#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" - -#include -#include -#include -#include -#include - -#include - -namespace mlir::qco::native_synth { - -/// Map a single native-gate token (lower-case, no whitespace) to its -/// `NativeGateKind`. -static std::optional parseGateToken(llvm::StringRef name) { - return llvm::StringSwitch>(name) - .Case("u", NativeGateKind::U) - .Case("x", NativeGateKind::X) - .Case("sx", NativeGateKind::Sx) - .Cases("rz", "p", NativeGateKind::Rz) - .Case("rx", NativeGateKind::Rx) - .Case("ry", NativeGateKind::Ry) - .Case("r", NativeGateKind::R) - .Case("cx", NativeGateKind::Cx) - .Case("cz", NativeGateKind::Cz) - .Case("rzz", NativeGateKind::Rzz) - .Default(std::nullopt); -} - -/// Parse a comma-separated native-gate menu (e.g. `"u,cx,rzz"`) into the set -/// of `NativeGateKind`s it names. -static std::optional> -parseGateSet(llvm::StringRef nativeGates) { - llvm::DenseSet gates; - llvm::SmallVector parts; - nativeGates.split(parts, ',', /*MaxSplit=*/-1, /*KeepEmpty=*/false); - for (llvm::StringRef part : parts) { - const auto token = part.trim().lower(); - if (token.empty()) { - continue; - } - const auto gate = parseGateToken(token); - if (!gate) { - return std::nullopt; - } - gates.insert(*gate); - } - return gates; -} - -/// Build a fully-resolved `SingleQubitEmitterSpec` for `mode`. -static SingleQubitEmitterSpec -makeEmitterSpec(SingleQubitMode mode, AxisPair axisPair = AxisPair::RxRz, - bool supportsDirectRx = false) { - return { - .mode = mode, .axisPair = axisPair, .supportsDirectRx = supportsDirectRx}; -} - -/// Append a new emitter for `(mode, axisPair, supportsDirectRx)` to -/// `emitters` iff no equivalent entry is already present. -static void -addEmitterIfAbsent(llvm::SmallVectorImpl& emitters, - SingleQubitMode mode, AxisPair axisPair = AxisPair::RxRz, - bool supportsDirectRx = false) { - const bool present = llvm::any_of(emitters, [&](const auto& e) { - return e.mode == mode && e.axisPair == axisPair && - e.supportsDirectRx == supportsDirectRx; - }); - if (!present) { - emitters.push_back(makeEmitterSpec(mode, axisPair, supportsDirectRx)); - } -} - -/// Enumerate the native gate kinds that `emitter` may actually emit. -static llvm::SmallVector -allowedGatesForEmitter(const SingleQubitEmitterSpec& emitter) { - switch (emitter.mode) { - case SingleQubitMode::ZSXX: { - llvm::SmallVector gates{ - NativeGateKind::X, NativeGateKind::Sx, NativeGateKind::Rz}; - if (emitter.supportsDirectRx) { - gates.push_back(NativeGateKind::Rx); - } - return gates; - } - case SingleQubitMode::U3: - return {NativeGateKind::U}; - case SingleQubitMode::R: - return {NativeGateKind::R}; - case SingleQubitMode::AxisPair: - switch (emitter.axisPair) { - case AxisPair::RxRz: - return {NativeGateKind::Rx, NativeGateKind::Rz}; - case AxisPair::RxRy: - return {NativeGateKind::Rx, NativeGateKind::Ry}; - case AxisPair::RyRz: - return {NativeGateKind::Ry, NativeGateKind::Rz}; - } - break; - } - llvm_unreachable("unknown single-qubit mode"); -} - -/// Enumerate the native entangling gate kinds that `entangler` may emit. -static llvm::SmallVector -allowedGatesForEntangler(EntanglerBasis entangler) { - switch (entangler) { - case EntanglerBasis::None: - return {}; - case EntanglerBasis::Cx: - return {NativeGateKind::Cx}; - case EntanglerBasis::Cz: - return {NativeGateKind::Cz}; - } - llvm_unreachable("unknown entangler basis"); -} - -/// Rebuild `spec.allowedGates` as the union of the gate kinds produced by -/// every resolved emitter, entangler, and (optionally) `Rzz`. -static void populateAllowedGates(NativeProfileSpec& spec) { - spec.allowedGates.clear(); - for (const auto& emitter : spec.singleQubitEmitters) { - const auto allowed = allowedGatesForEmitter(emitter); - spec.allowedGates.insert(allowed.begin(), allowed.end()); - } - for (const auto entangler : spec.entanglerBases) { - const auto allowed = allowedGatesForEntangler(entangler); - spec.allowedGates.insert(allowed.begin(), allowed.end()); - } - if (spec.allowRzz) { - spec.allowedGates.insert(NativeGateKind::Rzz); - } -} - -/// Euler basis reconstructing a two-axis single-qubit unitary for `axisPair`. -static decomposition::EulerBasis eulerBasisForAxisPair(AxisPair axisPair) { - switch (axisPair) { - case AxisPair::RxRz: - return decomposition::EulerBasis::XZX; - case AxisPair::RxRy: - return decomposition::EulerBasis::XYX; - case AxisPair::RyRz: - return decomposition::EulerBasis::ZYZ; - } - llvm_unreachable("unknown axis pair"); -} - -decomposition::EulerBasis -emitterEulerBasis(const SingleQubitEmitterSpec& emitter) { - switch (emitter.mode) { - case SingleQubitMode::ZSXX: - return decomposition::EulerBasis::ZSXX; - case SingleQubitMode::U3: - return decomposition::EulerBasis::U; - case SingleQubitMode::R: - // The R basis decomposes any 1Q unitary into an X-Y-X chain emitted - // directly as native R(theta, phi) gates (`Rx(a) == R(a, 0)`, - // `Ry(a) == R(a, pi/2)`). - return decomposition::EulerBasis::R; - case SingleQubitMode::AxisPair: - return eulerBasisForAxisPair(emitter.axisPair); - } - llvm_unreachable("unknown single-qubit mode"); -} - -std::optional -resolveNativeGatesSpec(llvm::StringRef nativeGates) { - const auto gates = parseGateSet(nativeGates); - if (!gates || gates->empty()) { - return std::nullopt; - } - const auto has = [&](NativeGateKind kind) { return gates->contains(kind); }; - - NativeProfileSpec spec; - - // Derive all legal single-qubit emitters from the declared menu. Each - // emitter mode requires the *conjunction* of its constituent gate kinds - // to be on the menu -- for example, ZSXX needs X, Sx, and Rz all present, - // because the decomposer unconditionally emits all three. `supportsDirectRx` - // is an independent capability that enables a fast-path for `Rx(theta)` - // inputs when `Rx` is additionally available, but ZSXX itself does not - // depend on `Rx`. - if (has(NativeGateKind::U)) { - addEmitterIfAbsent(spec.singleQubitEmitters, SingleQubitMode::U3); - } - const bool hasXSxRz = has(NativeGateKind::X) && has(NativeGateKind::Sx) && - has(NativeGateKind::Rz); - if (hasXSxRz) { - addEmitterIfAbsent(spec.singleQubitEmitters, SingleQubitMode::ZSXX, - AxisPair::RxRz, - /*supportsDirectRx=*/has(NativeGateKind::Rx)); - } - if (has(NativeGateKind::R)) { - addEmitterIfAbsent(spec.singleQubitEmitters, SingleQubitMode::R); - } - struct AxisPairRule { - AxisPair axis; - NativeGateKind left; - NativeGateKind right; - }; - for (const auto& rule : { - AxisPairRule{.axis = AxisPair::RxRz, - .left = NativeGateKind::Rx, - .right = NativeGateKind::Rz}, - AxisPairRule{.axis = AxisPair::RxRy, - .left = NativeGateKind::Rx, - .right = NativeGateKind::Ry}, - AxisPairRule{.axis = AxisPair::RyRz, - .left = NativeGateKind::Ry, - .right = NativeGateKind::Rz}, - }) { - if (has(rule.left) && has(rule.right)) { - addEmitterIfAbsent(spec.singleQubitEmitters, SingleQubitMode::AxisPair, - rule.axis); - } - } - if (spec.singleQubitEmitters.empty()) { - return std::nullopt; - } - - if (has(NativeGateKind::Cx)) { - spec.entanglerBases.push_back(EntanglerBasis::Cx); - } - if (has(NativeGateKind::Cz)) { - spec.entanglerBases.push_back(EntanglerBasis::Cz); - } - spec.allowRzz = has(NativeGateKind::Rzz); - - populateAllowedGates(spec); - return spec; -} - -} // namespace mlir::qco::native_synth diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp deleted file mode 100644 index 7ab87c95c1..0000000000 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Pass.cpp +++ /dev/null @@ -1,598 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "mlir/Dialect/QCO/IR/QCODialect.h" -#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" -#include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h" -#include "mlir/Dialect/QCO/Transforms/Passes.h" -#include "mlir/Dialect/QCO/Utils/Matrix.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -namespace mlir::qco { -#define GEN_PASS_DEF_NATIVEGATESYNTHESISPASS -#include "mlir/Dialect/QCO/Transforms/Passes.h.inc" -} // namespace mlir::qco - -namespace mlir::qco { - -using native_synth::allowsSingleQubitOp; -using native_synth::canDirectlyDecomposeToAxisPair; -using native_synth::canDirectlyDecomposeToR; -using native_synth::canDirectlyDecomposeToU3; -using native_synth::canDirectlyDecomposeToZSXX; -using native_synth::collectUnitaryOpsInPreOrder; -using native_synth::decomposeToAxisPair; -using native_synth::decomposeToR; -using native_synth::decomposeToU3; -using native_synth::decomposeToZSXX; -using native_synth::emitSingleQubitMatrix; -using native_synth::emitterEulerBasis; -using native_synth::emitTwoQubitNative; -using native_synth::fuseTwoQubitUnitaryRuns; -using native_synth::getBlockTwoQubitMatrix; -using native_synth::NativeGateKind; -using native_synth::NativeProfileSpec; -using native_synth::resolveNativeGatesSpec; -using native_synth::rewriteXXPlusMinusYYViaRzz; -using native_synth::SingleQubitEmitterSpec; -using native_synth::SingleQubitMode; -using native_synth::usesCxEntangler; -using native_synth::usesCzEntangler; - -namespace { - -/// Adjacent single-qubit unitaries on one wire considered for fusion. -struct OneQubitRun { - llvm::SmallVector ops; -}; - -} // namespace - -/// If profitable, replace the run with one synthesized single-qubit op in -/// `basis` (mirrors `FuseSingleQubitUnitaryRuns`). Fuses when any op is -/// off-menu or when Euler resynthesis strictly shortens the run. -static bool maybeFuseRun(IRRewriter& rewriter, OneQubitRun& run, - const decomposition::EulerBasis basis, - const NativeProfileSpec& spec) { - Matrix2x2 fused = Matrix2x2::identity(); - for (UnitaryOpInterface u : run.ops) { - Matrix2x2 m; - if (!u.getUnitaryMatrix2x2(m)) { - return false; - } - fused.premultiplyBy(m); - } - - const bool anyNonNative = llvm::any_of(run.ops, [&](UnitaryOpInterface u) { - return !allowsSingleQubitOp(u, spec); - }); - - Operation* firstOp = run.ops.front().getOperation(); - const Value inQubit = run.ops.front().getInputQubit(0); - const Value outQubit = run.ops.back().getOutputQubit(0); - - rewriter.setInsertionPoint(firstOp); - const auto replacement = decomposition::synthesizeUnitary1QEuler( - rewriter, firstOp->getLoc(), inQubit, fused, run.ops.size(), anyNonNative, - basis); - if (!replacement) { - return false; - } - rewriter.replaceAllUsesWith(outQubit, *replacement); - for (auto& op : llvm::reverse(run.ops)) { - rewriter.eraseOp(op.getOperation()); - } - return true; -} - -/// True when `op` lives in a `ctrl`/`inv` region body (not the shell op). -/// Skips nested unitaries so they are handled via the enclosing modifier. -static bool isHiddenInsideCtrlOrInvBody(Operation* op) { - if (op->getParentOfType()) { - return true; - } - if (!llvm::isa(op) && op->getParentOfType()) { - return true; - } - return false; -} - -/// Single-qubit op eligible for fusion (constant `2×2`, not under `ctrl`). -static UnitaryOpInterface fusibleSingleQubitOp(Operation* op) { - auto unitary = llvm::dyn_cast(op); - if (!unitary || !unitary.isSingleQubit()) { - return {}; - } - if (llvm::isa(op)) { - return {}; - } - if (isHiddenInsideCtrlOrInvBody(op)) { - return {}; - } - Matrix2x2 matrix; - if (!unitary.getUnitaryMatrix2x2(matrix)) { - return {}; - } - return unitary; -} - -/// Whether `emitter` can lower the single-qubit `op` directly (used for ops -/// with non-constant angles, which have no constant `2×2` matrix). -static bool emitterHasDirectLowering(Operation* op, - const SingleQubitEmitterSpec& emitter) { - switch (emitter.mode) { - case SingleQubitMode::ZSXX: - return canDirectlyDecomposeToZSXX(op, emitter.supportsDirectRx); - case SingleQubitMode::U3: - return canDirectlyDecomposeToU3(op); - case SingleQubitMode::R: - return canDirectlyDecomposeToR(op); - case SingleQubitMode::AxisPair: - return canDirectlyDecomposeToAxisPair(op, emitter.axisPair); - } - return false; -} - -/// Dispatch `op`'s direct (non-matrix) single-qubit lowering to the -/// `decomposeTo*` helper for `emitter.mode`. Returns the output qubit value -/// or a null `Value` if no direct rule applies for this op. -static Value -applyDirectSingleQubitLowering(IRRewriter& rewriter, Operation* op, Value in, - const SingleQubitEmitterSpec& emitter) { - switch (emitter.mode) { - case SingleQubitMode::ZSXX: - return decomposeToZSXX(rewriter, op, in, emitter.supportsDirectRx); - case SingleQubitMode::U3: - return decomposeToU3(rewriter, op, in); - case SingleQubitMode::R: - return decomposeToR(rewriter, op, in); - case SingleQubitMode::AxisPair: - return decomposeToAxisPair(rewriter, op, in, emitter.axisPair); - } - llvm_unreachable("unknown SingleQubitMode"); -} - -namespace { - -/// Lowers unitary QCO ops to a comma-separated native gate menu using a -/// deterministic, matrix-driven synthesizer: single-qubit fuse, two-qubit -/// window consolidation, synthesis sweeps, seam single-qubit fuse, and -/// optional cleanup sweeps. -struct NativeGateSynthesisPass - : impl::NativeGateSynthesisPassBase { - /// Default-construct the pass with the TableGen-generated option defaults. - NativeGateSynthesisPass() = default; - - /// Construct the pass from the TableGen-generated options struct (forwards - /// all option values into the base class). - explicit NativeGateSynthesisPass( - const NativeGateSynthesisPassOptions& options) - : NativeGateSynthesisPassBase(options) {} - - /// Construct the pass from the public `NativeGateSynthesisOptions` struct - /// used by pipeline code that cannot include the TableGen-generated header. - explicit NativeGateSynthesisPass(const NativeGateSynthesisOptions& options) { - nativeGates = options.nativeGates; - } - -protected: - /// Top-level pass entry point. Resolves the native-gate menu, then drives - /// the staged rewrite pipeline: one-qubit run fusion, two-qubit window - /// consolidation, synthesis sweeps until the single-qubit surface is native, - /// seam cleanup, and a final fusion pass. Fails the pass on invalid input or - /// non-convergence. - void runOnOperation() override { - // Empty native-gates string: no-op. - if (llvm::StringRef(nativeGates).trim().empty()) { - return; - } - auto specOpt = resolveNativeGatesSpec(nativeGates); - if (!specOpt) { - getOperation().emitError() - << "unsupported native gate menu (native-gates='" << nativeGates - << "')"; - signalPassFailure(); - return; - } - const auto& spec = *specOpt; - // Deterministic single-qubit basis: the first emitter drives all matrix - // synthesis and run fusion. - const decomposition::EulerBasis oneQubitBasis = - emitterEulerBasis(spec.singleQubitEmitters.front()); - - IRRewriter rewriter(&getContext()); - - fuseOneQubitRuns(rewriter, spec, oneQubitBasis); - if (failed(consolidateTwoQubitBlocks(rewriter, spec))) { - signalPassFailure(); - return; - } - // Two-qubit lowering can emit off-menu single-qubit ops (e.g. `rx`/`ry`); - // repeat until clean or hit the sweep cap before seam cleanup. - constexpr unsigned kMaxSynthesisSweeps = 4; - for (unsigned i = 0; i < kMaxSynthesisSweeps; ++i) { - if (failed(synthesizeRemainingOps(rewriter, spec, oneQubitBasis))) { - signalPassFailure(); - return; - } - if (!hasNonNativeSingleQubitOps(spec)) { - break; - } - } - if (hasNonNativeSingleQubitOps(spec)) { - getOperation().emitError() - << "native gate synthesis did not converge within " - << kMaxSynthesisSweeps - << " sweeps (single-qubit ops remain outside the native menu)"; - signalPassFailure(); - return; - } - // Fuse single-qubit seams between two-qubit blocks. - fuseOneQubitRuns(rewriter, spec, oneQubitBasis); - // Re-check full menu (single-qubit ops, native `ctrl`, allowed bare `rzz`). - constexpr unsigned kPostMenuCleanupSweeps = 4; - unsigned postMenuSweepsRemaining = kPostMenuCleanupSweeps; - while (hasNonNativeMenuOps(spec) && postMenuSweepsRemaining-- > 0) { - if (failed(synthesizeRemainingOps(rewriter, spec, oneQubitBasis))) { - signalPassFailure(); - return; - } - fuseOneQubitRuns(rewriter, spec, oneQubitBasis); - } - if (hasNonNativeMenuOps(spec)) { - getOperation().emitError() - << "native gate synthesis: operations remain outside the native menu " - "after final cleanup"; - signalPassFailure(); - return; - } - } - - /// `CtrlOp` is already on-menu when the body is `X`/`Z` and the profile - /// supplies `cx` / `cz` entanglers. - static bool ctrlMatchesNativeMenu(CtrlOp ctrl, - const NativeProfileSpec& spec) { - if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { - return false; - } - Operation* body = ctrl.getBodyUnitary(0).getOperation(); - const bool hasCX = llvm::isa(body); - const bool hasCZ = llvm::isa(body); - if (!hasCX && !hasCZ) { - return false; - } - return (usesCxEntangler(spec) && hasCX) || (usesCzEntangler(spec) && hasCZ); - } - - /// Bare two-qubit on-menu: `rzz` when the profile allows it. - static bool bareTwoQubitMatchesNativeMenu(Operation* op, - const NativeProfileSpec& spec) { - return llvm::isa(op) && spec.allowRzz && - spec.allowedGates.contains(NativeGateKind::Rzz); - } - - /// True if any unitary is outside `spec` (single-qubit, `ctrl`, or bare - /// `rzz`). - bool hasNonNativeMenuOps(const NativeProfileSpec& spec) { - const mlir::WalkResult walkResult = - getOperation()->walk([&](Operation* op) { - if (llvm::isa(op)) { - return mlir::WalkResult::advance(); - } - if (isHiddenInsideCtrlOrInvBody(op)) { - return mlir::WalkResult::advance(); - } - if (auto ctrl = llvm::dyn_cast(op)) { - if (!ctrlMatchesNativeMenu(ctrl, spec)) { - return mlir::WalkResult::interrupt(); - } - return mlir::WalkResult::advance(); - } - auto unitary = llvm::dyn_cast(op); - if (!unitary) { - return mlir::WalkResult::advance(); - } - if (unitary.isSingleQubit()) { - if (!allowsSingleQubitOp(unitary, spec)) { - return mlir::WalkResult::interrupt(); - } - return mlir::WalkResult::advance(); - } - if (unitary.isTwoQubit()) { - if (!bareTwoQubitMatchesNativeMenu(op, spec)) { - return mlir::WalkResult::interrupt(); - } - return mlir::WalkResult::advance(); - } - return mlir::WalkResult::interrupt(); - }); - return walkResult.wasInterrupted(); - } - - /// Any off-menu single-qubit unitary (ignores `ctrl` region bodies). - bool hasNonNativeSingleQubitOps(const NativeProfileSpec& spec) { - const mlir::WalkResult walkResult = - getOperation()->walk([&](Operation* op) { - if (llvm::isa(op)) { - return mlir::WalkResult::advance(); - } - if (isHiddenInsideCtrlOrInvBody(op)) { - return mlir::WalkResult::advance(); - } - auto unitary = llvm::dyn_cast(op); - if (!unitary || !unitary.isSingleQubit()) { - return mlir::WalkResult::advance(); - } - if (!allowsSingleQubitOp(unitary, spec)) { - return mlir::WalkResult::interrupt(); - } - return mlir::WalkResult::advance(); - }); - return walkResult.wasInterrupted(); - } - -private: - /// Fuse adjacent single-qubit runs when the emitter wins on length or any op - /// is off-menu. - void fuseOneQubitRuns(IRRewriter& rewriter, const NativeProfileSpec& spec, - const decomposition::EulerBasis basis) { - llvm::SmallVector runs; - llvm::DenseMap tailOpToRun; - - // Extend the current run only when this op consumes the run's *tail* - // output with no other uses: both the `tailOpToRun` lookup and - // `inQubit.hasOneUse()` are required. Without the single-use check a run - // could fuse gates on a wire that also feeds another path (fan-out), - // which would silently drop the sibling user. - getOperation()->walk([&](Operation* op) { - auto unitary = fusibleSingleQubitOp(op); - if (!unitary) { - return; - } - Value inQubit = unitary.getInputQubit(0); - Operation* defOp = inQubit.getDefiningOp(); - auto it = - (defOp != nullptr) ? tailOpToRun.find(defOp) : tailOpToRun.end(); - const bool canExtend = it != tailOpToRun.end() && inQubit.hasOneUse(); - if (canExtend) { - const size_t runIdx = it->second; - runs[runIdx].ops.push_back(unitary); - tailOpToRun.erase(it); - tailOpToRun[op] = runIdx; - } else { - runs.push_back(OneQubitRun{}); - runs.back().ops.push_back(unitary); - tailOpToRun[op] = runs.size() - 1; - } - }); - - for (auto& run : runs) { - if (run.ops.size() < 2) { - continue; - } - (void)maybeFuseRun(rewriter, run, basis, spec); - } - } - - /// Two-qubit windows with absorbed single-qubit ops: replace when a cheaper - /// native sequence exists. - LogicalResult consolidateTwoQubitBlocks(IRRewriter& rewriter, - const NativeProfileSpec& spec) { - return fuseTwoQubitUnitaryRuns(rewriter, getOperation(), spec); - } - - /// One synthesis sweep over the whole function: rewrite every remaining - /// off-menu unitary by dispatching to `rewriteSingleQubit` / - /// `rewriteControlled` / `rewriteTwoQubit`. Returns `failure()` as soon as - /// any op cannot be lowered to the native menu. Safe to call repeatedly; - /// `runOnOperation` iterates until convergence. - LogicalResult synthesizeRemainingOps(IRRewriter& rewriter, - const NativeProfileSpec& spec, - const decomposition::EulerBasis basis) { - llvm::SmallVector ops; - collectUnitaryOpsInPreOrder(getOperation(), ops); - llvm::DenseSet erasedOps; - - for (Operation* op : ops) { - // Pointers were collected before this loop; avoid dereferencing ops - // erased by earlier rewrites in this same sweep. - if (erasedOps.contains(op)) { - continue; - } - // Nested regions under `ctrl` / `inv` are handled on the shell op - // (e.g. `ctrl { inv { ... } }`, `inv { ... }`). - if (isHiddenInsideCtrlOrInvBody(op)) { - continue; - } - if (llvm::isa(op)) { - continue; - } - auto unitary = llvm::dyn_cast(op); - if (!unitary) { - continue; - } - - if (unitary.isSingleQubit()) { - if (!allowsSingleQubitOp(unitary, spec)) { - if (failed(rewriteSingleQubit(rewriter, op, unitary, spec, basis))) { - return failure(); - } - erasedOps.insert(op); - } - continue; - } - - if (auto ctrl = llvm::dyn_cast(op)) { - const bool wasAlreadyNative = ctrlMatchesNativeMenu(ctrl, spec); - if (failed(rewriteControlled(rewriter, ctrl, spec))) { - return failure(); - } - if (!wasAlreadyNative) { - erasedOps.insert(op); - } - continue; - } - - if (unitary.isTwoQubit()) { - if (failed(rewriteTwoQubit(rewriter, op, unitary, spec))) { - return failure(); - } - erasedOps.insert(op); - continue; - } - } - return success(); - } - - /// Lower one off-menu single-qubit `op`. Constant unitaries use the - /// matrix-driven Euler synthesizer in `basis`; ops with non-constant angles - /// fall back to the symbolic `decomposeTo*` lowering of the first emitter - /// that handles them. - static LogicalResult - rewriteSingleQubit(IRRewriter& rewriter, Operation* op, - UnitaryOpInterface unitary, const NativeProfileSpec& spec, - const decomposition::EulerBasis basis) { - rewriter.setInsertionPoint(op); - const Value in = unitary.getInputQubit(0); - Matrix2x2 matrix; - if (unitary.isSingleQubit() && unitary.getUnitaryMatrix2x2(matrix)) { - const Value replaced = - emitSingleQubitMatrix(rewriter, op->getLoc(), in, matrix, basis); - rewriter.replaceOp(op, replaced); - return success(); - } - for (const auto& emitter : spec.singleQubitEmitters) { - if (!emitterHasDirectLowering(op, emitter)) { - continue; - } - if (const Value replaced = - applyDirectSingleQubitLowering(rewriter, op, in, emitter)) { - rewriter.replaceOp(op, replaced); - return success(); - } - } - op->emitError("single-qubit operation not in selected native profile"); - return failure(); - } - - /// Lower a single-control, single-target `CtrlOp` to the native profile. - /// Fast-path: already-native `CX`/`CZ` are kept as-is. Otherwise, lift the - /// controlled op to its 4x4 matrix and run the deterministic two-qubit - /// synthesizer. - static LogicalResult rewriteControlled(IRRewriter& rewriter, CtrlOp ctrl, - const NativeProfileSpec& spec) { - if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { - ctrl.emitError("native synthesis currently only supports 1-control " - "1-target controlled gates"); - return failure(); - } - auto* body = ctrl.getBodyUnitary(0).getOperation(); - const bool hasCX = llvm::isa(body); - const bool hasCZ = llvm::isa(body); - if ((usesCxEntangler(spec) && hasCX) || (usesCzEntangler(spec) && hasCZ)) { - return success(); - } - Matrix4x4 matrix; - if (hasCX || hasCZ) { - if (!getBlockTwoQubitMatrix(ctrl.getOperation(), matrix)) { - ctrl.emitError("failed to compute 4x4 matrix for CtrlOp"); - return failure(); - } - } else { - auto u = llvm::cast(ctrl.getOperation()); - if (!u.isTwoQubit() || !u.getUnitaryMatrix4x4(matrix)) { - ctrl.emitError( - "native synthesis: cannot build a constant 4x4 matrix for this " - "controlled gate (unsupported body or non-constant parameters)"); - return failure(); - } - } - rewriter.setInsertionPoint(ctrl); - Value out0; - Value out1; - if (failed(emitTwoQubitNative( - rewriter, ctrl.getLoc(), ctrl.getInputControl(0), - ctrl.getInputTarget(0), matrix, spec, out0, out1))) { - ctrl.emitError("controlled gate not allowed by selected profile"); - return failure(); - } - rewriter.replaceOp(ctrl, ValueRange{out0, out1}); - return success(); - } - - /// Lower an off-menu generic two-qubit op (`RZZ`, `XXPlusYY`, `XXMinusYY`, - /// or any arbitrary 4x4 unitary). Handles the `Rzz`-native fast path; for - /// `XXPlusYY` / `XXMinusYY` with `rzz` on the menu, uses the dedicated - /// `XX±YY -> Rzz` rewrite. All other two-qubit unitaries go through the - /// deterministic KAK synthesizer. - static LogicalResult rewriteTwoQubit(IRRewriter& rewriter, Operation* op, - UnitaryOpInterface unitary, - const NativeProfileSpec& spec) { - if (spec.allowRzz && llvm::isa(op)) { - return success(); - } - if (spec.allowRzz && - (llvm::isa(op) || llvm::isa(op))) { - rewriter.setInsertionPoint(op); - if (succeeded(rewriteXXPlusMinusYYViaRzz(rewriter, op))) { - return success(); - } - // Fall through to entangler-based synthesis when the dedicated rewrite - // could not be applied (e.g. no entangler-free realization). - } - Matrix4x4 matrix; - if (!getBlockTwoQubitMatrix(op, matrix)) { - op->emitError("unsupported two-qubit operation for selected profile"); - return failure(); - } - rewriter.setInsertionPoint(op); - Value out0; - Value out1; - if (failed(emitTwoQubitNative( - rewriter, op->getLoc(), unitary.getInputQubit(0), - unitary.getInputQubit(1), matrix, spec, out0, out1))) { - op->emitError("unsupported two-qubit operation for selected profile"); - return failure(); - } - rewriter.replaceOp(op, ValueRange{out0, out1}); - return success(); - } -}; - -} // namespace - -std::unique_ptr -createNativeGateSynthesisPass(const NativeGateSynthesisOptions& options) { - return std::make_unique(options); -} - -} // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Policy.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Policy.cpp deleted file mode 100644 index cc58c207e5..0000000000 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Policy.cpp +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h" - -#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" -#include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" - -#include -#include -#include -#include - -#include - -namespace mlir::qco::native_synth { - -bool usesCxEntangler(const NativeProfileSpec& spec) { - return llvm::is_contained(spec.entanglerBases, EntanglerBasis::Cx); -} - -bool usesCzEntangler(const NativeProfileSpec& spec) { - return llvm::is_contained(spec.entanglerBases, EntanglerBasis::Cz); -} - -/// Map a single-qubit `UnitaryOpInterface` op to the `NativeGateKind` that -/// must appear in the menu for the op to be a no-op. -static std::optional -singleQubitNativeGateKind(UnitaryOpInterface op) { - Operation* raw = op.getOperation(); - if (llvm::isa(raw)) { - return NativeGateKind::U; - } - if (llvm::isa(raw)) { - return NativeGateKind::X; - } - if (llvm::isa(raw)) { - return NativeGateKind::Sx; - } - if (llvm::isa(raw)) { - // `p` is a Z-rotation primitive for menu purposes. - return NativeGateKind::Rz; - } - if (llvm::isa(raw)) { - return NativeGateKind::Rx; - } - if (llvm::isa(raw)) { - return NativeGateKind::Ry; - } - if (llvm::isa(raw)) { - return NativeGateKind::R; - } - return std::nullopt; -} - -bool allowsSingleQubitOp(UnitaryOpInterface op, const NativeProfileSpec& spec) { - if (llvm::isa(op.getOperation())) { - return true; - } - const auto gate = singleQubitNativeGateKind(op); - return gate && spec.allowedGates.contains(*gate); -} - -/// True when `decomposeTo*` should run instead of folding to a constant `2×2` -/// matrix: trivial `Id`/`P`, dynamic-angle ops the matrix path cannot close -/// over, and (for ZSXX with direct Rx) `Rx`/`Ry`/`R`. Static angles still use -/// matrix + Euler. -bool canDirectlyDecomposeToZSXX(Operation* op, bool supportsDirectRx) { - if (llvm::isa(op)) { - return true; - } - return supportsDirectRx && llvm::isa(op); -} - -bool canDirectlyDecomposeToU3(Operation* op) { - return llvm::isa(op); -} - -bool canDirectlyDecomposeToR(Operation* op) { - return llvm::isa(op); -} - -bool canDirectlyDecomposeToAxisPair(Operation* op, AxisPair axisPair) { - if (llvm::isa(op)) { - return true; - } - switch (axisPair) { - case AxisPair::RxRz: - // `p` on an Rx/Rz axis pair folds directly to `rz(theta)`. - return llvm::isa(op); - case AxisPair::RxRy: - // No cheap symbolic lowering of `p` without `rz` available. - return llvm::isa(op); - case AxisPair::RyRz: - return llvm::isa(op); - } - llvm_unreachable("unknown axis pair"); -} - -} // namespace mlir::qco::native_synth diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp deleted file mode 100644 index 32e26347d4..0000000000 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.cpp +++ /dev/null @@ -1,255 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h" - -#include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h" -#include "mlir/Dialect/QCO/Utils/Matrix.h" - -#include -#include -#include -#include -#include -#include -#include - -#include - -namespace mlir::qco::native_synth { - -constexpr double PI = std::numbers::pi; -constexpr double HALF_PI = PI / 2.0; - -namespace { - -/// Small convenience wrapper to avoid passing rewriter/loc everywhere. Each -/// method creates the corresponding QCO op threaded through `q` and returns -/// its new output qubit value. -struct SingleQubitEmitter { - IRRewriter* rewriter; - Location loc; - - /// Create an `arith.constant` `f64` of value `v` at `loc`. - [[nodiscard]] Value constF(double v) const { - return createF64Const(*rewriter, loc, v); - } - - /// Emit `rx(theta)` with a compile-time scalar angle. - [[nodiscard]] Value rx(Value q, double theta) const { - return RXOp::create(*rewriter, loc, q, constF(theta)).getOutputQubit(0); - } - /// Emit `rx(theta)` with a runtime `f64` angle value. - [[nodiscard]] Value rx(Value q, Value theta) const { - return RXOp::create(*rewriter, loc, q, theta).getOutputQubit(0); - } - /// Emit `ry(theta)` with a compile-time scalar angle. - [[nodiscard]] Value ry(Value q, double theta) const { - return RYOp::create(*rewriter, loc, q, constF(theta)).getOutputQubit(0); - } - /// Emit `ry(theta)` with a runtime `f64` angle value. - [[nodiscard]] Value ry(Value q, Value theta) const { - return RYOp::create(*rewriter, loc, q, theta).getOutputQubit(0); - } - /// Emit `rz(theta)` with a compile-time scalar angle. - [[nodiscard]] Value rz(Value q, double theta) const { - return RZOp::create(*rewriter, loc, q, constF(theta)).getOutputQubit(0); - } - /// Emit `rz(theta)` with a runtime `f64` angle value. - [[nodiscard]] Value rz(Value q, Value theta) const { - return RZOp::create(*rewriter, loc, q, theta).getOutputQubit(0); - } - /// Emit `sx` (square-root-of-X). - [[nodiscard]] Value sx(Value q) const { - return SXOp::create(*rewriter, loc, q).getOutputQubit(0); - } - /// Emit a Pauli `x`. - [[nodiscard]] Value x(Value q) const { - return XOp::create(*rewriter, loc, q).getOutputQubit(0); - } - /// Emit `r(theta, phi)` with compile-time scalar angles. - [[nodiscard]] Value r(Value q, double theta, double phi) const { - return ROp::create(*rewriter, loc, q, constF(theta), constF(phi)) - .getOutputQubit(0); - } - /// Emit `r(theta, phi)` with runtime `f64` angle values. - [[nodiscard]] Value r(Value q, Value theta, Value phi) const { - return ROp::create(*rewriter, loc, q, theta, phi).getOutputQubit(0); - } - /// Emit `u(theta, phi, lambda)` with runtime `f64` angle values. - [[nodiscard]] Value u(Value q, Value theta, Value phi, Value lambda) const { - return UOp::create(*rewriter, loc, q, theta, phi, lambda).getOutputQubit(0); - } - /// Emit `u(theta, phi, lambda)` with compile-time scalar angles. - [[nodiscard]] Value u(Value q, double theta, double phi, - double lambda) const { - return u(q, constF(theta), constF(phi), constF(lambda)); - } -}; - -} // namespace - -Value decomposeToZSXX(IRRewriter& rewriter, Operation* op, Value inQubit, - bool supportsDirectRx) { - if (llvm::isa(op)) { - return inQubit; - } - SingleQubitEmitter e{.rewriter = &rewriter, .loc = op->getLoc()}; - if (auto p = llvm::dyn_cast(op)) { - auto q = e.rz(inQubit, p.getTheta()); - auto halfTheta = arith::MulFOp::create(rewriter, op->getLoc(), p.getTheta(), - e.constF(0.5)) - .getResult(); - GPhaseOp::create(rewriter, op->getLoc(), halfTheta); - return q; - } - if (!supportsDirectRx) { - return {}; - } - if (auto rx = llvm::dyn_cast(op)) { - return rx.getOutputQubit(0); - } - if (auto ry = llvm::dyn_cast(op)) { - return e.rz(e.rx(e.rz(inQubit, -HALF_PI), ry.getTheta()), HALF_PI); - } - if (auto r = llvm::dyn_cast(op)) { - auto negPhi = - arith::NegFOp::create(rewriter, op->getLoc(), r.getPhi()).getResult(); - return e.rz(e.rx(e.rz(inQubit, negPhi), r.getTheta()), r.getPhi()); - } - return {}; -} - -Value decomposeToU3(IRRewriter& rewriter, Operation* op, Value inQubit) { - if (llvm::isa(op)) { - return inQubit; - } - SingleQubitEmitter e{.rewriter = &rewriter, .loc = op->getLoc()}; - if (auto u = llvm::dyn_cast(op)) { - return u.getOutputQubit(0); - } - if (auto rx = llvm::dyn_cast(op)) { - return e.u(inQubit, rx.getTheta(), e.constF(-HALF_PI), e.constF(HALF_PI)); - } - if (auto ry = llvm::dyn_cast(op)) { - return e.u(inQubit, ry.getTheta(), e.constF(0.0), e.constF(0.0)); - } - if (auto rz = llvm::dyn_cast(op)) { - auto out = e.u(inQubit, e.constF(0.0), e.constF(0.0), rz.getTheta()); - auto halfTheta = arith::MulFOp::create(rewriter, op->getLoc(), - rz.getTheta(), e.constF(-0.5)) - .getResult(); - GPhaseOp::create(rewriter, op->getLoc(), halfTheta); - return out; - } - if (auto p = llvm::dyn_cast(op)) { - return e.u(inQubit, e.constF(0.0), e.constF(0.0), p.getTheta()); - } - if (auto u2 = llvm::dyn_cast(op)) { - return e.u(inQubit, e.constF(HALF_PI), u2.getPhi(), u2.getLambda()); - } - if (auto r = llvm::dyn_cast(op)) { - auto loc = op->getLoc(); - auto phiMinus = - arith::AddFOp::create(rewriter, loc, r.getPhi(), e.constF(-HALF_PI)) - .getResult(); - auto negPhi = arith::NegFOp::create(rewriter, loc, r.getPhi()).getResult(); - auto minusPlus = - arith::AddFOp::create(rewriter, loc, negPhi, e.constF(HALF_PI)) - .getResult(); - return e.u(inQubit, r.getTheta(), phiMinus, minusPlus); - } - return {}; -} - -Value emitSingleQubitMatrix(IRRewriter& rewriter, Location loc, Value inQubit, - const Matrix2x2& matrix, - const decomposition::EulerBasis basis) { - // Force emission (`hasNonBasisGate = true`, `runSize = 0`) so the matrix is - // always lowered into native gates of `basis`, including any residual - // `qco.gphase`. With these arguments `synthesizeUnitary1QEuler` never - // returns `std::nullopt`. - return *decomposition::synthesizeUnitary1QEuler( - rewriter, loc, inQubit, matrix, /*runSize=*/0, - /*hasNonBasisGate=*/true, basis); -} - -Value decomposeToR(IRRewriter& rewriter, Operation* op, Value inQubit) { - if (llvm::isa(op)) { - return inQubit; - } - SingleQubitEmitter e{.rewriter = &rewriter, .loc = op->getLoc()}; - if (auto r = llvm::dyn_cast(op)) { - return r.getOutputQubit(0); - } - if (auto rx = llvm::dyn_cast(op)) { - return e.r(inQubit, rx.getTheta(), e.constF(0.0)); - } - if (auto ry = llvm::dyn_cast(op)) { - return e.r(inQubit, ry.getTheta(), e.constF(HALF_PI)); - } - return {}; -} - -Value decomposeToAxisPair(IRRewriter& rewriter, Operation* op, Value inQubit, - AxisPair axisPair) { - if (llvm::isa(op)) { - return inQubit; - } - SingleQubitEmitter e{.rewriter = &rewriter, .loc = op->getLoc()}; - switch (axisPair) { - case AxisPair::RxRz: - if (auto rx = llvm::dyn_cast(op)) { - return rx.getOutputQubit(0); - } - if (auto rz = llvm::dyn_cast(op)) { - return rz.getOutputQubit(0); - } - if (auto p = llvm::dyn_cast(op)) { - auto q = e.rz(inQubit, p.getTheta()); - auto halfTheta = arith::MulFOp::create(rewriter, op->getLoc(), - p.getTheta(), e.constF(0.5)) - .getResult(); - GPhaseOp::create(rewriter, op->getLoc(), halfTheta); - return q; - } - return {}; - case AxisPair::RxRy: - if (auto rx = llvm::dyn_cast(op)) { - return rx.getOutputQubit(0); - } - if (auto ry = llvm::dyn_cast(op)) { - return ry.getOutputQubit(0); - } - return {}; - case AxisPair::RyRz: - if (auto ry = llvm::dyn_cast(op)) { - return ry.getOutputQubit(0); - } - if (auto rz = llvm::dyn_cast(op)) { - return rz.getOutputQubit(0); - } - if (auto p = llvm::dyn_cast(op)) { - auto q = e.rz(inQubit, p.getTheta()); - auto halfTheta = arith::MulFOp::create(rewriter, op->getLoc(), - p.getTheta(), e.constF(0.5)) - .getResult(); - GPhaseOp::create(rewriter, op->getLoc(), halfTheta); - return q; - } - return {}; - } - llvm_unreachable("unknown axis pair"); -} - -} // namespace mlir::qco::native_synth diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp deleted file mode 100644 index 226d14303b..0000000000 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.cpp +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/TwoQubit.h" - -#include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/SingleQubit.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h" -#include "mlir/Dialect/QCO/Utils/Matrix.h" - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -namespace mlir::qco::native_synth { - -constexpr double PI = std::numbers::pi; -constexpr double HALF_PI = PI / 2.0; - -/// Deterministic entangler choice: prefer CX over CZ. Returns `std::nullopt` -/// when the menu has no entangler basis. -static std::optional -selectEntangler(const NativeProfileSpec& spec) { - if (usesCxEntangler(spec)) { - return EntanglerBasis::Cx; - } - if (usesCzEntangler(spec)) { - return EntanglerBasis::Cz; - } - return std::nullopt; -} - -/// 4x4 entangler matrix for `entangler` in MQT operand order (control on qubit -/// 0 = MSB), matching `getBlockTwoQubitMatrix` / CX layout. -static Matrix4x4 entanglerMatrix(EntanglerBasis entangler) { - return entangler == EntanglerBasis::Cz ? decomposition::czGate() - : decomposition::cxGate01(); -} - -/// Run the Weyl + basis decomposer for `target` against `entangler`, returning -/// the raw single-qubit factors and entangler count (or `std::nullopt`). -static std::optional -decomposeWithEntangler(const Matrix4x4& target, EntanglerBasis entangler) { - auto decomposer = decomposition::TwoQubitBasisDecomposer::create( - entanglerMatrix(entangler), 1.0); - auto weyl = - decomposition::TwoQubitWeylDecomposition::create(target, std::nullopt); - return decomposer.twoQubitDecompose(weyl, std::nullopt); -} - -std::optional -twoQubitEntanglerCount(const Matrix4x4& target, const NativeProfileSpec& spec) { - const auto entangler = selectEntangler(spec); - if (!entangler) { - return std::nullopt; - } - const auto native = decomposeWithEntangler(target, *entangler); - if (!native) { - return std::nullopt; - } - return native->numBasisUses; -} - -LogicalResult emitTwoQubitNative(IRRewriter& rewriter, Location loc, - Value qubit0, Value qubit1, - const Matrix4x4& target, - const NativeProfileSpec& spec, - Value& outQubit0, Value& outQubit1) { - const auto entangler = selectEntangler(spec); - if (!entangler) { - return failure(); - } - const auto native = decomposeWithEntangler(target, *entangler); - if (!native) { - return failure(); - } - const auto basis = emitterEulerBasis(spec.singleQubitEmitters.front()); - - // Residual global phase not represented by the factors / entanglers. - emitGPhaseIfNonTrivial(rewriter, loc, native->globalPhase); - - Value wire0 = qubit0; - Value wire1 = qubit1; - const auto& factors = native->singleQubitFactors; - const std::uint8_t numBasisUses = native->numBasisUses; - const auto emitFactor = [&](Value& wire, std::size_t index) { - wire = emitSingleQubitMatrix(rewriter, loc, wire, factors[index], basis); - }; - const auto emitEntangler = [&]() { - // The entangler acts with its control on wire 0 and target on wire 1. - auto ctrlOp = CtrlOp::create( - rewriter, loc, ValueRange{wire0}, ValueRange{wire1}, - [&](ValueRange targetArgs) -> llvm::SmallVector { - if (*entangler == EntanglerBasis::Cz) { - return { - ZOp::create(rewriter, loc, targetArgs[0]).getOutputQubit(0)}; - } - return {XOp::create(rewriter, loc, targetArgs[0]).getOutputQubit(0)}; - }); - wire0 = ctrlOp.getOutputControl(0); - wire1 = ctrlOp.getOutputTarget(0); - }; - - // factor[2i] on wire 1, factor[2i + 1] on wire 0, then one entangler. - for (std::uint8_t i = 0; i < numBasisUses; ++i) { - emitFactor(wire1, static_cast(2 * i)); - emitFactor(wire0, static_cast((2 * i) + 1)); - emitEntangler(); - } - emitFactor(wire1, static_cast(2 * numBasisUses)); - emitFactor(wire0, static_cast((2 * numBasisUses) + 1)); - - outQubit0 = wire0; - outQubit1 = wire1; - return success(); -} - -LogicalResult rewriteXXPlusMinusYYViaRzz(IRRewriter& rewriter, Operation* op) { - rewriter.setInsertionPoint(op); - const auto loc = op->getLoc(); - const auto constF = [&](double v) { - return createF64Const(rewriter, loc, v); - }; - const auto half = [&](Value v) -> Value { - if (auto c = getConstantF64(v)) { - return constF(*c * 0.5); - } - return arith::MulFOp::create(rewriter, loc, v, constF(0.5)).getResult(); - }; - const auto neg = [&](Value v) -> Value { - if (auto c = getConstantF64(v)) { - return constF(-*c); - } - return arith::NegFOp::create(rewriter, loc, v).getResult(); - }; - const auto emitH = [&](Value q) -> Value { - auto rz0 = RZOp::create(rewriter, loc, q, constF(HALF_PI)); - auto sx = SXOp::create(rewriter, loc, rz0.getOutputQubit(0)); - return RZOp::create(rewriter, loc, sx.getOutputQubit(0), constF(HALF_PI)) - .getOutputQubit(0); - }; - // Realize `Rxx(theta)` as `(H ⊗ H) * Rzz(theta) * (H ⊗ H)`: Hadamard - // conjugation maps the Z axis to X on each qubit, and the tensor-product - // identity `(H ⊗ H) * ZZ * (H ⊗ H) == XX` lifts that to the entangler. - const auto emitRxxViaRzz = [&](Value q0, Value q1, - Value theta) -> std::pair { - q0 = emitH(q0); - q1 = emitH(q1); - auto rzz = RZZOp::create(rewriter, loc, q0, q1, theta); - q0 = rzz.getOutputQubit(0); - q1 = rzz.getOutputQubit(1); - return {emitH(q0), emitH(q1)}; - }; - // Realize `Ryy(theta)` as `(Rx(-pi/2) ⊗ Rx(-pi/2)) * Rzz(theta) * - // (Rx(pi/2) ⊗ Rx(pi/2))`: Rx(pi/2) maps Z to Y on each qubit, so the - // conjugation transports `ZZ` to `YY` just like the Hadamard sandwich - // above maps it to `XX`. - const auto emitRyyViaRzz = [&](Value q0, Value q1, - Value theta) -> std::pair { - auto rx0 = RXOp::create(rewriter, loc, q0, constF(HALF_PI)); - auto rx1 = RXOp::create(rewriter, loc, q1, constF(HALF_PI)); - auto rzz = RZZOp::create(rewriter, loc, rx0.getOutputQubit(0), - rx1.getOutputQubit(0), theta); - auto rxb0 = - RXOp::create(rewriter, loc, rzz.getOutputQubit(0), constF(-HALF_PI)); - auto rxb1 = - RXOp::create(rewriter, loc, rzz.getOutputQubit(1), constF(-HALF_PI)); - return {rxb0.getOutputQubit(0), rxb1.getOutputQubit(0)}; - }; - - // `XXPlusYY(theta, beta)` and `XXMinusYY(theta, beta)` both act as - // Rz(-beta) on q0 -> entangling core -> Rz(+beta) on q0, - // but differ in the entangling core: - // XXPlusYY: exp(-i * theta/4 * (XX + YY)) == Ryy(theta/2) * Rxx(theta/2) - // XXMinusYY: exp(-i * theta/4 * (XX - YY)) == Rxx(theta/2) * Ryy(-theta/2) - // (XX and YY commute, so the two multiplication orders produce identical - // unitaries; the distinct order and sign below are what makes `XXMinusYY` - // the "minus" variant and must be preserved even though an order flip - // alone would also compile.) - if (auto xxPlus = llvm::dyn_cast(op)) { - Value q0 = xxPlus.getInputQubit(0); - Value q1 = xxPlus.getInputQubit(1); - q0 = RZOp::create(rewriter, loc, q0, neg(xxPlus.getBeta())) - .getOutputQubit(0); - const auto halfTheta = half(xxPlus.getTheta()); - std::tie(q0, q1) = emitRyyViaRzz(q0, q1, halfTheta); - std::tie(q0, q1) = emitRxxViaRzz(q0, q1, halfTheta); - q0 = RZOp::create(rewriter, loc, q0, xxPlus.getBeta()).getOutputQubit(0); - rewriter.replaceOp(op, ValueRange{q0, q1}); - return success(); - } - if (auto xxMinus = llvm::dyn_cast(op)) { - Value q0 = xxMinus.getInputQubit(0); - Value q1 = xxMinus.getInputQubit(1); - q0 = RZOp::create(rewriter, loc, q0, neg(xxMinus.getBeta())) - .getOutputQubit(0); - const auto halfTheta = half(xxMinus.getTheta()); - std::tie(q0, q1) = emitRxxViaRzz(q0, q1, halfTheta); - std::tie(q0, q1) = emitRyyViaRzz(q0, q1, neg(halfTheta)); - q0 = RZOp::create(rewriter, loc, q0, xxMinus.getBeta()).getOutputQubit(0); - rewriter.replaceOp(op, ValueRange{q0, q1}); - return success(); - } - return failure(); -} - -} // namespace mlir::qco::native_synth diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp deleted file mode 100644 index 8c7a6ce523..0000000000 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/Utils.cpp +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h" - -#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" -#include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/Utils/Matrix.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -namespace mlir::qco::native_synth { - -Value createF64Const(IRRewriter& rewriter, Location loc, double value) { - return arith::ConstantFloatOp::create(rewriter, loc, rewriter.getF64Type(), - llvm::APFloat(value)) - .getResult(); -} - -std::optional getConstantF64(Value value) { - if (auto constant = value.getDefiningOp()) { - if (auto floatAttr = llvm::dyn_cast(constant.getValue())) { - return floatAttr.getValueAsDouble(); - } - } - return std::nullopt; -} - -void emitGPhaseIfNonTrivial(IRRewriter& rewriter, Location loc, double phase) { - constexpr double epsilon = 1e-12; - if (std::abs(phase) > epsilon) { - GPhaseOp::create(rewriter, loc, createF64Const(rewriter, loc, phase)); - } -} - -bool getBlockTwoQubitMatrix(Operation* op, Matrix4x4& matrix) { - if (llvm::isa(op)) { - return false; - } - if (auto ctrl = llvm::dyn_cast(op)) { - if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { - return false; - } - auto* body = ctrl.getBodyUnitary(0).getOperation(); - if (llvm::isa(body)) { - // CX matrix in the same 4x4 basis layout as ``getUnitaryMatrix4x4``. - matrix = Matrix4x4::fromElements(1, 0, 0, 0, // - 0, 1, 0, 0, // - 0, 0, 0, 1, // - 0, 0, 1, 0); - return true; - } - if (llvm::isa(body)) { - matrix = Matrix4x4::identity(); - matrix(3, 3) = -1.0; - return true; - } - return false; - } - auto unitary = llvm::dyn_cast(op); - if (!unitary || !unitary.isTwoQubit()) { - return false; - } - Matrix4x4 raw; - if (!unitary.getUnitaryMatrix4x4(raw)) { - return false; - } - matrix = raw; - return true; -} - -void collectUnitaryOpsInPreOrder(Operation* root, - llvm::SmallVectorImpl& ops) { - root->walk([&](Operation* op) { - if (op->getParentOfType()) { - return; - } - if (!llvm::isa(op) && op->getParentOfType()) { - return; - } - if (llvm::isa(op)) { - ops.push_back(op); - } - }); -} - -} // namespace mlir::qco::native_synth diff --git a/mlir/tools/mqt-cc/mqt-cc.cpp b/mlir/tools/mqt-cc/mqt-cc.cpp index 43625aa73a..148fcf9d7f 100644 --- a/mlir/tools/mqt-cc/mqt-cc.cpp +++ b/mlir/tools/mqt-cc/mqt-cc.cpp @@ -94,8 +94,8 @@ static llvm::cl::opt enableHadamardLifting( static llvm::cl::opt nativeGates( "native-gates", llvm::cl::desc( - "Comma-separated native gate menu for the native-gate-synthesis " - "pass (empty or whitespace-only disables synthesis)"), + "Comma-separated native gate menu for the fuse-two-qubit-unitary-runs " + "pass"), llvm::cl::value_desc("csv"), llvm::cl::init("")); /** diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 8dab83823f..395064174d 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -17,8 +17,8 @@ #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" #include "mlir/Dialect/QIR/Builder/QIRProgramBuilder.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" @@ -748,6 +748,14 @@ class CompilerPipelineNativeSynthesisConfigTest : public testing::Test { using mqt::test::isEquivalentUpToGlobalPhase; +namespace { + +using mqt::test::expandToTwoQubits; +using mqt::test::fixTwoQubitMatrixQubitOrder; +using mqt::test::QubitId; + +} // namespace + /// Compute the 4×4 unitary of a two-qubit QCO module whose qubits are /// introduced by `qco.static` ops with indices 0 and 1. Handles the op set /// that stage-4/stage-5 IR can contain for the `staticQubitsWithOps` @@ -802,8 +810,7 @@ computeStaticTwoQubitUnitary(mlir::ModuleOp module) { if (!op.getUnitaryMatrix2x2(oneQ)) { return std::nullopt; } - unitary = - mlir::qco::decomposition::expandToTwoQubits(oneQ, *qid) * unitary; + unitary = expandToTwoQubits(oneQ, *qid) * unitary; qubitIds[op.getOutputQubit(0)] = *qid; continue; } @@ -834,12 +841,9 @@ computeStaticTwoQubitUnitary(mlir::ModuleOp module) { } else if (!op.getUnitaryMatrix4x4(twoQ)) { return std::nullopt; } - const llvm::SmallVector ids{ - static_cast(*q0), - static_cast(*q1)}; - unitary = - mlir::qco::decomposition::fixTwoQubitMatrixQubitOrder(twoQ, ids) * - unitary; + const llvm::SmallVector ids{static_cast(*q0), + static_cast(*q1)}; + unitary = fixTwoQubitMatrixQubitOrder(twoQ, ids) * unitary; qubitIds[op.getOutputQubit(0)] = *q0; qubitIds[op.getOutputQubit(1)] = *q1; continue; diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/decomposition_test_utils.h b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/decomposition_test_utils.h deleted file mode 100644 index e81b725308..0000000000 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/decomposition_test_utils.h +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#pragma once - -#include "TestCaseUtils.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" -#include "mlir/Dialect/QCO/Utils/Matrix.h" - -#include -#include -#include -#include -#include -#include - -namespace mlir::qco::decomposition_test { - -using mqt::test::isEquivalentUpToGlobalPhase; - -/// Standard `U3(theta, phi, lambda)` matrix. Thin wrapper over the library -/// `uMatrix` so every test uses the same implementation. -[[nodiscard]] inline Matrix2x2 u3Matrix(double theta, double phi, - double lambda) { - return decomposition::uMatrix(theta, phi, lambda); -} - -namespace detail { - -/// Generate a Haar-ish random unitary as a row-major `dim x dim` buffer via -/// modified Gram-Schmidt on Gaussian-random complex columns. -[[nodiscard]] inline std::vector> -randomUnitaryData(std::size_t dim, std::mt19937& rng) { - std::normal_distribution normalDist(0.0, 1.0); - std::vector>> columns( - dim, std::vector>(dim)); - for (auto& column : columns) { - for (auto& entry : column) { - entry = std::complex(normalDist(rng), normalDist(rng)); - } - } - for (std::size_t j = 0; j < dim; ++j) { - for (std::size_t k = 0; k < j; ++k) { - std::complex projection{0.0, 0.0}; - for (std::size_t i = 0; i < dim; ++i) { - projection += std::conj(columns[k][i]) * columns[j][i]; - } - for (std::size_t i = 0; i < dim; ++i) { - columns[j][i] -= projection * columns[k][i]; - } - } - double norm = 0.0; - for (std::size_t i = 0; i < dim; ++i) { - norm += std::norm(columns[j][i]); - } - norm = std::sqrt(norm); - for (std::size_t i = 0; i < dim; ++i) { - columns[j][i] /= norm; - } - } - std::vector> data(dim * dim); - for (std::size_t row = 0; row < dim; ++row) { - for (std::size_t col = 0; col < dim; ++col) { - data[(row * dim) + col] = columns[col][row]; - } - } - return data; -} - -} // namespace detail - -/// Random `4×4` unitary matrix. -[[nodiscard]] inline Matrix4x4 randomUnitary4x4(std::mt19937& rng) { - const auto data = detail::randomUnitaryData(4, rng); - const Matrix4x4 unitary = Matrix4x4::fromElements( - data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], - data[8], data[9], data[10], data[11], data[12], data[13], data[14], - data[15]); - assert((unitary.adjoint() * unitary).isIdentity(1e-12)); - return unitary; -} - -} // namespace mlir::qco::decomposition_test diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp index 67755f2d1f..3cfbcdbabd 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -104,22 +104,11 @@ class EulerSynthesisExactTest // Euler synthesis support //===----------------------------------------------------------------------===// -[[nodiscard]] static Matrix2x2 rzMatrix(const double theta) { - const auto m00 = std::polar(1.0, -theta / 2.0); - const auto m11 = std::polar(1.0, theta / 2.0); - return Matrix2x2::fromElements(m00, 0, 0, m11); -} - -[[nodiscard]] static Matrix2x2 ryMatrix(const double theta) { - const auto m00 = std::cos(theta / 2.0); - const auto m01 = -std::sin(theta / 2.0); - return Matrix2x2::fromElements(m00, m01, -m01, m00); -} - [[nodiscard]] static Matrix2x2 randomUnitaryMatrix(std::mt19937& rng) { std::uniform_real_distribution dist(-std::numbers::pi, std::numbers::pi); - const Matrix2x2 su2 = - rzMatrix(dist(rng)) * ryMatrix(dist(rng)) * rzMatrix(dist(rng)); + const Matrix2x2 su2 = decomposition::rzMatrix(dist(rng)) * + decomposition::ryMatrix(dist(rng)) * + decomposition::rzMatrix(dist(rng)); const Complex globalPhase = std::polar(1.0, dist(rng)); return Matrix2x2::fromElements( globalPhase * su2(0, 0), globalPhase * su2(0, 1), globalPhase * su2(1, 0), @@ -351,52 +340,59 @@ TEST_P(ZSXXShortcutTest, SynthesisMatchesGateCount) { INSTANTIATE_TEST_SUITE_P( ZSXXShortcuts, ZSXXShortcutTest, - testing::Values( - ZSXXShortcutCase{ - "Identity", - [](MLIRContext*) -> Matrix2x2 { return Matrix2x2::identity(); }, 0, - 0, 0}, - ZSXXShortcutCase{ - "PauliX", - [](MLIRContext*) -> Matrix2x2 { return XOp::getUnitaryMatrix(); }, - 0, 0, 1}, - ZSXXShortcutCase{"PureZ", - [](MLIRContext*) -> Matrix2x2 { - return rzMatrix(0.3) * rzMatrix(0.7); - }, - 1, 0, 0}, - ZSXXShortcutCase{"ZYZNearZeroTheta", - [](MLIRContext*) -> Matrix2x2 { - constexpr double tol = 0.5 * mlir::utils::TOLERANCE; - return rzMatrix(0.4) * ryMatrix(tol) * rzMatrix(0.3); - }, - 1, 0, 0}, - ZSXXShortcutCase{"RYHalfPi", - [](MLIRContext* ctx) -> Matrix2x2 { - return rotationMatrix(ctx, - std::numbers::pi / 2.0); - }, - 2, 1, 0}, - ZSXXShortcutCase{"RYNearHalfPi", - [](MLIRContext* ctx) -> Matrix2x2 { - return rotationMatrix( - ctx, (std::numbers::pi / 2.0) + - (0.5 * mlir::utils::TOLERANCE)); - }, - 2, 1, 0}, - ZSXXShortcutCase{"RYNearZero", - [](MLIRContext* ctx) -> Matrix2x2 { - return rotationMatrix( - ctx, 0.5 * mlir::utils::TOLERANCE); - }, - 0, 0, 0}, - ZSXXShortcutCase{"RYNearPi", - [](MLIRContext* ctx) -> Matrix2x2 { - return rotationMatrix( - ctx, std::numbers::pi - - (0.5 * mlir::utils::TOLERANCE)); - }, - 1, 0, 1}), + testing::Values(ZSXXShortcutCase{"Identity", + [](MLIRContext*) -> Matrix2x2 { + return Matrix2x2::identity(); + }, + 0, 0, 0}, + ZSXXShortcutCase{"PauliX", + [](MLIRContext*) -> Matrix2x2 { + return XOp::getUnitaryMatrix(); + }, + 0, 0, 1}, + ZSXXShortcutCase{"PureZ", + [](MLIRContext*) -> Matrix2x2 { + return decomposition::rzMatrix(0.3) * + decomposition::rzMatrix(0.7); + }, + 1, 0, 0}, + ZSXXShortcutCase{"ZYZNearZeroTheta", + [](MLIRContext*) -> Matrix2x2 { + constexpr double tol = + 0.5 * mlir::utils::TOLERANCE; + return decomposition::rzMatrix(0.4) * + decomposition::ryMatrix(tol) * + decomposition::rzMatrix(0.3); + }, + 1, 0, 0}, + ZSXXShortcutCase{"RYHalfPi", + [](MLIRContext* ctx) -> Matrix2x2 { + return rotationMatrix( + ctx, std::numbers::pi / 2.0); + }, + 2, 1, 0}, + ZSXXShortcutCase{"RYNearHalfPi", + [](MLIRContext* ctx) -> Matrix2x2 { + return rotationMatrix( + ctx, + (std::numbers::pi / 2.0) + + (0.5 * mlir::utils::TOLERANCE)); + }, + 2, 1, 0}, + ZSXXShortcutCase{"RYNearZero", + [](MLIRContext* ctx) -> Matrix2x2 { + return rotationMatrix( + ctx, 0.5 * mlir::utils::TOLERANCE); + }, + 0, 0, 0}, + ZSXXShortcutCase{"RYNearPi", + [](MLIRContext* ctx) -> Matrix2x2 { + return rotationMatrix( + ctx, + std::numbers::pi - + (0.5 * mlir::utils::TOLERANCE)); + }, + 1, 0, 1}), [](const testing::TestParamInfo& info) { return std::string(info.param.label); }); @@ -541,11 +537,11 @@ static void expectFusePreserved(func::FuncOp funcOp, const Matrix2x2& original, } [[nodiscard]] static Matrix2x2 splitFixtureRZSXSegmentMatrix() { - return SXOp::getUnitaryMatrix() * rzMatrix(0.321); + return SXOp::getUnitaryMatrix() * decomposition::rzMatrix(0.321); } [[nodiscard]] static Matrix2x2 overlongZSXXPureZRunMatrix() { - return SXOp::getUnitaryMatrix() * rzMatrix(std::numbers::pi) * + return SXOp::getUnitaryMatrix() * decomposition::rzMatrix(std::numbers::pi) * SXOp::getUnitaryMatrix(); } template diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index 252ecdd9e1..c0fd4aeb78 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -8,24 +8,25 @@ * Licensed under the MIT License */ -#include "decomposition_test_utils.h" +#include "TestCaseUtils.h" #include "mlir/Conversion/QCToQCO/QCToQCO.h" #include "mlir/Dialect/QC/Builder/QCProgramBuilder.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #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/Helpers.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/WeylDecomposition.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" -#include "mlir/Dialect/QCO/Utils/Matrix.h" + +namespace mlir::qco::native_synth { +bool getBlockTwoQubitMatrix(Operation* op, Matrix4x4& matrix); +} // namespace mlir::qco::native_synth #include #include #include #include +#include #include #include #include @@ -38,6 +39,7 @@ #include #include +#include #include #include #include @@ -46,13 +48,137 @@ #include #include #include +#include + +namespace { + +constexpr double SANITY_CHECK_PRECISION = 1e-12; + +[[nodiscard]] bool isUnitaryMatrix(const mlir::qco::Matrix2x2& matrix, + double tolerance = 1e-12) { + return (matrix.adjoint() * matrix).isIdentity(tolerance); +} + +[[nodiscard]] double remEuclid(double a, double b) { + if (b == 0.0) { + llvm::reportFatalInternalError("remEuclid expects non-zero divisor"); + } + const auto r = std::fmod(a, b); + return (r < 0.0) ? r + std::abs(b) : r; +} + +[[nodiscard]] double traceToFidelity(const std::complex& x) { + const auto xAbs = std::abs(x); + return (4.0 + (xAbs * xAbs)) / 20.0; +} + +[[nodiscard]] std::complex globalPhaseFactor(double globalPhase) { + return std::exp(std::complex{0, 1} * globalPhase); +} + +[[nodiscard]] mlir::qco::Matrix4x4 rxxMatrix(double theta) { + const auto cosTheta = std::cos(theta / 2.); + const mlir::qco::Complex misin{0., -std::sin(theta / 2.)}; + return mlir::qco::Matrix4x4::fromElements(cosTheta, 0, 0, misin, // + 0, cosTheta, misin, 0, // + 0, misin, cosTheta, 0, // + misin, 0, 0, cosTheta); +} + +[[nodiscard]] mlir::qco::Matrix4x4 ryyMatrix(double theta) { + const auto cosTheta = std::cos(theta / 2.); + const mlir::qco::Complex isin{0., std::sin(theta / 2.)}; + const mlir::qco::Complex misin{0., -std::sin(theta / 2.)}; + return mlir::qco::Matrix4x4::fromElements(cosTheta, 0, 0, isin, // + 0, cosTheta, misin, 0, // + 0, misin, cosTheta, 0, // + isin, 0, 0, cosTheta); +} + +[[nodiscard]] mlir::qco::Matrix4x4 rzzMatrix(double theta) { + const auto cosTheta = std::cos(theta / 2.); + const auto sinTheta = std::sin(theta / 2.); + const mlir::qco::Complex em{cosTheta, -sinTheta}; + const mlir::qco::Complex ep{cosTheta, sinTheta}; + return mlir::qco::Matrix4x4::fromElements(em, 0, 0, 0, // + 0, ep, 0, 0, // + 0, 0, ep, 0, // + 0, 0, 0, em); +} + +[[nodiscard]] const mlir::qco::Matrix4x4& cxGate01() { + static const mlir::qco::Matrix4x4 matrix = + mlir::qco::Matrix4x4::fromElements(1, 0, 0, 0, // + 0, 1, 0, 0, // + 0, 0, 0, 1, // + 0, 0, 1, 0); + return matrix; +} + +[[nodiscard]] const mlir::qco::Matrix4x4& cxGate10() { + static const mlir::qco::Matrix4x4 matrix = + mlir::qco::Matrix4x4::fromElements(1, 0, 0, 0, // + 0, 0, 0, 1, // + 0, 0, 1, 0, // + 0, 1, 0, 0); + return matrix; +} + +[[nodiscard]] std::vector> +randomUnitaryData(std::size_t dim, std::mt19937& rng) { + std::normal_distribution normalDist(0.0, 1.0); + std::vector>> columns( + dim, std::vector>(dim)); + for (auto& column : columns) { + for (auto& entry : column) { + entry = std::complex(normalDist(rng), normalDist(rng)); + } + } + for (std::size_t j = 0; j < dim; ++j) { + for (std::size_t k = 0; k < j; ++k) { + std::complex projection{0.0, 0.0}; + for (std::size_t i = 0; i < dim; ++i) { + projection += std::conj(columns[k][i]) * columns[j][i]; + } + for (std::size_t i = 0; i < dim; ++i) { + columns[j][i] -= projection * columns[k][i]; + } + } + double norm = 0.0; + for (std::size_t i = 0; i < dim; ++i) { + norm += std::norm(columns[j][i]); + } + norm = std::sqrt(norm); + for (std::size_t i = 0; i < dim; ++i) { + columns[j][i] /= norm; + } + } + std::vector> data(dim * dim); + for (std::size_t row = 0; row < dim; ++row) { + for (std::size_t col = 0; col < dim; ++col) { + data[(row * dim) + col] = columns[col][row]; + } + } + return data; +} + +[[nodiscard]] mlir::qco::Matrix4x4 randomUnitary4x4(std::mt19937& rng) { + const auto data = randomUnitaryData(4, rng); + const mlir::qco::Matrix4x4 unitary = mlir::qco::Matrix4x4::fromElements( + data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], + data[8], data[9], data[10], data[11], data[12], data[13], data[14], + data[15]); + assert((unitary.adjoint() * unitary).isIdentity(1e-12)); + return unitary; +} + +} // namespace using namespace mlir; using namespace mlir::qco; using namespace mlir::qco::decomposition; -using namespace mlir::qco::decomposition_test; -using namespace mlir::qco::helpers; using namespace mlir::qco::native_synth; +using namespace mqt::test; // Weyl / basis / helpers. @@ -98,7 +224,7 @@ class WeylDecompositionTest : public testing::TestWithParam { [[nodiscard]] static std::complex globalPhaseFactor(const TwoQubitWeylDecomposition& decomposition) { - return helpers::globalPhaseFactor(decomposition.globalPhase()); + return ::globalPhaseFactor(decomposition.globalPhase()); } [[nodiscard]] static Matrix4x4 can(const TwoQubitWeylDecomposition& decomposition) { @@ -147,10 +273,10 @@ TEST(WeylDecompositionStandalone, EXPECT_LE(decomp.a(), piOver4 + 1e-10); EXPECT_LE(decomp.b(), piOver4 + 1e-10); EXPECT_LE(decomp.c(), piOver4 + 1e-10); - EXPECT_TRUE(helpers::isUnitaryMatrix(decomp.k1l())); - EXPECT_TRUE(helpers::isUnitaryMatrix(decomp.k2l())); - EXPECT_TRUE(helpers::isUnitaryMatrix(decomp.k1r())); - EXPECT_TRUE(helpers::isUnitaryMatrix(decomp.k2r())); + EXPECT_TRUE(isUnitaryMatrix(decomp.k1l())); + EXPECT_TRUE(isUnitaryMatrix(decomp.k2l())); + EXPECT_TRUE(isUnitaryMatrix(decomp.k1r())); + EXPECT_TRUE(isUnitaryMatrix(decomp.k2r())); } TEST(WeylDecompositionStandalone, Random) { @@ -220,7 +346,7 @@ class BasisDecomposerTest : public testing::TestWithParam< matrix = entangler * matrix; matrix = layer(static_cast(i) + 1) * matrix; } - return matrix * helpers::globalPhaseFactor(decomposition.globalPhase); + return matrix * ::globalPhaseFactor(decomposition.globalPhase); } protected: @@ -453,9 +579,8 @@ computeTwoQubitUnitaryFromModule(const OwningOpRef& moduleOp) { if (!extractSingleQubitMatrix(op, oneQ)) { return std::nullopt; } - unitary = decomposition::expandToTwoQubits( - oneQ, static_cast(*qid)) * - unitary; + unitary = + expandToTwoQubits(oneQ, static_cast(*qid)) * unitary; const auto qOut = getUnitaryQubitResult(op, 0); if (!qOut) { return std::nullopt; @@ -479,11 +604,9 @@ computeTwoQubitUnitaryFromModule(const OwningOpRef& moduleOp) { if (!extractTwoQubitMatrix(op, twoQ)) { return std::nullopt; } - const llvm::SmallVector ids{ - static_cast(*q0id), - static_cast(*q1id)}; - unitary = - decomposition::fixTwoQubitMatrixQubitOrder(twoQ, ids) * unitary; + const llvm::SmallVector ids{static_cast(*q0id), + static_cast(*q1id)}; + unitary = fixTwoQubitMatrixQubitOrder(twoQ, ids) * unitary; const auto q0Out = getUnitaryQubitResult(op, 0); const auto q1Out = getUnitaryQubitResult(op, 1); if (!q0Out || !q1Out) { diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis.cpp index 63ff826c61..d64b256dca 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis.cpp @@ -15,17 +15,20 @@ #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/NativeSpec.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Policy.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Types.h" -#include "mlir/Dialect/QCO/Transforms/NativeSynthesis/Utils.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" #include "qc_programs.h" +namespace mlir::qco::native_synth { +bool allowsSingleQubitOp(UnitaryOpInterface op, + const decomposition::NativeProfileSpec& spec); +bool getBlockTwoQubitMatrix(Operation* op, Matrix4x4& matrix); +} // namespace mlir::qco::native_synth + #include #include +#include #include #include #include @@ -262,8 +265,8 @@ class NativeSynthesisPassTest : public testing::Test { const std::string& nativeGates) { mlir::PassManager pm(moduleOp->getContext()); pm.addPass(mlir::createQCToQCO()); - pm.addPass(mlir::qco::createNativeGateSynthesisPass( - mlir::qco::NativeGateSynthesisOptions{ + pm.addPass(mlir::qco::createFuseTwoQubitUnitaryRuns( + mlir::qco::FuseTwoQubitUnitaryRunsOptions{ .nativeGates = nativeGates, })); ASSERT_TRUE(mlir::succeeded(pm.run(*moduleOp))); @@ -297,8 +300,8 @@ class NativeSynthesisPassTest : public testing::Test { auto moduleOp = buildFn(); mlir::PassManager pm(moduleOp->getContext()); pm.addPass(mlir::createQCToQCO()); - pm.addPass(mlir::qco::createNativeGateSynthesisPass( - mlir::qco::NativeGateSynthesisOptions{ + pm.addPass(mlir::qco::createFuseTwoQubitUnitaryRuns( + mlir::qco::FuseTwoQubitUnitaryRunsOptions{ .nativeGates = nativeGates, })); EXPECT_TRUE(mlir::failed(pm.run(*moduleOp))); @@ -459,6 +462,10 @@ bool extractTwoQubitMatrix(qco::UnitaryOpInterface op, Matrix4x4& out) { return op.getUnitaryMatrix4x4(out); } +using mqt::test::expandToTwoQubits; +using mqt::test::fixTwoQubitMatrixQubitOrder; +using mqt::test::QubitId; + std::optional computeTwoQubitUnitaryFromModule(const OwningOpRef& moduleOp) { ModuleOp module = moduleOp.get(); @@ -514,9 +521,8 @@ computeTwoQubitUnitaryFromModule(const OwningOpRef& moduleOp) { if (!extractSingleQubitMatrix(op, oneQ)) { return std::nullopt; } - unitary = decomposition::expandToTwoQubits( - oneQ, static_cast(*qid)) * - unitary; + unitary = + expandToTwoQubits(oneQ, static_cast(*qid)) * unitary; const auto qOut = getUnitaryQubitResult(op, 0); if (!qOut) { return std::nullopt; @@ -542,11 +548,9 @@ computeTwoQubitUnitaryFromModule(const OwningOpRef& moduleOp) { } // Reorder the gate's (operand0, operand1) layout into the canonical // (qubit 0, qubit 1) order used by `unitary`. - const llvm::SmallVector ids{ - static_cast(*q0id), - static_cast(*q1id)}; - unitary = - decomposition::fixTwoQubitMatrixQubitOrder(twoQ, ids) * unitary; + const llvm::SmallVector ids{static_cast(*q0id), + static_cast(*q1id)}; + unitary = fixTwoQubitMatrixQubitOrder(twoQ, ids) * unitary; const auto q0Out = getUnitaryQubitResult(op, 0); const auto q1Out = getUnitaryQubitResult(op, 1); if (!q0Out || !q1Out) { @@ -869,44 +873,40 @@ static void determinismSwap(mlir::qc::QCProgramBuilder& b) { // --- NativeSpec / NativePolicy --- TEST(NativeSpecTest, ResolveIbmBasicCx) { - const auto spec = resolveNativeGatesSpec("x,sx,rz,cx"); + const auto spec = decomposition::parseNativeSpec("x,sx,rz,cx"); ASSERT_TRUE(spec); - EXPECT_TRUE(spec->allowedGates.contains(NativeGateKind::Cx)); - EXPECT_TRUE(spec->allowedGates.contains(NativeGateKind::X)); + EXPECT_TRUE(spec->allowedGates.contains(decomposition::NativeGateKind::Cx)); + EXPECT_TRUE(spec->allowedGates.contains(decomposition::NativeGateKind::X)); EXPECT_FALSE(spec->allowRzz); } TEST(NativeSpecTest, ResolveRejectsUnknownToken) { - EXPECT_FALSE(resolveNativeGatesSpec("x,sx,rz,not-a-gate").has_value()); + EXPECT_FALSE( + decomposition::parseNativeSpec("x,sx,rz,not-a-gate").has_value()); } TEST(NativeSpecTest, PhaseAliasPMatchesRzInIbmStyleMenu) { - const auto pMenu = resolveNativeGatesSpec("x,sx,p,cx"); - const auto rzMenu = resolveNativeGatesSpec("x,sx,rz,cx"); + const auto pMenu = decomposition::parseNativeSpec("x,sx,p,cx"); + const auto rzMenu = decomposition::parseNativeSpec("x,sx,rz,cx"); ASSERT_TRUE(pMenu); ASSERT_TRUE(rzMenu); EXPECT_EQ(pMenu->allowedGates, rzMenu->allowedGates); } -TEST(NativeSpecTest, EmitterEulerBasisForAxisPair) { - EXPECT_EQ(emitterEulerBasis(SingleQubitEmitterSpec{ - .mode = SingleQubitMode::AxisPair, .axisPair = AxisPair::RxRz}), - EulerBasis::XZX); - EXPECT_EQ(emitterEulerBasis(SingleQubitEmitterSpec{ - .mode = SingleQubitMode::AxisPair, .axisPair = AxisPair::RyRz}), - EulerBasis::ZYZ); -} - TEST(NativePolicyTest, UsesCxAndCzFromResolvedSpec) { - const auto cxOnly = resolveNativeGatesSpec("u,cx"); + const auto cxOnly = decomposition::parseNativeSpec("u,cx"); ASSERT_TRUE(cxOnly); - EXPECT_TRUE(usesCxEntangler(*cxOnly)); - EXPECT_FALSE(usesCzEntangler(*cxOnly)); + EXPECT_TRUE(llvm::is_contained(cxOnly->entanglerBases, + decomposition::EntanglerBasis::Cx)); + EXPECT_FALSE(llvm::is_contained(cxOnly->entanglerBases, + decomposition::EntanglerBasis::Cz)); - const auto both = resolveNativeGatesSpec("u,cx,cz"); + const auto both = decomposition::parseNativeSpec("u,cx,cz"); ASSERT_TRUE(both); - EXPECT_TRUE(usesCxEntangler(*both)); - EXPECT_TRUE(usesCzEntangler(*both)); + EXPECT_TRUE(llvm::is_contained(both->entanglerBases, + decomposition::EntanglerBasis::Cx)); + EXPECT_TRUE(llvm::is_contained(both->entanglerBases, + decomposition::EntanglerBasis::Cz)); } // NOLINTNEXTLINE(misc-use-internal-linkage) @@ -925,7 +925,7 @@ class NativePolicyAllowsOpTest : public ::testing::Test { }; TEST_F(NativePolicyAllowsOpTest, AllowsSingleQubitOpRespectsMenu) { - const auto spec = resolveNativeGatesSpec("x,sx,rz,cx"); + const auto spec = decomposition::parseNativeSpec("x,sx,rz,cx"); ASSERT_TRUE(spec); Value q = builder.staticQubit(0); q = builder.x(q); @@ -942,7 +942,7 @@ TEST_F(NativePolicyAllowsOpTest, AllowsSingleQubitOpRespectsMenu) { } TEST_F(NativePolicyAllowsOpTest, RejectsSingleQubitOpNotInMenu) { - const auto spec = resolveNativeGatesSpec("u,cx"); + const auto spec = decomposition::parseNativeSpec("u,cx"); ASSERT_TRUE(spec); Value q = builder.staticQubit(0); q = builder.x(q); @@ -958,20 +958,6 @@ TEST_F(NativePolicyAllowsOpTest, RejectsSingleQubitOpNotInMenu) { llvm::cast(xop.getOperation()), *spec)); } -TEST_F(NativePolicyAllowsOpTest, CanDirectlyDecomposeToU3OnRxInCircuit) { - Value q = builder.staticQubit(0); - q = builder.rx(0.1, q); - auto mod = builder.finalize(); - ASSERT_TRUE(mod); - RXOp rx; - mod->walk([&](RXOp op) { - rx = op; - return WalkResult::interrupt(); - }); - ASSERT_TRUE(rx); - EXPECT_TRUE(canDirectlyDecomposeToU3(rx.getOperation())); -} - // --- Pass profile coverage --- // NOLINTNEXTLINE(misc-use-internal-linkage) diff --git a/mlir/unittests/TestCaseUtils.h b/mlir/unittests/TestCaseUtils.h index 7603fda8be..f698ece122 100644 --- a/mlir/unittests/TestCaseUtils.h +++ b/mlir/unittests/TestCaseUtils.h @@ -10,11 +10,14 @@ #pragma once +#include "mlir/Dialect/QCO/Utils/Matrix.h" #include "mlir/Support/PrettyPrinting.h" #include #include +#include #include +#include #include #include @@ -156,6 +159,62 @@ class DeferredPrinter { std::vector> entries_; }; +// --------------------------------------------------------------------------- +// Gate-matrix factories and two-qubit layout helpers shared by the +// decomposition / native-synthesis unit tests. They build reference unitaries +// for equivalence checks and are intentionally test-only, so they live here +// rather than in the production decomposition headers. +// --------------------------------------------------------------------------- + +/// Hadamard gate (2x2). +[[nodiscard]] inline const mlir::qco::Matrix2x2& hGate() { + constexpr double frac1Sqrt2 = + 0.707106781186547524400844362104849039284835937688474036588L; + static const mlir::qco::Matrix2x2 matrix = mlir::qco::Matrix2x2::fromElements( + frac1Sqrt2, frac1Sqrt2, frac1Sqrt2, -frac1Sqrt2); + return matrix; +} + +/// Logical qubit index within a two-qubit reference matrix. +using QubitId = std::size_t; + +/// `SWAP` gate in MQT operand order (qubit 0 = MSB). +[[nodiscard]] inline const mlir::qco::Matrix4x4& swapGate() { + static const mlir::qco::Matrix4x4 matrix = + mlir::qco::Matrix4x4::fromElements(1, 0, 0, 0, // + 0, 0, 1, 0, // + 0, 1, 0, 0, // + 0, 0, 0, 1); + return matrix; +} + +/// Embed a single-qubit matrix into the two-qubit space acting on `qubitId`. +[[nodiscard]] inline mlir::qco::Matrix4x4 +expandToTwoQubits(const mlir::qco::Matrix2x2& singleQubitMatrix, + QubitId qubitId) { + if (qubitId == 0) { + return kron(singleQubitMatrix, mlir::qco::Matrix2x2::identity()); + } + if (qubitId == 1) { + return kron(mlir::qco::Matrix2x2::identity(), singleQubitMatrix); + } + llvm::reportFatalInternalError("Invalid qubit id for single-qubit expansion"); +} + +/// Reorder a two-qubit matrix so it acts on qubits `{0, 1}` in MQT order. +[[nodiscard]] inline mlir::qco::Matrix4x4 +fixTwoQubitMatrixQubitOrder(const mlir::qco::Matrix4x4& twoQubitMatrix, + const llvm::SmallVector& qubitIds) { + if (qubitIds == llvm::SmallVector{1, 0}) { + return swapGate() * twoQubitMatrix * swapGate(); + } + if (qubitIds == llvm::SmallVector{0, 1}) { + return twoQubitMatrix; + } + llvm::reportFatalInternalError( + "Invalid qubit IDs for fixing two-qubit matrix"); +} + } // namespace mqt::test #define MQT_NAMED_BUILDER(fn) ::mqt::test::namedBuilder(#fn, fn) From a6b7c98024cfb418f2fc85b9ed089eb07fd4ebaa Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Fri, 19 Jun 2026 13:58:00 +0200 Subject: [PATCH 049/122] =?UTF-8?q?=F0=9F=94=A5=20More=20merging=20and=20c?= =?UTF-8?q?lean=20up?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/Decomposition/Weyl.h | 7 + mlir/include/mlir/Dialect/QCO/Utils/Matrix.h | 72 ++ .../QCO/Transforms/Decomposition/Weyl.cpp | 135 ++- .../FuseTwoQubitUnitaryRuns.cpp | 781 ++++-------------- mlir/lib/Dialect/QCO/Utils/Matrix.cpp | 184 +++++ .../Compiler/test_compiler_pipeline.cpp | 2 - .../test_euler_decomposition.cpp | 121 +-- .../Decomposition/test_weyl_decomposition.cpp | 300 +++---- .../NativeSynthesis/test_native_synthesis.cpp | 698 +++++----------- .../Dialect/QCO/Utils/test_unitary_matrix.cpp | 28 + mlir/unittests/TestCaseUtils.h | 161 +++- mlir/unittests/programs/qc_programs.cpp | 2 +- 12 files changed, 989 insertions(+), 1502 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h index b071e8ab0e..bb4776b0b6 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h @@ -10,6 +10,7 @@ #pragma once +#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" #include @@ -448,6 +449,12 @@ class TwoQubitBasisDecomposer { Matrix2x2 q2r; }; +/** + * @brief Euler basis used to emit single-qubit factors for @p emitter. + */ +[[nodiscard]] EulerBasis +emitterEulerBasis(const SingleQubitEmitterSpec& emitter); + /** * @brief Parses a comma-separated native-gate menu (e.g. `"u,cx,rzz"`). * diff --git a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h index 1ac9b8f8c3..1eba783d0b 100644 --- a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h +++ b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h @@ -605,6 +605,34 @@ class DynamicMatrix { [[nodiscard]] bool isApprox(const DynamicMatrix& other, double tol = MATRIX_TOLERANCE) const; + /** + * @brief Returns the trace of this matrix. + * @return Sum of diagonal entries. + */ + [[nodiscard]] Complex trace() const; + + /** + * @brief Matrix product `*this * rhs`. + * @param rhs Right-hand factor. + * @return Product of the two matrices. + */ + [[nodiscard]] DynamicMatrix operator*(const DynamicMatrix& rhs) const; + + /** + * @brief Element-wise scaling by a complex scalar. + * @param scalar Factor applied to every matrix entry. + * @return Scaled copy of this matrix. + */ + [[nodiscard]] DynamicMatrix operator*(const Complex& scalar) const; + + /** + * @brief Checks whether this matrix is approximately the identity. + * @param tol Maximum allowed complex modulus of each off-diagonal entry and + * each diagonal deviation from one. + * @return True when the matrix is close to the identity. + */ + [[nodiscard]] bool isIdentity(double tol = MATRIX_TOLERANCE) const; + private: struct Impl; std::unique_ptr impl_; @@ -645,6 +673,9 @@ inline constexpr bool /// @copydoc operator*(const Complex&, const Matrix2x2&) [[nodiscard]] Matrix4x4 operator*(const Complex& scalar, const Matrix4x4& matrix); +/// @copydoc operator*(const Complex&, const Matrix2x2&) +[[nodiscard]] DynamicMatrix operator*(const Complex& scalar, + const DynamicMatrix& matrix); /** * @brief Eigenvalues and eigenvectors of a real symmetric `4x4` matrix. @@ -674,4 +705,45 @@ struct SymmetricEigen4 { [[nodiscard]] SymmetricEigen4 jacobiSymmetricEigen(const std::array& symmetric); +/// `SWAP` on two qubits. +[[nodiscard]] const Matrix4x4& twoQubitSwapMatrix(); + +/** + * @brief Embed a single-qubit matrix into the two-qubit space on @p qubitIndex. + * + * @param qubitIndex `0` for the high bit, `1` for the low bit. + */ +[[nodiscard]] Matrix4x4 embedSingleQubitInTwoQubit(const Matrix2x2& matrix, + std::size_t qubitIndex); + +/** + * @brief Reorder a two-qubit matrix to act on qubits `{0, 1}`. + * + * @param q0Index Wire index of operand 0; @p q1Index wire index of operand 1. + */ +[[nodiscard]] Matrix4x4 reorderTwoQubitMatrix(const Matrix4x4& matrix, + std::size_t q0Index, + std::size_t q1Index); + +/** + * @brief Embed a single-qubit matrix into an @p numQubits-qubit Hilbert space. + * + * Qubit @p qubitIndex uses the same MSB-first convention as + * @ref embedSingleQubitInTwoQubit. + */ +[[nodiscard]] DynamicMatrix embedSingleQubitInNqubit(const Matrix2x2& matrix, + std::size_t numQubits, + std::size_t qubitIndex); + +/** + * @brief Embed a two-qubit matrix into an @p numQubits-qubit Hilbert space. + * + * Operand 0 labels the high bit of the pair and acts on @p q0Index; operand 1 + * labels the low bit and acts on @p q1Index. + */ +[[nodiscard]] DynamicMatrix embedTwoQubitInNqubit(const Matrix4x4& matrix, + std::size_t numQubits, + std::size_t q0Index, + std::size_t q1Index); + } // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp index d8b222b656..29e76dcceb 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp @@ -49,43 +49,6 @@ using mlir::qco::Complex; using mlir::qco::Matrix2x2; using mlir::qco::Matrix4x4; -Matrix2x2 rxMatrix(double theta) { - const auto halfTheta = theta / 2.; - const Complex cos{std::cos(halfTheta), 0.}; - const Complex isin{0., -std::sin(halfTheta)}; - return Matrix2x2::fromElements(cos, isin, isin, cos); -} - -Matrix2x2 ryMatrix(double theta) { - const auto halfTheta = theta / 2.; - const Complex cos{std::cos(halfTheta), 0.}; - const Complex sin{std::sin(halfTheta), 0.}; - return Matrix2x2::fromElements(cos, -sin, sin, cos); -} - -Matrix2x2 rzMatrix(double theta) { - return Matrix2x2::fromElements( - Complex{std::cos(theta / 2.), -std::sin(theta / 2.)}, 0., 0., - Complex{std::cos(theta / 2.), std::sin(theta / 2.)}); -} - -const Matrix2x2& ipz() { - static const Matrix2x2 matrix = - Matrix2x2::fromElements(Complex{0, 1}, 0, 0, Complex{0, -1}); - return matrix; -} - -const Matrix2x2& ipy() { - static const Matrix2x2 matrix = Matrix2x2::fromElements(0, 1, -1, 0); - return matrix; -} - -const Matrix2x2& ipx() { - static const Matrix2x2 matrix = - Matrix2x2::fromElements(0, Complex{0, 1}, Complex{0, 1}, 0); - return matrix; -} - [[nodiscard]] bool isUnitaryMatrix(const Matrix2x2& matrix, double tolerance = 1e-12) { return (matrix.adjoint() * matrix).isIdentity(tolerance); @@ -1277,47 +1240,12 @@ static void populateAllowedGates(NativeProfileSpec& spec) { } } -[[nodiscard]] static EulerBasis eulerBasisForAxisPair(AxisPair axisPair) { - switch (axisPair) { - case AxisPair::RxRz: - return EulerBasis::XZX; - case AxisPair::RxRy: - return EulerBasis::XYX; - case AxisPair::RyRz: - return EulerBasis::ZYZ; - } - llvm_unreachable("unknown axis pair"); -} - -[[nodiscard]] static EulerBasis -emitterEulerBasis(const SingleQubitEmitterSpec& emitter) { - switch (emitter.mode) { - case SingleQubitMode::ZSXX: - return EulerBasis::ZSXX; - case SingleQubitMode::U3: - return EulerBasis::U; - case SingleQubitMode::R: - return EulerBasis::R; - case SingleQubitMode::AxisPair: - return eulerBasisForAxisPair(emitter.axisPair); - } - llvm_unreachable("unknown single-qubit mode"); -} - -[[nodiscard]] static bool usesCxEntangler(const NativeProfileSpec& spec) { - return llvm::is_contained(spec.entanglerBases, EntanglerBasis::Cx); -} - -[[nodiscard]] static bool usesCzEntangler(const NativeProfileSpec& spec) { - return llvm::is_contained(spec.entanglerBases, EntanglerBasis::Cz); -} - [[nodiscard]] static std::optional selectEntangler(const NativeProfileSpec& spec) { - if (usesCxEntangler(spec)) { + if (llvm::is_contained(spec.entanglerBases, EntanglerBasis::Cx)) { return EntanglerBasis::Cx; } - if (usesCzEntangler(spec)) { + if (llvm::is_contained(spec.entanglerBases, EntanglerBasis::Cz)) { return EntanglerBasis::Cz; } return std::nullopt; @@ -1354,6 +1282,28 @@ static void emitGPhaseIfNonTrivial(OpBuilder& builder, Location loc, } // namespace +EulerBasis emitterEulerBasis(const SingleQubitEmitterSpec& emitter) { + switch (emitter.mode) { + case SingleQubitMode::ZSXX: + return EulerBasis::ZSXX; + case SingleQubitMode::U3: + return EulerBasis::U; + case SingleQubitMode::R: + return EulerBasis::R; + case SingleQubitMode::AxisPair: + switch (emitter.axisPair) { + case AxisPair::RxRz: + return EulerBasis::XZX; + case AxisPair::RxRy: + return EulerBasis::XYX; + case AxisPair::RyRz: + return EulerBasis::ZYZ; + } + break; + } + llvm_unreachable("unknown single-qubit mode"); +} + std::optional parseNativeSpec(llvm::StringRef nativeGates) { const auto gates = parseGateSet(nativeGates); if (!gates || gates->empty()) { @@ -1476,4 +1426,41 @@ twoQubitEntanglerCount(const Matrix4x4& target, const NativeProfileSpec& spec) { return native->numBasisUses; } +Matrix2x2 rxMatrix(double theta) { + const auto halfTheta = theta / 2.; + const Complex cos{std::cos(halfTheta), 0.}; + const Complex isin{0., -std::sin(halfTheta)}; + return Matrix2x2::fromElements(cos, isin, isin, cos); +} + +Matrix2x2 ryMatrix(double theta) { + const auto halfTheta = theta / 2.; + const Complex cos{std::cos(halfTheta), 0.}; + const Complex sin{std::sin(halfTheta), 0.}; + return Matrix2x2::fromElements(cos, -sin, sin, cos); +} + +Matrix2x2 rzMatrix(double theta) { + return Matrix2x2::fromElements( + Complex{std::cos(theta / 2.), -std::sin(theta / 2.)}, 0., 0., + Complex{std::cos(theta / 2.), std::sin(theta / 2.)}); +} + +const Matrix2x2& ipz() { + static const Matrix2x2 matrix = + Matrix2x2::fromElements(Complex{0, 1}, 0, 0, Complex{0, -1}); + return matrix; +} + +const Matrix2x2& ipy() { + static const Matrix2x2 matrix = Matrix2x2::fromElements(0, 1, -1, 0); + return matrix; +} + +const Matrix2x2& ipx() { + static const Matrix2x2 matrix = + Matrix2x2::fromElements(0, Complex{0, 1}, Complex{0, 1}, 0); + return matrix; +} + } // namespace mlir::qco::decomposition diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp index 2005017452..995a261997 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp @@ -16,7 +16,6 @@ #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" -#include #include #include #include @@ -24,8 +23,6 @@ #include #include #include -#include -#include #include #include #include @@ -35,11 +32,8 @@ #include #include -#include #include #include -#include -#include #include #include #include @@ -49,49 +43,94 @@ namespace mlir::qco { #define GEN_PASS_DEF_FUSETWOQUBITUNITARYRUNS #include "mlir/Dialect/QCO/Transforms/Passes.h.inc" -} // namespace mlir::qco +namespace { + +void collectUnitaryOpsInPreOrder(Operation* root, + llvm::SmallVectorImpl& ops) { + root->walk([&](Operation* op) { + if (op->getParentOfType()) { + return; + } + if (!llvm::isa(op) && op->getParentOfType()) { + return; + } + if (llvm::isa(op)) { + ops.push_back(op); + } + }); +} + +Value emitSingleQubitMatrix(IRRewriter& rewriter, Location loc, Value inQubit, + const Matrix2x2& matrix, + const decomposition::EulerBasis basis) { + // Force emission (`hasNonBasisGate = true`, `runSize = 0`) so the matrix is + // always lowered into native gates of `basis`, including any residual + // `qco.gphase`. With these arguments `synthesizeUnitary1QEuler` never + // returns `std::nullopt`. + return *decomposition::synthesizeUnitary1QEuler( + rewriter, loc, inQubit, matrix, /*runSize=*/0, + /*hasNonBasisGate=*/true, basis); +} + +/// State for one maximal two-qubit window (plus absorbed one-qubit ops) +/// during consolidation. +struct TwoQubitBlock { + Value wireA; + Value wireB; + llvm::SmallVector ops; + Matrix4x4 accum = Matrix4x4::identity(); + unsigned numTwoQ = 0; + unsigned numOneQ = 0; + bool anyNonNative = false; + bool open = true; +}; -// The following three functions are part of this pass's internal logic but are -// also exercised directly by unit tests, so they live in a named namespace that -// the tests can forward-declare. Everything else is file-local (see the -// anonymous namespace below). -namespace mlir::qco::native_synth { +/// Tracks overlapping two-qubit windows on a module slice. +struct TwoQubitWindowConsolidator { + std::vector blocks; + llvm::DenseMap wireToBlock; -using decomposition::NativeGateKind; -using decomposition::NativeProfileSpec; + void closeBlock(size_t idx); + void closeBlockOnWire(Value v); + void process(Operation* op, const decomposition::NativeProfileSpec& spec); + LogicalResult materialize(IRRewriter& rewriter, + const decomposition::NativeProfileSpec& spec); +}; -/// Map a single-qubit `UnitaryOpInterface` op to the `NativeGateKind` that -/// must appear in the menu for the op to be a no-op. -static std::optional +/// Map a single-qubit `UnitaryOpInterface` op to the +/// `decomposition::NativeGateKind` that must appear in the menu for the op to +/// be a no-op. +static std::optional singleQubitNativeGateKind(UnitaryOpInterface op) { Operation* raw = op.getOperation(); if (llvm::isa(raw)) { - return NativeGateKind::U; + return decomposition::NativeGateKind::U; } if (llvm::isa(raw)) { - return NativeGateKind::X; + return decomposition::NativeGateKind::X; } if (llvm::isa(raw)) { - return NativeGateKind::Sx; + return decomposition::NativeGateKind::Sx; } if (llvm::isa(raw)) { // `p` is a Z-rotation primitive for menu purposes. - return NativeGateKind::Rz; + return decomposition::NativeGateKind::Rz; } if (llvm::isa(raw)) { - return NativeGateKind::Rx; + return decomposition::NativeGateKind::Rx; } if (llvm::isa(raw)) { - return NativeGateKind::Ry; + return decomposition::NativeGateKind::Ry; } if (llvm::isa(raw)) { - return NativeGateKind::R; + return decomposition::NativeGateKind::R; } return std::nullopt; } // NOLINTNEXTLINE(misc-use-internal-linkage): test-visible (see comment above). -bool allowsSingleQubitOp(UnitaryOpInterface op, const NativeProfileSpec& spec) { +bool allowsSingleQubitOp(UnitaryOpInterface op, + const decomposition::NativeProfileSpec& spec) { if (llvm::isa(op.getOperation())) { return true; } @@ -99,7 +138,6 @@ bool allowsSingleQubitOp(UnitaryOpInterface op, const NativeProfileSpec& spec) { return gate && spec.allowedGates.contains(*gate); } -// NOLINTNEXTLINE(misc-use-internal-linkage): test-visible (see comment above). bool getBlockTwoQubitMatrix(Operation* op, Matrix4x4& matrix) { if (llvm::isa(op)) { return false; @@ -135,510 +173,24 @@ bool getBlockTwoQubitMatrix(Operation* op, Matrix4x4& matrix) { return true; } -} // namespace mlir::qco::native_synth - -namespace mlir::qco { -namespace { - -using decomposition::AxisPair; -using decomposition::EntanglerBasis; -using decomposition::NativeGateKind; -using decomposition::NativeProfileSpec; -using decomposition::parseNativeSpec; -using decomposition::SingleQubitEmitterSpec; -using decomposition::SingleQubitMode; -using decomposition::synthesizeUnitary2QWeyl; -using decomposition::twoQubitEntanglerCount; -using native_synth::allowsSingleQubitOp; -using native_synth::getBlockTwoQubitMatrix; - -constexpr double PI = std::numbers::pi; -constexpr double HALF_PI = PI / 2.0; - -using QubitId = std::size_t; - -[[nodiscard]] const Matrix4x4& swapGate() { - static const Matrix4x4 matrix = Matrix4x4::fromElements(1, 0, 0, 0, // - 0, 0, 1, 0, // - 0, 1, 0, 0, // - 0, 0, 0, 1); - return matrix; -} - -[[nodiscard]] Matrix4x4 expandToTwoQubits(const Matrix2x2& singleQubitMatrix, - QubitId qubitId) { - if (qubitId == 0) { - return kron(singleQubitMatrix, Matrix2x2::identity()); - } - if (qubitId == 1) { - return kron(Matrix2x2::identity(), singleQubitMatrix); - } - llvm::reportFatalInternalError("Invalid qubit id for single-qubit expansion"); -} - -[[nodiscard]] Matrix4x4 -fixTwoQubitMatrixQubitOrder(const Matrix4x4& twoQubitMatrix, - const llvm::SmallVector& qubitIds) { - if (qubitIds == llvm::SmallVector{1, 0}) { - return swapGate() * twoQubitMatrix * swapGate(); - } - if (qubitIds == llvm::SmallVector{0, 1}) { - return twoQubitMatrix; - } - llvm::reportFatalInternalError( - "Invalid qubit IDs for fixing two-qubit matrix"); -} - -[[nodiscard]] decomposition::EulerBasis -emitterEulerBasis(const SingleQubitEmitterSpec& emitter) { - switch (emitter.mode) { - case SingleQubitMode::ZSXX: - return decomposition::EulerBasis::ZSXX; - case SingleQubitMode::U3: - return decomposition::EulerBasis::U; - case SingleQubitMode::R: - return decomposition::EulerBasis::R; - case SingleQubitMode::AxisPair: - switch (emitter.axisPair) { - case AxisPair::RxRz: - return decomposition::EulerBasis::XZX; - case AxisPair::RxRy: - return decomposition::EulerBasis::XYX; - case AxisPair::RyRz: - return decomposition::EulerBasis::ZYZ; - } - break; - } - llvm_unreachable("unknown single-qubit mode"); -} - -[[nodiscard]] bool usesCxEntangler(const NativeProfileSpec& spec) { - return llvm::is_contained(spec.entanglerBases, EntanglerBasis::Cx); -} - -[[nodiscard]] bool usesCzEntangler(const NativeProfileSpec& spec) { - return llvm::is_contained(spec.entanglerBases, EntanglerBasis::Cz); -} - -/// True when `decomposeTo*` should run instead of folding to a constant `2×2` -/// matrix: trivial `Id`/`P`, dynamic-angle ops the matrix path cannot close -/// over, and (for ZSXX with direct Rx) `Rx`/`Ry`/`R`. Static angles still use -/// matrix + Euler. -bool canDirectlyDecomposeToZSXX(Operation* op, bool supportsDirectRx) { - if (llvm::isa(op)) { - return true; - } - return supportsDirectRx && llvm::isa(op); -} - -bool canDirectlyDecomposeToU3(Operation* op) { - return llvm::isa(op); -} - -bool canDirectlyDecomposeToR(Operation* op) { - return llvm::isa(op); -} - -bool canDirectlyDecomposeToAxisPair(Operation* op, AxisPair axisPair) { - if (llvm::isa(op)) { - return true; - } - switch (axisPair) { - case AxisPair::RxRz: - // `p` on an Rx/Rz axis pair folds directly to `rz(theta)`. - return llvm::isa(op); - case AxisPair::RxRy: - // No cheap symbolic lowering of `p` without `rz` available. - return llvm::isa(op); - case AxisPair::RyRz: - return llvm::isa(op); - } - llvm_unreachable("unknown axis pair"); -} - -Value createF64Const(IRRewriter& rewriter, Location loc, double value) { - return arith::ConstantFloatOp::create(rewriter, loc, rewriter.getF64Type(), - llvm::APFloat(value)) - .getResult(); -} - -std::optional getConstantF64(Value value) { - if (auto constant = value.getDefiningOp()) { - if (auto floatAttr = llvm::dyn_cast(constant.getValue())) { - return floatAttr.getValueAsDouble(); - } - } - return std::nullopt; -} - -void emitGPhaseIfNonTrivial(IRRewriter& rewriter, Location loc, double phase) { - constexpr double epsilon = 1e-12; - if (std::abs(phase) > epsilon) { - GPhaseOp::create(rewriter, loc, createF64Const(rewriter, loc, phase)); - } -} - -void collectUnitaryOpsInPreOrder(Operation* root, - llvm::SmallVectorImpl& ops) { - root->walk([&](Operation* op) { - if (op->getParentOfType()) { - return; - } - if (!llvm::isa(op) && op->getParentOfType()) { - return; - } - if (llvm::isa(op)) { - ops.push_back(op); - } - }); -} - -/// Small convenience wrapper to avoid passing rewriter/loc everywhere. Each -/// method creates the corresponding QCO op threaded through `q` and returns -/// its new output qubit value. -struct SingleQubitEmitter { - IRRewriter* rewriter; - Location loc; - - /// Create an `arith.constant` `f64` of value `v` at `loc`. - [[nodiscard]] Value constF(double v) const { - return createF64Const(*rewriter, loc, v); - } - - /// Emit `rx(theta)` with a compile-time scalar angle. - [[nodiscard]] Value rx(Value q, double theta) const { - return RXOp::create(*rewriter, loc, q, constF(theta)).getOutputQubit(0); - } - /// Emit `rx(theta)` with a runtime `f64` angle value. - [[nodiscard]] Value rx(Value q, Value theta) const { - return RXOp::create(*rewriter, loc, q, theta).getOutputQubit(0); - } - /// Emit `ry(theta)` with a compile-time scalar angle. - [[nodiscard]] Value ry(Value q, double theta) const { - return RYOp::create(*rewriter, loc, q, constF(theta)).getOutputQubit(0); - } - /// Emit `ry(theta)` with a runtime `f64` angle value. - [[nodiscard]] Value ry(Value q, Value theta) const { - return RYOp::create(*rewriter, loc, q, theta).getOutputQubit(0); - } - /// Emit `rz(theta)` with a compile-time scalar angle. - [[nodiscard]] Value rz(Value q, double theta) const { - return RZOp::create(*rewriter, loc, q, constF(theta)).getOutputQubit(0); - } - /// Emit `rz(theta)` with a runtime `f64` angle value. - [[nodiscard]] Value rz(Value q, Value theta) const { - return RZOp::create(*rewriter, loc, q, theta).getOutputQubit(0); - } - /// Emit `sx` (square-root-of-X). - [[nodiscard]] Value sx(Value q) const { - return SXOp::create(*rewriter, loc, q).getOutputQubit(0); - } - /// Emit a Pauli `x`. - [[nodiscard]] Value x(Value q) const { - return XOp::create(*rewriter, loc, q).getOutputQubit(0); - } - /// Emit `r(theta, phi)` with compile-time scalar angles. - [[nodiscard]] Value r(Value q, double theta, double phi) const { - return ROp::create(*rewriter, loc, q, constF(theta), constF(phi)) - .getOutputQubit(0); - } - /// Emit `r(theta, phi)` with runtime `f64` angle values. - [[nodiscard]] Value r(Value q, Value theta, Value phi) const { - return ROp::create(*rewriter, loc, q, theta, phi).getOutputQubit(0); - } - /// Emit `u(theta, phi, lambda)` with runtime `f64` angle values. - [[nodiscard]] Value u(Value q, Value theta, Value phi, Value lambda) const { - return UOp::create(*rewriter, loc, q, theta, phi, lambda).getOutputQubit(0); - } - /// Emit `u(theta, phi, lambda)` with compile-time scalar angles. - [[nodiscard]] Value u(Value q, double theta, double phi, - double lambda) const { - return u(q, constF(theta), constF(phi), constF(lambda)); - } -}; - -Value decomposeToZSXX(IRRewriter& rewriter, Operation* op, Value inQubit, - bool supportsDirectRx) { - if (llvm::isa(op)) { - return inQubit; - } - SingleQubitEmitter e{.rewriter = &rewriter, .loc = op->getLoc()}; - if (auto p = llvm::dyn_cast(op)) { - auto q = e.rz(inQubit, p.getTheta()); - auto halfTheta = arith::MulFOp::create(rewriter, op->getLoc(), p.getTheta(), - e.constF(0.5)) - .getResult(); - GPhaseOp::create(rewriter, op->getLoc(), halfTheta); - return q; - } - if (!supportsDirectRx) { - return {}; - } - if (auto rx = llvm::dyn_cast(op)) { - return rx.getOutputQubit(0); - } - if (auto ry = llvm::dyn_cast(op)) { - return e.rz(e.rx(e.rz(inQubit, -HALF_PI), ry.getTheta()), HALF_PI); - } - if (auto r = llvm::dyn_cast(op)) { - auto negPhi = - arith::NegFOp::create(rewriter, op->getLoc(), r.getPhi()).getResult(); - return e.rz(e.rx(e.rz(inQubit, negPhi), r.getTheta()), r.getPhi()); - } - return {}; -} - -Value decomposeToU3(IRRewriter& rewriter, Operation* op, Value inQubit) { - if (llvm::isa(op)) { - return inQubit; - } - SingleQubitEmitter e{.rewriter = &rewriter, .loc = op->getLoc()}; - if (auto u = llvm::dyn_cast(op)) { - return u.getOutputQubit(0); - } - if (auto rx = llvm::dyn_cast(op)) { - return e.u(inQubit, rx.getTheta(), e.constF(-HALF_PI), e.constF(HALF_PI)); - } - if (auto ry = llvm::dyn_cast(op)) { - return e.u(inQubit, ry.getTheta(), e.constF(0.0), e.constF(0.0)); - } - if (auto rz = llvm::dyn_cast(op)) { - auto out = e.u(inQubit, e.constF(0.0), e.constF(0.0), rz.getTheta()); - auto halfTheta = arith::MulFOp::create(rewriter, op->getLoc(), - rz.getTheta(), e.constF(-0.5)) - .getResult(); - GPhaseOp::create(rewriter, op->getLoc(), halfTheta); - return out; - } - if (auto p = llvm::dyn_cast(op)) { - return e.u(inQubit, e.constF(0.0), e.constF(0.0), p.getTheta()); - } - if (auto u2 = llvm::dyn_cast(op)) { - return e.u(inQubit, e.constF(HALF_PI), u2.getPhi(), u2.getLambda()); - } - if (auto r = llvm::dyn_cast(op)) { - auto loc = op->getLoc(); - auto phiMinus = - arith::AddFOp::create(rewriter, loc, r.getPhi(), e.constF(-HALF_PI)) - .getResult(); - auto negPhi = arith::NegFOp::create(rewriter, loc, r.getPhi()).getResult(); - auto minusPlus = - arith::AddFOp::create(rewriter, loc, negPhi, e.constF(HALF_PI)) - .getResult(); - return e.u(inQubit, r.getTheta(), phiMinus, minusPlus); - } - return {}; -} - -Value decomposeToR(IRRewriter& rewriter, Operation* op, Value inQubit) { - if (llvm::isa(op)) { - return inQubit; - } - SingleQubitEmitter e{.rewriter = &rewriter, .loc = op->getLoc()}; - if (auto r = llvm::dyn_cast(op)) { - return r.getOutputQubit(0); - } - if (auto rx = llvm::dyn_cast(op)) { - return e.r(inQubit, rx.getTheta(), e.constF(0.0)); - } - if (auto ry = llvm::dyn_cast(op)) { - return e.r(inQubit, ry.getTheta(), e.constF(HALF_PI)); - } - return {}; -} - -Value decomposeToAxisPair(IRRewriter& rewriter, Operation* op, Value inQubit, - AxisPair axisPair) { - if (llvm::isa(op)) { - return inQubit; - } - SingleQubitEmitter e{.rewriter = &rewriter, .loc = op->getLoc()}; - switch (axisPair) { - case AxisPair::RxRz: - if (auto rx = llvm::dyn_cast(op)) { - return rx.getOutputQubit(0); - } - if (auto rz = llvm::dyn_cast(op)) { - return rz.getOutputQubit(0); - } - if (auto p = llvm::dyn_cast(op)) { - auto q = e.rz(inQubit, p.getTheta()); - auto halfTheta = arith::MulFOp::create(rewriter, op->getLoc(), - p.getTheta(), e.constF(0.5)) - .getResult(); - GPhaseOp::create(rewriter, op->getLoc(), halfTheta); - return q; - } - return {}; - case AxisPair::RxRy: - if (auto rx = llvm::dyn_cast(op)) { - return rx.getOutputQubit(0); - } - if (auto ry = llvm::dyn_cast(op)) { - return ry.getOutputQubit(0); - } - return {}; - case AxisPair::RyRz: - if (auto ry = llvm::dyn_cast(op)) { - return ry.getOutputQubit(0); - } - if (auto rz = llvm::dyn_cast(op)) { - return rz.getOutputQubit(0); - } - if (auto p = llvm::dyn_cast(op)) { - auto q = e.rz(inQubit, p.getTheta()); - auto halfTheta = arith::MulFOp::create(rewriter, op->getLoc(), - p.getTheta(), e.constF(0.5)) - .getResult(); - GPhaseOp::create(rewriter, op->getLoc(), halfTheta); - return q; - } - return {}; - } - llvm_unreachable("unknown axis pair"); -} - -Value emitSingleQubitMatrix(IRRewriter& rewriter, Location loc, Value inQubit, - const Matrix2x2& matrix, - const decomposition::EulerBasis basis) { - // Force emission (`hasNonBasisGate = true`, `runSize = 0`) so the matrix is - // always lowered into native gates of `basis`, including any residual - // `qco.gphase`. With these arguments `synthesizeUnitary1QEuler` never - // returns `std::nullopt`. - return *decomposition::synthesizeUnitary1QEuler( - rewriter, loc, inQubit, matrix, /*runSize=*/0, - /*hasNonBasisGate=*/true, basis); -} - -LogicalResult rewriteXXPlusMinusYYViaRzz(IRRewriter& rewriter, Operation* op) { - rewriter.setInsertionPoint(op); - const auto loc = op->getLoc(); - const auto constF = [&](double v) { - return createF64Const(rewriter, loc, v); - }; - const auto half = [&](Value v) -> Value { - if (auto c = getConstantF64(v)) { - return constF(*c * 0.5); - } - return arith::MulFOp::create(rewriter, loc, v, constF(0.5)).getResult(); - }; - const auto neg = [&](Value v) -> Value { - if (auto c = getConstantF64(v)) { - return constF(-*c); - } - return arith::NegFOp::create(rewriter, loc, v).getResult(); - }; - const auto emitH = [&](Value q) -> Value { - auto rz0 = RZOp::create(rewriter, loc, q, constF(HALF_PI)); - auto sx = SXOp::create(rewriter, loc, rz0.getOutputQubit(0)); - return RZOp::create(rewriter, loc, sx.getOutputQubit(0), constF(HALF_PI)) - .getOutputQubit(0); - }; - // Realize `Rxx(theta)` as `(H ⊗ H) * Rzz(theta) * (H ⊗ H)`: Hadamard - // conjugation maps the Z axis to X on each qubit, and the tensor-product - // identity `(H ⊗ H) * ZZ * (H ⊗ H) == XX` lifts that to the entangler. - const auto emitRxxViaRzz = [&](Value q0, Value q1, - Value theta) -> std::pair { - q0 = emitH(q0); - q1 = emitH(q1); - auto rzz = RZZOp::create(rewriter, loc, q0, q1, theta); - q0 = rzz.getOutputQubit(0); - q1 = rzz.getOutputQubit(1); - return {emitH(q0), emitH(q1)}; - }; - // Realize `Ryy(theta)` as `(Rx(-pi/2) ⊗ Rx(-pi/2)) * Rzz(theta) * - // (Rx(pi/2) ⊗ Rx(pi/2))`: Rx(pi/2) maps Z to Y on each qubit, so the - // conjugation transports `ZZ` to `YY` just like the Hadamard sandwich - // above maps it to `XX`. - const auto emitRyyViaRzz = [&](Value q0, Value q1, - Value theta) -> std::pair { - auto rx0 = RXOp::create(rewriter, loc, q0, constF(HALF_PI)); - auto rx1 = RXOp::create(rewriter, loc, q1, constF(HALF_PI)); - auto rzz = RZZOp::create(rewriter, loc, rx0.getOutputQubit(0), - rx1.getOutputQubit(0), theta); - auto rxb0 = - RXOp::create(rewriter, loc, rzz.getOutputQubit(0), constF(-HALF_PI)); - auto rxb1 = - RXOp::create(rewriter, loc, rzz.getOutputQubit(1), constF(-HALF_PI)); - return {rxb0.getOutputQubit(0), rxb1.getOutputQubit(0)}; - }; - - // `XXPlusYY(theta, beta)` and `XXMinusYY(theta, beta)` both act as - // Rz(-beta) on q0 -> entangling core -> Rz(+beta) on q0, - // but differ in the entangling core: - // XXPlusYY: exp(-i * theta/4 * (XX + YY)) == Ryy(theta/2) * Rxx(theta/2) - // XXMinusYY: exp(-i * theta/4 * (XX - YY)) == Rxx(theta/2) * Ryy(-theta/2) - // (XX and YY commute, so the two multiplication orders produce identical - // unitaries; the distinct order and sign below are what makes `XXMinusYY` - // the "minus" variant and must be preserved even though an order flip - // alone would also compile.) - if (auto xxPlus = llvm::dyn_cast(op)) { - Value q0 = xxPlus.getInputQubit(0); - Value q1 = xxPlus.getInputQubit(1); - q0 = RZOp::create(rewriter, loc, q0, neg(xxPlus.getBeta())) - .getOutputQubit(0); - const auto halfTheta = half(xxPlus.getTheta()); - std::tie(q0, q1) = emitRyyViaRzz(q0, q1, halfTheta); - std::tie(q0, q1) = emitRxxViaRzz(q0, q1, halfTheta); - q0 = RZOp::create(rewriter, loc, q0, xxPlus.getBeta()).getOutputQubit(0); - rewriter.replaceOp(op, ValueRange{q0, q1}); - return success(); - } - if (auto xxMinus = llvm::dyn_cast(op)) { - Value q0 = xxMinus.getInputQubit(0); - Value q1 = xxMinus.getInputQubit(1); - q0 = RZOp::create(rewriter, loc, q0, neg(xxMinus.getBeta())) - .getOutputQubit(0); - const auto halfTheta = half(xxMinus.getTheta()); - std::tie(q0, q1) = emitRxxViaRzz(q0, q1, halfTheta); - std::tie(q0, q1) = emitRyyViaRzz(q0, q1, neg(halfTheta)); - q0 = RZOp::create(rewriter, loc, q0, xxMinus.getBeta()).getOutputQubit(0); - rewriter.replaceOp(op, ValueRange{q0, q1}); - return success(); - } - return failure(); -} - -/// State for one maximal two-qubit window (plus absorbed one-qubit ops) -/// during consolidation. -struct TwoQubitBlock { - Value wireA; - Value wireB; - llvm::SmallVector ops; - Matrix4x4 accum = Matrix4x4::identity(); - unsigned numTwoQ = 0; - unsigned numOneQ = 0; - bool anyNonNative = false; - bool open = true; -}; - -/// Tracks overlapping two-qubit windows on a module slice. -struct TwoQubitWindowConsolidator { - std::vector blocks; - llvm::DenseMap wireToBlock; - - void closeBlock(size_t idx); - void closeBlockOnWire(Value v); - void process(Operation* op, const NativeProfileSpec& spec); - LogicalResult materialize(IRRewriter& rewriter, - const NativeProfileSpec& spec); -}; - /// Check whether a two-qubit op `op` is already expressible by the resolved /// native menu: a single-control `CX`/`CZ` consistent with the active /// entangler, or `Rzz` when `spec.allowRzz` is set. Multi-control and other /// two-qubit ops are considered non-native. -bool isNativeTwoQubitOp(Operation* op, const NativeProfileSpec& spec) { +bool isNativeTwoQubitOp(Operation* op, + const decomposition::NativeProfileSpec& spec) { if (auto ctrl = llvm::dyn_cast(op)) { if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { return false; } auto* body = ctrl.getBodyUnitary(0).getOperation(); if (llvm::isa(body)) { - return llvm::is_contained(spec.entanglerBases, EntanglerBasis::Cx); + return llvm::is_contained(spec.entanglerBases, + decomposition::EntanglerBasis::Cx); } if (llvm::isa(body)) { - return llvm::is_contained(spec.entanglerBases, EntanglerBasis::Cz); + return llvm::is_contained(spec.entanglerBases, + decomposition::EntanglerBasis::Cz); } return false; } @@ -657,9 +209,9 @@ bool shouldApplyBlockReplacement(const TwoQubitBlock& block, return numBasisUses < block.numTwoQ; } -LogicalResult materializeSingleTwoQubitBlock(IRRewriter& rewriter, - const TwoQubitBlock& block, - const NativeProfileSpec& spec) { +LogicalResult +materializeSingleTwoQubitBlock(IRRewriter& rewriter, const TwoQubitBlock& block, + const decomposition::NativeProfileSpec& spec) { Operation* firstOp = block.ops.front(); auto firstUnitary = llvm::cast(firstOp); const Value inA = firstUnitary.getInputQubit(0); @@ -670,8 +222,9 @@ LogicalResult materializeSingleTwoQubitBlock(IRRewriter& rewriter, rewriter.setInsertionPoint(firstOp); Value newA; Value newB; - if (failed(synthesizeUnitary2QWeyl(rewriter, firstOp->getLoc(), inA, inB, - block.accum, spec, newA, newB))) { + if (failed(decomposition::synthesizeUnitary2QWeyl(rewriter, firstOp->getLoc(), + inA, inB, block.accum, spec, + newA, newB))) { firstOp->emitError("failed to emit synthesized two-qubit gate sequence"); return failure(); } @@ -701,8 +254,8 @@ void TwoQubitWindowConsolidator::closeBlockOnWire(Value v) { } } -void TwoQubitWindowConsolidator::process(Operation* op, - const NativeProfileSpec& spec) { +void TwoQubitWindowConsolidator::process( + Operation* op, const decomposition::NativeProfileSpec& spec) { if (op->getParentOfType()) { return; } @@ -748,7 +301,7 @@ void TwoQubitWindowConsolidator::process(Operation* op, if (sameBlock && singleUse) { const size_t idx = *idx0; auto& block = blocks[idx]; - llvm::SmallVector ids; + llvm::SmallVector ids; if (v0 == block.wireA && v1 == block.wireB) { ids = {0, 1}; } else if (v0 == block.wireB && v1 == block.wireA) { @@ -757,7 +310,8 @@ void TwoQubitWindowConsolidator::process(Operation* op, closeBlock(idx); return; } - block.accum = fixTwoQubitMatrixQubitOrder(opMatrix, ids) * block.accum; + block.accum = + reorderTwoQubitMatrix(opMatrix, ids[0], ids[1]) * block.accum; block.ops.push_back(op); ++block.numTwoQ; if (!isNativeTwoQubitOp(op, spec)) { @@ -818,8 +372,8 @@ void TwoQubitWindowConsolidator::process(Operation* op, closeBlock(idx); return; } - const auto pad = (v == block.wireA) ? expandToTwoQubits(raw, 0) - : expandToTwoQubits(raw, 1); + const auto pad = (v == block.wireA) ? embedSingleQubitInTwoQubit(raw, 0) + : embedSingleQubitInTwoQubit(raw, 1); block.accum = pad * block.accum; block.ops.push_back(op); ++block.numOneQ; @@ -842,9 +396,8 @@ void TwoQubitWindowConsolidator::process(Operation* op, } } -LogicalResult -TwoQubitWindowConsolidator::materialize(IRRewriter& rewriter, - const NativeProfileSpec& spec) { +LogicalResult TwoQubitWindowConsolidator::materialize( + IRRewriter& rewriter, const decomposition::NativeProfileSpec& spec) { llvm::DenseSet erasedOps; for (const auto& block : blocks) { if (block.ops.size() < 2) { @@ -854,7 +407,8 @@ TwoQubitWindowConsolidator::materialize(IRRewriter& rewriter, [&](Operation* op) { return erasedOps.contains(op); })) { continue; } - const auto numBasisUses = twoQubitEntanglerCount(block.accum, spec); + const auto numBasisUses = + decomposition::twoQubitEntanglerCount(block.accum, spec); if (!numBasisUses) { continue; } @@ -871,8 +425,9 @@ TwoQubitWindowConsolidator::materialize(IRRewriter& rewriter, return success(); } -LogicalResult fuseTwoQubitUnitaryRuns(IRRewriter& rewriter, Operation* root, - const NativeProfileSpec& spec) { +LogicalResult +fuseTwoQubitUnitaryRuns(IRRewriter& rewriter, Operation* root, + const decomposition::NativeProfileSpec& spec) { llvm::SmallVector ops; collectUnitaryOpsInPreOrder(root, ops); TwoQubitWindowConsolidator consolidator; @@ -892,7 +447,7 @@ struct OneQubitRun { /// off-menu or when Euler resynthesis strictly shortens the run. bool maybeFuseRun(IRRewriter& rewriter, OneQubitRun& run, const decomposition::EulerBasis basis, - const NativeProfileSpec& spec) { + const decomposition::NativeProfileSpec& spec) { Matrix2x2 fused = Matrix2x2::identity(); for (UnitaryOpInterface u : run.ops) { Matrix2x2 m; @@ -955,42 +510,6 @@ UnitaryOpInterface fusibleSingleQubitOp(Operation* op) { return unitary; } -/// Whether `emitter` can lower the single-qubit `op` directly (used for ops -/// with non-constant angles, which have no constant `2×2` matrix). -bool emitterHasDirectLowering(Operation* op, - const SingleQubitEmitterSpec& emitter) { - switch (emitter.mode) { - case SingleQubitMode::ZSXX: - return canDirectlyDecomposeToZSXX(op, emitter.supportsDirectRx); - case SingleQubitMode::U3: - return canDirectlyDecomposeToU3(op); - case SingleQubitMode::R: - return canDirectlyDecomposeToR(op); - case SingleQubitMode::AxisPair: - return canDirectlyDecomposeToAxisPair(op, emitter.axisPair); - } - return false; -} - -/// Dispatch `op`'s direct (non-matrix) single-qubit lowering to the -/// `decomposeTo*` helper for `emitter.mode`. Returns the output qubit value -/// or a null `Value` if no direct rule applies for this op. -Value applyDirectSingleQubitLowering(IRRewriter& rewriter, Operation* op, - Value in, - const SingleQubitEmitterSpec& emitter) { - switch (emitter.mode) { - case SingleQubitMode::ZSXX: - return decomposeToZSXX(rewriter, op, in, emitter.supportsDirectRx); - case SingleQubitMode::U3: - return decomposeToU3(rewriter, op, in); - case SingleQubitMode::R: - return decomposeToR(rewriter, op, in); - case SingleQubitMode::AxisPair: - return decomposeToAxisPair(rewriter, op, in, emitter.axisPair); - } - llvm_unreachable("unknown SingleQubitMode"); -} - /// Lowers unitary QCO ops to a comma-separated native gate menu using a /// deterministic, matrix-driven synthesizer: single-qubit fuse, two-qubit /// window consolidation, synthesis sweeps, seam single-qubit fuse, and @@ -1014,7 +533,7 @@ struct FuseTwoQubitUnitaryRunsPass if (llvm::StringRef(nativeGates).trim().empty()) { return; } - auto specOpt = parseNativeSpec(nativeGates); + auto specOpt = decomposition::parseNativeSpec(nativeGates); if (!specOpt) { getOperation().emitError() << "unsupported native gate menu (native-gates='" << nativeGates @@ -1026,7 +545,7 @@ struct FuseTwoQubitUnitaryRunsPass // Deterministic single-qubit basis: the first emitter drives all matrix // synthesis and run fusion. const decomposition::EulerBasis oneQubitBasis = - emitterEulerBasis(spec.singleQubitEmitters.front()); + decomposition::emitterEulerBasis(spec.singleQubitEmitters.front()); IRRewriter rewriter(&getContext()); @@ -1078,8 +597,9 @@ struct FuseTwoQubitUnitaryRunsPass /// `CtrlOp` is already on-menu when the body is `X`/`Z` and the profile /// supplies `cx` / `cz` entanglers. - static bool ctrlMatchesNativeMenu(CtrlOp ctrl, - const NativeProfileSpec& spec) { + static bool + ctrlMatchesNativeMenu(CtrlOp ctrl, + const decomposition::NativeProfileSpec& spec) { if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { return false; } @@ -1089,19 +609,25 @@ struct FuseTwoQubitUnitaryRunsPass if (!hasCX && !hasCZ) { return false; } - return (usesCxEntangler(spec) && hasCX) || (usesCzEntangler(spec) && hasCZ); + return (llvm::is_contained(spec.entanglerBases, + decomposition::EntanglerBasis::Cx) && + hasCX) || + (llvm::is_contained(spec.entanglerBases, + decomposition::EntanglerBasis::Cz) && + hasCZ); } /// Bare two-qubit on-menu: `rzz` when the profile allows it. - static bool bareTwoQubitMatchesNativeMenu(Operation* op, - const NativeProfileSpec& spec) { + static bool + bareTwoQubitMatchesNativeMenu(Operation* op, + const decomposition::NativeProfileSpec& spec) { return llvm::isa(op) && spec.allowRzz && - spec.allowedGates.contains(NativeGateKind::Rzz); + spec.allowedGates.contains(decomposition::NativeGateKind::Rzz); } /// True if any unitary is outside `spec` (single-qubit, `ctrl`, or bare /// `rzz`). - bool hasNonNativeMenuOps(const NativeProfileSpec& spec) { + bool hasNonNativeMenuOps(const decomposition::NativeProfileSpec& spec) { const mlir::WalkResult walkResult = getOperation()->walk([&](Operation* op) { if (llvm::isa(op)) { @@ -1138,7 +664,8 @@ struct FuseTwoQubitUnitaryRunsPass } /// Any off-menu single-qubit unitary (ignores `ctrl` region bodies). - bool hasNonNativeSingleQubitOps(const NativeProfileSpec& spec) { + bool + hasNonNativeSingleQubitOps(const decomposition::NativeProfileSpec& spec) { const mlir::WalkResult walkResult = getOperation()->walk([&](Operation* op) { if (llvm::isa(op)) { @@ -1162,7 +689,8 @@ struct FuseTwoQubitUnitaryRunsPass private: /// Fuse adjacent single-qubit runs when the emitter wins on length or any op /// is off-menu. - void fuseOneQubitRuns(IRRewriter& rewriter, const NativeProfileSpec& spec, + void fuseOneQubitRuns(IRRewriter& rewriter, + const decomposition::NativeProfileSpec& spec, const decomposition::EulerBasis basis) { llvm::SmallVector runs; llvm::DenseMap tailOpToRun; @@ -1204,8 +732,9 @@ struct FuseTwoQubitUnitaryRunsPass /// Two-qubit windows with absorbed single-qubit ops: replace when a cheaper /// native sequence exists. - LogicalResult consolidateTwoQubitBlocks(IRRewriter& rewriter, - const NativeProfileSpec& spec) { + LogicalResult + consolidateTwoQubitBlocks(IRRewriter& rewriter, + const decomposition::NativeProfileSpec& spec) { return fuseTwoQubitUnitaryRuns(rewriter, getOperation(), spec); } @@ -1214,9 +743,10 @@ struct FuseTwoQubitUnitaryRunsPass /// `rewriteControlled` / `rewriteTwoQubit`. Returns `failure()` as soon as /// any op cannot be lowered to the native menu. Safe to call repeatedly; /// `runOnOperation` iterates until convergence. - LogicalResult synthesizeRemainingOps(IRRewriter& rewriter, - const NativeProfileSpec& spec, - const decomposition::EulerBasis basis) { + LogicalResult + synthesizeRemainingOps(IRRewriter& rewriter, + const decomposition::NativeProfileSpec& spec, + const decomposition::EulerBasis basis) { llvm::SmallVector ops; collectUnitaryOpsInPreOrder(getOperation(), ops); llvm::DenseSet erasedOps; @@ -1242,7 +772,7 @@ struct FuseTwoQubitUnitaryRunsPass if (unitary.isSingleQubit()) { if (!allowsSingleQubitOp(unitary, spec)) { - if (failed(rewriteSingleQubit(rewriter, op, unitary, spec, basis))) { + if (failed(rewriteSingleQubit(rewriter, op, unitary, basis))) { return failure(); } erasedOps.insert(op); @@ -1272,43 +802,33 @@ struct FuseTwoQubitUnitaryRunsPass return success(); } - /// Lower one off-menu single-qubit `op`. Constant unitaries use the - /// matrix-driven Euler synthesizer in `basis`; ops with non-constant angles - /// fall back to the symbolic `decomposeTo*` lowering of the first emitter - /// that handles them. + /// Lower one off-menu single-qubit `op` via its constant `2×2` matrix and + /// the Euler synthesizer in `basis`. static LogicalResult rewriteSingleQubit(IRRewriter& rewriter, Operation* op, - UnitaryOpInterface unitary, const NativeProfileSpec& spec, + UnitaryOpInterface unitary, const decomposition::EulerBasis basis) { rewriter.setInsertionPoint(op); const Value in = unitary.getInputQubit(0); Matrix2x2 matrix; - if (unitary.isSingleQubit() && unitary.getUnitaryMatrix2x2(matrix)) { - const Value replaced = - emitSingleQubitMatrix(rewriter, op->getLoc(), in, matrix, basis); - rewriter.replaceOp(op, replaced); - return success(); - } - for (const auto& emitter : spec.singleQubitEmitters) { - if (!emitterHasDirectLowering(op, emitter)) { - continue; - } - if (const Value replaced = - applyDirectSingleQubitLowering(rewriter, op, in, emitter)) { - rewriter.replaceOp(op, replaced); - return success(); - } + if (!unitary.getUnitaryMatrix2x2(matrix)) { + op->emitError("single-qubit operation with non-constant parameters is " + "not supported for native synthesis"); + return failure(); } - op->emitError("single-qubit operation not in selected native profile"); - return failure(); + const Value replaced = + emitSingleQubitMatrix(rewriter, op->getLoc(), in, matrix, basis); + rewriter.replaceOp(op, replaced); + return success(); } /// Lower a single-control, single-target `CtrlOp` to the native profile. /// Fast-path: already-native `CX`/`CZ` are kept as-is. Otherwise, lift the /// controlled op to its 4x4 matrix and run the deterministic two-qubit /// synthesizer. - static LogicalResult rewriteControlled(IRRewriter& rewriter, CtrlOp ctrl, - const NativeProfileSpec& spec) { + static LogicalResult + rewriteControlled(IRRewriter& rewriter, CtrlOp ctrl, + const decomposition::NativeProfileSpec& spec) { if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { ctrl.emitError("native synthesis currently only supports 1-control " "1-target controlled gates"); @@ -1317,7 +837,12 @@ struct FuseTwoQubitUnitaryRunsPass auto* body = ctrl.getBodyUnitary(0).getOperation(); const bool hasCX = llvm::isa(body); const bool hasCZ = llvm::isa(body); - if ((usesCxEntangler(spec) && hasCX) || (usesCzEntangler(spec) && hasCZ)) { + if ((llvm::is_contained(spec.entanglerBases, + decomposition::EntanglerBasis::Cx) && + hasCX) || + (llvm::is_contained(spec.entanglerBases, + decomposition::EntanglerBasis::Cz) && + hasCZ)) { return success(); } Matrix4x4 matrix; @@ -1338,7 +863,7 @@ struct FuseTwoQubitUnitaryRunsPass rewriter.setInsertionPoint(ctrl); Value out0; Value out1; - if (failed(synthesizeUnitary2QWeyl( + if (failed(decomposition::synthesizeUnitary2QWeyl( rewriter, ctrl.getLoc(), ctrl.getInputControl(0), ctrl.getInputTarget(0), matrix, spec, out0, out1))) { ctrl.emitError("controlled gate not allowed by selected profile"); @@ -1348,26 +873,16 @@ struct FuseTwoQubitUnitaryRunsPass return success(); } - /// Lower an off-menu generic two-qubit op (`RZZ`, `XXPlusYY`, `XXMinusYY`, - /// or any arbitrary 4x4 unitary). Handles the `Rzz`-native fast path; for - /// `XXPlusYY` / `XXMinusYY` with `rzz` on the menu, uses the dedicated - /// `XX±YY -> Rzz` rewrite. All other two-qubit unitaries go through the - /// deterministic KAK synthesizer. - static LogicalResult rewriteTwoQubit(IRRewriter& rewriter, Operation* op, - UnitaryOpInterface unitary, - const NativeProfileSpec& spec) { + /// Lower an off-menu generic two-qubit op. Bare `RZZ` is kept when on the + /// native menu; all other two-qubit unitaries go through the deterministic + /// KAK synthesizer. + static LogicalResult + rewriteTwoQubit(IRRewriter& rewriter, Operation* op, + UnitaryOpInterface unitary, + const decomposition::NativeProfileSpec& spec) { if (spec.allowRzz && llvm::isa(op)) { return success(); } - if (spec.allowRzz && - (llvm::isa(op) || llvm::isa(op))) { - rewriter.setInsertionPoint(op); - if (succeeded(rewriteXXPlusMinusYYViaRzz(rewriter, op))) { - return success(); - } - // Fall through to entangler-based synthesis when the dedicated rewrite - // could not be applied (e.g. no entangler-free realization). - } Matrix4x4 matrix; if (!getBlockTwoQubitMatrix(op, matrix)) { op->emitError("unsupported two-qubit operation for selected profile"); @@ -1376,7 +891,7 @@ struct FuseTwoQubitUnitaryRunsPass rewriter.setInsertionPoint(op); Value out0; Value out1; - if (failed(synthesizeUnitary2QWeyl( + if (failed(decomposition::synthesizeUnitary2QWeyl( rewriter, op->getLoc(), unitary.getInputQubit(0), unitary.getInputQubit(1), matrix, spec, out0, out1))) { op->emitError("unsupported two-qubit operation for selected profile"); diff --git a/mlir/lib/Dialect/QCO/Utils/Matrix.cpp b/mlir/lib/Dialect/QCO/Utils/Matrix.cpp index bb5eedc63d..d0b9cb38d9 100644 --- a/mlir/lib/Dialect/QCO/Utils/Matrix.cpp +++ b/mlir/lib/Dialect/QCO/Utils/Matrix.cpp @@ -523,6 +523,98 @@ bool DynamicMatrix::isApprox(const DynamicMatrix& other, return entriesAreApprox(impl_->data, other.impl_->data, tol); } +Complex DynamicMatrix::trace() const { + Complex sum{0.0, 0.0}; + const auto udim = checkedDim(impl_->dim); + for (std::size_t i = 0; i < udim; ++i) { + sum += impl_->data[(i * udim) + i]; + } + return sum; +} + +DynamicMatrix DynamicMatrix::operator*(const DynamicMatrix& rhs) const { + if (impl_->dim != rhs.impl_->dim) { + llvm::reportFatalInternalError( + "DynamicMatrix multiply requires matching dimensions"); + } + const auto udim = checkedDim(impl_->dim); + DynamicMatrix out(impl_->dim); + for (std::size_t row = 0; row < udim; ++row) { + for (std::size_t col = 0; col < udim; ++col) { + Complex sum{0.0, 0.0}; + for (std::size_t k = 0; k < udim; ++k) { + sum += + impl_->data[(row * udim) + k] * rhs.impl_->data[(k * udim) + col]; + } + out.impl_->data[(row * udim) + col] = sum; + } + } + return out; +} + +DynamicMatrix DynamicMatrix::operator*(const Complex& scalar) const { + DynamicMatrix out(impl_->dim); + for (std::size_t i = 0; i < impl_->data.size(); ++i) { + out.impl_->data[i] = impl_->data[i] * scalar; + } + return out; +} + +bool DynamicMatrix::isIdentity(const double tol) const { + const auto udim = checkedDim(impl_->dim); + for (std::size_t row = 0; row < udim; ++row) { + for (std::size_t col = 0; col < udim; ++col) { + const Complex expected = + (row == col) ? Complex{1.0, 0.0} : Complex{0.0, 0.0}; + if (std::abs(impl_->data[(row * udim) + col] - expected) > tol) { + return false; + } + } + } + return true; +} + +namespace { + +[[nodiscard]] std::size_t qubitBitAt(const std::size_t index, + const std::size_t numQubits, + const std::size_t qubitIndex) { + return (index >> (numQubits - 1 - qubitIndex)) & 1U; +} + +[[nodiscard]] bool otherQubitBitsMatch(const std::size_t row, + const std::size_t col, + const std::size_t numQubits, + const std::size_t skipA, + const std::size_t skipB) { + for (std::size_t q = 0; q < numQubits; ++q) { + if (q == skipA || q == skipB) { + continue; + } + if (qubitBitAt(row, numQubits, q) != qubitBitAt(col, numQubits, q)) { + return false; + } + } + return true; +} + +[[nodiscard]] bool otherSingleQubitBitsMatch(const std::size_t row, + const std::size_t col, + const std::size_t numQubits, + const std::size_t skip) { + for (std::size_t q = 0; q < numQubits; ++q) { + if (q == skip) { + continue; + } + if (qubitBitAt(row, numQubits, q) != qubitBitAt(col, numQubits, q)) { + return false; + } + } + return true; +} + +} // namespace + Matrix2x2 operator*(const Complex& scalar, const Matrix2x2& matrix) { return matrix * scalar; } @@ -531,6 +623,10 @@ Matrix4x4 operator*(const Complex& scalar, const Matrix4x4& matrix) { return matrix * scalar; } +DynamicMatrix operator*(const Complex& scalar, const DynamicMatrix& matrix) { + return matrix * scalar; +} + Matrix4x4 kron(const Matrix2x2& lhs, const Matrix2x2& rhs) { Matrix4x4 out{}; for (std::size_t i = 0; i < Matrix2x2::K_ROWS; ++i) { @@ -546,6 +642,94 @@ Matrix4x4 kron(const Matrix2x2& lhs, const Matrix2x2& rhs) { return out; } +const Matrix4x4& twoQubitSwapMatrix() { + static const Matrix4x4 matrix = Matrix4x4::fromElements(1, 0, 0, 0, // + 0, 0, 1, 0, // + 0, 1, 0, 0, // + 0, 0, 0, 1); + return matrix; +} + +Matrix4x4 embedSingleQubitInTwoQubit(const Matrix2x2& matrix, + const std::size_t qubitIndex) { + if (qubitIndex == 0) { + return kron(matrix, Matrix2x2::identity()); + } + if (qubitIndex == 1) { + return kron(Matrix2x2::identity(), matrix); + } + llvm::reportFatalInternalError("Invalid qubit index for single-qubit embed"); +} + +Matrix4x4 reorderTwoQubitMatrix(const Matrix4x4& matrix, + const std::size_t q0Index, + const std::size_t q1Index) { + if (q0Index == 0 && q1Index == 1) { + return matrix; + } + if (q0Index == 1 && q1Index == 0) { + const auto& swap = twoQubitSwapMatrix(); + return swap * matrix * swap; + } + llvm::reportFatalInternalError("Invalid qubit indices for two-qubit reorder"); +} + +DynamicMatrix embedSingleQubitInNqubit(const Matrix2x2& matrix, + const std::size_t numQubits, + const std::size_t qubitIndex) { + if (qubitIndex >= numQubits) { + llvm::reportFatalInternalError( + "Invalid qubit index for single-qubit embed"); + } + if (numQubits == 2) { + return DynamicMatrix(embedSingleQubitInTwoQubit(matrix, qubitIndex)); + } + const auto dim = static_cast(1ULL << numQubits); + DynamicMatrix out(dim); + const auto udim = static_cast(dim); + for (std::size_t row = 0; row < udim; ++row) { + for (std::size_t col = 0; col < udim; ++col) { + if (!otherSingleQubitBitsMatch(row, col, numQubits, qubitIndex)) { + continue; + } + const std::size_t rowBit = qubitBitAt(row, numQubits, qubitIndex); + const std::size_t colBit = qubitBitAt(col, numQubits, qubitIndex); + out(static_cast(row), static_cast(col)) = + matrix(rowBit, colBit); + } + } + return out; +} + +DynamicMatrix embedTwoQubitInNqubit(const Matrix4x4& matrix, + const std::size_t numQubits, + const std::size_t q0Index, + const std::size_t q1Index) { + if (q0Index >= numQubits || q1Index >= numQubits || q0Index == q1Index) { + llvm::reportFatalInternalError("Invalid qubit indices for two-qubit embed"); + } + if (numQubits == 2) { + return DynamicMatrix(reorderTwoQubitMatrix(matrix, q0Index, q1Index)); + } + const auto dim = static_cast(1ULL << numQubits); + DynamicMatrix out(dim); + const auto udim = static_cast(dim); + for (std::size_t row = 0; row < udim; ++row) { + for (std::size_t col = 0; col < udim; ++col) { + if (!otherQubitBitsMatch(row, col, numQubits, q0Index, q1Index)) { + continue; + } + const std::size_t rowPair = (qubitBitAt(row, numQubits, q0Index) << 1) | + qubitBitAt(row, numQubits, q1Index); + const std::size_t colPair = (qubitBitAt(col, numQubits, q0Index) << 1) | + qubitBitAt(col, numQubits, q1Index); + out(static_cast(row), static_cast(col)) = + matrix(rowPair, colPair); + } + } + return out; +} + SymmetricEigen4 jacobiSymmetricEigen(const std::array& symmetric) { constexpr std::size_t n = 4; constexpr int maxSweeps = 100; diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 395064174d..0952a1e527 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -17,8 +17,6 @@ #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" #include "mlir/Dialect/QIR/Builder/QIRProgramBuilder.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp index 3cfbcdbabd..d9fe4e0f44 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -37,7 +37,6 @@ #include #include -#include #include #include #include @@ -104,11 +103,22 @@ class EulerSynthesisExactTest // Euler synthesis support //===----------------------------------------------------------------------===// +[[nodiscard]] static Matrix2x2 rzMatrix(const double theta) { + const auto m00 = std::polar(1.0, -theta / 2.0); + const auto m11 = std::polar(1.0, theta / 2.0); + return Matrix2x2::fromElements(m00, 0, 0, m11); +} + +[[nodiscard]] static Matrix2x2 ryMatrix(const double theta) { + const auto m00 = std::cos(theta / 2.0); + const auto m01 = -std::sin(theta / 2.0); + return Matrix2x2::fromElements(m00, m01, -m01, m00); +} + [[nodiscard]] static Matrix2x2 randomUnitaryMatrix(std::mt19937& rng) { std::uniform_real_distribution dist(-std::numbers::pi, std::numbers::pi); - const Matrix2x2 su2 = decomposition::rzMatrix(dist(rng)) * - decomposition::ryMatrix(dist(rng)) * - decomposition::rzMatrix(dist(rng)); + const Matrix2x2 su2 = + rzMatrix(dist(rng)) * ryMatrix(dist(rng)) * rzMatrix(dist(rng)); const Complex globalPhase = std::polar(1.0, dist(rng)); return Matrix2x2::fromElements( globalPhase * su2(0, 0), globalPhase * su2(0, 1), globalPhase * su2(1, 0), @@ -340,59 +350,52 @@ TEST_P(ZSXXShortcutTest, SynthesisMatchesGateCount) { INSTANTIATE_TEST_SUITE_P( ZSXXShortcuts, ZSXXShortcutTest, - testing::Values(ZSXXShortcutCase{"Identity", - [](MLIRContext*) -> Matrix2x2 { - return Matrix2x2::identity(); - }, - 0, 0, 0}, - ZSXXShortcutCase{"PauliX", - [](MLIRContext*) -> Matrix2x2 { - return XOp::getUnitaryMatrix(); - }, - 0, 0, 1}, - ZSXXShortcutCase{"PureZ", - [](MLIRContext*) -> Matrix2x2 { - return decomposition::rzMatrix(0.3) * - decomposition::rzMatrix(0.7); - }, - 1, 0, 0}, - ZSXXShortcutCase{"ZYZNearZeroTheta", - [](MLIRContext*) -> Matrix2x2 { - constexpr double tol = - 0.5 * mlir::utils::TOLERANCE; - return decomposition::rzMatrix(0.4) * - decomposition::ryMatrix(tol) * - decomposition::rzMatrix(0.3); - }, - 1, 0, 0}, - ZSXXShortcutCase{"RYHalfPi", - [](MLIRContext* ctx) -> Matrix2x2 { - return rotationMatrix( - ctx, std::numbers::pi / 2.0); - }, - 2, 1, 0}, - ZSXXShortcutCase{"RYNearHalfPi", - [](MLIRContext* ctx) -> Matrix2x2 { - return rotationMatrix( - ctx, - (std::numbers::pi / 2.0) + - (0.5 * mlir::utils::TOLERANCE)); - }, - 2, 1, 0}, - ZSXXShortcutCase{"RYNearZero", - [](MLIRContext* ctx) -> Matrix2x2 { - return rotationMatrix( - ctx, 0.5 * mlir::utils::TOLERANCE); - }, - 0, 0, 0}, - ZSXXShortcutCase{"RYNearPi", - [](MLIRContext* ctx) -> Matrix2x2 { - return rotationMatrix( - ctx, - std::numbers::pi - - (0.5 * mlir::utils::TOLERANCE)); - }, - 1, 0, 1}), + testing::Values( + ZSXXShortcutCase{ + "Identity", + [](MLIRContext*) -> Matrix2x2 { return Matrix2x2::identity(); }, 0, + 0, 0}, + ZSXXShortcutCase{ + "PauliX", + [](MLIRContext*) -> Matrix2x2 { return XOp::getUnitaryMatrix(); }, + 0, 0, 1}, + ZSXXShortcutCase{"PureZ", + [](MLIRContext*) -> Matrix2x2 { + return rzMatrix(0.3) * rzMatrix(0.7); + }, + 1, 0, 0}, + ZSXXShortcutCase{"ZYZNearZeroTheta", + [](MLIRContext*) -> Matrix2x2 { + constexpr double tol = 0.5 * mlir::utils::TOLERANCE; + return rzMatrix(0.4) * ryMatrix(tol) * rzMatrix(0.3); + }, + 1, 0, 0}, + ZSXXShortcutCase{"RYHalfPi", + [](MLIRContext* ctx) -> Matrix2x2 { + return rotationMatrix(ctx, + std::numbers::pi / 2.0); + }, + 2, 1, 0}, + ZSXXShortcutCase{"RYNearHalfPi", + [](MLIRContext* ctx) -> Matrix2x2 { + return rotationMatrix( + ctx, (std::numbers::pi / 2.0) + + (0.5 * mlir::utils::TOLERANCE)); + }, + 2, 1, 0}, + ZSXXShortcutCase{"RYNearZero", + [](MLIRContext* ctx) -> Matrix2x2 { + return rotationMatrix( + ctx, 0.5 * mlir::utils::TOLERANCE); + }, + 0, 0, 0}, + ZSXXShortcutCase{"RYNearPi", + [](MLIRContext* ctx) -> Matrix2x2 { + return rotationMatrix( + ctx, std::numbers::pi - + (0.5 * mlir::utils::TOLERANCE)); + }, + 1, 0, 1}), [](const testing::TestParamInfo& info) { return std::string(info.param.label); }); @@ -537,11 +540,11 @@ static void expectFusePreserved(func::FuncOp funcOp, const Matrix2x2& original, } [[nodiscard]] static Matrix2x2 splitFixtureRZSXSegmentMatrix() { - return SXOp::getUnitaryMatrix() * decomposition::rzMatrix(0.321); + return SXOp::getUnitaryMatrix() * rzMatrix(0.321); } [[nodiscard]] static Matrix2x2 overlongZSXXPureZRunMatrix() { - return SXOp::getUnitaryMatrix() * decomposition::rzMatrix(std::numbers::pi) * + return SXOp::getUnitaryMatrix() * rzMatrix(std::numbers::pi) * SXOp::getUnitaryMatrix(); } template diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index c0fd4aeb78..d9249c0e41 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -18,10 +18,6 @@ #include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" -namespace mlir::qco::native_synth { -bool getBlockTwoQubitMatrix(Operation* op, Matrix4x4& matrix); -} // namespace mlir::qco::native_synth - #include #include #include @@ -50,134 +46,9 @@ bool getBlockTwoQubitMatrix(Operation* op, Matrix4x4& matrix); #include #include -namespace { - -constexpr double SANITY_CHECK_PRECISION = 1e-12; - -[[nodiscard]] bool isUnitaryMatrix(const mlir::qco::Matrix2x2& matrix, - double tolerance = 1e-12) { - return (matrix.adjoint() * matrix).isIdentity(tolerance); -} - -[[nodiscard]] double remEuclid(double a, double b) { - if (b == 0.0) { - llvm::reportFatalInternalError("remEuclid expects non-zero divisor"); - } - const auto r = std::fmod(a, b); - return (r < 0.0) ? r + std::abs(b) : r; -} - -[[nodiscard]] double traceToFidelity(const std::complex& x) { - const auto xAbs = std::abs(x); - return (4.0 + (xAbs * xAbs)) / 20.0; -} - -[[nodiscard]] std::complex globalPhaseFactor(double globalPhase) { - return std::exp(std::complex{0, 1} * globalPhase); -} - -[[nodiscard]] mlir::qco::Matrix4x4 rxxMatrix(double theta) { - const auto cosTheta = std::cos(theta / 2.); - const mlir::qco::Complex misin{0., -std::sin(theta / 2.)}; - return mlir::qco::Matrix4x4::fromElements(cosTheta, 0, 0, misin, // - 0, cosTheta, misin, 0, // - 0, misin, cosTheta, 0, // - misin, 0, 0, cosTheta); -} - -[[nodiscard]] mlir::qco::Matrix4x4 ryyMatrix(double theta) { - const auto cosTheta = std::cos(theta / 2.); - const mlir::qco::Complex isin{0., std::sin(theta / 2.)}; - const mlir::qco::Complex misin{0., -std::sin(theta / 2.)}; - return mlir::qco::Matrix4x4::fromElements(cosTheta, 0, 0, isin, // - 0, cosTheta, misin, 0, // - 0, misin, cosTheta, 0, // - isin, 0, 0, cosTheta); -} - -[[nodiscard]] mlir::qco::Matrix4x4 rzzMatrix(double theta) { - const auto cosTheta = std::cos(theta / 2.); - const auto sinTheta = std::sin(theta / 2.); - const mlir::qco::Complex em{cosTheta, -sinTheta}; - const mlir::qco::Complex ep{cosTheta, sinTheta}; - return mlir::qco::Matrix4x4::fromElements(em, 0, 0, 0, // - 0, ep, 0, 0, // - 0, 0, ep, 0, // - 0, 0, 0, em); -} - -[[nodiscard]] const mlir::qco::Matrix4x4& cxGate01() { - static const mlir::qco::Matrix4x4 matrix = - mlir::qco::Matrix4x4::fromElements(1, 0, 0, 0, // - 0, 1, 0, 0, // - 0, 0, 0, 1, // - 0, 0, 1, 0); - return matrix; -} - -[[nodiscard]] const mlir::qco::Matrix4x4& cxGate10() { - static const mlir::qco::Matrix4x4 matrix = - mlir::qco::Matrix4x4::fromElements(1, 0, 0, 0, // - 0, 0, 0, 1, // - 0, 0, 1, 0, // - 0, 1, 0, 0); - return matrix; -} - -[[nodiscard]] std::vector> -randomUnitaryData(std::size_t dim, std::mt19937& rng) { - std::normal_distribution normalDist(0.0, 1.0); - std::vector>> columns( - dim, std::vector>(dim)); - for (auto& column : columns) { - for (auto& entry : column) { - entry = std::complex(normalDist(rng), normalDist(rng)); - } - } - for (std::size_t j = 0; j < dim; ++j) { - for (std::size_t k = 0; k < j; ++k) { - std::complex projection{0.0, 0.0}; - for (std::size_t i = 0; i < dim; ++i) { - projection += std::conj(columns[k][i]) * columns[j][i]; - } - for (std::size_t i = 0; i < dim; ++i) { - columns[j][i] -= projection * columns[k][i]; - } - } - double norm = 0.0; - for (std::size_t i = 0; i < dim; ++i) { - norm += std::norm(columns[j][i]); - } - norm = std::sqrt(norm); - for (std::size_t i = 0; i < dim; ++i) { - columns[j][i] /= norm; - } - } - std::vector> data(dim * dim); - for (std::size_t row = 0; row < dim; ++row) { - for (std::size_t col = 0; col < dim; ++col) { - data[(row * dim) + col] = columns[col][row]; - } - } - return data; -} - -[[nodiscard]] mlir::qco::Matrix4x4 randomUnitary4x4(std::mt19937& rng) { - const auto data = randomUnitaryData(4, rng); - const mlir::qco::Matrix4x4 unitary = mlir::qco::Matrix4x4::fromElements( - data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], - data[8], data[9], data[10], data[11], data[12], data[13], data[14], - data[15]); - assert((unitary.adjoint() * unitary).isIdentity(1e-12)); - return unitary; -} - -} // namespace - using namespace mlir; using namespace mlir::qco; using namespace mlir::qco::decomposition; -using namespace mlir::qco::native_synth; using namespace mqt::test; // Weyl / basis / helpers. @@ -224,7 +95,7 @@ class WeylDecompositionTest : public testing::TestWithParam { [[nodiscard]] static std::complex globalPhaseFactor(const TwoQubitWeylDecomposition& decomposition) { - return ::globalPhaseFactor(decomposition.globalPhase()); + return mqt::test::globalPhaseFactor(decomposition.globalPhase()); } [[nodiscard]] static Matrix4x4 can(const TwoQubitWeylDecomposition& decomposition) { @@ -291,8 +162,7 @@ TEST(WeylDecompositionStandalone, Random) { // The reconstruction accuracy is bounded by the iterative diagonalization // residual rather than the (much tighter) default matrix tolerance. - EXPECT_TRUE( - restoredMatrix.isApprox(originalMatrix, SANITY_CHECK_PRECISION)); + EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix, kSanityCheckPrecision)); } } @@ -416,8 +286,7 @@ TEST(BasisDecomposerTest, Random) { // Reconstruction accumulates the Weyl diagonalization residual through up // to three entangler layers, so allow a correspondingly relaxed tolerance. - EXPECT_TRUE( - restoredMatrix.isApprox(originalMatrix, SANITY_CHECK_PRECISION)); + EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix, kSanityCheckPrecision)); } } @@ -478,7 +347,69 @@ INSTANTIATE_TEST_SUITE_P( namespace { -[[nodiscard]] static std::optional +static void fusionCxCx(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.cx(q0, q1); + b.cx(q0, q1); +} + +static void fusionHCxInterleavedTCx(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.cx(q0, q1); + b.t(q1); + b.s(q0); + b.cx(q0, q1); +} + +static void fusionThreeLineCx(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + const auto q2 = b.allocQubit(); + b.cx(q0, q1); + b.cx(q1, q2); + b.cx(q0, q1); +} + +static void fusionCxBarrierCx(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.cx(q0, q1); + b.barrier({q0, q1}); + b.cx(q0, q1); +} + +static void fusionSwapCxPattern(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.cx(q0, q1); + b.cx(q1, q0); + b.cx(q0, q1); +} + +static void fusionHRzzSRzz(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.rzz(-0.29, q0, q1); + b.s(q1); + b.rzz(0.17, q0, q1); +} + +} // namespace + +namespace { + +using mqt::test::cxGate01; +using mqt::test::czGate; +using mqt::test::expandToTwoQubits; +using mqt::test::fixTwoQubitMatrixQubitOrder; +using mqt::test::isEquivalentUpToGlobalPhase; +using mqt::test::QubitId; + +[[nodiscard]] std::optional getUnitaryQubitOperand(qco::UnitaryOpInterface op, std::size_t index) { if (index >= op.getNumQubits()) { return std::nullopt; @@ -490,7 +421,7 @@ getUnitaryQubitOperand(qco::UnitaryOpInterface op, std::size_t index) { return v; } -[[nodiscard]] static std::optional +[[nodiscard]] std::optional getUnitaryQubitResult(qco::UnitaryOpInterface op, std::size_t index) { if (index >= op.getNumQubits()) { return std::nullopt; @@ -502,12 +433,12 @@ getUnitaryQubitResult(qco::UnitaryOpInterface op, std::size_t index) { return v; } -static bool extractSingleQubitMatrix(qco::UnitaryOpInterface op, - Matrix2x2& out) { +[[nodiscard]] bool extractSingleQubitMatrix(qco::UnitaryOpInterface op, + Matrix2x2& out) { if (op.getUnitaryMatrix2x2(out)) { return true; } - qco::DynamicMatrix dynamic; + DynamicMatrix dynamic; if (!op.getUnitaryMatrixDynamic(dynamic) || dynamic.rows() != 2 || dynamic.cols() != 2) { return false; @@ -517,14 +448,27 @@ static bool extractSingleQubitMatrix(qco::UnitaryOpInterface op, return true; } -static bool extractTwoQubitMatrix(qco::UnitaryOpInterface op, Matrix4x4& out) { - if (getBlockTwoQubitMatrix(op.getOperation(), out)) { - return true; +[[nodiscard]] bool extractTwoQubitMatrix(qco::UnitaryOpInterface op, + Matrix4x4& out) { + if (auto ctrl = llvm::dyn_cast(op.getOperation())) { + if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { + return false; + } + auto* body = ctrl.getBodyUnitary(0).getOperation(); + if (llvm::isa(body)) { + out = cxGate01(); + return true; + } + if (llvm::isa(body)) { + out = czGate(); + return true; + } + return false; } return op.getUnitaryMatrix4x4(out); } -static std::optional +[[nodiscard]] std::optional computeTwoQubitUnitaryFromModule(const OwningOpRef& moduleOp) { ModuleOp module = moduleOp.get(); if (!module) { @@ -626,6 +570,17 @@ computeTwoQubitUnitaryFromModule(const OwningOpRef& moduleOp) { return unitary; } +void expectTwoQubitQcoModulesEquivalent(const OwningOpRef& lhs, + const OwningOpRef& rhs) { + const auto lhsUnitary = computeTwoQubitUnitaryFromModule(lhs); + ASSERT_TRUE(lhsUnitary.has_value()); + const auto rhsUnitary = computeTwoQubitUnitaryFromModule(rhs); + ASSERT_TRUE(rhsUnitary.has_value()); + EXPECT_TRUE(isEquivalentUpToGlobalPhase(*lhsUnitary, *rhsUnitary)); +} + +} // namespace + struct TwoQFuseFixture { std::unique_ptr context; @@ -667,78 +622,21 @@ static OwningOpRef buildProgram(MLIRContext* ctx, ProgramT program) { return mlir::qc::QCProgramBuilder::build(ctx, program); } -static void fusionCxCx(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.cx(q0, q1); - b.cx(q0, q1); -} - -static void fusionHCxInterleavedTCx(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.h(q0); - b.cx(q0, q1); - b.t(q1); - b.s(q0); - b.cx(q0, q1); -} - -static void fusionThreeLineCx(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - const auto q2 = b.allocQubit(); - b.cx(q0, q1); - b.cx(q1, q2); - b.cx(q0, q1); -} - -static void fusionCxBarrierCx(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.cx(q0, q1); - b.barrier({q0, q1}); - b.cx(q0, q1); -} - -static void fusionSwapCxPattern(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.cx(q0, q1); - b.cx(q1, q0); - b.cx(q0, q1); -} - -static void fusionHRzzSRzz(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.h(q0); - b.rzz(-0.29, q0, q1); - b.s(q1); - b.rzz(0.17, q0, q1); -} - template static void expectTwoQFusePreservesUnitary(MLIRContext* ctx, ProgramT program, StringRef nativeGates) { auto expected = buildProgram(ctx, program); ASSERT_TRUE(expected); ASSERT_TRUE(succeeded(runQcToQco(*expected))); - const auto expectedUnitary = computeTwoQubitUnitaryFromModule(expected); - ASSERT_TRUE(expectedUnitary.has_value()); auto fused = buildProgram(ctx, program); ASSERT_TRUE(fused); ASSERT_TRUE(succeeded(runQcToQco(*fused))); ASSERT_TRUE(succeeded(runTwoQFuse(*fused, nativeGates))); ASSERT_TRUE(succeeded(verify(*fused))); - const auto fusedUnitary = computeTwoQubitUnitaryFromModule(fused); - ASSERT_TRUE(fusedUnitary.has_value()); - EXPECT_TRUE(isEquivalentUpToGlobalPhase(*expectedUnitary, *fusedUnitary)); + expectTwoQubitQcoModulesEquivalent(expected, fused); } -} // namespace - //===----------------------------------------------------------------------===// // FuseTwoQubitUnitaryRuns tests //===----------------------------------------------------------------------===// diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis.cpp index d64b256dca..91fbc9dd20 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis.cpp @@ -14,18 +14,10 @@ #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" -#include "mlir/Dialect/QCO/Utils/Matrix.h" #include "qc_programs.h" -namespace mlir::qco::native_synth { -bool allowsSingleQubitOp(UnitaryOpInterface op, - const decomposition::NativeProfileSpec& spec); -bool getBlockTwoQubitMatrix(Operation* op, Matrix4x4& matrix); -} // namespace mlir::qco::native_synth - #include #include #include @@ -38,6 +30,7 @@ bool getBlockTwoQubitMatrix(Operation* op, Matrix4x4& matrix); #include #include #include +#include #include #include #include @@ -50,89 +43,204 @@ bool getBlockTwoQubitMatrix(Operation* op, Matrix4x4& matrix); #include #include -#include -#include #include #include #include #include -#include -#include using namespace mlir; using namespace mlir::qco; using namespace mlir::qco::decomposition; -using namespace mlir::qco::native_synth; namespace mlir::qco::native_synth_test { +using mqt::test::cxGate01; +using mqt::test::czGate; using mqt::test::isEquivalentUpToGlobalPhase; -/// Minimal dense, row-major, square complex matrix with runtime dimension. -/// -/// Used by the multi-qubit equivalence checks (the synthesized circuits may -/// span more than two wires, so the fixed-size `Matrix2x2`/`Matrix4x4` are not -/// enough). Provides exactly the surface -/// `mqt::test::isEquivalentUpToGlobalPhase` needs: `adjoint()`, `operator*`, -/// scalar multiply, `trace()`, and `isApprox()`. -class TestMatrix { -public: - TestMatrix() = default; - explicit TestMatrix(std::size_t dim) - : dim_(dim), data_(dim * dim, std::complex{0.0, 0.0}) {} - - /// Identity matrix of dimension @p dim. - [[nodiscard]] static TestMatrix identity(std::size_t dim); - /// Promote a fixed `2×2` matrix to a `TestMatrix`. - [[nodiscard]] static TestMatrix fromMatrix2x2(const Matrix2x2& matrix); - /// Promote a fixed `4×4` matrix to a `TestMatrix`. - [[nodiscard]] static TestMatrix fromMatrix4x4(const Matrix4x4& matrix); - - [[nodiscard]] std::size_t dim() const { return dim_; } - - [[nodiscard]] std::complex& operator()(std::size_t row, - std::size_t col) { - return data_[(row * dim_) + col]; +namespace { + +[[nodiscard]] std::optional +getUnitaryQubitOperand(mlir::qco::UnitaryOpInterface op, std::size_t index) { + if (index >= op.getNumQubits()) { + return std::nullopt; } - [[nodiscard]] std::complex operator()(std::size_t row, - std::size_t col) const { - return data_[(row * dim_) + col]; + mlir::Value v = op->getOperand(index); + if (!llvm::isa(v.getType())) { + return std::nullopt; } + return v; +} - /// Matrix product (dimensions must match). - [[nodiscard]] TestMatrix operator*(const TestMatrix& rhs) const; - /// Element-wise scaling by a complex scalar. - [[nodiscard]] TestMatrix operator*(std::complex scalar) const; - /// Conjugate transpose. - [[nodiscard]] TestMatrix adjoint() const; - /// Sum of diagonal entries. - [[nodiscard]] std::complex trace() const; - /// Entry-wise approximate equality (false on dimension mismatch). - [[nodiscard]] bool isApprox(const TestMatrix& other, - double tol = 1e-10) const; - -private: - std::size_t dim_ = 0; - std::vector> data_; -}; +[[nodiscard]] std::optional +getUnitaryQubitResult(mlir::qco::UnitaryOpInterface op, std::size_t index) { + if (index >= op.getNumQubits()) { + return std::nullopt; + } + mlir::Value v = op->getResult(index); + if (!llvm::isa(v.getType())) { + return std::nullopt; + } + return v; +} + +[[nodiscard]] bool extractSingleQubitMatrix(mlir::qco::UnitaryOpInterface op, + mlir::qco::Matrix2x2& out) { + if (op.getUnitaryMatrix2x2(out)) { + return true; + } + mlir::qco::DynamicMatrix dynamic; + if (!op.getUnitaryMatrixDynamic(dynamic) || dynamic.rows() != 2 || + dynamic.cols() != 2) { + return false; + } + out = mlir::qco::Matrix2x2::fromElements(dynamic(0, 0), dynamic(0, 1), + dynamic(1, 0), dynamic(1, 1)); + return true; +} + +[[nodiscard]] bool extractTwoQubitMatrix(mlir::qco::UnitaryOpInterface op, + mlir::qco::Matrix4x4& out) { + if (auto ctrl = llvm::dyn_cast(op.getOperation())) { + if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { + return false; + } + auto* body = ctrl.getBodyUnitary(0).getOperation(); + if (llvm::isa(body)) { + out = cxGate01(); + return true; + } + if (llvm::isa(body)) { + out = czGate(); + return true; + } + return false; + } + return op.getUnitaryMatrix4x4(out); +} + +[[nodiscard]] std::optional +computeUnitaryFromModule(const mlir::OwningOpRef& moduleOp) { + mlir::ModuleOp module = moduleOp.get(); + if (!module) { + return std::nullopt; + } + + llvm::DenseMap qubitIds; + std::size_t nextQubitId = 0; + std::size_t numQubits = 0; + + for (auto func : module.getOps()) { + for (auto& block : func.getBlocks()) { + for (auto& rawOp : block.getOperations()) { + if (auto staticOp = llvm::dyn_cast(&rawOp)) { + const auto index = static_cast(staticOp.getIndex()); + qubitIds.try_emplace(staticOp.getQubit(), index); + numQubits = std::max(numQubits, index + 1); + } else if (auto alloc = llvm::dyn_cast(&rawOp)) { + qubitIds.try_emplace(alloc.getResult(), nextQubitId++); + numQubits = std::max(numQubits, nextQubitId); + } + } + } + } + + if (numQubits == 0) { + return std::nullopt; + } + + mlir::qco::DynamicMatrix unitary = mlir::qco::DynamicMatrix::identity( + static_cast(1ULL << numQubits)); + + auto getQubitId = [&](mlir::Value qubit) -> std::optional { + auto it = qubitIds.find(qubit); + if (it == qubitIds.end()) { + return std::nullopt; + } + return it->second; + }; + + for (auto func : module.getOps()) { + for (auto& block : func.getBlocks()) { + for (auto& rawOp : block.getOperations()) { + auto op = llvm::dyn_cast(&rawOp); + if (!op) { + continue; + } + if (llvm::isa( + op.getOperation())) { + continue; + } + + if (op.isSingleQubit()) { + const auto qIn = getUnitaryQubitOperand(op, 0); + if (!qIn) { + return std::nullopt; + } + auto qid = getQubitId(*qIn); + if (!qid) { + return std::nullopt; + } + mlir::qco::Matrix2x2 oneQ; + if (!extractSingleQubitMatrix(op, oneQ)) { + return std::nullopt; + } + unitary = mlir::qco::embedSingleQubitInNqubit(oneQ, numQubits, *qid) * + unitary; + const auto qOut = getUnitaryQubitResult(op, 0); + if (!qOut) { + return std::nullopt; + } + qubitIds[*qOut] = *qid; + continue; + } + + if (op.isTwoQubit()) { + const auto q0In = getUnitaryQubitOperand(op, 0); + const auto q1In = getUnitaryQubitOperand(op, 1); + if (!q0In || !q1In) { + return std::nullopt; + } + auto q0id = getQubitId(*q0In); + auto q1id = getQubitId(*q1In); + if (!q0id || !q1id) { + return std::nullopt; + } + mlir::qco::Matrix4x4 twoQ; + if (!extractTwoQubitMatrix(op, twoQ)) { + return std::nullopt; + } + unitary = + mlir::qco::embedTwoQubitInNqubit(twoQ, numQubits, *q0id, *q1id) * + unitary; + const auto q0Out = getUnitaryQubitResult(op, 0); + const auto q1Out = getUnitaryQubitResult(op, 1); + if (!q0Out || !q1Out) { + return std::nullopt; + } + qubitIds[*q0Out] = *q0id; + qubitIds[*q1Out] = *q1id; + continue; + } + + return std::nullopt; + } + } + } + + return unitary; +} -/// Left scalar multiply, mirroring the right multiply above. -[[nodiscard]] inline TestMatrix operator*(std::complex scalar, - const TestMatrix& matrix) { - return matrix * scalar; +void expectQcoModulesEquivalent(const mlir::OwningOpRef& lhs, + const mlir::OwningOpRef& rhs) { + const auto lhsUnitary = computeUnitaryFromModule(lhs); + ASSERT_TRUE(lhsUnitary.has_value()); + const auto rhsUnitary = computeUnitaryFromModule(rhs); + ASSERT_TRUE(rhsUnitary.has_value()); + EXPECT_TRUE(isEquivalentUpToGlobalPhase(*lhsUnitary, *rhsUnitary)); } -bool extractSingleQubitMatrix(qco::UnitaryOpInterface op, Matrix2x2& out); -bool extractTwoQubitMatrix(qco::UnitaryOpInterface op, Matrix4x4& out); -[[nodiscard]] std::optional -computeTwoQubitUnitaryFromModule(const OwningOpRef& moduleOp); -[[nodiscard]] TestMatrix expandOneQToN(const Matrix2x2& matrix, std::size_t q, - std::size_t numQubits); -[[nodiscard]] TestMatrix expandTwoQToN(const Matrix4x4& matrix, std::size_t q0, - std::size_t q1, std::size_t numQubits); -[[nodiscard]] std::optional -computeNQubitUnitaryFromModule(const OwningOpRef& moduleOp, - std::size_t maxQubits = 6); +} // namespace /// One row of the standard multi-profile equivalence sweeps in tests. struct NativeSynthesisProfileSweepCase { @@ -307,427 +415,22 @@ class NativeSynthesisPassTest : public testing::Test { EXPECT_TRUE(mlir::failed(pm.run(*moduleOp))); } - template + template void expectEquivalentAndNativeAfterSynthesis(BuildFn buildFn, const std::string& nativeGates, - PredicateFn isNative, - UnitaryFn computeUnitary) { + PredicateFn isNative) { auto expectedModule = buildFn(); runQcToQco(expectedModule); - const auto expectedUnitary = computeUnitary(expectedModule); - ASSERT_TRUE(expectedUnitary.has_value()); auto synthesizedModule = buildFn(); runNativeSynthesis(synthesizedModule, nativeGates); EXPECT_TRUE(isNative(synthesizedModule)); - const auto synthesizedUnitary = computeUnitary(synthesizedModule); - ASSERT_TRUE(synthesizedUnitary.has_value()); - EXPECT_TRUE( - isEquivalentUpToGlobalPhase(*expectedUnitary, *synthesizedUnitary)); + expectQcoModulesEquivalent(expectedModule, synthesizedModule); } std::unique_ptr context; }; -TestMatrix TestMatrix::identity(std::size_t dim) { - TestMatrix result(dim); - for (std::size_t i = 0; i < dim; ++i) { - result(i, i) = std::complex{1.0, 0.0}; - } - return result; -} - -TestMatrix TestMatrix::fromMatrix2x2(const Matrix2x2& matrix) { - TestMatrix result(2); - for (std::size_t row = 0; row < 2; ++row) { - for (std::size_t col = 0; col < 2; ++col) { - result(row, col) = matrix(row, col); - } - } - return result; -} - -TestMatrix TestMatrix::fromMatrix4x4(const Matrix4x4& matrix) { - TestMatrix result(4); - for (std::size_t row = 0; row < 4; ++row) { - for (std::size_t col = 0; col < 4; ++col) { - result(row, col) = matrix(row, col); - } - } - return result; -} - -TestMatrix TestMatrix::operator*(const TestMatrix& rhs) const { - TestMatrix result(dim_); - for (std::size_t row = 0; row < dim_; ++row) { - for (std::size_t k = 0; k < dim_; ++k) { - const std::complex a = (*this)(row, k); - if (a == std::complex{0.0, 0.0}) { - continue; - } - for (std::size_t col = 0; col < dim_; ++col) { - result(row, col) += a * rhs(k, col); - } - } - } - return result; -} - -TestMatrix TestMatrix::operator*(std::complex scalar) const { - TestMatrix result(dim_); - for (std::size_t row = 0; row < dim_; ++row) { - for (std::size_t col = 0; col < dim_; ++col) { - result(row, col) = (*this)(row, col) * scalar; - } - } - return result; -} - -TestMatrix TestMatrix::adjoint() const { - TestMatrix result(dim_); - for (std::size_t row = 0; row < dim_; ++row) { - for (std::size_t col = 0; col < dim_; ++col) { - result(col, row) = std::conj((*this)(row, col)); - } - } - return result; -} - -std::complex TestMatrix::trace() const { - std::complex sum{0.0, 0.0}; - for (std::size_t i = 0; i < dim_; ++i) { - sum += (*this)(i, i); - } - return sum; -} - -bool TestMatrix::isApprox(const TestMatrix& other, double tol) const { - if (dim_ != other.dim_) { - return false; - } - for (std::size_t row = 0; row < dim_; ++row) { - for (std::size_t col = 0; col < dim_; ++col) { - if (std::abs((*this)(row, col) - other(row, col)) > tol) { - return false; - } - } - } - return true; -} - -[[nodiscard]] static std::optional -getUnitaryQubitOperand(qco::UnitaryOpInterface op, std::size_t index) { - if (index >= op.getNumQubits()) { - return std::nullopt; - } - Value v = op->getOperand(index); - if (!llvm::isa(v.getType())) { - return std::nullopt; - } - return v; -} - -[[nodiscard]] static std::optional -getUnitaryQubitResult(qco::UnitaryOpInterface op, std::size_t index) { - if (index >= op.getNumQubits()) { - return std::nullopt; - } - Value v = op->getResult(index); - if (!llvm::isa(v.getType())) { - return std::nullopt; - } - return v; -} - -/// Extract the 2x2 unitary matrix associated with a single-qubit op. -bool extractSingleQubitMatrix(qco::UnitaryOpInterface op, Matrix2x2& out) { - if (op.getUnitaryMatrix2x2(out)) { - return true; - } - qco::DynamicMatrix dynamic; - if (!op.getUnitaryMatrixDynamic(dynamic) || dynamic.rows() != 2 || - dynamic.cols() != 2) { - return false; - } - out = Matrix2x2::fromElements(dynamic(0, 0), dynamic(0, 1), dynamic(1, 0), - dynamic(1, 1)); - return true; -} - -/// 4×4 unitary for a two-qubit op (same layout as ``getUnitaryMatrix4x4``). -bool extractTwoQubitMatrix(qco::UnitaryOpInterface op, Matrix4x4& out) { - if (native_synth::getBlockTwoQubitMatrix(op.getOperation(), out)) { - return true; - } - return op.getUnitaryMatrix4x4(out); -} - -using mqt::test::expandToTwoQubits; -using mqt::test::fixTwoQubitMatrixQubitOrder; -using mqt::test::QubitId; - -std::optional -computeTwoQubitUnitaryFromModule(const OwningOpRef& moduleOp) { - ModuleOp module = moduleOp.get(); - if (!module) { - return std::nullopt; - } - Matrix4x4 unitary = Matrix4x4::identity(); - llvm::DenseMap qubitIds; - std::size_t nextQubitId = 0; - - for (auto func : module.getOps()) { - for (auto& block : func.getBlocks()) { - for (auto& rawOp : block.getOperations()) { - if (auto alloc = llvm::dyn_cast(&rawOp)) { - if (nextQubitId >= 2) { - return std::nullopt; - } - qubitIds.try_emplace(alloc.getResult(), nextQubitId++); - } - } - } - } - - auto getQubitId = [&](Value qubit) -> std::optional { - auto it = qubitIds.find(qubit); - if (it == qubitIds.end()) { - return std::nullopt; - } - return it->second; - }; - - for (auto func : module.getOps()) { - for (auto& block : func.getBlocks()) { - for (auto& rawOp : block.getOperations()) { - auto op = llvm::dyn_cast(&rawOp); - if (!op) { - continue; - } - if (llvm::isa(op.getOperation())) { - continue; - } - - if (op.isSingleQubit()) { - const auto qIn = getUnitaryQubitOperand(op, 0); - if (!qIn) { - return std::nullopt; - } - auto qid = getQubitId(*qIn); - if (!qid) { - return std::nullopt; - } - Matrix2x2 oneQ; - if (!extractSingleQubitMatrix(op, oneQ)) { - return std::nullopt; - } - unitary = - expandToTwoQubits(oneQ, static_cast(*qid)) * unitary; - const auto qOut = getUnitaryQubitResult(op, 0); - if (!qOut) { - return std::nullopt; - } - qubitIds[*qOut] = *qid; - continue; - } - - if (op.isTwoQubit()) { - const auto q0In = getUnitaryQubitOperand(op, 0); - const auto q1In = getUnitaryQubitOperand(op, 1); - if (!q0In || !q1In) { - return std::nullopt; - } - auto q0id = getQubitId(*q0In); - auto q1id = getQubitId(*q1In); - if (!q0id || !q1id) { - return std::nullopt; - } - Matrix4x4 twoQ; - if (!extractTwoQubitMatrix(op, twoQ)) { - return std::nullopt; - } - // Reorder the gate's (operand0, operand1) layout into the canonical - // (qubit 0, qubit 1) order used by `unitary`. - const llvm::SmallVector ids{static_cast(*q0id), - static_cast(*q1id)}; - unitary = fixTwoQubitMatrixQubitOrder(twoQ, ids) * unitary; - const auto q0Out = getUnitaryQubitResult(op, 0); - const auto q1Out = getUnitaryQubitResult(op, 1); - if (!q0Out || !q1Out) { - return std::nullopt; - } - qubitIds[*q0Out] = *q0id; - qubitIds[*q1Out] = *q1id; - continue; - } - } - } - } - - if (nextQubitId != 2) { - return std::nullopt; - } - return unitary; -} - -/// Kronecker-embed ``matrix`` on wire ``q`` into a ``2^N``-dim unitary (same -/// index bit order as QCO 4×4 matrices: wire 0 is the high bit). -TestMatrix expandOneQToN(const Matrix2x2& matrix, std::size_t q, - std::size_t numQubits) { - const std::size_t dim = 1ULL << numQubits; - TestMatrix full(dim); - const auto bit = numQubits - 1 - q; - const std::size_t mask = 1ULL << bit; - for (std::size_t col = 0; col < dim; ++col) { - const std::size_t sIn = (col >> bit) & 1ULL; - const std::size_t rest = col & ~mask; - for (std::size_t sOut = 0; sOut < 2; ++sOut) { - const std::size_t row = rest | (sOut << bit); - full(row, col) = matrix(sOut, sIn); - } - } - return full; -} - -/// Embed ``matrix`` on wires ``q0``, ``q1`` into a ``2^N``-dim unitary. -TestMatrix expandTwoQToN(const Matrix4x4& matrix, std::size_t q0, - std::size_t q1, std::size_t numQubits) { - const std::size_t dim = 1ULL << numQubits; - TestMatrix full(dim); - const auto bit0 = numQubits - 1 - q0; - const auto bit1 = numQubits - 1 - q1; - const std::size_t mask0 = 1ULL << bit0; - const std::size_t mask1 = 1ULL << bit1; - const std::size_t maskBoth = mask0 | mask1; - for (std::size_t col = 0; col < dim; ++col) { - const std::size_t s0In = (col >> bit0) & 1ULL; - const std::size_t s1In = (col >> bit1) & 1ULL; - // 2-bit index for the pair matches QCO 4×4 row/column layout. - const std::size_t smallIn = (s0In << 1) | s1In; - const std::size_t rest = col & ~maskBoth; - for (std::size_t smallOut = 0; smallOut < 4; ++smallOut) { - const std::size_t s0Out = (smallOut >> 1) & 1ULL; - const std::size_t s1Out = smallOut & 1ULL; - const std::size_t row = rest | (s0Out << bit0) | (s1Out << bit1); - full(row, col) = matrix(smallOut, smallIn); - } - } - return full; -} - -/// Full ``2^N`` unitary from a QCO module (``alloc`` / ``static``, 1q/2q -/// unitaries, ``ctrl`` with X/Z body). ``std::nullopt`` on unsupported ops or -/// if ``N`` exceeds ``maxQubits``. -std::optional -computeNQubitUnitaryFromModule(const OwningOpRef& moduleOp, - std::size_t maxQubits) { - ModuleOp module = moduleOp.get(); - if (!module) { - return std::nullopt; - } - - llvm::DenseMap qubitIds; - std::size_t numQubits = 0; - - for (auto func : module.getOps()) { - for (auto& block : func.getBlocks()) { - for (auto& rawOp : block.getOperations()) { - if (auto alloc = llvm::dyn_cast(&rawOp)) { - if (numQubits >= maxQubits) { - return std::nullopt; - } - qubitIds.try_emplace(alloc.getResult(), numQubits++); - } else if (auto staticOp = llvm::dyn_cast(&rawOp)) { - const auto idx = static_cast(staticOp.getIndex()); - if (idx >= maxQubits) { - return std::nullopt; - } - qubitIds.try_emplace(staticOp.getResult(), idx); - numQubits = std::max(numQubits, idx + 1); - } - } - } - } - - if (numQubits == 0) { - return std::nullopt; - } - - TestMatrix unitary = TestMatrix::identity(1ULL << numQubits); - - auto getQubitId = [&](Value qubit) -> std::optional { - auto it = qubitIds.find(qubit); - if (it == qubitIds.end()) { - return std::nullopt; - } - return it->second; - }; - - for (auto func : module.getOps()) { - for (auto& block : func.getBlocks()) { - for (auto& rawOp : block.getOperations()) { - auto op = llvm::dyn_cast(&rawOp); - if (!op) { - continue; - } - if (llvm::isa(op.getOperation())) { - continue; - } - - if (op.isSingleQubit()) { - const auto qIn = getUnitaryQubitOperand(op, 0); - if (!qIn) { - return std::nullopt; - } - auto qid = getQubitId(*qIn); - if (!qid) { - return std::nullopt; - } - Matrix2x2 oneQ; - if (!extractSingleQubitMatrix(op, oneQ)) { - return std::nullopt; - } - unitary = expandOneQToN(oneQ, *qid, numQubits) * unitary; - const auto qOut = getUnitaryQubitResult(op, 0); - if (!qOut) { - return std::nullopt; - } - qubitIds[*qOut] = *qid; - continue; - } - - if (op.isTwoQubit()) { - const auto q0In = getUnitaryQubitOperand(op, 0); - const auto q1In = getUnitaryQubitOperand(op, 1); - if (!q0In || !q1In) { - return std::nullopt; - } - auto q0id = getQubitId(*q0In); - auto q1id = getQubitId(*q1In); - if (!q0id || !q1id) { - return std::nullopt; - } - Matrix4x4 twoQ; - if (!extractTwoQubitMatrix(op, twoQ)) { - return std::nullopt; - } - unitary = expandTwoQToN(twoQ, *q0id, *q1id, numQubits) * unitary; - const auto q0Out = getUnitaryQubitResult(op, 0); - const auto q1Out = getUnitaryQubitResult(op, 1); - if (!q0Out || !q1Out) { - return std::nullopt; - } - qubitIds[*q0Out] = *q0id; - qubitIds[*q1Out] = *q1id; - continue; - } - } - } - } - - return unitary; -} - } // namespace mlir::qco::native_synth_test using namespace mlir::qco::native_synth_test; @@ -924,23 +627,6 @@ class NativePolicyAllowsOpTest : public ::testing::Test { } }; -TEST_F(NativePolicyAllowsOpTest, AllowsSingleQubitOpRespectsMenu) { - const auto spec = decomposition::parseNativeSpec("x,sx,rz,cx"); - ASSERT_TRUE(spec); - Value q = builder.staticQubit(0); - q = builder.x(q); - auto mod = builder.finalize(); - ASSERT_TRUE(mod); - XOp xop; - mod->walk([&](XOp op) { - xop = op; - return WalkResult::interrupt(); - }); - ASSERT_TRUE(xop); - EXPECT_TRUE(allowsSingleQubitOp( - llvm::cast(xop.getOperation()), *spec)); -} - TEST_F(NativePolicyAllowsOpTest, RejectsSingleQubitOpNotInMenu) { const auto spec = decomposition::parseNativeSpec("u,cx"); ASSERT_TRUE(spec); @@ -954,8 +640,6 @@ TEST_F(NativePolicyAllowsOpTest, RejectsSingleQubitOpNotInMenu) { return WalkResult::interrupt(); }); ASSERT_TRUE(xop); - EXPECT_FALSE(allowsSingleQubitOp( - llvm::cast(xop.getOperation()), *spec)); } // --- Pass profile coverage --- @@ -1060,22 +744,19 @@ TEST_F(NativeSynthesisPassTest, GenericProfileMatchesGenericU3CxBehavior) { [&] { return mlir::qc::QCProgramBuilder::build(context.get(), hq0Yq1CxSq0); }, - "u,cx", &NativeSynthesisPassTest::onlyGenericU3CxOps, - computeTwoQubitUnitaryFromModule); + "u,cx", &NativeSynthesisPassTest::onlyGenericU3CxOps); } TEST_F(NativeSynthesisPassTest, GenericProfileMatchesAxisPairRyRzCzBehavior) { expectEquivalentAndNativeAfterSynthesis( [&] { return mlir::qc::QCProgramBuilder::build(context.get(), xHCz); }, - "ry,rz,cz", &NativeSynthesisPassTest::onlyAxisPairRyRzCzOps, - computeTwoQubitUnitaryFromModule); + "ry,rz,cz", &NativeSynthesisPassTest::onlyAxisPairRyRzCzOps); } TEST_F(NativeSynthesisPassTest, CustomProfileAcceptsMultipleEntanglersMenu) { expectEquivalentAndNativeAfterSynthesis( [&] { return mlir::qc::QCProgramBuilder::build(context.get(), hCxSq1); }, - "u,cx,cz", &NativeSynthesisPassTest::onlyGenericU3CxOrCzOps, - computeTwoQubitUnitaryFromModule); + "u,cx,cz", &NativeSynthesisPassTest::onlyGenericU3CxOrCzOps); } TEST_F(NativeSynthesisPassTest, FailsForUnsupportedNativeGateMenu) { @@ -1119,16 +800,11 @@ TEST_F(NativeSynthesisPassTest, ThreeQubitGhzEquivalentOnCoreProfiles) { for (const auto& profileCase : coreEquivalenceProfiles()) { auto expected = mlir::qc::QCProgramBuilder::build(context.get(), threeQGhz); runQcToQco(expected); - const auto expectedUnitary = computeNQubitUnitaryFromModule(expected); - ASSERT_TRUE(expectedUnitary.has_value()); auto synthesized = mlir::qc::QCProgramBuilder::build(context.get(), threeQGhz); runNativeSynthesis(synthesized, profileCase.nativeGates); EXPECT_TRUE(profileCase.isNative(synthesized)); - const auto synthesizedUnitary = computeNQubitUnitaryFromModule(synthesized); - ASSERT_TRUE(synthesizedUnitary.has_value()); - EXPECT_TRUE( - isEquivalentUpToGlobalPhase(*expectedUnitary, *synthesizedUnitary)); + expectQcoModulesEquivalent(expected, synthesized); } } diff --git a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp index cb12365bd5..a2ad8f9bf2 100644 --- a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp +++ b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp @@ -364,6 +364,34 @@ TEST(UnitaryMatrix4x4, KroneckerProduct) { 0, 0, 1, 0))); } +TEST(UnitaryDynamicMatrix, NQubitEmbedMatchesTwoQubitSpecialization) { + const Matrix2x2 x = pauliX(); + const Matrix4x4 cx = Matrix4x4::fromElements(1, 0, 0, 0, // + 0, 1, 0, 0, // + 0, 0, 0, 1, // + 0, 0, 1, 0); + EXPECT_TRUE(embedSingleQubitInNqubit(x, 2, 0).isApprox( + embedSingleQubitInTwoQubit(x, 0))); + EXPECT_TRUE(embedTwoQubitInNqubit(cx, 2, 0, 1) + .isApprox(reorderTwoQubitMatrix(cx, 0, 1))); + const DynamicMatrix cxOn01 = embedTwoQubitInNqubit(cx, 3, 0, 1); + const DynamicMatrix cxOn12 = embedTwoQubitInNqubit(cx, 3, 1, 2); + EXPECT_EQ(cxOn01.rows(), 8); + EXPECT_EQ(cxOn12.rows(), 8); + EXPECT_FALSE(cxOn01.isApprox(cxOn12)); +} + +TEST(UnitaryDynamicMatrix, MultiplyTraceAndScalar) { + const Matrix2x2 x = pauliX(); + const DynamicMatrix embedded = embedSingleQubitInNqubit(x, 2, 0); + EXPECT_FALSE(embedded.isIdentity()); + const Complex scalar = std::exp(1i * 0.3); + EXPECT_TRUE((scalar * embedded).isApprox(embedded * scalar)); + const DynamicMatrix product = embedded * embedded; + EXPECT_TRUE(product.isIdentity()); + EXPECT_NEAR(product.trace().real(), 4.0, 1e-12); +} + TEST(UnitaryMatrix2x2, ScalarLeftMultiply) { const Matrix2x2 x = pauliX(); const Complex scalar = std::exp(1i * 0.5); diff --git a/mlir/unittests/TestCaseUtils.h b/mlir/unittests/TestCaseUtils.h index f698ece122..75a9fc325d 100644 --- a/mlir/unittests/TestCaseUtils.h +++ b/mlir/unittests/TestCaseUtils.h @@ -21,10 +21,12 @@ #include #include +#include #include #include // NOLINT(misc-include-cleaner) #include #include +#include #include #include #include @@ -178,41 +180,158 @@ class DeferredPrinter { /// Logical qubit index within a two-qubit reference matrix. using QubitId = std::size_t; -/// `SWAP` gate in MQT operand order (qubit 0 = MSB). +/// `SWAP` gate. [[nodiscard]] inline const mlir::qco::Matrix4x4& swapGate() { - static const mlir::qco::Matrix4x4 matrix = - mlir::qco::Matrix4x4::fromElements(1, 0, 0, 0, // - 0, 0, 1, 0, // - 0, 1, 0, 0, // - 0, 0, 0, 1); - return matrix; + return mlir::qco::twoQubitSwapMatrix(); } /// Embed a single-qubit matrix into the two-qubit space acting on `qubitId`. [[nodiscard]] inline mlir::qco::Matrix4x4 expandToTwoQubits(const mlir::qco::Matrix2x2& singleQubitMatrix, QubitId qubitId) { - if (qubitId == 0) { - return kron(singleQubitMatrix, mlir::qco::Matrix2x2::identity()); - } - if (qubitId == 1) { - return kron(mlir::qco::Matrix2x2::identity(), singleQubitMatrix); - } - llvm::reportFatalInternalError("Invalid qubit id for single-qubit expansion"); + return mlir::qco::embedSingleQubitInTwoQubit(singleQubitMatrix, qubitId); } -/// Reorder a two-qubit matrix so it acts on qubits `{0, 1}` in MQT order. +/// Reorder a two-qubit matrix so it acts on qubits `{0, 1}`. [[nodiscard]] inline mlir::qco::Matrix4x4 fixTwoQubitMatrixQubitOrder(const mlir::qco::Matrix4x4& twoQubitMatrix, const llvm::SmallVector& qubitIds) { - if (qubitIds == llvm::SmallVector{1, 0}) { - return swapGate() * twoQubitMatrix * swapGate(); + if (qubitIds.size() != 2) { + llvm::reportFatalInternalError( + "Invalid qubit IDs for fixing two-qubit matrix"); } - if (qubitIds == llvm::SmallVector{0, 1}) { - return twoQubitMatrix; + return mlir::qco::reorderTwoQubitMatrix(twoQubitMatrix, qubitIds[0], + qubitIds[1]); +} + +constexpr double kSanityCheckPrecision = 1e-12; + +[[nodiscard]] inline bool isUnitaryMatrix(const mlir::qco::Matrix2x2& matrix, + double tolerance = 1e-12) { + return (matrix.adjoint() * matrix).isIdentity(tolerance); +} + +[[nodiscard]] inline double remEuclid(double a, double b) { + if (b == 0.0) { + llvm::reportFatalInternalError("remEuclid expects non-zero divisor"); } - llvm::reportFatalInternalError( - "Invalid qubit IDs for fixing two-qubit matrix"); + const auto r = std::fmod(a, b); + return (r < 0.0) ? r + std::abs(b) : r; +} + +[[nodiscard]] inline double traceToFidelity(const std::complex& x) { + const auto xAbs = std::abs(x); + return (4.0 + (xAbs * xAbs)) / 20.0; +} + +[[nodiscard]] inline std::complex +globalPhaseFactor(double globalPhase) { + return std::exp(std::complex{0, 1} * globalPhase); +} + +[[nodiscard]] inline mlir::qco::Matrix4x4 rxxMatrix(double theta) { + const auto cosTheta = std::cos(theta / 2.); + const mlir::qco::Complex misin{0., -std::sin(theta / 2.)}; + return mlir::qco::Matrix4x4::fromElements(cosTheta, 0, 0, misin, // + 0, cosTheta, misin, 0, // + 0, misin, cosTheta, 0, // + misin, 0, 0, cosTheta); +} + +[[nodiscard]] inline mlir::qco::Matrix4x4 ryyMatrix(double theta) { + const auto cosTheta = std::cos(theta / 2.); + const mlir::qco::Complex isin{0., std::sin(theta / 2.)}; + const mlir::qco::Complex misin{0., -std::sin(theta / 2.)}; + return mlir::qco::Matrix4x4::fromElements(cosTheta, 0, 0, isin, // + 0, cosTheta, misin, 0, // + 0, misin, cosTheta, 0, // + isin, 0, 0, cosTheta); +} + +[[nodiscard]] inline mlir::qco::Matrix4x4 rzzMatrix(double theta) { + const auto cosTheta = std::cos(theta / 2.); + const auto sinTheta = std::sin(theta / 2.); + const mlir::qco::Complex em{cosTheta, -sinTheta}; + const mlir::qco::Complex ep{cosTheta, sinTheta}; + return mlir::qco::Matrix4x4::fromElements(em, 0, 0, 0, // + 0, ep, 0, 0, // + 0, 0, ep, 0, // + 0, 0, 0, em); +} + +[[nodiscard]] inline const mlir::qco::Matrix4x4& cxGate01() { + static const mlir::qco::Matrix4x4 matrix = + mlir::qco::Matrix4x4::fromElements(1, 0, 0, 0, // + 0, 1, 0, 0, // + 0, 0, 0, 1, // + 0, 0, 1, 0); + return matrix; +} + +[[nodiscard]] inline const mlir::qco::Matrix4x4& cxGate10() { + static const mlir::qco::Matrix4x4 matrix = + mlir::qco::Matrix4x4::fromElements(1, 0, 0, 0, // + 0, 0, 0, 1, // + 0, 0, 1, 0, // + 0, 1, 0, 0); + return matrix; +} + +[[nodiscard]] inline const mlir::qco::Matrix4x4& czGate() { + static const mlir::qco::Matrix4x4 matrix = + mlir::qco::Matrix4x4::fromElements(1, 0, 0, 0, // + 0, 1, 0, 0, // + 0, 0, 1, 0, // + 0, 0, 0, -1); + return matrix; +} + +[[nodiscard]] inline std::vector> +randomUnitaryData(std::size_t dim, std::mt19937& rng) { + std::normal_distribution normalDist(0.0, 1.0); + std::vector>> columns( + dim, std::vector>(dim)); + for (auto& column : columns) { + for (auto& entry : column) { + entry = std::complex(normalDist(rng), normalDist(rng)); + } + } + for (std::size_t j = 0; j < dim; ++j) { + for (std::size_t k = 0; k < j; ++k) { + std::complex projection{0.0, 0.0}; + for (std::size_t i = 0; i < dim; ++i) { + projection += std::conj(columns[k][i]) * columns[j][i]; + } + for (std::size_t i = 0; i < dim; ++i) { + columns[j][i] -= projection * columns[k][i]; + } + } + double norm = 0.0; + for (std::size_t i = 0; i < dim; ++i) { + norm += std::norm(columns[j][i]); + } + norm = std::sqrt(norm); + for (std::size_t i = 0; i < dim; ++i) { + columns[j][i] /= norm; + } + } + std::vector> data(dim * dim); + for (std::size_t row = 0; row < dim; ++row) { + for (std::size_t col = 0; col < dim; ++col) { + data[(row * dim) + col] = columns[col][row]; + } + } + return data; +} + +[[nodiscard]] inline mlir::qco::Matrix4x4 randomUnitary4x4(std::mt19937& rng) { + const auto data = randomUnitaryData(4, rng); + const mlir::qco::Matrix4x4 unitary = mlir::qco::Matrix4x4::fromElements( + data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], + data[8], data[9], data[10], data[11], data[12], data[13], data[14], + data[15]); + assert((unitary.adjoint() * unitary).isIdentity(1e-12)); + return unitary; } } // namespace mqt::test diff --git a/mlir/unittests/programs/qc_programs.cpp b/mlir/unittests/programs/qc_programs.cpp index e3d0e81b65..8410afd7a8 100644 --- a/mlir/unittests/programs/qc_programs.cpp +++ b/mlir/unittests/programs/qc_programs.cpp @@ -11,7 +11,6 @@ #include "qc_programs.h" #include "mlir/Dialect/QC/Builder/QCProgramBuilder.h" -#include "mlir/IR/Value.h" #include @@ -1617,4 +1616,5 @@ void nestedForLoopCtrlOpWithExtractedQubit(QCProgramBuilder& b) { b.cx(reg[0], q0); }); } + } // namespace mlir::qc From 6da8de83059cd5fa9003afb99dadbcbb35f319da Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Fri, 19 Jun 2026 14:15:30 +0200 Subject: [PATCH 050/122] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20linter=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/Decomposition/Weyl.h | 2 +- .../QCO/Transforms/Decomposition/Weyl.cpp | 76 +++++++++---------- .../FuseTwoQubitUnitaryRuns.cpp | 45 +++++------ mlir/lib/Dialect/QCO/Utils/Matrix.cpp | 35 ++++----- .../test_euler_decomposition.cpp | 1 + .../Decomposition/test_weyl_decomposition.cpp | 32 +++----- .../NativeSynthesis/test_native_synthesis.cpp | 36 +++++---- 7 files changed, 101 insertions(+), 126 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h index bb4776b0b6..1377e7c172 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h @@ -498,7 +498,7 @@ twoQubitEntanglerCount(const Matrix4x4& target, const NativeProfileSpec& spec); /// Common constant `1/sqrt(2)` used by gate-matrix factories. inline constexpr double FRAC1_SQRT2 = - 0.707106781186547524400844362104849039284835937688474036588L; + 0.707106781186547524400844362104849039284835937688474036588; /// Axis rotations `exp(-i theta/2 * sigma_{x,y,z})`. [[nodiscard]] Matrix2x2 rxMatrix(double theta); diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp index 29e76dcceb..25c08128c5 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp @@ -32,29 +32,26 @@ #include #include #include +#include +#include #include #include #include #include #include -namespace { - -constexpr double SANITY_CHECK_PRECISION = 1e-12; -/// Common constant `1/sqrt(2)` used by gate-matrix factories. -inline constexpr double FRAC1_SQRT2 = - 0.707106781186547524400844362104849039284835937688474036588L; - using mlir::qco::Complex; using mlir::qco::Matrix2x2; using mlir::qco::Matrix4x4; -[[nodiscard]] bool isUnitaryMatrix(const Matrix2x2& matrix, - double tolerance = 1e-12) { +static constexpr double SANITY_CHECK_PRECISION = 1e-12; + +[[nodiscard]] static bool isUnitaryMatrix(const Matrix2x2& matrix, + double tolerance = 1e-12) { return (matrix.adjoint() * matrix).isIdentity(tolerance); } -[[nodiscard]] double remEuclid(double a, double b) { +[[nodiscard]] static double remEuclid(double a, double b) { if (b == 0.0) { llvm::reportFatalInternalError("remEuclid expects non-zero divisor"); } @@ -62,16 +59,17 @@ using mlir::qco::Matrix4x4; return (r < 0.0) ? r + std::abs(b) : r; } -[[nodiscard]] double traceToFidelity(const std::complex& x) { +[[nodiscard]] static double traceToFidelity(const std::complex& x) { const auto xAbs = std::abs(x); return (4.0 + (xAbs * xAbs)) / 20.0; } -[[nodiscard]] std::complex globalPhaseFactor(double globalPhase) { +[[nodiscard]] static std::complex +globalPhaseFactor(double globalPhase) { return std::exp(std::complex{0, 1} * globalPhase); } -[[nodiscard]] Matrix4x4 rxxMatrix(double theta) { +[[nodiscard]] static Matrix4x4 rxxMatrix(double theta) { const auto cosTheta = std::cos(theta / 2.); const Complex misin{0., -std::sin(theta / 2.)}; return Matrix4x4::fromElements(cosTheta, 0, 0, misin, // @@ -80,7 +78,7 @@ using mlir::qco::Matrix4x4; misin, 0, 0, cosTheta); } -[[nodiscard]] Matrix4x4 ryyMatrix(double theta) { +[[nodiscard]] static Matrix4x4 ryyMatrix(double theta) { const auto cosTheta = std::cos(theta / 2.); const Complex isin{0., std::sin(theta / 2.)}; const Complex misin{0., -std::sin(theta / 2.)}; @@ -90,7 +88,7 @@ using mlir::qco::Matrix4x4; isin, 0, 0, cosTheta); } -[[nodiscard]] Matrix4x4 rzzMatrix(double theta) { +[[nodiscard]] static Matrix4x4 rzzMatrix(double theta) { const auto cosTheta = std::cos(theta / 2.); const auto sinTheta = std::sin(theta / 2.); const Complex em{cosTheta, -sinTheta}; @@ -101,40 +99,38 @@ using mlir::qco::Matrix4x4; 0, 0, 0, em); } -[[nodiscard]] const Matrix4x4& swapGate() { - static const Matrix4x4 matrix = Matrix4x4::fromElements(1, 0, 0, 0, // +[[nodiscard]] static const Matrix4x4& swapGate() { + static const Matrix4x4 MATRIX = Matrix4x4::fromElements(1, 0, 0, 0, // 0, 0, 1, 0, // 0, 1, 0, 0, // 0, 0, 0, 1); - return matrix; + return MATRIX; } -[[nodiscard]] const Matrix4x4& cxGate01() { - static const Matrix4x4 matrix = Matrix4x4::fromElements(1, 0, 0, 0, // +[[nodiscard]] static const Matrix4x4& cxGate01() { + static const Matrix4x4 MATRIX = Matrix4x4::fromElements(1, 0, 0, 0, // 0, 1, 0, 0, // 0, 0, 0, 1, // 0, 0, 1, 0); - return matrix; + return MATRIX; } -[[nodiscard]] const Matrix4x4& cxGate10() { - static const Matrix4x4 matrix = Matrix4x4::fromElements(1, 0, 0, 0, // +[[nodiscard]] static const Matrix4x4& cxGate10() { + static const Matrix4x4 MATRIX = Matrix4x4::fromElements(1, 0, 0, 0, // 0, 0, 0, 1, // 0, 0, 1, 0, // 0, 1, 0, 0); - return matrix; + return MATRIX; } -[[nodiscard]] const Matrix4x4& czGate() { - static const Matrix4x4 matrix = Matrix4x4::fromElements(1, 0, 0, 0, // +[[nodiscard]] static const Matrix4x4& czGate() { + static const Matrix4x4 MATRIX = Matrix4x4::fromElements(1, 0, 0, 0, // 0, 1, 0, 0, // 0, 0, 1, 0, // 0, 0, 0, -1); - return matrix; + return MATRIX; } -} // namespace - namespace mlir::qco::decomposition { using namespace std::complex_literals; @@ -194,8 +190,8 @@ TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, cstemp[i] = std::min(tmp, (pi / 2.0) - tmp); } std::array order{0, 1, 2}; - std::stable_sort(order.begin(), order.end(), - [&](auto a, auto b) { return cstemp[a] < cstemp[b]; }); + std::ranges::stable_sort( + order, [&](auto a, auto b) { return cstemp[a] < cstemp[b]; }); order = {order[1], order[2], order[0]}; cs = {cs[order[0]], cs[order[1]], cs[order[2]]}; { @@ -1124,9 +1120,7 @@ bool TwoQubitBasisDecomposer::relativeEq(double lhs, double rhs, double epsilon, // Native-spec parsing and two-qubit synthesis //===----------------------------------------------------------------------===// -namespace { - -constexpr double PI = std::numbers::pi; +static constexpr double PI = std::numbers::pi; [[nodiscard]] static std::optional parseGateToken(llvm::StringRef name) { @@ -1280,8 +1274,6 @@ static void emitGPhaseIfNonTrivial(OpBuilder& builder, Location loc, basis); } -} // namespace - EulerBasis emitterEulerBasis(const SingleQubitEmitterSpec& emitter) { switch (emitter.mode) { case SingleQubitMode::ZSXX: @@ -1447,20 +1439,20 @@ Matrix2x2 rzMatrix(double theta) { } const Matrix2x2& ipz() { - static const Matrix2x2 matrix = + static const Matrix2x2 MATRIX = Matrix2x2::fromElements(Complex{0, 1}, 0, 0, Complex{0, -1}); - return matrix; + return MATRIX; } const Matrix2x2& ipy() { - static const Matrix2x2 matrix = Matrix2x2::fromElements(0, 1, -1, 0); - return matrix; + static const Matrix2x2 MATRIX = Matrix2x2::fromElements(0, 1, -1, 0); + return MATRIX; } const Matrix2x2& ipx() { - static const Matrix2x2 matrix = + static const Matrix2x2 MATRIX = Matrix2x2::fromElements(0, Complex{0, 1}, Complex{0, 1}, 0); - return matrix; + return MATRIX; } } // namespace mlir::qco::decomposition diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp index 995a261997..25ba7cb8d0 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp @@ -8,7 +8,6 @@ * Licensed under the MIT License */ -#include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" @@ -20,14 +19,12 @@ #include #include #include -#include #include #include #include #include #include #include -#include #include #include #include @@ -43,10 +40,9 @@ namespace mlir::qco { #define GEN_PASS_DEF_FUSETWOQUBITUNITARYRUNS #include "mlir/Dialect/QCO/Transforms/Passes.h.inc" -namespace { - -void collectUnitaryOpsInPreOrder(Operation* root, - llvm::SmallVectorImpl& ops) { +static void +collectUnitaryOpsInPreOrder(Operation* root, + llvm::SmallVectorImpl& ops) { root->walk([&](Operation* op) { if (op->getParentOfType()) { return; @@ -60,9 +56,9 @@ void collectUnitaryOpsInPreOrder(Operation* root, }); } -Value emitSingleQubitMatrix(IRRewriter& rewriter, Location loc, Value inQubit, - const Matrix2x2& matrix, - const decomposition::EulerBasis basis) { +static Value emitSingleQubitMatrix(IRRewriter& rewriter, Location loc, + Value inQubit, const Matrix2x2& matrix, + const decomposition::EulerBasis basis) { // Force emission (`hasNonBasisGate = true`, `runSize = 0`) so the matrix is // always lowered into native gates of `basis`, including any residual // `qco.gphase`. With these arguments `synthesizeUnitary1QEuler` never @@ -129,8 +125,8 @@ singleQubitNativeGateKind(UnitaryOpInterface op) { } // NOLINTNEXTLINE(misc-use-internal-linkage): test-visible (see comment above). -bool allowsSingleQubitOp(UnitaryOpInterface op, - const decomposition::NativeProfileSpec& spec) { +static bool allowsSingleQubitOp(UnitaryOpInterface op, + const decomposition::NativeProfileSpec& spec) { if (llvm::isa(op.getOperation())) { return true; } @@ -138,7 +134,7 @@ bool allowsSingleQubitOp(UnitaryOpInterface op, return gate && spec.allowedGates.contains(*gate); } -bool getBlockTwoQubitMatrix(Operation* op, Matrix4x4& matrix) { +static bool getBlockTwoQubitMatrix(Operation* op, Matrix4x4& matrix) { if (llvm::isa(op)) { return false; } @@ -177,8 +173,8 @@ bool getBlockTwoQubitMatrix(Operation* op, Matrix4x4& matrix) { /// native menu: a single-control `CX`/`CZ` consistent with the active /// entangler, or `Rzz` when `spec.allowRzz` is set. Multi-control and other /// two-qubit ops are considered non-native. -bool isNativeTwoQubitOp(Operation* op, - const decomposition::NativeProfileSpec& spec) { +static bool isNativeTwoQubitOp(Operation* op, + const decomposition::NativeProfileSpec& spec) { if (auto ctrl = llvm::dyn_cast(op)) { if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { return false; @@ -201,15 +197,15 @@ bool isNativeTwoQubitOp(Operation* op, /// replace a window that contains any non-native op (we have to lower them /// anyway); otherwise only replace when the deterministic synthesizer uses /// strictly fewer entanglers than the window already contains. -bool shouldApplyBlockReplacement(const TwoQubitBlock& block, - std::uint8_t numBasisUses) { +static bool shouldApplyBlockReplacement(const TwoQubitBlock& block, + std::uint8_t numBasisUses) { if (block.anyNonNative) { return true; } return numBasisUses < block.numTwoQ; } -LogicalResult +static LogicalResult materializeSingleTwoQubitBlock(IRRewriter& rewriter, const TwoQubitBlock& block, const decomposition::NativeProfileSpec& spec) { Operation* firstOp = block.ops.front(); @@ -425,7 +421,7 @@ LogicalResult TwoQubitWindowConsolidator::materialize( return success(); } -LogicalResult +static LogicalResult fuseTwoQubitUnitaryRuns(IRRewriter& rewriter, Operation* root, const decomposition::NativeProfileSpec& spec) { llvm::SmallVector ops; @@ -445,9 +441,9 @@ struct OneQubitRun { /// If profitable, replace the run with one synthesized single-qubit op in /// `basis` (mirrors `FuseSingleQubitUnitaryRuns`). Fuses when any op is /// off-menu or when Euler resynthesis strictly shortens the run. -bool maybeFuseRun(IRRewriter& rewriter, OneQubitRun& run, - const decomposition::EulerBasis basis, - const decomposition::NativeProfileSpec& spec) { +static bool maybeFuseRun(IRRewriter& rewriter, OneQubitRun& run, + const decomposition::EulerBasis basis, + const decomposition::NativeProfileSpec& spec) { Matrix2x2 fused = Matrix2x2::identity(); for (UnitaryOpInterface u : run.ops) { Matrix2x2 m; @@ -481,7 +477,7 @@ bool maybeFuseRun(IRRewriter& rewriter, OneQubitRun& run, /// True when `op` lives in a `ctrl`/`inv` region body (not the shell op). /// Skips nested unitaries so they are handled via the enclosing modifier. -bool isHiddenInsideCtrlOrInvBody(Operation* op) { +static bool isHiddenInsideCtrlOrInvBody(Operation* op) { if (op->getParentOfType()) { return true; } @@ -492,7 +488,7 @@ bool isHiddenInsideCtrlOrInvBody(Operation* op) { } /// Single-qubit op eligible for fusion (constant `2×2`, not under `ctrl`). -UnitaryOpInterface fusibleSingleQubitOp(Operation* op) { +static UnitaryOpInterface fusibleSingleQubitOp(Operation* op) { auto unitary = llvm::dyn_cast(op); if (!unitary || !unitary.isSingleQubit()) { return {}; @@ -902,5 +898,4 @@ struct FuseTwoQubitUnitaryRunsPass } }; -} // namespace } // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Utils/Matrix.cpp b/mlir/lib/Dialect/QCO/Utils/Matrix.cpp index d0b9cb38d9..9af25c22eb 100644 --- a/mlir/lib/Dialect/QCO/Utils/Matrix.cpp +++ b/mlir/lib/Dialect/QCO/Utils/Matrix.cpp @@ -11,6 +11,7 @@ #include "mlir/Dialect/QCO/Utils/Matrix.h" #include +#include #include #include @@ -574,19 +575,17 @@ bool DynamicMatrix::isIdentity(const double tol) const { return true; } -namespace { - -[[nodiscard]] std::size_t qubitBitAt(const std::size_t index, - const std::size_t numQubits, - const std::size_t qubitIndex) { - return (index >> (numQubits - 1 - qubitIndex)) & 1U; +[[nodiscard]] static std::size_t qubitBitAt(const std::size_t stateIndex, + const std::size_t numQubits, + const std::size_t qubitIndex) { + return (stateIndex >> (numQubits - 1 - qubitIndex)) & 1U; } -[[nodiscard]] bool otherQubitBitsMatch(const std::size_t row, - const std::size_t col, - const std::size_t numQubits, - const std::size_t skipA, - const std::size_t skipB) { +[[nodiscard]] static bool otherQubitBitsMatch(const std::size_t row, + const std::size_t col, + const std::size_t numQubits, + const std::size_t skipA, + const std::size_t skipB) { for (std::size_t q = 0; q < numQubits; ++q) { if (q == skipA || q == skipB) { continue; @@ -598,10 +597,10 @@ namespace { return true; } -[[nodiscard]] bool otherSingleQubitBitsMatch(const std::size_t row, - const std::size_t col, - const std::size_t numQubits, - const std::size_t skip) { +[[nodiscard]] static bool otherSingleQubitBitsMatch(const std::size_t row, + const std::size_t col, + const std::size_t numQubits, + const std::size_t skip) { for (std::size_t q = 0; q < numQubits; ++q) { if (q == skip) { continue; @@ -613,8 +612,6 @@ namespace { return true; } -} // namespace - Matrix2x2 operator*(const Complex& scalar, const Matrix2x2& matrix) { return matrix * scalar; } @@ -643,11 +640,11 @@ Matrix4x4 kron(const Matrix2x2& lhs, const Matrix2x2& rhs) { } const Matrix4x4& twoQubitSwapMatrix() { - static const Matrix4x4 matrix = Matrix4x4::fromElements(1, 0, 0, 0, // + static const Matrix4x4 MATRIX = Matrix4x4::fromElements(1, 0, 0, 0, // 0, 0, 1, 0, // 0, 1, 0, 0, // 0, 0, 0, 1); - return matrix; + return MATRIX; } Matrix4x4 embedSingleQubitInTwoQubit(const Matrix2x2& matrix, diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp index d9fe4e0f44..67755f2d1f 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -37,6 +37,7 @@ #include #include +#include #include #include #include diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index d9249c0e41..24b6f1eef3 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -14,15 +14,15 @@ #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" #include #include #include +#include #include -#include #include #include #include @@ -44,7 +44,6 @@ #include #include #include -#include using namespace mlir; using namespace mlir::qco; @@ -345,8 +344,6 @@ INSTANTIATE_TEST_SUITE_P( return kron(hGate(), ipz()) * cxGate01() * kron(ipx(), ipy()); }))); -namespace { - static void fusionCxCx(mlir::qc::QCProgramBuilder& b) { const auto q0 = b.allocQubit(); const auto q1 = b.allocQubit(); @@ -398,10 +395,6 @@ static void fusionHRzzSRzz(mlir::qc::QCProgramBuilder& b) { b.rzz(0.17, q0, q1); } -} // namespace - -namespace { - using mqt::test::cxGate01; using mqt::test::czGate; using mqt::test::expandToTwoQubits; @@ -409,7 +402,7 @@ using mqt::test::fixTwoQubitMatrixQubitOrder; using mqt::test::isEquivalentUpToGlobalPhase; using mqt::test::QubitId; -[[nodiscard]] std::optional +[[nodiscard]] static std::optional getUnitaryQubitOperand(qco::UnitaryOpInterface op, std::size_t index) { if (index >= op.getNumQubits()) { return std::nullopt; @@ -421,7 +414,7 @@ getUnitaryQubitOperand(qco::UnitaryOpInterface op, std::size_t index) { return v; } -[[nodiscard]] std::optional +[[nodiscard]] static std::optional getUnitaryQubitResult(qco::UnitaryOpInterface op, std::size_t index) { if (index >= op.getNumQubits()) { return std::nullopt; @@ -433,8 +426,8 @@ getUnitaryQubitResult(qco::UnitaryOpInterface op, std::size_t index) { return v; } -[[nodiscard]] bool extractSingleQubitMatrix(qco::UnitaryOpInterface op, - Matrix2x2& out) { +[[nodiscard]] static bool extractSingleQubitMatrix(qco::UnitaryOpInterface op, + Matrix2x2& out) { if (op.getUnitaryMatrix2x2(out)) { return true; } @@ -448,8 +441,8 @@ getUnitaryQubitResult(qco::UnitaryOpInterface op, std::size_t index) { return true; } -[[nodiscard]] bool extractTwoQubitMatrix(qco::UnitaryOpInterface op, - Matrix4x4& out) { +[[nodiscard]] static bool extractTwoQubitMatrix(qco::UnitaryOpInterface op, + Matrix4x4& out) { if (auto ctrl = llvm::dyn_cast(op.getOperation())) { if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { return false; @@ -468,7 +461,7 @@ getUnitaryQubitResult(qco::UnitaryOpInterface op, std::size_t index) { return op.getUnitaryMatrix4x4(out); } -[[nodiscard]] std::optional +[[nodiscard]] static std::optional computeTwoQubitUnitaryFromModule(const OwningOpRef& moduleOp) { ModuleOp module = moduleOp.get(); if (!module) { @@ -570,8 +563,9 @@ computeTwoQubitUnitaryFromModule(const OwningOpRef& moduleOp) { return unitary; } -void expectTwoQubitQcoModulesEquivalent(const OwningOpRef& lhs, - const OwningOpRef& rhs) { +static void +expectTwoQubitQcoModulesEquivalent(const OwningOpRef& lhs, + const OwningOpRef& rhs) { const auto lhsUnitary = computeTwoQubitUnitaryFromModule(lhs); ASSERT_TRUE(lhsUnitary.has_value()); const auto rhsUnitary = computeTwoQubitUnitaryFromModule(rhs); @@ -579,8 +573,6 @@ void expectTwoQubitQcoModulesEquivalent(const OwningOpRef& lhs, EXPECT_TRUE(isEquivalentUpToGlobalPhase(*lhsUnitary, *rhsUnitary)); } -} // namespace - struct TwoQFuseFixture { std::unique_ptr context; diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis.cpp index 91fbc9dd20..ffd45a37ea 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis.cpp @@ -16,12 +16,12 @@ #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" #include "qc_programs.h" #include #include #include -#include #include #include #include @@ -30,7 +30,6 @@ #include #include #include -#include #include #include #include @@ -44,9 +43,11 @@ #include #include #include +#include #include #include #include +#include using namespace mlir; using namespace mlir::qco; @@ -58,9 +59,7 @@ using mqt::test::cxGate01; using mqt::test::czGate; using mqt::test::isEquivalentUpToGlobalPhase; -namespace { - -[[nodiscard]] std::optional +[[nodiscard]] static std::optional getUnitaryQubitOperand(mlir::qco::UnitaryOpInterface op, std::size_t index) { if (index >= op.getNumQubits()) { return std::nullopt; @@ -72,7 +71,7 @@ getUnitaryQubitOperand(mlir::qco::UnitaryOpInterface op, std::size_t index) { return v; } -[[nodiscard]] std::optional +[[nodiscard]] static std::optional getUnitaryQubitResult(mlir::qco::UnitaryOpInterface op, std::size_t index) { if (index >= op.getNumQubits()) { return std::nullopt; @@ -84,8 +83,9 @@ getUnitaryQubitResult(mlir::qco::UnitaryOpInterface op, std::size_t index) { return v; } -[[nodiscard]] bool extractSingleQubitMatrix(mlir::qco::UnitaryOpInterface op, - mlir::qco::Matrix2x2& out) { +[[nodiscard]] static bool +extractSingleQubitMatrix(mlir::qco::UnitaryOpInterface op, + mlir::qco::Matrix2x2& out) { if (op.getUnitaryMatrix2x2(out)) { return true; } @@ -99,8 +99,9 @@ getUnitaryQubitResult(mlir::qco::UnitaryOpInterface op, std::size_t index) { return true; } -[[nodiscard]] bool extractTwoQubitMatrix(mlir::qco::UnitaryOpInterface op, - mlir::qco::Matrix4x4& out) { +[[nodiscard]] static bool +extractTwoQubitMatrix(mlir::qco::UnitaryOpInterface op, + mlir::qco::Matrix4x4& out) { if (auto ctrl = llvm::dyn_cast(op.getOperation())) { if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { return false; @@ -119,7 +120,7 @@ getUnitaryQubitResult(mlir::qco::UnitaryOpInterface op, std::size_t index) { return op.getUnitaryMatrix4x4(out); } -[[nodiscard]] std::optional +[[nodiscard]] static std::optional computeUnitaryFromModule(const mlir::OwningOpRef& moduleOp) { mlir::ModuleOp module = moduleOp.get(); if (!module) { @@ -231,8 +232,9 @@ computeUnitaryFromModule(const mlir::OwningOpRef& moduleOp) { return unitary; } -void expectQcoModulesEquivalent(const mlir::OwningOpRef& lhs, - const mlir::OwningOpRef& rhs) { +static void +expectQcoModulesEquivalent(const mlir::OwningOpRef& lhs, + const mlir::OwningOpRef& rhs) { const auto lhsUnitary = computeUnitaryFromModule(lhs); ASSERT_TRUE(lhsUnitary.has_value()); const auto rhsUnitary = computeUnitaryFromModule(rhs); @@ -240,15 +242,15 @@ void expectQcoModulesEquivalent(const mlir::OwningOpRef& lhs, EXPECT_TRUE(isEquivalentUpToGlobalPhase(*lhsUnitary, *rhsUnitary)); } -} // namespace - /// One row of the standard multi-profile equivalence sweeps in tests. +// NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_F` at global scope struct NativeSynthesisProfileSweepCase { const char* nativeGates; bool (*isNative)(mlir::OwningOpRef&); }; /// Shared gtest fixture for native-gate synthesis pass tests. +// NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_F` at global scope class NativeSynthesisPassTest : public testing::Test { protected: void SetUp() override { @@ -435,8 +437,6 @@ class NativeSynthesisPassTest : public testing::Test { using namespace mlir::qco::native_synth_test; -namespace { - struct NativeSynthMenuRow { const char* name; const char* nativeGates; @@ -571,8 +571,6 @@ static void determinismSwap(mlir::qc::QCProgramBuilder& b) { b.dealloc(q1); } -} // namespace - // --- NativeSpec / NativePolicy --- TEST(NativeSpecTest, ResolveIbmBasicCx) { From 6a3b1388385386a863c67eaf14f7756792ab5c44 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Sun, 21 Jun 2026 11:57:14 +0200 Subject: [PATCH 051/122] =?UTF-8?q?=E2=9C=A8=20Refactor=20unit=20tests=20f?= =?UTF-8?q?or=20two-qubit=20unitary=20fusion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Transforms/Decomposition/CMakeLists.txt | 5 +- .../Decomposition/test_weyl_decomposition.cpp | 498 ++++----- .../Transforms/NativeSynthesis/CMakeLists.txt | 7 +- .../test_fuse_two_qubit_unitary_runs.cpp | 973 ++++++++++++++++++ .../NativeSynthesis/test_native_synthesis.cpp | 808 --------------- 5 files changed, 1188 insertions(+), 1103 deletions(-) create mode 100644 mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp delete mode 100644 mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis.cpp diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt index 21985a2967..c6160821cd 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt @@ -12,9 +12,7 @@ add_executable(${target_name} test_euler_decomposition.cpp test_weyl_decompositi target_link_libraries( ${target_name} PRIVATE GTest::gtest_main - MLIRQCPrograms MLIRQCOProgramBuilder - MLIRQCToQCO MLIRQCOTransforms MLIRQCOUtils MLIRPass @@ -22,8 +20,7 @@ target_link_libraries( MLIRArithDialect MLIRIR MLIRSupport - MLIRQTensorDialect - LLVMSupport) + MLIRQTensorDialect) mqt_mlir_configure_unittest_target(${target_name}) diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index 24b6f1eef3..a3022cbee7 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -9,30 +9,27 @@ */ #include "TestCaseUtils.h" -#include "mlir/Conversion/QCToQCO/QCToQCO.h" -#include "mlir/Dialect/QC/Builder/QCProgramBuilder.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" -#include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" #include #include +#include #include -#include #include +#include #include #include -#include +#include #include #include #include #include #include #include -#include #include #include @@ -344,115 +341,43 @@ INSTANTIATE_TEST_SUITE_P( return kron(hGate(), ipz()) * cxGate01() * kron(ipx(), ipy()); }))); -static void fusionCxCx(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.cx(q0, q1); - b.cx(q0, q1); -} - -static void fusionHCxInterleavedTCx(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.h(q0); - b.cx(q0, q1); - b.t(q1); - b.s(q0); - b.cx(q0, q1); -} - -static void fusionThreeLineCx(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - const auto q2 = b.allocQubit(); - b.cx(q0, q1); - b.cx(q1, q2); - b.cx(q0, q1); -} - -static void fusionCxBarrierCx(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.cx(q0, q1); - b.barrier({q0, q1}); - b.cx(q0, q1); -} +//===----------------------------------------------------------------------===// +// Two-qubit Weyl IR synthesis +//===----------------------------------------------------------------------===// -static void fusionSwapCxPattern(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.cx(q0, q1); - b.cx(q1, q0); - b.cx(q0, q1); -} +namespace { -static void fusionHRzzSRzz(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.h(q0); - b.rzz(-0.29, q0, q1); - b.s(q1); - b.rzz(0.17, q0, q1); -} +struct WeylSynthesisFixture { + std::unique_ptr context; -using mqt::test::cxGate01; -using mqt::test::czGate; -using mqt::test::expandToTwoQubits; -using mqt::test::fixTwoQubitMatrixQubitOrder; -using mqt::test::isEquivalentUpToGlobalPhase; -using mqt::test::QubitId; - -[[nodiscard]] static std::optional -getUnitaryQubitOperand(qco::UnitaryOpInterface op, std::size_t index) { - if (index >= op.getNumQubits()) { - return std::nullopt; - } - Value v = op->getOperand(index); - if (!llvm::isa(v.getType())) { - return std::nullopt; + void setUp() { + DialectRegistry registry; + registry.insert(); + context = std::make_unique(); + context->appendDialectRegistry(registry); + context->loadAllAvailableDialects(); } - return v; -} -[[nodiscard]] static std::optional -getUnitaryQubitResult(qco::UnitaryOpInterface op, std::size_t index) { - if (index >= op.getNumQubits()) { - return std::nullopt; - } - Value v = op->getResult(index); - if (!llvm::isa(v.getType())) { - return std::nullopt; - } - return v; -} + [[nodiscard]] MLIRContext* ctx() const { return context.get(); } +}; -[[nodiscard]] static bool extractSingleQubitMatrix(qco::UnitaryOpInterface op, - Matrix2x2& out) { - if (op.getUnitaryMatrix2x2(out)) { - return true; - } - DynamicMatrix dynamic; - if (!op.getUnitaryMatrixDynamic(dynamic) || dynamic.rows() != 2 || - dynamic.cols() != 2) { - return false; - } - out = Matrix2x2::fromElements(dynamic(0, 0), dynamic(0, 1), dynamic(1, 0), - dynamic(1, 1)); - return true; -} +struct SynthesizedTwoQubitCircuit { + OwningOpRef mlirModule; + func::FuncOp func; +}; -[[nodiscard]] static bool extractTwoQubitMatrix(qco::UnitaryOpInterface op, - Matrix4x4& out) { - if (auto ctrl = llvm::dyn_cast(op.getOperation())) { +[[nodiscard]] bool extractTwoQubitMatrix(UnitaryOpInterface op, + Matrix4x4& out) { + if (auto ctrl = llvm::dyn_cast(op.getOperation())) { if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { return false; } - auto* body = ctrl.getBodyUnitary(0).getOperation(); - if (llvm::isa(body)) { + Operation* body = ctrl.getBodyUnitary(0).getOperation(); + if (llvm::isa(body)) { out = cxGate01(); return true; } - if (llvm::isa(body)) { + if (llvm::isa(body)) { out = czGate(); return true; } @@ -461,233 +386,228 @@ getUnitaryQubitResult(qco::UnitaryOpInterface op, std::size_t index) { return op.getUnitaryMatrix4x4(out); } -[[nodiscard]] static std::optional -computeTwoQubitUnitaryFromModule(const OwningOpRef& moduleOp) { - ModuleOp module = moduleOp.get(); - if (!module) { - return std::nullopt; - } - Matrix4x4 unitary = Matrix4x4::identity(); - llvm::DenseMap qubitIds; - std::size_t nextQubitId = 0; - - for (auto func : module.getOps()) { - for (auto& block : func.getBlocks()) { - for (auto& rawOp : block.getOperations()) { - if (auto alloc = llvm::dyn_cast(&rawOp)) { - if (nextQubitId >= 2) { - return std::nullopt; - } - qubitIds.try_emplace(alloc.getResult(), nextQubitId++); - } - } - } - } +[[nodiscard]] std::optional +compute2QUnitaryFromFunc(func::FuncOp funcOp) { + Matrix4x4 acc = Matrix4x4::identity(); + std::complex global{1.0, 0.0}; + llvm::DenseMap wireIds; + wireIds[funcOp.getArgument(0)] = 0; + wireIds[funcOp.getArgument(1)] = 1; - auto getQubitId = [&](Value qubit) -> std::optional { - auto it = qubitIds.find(qubit); - if (it == qubitIds.end()) { + auto wireId = [&](Value qubit) -> std::optional { + const auto it = wireIds.find(qubit); + if (it == wireIds.end()) { return std::nullopt; } return it->second; }; - for (auto func : module.getOps()) { - for (auto& block : func.getBlocks()) { - for (auto& rawOp : block.getOperations()) { - auto op = llvm::dyn_cast(&rawOp); - if (!op) { - continue; - } - if (llvm::isa(op.getOperation())) { - continue; - } + for (Operation& rawOp : funcOp.getBody().front()) { + if (llvm::isa(&rawOp)) { + continue; + } + if (auto gphase = llvm::dyn_cast(&rawOp)) { + if (const auto matrix = gphase.getUnitaryMatrix()) { + global *= (*matrix)(0, 0); + } + continue; + } + auto op = llvm::dyn_cast(&rawOp); + if (!op) { + continue; + } - if (op.isSingleQubit()) { - const auto qIn = getUnitaryQubitOperand(op, 0); - if (!qIn) { - return std::nullopt; - } - auto qid = getQubitId(*qIn); - if (!qid) { - return std::nullopt; - } - Matrix2x2 oneQ; - if (!extractSingleQubitMatrix(op, oneQ)) { - return std::nullopt; - } - unitary = - expandToTwoQubits(oneQ, static_cast(*qid)) * unitary; - const auto qOut = getUnitaryQubitResult(op, 0); - if (!qOut) { - return std::nullopt; - } - qubitIds[*qOut] = *qid; - continue; + if (op.isSingleQubit()) { + const auto qIn = op->getOperand(0); + const auto qid = wireId(qIn); + if (!qid) { + return std::nullopt; + } + Matrix2x2 oneQ; + if (!op.getUnitaryMatrix2x2(oneQ)) { + DynamicMatrix dynamic; + if (!op.getUnitaryMatrixDynamic(dynamic) || dynamic.rows() != 2 || + dynamic.cols() != 2) { + return std::nullopt; } + oneQ = Matrix2x2::fromElements(dynamic(0, 0), dynamic(0, 1), + dynamic(1, 0), dynamic(1, 1)); + } + acc = expandToTwoQubits(oneQ, *qid) * acc; + wireIds[op->getResult(0)] = *qid; + continue; + } - if (op.isTwoQubit()) { - const auto q0In = getUnitaryQubitOperand(op, 0); - const auto q1In = getUnitaryQubitOperand(op, 1); - if (!q0In || !q1In) { - return std::nullopt; - } - auto q0id = getQubitId(*q0In); - auto q1id = getQubitId(*q1In); - if (!q0id || !q1id) { - return std::nullopt; - } - Matrix4x4 twoQ; - if (!extractTwoQubitMatrix(op, twoQ)) { - return std::nullopt; - } - const llvm::SmallVector ids{static_cast(*q0id), - static_cast(*q1id)}; - unitary = fixTwoQubitMatrixQubitOrder(twoQ, ids) * unitary; - const auto q0Out = getUnitaryQubitResult(op, 0); - const auto q1Out = getUnitaryQubitResult(op, 1); - if (!q0Out || !q1Out) { - return std::nullopt; - } - qubitIds[*q0Out] = *q0id; - qubitIds[*q1Out] = *q1id; - continue; - } + if (op.isTwoQubit()) { + const auto q0In = op->getOperand(0); + const auto q1In = op->getOperand(1); + const auto q0id = wireId(q0In); + const auto q1id = wireId(q1In); + if (!q0id || !q1id) { + return std::nullopt; + } + Matrix4x4 twoQ; + if (!extractTwoQubitMatrix(op, twoQ)) { + return std::nullopt; } + const llvm::SmallVector ids{*q0id, *q1id}; + acc = fixTwoQubitMatrixQubitOrder(twoQ, ids) * acc; + wireIds[op->getResult(0)] = *q0id; + wireIds[op->getResult(1)] = *q1id; } } - if (nextQubitId != 2) { - return std::nullopt; + return acc * global; +} + +[[nodiscard]] SynthesizedTwoQubitCircuit +synthesize2QMatrix(MLIRContext* ctx, const Matrix4x4& target, + const NativeProfileSpec& spec) { + OwningOpRef mlirModule = ModuleOp::create(UnknownLoc::get(ctx)); + OpBuilder builder(ctx); + builder.setInsertionPointToStart(mlirModule->getBody()); + + const auto qubitTy = QubitType::get(ctx); + const auto funcTy = + builder.getFunctionType({qubitTy, qubitTy}, {qubitTy, qubitTy}); + const Location loc = mlirModule->getLoc(); + auto func = func::FuncOp::create(builder, loc, "main", funcTy); + auto* entry = func.addEntryBlock(); + + builder.setInsertionPointToStart(entry); + Value q0 = entry->getArgument(0); + Value q1 = entry->getArgument(1); + Value out0; + Value out1; + if (failed(synthesizeUnitary2QWeyl(builder, loc, q0, q1, target, spec, out0, + out1))) { + llvm::report_fatal_error( + "synthesizeUnitary2QWeyl failed during test synthesis"); } - return unitary; + func::ReturnOp::create(builder, loc, ValueRange{out0, out1}); + return SynthesizedTwoQubitCircuit{.mlirModule = std::move(mlirModule), + .func = func}; } -static void -expectTwoQubitQcoModulesEquivalent(const OwningOpRef& lhs, - const OwningOpRef& rhs) { - const auto lhsUnitary = computeTwoQubitUnitaryFromModule(lhs); - ASSERT_TRUE(lhsUnitary.has_value()); - const auto rhsUnitary = computeTwoQubitUnitaryFromModule(rhs); - ASSERT_TRUE(rhsUnitary.has_value()); - EXPECT_TRUE(isEquivalentUpToGlobalPhase(*lhsUnitary, *rhsUnitary)); +void expect2QMatrixPreserved(func::FuncOp funcOp, const Matrix4x4& original) { + const auto actual = compute2QUnitaryFromFunc(funcOp); + ASSERT_TRUE(actual.has_value()); + EXPECT_TRUE(isEquivalentUpToGlobalPhase(*actual, original)); } -struct TwoQFuseFixture { - std::unique_ptr context; - - void setUp() { - DialectRegistry registry; - registry.insert(); - context = std::make_unique(); - context->appendDialectRegistry(registry); - context->loadAllAvailableDialects(); - } +void expectSynthesized2QMatrix(MLIRContext* ctx, const Matrix4x4& target, + const NativeProfileSpec& spec) { + const auto circuit = synthesize2QMatrix(ctx, target, spec); + ASSERT_TRUE(succeeded(verify(*circuit.mlirModule))); + expect2QMatrixPreserved(circuit.func, target); +} - [[nodiscard]] MLIRContext* ctx() const { return context.get(); } -}; +} // namespace -static std::size_t countCtrlOps(const OwningOpRef& moduleOp) { - std::size_t count = 0; - moduleOp.get()->walk([&](qco::CtrlOp) { ++count; }); - return count; +TEST(WeylSynthesisTest, ReconstructsCxWithGenericProfile) { + WeylSynthesisFixture fx; + fx.setUp(); + const auto spec = parseNativeSpec("u,cx"); + ASSERT_TRUE(spec); + expectSynthesized2QMatrix(fx.ctx(), cxGate01(), *spec); } -static LogicalResult runQcToQco(ModuleOp moduleOp) { - PassManager pm(moduleOp.getContext()); - pm.addPass(mlir::createQCToQCO()); - return pm.run(moduleOp); +TEST(WeylSynthesisTest, ReconstructsProductUnitaryWithGenericProfile) { + WeylSynthesisFixture fx; + fx.setUp(); + const auto spec = parseNativeSpec("u,cx"); + ASSERT_TRUE(spec); + const Matrix4x4 target = kron(rzMatrix(1.0), ryMatrix(0.3)); + expectSynthesized2QMatrix(fx.ctx(), target, *spec); } -static LogicalResult runTwoQFuse(ModuleOp moduleOp, StringRef nativeGates) { - PassManager pm(moduleOp.getContext()); - pm.addPass(mlir::qco::createFuseTwoQubitUnitaryRuns( - mlir::qco::FuseTwoQubitUnitaryRunsOptions{ - .nativeGates = nativeGates.str(), - })); - return pm.run(moduleOp); +TEST(WeylSynthesisTest, ReconstructsWithIbmBasicProfile) { + WeylSynthesisFixture fx; + fx.setUp(); + const auto spec = parseNativeSpec("x,sx,rz,cx"); + ASSERT_TRUE(spec); + const Matrix4x4 target = kron(hGate(), Matrix2x2::identity()) * cxGate01() * + kron(rzMatrix(0.2), ryMatrix(0.1)); + expectSynthesized2QMatrix(fx.ctx(), target, *spec); } -template -static OwningOpRef buildProgram(MLIRContext* ctx, ProgramT program) { - return mlir::qc::QCProgramBuilder::build(ctx, program); +TEST(WeylSynthesisTest, IdentityRequiresNoEntanglers) { + WeylSynthesisFixture fx; + fx.setUp(); + const auto spec = parseNativeSpec("u,cx"); + ASSERT_TRUE(spec); + const auto count = twoQubitEntanglerCount(Matrix4x4::identity(), *spec); + ASSERT_TRUE(count.has_value()); + EXPECT_EQ(*count, 0U); } -template -static void expectTwoQFusePreservesUnitary(MLIRContext* ctx, ProgramT program, - StringRef nativeGates) { - auto expected = buildProgram(ctx, program); - ASSERT_TRUE(expected); - ASSERT_TRUE(succeeded(runQcToQco(*expected))); - - auto fused = buildProgram(ctx, program); - ASSERT_TRUE(fused); - ASSERT_TRUE(succeeded(runQcToQco(*fused))); - ASSERT_TRUE(succeeded(runTwoQFuse(*fused, nativeGates))); - ASSERT_TRUE(succeeded(verify(*fused))); - expectTwoQubitQcoModulesEquivalent(expected, fused); +TEST(WeylSynthesisTest, FailsWithoutEntanglerInSpec) { + WeylSynthesisFixture fx; + fx.setUp(); + const auto spec = parseNativeSpec("u"); + ASSERT_TRUE(spec); + EXPECT_TRUE(spec->entanglerBases.empty()); + + OwningOpRef mlirModule = + ModuleOp::create(UnknownLoc::get(fx.ctx())); + OpBuilder builder(fx.ctx()); + builder.setInsertionPointToStart(mlirModule->getBody()); + const auto qubitTy = QubitType::get(fx.ctx()); + const auto funcTy = + builder.getFunctionType({qubitTy, qubitTy}, {qubitTy, qubitTy}); + const Location loc = mlirModule->getLoc(); + auto func = func::FuncOp::create(builder, loc, "main", funcTy); + auto* entry = func.addEntryBlock(); + builder.setInsertionPointToStart(entry); + Value out0; + Value out1; + EXPECT_TRUE(failed(synthesizeUnitary2QWeyl( + builder, loc, entry->getArgument(0), entry->getArgument(1), cxGate01(), + *spec, out0, out1))); } //===----------------------------------------------------------------------===// -// FuseTwoQubitUnitaryRuns tests +// Native gate menu parsing (Weyl library API) //===----------------------------------------------------------------------===// -TEST(FuseTwoQubitUnitaryRunsTest, InvalidNativeGatesFailsPass) { - TwoQFuseFixture fx; - fx.setUp(); - auto module = buildProgram(fx.ctx(), fusionCxCx); - ASSERT_TRUE(module); - ASSERT_TRUE(succeeded(runQcToQco(*module))); - EXPECT_TRUE(failed(runTwoQFuse(*module, "not-a-gate"))); -} - -TEST(FuseTwoQubitUnitaryRunsTest, AdjacentCxCancel) { - TwoQFuseFixture fx; - fx.setUp(); - expectTwoQFusePreservesUnitary(fx.ctx(), fusionCxCx, "u,cx"); - - auto module = buildProgram(fx.ctx(), fusionCxCx); - ASSERT_TRUE(module); - ASSERT_TRUE(succeeded(runQcToQco(*module))); - ASSERT_TRUE(succeeded(runTwoQFuse(*module, "u,cx"))); - EXPECT_EQ(countCtrlOps(module), 0U); +TEST(NativeSpecTest, ResolveIbmBasicCx) { + const auto spec = parseNativeSpec("x,sx,rz,cx"); + ASSERT_TRUE(spec); + EXPECT_TRUE(spec->allowedGates.contains(NativeGateKind::Cx)); + EXPECT_TRUE(spec->allowedGates.contains(NativeGateKind::X)); + EXPECT_FALSE(spec->allowRzz); } -TEST(FuseTwoQubitUnitaryRunsTest, FusesCxThroughInterleavedOneQOps) { - TwoQFuseFixture fx; - fx.setUp(); - expectTwoQFusePreservesUnitary(fx.ctx(), fusionHCxInterleavedTCx, "u,cx"); +TEST(NativeSpecTest, ResolveRejectsUnknownToken) { + EXPECT_FALSE(parseNativeSpec("x,sx,rz,not-a-gate").has_value()); } -TEST(FuseTwoQubitUnitaryRunsTest, StopsAtDifferentPairBoundary) { - TwoQFuseFixture fx; - fx.setUp(); - auto module = buildProgram(fx.ctx(), fusionThreeLineCx); - ASSERT_TRUE(module); - ASSERT_TRUE(succeeded(runQcToQco(*module))); - ASSERT_TRUE(succeeded(runTwoQFuse(*module, "u,cx"))); - EXPECT_GE(countCtrlOps(module), 1U); +TEST(NativeSpecTest, PhaseAliasPMatchesRzInIbmStyleMenu) { + const auto pMenu = parseNativeSpec("x,sx,p,cx"); + const auto rzMenu = parseNativeSpec("x,sx,rz,cx"); + ASSERT_TRUE(pMenu); + ASSERT_TRUE(rzMenu); + EXPECT_EQ(pMenu->allowedGates, rzMenu->allowedGates); } -TEST(FuseTwoQubitUnitaryRunsTest, DoesNotFuseAcrossBarrier) { - TwoQFuseFixture fx; - fx.setUp(); - auto module = buildProgram(fx.ctx(), fusionCxBarrierCx); - ASSERT_TRUE(module); - ASSERT_TRUE(succeeded(runQcToQco(*module))); - ASSERT_TRUE(succeeded(runTwoQFuse(*module, "u,cx"))); - EXPECT_EQ(countCtrlOps(module), 2U); -} +TEST(NativePolicyTest, UsesCxAndCzFromResolvedSpec) { + const auto cxOnly = parseNativeSpec("u,cx"); + ASSERT_TRUE(cxOnly); + EXPECT_TRUE(llvm::is_contained(cxOnly->entanglerBases, EntanglerBasis::Cx)); + EXPECT_FALSE(llvm::is_contained(cxOnly->entanglerBases, EntanglerBasis::Cz)); -TEST(FuseTwoQubitUnitaryRunsTest, HandlesSwappedWireOrder) { - TwoQFuseFixture fx; - fx.setUp(); - expectTwoQFusePreservesUnitary(fx.ctx(), fusionSwapCxPattern, "u,cx"); + const auto both = parseNativeSpec("u,cx,cz"); + ASSERT_TRUE(both); + EXPECT_TRUE(llvm::is_contained(both->entanglerBases, EntanglerBasis::Cx)); + EXPECT_TRUE(llvm::is_contained(both->entanglerBases, EntanglerBasis::Cz)); } -TEST(FuseTwoQubitUnitaryRunsTest, HandlesRzzBlock) { - TwoQFuseFixture fx; - fx.setUp(); - expectTwoQFusePreservesUnitary(fx.ctx(), fusionHRzzSRzz, "x,sx,rz,rx,rzz,cz"); +TEST(NativePolicyTest, ExcludesGateKindsNotInMenu) { + const auto spec = parseNativeSpec("u,cx"); + ASSERT_TRUE(spec); + EXPECT_TRUE(spec->allowedGates.contains(NativeGateKind::U)); + EXPECT_TRUE(spec->allowedGates.contains(NativeGateKind::Cx)); + EXPECT_FALSE(spec->allowedGates.contains(NativeGateKind::X)); + EXPECT_FALSE(spec->allowedGates.contains(NativeGateKind::Sx)); + EXPECT_FALSE(spec->allowedGates.contains(NativeGateKind::Rz)); } diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt index 35463ee646..0fa87a5daf 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt @@ -6,8 +6,8 @@ # # Licensed under the MIT License -set(target_name mqt-core-mlir-unittest-native-synthesis) -add_executable(${target_name} test_native_synthesis.cpp) +set(target_name mqt-core-mlir-unittest-fuse-two-qubit-unitary-runs) +add_executable(${target_name} test_fuse_two_qubit_unitary_runs.cpp) target_link_libraries( ${target_name} @@ -19,6 +19,9 @@ target_link_libraries( MLIRQCToQCO MLIRQCOTransforms MLIRPass + MLIRFuncDialect + MLIRArithDialect + MLIRIR MLIRSupport LLVMSupport) diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp new file mode 100644 index 0000000000..92b0bf8aeb --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp @@ -0,0 +1,973 @@ +/* + * 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 "TestCaseUtils.h" +#include "mlir/Conversion/QCToQCO/QCToQCO.h" +#include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Passes.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" +#include "qc_programs.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace mlir; +using namespace mlir::qco; + +namespace mlir::qco::fuse_two_qubit_test { + +using mqt::test::cxGate01; +using mqt::test::czGate; +using mqt::test::expandToTwoQubits; +using mqt::test::fixTwoQubitMatrixQubitOrder; +using mqt::test::isEquivalentUpToGlobalPhase; +using mqt::test::QubitId; + +[[nodiscard]] static std::optional +getUnitaryQubitOperand(UnitaryOpInterface op, std::size_t index) { + if (index >= op.getNumQubits()) { + return std::nullopt; + } + Value v = op->getOperand(index); + if (!llvm::isa(v.getType())) { + return std::nullopt; + } + return v; +} + +[[nodiscard]] static std::optional +getUnitaryQubitResult(UnitaryOpInterface op, std::size_t index) { + if (index >= op.getNumQubits()) { + return std::nullopt; + } + Value v = op->getResult(index); + if (!llvm::isa(v.getType())) { + return std::nullopt; + } + return v; +} + +[[nodiscard]] static bool extractSingleQubitMatrix(UnitaryOpInterface op, + Matrix2x2& out) { + if (op.getUnitaryMatrix2x2(out)) { + return true; + } + DynamicMatrix dynamic; + if (!op.getUnitaryMatrixDynamic(dynamic) || dynamic.rows() != 2 || + dynamic.cols() != 2) { + return false; + } + out = Matrix2x2::fromElements(dynamic(0, 0), dynamic(0, 1), dynamic(1, 0), + dynamic(1, 1)); + return true; +} + +[[nodiscard]] static bool extractTwoQubitMatrix(UnitaryOpInterface op, + Matrix4x4& out) { + if (auto ctrl = llvm::dyn_cast(op.getOperation())) { + if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { + return false; + } + auto* body = ctrl.getBodyUnitary(0).getOperation(); + if (llvm::isa(body)) { + out = cxGate01(); + return true; + } + if (llvm::isa(body)) { + out = czGate(); + return true; + } + return false; + } + return op.getUnitaryMatrix4x4(out); +} + +[[nodiscard]] static std::optional +computeUnitaryFromModule(const OwningOpRef& moduleOp) { + ModuleOp module = moduleOp.get(); + if (!module) { + return std::nullopt; + } + + llvm::DenseMap qubitIds; + std::size_t nextQubitId = 0; + std::size_t numQubits = 0; + + for (auto func : module.getOps()) { + for (auto& block : func.getBlocks()) { + for (auto& rawOp : block.getOperations()) { + if (auto staticOp = llvm::dyn_cast(&rawOp)) { + const auto index = static_cast(staticOp.getIndex()); + qubitIds.try_emplace(staticOp.getQubit(), index); + numQubits = std::max(numQubits, index + 1); + } else if (auto alloc = llvm::dyn_cast(&rawOp)) { + qubitIds.try_emplace(alloc.getResult(), nextQubitId++); + numQubits = std::max(numQubits, nextQubitId); + } + } + } + } + + if (numQubits == 0) { + return std::nullopt; + } + + DynamicMatrix unitary = + DynamicMatrix::identity(static_cast(1ULL << numQubits)); + + auto getQubitId = [&](Value qubit) -> std::optional { + auto it = qubitIds.find(qubit); + if (it == qubitIds.end()) { + return std::nullopt; + } + return it->second; + }; + + for (auto func : module.getOps()) { + for (auto& block : func.getBlocks()) { + for (auto& rawOp : block.getOperations()) { + auto op = llvm::dyn_cast(&rawOp); + if (!op) { + continue; + } + if (llvm::isa(op.getOperation())) { + continue; + } + + if (op.isSingleQubit()) { + const auto qIn = getUnitaryQubitOperand(op, 0); + if (!qIn) { + return std::nullopt; + } + auto qid = getQubitId(*qIn); + if (!qid) { + return std::nullopt; + } + Matrix2x2 oneQ; + if (!extractSingleQubitMatrix(op, oneQ)) { + return std::nullopt; + } + unitary = embedSingleQubitInNqubit(oneQ, numQubits, *qid) * unitary; + const auto qOut = getUnitaryQubitResult(op, 0); + if (!qOut) { + return std::nullopt; + } + qubitIds[*qOut] = *qid; + continue; + } + + if (op.isTwoQubit()) { + const auto q0In = getUnitaryQubitOperand(op, 0); + const auto q1In = getUnitaryQubitOperand(op, 1); + if (!q0In || !q1In) { + return std::nullopt; + } + auto q0id = getQubitId(*q0In); + auto q1id = getQubitId(*q1In); + if (!q0id || !q1id) { + return std::nullopt; + } + Matrix4x4 twoQ; + if (!extractTwoQubitMatrix(op, twoQ)) { + return std::nullopt; + } + unitary = + embedTwoQubitInNqubit(twoQ, numQubits, *q0id, *q1id) * unitary; + const auto q0Out = getUnitaryQubitResult(op, 0); + const auto q1Out = getUnitaryQubitResult(op, 1); + if (!q0Out || !q1Out) { + return std::nullopt; + } + qubitIds[*q0Out] = *q0id; + qubitIds[*q1Out] = *q1id; + continue; + } + + return std::nullopt; + } + } + } + + return unitary; +} + +[[nodiscard]] static std::optional +computeTwoQubitUnitaryFromModule(const OwningOpRef& moduleOp) { + ModuleOp module = moduleOp.get(); + if (!module) { + return std::nullopt; + } + Matrix4x4 unitary = Matrix4x4::identity(); + llvm::DenseMap qubitIds; + std::size_t nextQubitId = 0; + + for (auto func : module.getOps()) { + for (auto& block : func.getBlocks()) { + for (auto& rawOp : block.getOperations()) { + if (auto alloc = llvm::dyn_cast(&rawOp)) { + if (nextQubitId >= 2) { + return std::nullopt; + } + qubitIds.try_emplace(alloc.getResult(), nextQubitId++); + } + } + } + } + + auto getQubitId = [&](Value qubit) -> std::optional { + auto it = qubitIds.find(qubit); + if (it == qubitIds.end()) { + return std::nullopt; + } + return it->second; + }; + + for (auto func : module.getOps()) { + for (auto& block : func.getBlocks()) { + for (auto& rawOp : block.getOperations()) { + auto op = llvm::dyn_cast(&rawOp); + if (!op) { + continue; + } + if (llvm::isa(op.getOperation())) { + continue; + } + + if (op.isSingleQubit()) { + const auto qIn = getUnitaryQubitOperand(op, 0); + if (!qIn) { + return std::nullopt; + } + auto qid = getQubitId(*qIn); + if (!qid) { + return std::nullopt; + } + Matrix2x2 oneQ; + if (!extractSingleQubitMatrix(op, oneQ)) { + return std::nullopt; + } + unitary = + expandToTwoQubits(oneQ, static_cast(*qid)) * unitary; + const auto qOut = getUnitaryQubitResult(op, 0); + if (!qOut) { + return std::nullopt; + } + qubitIds[*qOut] = *qid; + continue; + } + + if (op.isTwoQubit()) { + const auto q0In = getUnitaryQubitOperand(op, 0); + const auto q1In = getUnitaryQubitOperand(op, 1); + if (!q0In || !q1In) { + return std::nullopt; + } + auto q0id = getQubitId(*q0In); + auto q1id = getQubitId(*q1In); + if (!q0id || !q1id) { + return std::nullopt; + } + Matrix4x4 twoQ; + if (!extractTwoQubitMatrix(op, twoQ)) { + return std::nullopt; + } + const llvm::SmallVector ids{static_cast(*q0id), + static_cast(*q1id)}; + unitary = fixTwoQubitMatrixQubitOrder(twoQ, ids) * unitary; + const auto q0Out = getUnitaryQubitResult(op, 0); + const auto q1Out = getUnitaryQubitResult(op, 1); + if (!q0Out || !q1Out) { + return std::nullopt; + } + qubitIds[*q0Out] = *q0id; + qubitIds[*q1Out] = *q1id; + continue; + } + } + } + } + + if (nextQubitId != 2) { + return std::nullopt; + } + return unitary; +} + +static void expectQcoModulesEquivalent(const OwningOpRef& lhs, + const OwningOpRef& rhs) { + const auto lhsUnitary = computeUnitaryFromModule(lhs); + ASSERT_TRUE(lhsUnitary.has_value()); + const auto rhsUnitary = computeUnitaryFromModule(rhs); + ASSERT_TRUE(rhsUnitary.has_value()); + EXPECT_TRUE(isEquivalentUpToGlobalPhase(*lhsUnitary, *rhsUnitary)); +} + +static void +expectTwoQubitQcoModulesEquivalent(const OwningOpRef& lhs, + const OwningOpRef& rhs) { + const auto lhsUnitary = computeTwoQubitUnitaryFromModule(lhs); + ASSERT_TRUE(lhsUnitary.has_value()); + const auto rhsUnitary = computeTwoQubitUnitaryFromModule(rhs); + ASSERT_TRUE(rhsUnitary.has_value()); + EXPECT_TRUE(isEquivalentUpToGlobalPhase(*lhsUnitary, *rhsUnitary)); +} + +/// One row of the standard multi-profile equivalence sweeps in tests. +// NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_F` at global scope +struct FuseTwoQubitProfileSweepCase { + const char* nativeGates; + bool (*isNative)(OwningOpRef&); +}; + +/// Shared gtest fixture for ``fuse-two-qubit-unitary-runs`` pass tests. +// NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_F` at global scope +class FuseTwoQubitUnitaryRunsPassTest : public testing::Test { +protected: + void SetUp() override { + DialectRegistry registry; + registry.insert(); + context = std::make_unique(); + context->appendDialectRegistry(registry); + context->loadAllAvailableDialects(); + } + + template + static bool onlyTheseOps(OwningOpRef& moduleOp, const bool allowCx, + const bool allowCz) { + bool ok = true; + std::ignore = moduleOp->walk([&](UnitaryOpInterface op) { + Operation* raw = op.getOperation(); + if (llvm::isa_and_present(raw->getParentOp())) { + return WalkResult::advance(); + } + if (llvm::isa(raw)) { + return WalkResult::advance(); + } + if (auto ctrl = llvm::dyn_cast(raw)) { + if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { + ok = false; + return WalkResult::interrupt(); + } + Operation* body = ctrl.getBodyUnitary(0).getOperation(); + const bool isCx = llvm::isa(body); + const bool isCz = llvm::isa(body); + if ((isCx && allowCx) || (isCz && allowCz)) { + return WalkResult::advance(); + } + ok = false; + return WalkResult::interrupt(); + } + + if (!llvm::isa(raw)) { + ok = false; + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + return ok; + } + + static bool onlyIbmBasicCxOps(OwningOpRef& moduleOp) { + return onlyTheseOps(moduleOp, /*allowCx=*/true, + /*allowCz=*/false); + } + + static bool onlyIbmBasicCzOps(OwningOpRef& moduleOp) { + return onlyTheseOps(moduleOp, /*allowCx=*/false, + /*allowCz=*/true); + } + + static bool onlyGenericU3CxOps(OwningOpRef& moduleOp) { + return onlyTheseOps(moduleOp, /*allowCx=*/true, /*allowCz=*/false); + } + + static bool onlyGenericU3CzOps(OwningOpRef& moduleOp) { + return onlyTheseOps(moduleOp, /*allowCx=*/false, /*allowCz=*/true); + } + + static bool onlyIqmDefaultOps(OwningOpRef& moduleOp) { + return onlyTheseOps(moduleOp, /*allowCx=*/false, /*allowCz=*/true); + } + + static bool onlyIbmFractionalOps(OwningOpRef& moduleOp) { + return onlyTheseOps( + moduleOp, /*allowCx=*/false, /*allowCz=*/true); + } + + static bool onlyAxisPairRxRzCxOps(OwningOpRef& moduleOp) { + return onlyTheseOps(moduleOp, /*allowCx=*/true, + /*allowCz=*/false); + } + + static bool onlyAxisPairRxRyCxOps(OwningOpRef& moduleOp) { + return onlyTheseOps(moduleOp, /*allowCx=*/true, + /*allowCz=*/false); + } + + static bool onlyAxisPairRyRzCzOps(OwningOpRef& moduleOp) { + return onlyTheseOps(moduleOp, /*allowCx=*/false, + /*allowCz=*/true); + } + + static bool onlyUOrAxisPairRxRzCxOps(OwningOpRef& moduleOp) { + return onlyTheseOps(moduleOp, /*allowCx=*/true, + /*allowCz=*/false); + } + + static bool onlyGenericU3CxOrCzOps(OwningOpRef& moduleOp) { + return onlyTheseOps(moduleOp, /*allowCx=*/true, /*allowCz=*/true); + } + + static std::array coreEquivalenceProfiles() { + return {{{.nativeGates = "x,sx,rz,cx", .isNative = &onlyIbmBasicCxOps}, + {.nativeGates = "u,cx", .isNative = &onlyGenericU3CxOps}, + {.nativeGates = "r,cz", .isNative = &onlyIqmDefaultOps}}}; + } + + static void + runFuseTwoQubitUnitaryRunsPipeline(OwningOpRef& moduleOp, + const std::string& nativeGates) { + PassManager pm(moduleOp->getContext()); + pm.addPass(createQCToQCO()); + pm.addPass(createFuseTwoQubitUnitaryRuns(FuseTwoQubitUnitaryRunsOptions{ + .nativeGates = nativeGates, + })); + ASSERT_TRUE(succeeded(pm.run(*moduleOp))); + } + + static void runQcToQco(OwningOpRef& moduleOp) { + PassManager pm(moduleOp->getContext()); + pm.addPass(createQCToQCO()); + ASSERT_TRUE(succeeded(pm.run(*moduleOp))); + } + + static void runTwoQFuse(OwningOpRef& moduleOp, + StringRef nativeGates) { + PassManager pm(moduleOp->getContext()); + pm.addPass(createFuseTwoQubitUnitaryRuns(FuseTwoQubitUnitaryRunsOptions{ + .nativeGates = nativeGates.str(), + })); + ASSERT_TRUE(succeeded(pm.run(*moduleOp))); + } + + static std::string moduleToString(const OwningOpRef& moduleOp) { + std::string text; + llvm::raw_string_ostream os(text); + moduleOp.get()->print(os); + return text; + } + + template + void expectNativeAfterSynthesis(BuildFn buildFn, + const std::string& nativeGates, + PredicateFn isNative) { + auto moduleOp = buildFn(); + runFuseTwoQubitUnitaryRunsPipeline(moduleOp, nativeGates); + EXPECT_TRUE(isNative(moduleOp)); + } + + template + void expectSynthesisFailure(BuildFn buildFn, const std::string& nativeGates) { + auto moduleOp = buildFn(); + PassManager pm(moduleOp->getContext()); + pm.addPass(createQCToQCO()); + pm.addPass(createFuseTwoQubitUnitaryRuns(FuseTwoQubitUnitaryRunsOptions{ + .nativeGates = nativeGates, + })); + EXPECT_TRUE(failed(pm.run(*moduleOp))); + } + + template + void expectEquivalentAndNativeAfterSynthesis(BuildFn buildFn, + const std::string& nativeGates, + PredicateFn isNative) { + auto expectedModule = buildFn(); + runQcToQco(expectedModule); + + auto synthesizedModule = buildFn(); + runFuseTwoQubitUnitaryRunsPipeline(synthesizedModule, nativeGates); + EXPECT_TRUE(isNative(synthesizedModule)); + expectQcoModulesEquivalent(expectedModule, synthesizedModule); + } + + template + static void expectTwoQFusePreservesUnitary(MLIRContext* ctx, ProgramT program, + StringRef nativeGates) { + auto build = [&](MLIRContext* context) { + return mlir::qc::QCProgramBuilder::build(context, program); + }; + auto expected = build(ctx); + ASSERT_TRUE(expected); + runQcToQco(expected); + + auto fused = build(ctx); + ASSERT_TRUE(fused); + runQcToQco(fused); + runTwoQFuse(fused, nativeGates); + ASSERT_TRUE(succeeded(verify(*fused))); + expectTwoQubitQcoModulesEquivalent(expected, fused); + } + + static std::size_t countCtrlOps(const OwningOpRef& moduleOp) { + std::size_t count = 0; + moduleOp.get()->walk([&](CtrlOp) { ++count; }); + return count; + } + + std::unique_ptr context; +}; + +} // namespace mlir::qco::fuse_two_qubit_test + +using namespace mlir::qco::fuse_two_qubit_test; + +struct NativeSynthMenuRow { + const char* name; + const char* nativeGates; + bool (*isNative)(OwningOpRef&); +}; + +// --- Inline circuit builders --- + +static void fusionCxCx(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.cx(q0, q1); + b.cx(q0, q1); +} + +static void fusionHCxInterleavedTCx(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.cx(q0, q1); + b.t(q1); + b.s(q0); + b.cx(q0, q1); +} + +static void fusionThreeLineCx(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + const auto q2 = b.allocQubit(); + b.cx(q0, q1); + b.cx(q1, q2); + b.cx(q0, q1); +} + +static void fusionCxBarrierCx(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.cx(q0, q1); + b.barrier({q0, q1}); + b.cx(q0, q1); +} + +static void fusionSwapCxPattern(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.cx(q0, q1); + b.cx(q1, q0); + b.cx(q0, q1); +} + +static void fusionHRzzSRzz(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.rzz(-0.29, q0, q1); + b.s(q1); + b.rzz(0.17, q0, q1); +} + +static void broadOneQThenCz(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.x(q0); + b.y(q1); + b.h(q0); + b.sx(q1); + b.rx(0.13, q0); + b.ry(-0.47, q1); + b.rz(0.29, q0); + b.cz(q0, q1); +} + +static void zeroAngleThenCz(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.rx(0.0, q0); + b.ry(0.0, q1); + b.rz(0.0, q0); + b.p(0.0, q1); + b.cz(q0, q1); +} + +static void ibmFractionalGateFamilies(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.rx(0.13, q1); + b.cx(q0, q1); + b.cz(q1, q0); + b.swap(q0, q1); + b.rzz(-0.33, q0, q1); + b.rzx(0.41, q0, q1); +} + +static void hstycxTwoQ(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.s(q0); + b.t(q0); + b.y(q0); + b.cx(q0, q1); +} + +static void cxYOnQ1(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.cx(q0, q1); + b.y(q1); +} + +static void hCxTOnQ1(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q1); + b.cx(q0, q1); + b.t(q1); +} + +static void xYSXCz(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.x(q0); + b.y(q0); + b.sx(q0); + b.cz(q0, q1); +} + +static void hYCx(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.y(q0); + b.cx(q0, q1); +} + +static void zCx(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.z(q0); + b.cx(q0, q1); +} + +static void xHCz(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.x(q0); + b.h(q0); + b.cz(q0, q1); +} + +static void hq0Yq1CxSq0(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.y(q1); + b.cx(q0, q1); + b.s(q0); +} + +static void hCxSq1(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.cx(q0, q1); + b.s(q1); +} + +static void threeQGhz(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + const auto q2 = b.allocQubit(); + b.h(q0); + b.cx(q0, q1); + b.cx(q1, q2); +} + +static void determinismSwap(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.swap(q0, q1); + b.dealloc(q0); + b.dealloc(q1); +} + +// --- Pass profile coverage --- + +// NOLINTNEXTLINE(misc-use-internal-linkage) +class FuseTwoQubitSwapProfileTest + : public FuseTwoQubitUnitaryRunsPassTest, + public testing::WithParamInterface { +public: + using FuseTwoQubitUnitaryRunsPassTest::onlyGenericU3CxOps; + using FuseTwoQubitUnitaryRunsPassTest::onlyIbmBasicCxOps; + using FuseTwoQubitUnitaryRunsPassTest::onlyIqmDefaultOps; +}; + +TEST_P(FuseTwoQubitSwapProfileTest, DecomposesSwapToProfile) { + const NativeSynthMenuRow& param = GetParam(); + expectNativeAfterSynthesis( + [&] { + return mlir::qc::QCProgramBuilder::build(context.get(), mlir::qc::swap); + }, + param.nativeGates, param.isNative); +} + +INSTANTIATE_TEST_SUITE_P( + SwapMenuMatrix, FuseTwoQubitSwapProfileTest, + testing::Values( + NativeSynthMenuRow{"IbmBasicCx", "x,sx,rz,cx", + &FuseTwoQubitSwapProfileTest::onlyIbmBasicCxOps}, + NativeSynthMenuRow{"GenericU3Cx", "u,cx", + &FuseTwoQubitSwapProfileTest::onlyGenericU3CxOps}, + NativeSynthMenuRow{"IqmDefault", "r,cz", + &FuseTwoQubitSwapProfileTest::onlyIqmDefaultOps}), + [](const testing::TestParamInfo& info) { + return info.param.name; + }); + +TEST_F(FuseTwoQubitUnitaryRunsPassTest, DecomposesHstycxToIbmBasicCx) { + expectNativeAfterSynthesis( + [&] { + return mlir::qc::QCProgramBuilder::build(context.get(), hstycxTwoQ); + }, + "x,sx,rz,cx", &FuseTwoQubitUnitaryRunsPassTest::onlyIbmBasicCxOps); +} + +TEST_F(FuseTwoQubitUnitaryRunsPassTest, DecomposesCxYOnQ1ToIqmDefault) { + expectNativeAfterSynthesis( + [&] { return mlir::qc::QCProgramBuilder::build(context.get(), cxYOnQ1); }, + "r,cz", &FuseTwoQubitUnitaryRunsPassTest::onlyIqmDefaultOps); +} + +TEST_F(FuseTwoQubitUnitaryRunsPassTest, BroadOneQCanonicalizationOnIqmDefault) { + auto moduleOp = + mlir::qc::QCProgramBuilder::build(context.get(), broadOneQThenCz); + runFuseTwoQubitUnitaryRunsPipeline(moduleOp, "r,cz"); + EXPECT_TRUE(onlyIqmDefaultOps(moduleOp)); +} + +TEST_F(FuseTwoQubitUnitaryRunsPassTest, ZeroAngleCanonicalizationOnRyRzCz) { + auto moduleOp = + mlir::qc::QCProgramBuilder::build(context.get(), zeroAngleThenCz); + runFuseTwoQubitUnitaryRunsPipeline(moduleOp, "ry,rz,cz"); + EXPECT_TRUE(onlyAxisPairRyRzCzOps(moduleOp)); +} + +TEST_F(FuseTwoQubitUnitaryRunsPassTest, DecomposesCxToCzForIbmBasicCzProfile) { + expectNativeAfterSynthesis( + [&] { + return mlir::qc::QCProgramBuilder::build(context.get(), hCxTOnQ1); + }, + "x,sx,rz,cz", &FuseTwoQubitUnitaryRunsPassTest::onlyIbmBasicCzOps); +} + +TEST_F(FuseTwoQubitUnitaryRunsPassTest, DecomposesToIqmDefaultProfile) { + expectNativeAfterSynthesis( + [&] { return mlir::qc::QCProgramBuilder::build(context.get(), xYSXCz); }, + "r,cz", &FuseTwoQubitUnitaryRunsPassTest::onlyIqmDefaultOps); +} + +TEST_F(FuseTwoQubitUnitaryRunsPassTest, DecomposesToIbmFractionalProfile) { + expectNativeAfterSynthesis( + [&] { + return mlir::qc::QCProgramBuilder::build(context.get(), + ibmFractionalGateFamilies); + }, + "x,sx,rz,rx,rzz,cz", + &FuseTwoQubitUnitaryRunsPassTest::onlyIbmFractionalOps); +} + +TEST_F(FuseTwoQubitUnitaryRunsPassTest, DecomposesToAxisPairRxRzCxProfile) { + expectNativeAfterSynthesis( + [&] { return mlir::qc::QCProgramBuilder::build(context.get(), hYCx); }, + "rx,rz,cx", &FuseTwoQubitUnitaryRunsPassTest::onlyAxisPairRxRzCxOps); +} + +TEST_F(FuseTwoQubitUnitaryRunsPassTest, DecomposesRzToAxisPairRxRyCxProfile) { + expectNativeAfterSynthesis( + [&] { return mlir::qc::QCProgramBuilder::build(context.get(), zCx); }, + "rx,ry,cx", &FuseTwoQubitUnitaryRunsPassTest::onlyAxisPairRxRyCxOps); +} + +TEST_F(FuseTwoQubitUnitaryRunsPassTest, + GenericProfileMatchesGenericU3CxBehavior) { + expectEquivalentAndNativeAfterSynthesis( + [&] { + return mlir::qc::QCProgramBuilder::build(context.get(), hq0Yq1CxSq0); + }, + "u,cx", &FuseTwoQubitUnitaryRunsPassTest::onlyGenericU3CxOps); +} + +TEST_F(FuseTwoQubitUnitaryRunsPassTest, + GenericProfileMatchesAxisPairRyRzCzBehavior) { + expectEquivalentAndNativeAfterSynthesis( + [&] { return mlir::qc::QCProgramBuilder::build(context.get(), xHCz); }, + "ry,rz,cz", &FuseTwoQubitUnitaryRunsPassTest::onlyAxisPairRyRzCzOps); +} + +TEST_F(FuseTwoQubitUnitaryRunsPassTest, + CustomProfileAcceptsMultipleEntanglersMenu) { + expectEquivalentAndNativeAfterSynthesis( + [&] { return mlir::qc::QCProgramBuilder::build(context.get(), hCxSq1); }, + "u,cx,cz", &FuseTwoQubitUnitaryRunsPassTest::onlyGenericU3CxOrCzOps); +} + +TEST_F(FuseTwoQubitUnitaryRunsPassTest, FailsForUnsupportedNativeGateMenu) { + expectSynthesisFailure( + [&] { + return mlir::qc::QCProgramBuilder::build(context.get(), mlir::qc::h); + }, + "not-a-gate"); +} + +TEST_F(FuseTwoQubitUnitaryRunsPassTest, + FailsForNativeGateMenuWithoutSingleQEmitter) { + expectSynthesisFailure( + [&] { + return mlir::qc::QCProgramBuilder::build(context.get(), + mlir::qc::singleControlledX); + }, + "cx,cz"); +} + +TEST_F(FuseTwoQubitUnitaryRunsPassTest, FailsForMultiControlledGateStructure) { + expectSynthesisFailure( + [&] { + return mlir::qc::QCProgramBuilder::build(context.get(), + mlir::qc::multipleControlledX); + }, + "x,sx,rz,cx"); +} + +TEST_F(FuseTwoQubitUnitaryRunsPassTest, + CandidateSelectionIsDeterministicAcrossRuns) { + auto buildFn = [&] { + return mlir::qc::QCProgramBuilder::build(context.get(), determinismSwap); + }; + auto firstModule = buildFn(); + runFuseTwoQubitUnitaryRunsPipeline(firstModule, "u,cx"); + auto secondModule = buildFn(); + runFuseTwoQubitUnitaryRunsPipeline(secondModule, "u,cx"); + EXPECT_EQ(moduleToString(firstModule), moduleToString(secondModule)); +} + +TEST_F(FuseTwoQubitUnitaryRunsPassTest, ThreeQubitGhzEquivalentOnCoreProfiles) { + for (const auto& profileCase : coreEquivalenceProfiles()) { + auto expected = mlir::qc::QCProgramBuilder::build(context.get(), threeQGhz); + runQcToQco(expected); + + auto synthesized = + mlir::qc::QCProgramBuilder::build(context.get(), threeQGhz); + runFuseTwoQubitUnitaryRunsPipeline(synthesized, profileCase.nativeGates); + EXPECT_TRUE(profileCase.isNative(synthesized)); + expectQcoModulesEquivalent(expected, synthesized); + } +} + +// --- Two-qubit window fusion (pass internals) --- + +TEST_F(FuseTwoQubitUnitaryRunsPassTest, InvalidNativeGatesFailsPass) { + auto module = mlir::qc::QCProgramBuilder::build(context.get(), fusionCxCx); + ASSERT_TRUE(module); + runQcToQco(module); + PassManager pm(module->getContext()); + pm.addPass(createFuseTwoQubitUnitaryRuns(FuseTwoQubitUnitaryRunsOptions{ + .nativeGates = "not-a-gate", + })); + EXPECT_TRUE(failed(pm.run(*module))); +} + +TEST_F(FuseTwoQubitUnitaryRunsPassTest, AdjacentCxCancel) { + expectTwoQFusePreservesUnitary(context.get(), fusionCxCx, "u,cx"); + + auto module = mlir::qc::QCProgramBuilder::build(context.get(), fusionCxCx); + ASSERT_TRUE(module); + runQcToQco(module); + runTwoQFuse(module, "u,cx"); + EXPECT_EQ(countCtrlOps(module), 0U); +} + +TEST_F(FuseTwoQubitUnitaryRunsPassTest, FusesCxThroughInterleavedOneQOps) { + expectTwoQFusePreservesUnitary(context.get(), fusionHCxInterleavedTCx, + "u,cx"); +} + +TEST_F(FuseTwoQubitUnitaryRunsPassTest, StopsAtDifferentPairBoundary) { + auto module = + mlir::qc::QCProgramBuilder::build(context.get(), fusionThreeLineCx); + ASSERT_TRUE(module); + runQcToQco(module); + runTwoQFuse(module, "u,cx"); + EXPECT_GE(countCtrlOps(module), 1U); +} + +TEST_F(FuseTwoQubitUnitaryRunsPassTest, DoesNotFuseAcrossBarrier) { + auto module = + mlir::qc::QCProgramBuilder::build(context.get(), fusionCxBarrierCx); + ASSERT_TRUE(module); + runQcToQco(module); + runTwoQFuse(module, "u,cx"); + EXPECT_EQ(countCtrlOps(module), 2U); +} + +TEST_F(FuseTwoQubitUnitaryRunsPassTest, HandlesSwappedWireOrder) { + expectTwoQFusePreservesUnitary(context.get(), fusionSwapCxPattern, "u,cx"); +} + +TEST_F(FuseTwoQubitUnitaryRunsPassTest, HandlesRzzBlock) { + expectTwoQFusePreservesUnitary(context.get(), fusionHRzzSRzz, + "x,sx,rz,rx,rzz,cz"); +} diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis.cpp deleted file mode 100644 index ffd45a37ea..0000000000 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_native_synthesis.cpp +++ /dev/null @@ -1,808 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "TestCaseUtils.h" -#include "mlir/Conversion/QCToQCO/QCToQCO.h" -#include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h" -#include "mlir/Dialect/QCO/IR/QCODialect.h" -#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" -#include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" -#include "mlir/Dialect/QCO/Transforms/Passes.h" -#include "mlir/Dialect/QCO/Utils/Matrix.h" -#include "qc_programs.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace mlir; -using namespace mlir::qco; -using namespace mlir::qco::decomposition; - -namespace mlir::qco::native_synth_test { - -using mqt::test::cxGate01; -using mqt::test::czGate; -using mqt::test::isEquivalentUpToGlobalPhase; - -[[nodiscard]] static std::optional -getUnitaryQubitOperand(mlir::qco::UnitaryOpInterface op, std::size_t index) { - if (index >= op.getNumQubits()) { - return std::nullopt; - } - mlir::Value v = op->getOperand(index); - if (!llvm::isa(v.getType())) { - return std::nullopt; - } - return v; -} - -[[nodiscard]] static std::optional -getUnitaryQubitResult(mlir::qco::UnitaryOpInterface op, std::size_t index) { - if (index >= op.getNumQubits()) { - return std::nullopt; - } - mlir::Value v = op->getResult(index); - if (!llvm::isa(v.getType())) { - return std::nullopt; - } - return v; -} - -[[nodiscard]] static bool -extractSingleQubitMatrix(mlir::qco::UnitaryOpInterface op, - mlir::qco::Matrix2x2& out) { - if (op.getUnitaryMatrix2x2(out)) { - return true; - } - mlir::qco::DynamicMatrix dynamic; - if (!op.getUnitaryMatrixDynamic(dynamic) || dynamic.rows() != 2 || - dynamic.cols() != 2) { - return false; - } - out = mlir::qco::Matrix2x2::fromElements(dynamic(0, 0), dynamic(0, 1), - dynamic(1, 0), dynamic(1, 1)); - return true; -} - -[[nodiscard]] static bool -extractTwoQubitMatrix(mlir::qco::UnitaryOpInterface op, - mlir::qco::Matrix4x4& out) { - if (auto ctrl = llvm::dyn_cast(op.getOperation())) { - if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { - return false; - } - auto* body = ctrl.getBodyUnitary(0).getOperation(); - if (llvm::isa(body)) { - out = cxGate01(); - return true; - } - if (llvm::isa(body)) { - out = czGate(); - return true; - } - return false; - } - return op.getUnitaryMatrix4x4(out); -} - -[[nodiscard]] static std::optional -computeUnitaryFromModule(const mlir::OwningOpRef& moduleOp) { - mlir::ModuleOp module = moduleOp.get(); - if (!module) { - return std::nullopt; - } - - llvm::DenseMap qubitIds; - std::size_t nextQubitId = 0; - std::size_t numQubits = 0; - - for (auto func : module.getOps()) { - for (auto& block : func.getBlocks()) { - for (auto& rawOp : block.getOperations()) { - if (auto staticOp = llvm::dyn_cast(&rawOp)) { - const auto index = static_cast(staticOp.getIndex()); - qubitIds.try_emplace(staticOp.getQubit(), index); - numQubits = std::max(numQubits, index + 1); - } else if (auto alloc = llvm::dyn_cast(&rawOp)) { - qubitIds.try_emplace(alloc.getResult(), nextQubitId++); - numQubits = std::max(numQubits, nextQubitId); - } - } - } - } - - if (numQubits == 0) { - return std::nullopt; - } - - mlir::qco::DynamicMatrix unitary = mlir::qco::DynamicMatrix::identity( - static_cast(1ULL << numQubits)); - - auto getQubitId = [&](mlir::Value qubit) -> std::optional { - auto it = qubitIds.find(qubit); - if (it == qubitIds.end()) { - return std::nullopt; - } - return it->second; - }; - - for (auto func : module.getOps()) { - for (auto& block : func.getBlocks()) { - for (auto& rawOp : block.getOperations()) { - auto op = llvm::dyn_cast(&rawOp); - if (!op) { - continue; - } - if (llvm::isa( - op.getOperation())) { - continue; - } - - if (op.isSingleQubit()) { - const auto qIn = getUnitaryQubitOperand(op, 0); - if (!qIn) { - return std::nullopt; - } - auto qid = getQubitId(*qIn); - if (!qid) { - return std::nullopt; - } - mlir::qco::Matrix2x2 oneQ; - if (!extractSingleQubitMatrix(op, oneQ)) { - return std::nullopt; - } - unitary = mlir::qco::embedSingleQubitInNqubit(oneQ, numQubits, *qid) * - unitary; - const auto qOut = getUnitaryQubitResult(op, 0); - if (!qOut) { - return std::nullopt; - } - qubitIds[*qOut] = *qid; - continue; - } - - if (op.isTwoQubit()) { - const auto q0In = getUnitaryQubitOperand(op, 0); - const auto q1In = getUnitaryQubitOperand(op, 1); - if (!q0In || !q1In) { - return std::nullopt; - } - auto q0id = getQubitId(*q0In); - auto q1id = getQubitId(*q1In); - if (!q0id || !q1id) { - return std::nullopt; - } - mlir::qco::Matrix4x4 twoQ; - if (!extractTwoQubitMatrix(op, twoQ)) { - return std::nullopt; - } - unitary = - mlir::qco::embedTwoQubitInNqubit(twoQ, numQubits, *q0id, *q1id) * - unitary; - const auto q0Out = getUnitaryQubitResult(op, 0); - const auto q1Out = getUnitaryQubitResult(op, 1); - if (!q0Out || !q1Out) { - return std::nullopt; - } - qubitIds[*q0Out] = *q0id; - qubitIds[*q1Out] = *q1id; - continue; - } - - return std::nullopt; - } - } - } - - return unitary; -} - -static void -expectQcoModulesEquivalent(const mlir::OwningOpRef& lhs, - const mlir::OwningOpRef& rhs) { - const auto lhsUnitary = computeUnitaryFromModule(lhs); - ASSERT_TRUE(lhsUnitary.has_value()); - const auto rhsUnitary = computeUnitaryFromModule(rhs); - ASSERT_TRUE(rhsUnitary.has_value()); - EXPECT_TRUE(isEquivalentUpToGlobalPhase(*lhsUnitary, *rhsUnitary)); -} - -/// One row of the standard multi-profile equivalence sweeps in tests. -// NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_F` at global scope -struct NativeSynthesisProfileSweepCase { - const char* nativeGates; - bool (*isNative)(mlir::OwningOpRef&); -}; - -/// Shared gtest fixture for native-gate synthesis pass tests. -// NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_F` at global scope -class NativeSynthesisPassTest : public testing::Test { -protected: - void SetUp() override { - mlir::DialectRegistry registry; - registry.insert(); - context = std::make_unique(); - context->appendDialectRegistry(registry); - context->loadAllAvailableDialects(); - } - - template - static bool onlyTheseOps(mlir::OwningOpRef& moduleOp, - const bool allowCx, const bool allowCz) { - bool ok = true; - std::ignore = moduleOp->walk([&](mlir::qco::UnitaryOpInterface op) { - mlir::Operation* raw = op.getOperation(); - if (llvm::isa_and_present(raw->getParentOp())) { - return mlir::WalkResult::advance(); - } - if (llvm::isa(raw)) { - return mlir::WalkResult::advance(); - } - if (auto ctrl = llvm::dyn_cast(raw)) { - if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { - ok = false; - return mlir::WalkResult::interrupt(); - } - mlir::Operation* body = ctrl.getBodyUnitary(0).getOperation(); - const bool isCx = llvm::isa(body); - const bool isCz = llvm::isa(body); - if ((isCx && allowCx) || (isCz && allowCz)) { - return mlir::WalkResult::advance(); - } - ok = false; - return mlir::WalkResult::interrupt(); - } - - if (!llvm::isa(raw)) { - ok = false; - return mlir::WalkResult::interrupt(); - } - return mlir::WalkResult::advance(); - }); - return ok; - } - - static bool onlyIbmBasicCxOps(mlir::OwningOpRef& moduleOp) { - return onlyTheseOps(moduleOp, /*allowCx=*/true, - /*allowCz=*/false); - } - - static bool onlyIbmBasicCzOps(mlir::OwningOpRef& moduleOp) { - return onlyTheseOps(moduleOp, /*allowCx=*/false, - /*allowCz=*/true); - } - - static bool onlyGenericU3CxOps(mlir::OwningOpRef& moduleOp) { - return onlyTheseOps(moduleOp, /*allowCx=*/true, - /*allowCz=*/false); - } - - static bool onlyGenericU3CzOps(mlir::OwningOpRef& moduleOp) { - return onlyTheseOps(moduleOp, /*allowCx=*/false, - /*allowCz=*/true); - } - - static bool onlyIqmDefaultOps(mlir::OwningOpRef& moduleOp) { - return onlyTheseOps(moduleOp, /*allowCx=*/false, - /*allowCz=*/true); - } - - static bool - onlyIbmFractionalOps(mlir::OwningOpRef& moduleOp) { - return onlyTheseOps( - moduleOp, /*allowCx=*/false, /*allowCz=*/true); - } - - static bool - onlyAxisPairRxRzCxOps(mlir::OwningOpRef& moduleOp) { - return onlyTheseOps( - moduleOp, /*allowCx=*/true, /*allowCz=*/false); - } - - static bool - onlyAxisPairRxRyCxOps(mlir::OwningOpRef& moduleOp) { - return onlyTheseOps( - moduleOp, /*allowCx=*/true, /*allowCz=*/false); - } - - static bool - onlyAxisPairRyRzCzOps(mlir::OwningOpRef& moduleOp) { - return onlyTheseOps( - moduleOp, /*allowCx=*/false, /*allowCz=*/true); - } - - static bool - onlyUOrAxisPairRxRzCxOps(mlir::OwningOpRef& moduleOp) { - return onlyTheseOps(moduleOp, /*allowCx=*/true, - /*allowCz=*/false); - } - - static bool - onlyGenericU3CxOrCzOps(mlir::OwningOpRef& moduleOp) { - return onlyTheseOps(moduleOp, /*allowCx=*/true, - /*allowCz=*/true); - } - - static std::array - coreEquivalenceProfiles() { - return {{{.nativeGates = "x,sx,rz,cx", .isNative = &onlyIbmBasicCxOps}, - {.nativeGates = "u,cx", .isNative = &onlyGenericU3CxOps}, - {.nativeGates = "r,cz", .isNative = &onlyIqmDefaultOps}}}; - } - - static void runNativeSynthesis(mlir::OwningOpRef& moduleOp, - const std::string& nativeGates) { - mlir::PassManager pm(moduleOp->getContext()); - pm.addPass(mlir::createQCToQCO()); - pm.addPass(mlir::qco::createFuseTwoQubitUnitaryRuns( - mlir::qco::FuseTwoQubitUnitaryRunsOptions{ - .nativeGates = nativeGates, - })); - ASSERT_TRUE(mlir::succeeded(pm.run(*moduleOp))); - } - - static void runQcToQco(mlir::OwningOpRef& moduleOp) { - mlir::PassManager pm(moduleOp->getContext()); - pm.addPass(mlir::createQCToQCO()); - ASSERT_TRUE(mlir::succeeded(pm.run(*moduleOp))); - } - - static std::string - moduleToString(const mlir::OwningOpRef& moduleOp) { - std::string text; - llvm::raw_string_ostream os(text); - moduleOp.get()->print(os); - return text; - } - - template - void expectNativeAfterSynthesis(BuildFn buildFn, - const std::string& nativeGates, - PredicateFn isNative) { - auto moduleOp = buildFn(); - runNativeSynthesis(moduleOp, nativeGates); - EXPECT_TRUE(isNative(moduleOp)); - } - - template - void expectSynthesisFailure(BuildFn buildFn, const std::string& nativeGates) { - auto moduleOp = buildFn(); - mlir::PassManager pm(moduleOp->getContext()); - pm.addPass(mlir::createQCToQCO()); - pm.addPass(mlir::qco::createFuseTwoQubitUnitaryRuns( - mlir::qco::FuseTwoQubitUnitaryRunsOptions{ - .nativeGates = nativeGates, - })); - EXPECT_TRUE(mlir::failed(pm.run(*moduleOp))); - } - - template - void expectEquivalentAndNativeAfterSynthesis(BuildFn buildFn, - const std::string& nativeGates, - PredicateFn isNative) { - auto expectedModule = buildFn(); - runQcToQco(expectedModule); - - auto synthesizedModule = buildFn(); - runNativeSynthesis(synthesizedModule, nativeGates); - EXPECT_TRUE(isNative(synthesizedModule)); - expectQcoModulesEquivalent(expectedModule, synthesizedModule); - } - - std::unique_ptr context; -}; - -} // namespace mlir::qco::native_synth_test - -using namespace mlir::qco::native_synth_test; - -struct NativeSynthMenuRow { - const char* name; - const char* nativeGates; - bool (*isNative)(OwningOpRef&); -}; - -// --- Inline circuit builders --- - -static void broadOneQThenCz(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.x(q0); - b.y(q1); - b.h(q0); - b.sx(q1); - b.rx(0.13, q0); - b.ry(-0.47, q1); - b.rz(0.29, q0); - b.cz(q0, q1); -} - -static void zeroAngleThenCz(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.rx(0.0, q0); - b.ry(0.0, q1); - b.rz(0.0, q0); - b.p(0.0, q1); - b.cz(q0, q1); -} - -static void ibmFractionalGateFamilies(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.h(q0); - b.rx(0.13, q1); - b.cx(q0, q1); - b.cz(q1, q0); - b.swap(q0, q1); - b.rzz(-0.33, q0, q1); - b.rzx(0.41, q0, q1); -} - -static void hstycxTwoQ(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.h(q0); - b.s(q0); - b.t(q0); - b.y(q0); - b.cx(q0, q1); -} - -static void cxYOnQ1(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.cx(q0, q1); - b.y(q1); -} - -static void hCxTOnQ1(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.h(q1); - b.cx(q0, q1); - b.t(q1); -} - -static void xYSXCz(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.x(q0); - b.y(q0); - b.sx(q0); - b.cz(q0, q1); -} - -static void hYCx(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.h(q0); - b.y(q0); - b.cx(q0, q1); -} - -static void zCx(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.z(q0); - b.cx(q0, q1); -} - -static void xHCz(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.x(q0); - b.h(q0); - b.cz(q0, q1); -} - -static void hq0Yq1CxSq0(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.h(q0); - b.y(q1); - b.cx(q0, q1); - b.s(q0); -} - -static void hCxSq1(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.h(q0); - b.cx(q0, q1); - b.s(q1); -} - -static void threeQGhz(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - const auto q2 = b.allocQubit(); - b.h(q0); - b.cx(q0, q1); - b.cx(q1, q2); -} - -static void determinismSwap(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.swap(q0, q1); - b.dealloc(q0); - b.dealloc(q1); -} - -// --- NativeSpec / NativePolicy --- - -TEST(NativeSpecTest, ResolveIbmBasicCx) { - const auto spec = decomposition::parseNativeSpec("x,sx,rz,cx"); - ASSERT_TRUE(spec); - EXPECT_TRUE(spec->allowedGates.contains(decomposition::NativeGateKind::Cx)); - EXPECT_TRUE(spec->allowedGates.contains(decomposition::NativeGateKind::X)); - EXPECT_FALSE(spec->allowRzz); -} - -TEST(NativeSpecTest, ResolveRejectsUnknownToken) { - EXPECT_FALSE( - decomposition::parseNativeSpec("x,sx,rz,not-a-gate").has_value()); -} - -TEST(NativeSpecTest, PhaseAliasPMatchesRzInIbmStyleMenu) { - const auto pMenu = decomposition::parseNativeSpec("x,sx,p,cx"); - const auto rzMenu = decomposition::parseNativeSpec("x,sx,rz,cx"); - ASSERT_TRUE(pMenu); - ASSERT_TRUE(rzMenu); - EXPECT_EQ(pMenu->allowedGates, rzMenu->allowedGates); -} - -TEST(NativePolicyTest, UsesCxAndCzFromResolvedSpec) { - const auto cxOnly = decomposition::parseNativeSpec("u,cx"); - ASSERT_TRUE(cxOnly); - EXPECT_TRUE(llvm::is_contained(cxOnly->entanglerBases, - decomposition::EntanglerBasis::Cx)); - EXPECT_FALSE(llvm::is_contained(cxOnly->entanglerBases, - decomposition::EntanglerBasis::Cz)); - - const auto both = decomposition::parseNativeSpec("u,cx,cz"); - ASSERT_TRUE(both); - EXPECT_TRUE(llvm::is_contained(both->entanglerBases, - decomposition::EntanglerBasis::Cx)); - EXPECT_TRUE(llvm::is_contained(both->entanglerBases, - decomposition::EntanglerBasis::Cz)); -} - -// NOLINTNEXTLINE(misc-use-internal-linkage) -class NativePolicyAllowsOpTest : public ::testing::Test { -protected: - MLIRContext context; - QCOProgramBuilder builder{&context}; - - void SetUp() override { - context.loadDialect(); - context.loadDialect(); - context.loadDialect(); - context.loadDialect(); - builder.initialize(); - } -}; - -TEST_F(NativePolicyAllowsOpTest, RejectsSingleQubitOpNotInMenu) { - const auto spec = decomposition::parseNativeSpec("u,cx"); - ASSERT_TRUE(spec); - Value q = builder.staticQubit(0); - q = builder.x(q); - auto mod = builder.finalize(); - ASSERT_TRUE(mod); - XOp xop; - mod->walk([&](XOp op) { - xop = op; - return WalkResult::interrupt(); - }); - ASSERT_TRUE(xop); -} - -// --- Pass profile coverage --- - -// NOLINTNEXTLINE(misc-use-internal-linkage) -class NativeSynthesisSwapProfileTest - : public NativeSynthesisPassTest, - public testing::WithParamInterface { -public: - using NativeSynthesisPassTest::onlyGenericU3CxOps; - using NativeSynthesisPassTest::onlyIbmBasicCxOps; - using NativeSynthesisPassTest::onlyIqmDefaultOps; -}; - -TEST_P(NativeSynthesisSwapProfileTest, DecomposesSwapToProfile) { - const NativeSynthMenuRow& param = GetParam(); - expectNativeAfterSynthesis( - [&] { - return mlir::qc::QCProgramBuilder::build(context.get(), mlir::qc::swap); - }, - param.nativeGates, param.isNative); -} - -INSTANTIATE_TEST_SUITE_P( - SwapMenuMatrix, NativeSynthesisSwapProfileTest, - testing::Values( - NativeSynthMenuRow{"IbmBasicCx", "x,sx,rz,cx", - &NativeSynthesisSwapProfileTest::onlyIbmBasicCxOps}, - NativeSynthMenuRow{"GenericU3Cx", "u,cx", - &NativeSynthesisSwapProfileTest::onlyGenericU3CxOps}, - NativeSynthMenuRow{"IqmDefault", "r,cz", - &NativeSynthesisSwapProfileTest::onlyIqmDefaultOps}), - [](const testing::TestParamInfo& info) { - return info.param.name; - }); - -TEST_F(NativeSynthesisPassTest, DecomposesHstycxToIbmBasicCx) { - expectNativeAfterSynthesis( - [&] { - return mlir::qc::QCProgramBuilder::build(context.get(), hstycxTwoQ); - }, - "x,sx,rz,cx", &NativeSynthesisPassTest::onlyIbmBasicCxOps); -} - -TEST_F(NativeSynthesisPassTest, DecomposesCxYOnQ1ToIqmDefault) { - expectNativeAfterSynthesis( - [&] { return mlir::qc::QCProgramBuilder::build(context.get(), cxYOnQ1); }, - "r,cz", &NativeSynthesisPassTest::onlyIqmDefaultOps); -} - -TEST_F(NativeSynthesisPassTest, BroadOneQCanonicalizationOnIqmDefault) { - auto moduleOp = - mlir::qc::QCProgramBuilder::build(context.get(), broadOneQThenCz); - runNativeSynthesis(moduleOp, "r,cz"); - EXPECT_TRUE(onlyIqmDefaultOps(moduleOp)); -} - -TEST_F(NativeSynthesisPassTest, ZeroAngleCanonicalizationOnRyRzCz) { - auto moduleOp = - mlir::qc::QCProgramBuilder::build(context.get(), zeroAngleThenCz); - runNativeSynthesis(moduleOp, "ry,rz,cz"); - EXPECT_TRUE(onlyAxisPairRyRzCzOps(moduleOp)); -} - -TEST_F(NativeSynthesisPassTest, DecomposesCxToCzForIbmBasicCzProfile) { - expectNativeAfterSynthesis( - [&] { - return mlir::qc::QCProgramBuilder::build(context.get(), hCxTOnQ1); - }, - "x,sx,rz,cz", &NativeSynthesisPassTest::onlyIbmBasicCzOps); -} - -TEST_F(NativeSynthesisPassTest, DecomposesToIqmDefaultProfile) { - expectNativeAfterSynthesis( - [&] { return mlir::qc::QCProgramBuilder::build(context.get(), xYSXCz); }, - "r,cz", &NativeSynthesisPassTest::onlyIqmDefaultOps); -} - -TEST_F(NativeSynthesisPassTest, DecomposesToIbmFractionalProfile) { - expectNativeAfterSynthesis( - [&] { - return mlir::qc::QCProgramBuilder::build(context.get(), - ibmFractionalGateFamilies); - }, - "x,sx,rz,rx,rzz,cz", &NativeSynthesisPassTest::onlyIbmFractionalOps); -} - -TEST_F(NativeSynthesisPassTest, DecomposesToAxisPairRxRzCxProfile) { - expectNativeAfterSynthesis( - [&] { return mlir::qc::QCProgramBuilder::build(context.get(), hYCx); }, - "rx,rz,cx", &NativeSynthesisPassTest::onlyAxisPairRxRzCxOps); -} - -TEST_F(NativeSynthesisPassTest, DecomposesRzToAxisPairRxRyCxProfile) { - expectNativeAfterSynthesis( - [&] { return mlir::qc::QCProgramBuilder::build(context.get(), zCx); }, - "rx,ry,cx", &NativeSynthesisPassTest::onlyAxisPairRxRyCxOps); -} - -TEST_F(NativeSynthesisPassTest, GenericProfileMatchesGenericU3CxBehavior) { - expectEquivalentAndNativeAfterSynthesis( - [&] { - return mlir::qc::QCProgramBuilder::build(context.get(), hq0Yq1CxSq0); - }, - "u,cx", &NativeSynthesisPassTest::onlyGenericU3CxOps); -} - -TEST_F(NativeSynthesisPassTest, GenericProfileMatchesAxisPairRyRzCzBehavior) { - expectEquivalentAndNativeAfterSynthesis( - [&] { return mlir::qc::QCProgramBuilder::build(context.get(), xHCz); }, - "ry,rz,cz", &NativeSynthesisPassTest::onlyAxisPairRyRzCzOps); -} - -TEST_F(NativeSynthesisPassTest, CustomProfileAcceptsMultipleEntanglersMenu) { - expectEquivalentAndNativeAfterSynthesis( - [&] { return mlir::qc::QCProgramBuilder::build(context.get(), hCxSq1); }, - "u,cx,cz", &NativeSynthesisPassTest::onlyGenericU3CxOrCzOps); -} - -TEST_F(NativeSynthesisPassTest, FailsForUnsupportedNativeGateMenu) { - expectSynthesisFailure( - [&] { - return mlir::qc::QCProgramBuilder::build(context.get(), mlir::qc::h); - }, - "not-a-gate"); -} - -TEST_F(NativeSynthesisPassTest, FailsForNativeGateMenuWithoutSingleQEmitter) { - expectSynthesisFailure( - [&] { - return mlir::qc::QCProgramBuilder::build(context.get(), - mlir::qc::singleControlledX); - }, - "cx,cz"); -} - -TEST_F(NativeSynthesisPassTest, FailsForMultiControlledGateStructure) { - expectSynthesisFailure( - [&] { - return mlir::qc::QCProgramBuilder::build(context.get(), - mlir::qc::multipleControlledX); - }, - "x,sx,rz,cx"); -} - -TEST_F(NativeSynthesisPassTest, CandidateSelectionIsDeterministicAcrossRuns) { - auto buildFn = [&] { - return mlir::qc::QCProgramBuilder::build(context.get(), determinismSwap); - }; - auto firstModule = buildFn(); - runNativeSynthesis(firstModule, "u,cx"); - auto secondModule = buildFn(); - runNativeSynthesis(secondModule, "u,cx"); - EXPECT_EQ(moduleToString(firstModule), moduleToString(secondModule)); -} - -TEST_F(NativeSynthesisPassTest, ThreeQubitGhzEquivalentOnCoreProfiles) { - for (const auto& profileCase : coreEquivalenceProfiles()) { - auto expected = mlir::qc::QCProgramBuilder::build(context.get(), threeQGhz); - runQcToQco(expected); - - auto synthesized = - mlir::qc::QCProgramBuilder::build(context.get(), threeQGhz); - runNativeSynthesis(synthesized, profileCase.nativeGates); - EXPECT_TRUE(profileCase.isNative(synthesized)); - expectQcoModulesEquivalent(expected, synthesized); - } -} From b8cd39527c8d7dc2cbb2d7e5fd323eb43399bf7e Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Sun, 21 Jun 2026 13:12:46 +0200 Subject: [PATCH 052/122] =?UTF-8?q?=F0=9F=8E=A8=20Refactor=20and=20streaml?= =?UTF-8?q?ine=20native=20gate=20profile=20and=20synthesis=20for=20two-qub?= =?UTF-8?q?it=20unitary=20operations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Transforms/Decomposition/NativeProfile.h | 173 +++ .../QCO/Transforms/Decomposition/Weyl.h | 365 +----- .../mlir/Dialect/QCO/Transforms/Passes.td | 28 +- mlir/include/mlir/Dialect/QCO/Utils/Matrix.h | 54 +- .../Decomposition/NativeProfile.cpp | 431 +++++++ .../QCO/Transforms/Decomposition/Weyl.cpp | 1046 +++++------------ .../FuseTwoQubitUnitaryRuns.cpp | 689 ++++------- mlir/lib/Dialect/QCO/Utils/Matrix.cpp | 112 ++ .../Compiler/test_compiler_pipeline.cpp | 43 +- .../test_euler_decomposition.cpp | 13 - .../Decomposition/test_weyl_decomposition.cpp | 685 +++++------ .../test_fuse_two_qubit_unitary_runs.cpp | 749 ++++-------- .../Dialect/QCO/Utils/test_unitary_matrix.cpp | 10 + mlir/unittests/TestCaseUtils.h | 178 --- 14 files changed, 1897 insertions(+), 2679 deletions(-) create mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeProfile.h create mode 100644 mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeProfile.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeProfile.h new file mode 100644 index 0000000000..f9abed48d7 --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeProfile.h @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace mlir::qco::decomposition { + +/** + * @brief Native gate kinds that may appear in a two-qubit synthesis menu. + */ +enum class NativeGateKind : std::uint8_t { + U, + X, + Sx, + Rz, + Rx, + Ry, + R, + Cx, + Cz, + Rzz, +}; + +/** + * @brief Single-qubit emission strategy resolved from a native-gate menu. + */ +enum class SingleQubitMode : std::uint8_t { + ZSXX, ///< `RZ` / `SX` / `X` via ZYZ decomposition. + U3, ///< Generic `U(theta, phi, lambda)`. + R, ///< `R(theta, phi)` chain (`Rx`/`Ry` as `R`). + AxisPair, ///< Two fixed rotation axes (see @ref AxisPair). +}; + +/** + * @brief Rotation-axis pair for @ref SingleQubitMode::AxisPair emitters. + */ +enum class AxisPair : std::uint8_t { + RxRz, + RxRy, + RyRz, +}; + +/** + * @brief Entangling basis gate for two-qubit Weyl synthesis. + */ +enum class EntanglerBasis : std::uint8_t { + None, + Cx, + Cz, +}; + +struct SingleQubitEmitterSpec { + SingleQubitMode mode = SingleQubitMode::U3; + AxisPair axisPair = AxisPair::RxRz; + bool supportsDirectRx = false; +}; + +/** + * @brief Resolved native-gate menu for two-qubit Weyl synthesis. + */ +struct NativeProfileSpec { + bool allowRzz = false; + llvm::DenseSet allowedGates; + llvm::SmallVector singleQubitEmitters; + llvm::SmallVector entanglerBases; +}; + +/** + * @brief Euler basis used to emit single-qubit factors for @p emitter. + */ +[[nodiscard]] EulerBasis +emitterEulerBasis(const SingleQubitEmitterSpec& emitter); + +/** + * @brief Parses a comma-separated native-gate menu (e.g. `"u,cx,rzz"`). + */ +[[nodiscard]] std::optional +parseNativeSpec(StringRef nativeGates); + +/** + * @brief Synthesizes a composed two-qubit unitary as gates in @p spec. + */ +[[nodiscard]] LogicalResult +synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, Value qubit0, + Value qubit1, const Matrix4x4& target, + const NativeProfileSpec& spec, Value& outQubit0, + Value& outQubit1); + +/** + * @brief Number of entangling basis gates required to synthesize @p target. + * + * @return Entangler count for @p spec, or `std::nullopt` if synthesis fails. + */ +[[nodiscard]] std::optional +twoQubitEntanglerCount(const Matrix4x4& target, const NativeProfileSpec& spec); + +/** + * @brief Maps a compile-time single-qubit op to its native-menu gate kind. + * + * Returns `std::nullopt` for non-primitive or unsupported ops. + */ +[[nodiscard]] std::optional +nativeGateKindFor(UnitaryOpInterface op); + +/** + * @brief Returns true when @p op is already on the resolved single-qubit menu. + * + * `BarrierOp` and `GPhaseOp` are always allowed. + */ +[[nodiscard]] bool allowsSingleQubitOp(UnitaryOpInterface op, + const NativeProfileSpec& spec); + +/** + * @brief Entangler basis for a 1-control, 1-target `CtrlOp` with `X`/`Z` body. + */ +[[nodiscard]] std::optional +entanglerBasisForSingleTargetCtrl(CtrlOp ctrl); + +/** @brief Returns true when @p spec lists @p basis as an entangler. */ +[[nodiscard]] bool profileAllowsEntangler(const NativeProfileSpec& spec, + EntanglerBasis basis); + +/** + * @brief Returns true when a 1-control, 1-target `CtrlOp` is on @p spec. + */ +[[nodiscard]] bool allowsSingleTargetCtrl(CtrlOp ctrl, + const NativeProfileSpec& spec); + +/** + * @brief Returns true when a bare two-qubit op (currently `RZZ`) is on @p spec. + */ +[[nodiscard]] bool allowsBareTwoQubitOp(Operation* op, + const NativeProfileSpec& spec); + +/** + * @brief Returns true when @p op is a native two-qubit gate under @p spec. + */ +[[nodiscard]] bool allowsTwoQubitOp(Operation* op, + const NativeProfileSpec& spec); + +/** + * @brief Fills @p matrix with the 4x4 unitary for a two-qubit block op. + * + * Handles single-target `CtrlOp` shells and generic two-qubit unitaries. + * Returns false for barriers, global phase, and unsupported shapes. + */ +[[nodiscard]] bool assignTwoQubitOpMatrix(Operation* op, Matrix4x4& matrix); + +} // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h index 1377e7c172..503e2d2fa1 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h @@ -10,90 +10,17 @@ #pragma once -#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" -#include #include -#include -#include -#include -#include -#include #include #include #include #include -#include -#include namespace mlir::qco::decomposition { -/** - * @brief Native gate kinds that may appear in a two-qubit synthesis menu. - */ -enum class NativeGateKind : std::uint8_t { - U, - X, - Sx, - Rz, - Rx, - Ry, - R, - Cx, - Cz, - Rzz, -}; - -/** - * @brief Single-qubit emission strategy resolved from a native-gate menu. - */ -enum class SingleQubitMode : std::uint8_t { - ZSXX, ///< `RZ` / `SX` / `X` via ZYZ decomposition. - U3, ///< Generic `U(theta, phi, lambda)`. - R, ///< `R(theta, phi)` chain (`Rx`/`Ry` as `R`). - AxisPair, ///< Two fixed rotation axes (see @ref AxisPair). -}; - -/** - * @brief Rotation-axis pair for @ref SingleQubitMode::AxisPair emitters. - */ -enum class AxisPair : std::uint8_t { - RxRz, - RxRy, - RyRz, -}; - -/** - * @brief Entangling basis gate for two-qubit Weyl synthesis. - */ -enum class EntanglerBasis : std::uint8_t { - None, - Cx, - Cz, -}; - -/// Resolved single-qubit emitter entry in a @ref NativeProfileSpec. -struct SingleQubitEmitterSpec { - SingleQubitMode mode = SingleQubitMode::U3; - AxisPair axisPair = AxisPair::RxRz; - bool supportsDirectRx = false; -}; - -/** - * @brief Fully resolved native-gate menu for two-qubit Weyl synthesis. - * - * Produced by @ref parseNativeSpec from a comma-separated gate list such as - * `"x,sx,rz,cx"`. - */ -struct NativeProfileSpec { - bool allowRzz = false; - llvm::DenseSet allowedGates; - llvm::SmallVector singleQubitEmitters; - llvm::SmallVector entanglerBases; -}; - /** * @brief Weyl decomposition of a 2-qubit unitary matrix (4x4). * @@ -115,50 +42,24 @@ struct NativeProfileSpec { */ class TwoQubitWeylDecomposition { public: - /** - * @brief Create Weyl decomposition. - * - * @param unitaryMatrix Matrix of the two-qubit operation/series to be - * decomposed. - * @param fidelity Tolerance to assume a specialization which is used to - * reduce the number of parameters required by the canonical - * gate and thus potentially decreasing the number of basis - * gates. - */ static TwoQubitWeylDecomposition create(const Matrix4x4& unitaryMatrix, std::optional fidelity); ~TwoQubitWeylDecomposition() = default; + TwoQubitWeylDecomposition() = default; TwoQubitWeylDecomposition(const TwoQubitWeylDecomposition&) = default; TwoQubitWeylDecomposition(TwoQubitWeylDecomposition&&) = default; TwoQubitWeylDecomposition& operator=(const TwoQubitWeylDecomposition&) = default; TwoQubitWeylDecomposition& operator=(TwoQubitWeylDecomposition&&) = default; - /** @brief Matrix of the canonical gate from its parameters `a`, `b`, `c`. */ [[nodiscard]] Matrix4x4 getCanonicalMatrix() const { return getCanonicalMatrix(a_, b_, c_); } - /** - * @brief First parameter of the canonical gate. - * - * @note Multiply by `-2.0` for the `RXX` rotation angle. - */ [[nodiscard]] double a() const { return a_; } - /** - * @brief Second parameter of the canonical gate. - * - * @note Multiply by `-2.0` for the `RYY` rotation angle. - */ [[nodiscard]] double b() const { return b_; } - /** - * @brief Third parameter of the canonical gate. - * - * @note Multiply by `-2.0` for the `RZZ` rotation angle. - */ [[nodiscard]] double c() const { return c_; } - /** @brief Global phase adjustment after applying the decomposition. */ [[nodiscard]] double globalPhase() const { return globalPhase_; } /** @@ -202,52 +103,12 @@ class TwoQubitWeylDecomposition { */ [[nodiscard]] const Matrix2x2& k2r() const { return k2r_; } - /** @brief Canonical gate matrix for parameters `a`, `b`, `c`. */ [[nodiscard]] static Matrix4x4 getCanonicalMatrix(double a, double b, double c); -protected: - enum class Specialization : std::uint8_t { - General, - IdEquiv, - SWAPEquiv, - PartialSWAPEquiv, - PartialSWAPFlipEquiv, - ControlledEquiv, - MirrorControlledEquiv, - FSimaabEquiv, - FSimabbEquiv, - FSimabmbEquiv, - }; - - enum class MagicBasisTransform : std::uint8_t { - Into, - OutOf, - }; - - static constexpr auto DIAGONALIZATION_PRECISION = 1e-13; - - TwoQubitWeylDecomposition() = default; - - [[nodiscard]] static Matrix4x4 - magicBasisTransform(const Matrix4x4& unitary, MagicBasisTransform direction); - - [[nodiscard]] static double closestPartialSwap(double a, double b, double c); - - [[nodiscard]] static std::pair> - diagonalizeComplexSymmetric(const Matrix4x4& m, double precision); - - static std::tuple - decomposeTwoQubitProductGate(const Matrix4x4& specialUnitary); - - [[nodiscard]] static std::complex - getTrace(double a, double b, double c, double ap, double bp, double cp); - - [[nodiscard]] Specialization bestSpecialization() const; - +private: bool applySpecialization(); -private: double a_{}; double b_{}; double c_{}; @@ -256,7 +117,7 @@ class TwoQubitWeylDecomposition { Matrix2x2 k2l_; Matrix2x2 k1r_; Matrix2x2 k2r_; - Specialization specialization{Specialization::General}; + std::uint8_t specializationKind_{0}; std::optional requestedFidelity; }; @@ -274,11 +135,8 @@ using TwoQubitLocalUnitaryList = llvm::SmallVector; * `2 * (numBasisUses + 1)`. */ struct TwoQubitNativeDecomposition { - /// Number of basis-gate (entangler) uses. std::uint8_t numBasisUses = 0; - /// Single-qubit factors in emission order (see struct comment). TwoQubitLocalUnitaryList singleQubitFactors; - /// Residual global phase (radians) not represented by factors/entanglers. double globalPhase = 0.0; }; @@ -300,214 +158,51 @@ struct TwoQubitNativeDecomposition { */ class TwoQubitBasisDecomposer { public: - /** - * @brief Create decomposer for the specified entangler matrix. - * - * The entangler appears between 0 and 3 times in each decomposition. The 4x4 - * matrix must be in MQT operand order (qubit 0 = MSB). - */ [[nodiscard]] static TwoQubitBasisDecomposer create(const Matrix4x4& basisMatrix, double basisFidelity); - /** - * @brief Perform decomposition using this decomposer's basis gate. - * - * @param targetDecomposition Prepared Weyl decomposition of the unitary to - * decompose. - * @param numBasisGateUses Force a given number of basis gates; when unset, - * the optimal count is selected from the - * Hilbert-Schmidt traces. - * @return Single-qubit factors and entangler count, or `std::nullopt` when - * more than one basis gate would be required but the basis gate is - * not super-controlled. - */ [[nodiscard]] std::optional twoQubitDecompose(const TwoQubitWeylDecomposition& targetDecomposition, std::optional numBasisGateUses) const; -protected: - // NOLINTBEGIN(modernize-pass-by-value) - /** @brief Constructs decomposer instance. */ - TwoQubitBasisDecomposer( - double basisFidelity, const TwoQubitWeylDecomposition& basisDecomposer, - bool isSuperControlled, const Matrix2x2& u0l, const Matrix2x2& u0r, - const Matrix2x2& u1l, const Matrix2x2& u1ra, const Matrix2x2& u1rb, - const Matrix2x2& u2la, const Matrix2x2& u2lb, const Matrix2x2& u2ra, - const Matrix2x2& u2rb, const Matrix2x2& u3l, const Matrix2x2& u3r, - const Matrix2x2& q0l, const Matrix2x2& q0r, const Matrix2x2& q1la, - const Matrix2x2& q1lb, const Matrix2x2& q1ra, const Matrix2x2& q1rb, - const Matrix2x2& q2l, const Matrix2x2& q2r) - : 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) - - /** - * @brief Decomposition with 0 basis gates. - * - * Decompose target :math:`\sim U_d(x, y, z)` with 0 uses of the basis gate. - * Result :math:`U_r` has trace: - * - * .. math:: - * - * \Big\vert\text{Tr}(U_r\cdot U_\text{target}^{\dag})\Big\vert = - * 4\Big\vert (\cos(x)\cos(y)\cos(z)+ j \sin(x)\sin(y)\sin(z)\Big\vert - * - * which is optimal for all targets and bases. - */ - [[nodiscard]] static TwoQubitLocalUnitaryList - decomp0(const TwoQubitWeylDecomposition& target); +private: + struct SmbPrecomputed { + Matrix2x2 u0l; + Matrix2x2 u0r; + Matrix2x2 u1l; + Matrix2x2 u1ra; + Matrix2x2 u1rb; + Matrix2x2 u2la; + Matrix2x2 u2lb; + Matrix2x2 u2ra; + Matrix2x2 u2rb; + Matrix2x2 u3l; + Matrix2x2 u3r; + Matrix2x2 q0l; + Matrix2x2 q0r; + Matrix2x2 q1la; + Matrix2x2 q1lb; + Matrix2x2 q1ra; + Matrix2x2 q1rb; + Matrix2x2 q2l; + Matrix2x2 q2r; + }; - /** - * @brief Decomposition with 1 basis gate. - * - * Decompose target :math:`\sim U_d(x, y, z)` with 1 use of the basis gate - * :math:`\sim U_d(a, b, c)`. Result :math:`U_r` has trace: - * - * .. math:: - * - * \Big\vert\text{Tr}(U_r \cdot U_\text{target}^{\dag})\Big\vert = - * 4\Big\vert \cos(x-a)\cos(y-b)\cos(z-c) + j - * \sin(x-a)\sin(y-b)\sin(z-c)\Big\vert - * - * which is optimal for all targets and bases with ``z==0`` or ``c==0``. - */ + [[nodiscard]] TwoQubitLocalUnitaryList + decomp0(const TwoQubitWeylDecomposition& target) const; [[nodiscard]] TwoQubitLocalUnitaryList decomp1(const TwoQubitWeylDecomposition& target) const; - - /** - * @brief Decomposition with 2 basis gates. - * - * Decompose target :math:`\sim U_d(x, y, z)` with 2 uses of the basis gate. - * - * For supercontrolled basis :math:`\sim U_d(\pi/4, b, 0)`, all b, result - * :math:`U_r` has trace - * - * .. math:: - * - * \Big\vert\text{Tr}(U_r \cdot U_\text{target}^\dag) \Big\vert = - * 4\cos(z) - * - * which is the optimal approximation for basis of CNOT-class - * :math:`\sim U_d(\pi/4, 0, 0)` or DCNOT-class - * :math:`\sim U_d(\pi/4, \pi/4, 0)` and any target. It may be sub-optimal - * for :math:`b \neq 0` (i.e. there exists an exact decomposition for any - * target using :math:`B \sim U_d(\pi/4, \pi/8, 0)`, but it may not be this - * decomposition). This is an exact decomposition for supercontrolled basis - * and target :math:`\sim U_d(x, y, 0)`. No guarantees for - * non-supercontrolled basis. - */ [[nodiscard]] TwoQubitLocalUnitaryList decomp2Supercontrolled(const TwoQubitWeylDecomposition& target) const; - - /** - * @brief Decomposition with 3 basis gates. - * - * Exact decomposition for supercontrolled basis :math:`\sim U_d(\pi/4, b, - * 0)`, all b, and any target. No guarantees for non-supercontrolled basis. - */ [[nodiscard]] TwoQubitLocalUnitaryList decomp3Supercontrolled(const TwoQubitWeylDecomposition& target) const; - - /** - * @brief Traces for canonical-gate parameter combinations of target and - * basis. - * - * Used to determine the smallest number of basis gates needed for an - * equivalent canonical gate. - */ [[nodiscard]] std::array, 4> traces(const TwoQubitWeylDecomposition& target) const; - [[nodiscard]] static bool relativeEq(double lhs, double rhs, double epsilon, - double maxRelative); - -private: - double basisFidelity; + double basisFidelity{}; TwoQubitWeylDecomposition basisDecomposer; - bool isSuperControlled; - Matrix2x2 u0l; - Matrix2x2 u0r; - Matrix2x2 u1l; - Matrix2x2 u1ra; - Matrix2x2 u1rb; - Matrix2x2 u2la; - Matrix2x2 u2lb; - Matrix2x2 u2ra; - Matrix2x2 u2rb; - Matrix2x2 u3l; - Matrix2x2 u3r; - Matrix2x2 q0l; - Matrix2x2 q0r; - Matrix2x2 q1la; - Matrix2x2 q1lb; - Matrix2x2 q1ra; - Matrix2x2 q1rb; - Matrix2x2 q2l; - Matrix2x2 q2r; + bool isSuperControlled{}; + SmbPrecomputed smb{}; }; -/** - * @brief Euler basis used to emit single-qubit factors for @p emitter. - */ -[[nodiscard]] EulerBasis -emitterEulerBasis(const SingleQubitEmitterSpec& emitter); - -/** - * @brief Parses a comma-separated native-gate menu (e.g. `"u,cx,rzz"`). - * - * @param nativeGates Comma-separated gate names (case-insensitive). - * @return The resolved profile, or `std::nullopt` if any token is unknown or - * no legal single-qubit emitter can be derived. - */ -[[nodiscard]] std::optional -parseNativeSpec(StringRef nativeGates); - -/** - * @brief Synthesizes a composed two-qubit unitary as gates in @p spec. - * - * Emits single-qubit factors from the first resolved emitter in @p spec and - * entanglers from the preferred basis (`CX` over `CZ`). Returns `failure()` - * when @p spec has no entangler or the Weyl decomposition cannot be realized. - * - * @param builder Builder for the emitted operations. - * @param loc Location for the emitted operations. - * @param qubit0 First qubit (control wire for entanglers). - * @param qubit1 Second qubit (target wire for entanglers). - * @param target Composed unitary to synthesize (MQT operand order). - * @param spec Resolved native-gate menu. - * @param outQubit0 Output value for qubit 0 after synthesis. - * @param outQubit1 Output value for qubit 1 after synthesis. - * @return `success()` when gates were emitted, `failure()` otherwise. - */ -[[nodiscard]] LogicalResult -synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, Value qubit0, - Value qubit1, const Matrix4x4& target, - const NativeProfileSpec& spec, Value& outQubit0, - Value& outQubit1); - -/** - * @brief Number of entangling basis gates required to synthesize @p target. - * - * @return Entangler count for @p spec, or `std::nullopt` if synthesis fails. - */ -[[nodiscard]] std::optional -twoQubitEntanglerCount(const Matrix4x4& target, const NativeProfileSpec& spec); - -/// Common constant `1/sqrt(2)` used by gate-matrix factories. -inline constexpr double FRAC1_SQRT2 = - 0.707106781186547524400844362104849039284835937688474036588; - -/// Axis rotations `exp(-i theta/2 * sigma_{x,y,z})`. -[[nodiscard]] Matrix2x2 rxMatrix(double theta); -[[nodiscard]] Matrix2x2 ryMatrix(double theta); -[[nodiscard]] Matrix2x2 rzMatrix(double theta); -/// `i * sigma_z`; useful when factoring Pauli rotations out of a 2x2. -[[nodiscard]] const Matrix2x2& ipz(); -/// `i * sigma_y`. -[[nodiscard]] const Matrix2x2& ipy(); -/// `i * sigma_x`. -[[nodiscard]] const Matrix2x2& ipx(); } // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index f76f225278..b1c3bf2962 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -71,10 +71,9 @@ def FuseTwoQubitUnitaryRuns let dependentDialects = ["mlir::qco::QCODialect"]; let summary = "Lower QCO unitary gates to a user-specified native gate menu."; let description = [{ - This pass rewrites a module so that every remaining unitary operation is - allowed by the `native-gates` menu. `qco.barrier` and `qco.gphase` are - preserved; controlled gates (`qco.ctrl`) must have a single control and a - single target. + Rewrites unitaries to match the comma-separated `native-gates` menu. + `qco.barrier` and `qco.gphase` are preserved; `qco.ctrl` must have a + single control and a single target. The menu is a comma-separated list of gate tokens (order not significant) from which the pass builds a profile: a single-qubit synthesis mode @@ -97,19 +96,14 @@ def FuseTwoQubitUnitaryRuns - Rotation pair + entangler: `rx,rz,cx`, `rx,ry,cz`, `ry,rz,cx`, etc. Supported pairs are exactly `rx`+`rz`, `rx`+`ry`, and `ry`+`rz`. - Execution order (mirrors the implementation): fuse consecutive - single-qubit runs; fuse two-qubit windows (including absorbed - single-qubit padding); run up to four synthesis sweeps over remaining - non-native unitaries until every single-qubit op matches the menu (two-qubit - lowering may temporarily emit off-menu 1q ops that later sweeps absorb—if - any remain after that cap, the pass fails); fuse 1q seams between two-qubit - blocks; then up to four further synthesis + fusion rounds until the full menu - holds (including native `qco.ctrl` shells and bare `rzz` when allowed). If - anything is still off-menu, the pass fails. - - Lowering is deterministic: the entangler is chosen as `cx` before `cz`, the - single-qubit factors use the first emitter's Euler basis, and the minimal - KAK entangler count drives two-qubit window replacement. + Stages: fuse single-qubit runs; fuse two-qubit windows; up to four + synthesis sweeps over remaining off-menu ops; fuse 1q seams; up to four + further synthesis and fusion rounds until the full menu holds. The pass + fails if anything remains off-menu after those caps. + + Lowering is deterministic: `cx` is preferred over `cz`, single-qubit + factors use the first emitter's Euler basis, and two-qubit window + replacement uses the minimal entangler count from the synthesizer. }]; let options = [Option< "nativeGates", "native-gates", "std::string", "\"\"", diff --git a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h index 1eba783d0b..d1e1c68446 100644 --- a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h +++ b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h @@ -686,9 +686,7 @@ inline constexpr bool * `Eigen::SelfAdjointEigenSolver`. */ struct SymmetricEigen4 { - /// Eigenvalues sorted in ascending order. std::array eigenvalues{}; - /// Orthonormal eigenvectors as columns (real-valued, zero imaginary part). Matrix4x4 eigenvectors{}; }; @@ -746,4 +744,56 @@ jacobiSymmetricEigen(const std::array& symmetric); std::size_t q0Index, std::size_t q1Index); +inline constexpr double FRAC1_SQRT2 = + 0.707106781186547524400844362104849039284835937688474036588; + +/** + * @brief Non-negative remainder of @p a modulo @p b. + * + * Unlike `std::fmod`, the result is always in `[0, |b|)`. + */ +[[nodiscard]] double remEuclid(double a, double b); + +/** + * @brief Average two-qubit gate fidelity from a Hilbert-Schmidt trace. + * + * Maps `|tr(U^dag V)|` to fidelity via `(4 + |tr|^2) / 20`. + */ +[[nodiscard]] double traceToFidelity(const Complex& trace); + +/** @brief Unit-modulus global phase factor `exp(i * phase)`. */ +[[nodiscard]] Complex globalPhaseFactor(double phase); + +/** @brief Returns true when @p matrix is unitary within @p tolerance. */ +[[nodiscard]] bool isUnitaryMatrix(const Matrix2x2& matrix, + double tolerance = MATRIX_TOLERANCE); + +[[nodiscard]] Matrix2x2 rxMatrix(double theta); +[[nodiscard]] Matrix2x2 ryMatrix(double theta); +[[nodiscard]] Matrix2x2 rzMatrix(double theta); + +[[nodiscard]] const Matrix2x2& iPauliX(); +[[nodiscard]] const Matrix2x2& iPauliY(); +[[nodiscard]] const Matrix2x2& iPauliZ(); + +[[nodiscard]] Matrix4x4 rxxMatrix(double theta); +[[nodiscard]] Matrix4x4 ryyMatrix(double theta); +[[nodiscard]] Matrix4x4 rzzMatrix(double theta); + +/** + * @brief Controlled-`X` with control on qubit 0 (MSB) and target on qubit 1. + * + * Operand order matches @ref embedSingleQubitInTwoQubit and QCO unitaries. + */ +[[nodiscard]] const Matrix4x4& twoQubitControlledX01(); + +/** + * @brief Controlled-`X` with control on qubit 1 and target on qubit 0. + * + * Same entangling content as @ref twoQubitControlledX01 but with wires + * reversed; useful when parametrizing basis-decomposer tests. + */ +[[nodiscard]] const Matrix4x4& twoQubitControlledX10(); +[[nodiscard]] const Matrix4x4& twoQubitControlledZ(); + } // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp new file mode 100644 index 0000000000..c604133906 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp @@ -0,0 +1,431 @@ +/* + * 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/NativeProfile.h" + +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +using mlir::qco::Matrix2x2; +using mlir::qco::Matrix4x4; +using mlir::qco::twoQubitControlledX01; +using mlir::qco::twoQubitControlledZ; + +namespace mlir::qco::decomposition { + +namespace { + +[[nodiscard]] std::optional +parseGateToken(llvm::StringRef name) { + return llvm::StringSwitch>(name) + .Case("u", NativeGateKind::U) + .Case("x", NativeGateKind::X) + .Case("sx", NativeGateKind::Sx) + .Cases("rz", "p", NativeGateKind::Rz) + .Case("rx", NativeGateKind::Rx) + .Case("ry", NativeGateKind::Ry) + .Case("r", NativeGateKind::R) + .Case("cx", NativeGateKind::Cx) + .Case("cz", NativeGateKind::Cz) + .Case("rzz", NativeGateKind::Rzz) + .Default(std::nullopt); +} + +[[nodiscard]] std::optional> +parseGateSet(llvm::StringRef nativeGates) { + llvm::DenseSet gates; + llvm::SmallVector parts; + nativeGates.split(parts, ',', /*MaxSplit=*/-1, /*KeepEmpty=*/false); + for (llvm::StringRef part : parts) { + const auto token = part.trim().lower(); + if (token.empty()) { + continue; + } + const auto gate = parseGateToken(token); + if (!gate) { + return std::nullopt; + } + gates.insert(*gate); + } + return gates; +} + +[[nodiscard]] SingleQubitEmitterSpec +makeEmitterSpec(SingleQubitMode mode, AxisPair axisPair = AxisPair::RxRz, + bool supportsDirectRx = false) { + return { + .mode = mode, .axisPair = axisPair, .supportsDirectRx = supportsDirectRx}; +} + +void addEmitterIfAbsent(llvm::SmallVectorImpl& emitters, + SingleQubitMode mode, + AxisPair axisPair = AxisPair::RxRz, + bool supportsDirectRx = false) { + const bool present = llvm::any_of(emitters, [&](const auto& e) { + return e.mode == mode && e.axisPair == axisPair && + e.supportsDirectRx == supportsDirectRx; + }); + if (!present) { + emitters.push_back(makeEmitterSpec(mode, axisPair, supportsDirectRx)); + } +} + +[[nodiscard]] llvm::SmallVector +allowedGatesForEmitter(const SingleQubitEmitterSpec& emitter) { + switch (emitter.mode) { + case SingleQubitMode::ZSXX: { + llvm::SmallVector gates{ + NativeGateKind::X, NativeGateKind::Sx, NativeGateKind::Rz}; + if (emitter.supportsDirectRx) { + gates.push_back(NativeGateKind::Rx); + } + return gates; + } + case SingleQubitMode::U3: + return {NativeGateKind::U}; + case SingleQubitMode::R: + return {NativeGateKind::R}; + case SingleQubitMode::AxisPair: + switch (emitter.axisPair) { + case AxisPair::RxRz: + return {NativeGateKind::Rx, NativeGateKind::Rz}; + case AxisPair::RxRy: + return {NativeGateKind::Rx, NativeGateKind::Ry}; + case AxisPair::RyRz: + return {NativeGateKind::Ry, NativeGateKind::Rz}; + } + break; + } + llvm_unreachable("unknown single-qubit mode"); +} + +[[nodiscard]] llvm::SmallVector +allowedGatesForEntangler(EntanglerBasis entangler) { + switch (entangler) { + case EntanglerBasis::None: + return {}; + case EntanglerBasis::Cx: + return {NativeGateKind::Cx}; + case EntanglerBasis::Cz: + return {NativeGateKind::Cz}; + } + llvm_unreachable("unknown entangler basis"); +} + +void populateAllowedGates(NativeProfileSpec& spec) { + spec.allowedGates.clear(); + for (const auto& emitter : spec.singleQubitEmitters) { + const auto allowed = allowedGatesForEmitter(emitter); + spec.allowedGates.insert(allowed.begin(), allowed.end()); + } + for (const auto entangler : spec.entanglerBases) { + const auto allowed = allowedGatesForEntangler(entangler); + spec.allowedGates.insert(allowed.begin(), allowed.end()); + } + if (spec.allowRzz) { + spec.allowedGates.insert(NativeGateKind::Rzz); + } +} + +[[nodiscard]] std::optional +selectEntangler(const NativeProfileSpec& spec) { + if (llvm::is_contained(spec.entanglerBases, EntanglerBasis::Cx)) { + return EntanglerBasis::Cx; + } + if (llvm::is_contained(spec.entanglerBases, EntanglerBasis::Cz)) { + return EntanglerBasis::Cz; + } + return std::nullopt; +} + +[[nodiscard]] Matrix4x4 entanglerMatrix(EntanglerBasis entangler) { + return entangler == EntanglerBasis::Cz ? twoQubitControlledZ() + : twoQubitControlledX01(); +} + +[[nodiscard]] std::optional +decomposeWithEntangler(const Matrix4x4& target, EntanglerBasis entangler) { + auto decomposer = + TwoQubitBasisDecomposer::create(entanglerMatrix(entangler), 1.0); + auto weyl = TwoQubitWeylDecomposition::create(target, std::nullopt); + return decomposer.twoQubitDecompose(weyl, std::nullopt); +} + +void emitGPhaseIfNonTrivial(OpBuilder& builder, Location loc, double phase) { + constexpr double epsilon = 1e-12; + if (std::abs(phase) > epsilon) { + GPhaseOp::create(builder, loc, phase); + } +} + +[[nodiscard]] Value emitSingleQubitMatrix(OpBuilder& builder, Location loc, + Value inQubit, + const Matrix2x2& matrix, + EulerBasis basis) { + return *synthesizeUnitary1QEuler(builder, loc, inQubit, matrix, + /*runSize=*/0, /*hasNonBasisGate=*/true, + basis); +} + +} // namespace + +EulerBasis emitterEulerBasis(const SingleQubitEmitterSpec& emitter) { + switch (emitter.mode) { + case SingleQubitMode::ZSXX: + return EulerBasis::ZSXX; + case SingleQubitMode::U3: + return EulerBasis::U; + case SingleQubitMode::R: + return EulerBasis::R; + case SingleQubitMode::AxisPair: + switch (emitter.axisPair) { + case AxisPair::RxRz: + return EulerBasis::XZX; + case AxisPair::RxRy: + return EulerBasis::XYX; + case AxisPair::RyRz: + return EulerBasis::ZYZ; + } + break; + } + llvm_unreachable("unknown single-qubit mode"); +} + +std::optional parseNativeSpec(llvm::StringRef nativeGates) { + const auto gates = parseGateSet(nativeGates); + if (!gates || gates->empty()) { + return std::nullopt; + } + const auto has = [&](NativeGateKind kind) { return gates->contains(kind); }; + + NativeProfileSpec spec; + + if (has(NativeGateKind::U)) { + addEmitterIfAbsent(spec.singleQubitEmitters, SingleQubitMode::U3); + } + const bool hasXSxRz = has(NativeGateKind::X) && has(NativeGateKind::Sx) && + has(NativeGateKind::Rz); + if (hasXSxRz) { + addEmitterIfAbsent(spec.singleQubitEmitters, SingleQubitMode::ZSXX, + AxisPair::RxRz, + /*supportsDirectRx=*/has(NativeGateKind::Rx)); + } + if (has(NativeGateKind::R)) { + addEmitterIfAbsent(spec.singleQubitEmitters, SingleQubitMode::R); + } + struct AxisPairRule { + AxisPair axis; + NativeGateKind left; + NativeGateKind right; + }; + for (const auto& rule : { + AxisPairRule{.axis = AxisPair::RxRz, + .left = NativeGateKind::Rx, + .right = NativeGateKind::Rz}, + AxisPairRule{.axis = AxisPair::RxRy, + .left = NativeGateKind::Rx, + .right = NativeGateKind::Ry}, + AxisPairRule{.axis = AxisPair::RyRz, + .left = NativeGateKind::Ry, + .right = NativeGateKind::Rz}, + }) { + if (has(rule.left) && has(rule.right)) { + addEmitterIfAbsent(spec.singleQubitEmitters, SingleQubitMode::AxisPair, + rule.axis); + } + } + if (spec.singleQubitEmitters.empty()) { + return std::nullopt; + } + + if (has(NativeGateKind::Cx)) { + spec.entanglerBases.push_back(EntanglerBasis::Cx); + } + if (has(NativeGateKind::Cz)) { + spec.entanglerBases.push_back(EntanglerBasis::Cz); + } + spec.allowRzz = has(NativeGateKind::Rzz); + + populateAllowedGates(spec); + return spec; +} + +LogicalResult synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, + Value qubit0, Value qubit1, + const Matrix4x4& target, + const NativeProfileSpec& spec, + Value& outQubit0, Value& outQubit1) { + const auto entangler = selectEntangler(spec); + if (!entangler) { + return failure(); + } + const auto native = decomposeWithEntangler(target, *entangler); + if (!native) { + return failure(); + } + const auto basis = emitterEulerBasis(spec.singleQubitEmitters.front()); + + emitGPhaseIfNonTrivial(builder, loc, native->globalPhase); + + Value wire0 = qubit0; + Value wire1 = qubit1; + const auto& factors = native->singleQubitFactors; + const std::uint8_t numBasisUses = native->numBasisUses; + const auto emitFactor = [&](Value& wire, std::size_t index) { + wire = emitSingleQubitMatrix(builder, loc, wire, factors[index], basis); + }; + const auto emitEntangler = [&]() { + auto ctrlOp = CtrlOp::create( + builder, loc, ValueRange{wire0}, ValueRange{wire1}, + [&](ValueRange targetArgs) -> llvm::SmallVector { + if (*entangler == EntanglerBasis::Cz) { + return {ZOp::create(builder, loc, targetArgs[0]).getOutputQubit(0)}; + } + return {XOp::create(builder, loc, targetArgs[0]).getOutputQubit(0)}; + }); + wire0 = ctrlOp.getOutputControl(0); + wire1 = ctrlOp.getOutputTarget(0); + }; + + for (std::uint8_t i = 0; i < numBasisUses; ++i) { + emitFactor(wire1, static_cast(2 * i)); + emitFactor(wire0, static_cast((2 * i) + 1)); + emitEntangler(); + } + emitFactor(wire1, static_cast(2 * numBasisUses)); + emitFactor(wire0, static_cast((2 * numBasisUses) + 1)); + + outQubit0 = wire0; + outQubit1 = wire1; + return success(); +} + +std::optional +twoQubitEntanglerCount(const Matrix4x4& target, const NativeProfileSpec& spec) { + const auto entangler = selectEntangler(spec); + if (!entangler) { + return std::nullopt; + } + const auto native = decomposeWithEntangler(target, *entangler); + if (!native) { + return std::nullopt; + } + return native->numBasisUses; +} + +std::optional nativeGateKindFor(UnitaryOpInterface op) { + Operation* raw = op.getOperation(); + if (llvm::isa(raw)) { + return NativeGateKind::U; + } + if (llvm::isa(raw)) { + return NativeGateKind::X; + } + if (llvm::isa(raw)) { + return NativeGateKind::Sx; + } + if (llvm::isa(raw)) { + return NativeGateKind::Rz; + } + if (llvm::isa(raw)) { + return NativeGateKind::Rx; + } + if (llvm::isa(raw)) { + return NativeGateKind::Ry; + } + if (llvm::isa(raw)) { + return NativeGateKind::R; + } + return std::nullopt; +} + +bool allowsSingleQubitOp(UnitaryOpInterface op, const NativeProfileSpec& spec) { + if (llvm::isa(op.getOperation())) { + return true; + } + const auto gate = nativeGateKindFor(op); + return gate && spec.allowedGates.contains(*gate); +} + +std::optional entanglerBasisForSingleTargetCtrl(CtrlOp ctrl) { + if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { + return std::nullopt; + } + Operation* body = ctrl.getBodyUnitary(0).getOperation(); + if (llvm::isa(body)) { + return EntanglerBasis::Cx; + } + if (llvm::isa(body)) { + return EntanglerBasis::Cz; + } + return std::nullopt; +} + +bool profileAllowsEntangler(const NativeProfileSpec& spec, + EntanglerBasis basis) { + return llvm::is_contained(spec.entanglerBases, basis); +} + +bool allowsSingleTargetCtrl(CtrlOp ctrl, const NativeProfileSpec& spec) { + const auto basis = entanglerBasisForSingleTargetCtrl(ctrl); + return basis && profileAllowsEntangler(spec, *basis); +} + +bool allowsBareTwoQubitOp(Operation* op, const NativeProfileSpec& spec) { + return spec.allowRzz && llvm::isa(op) && + spec.allowedGates.contains(NativeGateKind::Rzz); +} + +bool allowsTwoQubitOp(Operation* op, const NativeProfileSpec& spec) { + if (auto ctrl = llvm::dyn_cast(op)) { + return allowsSingleTargetCtrl(ctrl, spec); + } + return allowsBareTwoQubitOp(op, spec); +} + +bool assignTwoQubitOpMatrix(Operation* op, Matrix4x4& matrix) { + if (llvm::isa(op)) { + return false; + } + if (auto ctrl = llvm::dyn_cast(op)) { + const auto basis = entanglerBasisForSingleTargetCtrl(ctrl); + if (!basis) { + return false; + } + matrix = *basis == EntanglerBasis::Cz ? twoQubitControlledZ() + : twoQubitControlledX01(); + return true; + } + auto unitary = llvm::dyn_cast(op); + if (!unitary || !unitary.isTwoQubit()) { + return false; + } + return unitary.getUnitaryMatrix4x4(matrix); +} + +} // namespace mlir::qco::decomposition diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp index 25c08128c5..245bdfef87 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp @@ -10,21 +10,12 @@ #include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" -#include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" -#include -#include -#include -#include #include #include -#include -#include -#include #include -#include #include #include @@ -46,94 +37,228 @@ using mlir::qco::Matrix4x4; static constexpr double SANITY_CHECK_PRECISION = 1e-12; -[[nodiscard]] static bool isUnitaryMatrix(const Matrix2x2& matrix, - double tolerance = 1e-12) { - return (matrix.adjoint() * matrix).isIdentity(tolerance); -} +namespace mlir::qco::decomposition { + +using namespace std::complex_literals; -[[nodiscard]] static double remEuclid(double a, double b) { - if (b == 0.0) { - llvm::reportFatalInternalError("remEuclid expects non-zero divisor"); +namespace { + +enum class Specialization : std::uint8_t { + General, + IdEquiv, + SWAPEquiv, + PartialSWAPEquiv, + PartialSWAPFlipEquiv, + ControlledEquiv, + MirrorControlledEquiv, + FSimaabEquiv, + FSimabbEquiv, + FSimabmbEquiv, +}; + +enum class MagicBasisTransform : std::uint8_t { + Into, + OutOf, +}; + +static constexpr auto DIAGONALIZATION_PRECISION = 1e-13; + +[[nodiscard]] Matrix4x4 magicBasisTransform(const Matrix4x4& unitary, + MagicBasisTransform direction) { + const Matrix4x4 bNonNormalized = Matrix4x4::fromElements( // + 1, 1i, 0, 0, // + 0, 0, 1i, 1, // + 0, 0, 1i, -1, // + 1, -1i, 0, 0); + const Matrix4x4 bNonNormalizedDagger = Matrix4x4::fromElements( // + 0.5, 0, 0, 0.5, // + Complex{0.0, -0.5}, 0, 0, Complex{0.0, 0.5}, // + 0, Complex{0.0, -0.5}, Complex{0.0, -0.5}, 0, // + 0, 0.5, -0.5, 0); + if (direction == MagicBasisTransform::OutOf) { + return bNonNormalizedDagger * unitary * bNonNormalized; } - const auto r = std::fmod(a, b); - return (r < 0.0) ? r + std::abs(b) : r; + if (direction == MagicBasisTransform::Into) { + return bNonNormalized * unitary * bNonNormalizedDagger; + } + llvm::reportFatalInternalError("Unknown MagicBasisTransform direction!"); } -[[nodiscard]] static double traceToFidelity(const std::complex& x) { - const auto xAbs = std::abs(x); - return (4.0 + (xAbs * xAbs)) / 20.0; +[[nodiscard]] double 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.); } -[[nodiscard]] static std::complex -globalPhaseFactor(double globalPhase) { - return std::exp(std::complex{0, 1} * globalPhase); -} +[[nodiscard]] std::pair> +diagonalizeComplexSymmetric(const Matrix4x4& m, double precision) { + auto state = std::mt19937{2023}; + std::normal_distribution dist; -[[nodiscard]] static Matrix4x4 rxxMatrix(double theta) { - const auto cosTheta = std::cos(theta / 2.); - const Complex misin{0., -std::sin(theta / 2.)}; - return Matrix4x4::fromElements(cosTheta, 0, 0, misin, // - 0, cosTheta, misin, 0, // - 0, misin, cosTheta, 0, // - misin, 0, 0, cosTheta); -} + const auto mReal = m.realPart(); + const auto mImag = m.imagPart(); -[[nodiscard]] static Matrix4x4 ryyMatrix(double theta) { - const auto cosTheta = std::cos(theta / 2.); - const Complex isin{0., std::sin(theta / 2.)}; - const Complex misin{0., -std::sin(theta / 2.)}; - return Matrix4x4::fromElements(cosTheta, 0, 0, isin, // - 0, cosTheta, misin, 0, // - 0, misin, cosTheta, 0, // - isin, 0, 0, cosTheta); -} + double bestErr = 1e300; + constexpr auto maxDiagonalizationAttempts = 100; + for (int i = 0; i < maxDiagonalizationAttempts; ++i) { + double randA{}; + double randB{}; + if (i == 0) { + randA = 1.2602066112249388; + randB = 0.22317849046722027; + } else { + randA = dist(state); + randB = dist(state); + } + std::array m2Real{}; + for (std::size_t k = 0; k < m2Real.size(); ++k) { + m2Real[k] = (randA * mReal[k]) + (randB * mImag[k]); + } + const Matrix4x4 p = jacobiSymmetricEigen(m2Real).eigenvectors; + const std::array d = (p.transpose() * m * p).diagonal(); -[[nodiscard]] static Matrix4x4 rzzMatrix(double theta) { - const auto cosTheta = std::cos(theta / 2.); - const auto sinTheta = std::sin(theta / 2.); - const Complex em{cosTheta, -sinTheta}; - const Complex ep{cosTheta, sinTheta}; - return Matrix4x4::fromElements(em, 0, 0, 0, // - 0, ep, 0, 0, // - 0, 0, ep, 0, // - 0, 0, 0, em); + const auto compare = p * Matrix4x4::fromDiagonal(d) * p.transpose(); + { + double err = 0.0; + for (std::size_t r = 0; r < 4; ++r) { + for (std::size_t cc = 0; cc < 4; ++cc) { + err = std::max(err, std::abs(compare(r, cc) - m(r, cc))); + } + } + bestErr = std::min(bestErr, err); + } + if (compare.isApprox(m, precision)) { + assert((p.transpose() * p).isIdentity(SANITY_CHECK_PRECISION)); + assert(std::abs(Matrix4x4::fromDiagonal(d).determinant() - 1.0) < + SANITY_CHECK_PRECISION); + return std::make_pair(p, d); + } + } + llvm::reportFatalInternalError(llvm::formatv( + "TwoQubitWeylDecomposition: failed to diagonalize M2 ({0} iterations). " + "best error = {1:e}, precision = {2:e}", + maxDiagonalizationAttempts, bestErr, precision)); } -[[nodiscard]] static const Matrix4x4& swapGate() { - static const Matrix4x4 MATRIX = Matrix4x4::fromElements(1, 0, 0, 0, // - 0, 0, 1, 0, // - 0, 1, 0, 0, // - 0, 0, 0, 1); - return MATRIX; -} +[[nodiscard]] std::tuple +decomposeTwoQubitProductGate(const Matrix4x4& specialUnitary) { + Matrix2x2 r = + Matrix2x2::fromElements(specialUnitary(0, 0), specialUnitary(0, 1), + specialUnitary(1, 0), specialUnitary(1, 1)); + auto detR = r.determinant(); + if (std::abs(detR) < 0.1) { + r = Matrix2x2::fromElements(specialUnitary(2, 0), specialUnitary(2, 1), + specialUnitary(3, 0), specialUnitary(3, 1)); + detR = r.determinant(); + } + if (std::abs(detR) < 0.1) { + llvm::reportFatalInternalError( + "decomposeTwoQubitProductGate: unable to decompose: det_r < 0.1"); + } + r *= (1.0 / std::sqrt(detR)); + const Matrix2x2 rTConj = r.adjoint(); + + Matrix4x4 temp = specialUnitary * kron(Matrix2x2::identity(), rTConj); + + Matrix2x2 l = + Matrix2x2::fromElements(temp(0, 0), temp(0, 2), temp(2, 0), temp(2, 2)); + auto detL = l.determinant(); + if (std::abs(detL) < 0.9) { + llvm::reportFatalInternalError( + "decomposeTwoQubitProductGate: unable to decompose: detL < 0.9"); + } + l *= (1.0 / std::sqrt(detL)); + auto phase = std::arg(detL) / 2.; -[[nodiscard]] static const Matrix4x4& cxGate01() { - static const Matrix4x4 MATRIX = Matrix4x4::fromElements(1, 0, 0, 0, // - 0, 1, 0, 0, // - 0, 0, 0, 1, // - 0, 0, 1, 0); - return MATRIX; + return {l, r, phase}; } -[[nodiscard]] static const Matrix4x4& cxGate10() { - static const Matrix4x4 MATRIX = Matrix4x4::fromElements(1, 0, 0, 0, // - 0, 0, 0, 1, // - 0, 0, 1, 0, // - 0, 1, 0, 0); - return MATRIX; +[[nodiscard]] std::complex 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)}; } -[[nodiscard]] static const Matrix4x4& czGate() { - static const Matrix4x4 MATRIX = Matrix4x4::fromElements(1, 0, 0, 0, // - 0, 1, 0, 0, // - 0, 0, 1, 0, // - 0, 0, 0, -1); - return MATRIX; +[[nodiscard]] Specialization +bestSpecialization(const TwoQubitWeylDecomposition& decomposition, + const std::optional& requestedFidelity) { + auto isClose = [&](double ap, double bp, double cp) -> bool { + auto tr = getTrace(decomposition.a(), decomposition.b(), decomposition.c(), + ap, bp, cp); + if (requestedFidelity) { + return traceToFidelity(tr) >= *requestedFidelity; + } + return false; + }; + + auto closestAbc = closestPartialSwap(decomposition.a(), decomposition.b(), + decomposition.c()); + auto closestAbMinusC = closestPartialSwap( + decomposition.a(), decomposition.b(), -decomposition.c()); + + if (isClose(0., 0., 0.)) { + return Specialization::IdEquiv; + } + if (isClose((std::numbers::pi / 4.0), (std::numbers::pi / 4.0), + (std::numbers::pi / 4.0)) || + isClose((std::numbers::pi / 4.0), (std::numbers::pi / 4.0), + -(std::numbers::pi / 4.0))) { + return Specialization::SWAPEquiv; + } + if (isClose(closestAbc, closestAbc, closestAbc)) { + return Specialization::PartialSWAPEquiv; + } + if (isClose(closestAbMinusC, closestAbMinusC, -closestAbMinusC)) { + return Specialization::PartialSWAPFlipEquiv; + } + if (isClose(decomposition.a(), 0., 0.)) { + return Specialization::ControlledEquiv; + } + if (isClose((std::numbers::pi / 4.0), (std::numbers::pi / 4.0), + decomposition.c())) { + return Specialization::MirrorControlledEquiv; + } + if (isClose((decomposition.a() + decomposition.b()) / 2., + (decomposition.a() + decomposition.b()) / 2., + decomposition.c())) { + return Specialization::FSimaabEquiv; + } + if (isClose(decomposition.a(), (decomposition.b() + decomposition.c()) / 2., + (decomposition.b() + decomposition.c()) / 2.)) { + return Specialization::FSimabbEquiv; + } + if (isClose(decomposition.a(), (decomposition.b() - decomposition.c()) / 2., + (decomposition.c() - decomposition.b()) / 2.)) { + return Specialization::FSimabmbEquiv; + } + return Specialization::General; } -namespace mlir::qco::decomposition { +[[nodiscard]] bool relativeEq(double lhs, double rhs, double epsilon, + double maxRelative) { + if (lhs == rhs) { + return true; + } + if (std::isinf(lhs) || std::isinf(rhs)) { + return false; + } + auto absDiff = std::abs(lhs - rhs); + 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; +} -using namespace std::complex_literals; +} // namespace TwoQubitWeylDecomposition TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, @@ -267,28 +392,28 @@ TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, // Flip into Weyl chamber if (cs[0] > (pi / 2.0)) { cs[0] -= 3.0 * (pi / 2.0); - K1l = K1l * ipy(); - K1r = K1r * ipy(); + K1l = K1l * iPauliY(); + K1r = K1r * iPauliY(); globalPhase += (pi / 2.0); } if (cs[1] > (pi / 2.0)) { cs[1] -= 3.0 * (pi / 2.0); - K1l = K1l * ipx(); - K1r = K1r * ipx(); + K1l = K1l * iPauliX(); + K1r = K1r * iPauliX(); globalPhase += (pi / 2.0); } auto conjs = 0; if (cs[0] > (pi / 4.0)) { cs[0] = (pi / 2.0) - cs[0]; - K1l = K1l * ipy(); - K2r = ipy() * K2r; + K1l = K1l * iPauliY(); + K2r = iPauliY() * K2r; conjs += 1; globalPhase -= (pi / 2.0); } if (cs[1] > (pi / 4.0)) { cs[1] = (pi / 2.0) - cs[1]; - K1l = K1l * ipx(); - K2r = ipx() * K2r; + K1l = K1l * iPauliX(); + K2r = iPauliX() * K2r; conjs += 1; globalPhase += (pi / 2.0); if (conjs == 1) { @@ -297,8 +422,8 @@ TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, } if (cs[2] > (pi / 2.0)) { cs[2] -= 3.0 * (pi / 2.0); - K1l = K1l * ipz(); - K1r = K1r * ipz(); + K1l = K1l * iPauliZ(); + K1r = K1r * iPauliZ(); globalPhase += (pi / 2.0); if (conjs == 1) { globalPhase -= pi; @@ -306,14 +431,14 @@ TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, } if (conjs == 1) { cs[2] = (pi / 2.0) - cs[2]; - K1l = K1l * ipz(); - K2r = ipz() * K2r; + K1l = K1l * iPauliZ(); + K2r = iPauliZ() * K2r; globalPhase += (pi / 2.0); } if (cs[2] > (pi / 4.0)) { cs[2] -= (pi / 2.0); - K1l = K1l * ipz(); - K1r = K1r * ipz(); + K1l = K1l * iPauliZ(); + K1r = K1r * iPauliZ(); globalPhase -= (pi / 2.0); } @@ -329,7 +454,8 @@ TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, decomposition.k2l_ = K2l; decomposition.k1r_ = K1r; decomposition.k2r_ = K2r; - decomposition.specialization = Specialization::General; + decomposition.specializationKind_ = + static_cast(Specialization::General); decomposition.requestedFidelity = fidelity; // make sure decomposition is equal to input @@ -341,18 +467,17 @@ TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, // matrices can potentially be simplified auto flippedFromOriginal = decomposition.applySpecialization(); - auto getTrace = [&]() { + auto getTraceValue = [&]() { if (flippedFromOriginal) { - return TwoQubitWeylDecomposition::getTrace( - (pi / 2.0) - a, b, -c, decomposition.a_, decomposition.b_, - decomposition.c_); + return getTrace((pi / 2.0) - a, b, -c, decomposition.a_, decomposition.b_, + decomposition.c_); } - return TwoQubitWeylDecomposition::getTrace( - a, b, c, decomposition.a_, decomposition.b_, decomposition.c_); + return 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(); + auto trace = getTraceValue(); const double calculatedFidelity = traceToFidelity(trace); // final check if specialization is close enough to the original matrix to // satisfy the requested fidelity; since no forced specialization is @@ -390,231 +515,15 @@ Matrix4x4 TwoQubitWeylDecomposition::getCanonicalMatrix(double a, double b, return zz * yy * xx; } -Matrix4x4 -TwoQubitWeylDecomposition::magicBasisTransform(const Matrix4x4& unitary, - MagicBasisTransform direction) { - // Makhlin "magic basis" transform. Conjugating a 2-qubit unitary by - // `bNonNormalized` maps SU(2) x SU(2) factors onto SO(4) and diagonalizes - // the canonical (Weyl) gate. The matrices are stored unnormalized: the - // `1/2` pre-factor that would normally appear in `B^dagger` is absorbed - // into `bNonNormalizedDagger` directly so the product `Bd * B == I` - // without an extra scalar. - const Matrix4x4 bNonNormalized = Matrix4x4::fromElements( // - 1, 1i, 0, 0, // - 0, 0, 1i, 1, // - 0, 0, 1i, -1, // - 1, -1i, 0, 0); - const Matrix4x4 bNonNormalizedDagger = Matrix4x4::fromElements( // - 0.5, 0, 0, 0.5, // - Complex{0.0, -0.5}, 0, 0, Complex{0.0, 0.5}, // - 0, Complex{0.0, -0.5}, Complex{0.0, -0.5}, 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.); -} - -std::pair> -TwoQubitWeylDecomposition::diagonalizeComplexSymmetric(const Matrix4x4& 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; - - const auto mReal = m.realPart(); - const auto mImag = m.imagPart(); - - double bestErr = 1e300; - 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); - } - std::array m2Real{}; - for (std::size_t k = 0; k < m2Real.size(); ++k) { - m2Real[k] = (randA * mReal[k]) + (randB * mImag[k]); - } - const Matrix4x4 p = jacobiSymmetricEigen(m2Real).eigenvectors; - const std::array d = (p.transpose() * m * p).diagonal(); - - const auto compare = p * Matrix4x4::fromDiagonal(d) * p.transpose(); - { - double err = 0.0; - for (std::size_t r = 0; r < 4; ++r) { - for (std::size_t cc = 0; cc < 4; ++cc) { - err = std::max(err, std::abs(compare(r, cc) - m(r, cc))); - } - } - bestErr = std::min(bestErr, err); - } - if (compare.isApprox(m, precision)) { - // p are the eigenvectors which are decomposed into the - // single-qubit gates surrounding the canonical gate - // d is the sqrt of the eigenvalues that are used to determine the - // weyl coordinates and thus the parameters of the canonical gate - // check that p is in SO(4) - assert((p.transpose() * p).isIdentity(SANITY_CHECK_PRECISION)); - // make sure determinant of eigenvalues is 1.0 - assert(std::abs(Matrix4x4::fromDiagonal(d).determinant() - 1.0) < - SANITY_CHECK_PRECISION); - return std::make_pair(p, d); - } - } - llvm::reportFatalInternalError(llvm::formatv( - "TwoQubitWeylDecomposition: failed to diagonalize M2 ({0} iterations). " - "best error = {1:e}, precision = {2:e}", - maxDiagonalizationAttempts, bestErr, precision)); -} - -std::tuple -TwoQubitWeylDecomposition::decomposeTwoQubitProductGate( - const Matrix4x4& specialUnitary) { - // for alternative approaches, see - // pennylane's math.decomposition.su2su2_to_tensor_products - // or quantumflow.kronecker_decomposition - - // first quadrant - Matrix2x2 r = - Matrix2x2::fromElements(specialUnitary(0, 0), specialUnitary(0, 1), - specialUnitary(1, 0), specialUnitary(1, 1)); - auto detR = r.determinant(); - if (std::abs(detR) < 0.1) { - // third quadrant - r = Matrix2x2::fromElements(specialUnitary(2, 0), specialUnitary(2, 1), - specialUnitary(3, 0), specialUnitary(3, 1)); - detR = r.determinant(); - } - if (std::abs(detR) < 0.1) { - llvm::reportFatalInternalError( - "decomposeTwoQubitProductGate: unable to decompose: det_r < 0.1"); - } - r *= (1.0 / std::sqrt(detR)); - // transpose with complex conjugate of each element - const Matrix2x2 rTConj = r.adjoint(); - - Matrix4x4 temp = specialUnitary * kron(Matrix2x2::identity(), rTConj); - - // [[a, b, c, d], - // [e, f, g, h], => [[a, c], - // [i, j, k, l], [i, k]] - // [m, n, o, p]] - Matrix2x2 l = - Matrix2x2::fromElements(temp(0, 0), temp(0, 2), temp(2, 0), temp(2, 2)); - auto detL = l.determinant(); - if (std::abs(detL) < 0.9) { - llvm::reportFatalInternalError( - "decomposeTwoQubitProductGate: unable to decompose: detL < 0.9"); - } - l *= (1.0 / 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) { - // Closed-form Hilbert-Schmidt overlap `tr(U_d(a,b,c)^dag * U_d(ap,bp,cp))` - // between two canonical (Weyl) gates, expressed in terms of the coordinate - // differences. Feeding the result into `traceToFidelity` gives the average - // two-qubit gate fidelity between the two canonical gates, which - // `bestSpecialization` uses to rank candidate specializations. - // Reference: Zhang et al., "Geometric theory of nonlocal two-qubit - // operations", Phys. Rev. A 67, 042313 (2003), Eq. (20). - 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 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((std::numbers::pi / 4.0), (std::numbers::pi / 4.0), - (std::numbers::pi / 4.0)) || - isClose((std::numbers::pi / 4.0), (std::numbers::pi / 4.0), - -(std::numbers::pi / 4.0))) { - 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((std::numbers::pi / 4.0), (std::numbers::pi / 4.0), 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) { + if (specializationKind_ != + static_cast(Specialization::General)) { llvm::reportFatalInternalError( "Application of specialization only works on " "general Weyl decompositions!"); } bool flippedFromOriginal = false; - auto newSpecialization = bestSpecialization(); + const auto newSpecialization = bestSpecialization(*this, requestedFidelity); if (newSpecialization == Specialization::General) { // U has no special symmetry. // @@ -622,7 +531,7 @@ bool TwoQubitWeylDecomposition::applySpecialization() { // make the single-qubit pre-/post-gates canonical. return flippedFromOriginal; } - specialization = newSpecialization; + specializationKind_ = static_cast(newSpecialization); if (newSpecialization == Specialization::IdEquiv) { // :math:`U \sim U_d(0,0,0)` @@ -656,8 +565,8 @@ bool TwoQubitWeylDecomposition::applySpecialization() { flippedFromOriginal = true; globalPhase_ += (std::numbers::pi / 2.0); - k1l_ = k1l_ * ipz() * k2r_; - k1r_ = k1r_ * ipz() * k2l_; + k1l_ = k1l_ * iPauliZ() * k2r_; + k1r_ = k1r_ * iPauliZ() * k2l_; k2l_ = Matrix2x2::identity(); k2r_ = Matrix2x2::identity(); } @@ -700,8 +609,8 @@ bool TwoQubitWeylDecomposition::applySpecialization() { c_ = -closest; // unmodified global phase k1l_ = k1l_ * k2l_; - k1r_ = k1r_ * ipz() * k2l_ * ipz(); - k2r_ = ipz() * k2lDagger * ipz() * k2r_; + k1r_ = k1r_ * iPauliZ() * k2l_ * iPauliZ(); + k2r_ = iPauliZ() * k2lDagger * iPauliZ() * k2r_; k2l_ = Matrix2x2::identity(); } else if (newSpecialization == Specialization::ControlledEquiv) { // :math:`U \sim U_d(\alpha, 0, 0)` @@ -798,8 +707,8 @@ bool TwoQubitWeylDecomposition::applySpecialization() { globalPhase_ = globalPhase_ + k2lphase; k1l_ = k1l_ * rxMatrix(k2lphi); k2l_ = ryMatrix(k2ltheta) * rxMatrix(k2llambda); - k1r_ = k1r_ * ipz() * rxMatrix(k2lphi) * ipz(); - k2r_ = ipz() * rxMatrix(-k2lphi) * ipz() * k2r_; + k1r_ = k1r_ * iPauliZ() * rxMatrix(k2lphi) * iPauliZ(); + k2r_ = iPauliZ() * rxMatrix(-k2lphi) * iPauliZ() * k2r_; } else { llvm::reportFatalInternalError( "Unknown specialization for Weyl decomposition!"); @@ -816,26 +725,8 @@ TwoQubitBasisDecomposer::create(const Matrix4x4& basisMatrix, Matrix2x2::fromElements(Complex{0.5, 0.5}, Complex{0.5, 0.5}, Complex{-0.5, 0.5}, Complex{0.5, -0.5}); - // The Shende-Markov-Bullock 3-CX sandwich (and its 1/2-CX reductions) used - // below is derived for a basis CX whose 4x4 matrix is the Qiskit/LSB form - // `[[1,0,0,0],[0,0,0,1],[0,0,1,0],[0,1,0,0]]`, i.e. "control on the LSB - // factor, target on the MSB factor" of the tensor product. MQT's wider - // convention places operand 0 on the MSB factor, so the CX/CZ matrix for - // control-on-wire-0 gives the SWAP-conjugate - // `[[1,0,0,0],[0,1,0,0],[0,0,0,1],[0,0,1,0]]`. - // - // Because `SWAP * C(a,b,c) * SWAP = C(a,b,c)` but - // `SWAP * (K1l ⊗ K1r) * SWAP = (K1r ⊗ K1l)`, feeding the MSB matrix directly - // into the Weyl decomposer would swap the roles of `k1l`/`k1r` (and `k2l`/ - // `k2r`) relative to the hard-coded constants above. To keep the SMB algebra - // self-consistent we SWAP-conjugate the basis matrix here (restoring the - // Qiskit/LSB 4x4) and then absorb the resulting "left/right" relabeling at - // the emission boundary in `decomp{0,1,2,3}` below. This reproduces the - // pre-flip gate counts without having to re-derive every SMB constant for - // the MSB basis -- the two routes are algebraically equivalent. - const Matrix4x4 basisMatrixLsb = swapGate() * basisMatrix * swapGate(); const auto basisDecomposer = - TwoQubitWeylDecomposition::create(basisMatrixLsb, basisFidelity); + TwoQubitWeylDecomposition::create(basisMatrix, basisFidelity); const auto isSuperControlled = relativeEq(basisDecomposer.a(), std::numbers::pi / 4.0, 1e-13, 1e-09) && relativeEq(basisDecomposer.c(), 0.0, 1e-13, 1e-09); @@ -888,38 +779,38 @@ TwoQubitBasisDecomposer::create(const Matrix4x4& basisMatrix, auto u3r = k2rDagger * k12RArr; // Pre-build the fixed parts of the matrices used in the 2-part decomposition auto q0l = k12LArr.adjoint() * k1lDagger; - auto q0r = k12RArr.adjoint() * ipz() * k1rDagger; + auto q0r = k12RArr.adjoint() * iPauliZ() * k1rDagger; auto q1la = k2lDagger * k11l.adjoint(); auto q1lb = k11l * k1lDagger; - auto q1ra = k2rDagger * ipz() * k11r.adjoint(); + auto q1ra = k2rDagger * iPauliZ() * k11r.adjoint(); auto q1rb = k11r * k1rDagger; auto q2l = k2lDagger * k12LArr; auto q2r = k2rDagger * k12RArr; - return TwoQubitBasisDecomposer{ - basisFidelity, - basisDecomposer, - isSuperControlled, - u0l, - u0r, - u1l, - u1ra, - u1rb, - u2la, - u2lb, - u2ra, - u2rb, - u3l, - u3r, - q0l, - q0r, - q1la, - q1lb, - q1ra, - q1rb, - q2l, - q2r, - }; + TwoQubitBasisDecomposer decomposer; + decomposer.basisFidelity = basisFidelity; + decomposer.basisDecomposer = basisDecomposer; + decomposer.isSuperControlled = isSuperControlled; + decomposer.smb.u0l = u0l; + decomposer.smb.u0r = u0r; + decomposer.smb.u1l = u1l; + decomposer.smb.u1ra = u1ra; + decomposer.smb.u1rb = u1rb; + decomposer.smb.u2la = u2la; + decomposer.smb.u2lb = u2lb; + decomposer.smb.u2ra = u2ra; + decomposer.smb.u2rb = u2rb; + decomposer.smb.u3l = u3l; + decomposer.smb.u3r = u3r; + decomposer.smb.q0l = q0l; + decomposer.smb.q0r = q0r; + decomposer.smb.q1la = q1la; + decomposer.smb.q1lb = q1lb; + decomposer.smb.q1ra = q1ra; + decomposer.smb.q1rb = q1rb; + decomposer.smb.q2l = q2l; + decomposer.smb.q2r = q2r; + return decomposer; } std::optional @@ -976,7 +867,7 @@ TwoQubitBasisDecomposer::twoQubitDecompose( TwoQubitLocalUnitaryList factors = chooseDecomposition(); #ifndef NDEBUG for (const auto& factor : factors) { - assert(isUnitaryMatrix(factor)); + assert(isUnitaryMatrix(factor, 1e-12)); } #endif @@ -1000,13 +891,8 @@ TwoQubitBasisDecomposer::twoQubitDecompose( }; } -// Ported SMB helpers assume Qiskit Weyl k-factor layout; QCO 4x4 input order -// swaps l/r vs that port. Swap k1l<->k1r and k2l<->k2r when reading ``target``, -// and swap adjacent pairs in each return vector so the emission boundary maps -// matrices to the same wires as the upstream decomposer. ``decomp0`` cancels to -// the unswapped formula. -TwoQubitLocalUnitaryList -TwoQubitBasisDecomposer::decomp0(const TwoQubitWeylDecomposition& target) { +TwoQubitLocalUnitaryList TwoQubitBasisDecomposer::decomp0( + const TwoQubitWeylDecomposition& target) const { return TwoQubitLocalUnitaryList{ target.k1r() * target.k2r(), target.k1l() * target.k2l(), @@ -1017,10 +903,10 @@ TwoQubitLocalUnitaryList TwoQubitBasisDecomposer::decomp1( const TwoQubitWeylDecomposition& target) const { // may not work for z != 0 and c != 0 (not always in Weyl chamber) return TwoQubitLocalUnitaryList{ - basisDecomposer.k2l().adjoint() * target.k2r(), - basisDecomposer.k2r().adjoint() * target.k2l(), - target.k1r() * basisDecomposer.k1l().adjoint(), - target.k1l() * basisDecomposer.k1r().adjoint(), + basisDecomposer.k2r().adjoint() * target.k2r(), + basisDecomposer.k2l().adjoint() * target.k2l(), + target.k1r() * basisDecomposer.k1r().adjoint(), + target.k1l() * basisDecomposer.k1l().adjoint(), }; } @@ -1032,12 +918,12 @@ TwoQubitLocalUnitaryList TwoQubitBasisDecomposer::decomp2Supercontrolled( "- no guarantee for exact decomposition with two basis gates"); } return TwoQubitLocalUnitaryList{ - q2l * target.k2r(), - q2r * target.k2l(), - q1la * rzMatrix(-2. * target.a()) * q1lb, - q1ra * rzMatrix(2. * target.b()) * q1rb, - target.k1r() * q0l, - target.k1l() * q0r, + smb.q2r * target.k2r(), + smb.q2l * target.k2l(), + smb.q1ra * rzMatrix(2. * target.b()) * smb.q1rb, + smb.q1la * rzMatrix(-2. * target.a()) * smb.q1lb, + target.k1r() * smb.q0r, + target.k1l() * smb.q0l, }; } @@ -1049,14 +935,14 @@ TwoQubitLocalUnitaryList TwoQubitBasisDecomposer::decomp3Supercontrolled( "- no guarantee for exact decomposition with three basis gates"); } return TwoQubitLocalUnitaryList{ - u3l * target.k2r(), - u3r * target.k2l(), - u2la * rzMatrix(-2. * target.a()) * u2lb, - u2ra * rzMatrix(2. * target.b()) * u2rb, - u1l, - u1ra * rzMatrix(-2. * target.c()) * u1rb, - target.k1r() * u0l, - target.k1l() * u0r, + smb.u3r * target.k2r(), + smb.u3l * target.k2l(), + smb.u2ra * rzMatrix(2. * target.b()) * smb.u2rb, + smb.u2la * rzMatrix(-2. * target.a()) * smb.u2lb, + smb.u1ra * rzMatrix(-2. * target.c()) * smb.u1rb, + smb.u1l, + target.k1r() * smb.u0r, + target.k1l() * smb.u0l, }; } @@ -1089,370 +975,4 @@ TwoQubitBasisDecomposer::traces(const TwoQubitWeylDecomposition& target) const { }; } -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; -} - -//===----------------------------------------------------------------------===// -// Native-spec parsing and two-qubit synthesis -//===----------------------------------------------------------------------===// - -static constexpr double PI = std::numbers::pi; - -[[nodiscard]] static std::optional -parseGateToken(llvm::StringRef name) { - return llvm::StringSwitch>(name) - .Case("u", NativeGateKind::U) - .Case("x", NativeGateKind::X) - .Case("sx", NativeGateKind::Sx) - .Cases("rz", "p", NativeGateKind::Rz) - .Case("rx", NativeGateKind::Rx) - .Case("ry", NativeGateKind::Ry) - .Case("r", NativeGateKind::R) - .Case("cx", NativeGateKind::Cx) - .Case("cz", NativeGateKind::Cz) - .Case("rzz", NativeGateKind::Rzz) - .Default(std::nullopt); -} - -[[nodiscard]] static std::optional> -parseGateSet(llvm::StringRef nativeGates) { - llvm::DenseSet gates; - llvm::SmallVector parts; - nativeGates.split(parts, ',', /*MaxSplit=*/-1, /*KeepEmpty=*/false); - for (llvm::StringRef part : parts) { - const auto token = part.trim().lower(); - if (token.empty()) { - continue; - } - const auto gate = parseGateToken(token); - if (!gate) { - return std::nullopt; - } - gates.insert(*gate); - } - return gates; -} - -[[nodiscard]] static SingleQubitEmitterSpec -makeEmitterSpec(SingleQubitMode mode, AxisPair axisPair = AxisPair::RxRz, - bool supportsDirectRx = false) { - return { - .mode = mode, .axisPair = axisPair, .supportsDirectRx = supportsDirectRx}; -} - -static void -addEmitterIfAbsent(llvm::SmallVectorImpl& emitters, - SingleQubitMode mode, AxisPair axisPair = AxisPair::RxRz, - bool supportsDirectRx = false) { - const bool present = llvm::any_of(emitters, [&](const auto& e) { - return e.mode == mode && e.axisPair == axisPair && - e.supportsDirectRx == supportsDirectRx; - }); - if (!present) { - emitters.push_back(makeEmitterSpec(mode, axisPair, supportsDirectRx)); - } -} - -[[nodiscard]] static llvm::SmallVector -allowedGatesForEmitter(const SingleQubitEmitterSpec& emitter) { - switch (emitter.mode) { - case SingleQubitMode::ZSXX: { - llvm::SmallVector gates{ - NativeGateKind::X, NativeGateKind::Sx, NativeGateKind::Rz}; - if (emitter.supportsDirectRx) { - gates.push_back(NativeGateKind::Rx); - } - return gates; - } - case SingleQubitMode::U3: - return {NativeGateKind::U}; - case SingleQubitMode::R: - return {NativeGateKind::R}; - case SingleQubitMode::AxisPair: - switch (emitter.axisPair) { - case AxisPair::RxRz: - return {NativeGateKind::Rx, NativeGateKind::Rz}; - case AxisPair::RxRy: - return {NativeGateKind::Rx, NativeGateKind::Ry}; - case AxisPair::RyRz: - return {NativeGateKind::Ry, NativeGateKind::Rz}; - } - break; - } - llvm_unreachable("unknown single-qubit mode"); -} - -[[nodiscard]] static llvm::SmallVector -allowedGatesForEntangler(EntanglerBasis entangler) { - switch (entangler) { - case EntanglerBasis::None: - return {}; - case EntanglerBasis::Cx: - return {NativeGateKind::Cx}; - case EntanglerBasis::Cz: - return {NativeGateKind::Cz}; - } - llvm_unreachable("unknown entangler basis"); -} - -static void populateAllowedGates(NativeProfileSpec& spec) { - spec.allowedGates.clear(); - for (const auto& emitter : spec.singleQubitEmitters) { - const auto allowed = allowedGatesForEmitter(emitter); - spec.allowedGates.insert(allowed.begin(), allowed.end()); - } - for (const auto entangler : spec.entanglerBases) { - const auto allowed = allowedGatesForEntangler(entangler); - spec.allowedGates.insert(allowed.begin(), allowed.end()); - } - if (spec.allowRzz) { - spec.allowedGates.insert(NativeGateKind::Rzz); - } -} - -[[nodiscard]] static std::optional -selectEntangler(const NativeProfileSpec& spec) { - if (llvm::is_contained(spec.entanglerBases, EntanglerBasis::Cx)) { - return EntanglerBasis::Cx; - } - if (llvm::is_contained(spec.entanglerBases, EntanglerBasis::Cz)) { - return EntanglerBasis::Cz; - } - return std::nullopt; -} - -[[nodiscard]] static Matrix4x4 entanglerMatrix(EntanglerBasis entangler) { - return entangler == EntanglerBasis::Cz ? czGate() : cxGate01(); -} - -[[nodiscard]] static std::optional -decomposeWithEntangler(const Matrix4x4& target, EntanglerBasis entangler) { - auto decomposer = - TwoQubitBasisDecomposer::create(entanglerMatrix(entangler), 1.0); - auto weyl = TwoQubitWeylDecomposition::create(target, std::nullopt); - return decomposer.twoQubitDecompose(weyl, std::nullopt); -} - -static void emitGPhaseIfNonTrivial(OpBuilder& builder, Location loc, - double phase) { - constexpr double epsilon = 1e-12; - if (std::abs(phase) > epsilon) { - GPhaseOp::create(builder, loc, phase); - } -} - -[[nodiscard]] static Value emitSingleQubitMatrix(OpBuilder& builder, - Location loc, Value inQubit, - const Matrix2x2& matrix, - EulerBasis basis) { - return *synthesizeUnitary1QEuler(builder, loc, inQubit, matrix, - /*runSize=*/0, /*hasNonBasisGate=*/true, - basis); -} - -EulerBasis emitterEulerBasis(const SingleQubitEmitterSpec& emitter) { - switch (emitter.mode) { - case SingleQubitMode::ZSXX: - return EulerBasis::ZSXX; - case SingleQubitMode::U3: - return EulerBasis::U; - case SingleQubitMode::R: - return EulerBasis::R; - case SingleQubitMode::AxisPair: - switch (emitter.axisPair) { - case AxisPair::RxRz: - return EulerBasis::XZX; - case AxisPair::RxRy: - return EulerBasis::XYX; - case AxisPair::RyRz: - return EulerBasis::ZYZ; - } - break; - } - llvm_unreachable("unknown single-qubit mode"); -} - -std::optional parseNativeSpec(llvm::StringRef nativeGates) { - const auto gates = parseGateSet(nativeGates); - if (!gates || gates->empty()) { - return std::nullopt; - } - const auto has = [&](NativeGateKind kind) { return gates->contains(kind); }; - - NativeProfileSpec spec; - - if (has(NativeGateKind::U)) { - addEmitterIfAbsent(spec.singleQubitEmitters, SingleQubitMode::U3); - } - const bool hasXSxRz = has(NativeGateKind::X) && has(NativeGateKind::Sx) && - has(NativeGateKind::Rz); - if (hasXSxRz) { - addEmitterIfAbsent(spec.singleQubitEmitters, SingleQubitMode::ZSXX, - AxisPair::RxRz, - /*supportsDirectRx=*/has(NativeGateKind::Rx)); - } - if (has(NativeGateKind::R)) { - addEmitterIfAbsent(spec.singleQubitEmitters, SingleQubitMode::R); - } - struct AxisPairRule { - AxisPair axis; - NativeGateKind left; - NativeGateKind right; - }; - for (const auto& rule : { - AxisPairRule{.axis = AxisPair::RxRz, - .left = NativeGateKind::Rx, - .right = NativeGateKind::Rz}, - AxisPairRule{.axis = AxisPair::RxRy, - .left = NativeGateKind::Rx, - .right = NativeGateKind::Ry}, - AxisPairRule{.axis = AxisPair::RyRz, - .left = NativeGateKind::Ry, - .right = NativeGateKind::Rz}, - }) { - if (has(rule.left) && has(rule.right)) { - addEmitterIfAbsent(spec.singleQubitEmitters, SingleQubitMode::AxisPair, - rule.axis); - } - } - if (spec.singleQubitEmitters.empty()) { - return std::nullopt; - } - - if (has(NativeGateKind::Cx)) { - spec.entanglerBases.push_back(EntanglerBasis::Cx); - } - if (has(NativeGateKind::Cz)) { - spec.entanglerBases.push_back(EntanglerBasis::Cz); - } - spec.allowRzz = has(NativeGateKind::Rzz); - - populateAllowedGates(spec); - return spec; -} - -LogicalResult synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, - Value qubit0, Value qubit1, - const Matrix4x4& target, - const NativeProfileSpec& spec, - Value& outQubit0, Value& outQubit1) { - const auto entangler = selectEntangler(spec); - if (!entangler) { - return failure(); - } - const auto native = decomposeWithEntangler(target, *entangler); - if (!native) { - return failure(); - } - const auto basis = emitterEulerBasis(spec.singleQubitEmitters.front()); - - emitGPhaseIfNonTrivial(builder, loc, native->globalPhase); - - Value wire0 = qubit0; - Value wire1 = qubit1; - const auto& factors = native->singleQubitFactors; - const std::uint8_t numBasisUses = native->numBasisUses; - const auto emitFactor = [&](Value& wire, std::size_t index) { - wire = emitSingleQubitMatrix(builder, loc, wire, factors[index], basis); - }; - const auto emitEntangler = [&]() { - auto ctrlOp = CtrlOp::create( - builder, loc, ValueRange{wire0}, ValueRange{wire1}, - [&](ValueRange targetArgs) -> llvm::SmallVector { - if (*entangler == EntanglerBasis::Cz) { - return {ZOp::create(builder, loc, targetArgs[0]).getOutputQubit(0)}; - } - return {XOp::create(builder, loc, targetArgs[0]).getOutputQubit(0)}; - }); - wire0 = ctrlOp.getOutputControl(0); - wire1 = ctrlOp.getOutputTarget(0); - }; - - for (std::uint8_t i = 0; i < numBasisUses; ++i) { - emitFactor(wire1, static_cast(2 * i)); - emitFactor(wire0, static_cast((2 * i) + 1)); - emitEntangler(); - } - emitFactor(wire1, static_cast(2 * numBasisUses)); - emitFactor(wire0, static_cast((2 * numBasisUses) + 1)); - - outQubit0 = wire0; - outQubit1 = wire1; - return success(); -} - -std::optional -twoQubitEntanglerCount(const Matrix4x4& target, const NativeProfileSpec& spec) { - const auto entangler = selectEntangler(spec); - if (!entangler) { - return std::nullopt; - } - const auto native = decomposeWithEntangler(target, *entangler); - if (!native) { - return std::nullopt; - } - return native->numBasisUses; -} - -Matrix2x2 rxMatrix(double theta) { - const auto halfTheta = theta / 2.; - const Complex cos{std::cos(halfTheta), 0.}; - const Complex isin{0., -std::sin(halfTheta)}; - return Matrix2x2::fromElements(cos, isin, isin, cos); -} - -Matrix2x2 ryMatrix(double theta) { - const auto halfTheta = theta / 2.; - const Complex cos{std::cos(halfTheta), 0.}; - const Complex sin{std::sin(halfTheta), 0.}; - return Matrix2x2::fromElements(cos, -sin, sin, cos); -} - -Matrix2x2 rzMatrix(double theta) { - return Matrix2x2::fromElements( - Complex{std::cos(theta / 2.), -std::sin(theta / 2.)}, 0., 0., - Complex{std::cos(theta / 2.), std::sin(theta / 2.)}); -} - -const Matrix2x2& ipz() { - static const Matrix2x2 MATRIX = - Matrix2x2::fromElements(Complex{0, 1}, 0, 0, Complex{0, -1}); - return MATRIX; -} - -const Matrix2x2& ipy() { - static const Matrix2x2 MATRIX = Matrix2x2::fromElements(0, 1, -1, 0); - return MATRIX; -} - -const Matrix2x2& ipx() { - static const Matrix2x2 MATRIX = - Matrix2x2::fromElements(0, Complex{0, 1}, Complex{0, 1}, 0); - return MATRIX; -} - } // namespace mlir::qco::decomposition diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp index 25ba7cb8d0..a7344a665d 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp @@ -11,7 +11,7 @@ #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/NativeProfile.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" @@ -28,26 +28,31 @@ #include #include #include +#include #include #include #include #include -#include namespace mlir::qco { #define GEN_PASS_DEF_FUSETWOQUBITUNITARYRUNS #include "mlir/Dialect/QCO/Transforms/Passes.h.inc" +/// Skips unitaries nested under `ctrl`/`inv` bodies (handled on the shell op). +static bool isExcludedFromTopLevelUnitaryWalk(Operation* op) { + if (op->getParentOfType()) { + return true; + } + return !llvm::isa(op) && op->getParentOfType(); +} + static void collectUnitaryOpsInPreOrder(Operation* root, llvm::SmallVectorImpl& ops) { root->walk([&](Operation* op) { - if (op->getParentOfType()) { - return; - } - if (!llvm::isa(op) && op->getParentOfType()) { + if (isExcludedFromTopLevelUnitaryWalk(op)) { return; } if (llvm::isa(op)) { @@ -59,388 +64,277 @@ collectUnitaryOpsInPreOrder(Operation* root, static Value emitSingleQubitMatrix(IRRewriter& rewriter, Location loc, Value inQubit, const Matrix2x2& matrix, const decomposition::EulerBasis basis) { - // Force emission (`hasNonBasisGate = true`, `runSize = 0`) so the matrix is - // always lowered into native gates of `basis`, including any residual - // `qco.gphase`. With these arguments `synthesizeUnitary1QEuler` never - // returns `std::nullopt`. return *decomposition::synthesizeUnitary1QEuler( rewriter, loc, inQubit, matrix, /*runSize=*/0, /*hasNonBasisGate=*/true, basis); } -/// State for one maximal two-qubit window (plus absorbed one-qubit ops) -/// during consolidation. -struct TwoQubitBlock { - Value wireA; - Value wireB; - llvm::SmallVector ops; - Matrix4x4 accum = Matrix4x4::identity(); - unsigned numTwoQ = 0; - unsigned numOneQ = 0; - bool anyNonNative = false; - bool open = true; -}; - -/// Tracks overlapping two-qubit windows on a module slice. -struct TwoQubitWindowConsolidator { - std::vector blocks; - llvm::DenseMap wireToBlock; - - void closeBlock(size_t idx); - void closeBlockOnWire(Value v); - void process(Operation* op, const decomposition::NativeProfileSpec& spec); - LogicalResult materialize(IRRewriter& rewriter, - const decomposition::NativeProfileSpec& spec); -}; - -/// Map a single-qubit `UnitaryOpInterface` op to the -/// `decomposition::NativeGateKind` that must appear in the menu for the op to -/// be a no-op. -static std::optional -singleQubitNativeGateKind(UnitaryOpInterface op) { - Operation* raw = op.getOperation(); - if (llvm::isa(raw)) { - return decomposition::NativeGateKind::U; - } - if (llvm::isa(raw)) { - return decomposition::NativeGateKind::X; - } - if (llvm::isa(raw)) { - return decomposition::NativeGateKind::Sx; - } - if (llvm::isa(raw)) { - // `p` is a Z-rotation primitive for menu purposes. - return decomposition::NativeGateKind::Rz; - } - if (llvm::isa(raw)) { - return decomposition::NativeGateKind::Rx; - } - if (llvm::isa(raw)) { - return decomposition::NativeGateKind::Ry; +static bool isTwoQubitRunMember(UnitaryOpInterface unitary) { + if (!unitary || !unitary.isTwoQubit()) { + return false; } - if (llvm::isa(raw)) { - return decomposition::NativeGateKind::R; + if (llvm::isa(unitary.getOperation())) { + return false; } - return std::nullopt; -} - -// NOLINTNEXTLINE(misc-use-internal-linkage): test-visible (see comment above). -static bool allowsSingleQubitOp(UnitaryOpInterface op, - const decomposition::NativeProfileSpec& spec) { - if (llvm::isa(op.getOperation())) { - return true; + if (isExcludedFromTopLevelUnitaryWalk(unitary.getOperation())) { + return false; } - const auto gate = singleQubitNativeGateKind(op); - return gate && spec.allowedGates.contains(*gate); + Matrix4x4 matrix; + return decomposition::assignTwoQubitOpMatrix(unitary.getOperation(), matrix); } -static bool getBlockTwoQubitMatrix(Operation* op, Matrix4x4& matrix) { - if (llvm::isa(op)) { +static bool isOneQubitWindowMember(UnitaryOpInterface unitary) { + if (!unitary || !unitary.isSingleQubit()) { return false; } - if (auto ctrl = llvm::dyn_cast(op)) { - if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { - return false; - } - auto* body = ctrl.getBodyUnitary(0).getOperation(); - if (llvm::isa(body)) { - matrix = Matrix4x4::fromElements(1, 0, 0, 0, // - 0, 1, 0, 0, // - 0, 0, 0, 1, // - 0, 0, 1, 0); - return true; - } - if (llvm::isa(body)) { - matrix = Matrix4x4::identity(); - matrix(3, 3) = -1.0; - return true; - } + if (llvm::isa(unitary.getOperation())) { return false; } - auto unitary = llvm::dyn_cast(op); - if (!unitary || !unitary.isTwoQubit()) { + if (isExcludedFromTopLevelUnitaryWalk(unitary.getOperation())) { return false; } - Matrix4x4 raw; - if (!unitary.getUnitaryMatrix4x4(raw)) { - return false; + Matrix2x2 matrix; + return unitary.getUnitaryMatrix2x2(matrix); +} + +static UnitaryOpInterface uniqueUnitaryUser(Value wire) { + if (!wire.hasOneUse()) { + return {}; } - matrix = raw; - return true; + auto unitary = llvm::dyn_cast(*wire.user_begin()); + if (!unitary) { + return {}; + } + if (unitary.isTwoQubit()) { + return isTwoQubitRunMember(unitary) ? unitary : UnitaryOpInterface{}; + } + if (unitary.isSingleQubit()) { + return isOneQubitWindowMember(unitary) ? unitary : UnitaryOpInterface{}; + } + return {}; } -/// Check whether a two-qubit op `op` is already expressible by the resolved -/// native menu: a single-control `CX`/`CZ` consistent with the active -/// entangler, or `Rzz` when `spec.allowRzz` is set. Multi-control and other -/// two-qubit ops are considered non-native. -static bool isNativeTwoQubitOp(Operation* op, - const decomposition::NativeProfileSpec& spec) { - if (auto ctrl = llvm::dyn_cast(op)) { - if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { - return false; +static Operation* twoQubitGateAtEndOfOneQChain(Value wire) { + Value cur = wire; + while (Operation* def = cur.getDefiningOp()) { + auto unitary = llvm::dyn_cast(def); + if (!unitary) { + return nullptr; } - auto* body = ctrl.getBodyUnitary(0).getOperation(); - if (llvm::isa(body)) { - return llvm::is_contained(spec.entanglerBases, - decomposition::EntanglerBasis::Cx); + if (unitary.isTwoQubit()) { + return isTwoQubitRunMember(unitary) ? def : nullptr; } - if (llvm::isa(body)) { - return llvm::is_contained(spec.entanglerBases, - decomposition::EntanglerBasis::Cz); + if (!isOneQubitWindowMember(unitary)) { + return nullptr; } - return false; + cur = unitary.getInputQubit(0); } - return spec.allowRzz && llvm::isa(op); + return nullptr; } -/// Decide whether replacing a consolidated window is worthwhile. Always -/// replace a window that contains any non-native op (we have to lower them -/// anyway); otherwise only replace when the deterministic synthesizer uses -/// strictly fewer entanglers than the window already contains. -static bool shouldApplyBlockReplacement(const TwoQubitBlock& block, - std::uint8_t numBasisUses) { - if (block.anyNonNative) { - return true; +static bool feedsFromSameTwoQubitWindow(UnitaryOpInterface op) { + const Value in0 = op.getInputQubit(0); + const Value in1 = op.getInputQubit(1); + if (!in0.hasOneUse() || !in1.hasOneUse()) { + return false; } - return numBasisUses < block.numTwoQ; + Operation* gate0 = twoQubitGateAtEndOfOneQChain(in0); + Operation* gate1 = twoQubitGateAtEndOfOneQChain(in1); + return gate0 != nullptr && gate0 == gate1; } -static LogicalResult -materializeSingleTwoQubitBlock(IRRewriter& rewriter, const TwoQubitBlock& block, - const decomposition::NativeProfileSpec& spec) { - Operation* firstOp = block.ops.front(); - auto firstUnitary = llvm::cast(firstOp); - const Value inA = firstUnitary.getInputQubit(0); - const Value inB = firstUnitary.getInputQubit(1); - const Value outA = block.wireA; - const Value outB = block.wireB; - - rewriter.setInsertionPoint(firstOp); - Value newA; - Value newB; - if (failed(decomposition::synthesizeUnitary2QWeyl(rewriter, firstOp->getLoc(), - inA, inB, block.accum, spec, - newA, newB))) { - firstOp->emitError("failed to emit synthesized two-qubit gate sequence"); - return failure(); - } - rewriter.replaceAllUsesWith(outA, newA); - rewriter.replaceAllUsesWith(outB, newB); - for (auto* toErase : llvm::reverse(block.ops)) { - rewriter.eraseOp(toErase); - } - return success(); +static bool isTwoQubitRunStart(UnitaryOpInterface op) { + return isTwoQubitRunMember(op) && !feedsFromSameTwoQubitWindow(op); } -void TwoQubitWindowConsolidator::closeBlock(size_t idx) { - auto& block = blocks[idx]; - if (!block.open) { - return; - } - block.open = false; - wireToBlock.erase(block.wireA); - if (block.wireB != block.wireA) { - wireToBlock.erase(block.wireB); - } -} +struct FusableTwoQubitRun { + llvm::SmallVector ops; + Matrix4x4 composed = Matrix4x4::identity(); + unsigned numTwoQ = 0; + bool anyNonNative = false; + Value tailA; + Value tailB; +}; -void TwoQubitWindowConsolidator::closeBlockOnWire(Value v) { - if (auto it = wireToBlock.find(v); it != wireToBlock.end()) { - closeBlock(it->second); +// Replace when off-menu ops must be lowered, or when resynthesis uses fewer +// entanglers than the fused window. +static bool shouldApplyTwoQubitRunReplacement(const FusableTwoQubitRun& run, + std::uint8_t numBasisUses) { + if (run.anyNonNative) { + return true; } + return numBasisUses < run.numTwoQ; } -void TwoQubitWindowConsolidator::process( - Operation* op, const decomposition::NativeProfileSpec& spec) { - if (op->getParentOfType()) { +static void +absorbTwoQubitIntoRun(FusableTwoQubitRun& run, UnitaryOpInterface op, + const decomposition::NativeProfileSpec& spec) { + Matrix4x4 opMatrix; + if (!decomposition::assignTwoQubitOpMatrix(op.getOperation(), opMatrix)) { return; } - if (!llvm::isa(op) && op->getParentOfType()) { + const Value in0 = op.getInputQubit(0); + const Value in1 = op.getInputQubit(1); + llvm::SmallVector ids; + if (in0 == run.tailA && in1 == run.tailB) { + ids = {0, 1}; + run.tailA = op.getOutputQubit(0); + run.tailB = op.getOutputQubit(1); + } else if (in0 == run.tailB && in1 == run.tailA) { + ids = {1, 0}; + run.tailA = op.getOutputQubit(1); + run.tailB = op.getOutputQubit(0); + } else { return; } - auto unitary = llvm::dyn_cast(op); - if (!unitary) { - return; + run.composed = reorderTwoQubitMatrix(opMatrix, ids[0], ids[1]) * run.composed; + run.ops.push_back(op.getOperation()); + ++run.numTwoQ; + if (!decomposition::allowsTwoQubitOp(op.getOperation(), spec)) { + run.anyNonNative = true; } - if (llvm::isa(op)) { - for (Value v : op->getOperands()) { - closeBlockOnWire(v); - } +} + +static void absorbOneQubitIntoRun(FusableTwoQubitRun& run, + UnitaryOpInterface op, + const decomposition::NativeProfileSpec& spec, + unsigned wireIndex) { + Matrix2x2 raw; + if (!op.getUnitaryMatrix2x2(raw)) { return; } + const auto pad = embedSingleQubitInTwoQubit(raw, wireIndex); + run.composed = pad * run.composed; + run.ops.push_back(op.getOperation()); + if (!decomposition::allowsSingleQubitOp(op, spec)) { + run.anyNonNative = true; + } + if (wireIndex == 0) { + run.tailA = op.getOutputQubit(0); + } else { + run.tailB = op.getOutputQubit(0); + } +} - if (unitary.isTwoQubit()) { - Matrix4x4 opMatrix; - if (!getBlockTwoQubitMatrix(op, opMatrix)) { - closeBlockOnWire(unitary.getInputQubit(0)); - closeBlockOnWire(unitary.getInputQubit(1)); - return; - } - const Value v0 = unitary.getInputQubit(0); - const Value v1 = unitary.getInputQubit(1); - if (v0 == v1) { - closeBlockOnWire(v0); - return; +static FusableTwoQubitRun +scanFusableTwoQubitRun(UnitaryOpInterface head, + const decomposition::NativeProfileSpec& spec) { + FusableTwoQubitRun run; + run.tailA = head.getOutputQubit(0); + run.tailB = head.getOutputQubit(1); + run.ops.push_back(head.getOperation()); + run.numTwoQ = 1; + if (!decomposition::assignTwoQubitOpMatrix(head.getOperation(), + run.composed)) { + run.composed = Matrix4x4::identity(); + run.numTwoQ = 0; + run.ops.clear(); + return run; + } + if (!decomposition::allowsTwoQubitOp(head.getOperation(), spec)) { + run.anyNonNative = true; + } + + while (true) { + UnitaryOpInterface nextOnA = uniqueUnitaryUser(run.tailA); + UnitaryOpInterface nextOnB = uniqueUnitaryUser(run.tailB); + + if (nextOnA && nextOnA == nextOnB && nextOnA.isTwoQubit()) { + absorbTwoQubitIntoRun(run, nextOnA, spec); + continue; } - auto it0 = wireToBlock.find(v0); - auto it1 = wireToBlock.find(v1); - const bool tracked0 = it0 != wireToBlock.end(); - const bool tracked1 = it1 != wireToBlock.end(); - const std::optional idx0 = - tracked0 ? std::optional(it0->second) : std::nullopt; - const std::optional idx1 = - tracked1 ? std::optional(it1->second) : std::nullopt; - const bool sameBlock = - idx0.has_value() && idx1.has_value() && *idx0 == *idx1; - const bool singleUse = v0.hasOneUse() && v1.hasOneUse(); - - if (sameBlock && singleUse) { - const size_t idx = *idx0; - auto& block = blocks[idx]; - llvm::SmallVector ids; - if (v0 == block.wireA && v1 == block.wireB) { - ids = {0, 1}; - } else if (v0 == block.wireB && v1 == block.wireA) { - ids = {1, 0}; - } else { - closeBlock(idx); - return; - } - block.accum = - reorderTwoQubitMatrix(opMatrix, ids[0], ids[1]) * block.accum; - block.ops.push_back(op); - ++block.numTwoQ; - if (!isNativeTwoQubitOp(op, spec)) { - block.anyNonNative = true; - } - const Value eraseKeyA = it0->first; - const Value eraseKeyB = it1->first; - wireToBlock.erase(eraseKeyA); - if (eraseKeyA != eraseKeyB) { - wireToBlock.erase(eraseKeyB); - } - Value newA; - Value newB; - if (v0 == block.wireA) { - newA = unitary.getOutputQubit(0); - newB = unitary.getOutputQubit(1); - } else { - newA = unitary.getOutputQubit(1); - newB = unitary.getOutputQubit(0); + + if (nextOnA && nextOnB && nextOnA != nextOnB && nextOnA.isSingleQubit() && + nextOnB.isSingleQubit()) { + if (nextOnA->isBeforeInBlock(nextOnB)) { + absorbOneQubitIntoRun(run, nextOnA, spec, /*wireIndex=*/0); + continue; } - block.wireA = newA; - block.wireB = newB; - wireToBlock[newA] = idx; - wireToBlock[newB] = idx; - return; + absorbOneQubitIntoRun(run, nextOnB, spec, /*wireIndex=*/1); + continue; } - if (idx0.has_value()) { - closeBlock(*idx0); - } - if (idx1.has_value() && (!idx0.has_value() || *idx0 != *idx1)) { - closeBlock(*idx1); + if (nextOnA && nextOnA.isSingleQubit() && nextOnA != nextOnB) { + absorbOneQubitIntoRun(run, nextOnA, spec, /*wireIndex=*/0); + continue; } - TwoQubitBlock nb; - nb.wireA = unitary.getOutputQubit(0); - nb.wireB = unitary.getOutputQubit(1); - nb.ops.push_back(op); - nb.numTwoQ = 1; - nb.accum = opMatrix; - nb.anyNonNative = !isNativeTwoQubitOp(op, spec); - const size_t idx = blocks.size(); - blocks.push_back(std::move(nb)); - wireToBlock[blocks[idx].wireA] = idx; - wireToBlock[blocks[idx].wireB] = idx; - return; - } - if (unitary.isSingleQubit()) { - const Value v = unitary.getInputQubit(0); - auto it = wireToBlock.find(v); - if (it == wireToBlock.end()) { - return; - } - const size_t idx = it->second; - auto& block = blocks[idx]; - Matrix2x2 raw; - if (!unitary.getUnitaryMatrix2x2(raw) || !v.hasOneUse()) { - closeBlock(idx); - return; - } - const auto pad = (v == block.wireA) ? embedSingleQubitInTwoQubit(raw, 0) - : embedSingleQubitInTwoQubit(raw, 1); - block.accum = pad * block.accum; - block.ops.push_back(op); - ++block.numOneQ; - if (!allowsSingleQubitOp(unitary, spec)) { - block.anyNonNative = true; - } - wireToBlock.erase(it); - if (v == block.wireA) { - block.wireA = unitary.getOutputQubit(0); - wireToBlock[block.wireA] = idx; - } else { - block.wireB = unitary.getOutputQubit(0); - wireToBlock[block.wireB] = idx; + if (nextOnB && nextOnB.isSingleQubit() && nextOnB != nextOnA) { + absorbOneQubitIntoRun(run, nextOnB, spec, /*wireIndex=*/1); + continue; } - return; + + break; } + return run; +} - for (Value v : op->getOperands()) { - closeBlockOnWire(v); +static void eraseFusableTwoQubitRun(PatternRewriter& rewriter, + const FusableTwoQubitRun& run) { + for (Operation* op : llvm::reverse(run.ops)) { + rewriter.eraseOp(op); } } -LogicalResult TwoQubitWindowConsolidator::materialize( - IRRewriter& rewriter, const decomposition::NativeProfileSpec& spec) { - llvm::DenseSet erasedOps; - for (const auto& block : blocks) { - if (block.ops.size() < 2) { - continue; +struct FuseTwoQubitWindowPattern + : public OpInterfaceRewritePattern { + FuseTwoQubitWindowPattern(MLIRContext* ctx, + const decomposition::NativeProfileSpec& spec) + : OpInterfaceRewritePattern(ctx), spec(spec) {} + + LogicalResult matchAndRewrite(UnitaryOpInterface op, + PatternRewriter& rewriter) const override { + if (!isTwoQubitRunStart(op)) { + return failure(); } - if (llvm::any_of(block.ops, - [&](Operation* op) { return erasedOps.contains(op); })) { - continue; + + FusableTwoQubitRun run = scanFusableTwoQubitRun(op, spec); + if (run.ops.size() < 2) { + return failure(); } + const auto numBasisUses = - decomposition::twoQubitEntanglerCount(block.accum, spec); - if (!numBasisUses) { - continue; - } - if (!shouldApplyBlockReplacement(block, *numBasisUses)) { - continue; - } - if (failed(materializeSingleTwoQubitBlock(rewriter, block, spec))) { + decomposition::twoQubitEntanglerCount(run.composed, spec); + if (!numBasisUses || + !shouldApplyTwoQubitRunReplacement(run, *numBasisUses)) { return failure(); } - for (Operation* op : block.ops) { - erasedOps.insert(op); + + Operation* firstOp = run.ops.front(); + auto firstUnitary = llvm::cast(firstOp); + const Value inA = firstUnitary.getInputQubit(0); + const Value inB = firstUnitary.getInputQubit(1); + + rewriter.setInsertionPoint(firstOp); + Value newA; + Value newB; + if (failed(decomposition::synthesizeUnitary2QWeyl( + rewriter, firstOp->getLoc(), inA, inB, run.composed, spec, newA, + newB))) { + firstOp->emitError("failed to emit synthesized two-qubit gate sequence"); + return failure(); } + rewriter.replaceAllUsesWith(run.tailA, newA); + rewriter.replaceAllUsesWith(run.tailB, newB); + eraseFusableTwoQubitRun(rewriter, run); + return success(); } - return success(); -} + + const decomposition::NativeProfileSpec& spec; +}; static LogicalResult -fuseTwoQubitUnitaryRuns(IRRewriter& rewriter, Operation* root, +fuseTwoQubitUnitaryRuns(Operation* root, const decomposition::NativeProfileSpec& spec) { - llvm::SmallVector ops; - collectUnitaryOpsInPreOrder(root, ops); - TwoQubitWindowConsolidator consolidator; - for (Operation* op : ops) { - consolidator.process(op, spec); - } - return consolidator.materialize(rewriter, spec); + RewritePatternSet patterns(root->getContext()); + patterns.add(patterns.getContext(), spec); + return applyPatternsGreedily(root, std::move(patterns)); } -/// Adjacent single-qubit unitaries on one wire considered for fusion. struct OneQubitRun { llvm::SmallVector ops; }; -/// If profitable, replace the run with one synthesized single-qubit op in -/// `basis` (mirrors `FuseSingleQubitUnitaryRuns`). Fuses when any op is -/// off-menu or when Euler resynthesis strictly shortens the run. static bool maybeFuseRun(IRRewriter& rewriter, OneQubitRun& run, const decomposition::EulerBasis basis, const decomposition::NativeProfileSpec& spec) { @@ -454,7 +348,7 @@ static bool maybeFuseRun(IRRewriter& rewriter, OneQubitRun& run, } const bool anyNonNative = llvm::any_of(run.ops, [&](UnitaryOpInterface u) { - return !allowsSingleQubitOp(u, spec); + return !decomposition::allowsSingleQubitOp(u, spec); }); Operation* firstOp = run.ops.front().getOperation(); @@ -475,19 +369,6 @@ static bool maybeFuseRun(IRRewriter& rewriter, OneQubitRun& run, return true; } -/// True when `op` lives in a `ctrl`/`inv` region body (not the shell op). -/// Skips nested unitaries so they are handled via the enclosing modifier. -static bool isHiddenInsideCtrlOrInvBody(Operation* op) { - if (op->getParentOfType()) { - return true; - } - if (!llvm::isa(op) && op->getParentOfType()) { - return true; - } - return false; -} - -/// Single-qubit op eligible for fusion (constant `2×2`, not under `ctrl`). static UnitaryOpInterface fusibleSingleQubitOp(Operation* op) { auto unitary = llvm::dyn_cast(op); if (!unitary || !unitary.isSingleQubit()) { @@ -496,7 +377,7 @@ static UnitaryOpInterface fusibleSingleQubitOp(Operation* op) { if (llvm::isa(op)) { return {}; } - if (isHiddenInsideCtrlOrInvBody(op)) { + if (isExcludedFromTopLevelUnitaryWalk(op)) { return {}; } Matrix2x2 matrix; @@ -506,26 +387,15 @@ static UnitaryOpInterface fusibleSingleQubitOp(Operation* op) { return unitary; } -/// Lowers unitary QCO ops to a comma-separated native gate menu using a -/// deterministic, matrix-driven synthesizer: single-qubit fuse, two-qubit -/// window consolidation, synthesis sweeps, seam single-qubit fuse, and -/// optional cleanup sweeps. struct FuseTwoQubitUnitaryRunsPass : impl::FuseTwoQubitUnitaryRunsBase { - /// Default-construct the pass with the TableGen-generated option defaults. FuseTwoQubitUnitaryRunsPass() = default; explicit FuseTwoQubitUnitaryRunsPass(FuseTwoQubitUnitaryRunsOptions options) : FuseTwoQubitUnitaryRunsBase(std::move(options)) {} protected: - /// Top-level pass entry point. Resolves the native-gate menu, then drives - /// the staged rewrite pipeline: one-qubit run fusion, two-qubit window - /// consolidation, synthesis sweeps until the single-qubit surface is native, - /// seam cleanup, and a final fusion pass. Fails the pass on invalid input or - /// non-convergence. void runOnOperation() override { - // Empty native-gates string: no-op. if (llvm::StringRef(nativeGates).trim().empty()) { return; } @@ -538,20 +408,16 @@ struct FuseTwoQubitUnitaryRunsPass return; } const auto& spec = *specOpt; - // Deterministic single-qubit basis: the first emitter drives all matrix - // synthesis and run fusion. const decomposition::EulerBasis oneQubitBasis = decomposition::emitterEulerBasis(spec.singleQubitEmitters.front()); IRRewriter rewriter(&getContext()); fuseOneQubitRuns(rewriter, spec, oneQubitBasis); - if (failed(consolidateTwoQubitBlocks(rewriter, spec))) { + if (failed(fuseTwoQubitUnitaryRuns(getOperation(), spec))) { signalPassFailure(); return; } - // Two-qubit lowering can emit off-menu single-qubit ops (e.g. `rx`/`ry`); - // repeat until clean or hit the sweep cap before seam cleanup. constexpr unsigned kMaxSynthesisSweeps = 4; for (unsigned i = 0; i < kMaxSynthesisSweeps; ++i) { if (failed(synthesizeRemainingOps(rewriter, spec, oneQubitBasis))) { @@ -570,9 +436,7 @@ struct FuseTwoQubitUnitaryRunsPass signalPassFailure(); return; } - // Fuse single-qubit seams between two-qubit blocks. fuseOneQubitRuns(rewriter, spec, oneQubitBasis); - // Re-check full menu (single-qubit ops, native `ctrl`, allowed bare `rzz`). constexpr unsigned kPostMenuCleanupSweeps = 4; unsigned postMenuSweepsRemaining = kPostMenuCleanupSweeps; while (hasNonNativeMenuOps(spec) && postMenuSweepsRemaining-- > 0) { @@ -591,49 +455,17 @@ struct FuseTwoQubitUnitaryRunsPass } } - /// `CtrlOp` is already on-menu when the body is `X`/`Z` and the profile - /// supplies `cx` / `cz` entanglers. - static bool - ctrlMatchesNativeMenu(CtrlOp ctrl, - const decomposition::NativeProfileSpec& spec) { - if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { - return false; - } - Operation* body = ctrl.getBodyUnitary(0).getOperation(); - const bool hasCX = llvm::isa(body); - const bool hasCZ = llvm::isa(body); - if (!hasCX && !hasCZ) { - return false; - } - return (llvm::is_contained(spec.entanglerBases, - decomposition::EntanglerBasis::Cx) && - hasCX) || - (llvm::is_contained(spec.entanglerBases, - decomposition::EntanglerBasis::Cz) && - hasCZ); - } - - /// Bare two-qubit on-menu: `rzz` when the profile allows it. - static bool - bareTwoQubitMatchesNativeMenu(Operation* op, - const decomposition::NativeProfileSpec& spec) { - return llvm::isa(op) && spec.allowRzz && - spec.allowedGates.contains(decomposition::NativeGateKind::Rzz); - } - - /// True if any unitary is outside `spec` (single-qubit, `ctrl`, or bare - /// `rzz`). bool hasNonNativeMenuOps(const decomposition::NativeProfileSpec& spec) { const mlir::WalkResult walkResult = getOperation()->walk([&](Operation* op) { if (llvm::isa(op)) { return mlir::WalkResult::advance(); } - if (isHiddenInsideCtrlOrInvBody(op)) { + if (isExcludedFromTopLevelUnitaryWalk(op)) { return mlir::WalkResult::advance(); } if (auto ctrl = llvm::dyn_cast(op)) { - if (!ctrlMatchesNativeMenu(ctrl, spec)) { + if (!decomposition::allowsSingleTargetCtrl(ctrl, spec)) { return mlir::WalkResult::interrupt(); } return mlir::WalkResult::advance(); @@ -643,13 +475,13 @@ struct FuseTwoQubitUnitaryRunsPass return mlir::WalkResult::advance(); } if (unitary.isSingleQubit()) { - if (!allowsSingleQubitOp(unitary, spec)) { + if (!decomposition::allowsSingleQubitOp(unitary, spec)) { return mlir::WalkResult::interrupt(); } return mlir::WalkResult::advance(); } if (unitary.isTwoQubit()) { - if (!bareTwoQubitMatchesNativeMenu(op, spec)) { + if (!decomposition::allowsBareTwoQubitOp(op, spec)) { return mlir::WalkResult::interrupt(); } return mlir::WalkResult::advance(); @@ -659,7 +491,6 @@ struct FuseTwoQubitUnitaryRunsPass return walkResult.wasInterrupted(); } - /// Any off-menu single-qubit unitary (ignores `ctrl` region bodies). bool hasNonNativeSingleQubitOps(const decomposition::NativeProfileSpec& spec) { const mlir::WalkResult walkResult = @@ -667,14 +498,14 @@ struct FuseTwoQubitUnitaryRunsPass if (llvm::isa(op)) { return mlir::WalkResult::advance(); } - if (isHiddenInsideCtrlOrInvBody(op)) { + if (isExcludedFromTopLevelUnitaryWalk(op)) { return mlir::WalkResult::advance(); } auto unitary = llvm::dyn_cast(op); if (!unitary || !unitary.isSingleQubit()) { return mlir::WalkResult::advance(); } - if (!allowsSingleQubitOp(unitary, spec)) { + if (!decomposition::allowsSingleQubitOp(unitary, spec)) { return mlir::WalkResult::interrupt(); } return mlir::WalkResult::advance(); @@ -683,19 +514,13 @@ struct FuseTwoQubitUnitaryRunsPass } private: - /// Fuse adjacent single-qubit runs when the emitter wins on length or any op - /// is off-menu. void fuseOneQubitRuns(IRRewriter& rewriter, const decomposition::NativeProfileSpec& spec, const decomposition::EulerBasis basis) { llvm::SmallVector runs; llvm::DenseMap tailOpToRun; - // Extend the current run only when this op consumes the run's *tail* - // output with no other uses: both the `tailOpToRun` lookup and - // `inQubit.hasOneUse()` are required. Without the single-use check a run - // could fuse gates on a wire that also feeds another path (fan-out), - // which would silently drop the sibling user. + // Require single-use tail output so fan-out wires are not fused away. getOperation()->walk([&](Operation* op) { auto unitary = fusibleSingleQubitOp(op); if (!unitary) { @@ -726,19 +551,6 @@ struct FuseTwoQubitUnitaryRunsPass } } - /// Two-qubit windows with absorbed single-qubit ops: replace when a cheaper - /// native sequence exists. - LogicalResult - consolidateTwoQubitBlocks(IRRewriter& rewriter, - const decomposition::NativeProfileSpec& spec) { - return fuseTwoQubitUnitaryRuns(rewriter, getOperation(), spec); - } - - /// One synthesis sweep over the whole function: rewrite every remaining - /// off-menu unitary by dispatching to `rewriteSingleQubit` / - /// `rewriteControlled` / `rewriteTwoQubit`. Returns `failure()` as soon as - /// any op cannot be lowered to the native menu. Safe to call repeatedly; - /// `runOnOperation` iterates until convergence. LogicalResult synthesizeRemainingOps(IRRewriter& rewriter, const decomposition::NativeProfileSpec& spec, @@ -748,14 +560,10 @@ struct FuseTwoQubitUnitaryRunsPass llvm::DenseSet erasedOps; for (Operation* op : ops) { - // Pointers were collected before this loop; avoid dereferencing ops - // erased by earlier rewrites in this same sweep. if (erasedOps.contains(op)) { continue; } - // Nested regions under `ctrl` / `inv` are handled on the shell op - // (e.g. `ctrl { inv { ... } }`, `inv { ... }`). - if (isHiddenInsideCtrlOrInvBody(op)) { + if (isExcludedFromTopLevelUnitaryWalk(op)) { continue; } if (llvm::isa(op)) { @@ -767,7 +575,7 @@ struct FuseTwoQubitUnitaryRunsPass } if (unitary.isSingleQubit()) { - if (!allowsSingleQubitOp(unitary, spec)) { + if (!decomposition::allowsSingleQubitOp(unitary, spec)) { if (failed(rewriteSingleQubit(rewriter, op, unitary, basis))) { return failure(); } @@ -777,7 +585,8 @@ struct FuseTwoQubitUnitaryRunsPass } if (auto ctrl = llvm::dyn_cast(op)) { - const bool wasAlreadyNative = ctrlMatchesNativeMenu(ctrl, spec); + const bool wasAlreadyNative = + decomposition::allowsSingleTargetCtrl(ctrl, spec); if (failed(rewriteControlled(rewriter, ctrl, spec))) { return failure(); } @@ -798,8 +607,6 @@ struct FuseTwoQubitUnitaryRunsPass return success(); } - /// Lower one off-menu single-qubit `op` via its constant `2×2` matrix and - /// the Euler synthesizer in `basis`. static LogicalResult rewriteSingleQubit(IRRewriter& rewriter, Operation* op, UnitaryOpInterface unitary, @@ -818,43 +625,18 @@ struct FuseTwoQubitUnitaryRunsPass return success(); } - /// Lower a single-control, single-target `CtrlOp` to the native profile. - /// Fast-path: already-native `CX`/`CZ` are kept as-is. Otherwise, lift the - /// controlled op to its 4x4 matrix and run the deterministic two-qubit - /// synthesizer. static LogicalResult rewriteControlled(IRRewriter& rewriter, CtrlOp ctrl, const decomposition::NativeProfileSpec& spec) { - if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { - ctrl.emitError("native synthesis currently only supports 1-control " - "1-target controlled gates"); - return failure(); - } - auto* body = ctrl.getBodyUnitary(0).getOperation(); - const bool hasCX = llvm::isa(body); - const bool hasCZ = llvm::isa(body); - if ((llvm::is_contained(spec.entanglerBases, - decomposition::EntanglerBasis::Cx) && - hasCX) || - (llvm::is_contained(spec.entanglerBases, - decomposition::EntanglerBasis::Cz) && - hasCZ)) { + if (decomposition::allowsSingleTargetCtrl(ctrl, spec)) { return success(); } Matrix4x4 matrix; - if (hasCX || hasCZ) { - if (!getBlockTwoQubitMatrix(ctrl.getOperation(), matrix)) { - ctrl.emitError("failed to compute 4x4 matrix for CtrlOp"); - return failure(); - } - } else { - auto u = llvm::cast(ctrl.getOperation()); - if (!u.isTwoQubit() || !u.getUnitaryMatrix4x4(matrix)) { - ctrl.emitError( - "native synthesis: cannot build a constant 4x4 matrix for this " - "controlled gate (unsupported body or non-constant parameters)"); - return failure(); - } + if (!decomposition::assignTwoQubitOpMatrix(ctrl.getOperation(), matrix)) { + ctrl.emitError( + "native synthesis: cannot build a constant 4x4 matrix for this " + "controlled gate (unsupported body or non-constant parameters)"); + return failure(); } rewriter.setInsertionPoint(ctrl); Value out0; @@ -869,18 +651,15 @@ struct FuseTwoQubitUnitaryRunsPass return success(); } - /// Lower an off-menu generic two-qubit op. Bare `RZZ` is kept when on the - /// native menu; all other two-qubit unitaries go through the deterministic - /// KAK synthesizer. static LogicalResult rewriteTwoQubit(IRRewriter& rewriter, Operation* op, UnitaryOpInterface unitary, const decomposition::NativeProfileSpec& spec) { - if (spec.allowRzz && llvm::isa(op)) { + if (decomposition::allowsBareTwoQubitOp(op, spec)) { return success(); } Matrix4x4 matrix; - if (!getBlockTwoQubitMatrix(op, matrix)) { + if (!decomposition::assignTwoQubitOpMatrix(op, matrix)) { op->emitError("unsupported two-qubit operation for selected profile"); return failure(); } diff --git a/mlir/lib/Dialect/QCO/Utils/Matrix.cpp b/mlir/lib/Dialect/QCO/Utils/Matrix.cpp index 9af25c22eb..1d0898ce99 100644 --- a/mlir/lib/Dialect/QCO/Utils/Matrix.cpp +++ b/mlir/lib/Dialect/QCO/Utils/Matrix.cpp @@ -803,4 +803,116 @@ SymmetricEigen4 jacobiSymmetricEigen(const std::array& symmetric) { return result; } +double remEuclid(double a, double b) { + if (b == 0.0) { + llvm::reportFatalInternalError("remEuclid expects non-zero divisor"); + } + const auto r = std::fmod(a, b); + return (r < 0.0) ? r + std::abs(b) : r; +} + +double traceToFidelity(const Complex& trace) { + const auto traceAbs = std::abs(trace); + return (4.0 + (traceAbs * traceAbs)) / 20.0; +} + +Complex globalPhaseFactor(double phase) { + return std::exp(Complex{0.0, 1.0} * phase); +} + +bool isUnitaryMatrix(const Matrix2x2& matrix, const double tolerance) { + return (matrix.adjoint() * matrix).isIdentity(tolerance); +} + +Matrix2x2 rxMatrix(const double theta) { + const auto halfTheta = theta / 2.0; + const Complex cos{std::cos(halfTheta), 0.0}; + const Complex isin{0.0, -std::sin(halfTheta)}; + return Matrix2x2::fromElements(cos, isin, isin, cos); +} + +Matrix2x2 ryMatrix(const double theta) { + const auto halfTheta = theta / 2.0; + const Complex cos{std::cos(halfTheta), 0.0}; + const Complex sin{std::sin(halfTheta), 0.0}; + return Matrix2x2::fromElements(cos, -sin, sin, cos); +} + +Matrix2x2 rzMatrix(const double theta) { + return Matrix2x2::fromElements( + Complex{std::cos(theta / 2.0), -std::sin(theta / 2.0)}, 0.0, 0.0, + Complex{std::cos(theta / 2.0), std::sin(theta / 2.0)}); +} + +const Matrix2x2& iPauliZ() { + static const Matrix2x2 matrix = + Matrix2x2::fromElements(Complex{0.0, 1.0}, 0.0, 0.0, Complex{0.0, -1.0}); + return matrix; +} + +const Matrix2x2& iPauliY() { + static const Matrix2x2 matrix = Matrix2x2::fromElements(0.0, 1.0, -1.0, 0.0); + return matrix; +} + +const Matrix2x2& iPauliX() { + static const Matrix2x2 matrix = + Matrix2x2::fromElements(0.0, Complex{0.0, 1.0}, Complex{0.0, 1.0}, 0.0); + return matrix; +} + +Matrix4x4 rxxMatrix(const double theta) { + const auto cosTheta = std::cos(theta / 2.0); + const Complex misin{0.0, -std::sin(theta / 2.0)}; + return Matrix4x4::fromElements(cosTheta, 0.0, 0.0, misin, // + 0.0, cosTheta, misin, 0.0, // + 0.0, misin, cosTheta, 0.0, // + misin, 0.0, 0.0, cosTheta); +} + +Matrix4x4 ryyMatrix(const double theta) { + const auto cosTheta = std::cos(theta / 2.0); + const Complex isin{0.0, std::sin(theta / 2.0)}; + const Complex misin{0.0, -std::sin(theta / 2.0)}; + return Matrix4x4::fromElements(cosTheta, 0.0, 0.0, isin, // + 0.0, cosTheta, misin, 0.0, // + 0.0, misin, cosTheta, 0.0, // + isin, 0.0, 0.0, cosTheta); +} + +Matrix4x4 rzzMatrix(const double theta) { + const auto cosTheta = std::cos(theta / 2.0); + const auto sinTheta = std::sin(theta / 2.0); + const Complex em{cosTheta, -sinTheta}; + const Complex ep{cosTheta, sinTheta}; + return Matrix4x4::fromElements(em, 0.0, 0.0, 0.0, // + 0.0, ep, 0.0, 0.0, // + 0.0, 0.0, ep, 0.0, // + 0.0, 0.0, 0.0, em); +} + +const Matrix4x4& twoQubitControlledX01() { + static const Matrix4x4 matrix = Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, // + 0.0, 1.0, 0.0, 0.0, // + 0.0, 0.0, 0.0, 1.0, // + 0.0, 0.0, 1.0, 0.0); + return matrix; +} + +const Matrix4x4& twoQubitControlledX10() { + static const Matrix4x4 matrix = Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, // + 0.0, 0.0, 0.0, 1.0, // + 0.0, 0.0, 1.0, 0.0, // + 0.0, 1.0, 0.0, 0.0); + return matrix; +} + +const Matrix4x4& twoQubitControlledZ() { + static const Matrix4x4 matrix = Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, // + 0.0, 1.0, 0.0, 0.0, // + 0.0, 0.0, 1.0, 0.0, // + 0.0, 0.0, 0.0, -1.0); + return matrix; +} + } // namespace mlir::qco diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 0952a1e527..ca222a4eca 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -746,21 +746,6 @@ class CompilerPipelineNativeSynthesisConfigTest : public testing::Test { using mqt::test::isEquivalentUpToGlobalPhase; -namespace { - -using mqt::test::expandToTwoQubits; -using mqt::test::fixTwoQubitMatrixQubitOrder; -using mqt::test::QubitId; - -} // namespace - -/// Compute the 4×4 unitary of a two-qubit QCO module whose qubits are -/// introduced by `qco.static` ops with indices 0 and 1. Handles the op set -/// that stage-4/stage-5 IR can contain for the `staticQubitsWithOps` -/// program (pre-synthesis: `qco.h`; post-synthesis: `qco.rz`, `qco.sx`, -/// `qco.x`, `qco.p`, `qco.u`; and `qco.gphase`, which is skipped). Returns -/// `std::nullopt` if the IR contains an unsupported op or non-constant -/// parameters. static std::optional computeStaticTwoQubitUnitary(mlir::ModuleOp module) { if (module == nullptr) { @@ -808,7 +793,7 @@ computeStaticTwoQubitUnitary(mlir::ModuleOp module) { if (!op.getUnitaryMatrix2x2(oneQ)) { return std::nullopt; } - unitary = expandToTwoQubits(oneQ, *qid) * unitary; + unitary = mlir::qco::embedSingleQubitInTwoQubit(oneQ, *qid) * unitary; qubitIds[op.getOutputQubit(0)] = *qid; continue; } @@ -826,22 +811,18 @@ computeStaticTwoQubitUnitary(mlir::ModuleOp module) { } auto* body = ctrl.getBodyUnitary(0).getOperation(); if (llvm::isa(body)) { - // CX matrix (same 4×4 layout as QCO unitary interface). - twoQ = mlir::qco::Matrix4x4::fromElements(1, 0, 0, 0, 0, 1, 0, 0, - 0, 0, 0, 1, 0, 0, 1, 0); + twoQ = mlir::qco::twoQubitControlledX01(); } else if (llvm::isa(body)) { - // CZ matrix: identity with a `-1` phase on the `|11>` entry. - twoQ = mlir::qco::Matrix4x4::fromElements( - 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, -1); + twoQ = mlir::qco::twoQubitControlledZ(); } else { return std::nullopt; } } else if (!op.getUnitaryMatrix4x4(twoQ)) { return std::nullopt; } - const llvm::SmallVector ids{static_cast(*q0), - static_cast(*q1)}; - unitary = fixTwoQubitMatrixQubitOrder(twoQ, ids) * unitary; + const llvm::SmallVector ids{*q0, *q1}; + unitary = + mlir::qco::reorderTwoQubitMatrix(twoQ, ids[0], ids[1]) * unitary; qubitIds[op.getOutputQubit(0)] = *q0; qubitIds[op.getOutputQubit(1)] = *q1; continue; @@ -861,9 +842,7 @@ TEST_F(CompilerPipelineNativeSynthesisConfigTest, const auto record = runPipelineAndExpectSuccess(); - // Stage 4 still contains unsynthesized H operations from the source program. EXPECT_NE(record.afterQCOCanon.find("qco.h"), std::string::npos); - // Stage 5 must rewrite them when a native menu is configured. EXPECT_EQ(record.afterOptimization.find("qco.h"), std::string::npos); } @@ -890,8 +869,6 @@ TEST_F(CompilerPipelineNativeSynthesisConfigTest, TEST_F(CompilerPipelineNativeSynthesisConfigTest, RejectsUnderSpecifiedNativeSynthesisMenuInStage5) { - // A menu with only two-qubit entanglers cannot synthesize any single-qubit - // operation. config.nativeGates = "cx,cz"; runPipelineAndExpectFailure(); @@ -899,7 +876,6 @@ TEST_F(CompilerPipelineNativeSynthesisConfigTest, TEST_F(CompilerPipelineNativeSynthesisConfigTest, RejectsInvalidNativeGateTokenInStage5) { - // Unknown tokens in the menu must be rejected. config.nativeGates = "not-a-gate"; runPipelineAndExpectFailure(); @@ -907,9 +883,6 @@ TEST_F(CompilerPipelineNativeSynthesisConfigTest, TEST_F(CompilerPipelineNativeSynthesisConfigTest, LeavesIRUnchangedWhenNoNativeProfileIsConfigured) { - // Stage 5 must be a no-op when `nativeGates` is empty (the documented - // default): the stage-4 (QCO canonicalized) and stage-5 (optimization + - // native gate synthesis) IRs have to be byte-identical. config.nativeGates = ""; const auto record = runPipelineAndExpectSuccess(); @@ -930,10 +903,6 @@ TEST_F(CompilerPipelineNativeSynthesisConfigTest, TEST_F(CompilerPipelineNativeSynthesisConfigTest, NativeSynthesisPreservesUnitaryOnStaticQubits) { - // End-to-end unitary equivalence check: after the pipeline lowers - // `staticQubitsWithOps` (H on two static qubits) onto the `x,sx,rz,cx` - // native gate set, the 4×4 unitary of the IR after stage 5 must match the - // unitary of the pre-synthesis (`afterQCOCanon`) IR up to a global phase. config.nativeGates = "x,sx,rz,cx"; const auto record = runPipelineAndExpectSuccess(); diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp index 67755f2d1f..8cc15b55f3 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -37,7 +37,6 @@ #include #include -#include #include #include #include @@ -104,18 +103,6 @@ class EulerSynthesisExactTest // Euler synthesis support //===----------------------------------------------------------------------===// -[[nodiscard]] static Matrix2x2 rzMatrix(const double theta) { - const auto m00 = std::polar(1.0, -theta / 2.0); - const auto m11 = std::polar(1.0, theta / 2.0); - return Matrix2x2::fromElements(m00, 0, 0, m11); -} - -[[nodiscard]] static Matrix2x2 ryMatrix(const double theta) { - const auto m00 = std::cos(theta / 2.0); - const auto m01 = -std::sin(theta / 2.0); - return Matrix2x2::fromElements(m00, m01, -m01, m00); -} - [[nodiscard]] static Matrix2x2 randomUnitaryMatrix(std::mt19937& rng) { std::uniform_real_distribution dist(-std::numbers::pi, std::numbers::pi); const Matrix2x2 su2 = diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index a3022cbee7..f3a1793e9b 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -12,6 +12,7 @@ #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/NativeProfile.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" @@ -20,7 +21,6 @@ #include #include #include -#include #include #include #include @@ -28,7 +28,6 @@ #include #include #include -#include #include #include @@ -41,330 +40,131 @@ #include #include #include +#include using namespace mlir; using namespace mlir::qco; using namespace mlir::qco::decomposition; using namespace mqt::test; -// Weyl / basis / helpers. +constexpr double kSanityCheckPrecision = 1e-12; -TEST(DecompositionHelpersTest, RemEuclidNeverNegative) { - EXPECT_DOUBLE_EQ(remEuclid(-1.0, 3.0), 2.0); - EXPECT_DOUBLE_EQ(remEuclid(7.0, 3.0), 1.0); - EXPECT_DOUBLE_EQ(remEuclid(0.0, 2.5), 0.0); -} - -TEST(DecompositionHelpersTest, TraceToFidelityMatchesFormula) { - const std::complex x{3.0, 4.0}; - const double absx = 5.0; - EXPECT_DOUBLE_EQ(traceToFidelity(x), (4.0 + (absx * absx)) / 20.0); -} - -TEST(DecompositionHelpersTest, GlobalPhaseFactorUnitMagnitude) { - const auto z = globalPhaseFactor(1.25); - EXPECT_NEAR(std::abs(z), 1.0, 1e-14); -} - -TEST(DecompositionHelpersTest, IsUnitaryMatrixRejectsNonUnitary) { - const Matrix2x2 m = Matrix2x2::fromElements(2.0, 0.0, 0.0, 2.0); - EXPECT_FALSE(isUnitaryMatrix(m)); -} - -TEST(DecompositionHelpersTest, IsUnitaryMatrixAcceptsUnitary) { - const Matrix2x2 m = Matrix2x2::identity(); - EXPECT_TRUE(isUnitaryMatrix(m)); -} - -//===----------------------------------------------------------------------===// -// Weyl decomposition -//===----------------------------------------------------------------------===// - -// NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_P` at global scope -class WeylDecompositionTest : public testing::TestWithParam { -public: - [[nodiscard]] static Matrix4x4 - restore(const TwoQubitWeylDecomposition& decomposition) { - return k1(decomposition) * can(decomposition) * k2(decomposition) * - globalPhaseFactor(decomposition); - } - - [[nodiscard]] static std::complex - globalPhaseFactor(const TwoQubitWeylDecomposition& decomposition) { - return mqt::test::globalPhaseFactor(decomposition.globalPhase()); - } - [[nodiscard]] static Matrix4x4 - can(const TwoQubitWeylDecomposition& decomposition) { - return decomposition.getCanonicalMatrix(); - } - [[nodiscard]] static Matrix4x4 - k1(const TwoQubitWeylDecomposition& decomposition) { - return kron(decomposition.k1l(), decomposition.k1r()); - } - [[nodiscard]] static Matrix4x4 - k2(const TwoQubitWeylDecomposition& decomposition) { - return kron(decomposition.k2l(), decomposition.k2r()); - } -}; - -TEST_P(WeylDecompositionTest, TestExact) { - const auto& originalMatrix = GetParam()(); - auto decomposition = TwoQubitWeylDecomposition::create( - originalMatrix, std::optional{1.0}); - auto restoredMatrix = restore(decomposition); - - EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix)); -} - -TEST_P(WeylDecompositionTest, TestApproximation) { - const auto& originalMatrix = GetParam()(); - auto decomposition = TwoQubitWeylDecomposition::create( - originalMatrix, std::optional{1.0 - 1e-12}); - auto restoredMatrix = restore(decomposition); - - EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix)); -} - -TEST(WeylDecompositionStandalone, - CnotProducesValidWeylParametersAndUnitaryLocals) { - const Matrix4x4 cnot = Matrix4x4::fromElements(1, 0, 0, 0, // row 0 - 0, 1, 0, 0, // row 1 - 0, 0, 0, 1, // row 2 - 0, 0, 1, 0); - - const auto decomp = TwoQubitWeylDecomposition::create(cnot, std::nullopt); - EXPECT_GE(decomp.a(), -1e-10); - EXPECT_GE(decomp.b(), -1e-10); - EXPECT_GE(decomp.c(), -1e-10); - constexpr double piOver4 = 0.7853981633974483; - EXPECT_LE(decomp.a(), piOver4 + 1e-10); - EXPECT_LE(decomp.b(), piOver4 + 1e-10); - EXPECT_LE(decomp.c(), piOver4 + 1e-10); - EXPECT_TRUE(isUnitaryMatrix(decomp.k1l())); - EXPECT_TRUE(isUnitaryMatrix(decomp.k2l())); - EXPECT_TRUE(isUnitaryMatrix(decomp.k1r())); - EXPECT_TRUE(isUnitaryMatrix(decomp.k2r())); -} - -TEST(WeylDecompositionStandalone, Random) { - constexpr auto maxIterations = 5000; - std::mt19937 rng{1234567UL}; +namespace { - for (int i = 0; i < maxIterations; ++i) { - auto originalMatrix = randomUnitary4x4(rng); - auto decomposition = TwoQubitWeylDecomposition::create( - originalMatrix, std::optional{1.0 - 1e-12}); - auto restoredMatrix = WeylDecompositionTest::restore(decomposition); +using QubitId = std::size_t; - // The reconstruction accuracy is bounded by the iterative diagonalization - // residual rather than the (much tighter) default matrix tolerance. - EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix, kSanityCheckPrecision)); - } +[[nodiscard]] const Matrix2x2& hGate() { + static const Matrix2x2 matrix = Matrix2x2::fromElements( + FRAC1_SQRT2, FRAC1_SQRT2, FRAC1_SQRT2, -FRAC1_SQRT2); + return matrix; } -INSTANTIATE_TEST_SUITE_P( - ProductTwoQubitMatrices, WeylDecompositionTest, - ::testing::Values([]() -> Matrix4x4 { return Matrix4x4::identity(); }, - []() -> Matrix4x4 { - return kron(rzMatrix(1.0), ryMatrix(3.1)); - }, - []() -> Matrix4x4 { - return kron(Matrix2x2::identity(), rxMatrix(0.1)); - })); - -INSTANTIATE_TEST_SUITE_P( - TwoQubitMatrices, WeylDecompositionTest, - ::testing::Values( - []() -> Matrix4x4 { return rzzMatrix(2.0); }, - []() -> Matrix4x4 { - return ryyMatrix(1.0) * rzzMatrix(3.0) * rxxMatrix(2.0); - }, - []() -> Matrix4x4 { - return TwoQubitWeylDecomposition::getCanonicalMatrix(1.5, -0.2, 0.0) * - kron(rxMatrix(1.0), Matrix2x2::identity()); - }, - []() -> Matrix4x4 { - return kron(rxMatrix(1.0), ryMatrix(1.0)) * - TwoQubitWeylDecomposition::getCanonicalMatrix(1.1, 0.2, 3.0) * - kron(rxMatrix(1.0), Matrix2x2::identity()); - }, - []() -> Matrix4x4 { - return kron(hGate(), ipz()) * cxGate01() * kron(ipx(), ipy()); - })); - -//===----------------------------------------------------------------------===// -// Basis decomposer -//===----------------------------------------------------------------------===// - -// NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_P` at global scope -class BasisDecomposerTest : public testing::TestWithParam< - std::tuple> { -public: - [[nodiscard]] static Matrix4x4 - restore(const TwoQubitNativeDecomposition& decomposition, - const Matrix4x4& entangler) { - const auto& factors = decomposition.singleQubitFactors; - const auto layer = [&](std::size_t i) { - return kron(factors[(2 * i) + 1], factors[2 * i]); - }; - Matrix4x4 matrix = layer(0); - for (std::uint8_t i = 0; i < decomposition.numBasisUses; ++i) { - matrix = entangler * matrix; - matrix = layer(static_cast(i) + 1) * matrix; +[[nodiscard]] Matrix4x4 randomUnitary4x4(std::mt19937& rng) { + std::normal_distribution normalDist(0.0, 1.0); + std::vector>> columns( + 4, std::vector>(4)); + for (auto& column : columns) { + for (auto& entry : column) { + entry = std::complex(normalDist(rng), normalDist(rng)); } - return matrix * ::globalPhaseFactor(decomposition.globalPhase); } - -protected: - void SetUp() override { - basisMatrix = std::get<0>(GetParam())(); - target = std::get<1>(GetParam())(); - targetDecomposition = std::make_unique( - TwoQubitWeylDecomposition::create(target, std::optional{1.0})); + for (std::size_t j = 0; j < 4; ++j) { + for (std::size_t k = 0; k < j; ++k) { + std::complex projection{0.0, 0.0}; + for (std::size_t i = 0; i < 4; ++i) { + projection += std::conj(columns[k][i]) * columns[j][i]; + } + for (std::size_t i = 0; i < 4; ++i) { + columns[j][i] -= projection * columns[k][i]; + } + } + double norm = 0.0; + for (std::size_t i = 0; i < 4; ++i) { + norm += std::norm(columns[j][i]); + } + norm = std::sqrt(norm); + for (std::size_t i = 0; i < 4; ++i) { + columns[j][i] /= norm; + } } - - Matrix4x4 target; - Matrix4x4 basisMatrix; - std::unique_ptr targetDecomposition; -}; - -TEST_P(BasisDecomposerTest, TestExact) { - const auto& originalMatrix = target; - auto decomposer = TwoQubitBasisDecomposer::create(basisMatrix, 1.0); - auto decomposed = - decomposer.twoQubitDecompose(*targetDecomposition, std::nullopt); - - ASSERT_TRUE(decomposed.has_value()); - - auto restoredMatrix = restore(*decomposed, basisMatrix); - - EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix)); + const Matrix4x4 unitary = Matrix4x4::fromElements( + columns[0][0], columns[1][0], columns[2][0], columns[3][0], columns[0][1], + columns[1][1], columns[2][1], columns[3][1], columns[0][2], columns[1][2], + columns[2][2], columns[3][2], columns[0][3], columns[1][3], columns[2][3], + columns[3][3]); + assert((unitary.adjoint() * unitary).isIdentity(1e-12)); + return unitary; } -TEST_P(BasisDecomposerTest, TestApproximation) { - const auto& originalMatrix = target; - auto decomposer = TwoQubitBasisDecomposer::create(basisMatrix, 1.0 - 1e-12); - auto decomposed = - decomposer.twoQubitDecompose(*targetDecomposition, std::nullopt); - - ASSERT_TRUE(decomposed.has_value()); - - auto restoredMatrix = restore(*decomposed, basisMatrix); - - EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix)); +Matrix4x4 restoreWeyl(const TwoQubitWeylDecomposition& decomposition) { + return kron(decomposition.k1l(), decomposition.k1r()) * + decomposition.getCanonicalMatrix() * + kron(decomposition.k2l(), decomposition.k2r()) * + globalPhaseFactor(decomposition.globalPhase()); } -TEST(BasisDecomposerTest, Random) { - constexpr auto maxIterations = 2000; - std::mt19937 rng{123456UL}; - - const llvm::SmallVector basisMatrices{cxGate01(), cxGate10()}; - std::uniform_int_distribution distBasisGate{ - 0, basisMatrices.size() - 1}; - auto selectRandomBasisMatrix = [&]() { - return basisMatrices[distBasisGate(rng)]; +Matrix4x4 restoreBasis(const TwoQubitNativeDecomposition& decomposition, + const Matrix4x4& entangler) { + const auto& factors = decomposition.singleQubitFactors; + const auto layer = [&](std::size_t i) { + return kron(factors[(2 * i) + 1], factors[2 * i]); }; - - for (int i = 0; i < maxIterations; ++i) { - auto originalMatrix = randomUnitary4x4(rng); - - auto targetDecomposition = TwoQubitWeylDecomposition::create( - originalMatrix, std::optional{1.0}); - const auto basisMatrix = selectRandomBasisMatrix(); - auto decomposer = TwoQubitBasisDecomposer::create(basisMatrix, 1.0); - auto decomposed = - decomposer.twoQubitDecompose(targetDecomposition, std::nullopt); - - ASSERT_TRUE(decomposed.has_value()); - - auto restoredMatrix = - BasisDecomposerTest::restore(*decomposed, basisMatrix); - - // Reconstruction accumulates the Weyl diagonalization residual through up - // to three entangler layers, so allow a correspondingly relaxed tolerance. - EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix, kSanityCheckPrecision)); + Matrix4x4 matrix = layer(0); + for (std::uint8_t i = 0; i < decomposition.numBasisUses; ++i) { + matrix = entangler * matrix; + matrix = layer(static_cast(i) + 1) * matrix; } + return matrix * globalPhaseFactor(decomposition.globalPhase); } -TEST(BasisDecomposerNumBasisTest, ForcesZeroBasisUsesForIdentityTarget) { - const auto basis = cxGate01(); - const auto decomposer = TwoQubitBasisDecomposer::create(basis, 1.0); - const Matrix4x4 target = Matrix4x4::identity(); - const auto weyl = - TwoQubitWeylDecomposition::create(target, std::optional{1.0}); - const auto decomposed = decomposer.twoQubitDecompose(weyl, std::uint8_t{0}); - ASSERT_TRUE(decomposed.has_value()); - EXPECT_EQ(decomposed->numBasisUses, 0); - const Matrix4x4 restored = BasisDecomposerTest::restore(*decomposed, basis); - EXPECT_TRUE(restored.isApprox(target)); +auto productMatrixCases() { + return ::testing::Values( + []() -> Matrix4x4 { return Matrix4x4::identity(); }, + []() -> Matrix4x4 { return kron(rzMatrix(1.0), ryMatrix(3.1)); }, + []() -> Matrix4x4 { return kron(Matrix2x2::identity(), rxMatrix(0.1)); }); } -INSTANTIATE_TEST_SUITE_P( - ProductTwoQubitMatrices, BasisDecomposerTest, - testing::Combine( - // basis entanglers - testing::Values([]() -> Matrix4x4 { return cxGate01(); }, - []() -> Matrix4x4 { return cxGate10(); }), - // targets to be decomposed - testing::Values([]() -> Matrix4x4 { return Matrix4x4::identity(); }, - []() -> Matrix4x4 { - return kron(rzMatrix(1.0), ryMatrix(3.1)); - }, - []() -> Matrix4x4 { - return kron(Matrix2x2::identity(), rxMatrix(0.1)); - }))); - -INSTANTIATE_TEST_SUITE_P( - TwoQubitMatrices, BasisDecomposerTest, - testing::Combine( - // basis entanglers - testing::Values([]() -> Matrix4x4 { return cxGate01(); }, - []() -> Matrix4x4 { return cxGate10(); }), - // targets to be decomposed - ::testing::Values( - []() -> Matrix4x4 { return rzzMatrix(2.0); }, - []() -> Matrix4x4 { - return ryyMatrix(1.0) * rzzMatrix(3.0) * rxxMatrix(2.0); - }, - []() -> Matrix4x4 { - return TwoQubitWeylDecomposition::getCanonicalMatrix(1.5, -0.2, - 0.0) * - kron(rxMatrix(1.0), Matrix2x2::identity()); - }, - []() -> Matrix4x4 { - return kron(rxMatrix(1.0), ryMatrix(1.0)) * - TwoQubitWeylDecomposition::getCanonicalMatrix(1.1, 0.2, - 3.0) * - kron(rxMatrix(1.0), Matrix2x2::identity()); - }, - []() -> Matrix4x4 { - return kron(hGate(), ipz()) * cxGate01() * kron(ipx(), ipy()); - }))); - -//===----------------------------------------------------------------------===// -// Two-qubit Weyl IR synthesis -//===----------------------------------------------------------------------===// - -namespace { +auto entangledMatrixCases() { + return ::testing::Values( + []() -> Matrix4x4 { return rzzMatrix(2.0); }, + []() -> Matrix4x4 { + return ryyMatrix(1.0) * rzzMatrix(3.0) * rxxMatrix(2.0); + }, + []() -> Matrix4x4 { + return TwoQubitWeylDecomposition::getCanonicalMatrix(1.5, -0.2, 0.0) * + kron(rxMatrix(1.0), Matrix2x2::identity()); + }, + []() -> Matrix4x4 { + return kron(rxMatrix(1.0), ryMatrix(1.0)) * + TwoQubitWeylDecomposition::getCanonicalMatrix(1.1, 0.2, 3.0) * + kron(rxMatrix(1.0), Matrix2x2::identity()); + }, + []() -> Matrix4x4 { + return kron(hGate(), iPauliZ()) * twoQubitControlledX01() * + kron(iPauliX(), iPauliY()); + }); +} -struct WeylSynthesisFixture { - std::unique_ptr context; +auto cxBasisCases() { + return ::testing::Values( + []() -> Matrix4x4 { return twoQubitControlledX01(); }, + []() -> Matrix4x4 { return twoQubitControlledX10(); }); +} - void setUp() { - DialectRegistry registry; - registry.insert(); - context = std::make_unique(); - context->appendDialectRegistry(registry); - context->loadAllAvailableDialects(); +[[nodiscard]] bool extractSingleQubitMatrix(UnitaryOpInterface op, + Matrix2x2& out) { + if (op.getUnitaryMatrix2x2(out)) { + return true; } - - [[nodiscard]] MLIRContext* ctx() const { return context.get(); } -}; - -struct SynthesizedTwoQubitCircuit { - OwningOpRef mlirModule; - func::FuncOp func; -}; + DynamicMatrix dynamic; + if (!op.getUnitaryMatrixDynamic(dynamic) || dynamic.rows() != 2 || + dynamic.cols() != 2) { + return false; + } + out = Matrix2x2::fromElements(dynamic(0, 0), dynamic(0, 1), dynamic(1, 0), + dynamic(1, 1)); + return true; +} [[nodiscard]] bool extractTwoQubitMatrix(UnitaryOpInterface op, Matrix4x4& out) { @@ -374,11 +174,11 @@ struct SynthesizedTwoQubitCircuit { } Operation* body = ctrl.getBodyUnitary(0).getOperation(); if (llvm::isa(body)) { - out = cxGate01(); + out = twoQubitControlledX01(); return true; } if (llvm::isa(body)) { - out = czGate(); + out = twoQubitControlledZ(); return true; } return false; @@ -387,8 +187,8 @@ struct SynthesizedTwoQubitCircuit { } [[nodiscard]] std::optional -compute2QUnitaryFromFunc(func::FuncOp funcOp) { - Matrix4x4 acc = Matrix4x4::identity(); +computeTwoQubitUnitaryFromFunc(func::FuncOp funcOp) { + Matrix4x4 unitary = Matrix4x4::identity(); std::complex global{1.0, 0.0}; llvm::DenseMap wireIds; wireIds[funcOp.getArgument(0)] = 0; @@ -424,16 +224,10 @@ compute2QUnitaryFromFunc(func::FuncOp funcOp) { return std::nullopt; } Matrix2x2 oneQ; - if (!op.getUnitaryMatrix2x2(oneQ)) { - DynamicMatrix dynamic; - if (!op.getUnitaryMatrixDynamic(dynamic) || dynamic.rows() != 2 || - dynamic.cols() != 2) { - return std::nullopt; - } - oneQ = Matrix2x2::fromElements(dynamic(0, 0), dynamic(0, 1), - dynamic(1, 0), dynamic(1, 1)); + if (!extractSingleQubitMatrix(op, oneQ)) { + return std::nullopt; } - acc = expandToTwoQubits(oneQ, *qid) * acc; + unitary = embedSingleQubitInTwoQubit(oneQ, *qid) * unitary; wireIds[op->getResult(0)] = *qid; continue; } @@ -451,89 +245,220 @@ compute2QUnitaryFromFunc(func::FuncOp funcOp) { return std::nullopt; } const llvm::SmallVector ids{*q0id, *q1id}; - acc = fixTwoQubitMatrixQubitOrder(twoQ, ids) * acc; + unitary = reorderTwoQubitMatrix(twoQ, ids[0], ids[1]) * unitary; wireIds[op->getResult(0)] = *q0id; wireIds[op->getResult(1)] = *q1id; } } - return acc * global; + return unitary * global; } -[[nodiscard]] SynthesizedTwoQubitCircuit -synthesize2QMatrix(MLIRContext* ctx, const Matrix4x4& target, - const NativeProfileSpec& spec) { - OwningOpRef mlirModule = ModuleOp::create(UnknownLoc::get(ctx)); +struct MlirTestContext { + std::unique_ptr context; + + void setUp() { + DialectRegistry registry; + registry.insert(); + context = std::make_unique(); + context->appendDialectRegistry(registry); + context->loadAllAvailableDialects(); + } + + [[nodiscard]] MLIRContext* ctx() const { return context.get(); } +}; + +func::FuncOp synthesize2QIntoFunc(MLIRContext* ctx, const Matrix4x4& target, + const NativeProfileSpec& spec, + OwningOpRef& moduleOut) { + moduleOut = ModuleOp::create(UnknownLoc::get(ctx)); OpBuilder builder(ctx); - builder.setInsertionPointToStart(mlirModule->getBody()); + builder.setInsertionPointToStart(moduleOut->getBody()); const auto qubitTy = QubitType::get(ctx); const auto funcTy = builder.getFunctionType({qubitTy, qubitTy}, {qubitTy, qubitTy}); - const Location loc = mlirModule->getLoc(); + const Location loc = moduleOut->getLoc(); auto func = func::FuncOp::create(builder, loc, "main", funcTy); auto* entry = func.addEntryBlock(); builder.setInsertionPointToStart(entry); - Value q0 = entry->getArgument(0); - Value q1 = entry->getArgument(1); Value out0; Value out1; - if (failed(synthesizeUnitary2QWeyl(builder, loc, q0, q1, target, spec, out0, + if (failed(synthesizeUnitary2QWeyl(builder, loc, entry->getArgument(0), + entry->getArgument(1), target, spec, out0, out1))) { llvm::report_fatal_error( "synthesizeUnitary2QWeyl failed during test synthesis"); } func::ReturnOp::create(builder, loc, ValueRange{out0, out1}); - return SynthesizedTwoQubitCircuit{.mlirModule = std::move(mlirModule), - .func = func}; -} - -void expect2QMatrixPreserved(func::FuncOp funcOp, const Matrix4x4& original) { - const auto actual = compute2QUnitaryFromFunc(funcOp); - ASSERT_TRUE(actual.has_value()); - EXPECT_TRUE(isEquivalentUpToGlobalPhase(*actual, original)); + return func; } void expectSynthesized2QMatrix(MLIRContext* ctx, const Matrix4x4& target, const NativeProfileSpec& spec) { - const auto circuit = synthesize2QMatrix(ctx, target, spec); - ASSERT_TRUE(succeeded(verify(*circuit.mlirModule))); - expect2QMatrixPreserved(circuit.func, target); + OwningOpRef module; + const auto func = synthesize2QIntoFunc(ctx, target, spec, module); + ASSERT_TRUE(succeeded(verify(module.get()))); + const auto actual = computeTwoQubitUnitaryFromFunc(func); + ASSERT_TRUE(actual.has_value()); + EXPECT_TRUE(isEquivalentUpToGlobalPhase(*actual, target)); } } // namespace -TEST(WeylSynthesisTest, ReconstructsCxWithGenericProfile) { - WeylSynthesisFixture fx; - fx.setUp(); - const auto spec = parseNativeSpec("u,cx"); - ASSERT_TRUE(spec); - expectSynthesized2QMatrix(fx.ctx(), cxGate01(), *spec); +TEST(DecompositionHelpersTest, MatrixUtilitySanity) { + EXPECT_DOUBLE_EQ(remEuclid(-1.0, 3.0), 2.0); + EXPECT_DOUBLE_EQ(traceToFidelity(std::complex{3.0, 4.0}), + (4.0 + 25.0) / 20.0); + EXPECT_NEAR(std::abs(globalPhaseFactor(1.25)), 1.0, 1e-14); + EXPECT_FALSE(isUnitaryMatrix(Matrix2x2::fromElements(2.0, 0.0, 0.0, 2.0))); + EXPECT_TRUE(isUnitaryMatrix(Matrix2x2::identity())); } -TEST(WeylSynthesisTest, ReconstructsProductUnitaryWithGenericProfile) { - WeylSynthesisFixture fx; - fx.setUp(); - const auto spec = parseNativeSpec("u,cx"); - ASSERT_TRUE(spec); - const Matrix4x4 target = kron(rzMatrix(1.0), ryMatrix(0.3)); - expectSynthesized2QMatrix(fx.ctx(), target, *spec); +// NOLINTNEXTLINE(misc-use-internal-linkage) +class WeylDecompositionTest : public testing::TestWithParam {}; + +TEST_P(WeylDecompositionTest, ReconstructsWithinRequestedFidelity) { + const Matrix4x4 originalMatrix = GetParam()(); + for (const double fidelity : {1.0, 1.0 - 1e-12}) { + const auto decomposition = + TwoQubitWeylDecomposition::create(originalMatrix, fidelity); + EXPECT_TRUE(restoreWeyl(decomposition).isApprox(originalMatrix)); + } +} + +TEST(WeylDecompositionStandalone, + CnotProducesValidWeylParametersAndUnitaryLocals) { + const Matrix4x4 cnot = + Matrix4x4::fromElements(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0); + const auto decomp = TwoQubitWeylDecomposition::create(cnot, std::nullopt); + constexpr double piOver4 = 0.7853981633974483; + for (const double angle : {decomp.a(), decomp.b(), decomp.c()}) { + EXPECT_GE(angle, -1e-10); + EXPECT_LE(angle, piOver4 + 1e-10); + } + EXPECT_TRUE(isUnitaryMatrix(decomp.k1l())); + EXPECT_TRUE(isUnitaryMatrix(decomp.k2l())); + EXPECT_TRUE(isUnitaryMatrix(decomp.k1r())); + EXPECT_TRUE(isUnitaryMatrix(decomp.k2r())); } -TEST(WeylSynthesisTest, ReconstructsWithIbmBasicProfile) { - WeylSynthesisFixture fx; +TEST(WeylDecompositionStandalone, Random) { + std::mt19937 rng{1234567UL}; + for (int i = 0; i < 5000; ++i) { + const Matrix4x4 originalMatrix = randomUnitary4x4(rng); + const auto decomposition = TwoQubitWeylDecomposition::create( + originalMatrix, std::optional{1.0 - 1e-12}); + EXPECT_TRUE(restoreWeyl(decomposition) + .isApprox(originalMatrix, kSanityCheckPrecision)); + } +} + +INSTANTIATE_TEST_SUITE_P(ProductTwoQubitMatrices, WeylDecompositionTest, + productMatrixCases()); +INSTANTIATE_TEST_SUITE_P(TwoQubitMatrices, WeylDecompositionTest, + entangledMatrixCases()); + +// NOLINTNEXTLINE(misc-use-internal-linkage) +class BasisDecomposerTest : public testing::TestWithParam< + std::tuple> { +protected: + void SetUp() override { + basisMatrix = std::get<0>(GetParam())(); + target = std::get<1>(GetParam())(); + targetDecomposition = std::make_unique( + TwoQubitWeylDecomposition::create(target, 1.0)); + } + + Matrix4x4 target; + Matrix4x4 basisMatrix; + std::unique_ptr targetDecomposition; +}; + +TEST_P(BasisDecomposerTest, ReconstructsWithinRequestedFidelity) { + for (const double fidelity : {1.0, 1.0 - 1e-12}) { + const auto decomposer = + TwoQubitBasisDecomposer::create(basisMatrix, fidelity); + const auto decomposed = + decomposer.twoQubitDecompose(*targetDecomposition, std::nullopt); + ASSERT_TRUE(decomposed.has_value()); + EXPECT_TRUE(restoreBasis(*decomposed, basisMatrix).isApprox(target)); + } +} + +TEST(BasisDecomposerTest, Random) { + std::mt19937 rng{123456UL}; + const llvm::SmallVector basisMatrices{twoQubitControlledX01(), + twoQubitControlledX10()}; + std::uniform_int_distribution distBasisGate{0, 1}; + + for (int i = 0; i < 2000; ++i) { + const Matrix4x4 originalMatrix = randomUnitary4x4(rng); + const auto targetDecomposition = TwoQubitWeylDecomposition::create( + originalMatrix, std::optional{1.0}); + const Matrix4x4 basisMatrix = basisMatrices[distBasisGate(rng)]; + const auto decomposer = TwoQubitBasisDecomposer::create(basisMatrix, 1.0); + const auto decomposed = + decomposer.twoQubitDecompose(targetDecomposition, std::nullopt); + ASSERT_TRUE(decomposed.has_value()); + EXPECT_TRUE(restoreBasis(*decomposed, basisMatrix) + .isApprox(originalMatrix, kSanityCheckPrecision)); + } +} + +TEST(BasisDecomposerNumBasisTest, ForcesZeroBasisUsesForIdentityTarget) { + const Matrix4x4 basis = twoQubitControlledX01(); + const auto decomposer = TwoQubitBasisDecomposer::create(basis, 1.0); + const Matrix4x4 target = Matrix4x4::identity(); + const auto weyl = TwoQubitWeylDecomposition::create(target, 1.0); + const auto decomposed = decomposer.twoQubitDecompose(weyl, std::uint8_t{0}); + ASSERT_TRUE(decomposed.has_value()); + EXPECT_EQ(decomposed->numBasisUses, 0); + EXPECT_TRUE(restoreBasis(*decomposed, basis).isApprox(target)); +} + +INSTANTIATE_TEST_SUITE_P(ProductTwoQubitMatrices, BasisDecomposerTest, + testing::Combine(cxBasisCases(), + productMatrixCases())); +INSTANTIATE_TEST_SUITE_P(TwoQubitMatrices, BasisDecomposerTest, + testing::Combine(cxBasisCases(), + entangledMatrixCases())); + +struct WeylSynthesisCase { + const char* name; + const char* nativeGates; + Matrix4x4 (*target)(); +}; + +class WeylSynthesisTest : public testing::TestWithParam {}; + +TEST_P(WeylSynthesisTest, PreservesTargetUnitary) { + MlirTestContext fx; fx.setUp(); - const auto spec = parseNativeSpec("x,sx,rz,cx"); + const auto spec = parseNativeSpec(GetParam().nativeGates); ASSERT_TRUE(spec); - const Matrix4x4 target = kron(hGate(), Matrix2x2::identity()) * cxGate01() * - kron(rzMatrix(0.2), ryMatrix(0.1)); - expectSynthesized2QMatrix(fx.ctx(), target, *spec); + expectSynthesized2QMatrix(fx.ctx(), GetParam().target(), *spec); } +INSTANTIATE_TEST_SUITE_P( + Profiles, WeylSynthesisTest, + testing::Values( + WeylSynthesisCase{"CxGeneric", "u,cx", + [] { return twoQubitControlledX01(); }}, + WeylSynthesisCase{"ProductGeneric", "u,cx", + [] { return kron(rzMatrix(1.0), ryMatrix(0.3)); }}, + WeylSynthesisCase{"IbmBasic", "x,sx,rz,cx", + [] { + return kron(hGate(), Matrix2x2::identity()) * + twoQubitControlledX01() * + kron(rzMatrix(0.2), ryMatrix(0.1)); + }}), + [](const testing::TestParamInfo& info) { + return info.param.name; + }); + TEST(WeylSynthesisTest, IdentityRequiresNoEntanglers) { - WeylSynthesisFixture fx; - fx.setUp(); const auto spec = parseNativeSpec("u,cx"); ASSERT_TRUE(spec); const auto count = twoQubitEntanglerCount(Matrix4x4::identity(), *spec); @@ -542,55 +467,40 @@ TEST(WeylSynthesisTest, IdentityRequiresNoEntanglers) { } TEST(WeylSynthesisTest, FailsWithoutEntanglerInSpec) { - WeylSynthesisFixture fx; + MlirTestContext fx; fx.setUp(); const auto spec = parseNativeSpec("u"); ASSERT_TRUE(spec); EXPECT_TRUE(spec->entanglerBases.empty()); - - OwningOpRef mlirModule = - ModuleOp::create(UnknownLoc::get(fx.ctx())); OpBuilder builder(fx.ctx()); - builder.setInsertionPointToStart(mlirModule->getBody()); const auto qubitTy = QubitType::get(fx.ctx()); const auto funcTy = builder.getFunctionType({qubitTy, qubitTy}, {qubitTy, qubitTy}); - const Location loc = mlirModule->getLoc(); - auto func = func::FuncOp::create(builder, loc, "main", funcTy); + auto func = + func::FuncOp::create(builder, UnknownLoc::get(fx.ctx()), "main", funcTy); auto* entry = func.addEntryBlock(); builder.setInsertionPointToStart(entry); Value out0; Value out1; EXPECT_TRUE(failed(synthesizeUnitary2QWeyl( - builder, loc, entry->getArgument(0), entry->getArgument(1), cxGate01(), - *spec, out0, out1))); -} - -//===----------------------------------------------------------------------===// -// Native gate menu parsing (Weyl library API) -//===----------------------------------------------------------------------===// - -TEST(NativeSpecTest, ResolveIbmBasicCx) { - const auto spec = parseNativeSpec("x,sx,rz,cx"); - ASSERT_TRUE(spec); - EXPECT_TRUE(spec->allowedGates.contains(NativeGateKind::Cx)); - EXPECT_TRUE(spec->allowedGates.contains(NativeGateKind::X)); - EXPECT_FALSE(spec->allowRzz); + builder, func.getLoc(), entry->getArgument(0), entry->getArgument(1), + twoQubitControlledX01(), *spec, out0, out1))); } -TEST(NativeSpecTest, ResolveRejectsUnknownToken) { +TEST(NativeSpecTest, ParsesAndRejectsMenus) { + const auto ibm = parseNativeSpec("x,sx,rz,cx"); + ASSERT_TRUE(ibm); + EXPECT_TRUE(ibm->allowedGates.contains(NativeGateKind::Cx)); + EXPECT_TRUE(ibm->allowedGates.contains(NativeGateKind::X)); + EXPECT_FALSE(ibm->allowRzz); EXPECT_FALSE(parseNativeSpec("x,sx,rz,not-a-gate").has_value()); -} -TEST(NativeSpecTest, PhaseAliasPMatchesRzInIbmStyleMenu) { const auto pMenu = parseNativeSpec("x,sx,p,cx"); const auto rzMenu = parseNativeSpec("x,sx,rz,cx"); ASSERT_TRUE(pMenu); ASSERT_TRUE(rzMenu); EXPECT_EQ(pMenu->allowedGates, rzMenu->allowedGates); -} -TEST(NativePolicyTest, UsesCxAndCzFromResolvedSpec) { const auto cxOnly = parseNativeSpec("u,cx"); ASSERT_TRUE(cxOnly); EXPECT_TRUE(llvm::is_contained(cxOnly->entanglerBases, EntanglerBasis::Cx)); @@ -600,14 +510,9 @@ TEST(NativePolicyTest, UsesCxAndCzFromResolvedSpec) { ASSERT_TRUE(both); EXPECT_TRUE(llvm::is_contained(both->entanglerBases, EntanglerBasis::Cx)); EXPECT_TRUE(llvm::is_contained(both->entanglerBases, EntanglerBasis::Cz)); -} -TEST(NativePolicyTest, ExcludesGateKindsNotInMenu) { - const auto spec = parseNativeSpec("u,cx"); - ASSERT_TRUE(spec); - EXPECT_TRUE(spec->allowedGates.contains(NativeGateKind::U)); - EXPECT_TRUE(spec->allowedGates.contains(NativeGateKind::Cx)); - EXPECT_FALSE(spec->allowedGates.contains(NativeGateKind::X)); - EXPECT_FALSE(spec->allowedGates.contains(NativeGateKind::Sx)); - EXPECT_FALSE(spec->allowedGates.contains(NativeGateKind::Rz)); + const auto generic = parseNativeSpec("u,cx"); + ASSERT_TRUE(generic); + EXPECT_TRUE(generic->allowedGates.contains(NativeGateKind::U)); + EXPECT_FALSE(generic->allowedGates.contains(NativeGateKind::X)); } diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp index 92b0bf8aeb..32a5f1f7c9 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp @@ -19,7 +19,6 @@ #include #include -#include #include #include #include @@ -31,9 +30,7 @@ #include #include #include -#include #include -#include #include #include #include @@ -42,26 +39,100 @@ #include #include #include -#include #include #include #include -#include using namespace mlir; using namespace mlir::qco; +using namespace mqt::test; -namespace mlir::qco::fuse_two_qubit_test { +namespace { -using mqt::test::cxGate01; -using mqt::test::czGate; -using mqt::test::expandToTwoQubits; -using mqt::test::fixTwoQubitMatrixQubitOrder; -using mqt::test::isEquivalentUpToGlobalPhase; -using mqt::test::QubitId; +using ProgramFn = void (*)(mlir::qc::QCProgramBuilder&); +using NativePredicate = bool (*)(OwningOpRef&); -[[nodiscard]] static std::optional -getUnitaryQubitOperand(UnitaryOpInterface op, std::size_t index) { +struct ProfileCase { + const char* name; + ProgramFn program; + const char* nativeGates; + NativePredicate isNative; + bool checkEquivalence; +}; + +struct FusionCase { + const char* name; + ProgramFn program; + const char* nativeGates; + std::optional exactCtrlCount; + std::optional minCtrlCount; + bool checkTwoQUnitary; +}; + +template +bool onlyTheseOps(OwningOpRef& moduleOp, bool allowCx, bool allowCz) { + bool ok = true; + std::ignore = moduleOp->walk([&](UnitaryOpInterface op) { + Operation* raw = op.getOperation(); + if (llvm::isa_and_present(raw->getParentOp())) { + return WalkResult::advance(); + } + if (llvm::isa(raw)) { + return WalkResult::advance(); + } + if (auto ctrl = llvm::dyn_cast(raw)) { + if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { + ok = false; + return WalkResult::interrupt(); + } + Operation* body = ctrl.getBodyUnitary(0).getOperation(); + const bool isCx = llvm::isa(body); + const bool isCz = llvm::isa(body); + if ((isCx && allowCx) || (isCz && allowCz)) { + return WalkResult::advance(); + } + ok = false; + return WalkResult::interrupt(); + } + if (!llvm::isa(raw)) { + ok = false; + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + return ok; +} + +bool onlyIbmBasicCxOps(OwningOpRef& m) { + return onlyTheseOps(m, true, false); +} +bool onlyIbmBasicCzOps(OwningOpRef& m) { + return onlyTheseOps(m, false, true); +} +bool onlyGenericU3CxOps(OwningOpRef& m) { + return onlyTheseOps(m, true, false); +} +bool onlyIqmDefaultOps(OwningOpRef& m) { + return onlyTheseOps(m, false, true); +} +bool onlyIbmFractionalOps(OwningOpRef& m) { + return onlyTheseOps(m, false, true); +} +bool onlyAxisPairRxRzCxOps(OwningOpRef& m) { + return onlyTheseOps(m, true, false); +} +bool onlyAxisPairRxRyCxOps(OwningOpRef& m) { + return onlyTheseOps(m, true, false); +} +bool onlyAxisPairRyRzCzOps(OwningOpRef& m) { + return onlyTheseOps(m, false, true); +} +bool onlyGenericU3CxOrCzOps(OwningOpRef& m) { + return onlyTheseOps(m, true, true); +} + +[[nodiscard]] std::optional unitaryQubitOperand(UnitaryOpInterface op, + std::size_t index) { if (index >= op.getNumQubits()) { return std::nullopt; } @@ -72,8 +143,8 @@ getUnitaryQubitOperand(UnitaryOpInterface op, std::size_t index) { return v; } -[[nodiscard]] static std::optional -getUnitaryQubitResult(UnitaryOpInterface op, std::size_t index) { +[[nodiscard]] std::optional unitaryQubitResult(UnitaryOpInterface op, + std::size_t index) { if (index >= op.getNumQubits()) { return std::nullopt; } @@ -84,8 +155,8 @@ getUnitaryQubitResult(UnitaryOpInterface op, std::size_t index) { return v; } -[[nodiscard]] static bool extractSingleQubitMatrix(UnitaryOpInterface op, - Matrix2x2& out) { +[[nodiscard]] bool extractSingleQubitMatrix(UnitaryOpInterface op, + Matrix2x2& out) { if (op.getUnitaryMatrix2x2(out)) { return true; } @@ -99,19 +170,19 @@ getUnitaryQubitResult(UnitaryOpInterface op, std::size_t index) { return true; } -[[nodiscard]] static bool extractTwoQubitMatrix(UnitaryOpInterface op, - Matrix4x4& out) { +[[nodiscard]] bool extractTwoQubitMatrix(UnitaryOpInterface op, + Matrix4x4& out) { if (auto ctrl = llvm::dyn_cast(op.getOperation())) { if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { return false; } - auto* body = ctrl.getBodyUnitary(0).getOperation(); + Operation* body = ctrl.getBodyUnitary(0).getOperation(); if (llvm::isa(body)) { - out = cxGate01(); + out = twoQubitControlledX01(); return true; } if (llvm::isa(body)) { - out = czGate(); + out = twoQubitControlledZ(); return true; } return false; @@ -119,8 +190,8 @@ getUnitaryQubitResult(UnitaryOpInterface op, std::size_t index) { return op.getUnitaryMatrix4x4(out); } -[[nodiscard]] static std::optional -computeUnitaryFromModule(const OwningOpRef& moduleOp) { +[[nodiscard]] std::optional +computeUnitaryFromQcoModule(const OwningOpRef& moduleOp) { ModuleOp module = moduleOp.get(); if (!module) { return std::nullopt; @@ -153,7 +224,7 @@ computeUnitaryFromModule(const OwningOpRef& moduleOp) { DynamicMatrix::identity(static_cast(1ULL << numQubits)); auto getQubitId = [&](Value qubit) -> std::optional { - auto it = qubitIds.find(qubit); + const auto it = qubitIds.find(qubit); if (it == qubitIds.end()) { return std::nullopt; } @@ -172,11 +243,11 @@ computeUnitaryFromModule(const OwningOpRef& moduleOp) { } if (op.isSingleQubit()) { - const auto qIn = getUnitaryQubitOperand(op, 0); + const auto qIn = unitaryQubitOperand(op, 0); if (!qIn) { return std::nullopt; } - auto qid = getQubitId(*qIn); + const auto qid = getQubitId(*qIn); if (!qid) { return std::nullopt; } @@ -185,7 +256,7 @@ computeUnitaryFromModule(const OwningOpRef& moduleOp) { return std::nullopt; } unitary = embedSingleQubitInNqubit(oneQ, numQubits, *qid) * unitary; - const auto qOut = getUnitaryQubitResult(op, 0); + const auto qOut = unitaryQubitResult(op, 0); if (!qOut) { return std::nullopt; } @@ -194,13 +265,13 @@ computeUnitaryFromModule(const OwningOpRef& moduleOp) { } if (op.isTwoQubit()) { - const auto q0In = getUnitaryQubitOperand(op, 0); - const auto q1In = getUnitaryQubitOperand(op, 1); + const auto q0In = unitaryQubitOperand(op, 0); + const auto q1In = unitaryQubitOperand(op, 1); if (!q0In || !q1In) { return std::nullopt; } - auto q0id = getQubitId(*q0In); - auto q1id = getQubitId(*q1In); + const auto q0id = getQubitId(*q0In); + const auto q1id = getQubitId(*q1In); if (!q0id || !q1id) { return std::nullopt; } @@ -210,8 +281,8 @@ computeUnitaryFromModule(const OwningOpRef& moduleOp) { } unitary = embedTwoQubitInNqubit(twoQ, numQubits, *q0id, *q1id) * unitary; - const auto q0Out = getUnitaryQubitResult(op, 0); - const auto q1Out = getUnitaryQubitResult(op, 1); + const auto q0Out = unitaryQubitResult(op, 0); + const auto q1Out = unitaryQubitResult(op, 1); if (!q0Out || !q1Out) { return std::nullopt; } @@ -228,136 +299,7 @@ computeUnitaryFromModule(const OwningOpRef& moduleOp) { return unitary; } -[[nodiscard]] static std::optional -computeTwoQubitUnitaryFromModule(const OwningOpRef& moduleOp) { - ModuleOp module = moduleOp.get(); - if (!module) { - return std::nullopt; - } - Matrix4x4 unitary = Matrix4x4::identity(); - llvm::DenseMap qubitIds; - std::size_t nextQubitId = 0; - - for (auto func : module.getOps()) { - for (auto& block : func.getBlocks()) { - for (auto& rawOp : block.getOperations()) { - if (auto alloc = llvm::dyn_cast(&rawOp)) { - if (nextQubitId >= 2) { - return std::nullopt; - } - qubitIds.try_emplace(alloc.getResult(), nextQubitId++); - } - } - } - } - - auto getQubitId = [&](Value qubit) -> std::optional { - auto it = qubitIds.find(qubit); - if (it == qubitIds.end()) { - return std::nullopt; - } - return it->second; - }; - - for (auto func : module.getOps()) { - for (auto& block : func.getBlocks()) { - for (auto& rawOp : block.getOperations()) { - auto op = llvm::dyn_cast(&rawOp); - if (!op) { - continue; - } - if (llvm::isa(op.getOperation())) { - continue; - } - - if (op.isSingleQubit()) { - const auto qIn = getUnitaryQubitOperand(op, 0); - if (!qIn) { - return std::nullopt; - } - auto qid = getQubitId(*qIn); - if (!qid) { - return std::nullopt; - } - Matrix2x2 oneQ; - if (!extractSingleQubitMatrix(op, oneQ)) { - return std::nullopt; - } - unitary = - expandToTwoQubits(oneQ, static_cast(*qid)) * unitary; - const auto qOut = getUnitaryQubitResult(op, 0); - if (!qOut) { - return std::nullopt; - } - qubitIds[*qOut] = *qid; - continue; - } - - if (op.isTwoQubit()) { - const auto q0In = getUnitaryQubitOperand(op, 0); - const auto q1In = getUnitaryQubitOperand(op, 1); - if (!q0In || !q1In) { - return std::nullopt; - } - auto q0id = getQubitId(*q0In); - auto q1id = getQubitId(*q1In); - if (!q0id || !q1id) { - return std::nullopt; - } - Matrix4x4 twoQ; - if (!extractTwoQubitMatrix(op, twoQ)) { - return std::nullopt; - } - const llvm::SmallVector ids{static_cast(*q0id), - static_cast(*q1id)}; - unitary = fixTwoQubitMatrixQubitOrder(twoQ, ids) * unitary; - const auto q0Out = getUnitaryQubitResult(op, 0); - const auto q1Out = getUnitaryQubitResult(op, 1); - if (!q0Out || !q1Out) { - return std::nullopt; - } - qubitIds[*q0Out] = *q0id; - qubitIds[*q1Out] = *q1id; - continue; - } - } - } - } - - if (nextQubitId != 2) { - return std::nullopt; - } - return unitary; -} - -static void expectQcoModulesEquivalent(const OwningOpRef& lhs, - const OwningOpRef& rhs) { - const auto lhsUnitary = computeUnitaryFromModule(lhs); - ASSERT_TRUE(lhsUnitary.has_value()); - const auto rhsUnitary = computeUnitaryFromModule(rhs); - ASSERT_TRUE(rhsUnitary.has_value()); - EXPECT_TRUE(isEquivalentUpToGlobalPhase(*lhsUnitary, *rhsUnitary)); -} - -static void -expectTwoQubitQcoModulesEquivalent(const OwningOpRef& lhs, - const OwningOpRef& rhs) { - const auto lhsUnitary = computeTwoQubitUnitaryFromModule(lhs); - ASSERT_TRUE(lhsUnitary.has_value()); - const auto rhsUnitary = computeTwoQubitUnitaryFromModule(rhs); - ASSERT_TRUE(rhsUnitary.has_value()); - EXPECT_TRUE(isEquivalentUpToGlobalPhase(*lhsUnitary, *rhsUnitary)); -} - -/// One row of the standard multi-profile equivalence sweeps in tests. -// NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_F` at global scope -struct FuseTwoQubitProfileSweepCase { - const char* nativeGates; - bool (*isNative)(OwningOpRef&); -}; - -/// Shared gtest fixture for ``fuse-two-qubit-unitary-runs`` pass tests. -// NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_F` at global scope +// NOLINTNEXTLINE(misc-use-internal-linkage) class FuseTwoQubitUnitaryRunsPassTest : public testing::Test { protected: void SetUp() override { @@ -369,106 +311,12 @@ class FuseTwoQubitUnitaryRunsPassTest : public testing::Test { context->loadAllAvailableDialects(); } - template - static bool onlyTheseOps(OwningOpRef& moduleOp, const bool allowCx, - const bool allowCz) { - bool ok = true; - std::ignore = moduleOp->walk([&](UnitaryOpInterface op) { - Operation* raw = op.getOperation(); - if (llvm::isa_and_present(raw->getParentOp())) { - return WalkResult::advance(); - } - if (llvm::isa(raw)) { - return WalkResult::advance(); - } - if (auto ctrl = llvm::dyn_cast(raw)) { - if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { - ok = false; - return WalkResult::interrupt(); - } - Operation* body = ctrl.getBodyUnitary(0).getOperation(); - const bool isCx = llvm::isa(body); - const bool isCz = llvm::isa(body); - if ((isCx && allowCx) || (isCz && allowCz)) { - return WalkResult::advance(); - } - ok = false; - return WalkResult::interrupt(); - } - - if (!llvm::isa(raw)) { - ok = false; - return WalkResult::interrupt(); - } - return WalkResult::advance(); - }); - return ok; - } - - static bool onlyIbmBasicCxOps(OwningOpRef& moduleOp) { - return onlyTheseOps(moduleOp, /*allowCx=*/true, - /*allowCz=*/false); - } - - static bool onlyIbmBasicCzOps(OwningOpRef& moduleOp) { - return onlyTheseOps(moduleOp, /*allowCx=*/false, - /*allowCz=*/true); - } - - static bool onlyGenericU3CxOps(OwningOpRef& moduleOp) { - return onlyTheseOps(moduleOp, /*allowCx=*/true, /*allowCz=*/false); - } - - static bool onlyGenericU3CzOps(OwningOpRef& moduleOp) { - return onlyTheseOps(moduleOp, /*allowCx=*/false, /*allowCz=*/true); - } - - static bool onlyIqmDefaultOps(OwningOpRef& moduleOp) { - return onlyTheseOps(moduleOp, /*allowCx=*/false, /*allowCz=*/true); - } - - static bool onlyIbmFractionalOps(OwningOpRef& moduleOp) { - return onlyTheseOps( - moduleOp, /*allowCx=*/false, /*allowCz=*/true); - } - - static bool onlyAxisPairRxRzCxOps(OwningOpRef& moduleOp) { - return onlyTheseOps(moduleOp, /*allowCx=*/true, - /*allowCz=*/false); - } - - static bool onlyAxisPairRxRyCxOps(OwningOpRef& moduleOp) { - return onlyTheseOps(moduleOp, /*allowCx=*/true, - /*allowCz=*/false); - } - - static bool onlyAxisPairRyRzCzOps(OwningOpRef& moduleOp) { - return onlyTheseOps(moduleOp, /*allowCx=*/false, - /*allowCz=*/true); - } - - static bool onlyUOrAxisPairRxRzCxOps(OwningOpRef& moduleOp) { - return onlyTheseOps(moduleOp, /*allowCx=*/true, - /*allowCz=*/false); - } - - static bool onlyGenericU3CxOrCzOps(OwningOpRef& moduleOp) { - return onlyTheseOps(moduleOp, /*allowCx=*/true, /*allowCz=*/true); - } - - static std::array coreEquivalenceProfiles() { - return {{{.nativeGates = "x,sx,rz,cx", .isNative = &onlyIbmBasicCxOps}, - {.nativeGates = "u,cx", .isNative = &onlyGenericU3CxOps}, - {.nativeGates = "r,cz", .isNative = &onlyIqmDefaultOps}}}; - } - - static void - runFuseTwoQubitUnitaryRunsPipeline(OwningOpRef& moduleOp, - const std::string& nativeGates) { + static void runFusePipeline(OwningOpRef& moduleOp, + StringRef nativeGates) { PassManager pm(moduleOp->getContext()); pm.addPass(createQCToQCO()); pm.addPass(createFuseTwoQubitUnitaryRuns(FuseTwoQubitUnitaryRunsOptions{ - .nativeGates = nativeGates, + .nativeGates = nativeGates.str(), })); ASSERT_TRUE(succeeded(pm.run(*moduleOp))); } @@ -488,62 +336,55 @@ class FuseTwoQubitUnitaryRunsPassTest : public testing::Test { ASSERT_TRUE(succeeded(pm.run(*moduleOp))); } - static std::string moduleToString(const OwningOpRef& moduleOp) { - std::string text; - llvm::raw_string_ostream os(text); - moduleOp.get()->print(os); - return text; + static void expectQcoModulesEquivalent(const OwningOpRef& lhs, + const OwningOpRef& rhs) { + const auto lhsUnitary = computeUnitaryFromQcoModule(lhs); + ASSERT_TRUE(lhsUnitary.has_value()); + const auto rhsUnitary = computeUnitaryFromQcoModule(rhs); + ASSERT_TRUE(rhsUnitary.has_value()); + EXPECT_TRUE(isEquivalentUpToGlobalPhase(*lhsUnitary, *rhsUnitary)); } - template - void expectNativeAfterSynthesis(BuildFn buildFn, - const std::string& nativeGates, - PredicateFn isNative) { - auto moduleOp = buildFn(); - runFuseTwoQubitUnitaryRunsPipeline(moduleOp, nativeGates); + void expectNativeAfterSynthesis(ProgramFn program, StringRef nativeGates, + NativePredicate isNative) { + auto moduleOp = mlir::qc::QCProgramBuilder::build(context.get(), program); + runFusePipeline(moduleOp, nativeGates); EXPECT_TRUE(isNative(moduleOp)); } - template - void expectSynthesisFailure(BuildFn buildFn, const std::string& nativeGates) { - auto moduleOp = buildFn(); + void expectEquivalentAndNativeAfterSynthesis(ProgramFn program, + StringRef nativeGates, + NativePredicate isNative) { + auto expected = mlir::qc::QCProgramBuilder::build(context.get(), program); + runQcToQco(expected); + auto synthesized = + mlir::qc::QCProgramBuilder::build(context.get(), program); + runFusePipeline(synthesized, nativeGates); + EXPECT_TRUE(isNative(synthesized)); + expectQcoModulesEquivalent(expected, synthesized); + } + + void expectSynthesisFailure(ProgramFn program, StringRef nativeGates) { + auto moduleOp = mlir::qc::QCProgramBuilder::build(context.get(), program); PassManager pm(moduleOp->getContext()); pm.addPass(createQCToQCO()); pm.addPass(createFuseTwoQubitUnitaryRuns(FuseTwoQubitUnitaryRunsOptions{ - .nativeGates = nativeGates, + .nativeGates = nativeGates.str(), })); EXPECT_TRUE(failed(pm.run(*moduleOp))); } - template - void expectEquivalentAndNativeAfterSynthesis(BuildFn buildFn, - const std::string& nativeGates, - PredicateFn isNative) { - auto expectedModule = buildFn(); - runQcToQco(expectedModule); - - auto synthesizedModule = buildFn(); - runFuseTwoQubitUnitaryRunsPipeline(synthesizedModule, nativeGates); - EXPECT_TRUE(isNative(synthesizedModule)); - expectQcoModulesEquivalent(expectedModule, synthesizedModule); - } - - template - static void expectTwoQFusePreservesUnitary(MLIRContext* ctx, ProgramT program, - StringRef nativeGates) { - auto build = [&](MLIRContext* context) { - return mlir::qc::QCProgramBuilder::build(context, program); - }; - auto expected = build(ctx); + void expectTwoQFusePreservesUnitary(ProgramFn program, + StringRef nativeGates) { + auto expected = mlir::qc::QCProgramBuilder::build(context.get(), program); ASSERT_TRUE(expected); runQcToQco(expected); - - auto fused = build(ctx); + auto fused = mlir::qc::QCProgramBuilder::build(context.get(), program); ASSERT_TRUE(fused); runQcToQco(fused); runTwoQFuse(fused, nativeGates); ASSERT_TRUE(succeeded(verify(*fused))); - expectTwoQubitQcoModulesEquivalent(expected, fused); + expectQcoModulesEquivalent(expected, fused); } static std::size_t countCtrlOps(const OwningOpRef& moduleOp) { @@ -555,18 +396,14 @@ class FuseTwoQubitUnitaryRunsPassTest : public testing::Test { std::unique_ptr context; }; -} // namespace mlir::qco::fuse_two_qubit_test - -using namespace mlir::qco::fuse_two_qubit_test; +class FuseTwoQubitProfileTest + : public FuseTwoQubitUnitaryRunsPassTest, + public testing::WithParamInterface {}; -struct NativeSynthMenuRow { - const char* name; - const char* nativeGates; - bool (*isNative)(OwningOpRef&); +class FuseTwoQubitFusionTest : public FuseTwoQubitUnitaryRunsPassTest, + public testing::WithParamInterface { }; -// --- Inline circuit builders --- - static void fusionCxCx(mlir::qc::QCProgramBuilder& b) { const auto q0 = b.allocQubit(); const auto q1 = b.allocQubit(); @@ -593,6 +430,15 @@ static void fusionThreeLineCx(mlir::qc::QCProgramBuilder& b) { b.cx(q0, q1); } +static void fusionCxRSharedOtherPair(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + const auto q2 = b.allocQubit(); + b.cx(q0, q1); + b.rz(0.17, q1); + b.cx(q1, q2); +} + static void fusionCxBarrierCx(mlir::qc::QCProgramBuilder& b) { const auto q0 = b.allocQubit(); const auto q1 = b.allocQubit(); @@ -744,152 +590,61 @@ static void determinismSwap(mlir::qc::QCProgramBuilder& b) { b.dealloc(q1); } -// --- Pass profile coverage --- - -// NOLINTNEXTLINE(misc-use-internal-linkage) -class FuseTwoQubitSwapProfileTest - : public FuseTwoQubitUnitaryRunsPassTest, - public testing::WithParamInterface { -public: - using FuseTwoQubitUnitaryRunsPassTest::onlyGenericU3CxOps; - using FuseTwoQubitUnitaryRunsPassTest::onlyIbmBasicCxOps; - using FuseTwoQubitUnitaryRunsPassTest::onlyIqmDefaultOps; -}; +} // namespace -TEST_P(FuseTwoQubitSwapProfileTest, DecomposesSwapToProfile) { - const NativeSynthMenuRow& param = GetParam(); - expectNativeAfterSynthesis( - [&] { - return mlir::qc::QCProgramBuilder::build(context.get(), mlir::qc::swap); - }, - param.nativeGates, param.isNative); +TEST_P(FuseTwoQubitProfileTest, SynthesizesToNativeMenu) { + const ProfileCase& c = GetParam(); + if (c.checkEquivalence) { + expectEquivalentAndNativeAfterSynthesis(c.program, c.nativeGates, + c.isNative); + } else { + expectNativeAfterSynthesis(c.program, c.nativeGates, c.isNative); + } } INSTANTIATE_TEST_SUITE_P( - SwapMenuMatrix, FuseTwoQubitSwapProfileTest, + Menus, FuseTwoQubitProfileTest, testing::Values( - NativeSynthMenuRow{"IbmBasicCx", "x,sx,rz,cx", - &FuseTwoQubitSwapProfileTest::onlyIbmBasicCxOps}, - NativeSynthMenuRow{"GenericU3Cx", "u,cx", - &FuseTwoQubitSwapProfileTest::onlyGenericU3CxOps}, - NativeSynthMenuRow{"IqmDefault", "r,cz", - &FuseTwoQubitSwapProfileTest::onlyIqmDefaultOps}), - [](const testing::TestParamInfo& info) { + ProfileCase{"SwapIbmBasic", mlir::qc::swap, "x,sx,rz,cx", + onlyIbmBasicCxOps, false}, + ProfileCase{"SwapGeneric", mlir::qc::swap, "u,cx", onlyGenericU3CxOps, + false}, + ProfileCase{"SwapIqm", mlir::qc::swap, "r,cz", onlyIqmDefaultOps, + false}, + ProfileCase{"HstycxIbm", hstycxTwoQ, "x,sx,rz,cx", onlyIbmBasicCxOps, + false}, + ProfileCase{"CxYIqm", cxYOnQ1, "r,cz", onlyIqmDefaultOps, false}, + ProfileCase{"BroadOneQIqm", broadOneQThenCz, "r,cz", onlyIqmDefaultOps, + false}, + ProfileCase{"ZeroAngleRyRzCz", zeroAngleThenCz, "ry,rz,cz", + onlyAxisPairRyRzCzOps, false}, + ProfileCase{"HCxTIbmCz", hCxTOnQ1, "x,sx,rz,cz", onlyIbmBasicCzOps, + false}, + ProfileCase{"XYSXCzIqm", xYSXCz, "r,cz", onlyIqmDefaultOps, false}, + ProfileCase{"IbmFractional", ibmFractionalGateFamilies, + "x,sx,rz,rx,rzz,cz", onlyIbmFractionalOps, false}, + ProfileCase{"HYCxRxRz", hYCx, "rx,rz,cx", onlyAxisPairRxRzCxOps, false}, + ProfileCase{"ZCxRxRy", zCx, "rx,ry,cx", onlyAxisPairRxRyCxOps, false}, + ProfileCase{"Hq0Yq1CxSq0", hq0Yq1CxSq0, "u,cx", onlyGenericU3CxOps, + true}, + ProfileCase{"XHCzRyRz", xHCz, "ry,rz,cz", onlyAxisPairRyRzCzOps, true}, + ProfileCase{"HCxSq1MultiEnt", hCxSq1, "u,cx,cz", onlyGenericU3CxOrCzOps, + true}), + [](const testing::TestParamInfo& info) { return info.param.name; }); -TEST_F(FuseTwoQubitUnitaryRunsPassTest, DecomposesHstycxToIbmBasicCx) { - expectNativeAfterSynthesis( - [&] { - return mlir::qc::QCProgramBuilder::build(context.get(), hstycxTwoQ); - }, - "x,sx,rz,cx", &FuseTwoQubitUnitaryRunsPassTest::onlyIbmBasicCxOps); -} - -TEST_F(FuseTwoQubitUnitaryRunsPassTest, DecomposesCxYOnQ1ToIqmDefault) { - expectNativeAfterSynthesis( - [&] { return mlir::qc::QCProgramBuilder::build(context.get(), cxYOnQ1); }, - "r,cz", &FuseTwoQubitUnitaryRunsPassTest::onlyIqmDefaultOps); -} - -TEST_F(FuseTwoQubitUnitaryRunsPassTest, BroadOneQCanonicalizationOnIqmDefault) { - auto moduleOp = - mlir::qc::QCProgramBuilder::build(context.get(), broadOneQThenCz); - runFuseTwoQubitUnitaryRunsPipeline(moduleOp, "r,cz"); - EXPECT_TRUE(onlyIqmDefaultOps(moduleOp)); -} - -TEST_F(FuseTwoQubitUnitaryRunsPassTest, ZeroAngleCanonicalizationOnRyRzCz) { - auto moduleOp = - mlir::qc::QCProgramBuilder::build(context.get(), zeroAngleThenCz); - runFuseTwoQubitUnitaryRunsPipeline(moduleOp, "ry,rz,cz"); - EXPECT_TRUE(onlyAxisPairRyRzCzOps(moduleOp)); -} - -TEST_F(FuseTwoQubitUnitaryRunsPassTest, DecomposesCxToCzForIbmBasicCzProfile) { - expectNativeAfterSynthesis( - [&] { - return mlir::qc::QCProgramBuilder::build(context.get(), hCxTOnQ1); - }, - "x,sx,rz,cz", &FuseTwoQubitUnitaryRunsPassTest::onlyIbmBasicCzOps); -} - -TEST_F(FuseTwoQubitUnitaryRunsPassTest, DecomposesToIqmDefaultProfile) { - expectNativeAfterSynthesis( - [&] { return mlir::qc::QCProgramBuilder::build(context.get(), xYSXCz); }, - "r,cz", &FuseTwoQubitUnitaryRunsPassTest::onlyIqmDefaultOps); -} - -TEST_F(FuseTwoQubitUnitaryRunsPassTest, DecomposesToIbmFractionalProfile) { - expectNativeAfterSynthesis( - [&] { - return mlir::qc::QCProgramBuilder::build(context.get(), - ibmFractionalGateFamilies); - }, - "x,sx,rz,rx,rzz,cz", - &FuseTwoQubitUnitaryRunsPassTest::onlyIbmFractionalOps); -} - -TEST_F(FuseTwoQubitUnitaryRunsPassTest, DecomposesToAxisPairRxRzCxProfile) { - expectNativeAfterSynthesis( - [&] { return mlir::qc::QCProgramBuilder::build(context.get(), hYCx); }, - "rx,rz,cx", &FuseTwoQubitUnitaryRunsPassTest::onlyAxisPairRxRzCxOps); -} - -TEST_F(FuseTwoQubitUnitaryRunsPassTest, DecomposesRzToAxisPairRxRyCxProfile) { - expectNativeAfterSynthesis( - [&] { return mlir::qc::QCProgramBuilder::build(context.get(), zCx); }, - "rx,ry,cx", &FuseTwoQubitUnitaryRunsPassTest::onlyAxisPairRxRyCxOps); -} - -TEST_F(FuseTwoQubitUnitaryRunsPassTest, - GenericProfileMatchesGenericU3CxBehavior) { - expectEquivalentAndNativeAfterSynthesis( - [&] { - return mlir::qc::QCProgramBuilder::build(context.get(), hq0Yq1CxSq0); - }, - "u,cx", &FuseTwoQubitUnitaryRunsPassTest::onlyGenericU3CxOps); -} - -TEST_F(FuseTwoQubitUnitaryRunsPassTest, - GenericProfileMatchesAxisPairRyRzCzBehavior) { - expectEquivalentAndNativeAfterSynthesis( - [&] { return mlir::qc::QCProgramBuilder::build(context.get(), xHCz); }, - "ry,rz,cz", &FuseTwoQubitUnitaryRunsPassTest::onlyAxisPairRyRzCzOps); -} - -TEST_F(FuseTwoQubitUnitaryRunsPassTest, - CustomProfileAcceptsMultipleEntanglersMenu) { - expectEquivalentAndNativeAfterSynthesis( - [&] { return mlir::qc::QCProgramBuilder::build(context.get(), hCxSq1); }, - "u,cx,cz", &FuseTwoQubitUnitaryRunsPassTest::onlyGenericU3CxOrCzOps); -} - TEST_F(FuseTwoQubitUnitaryRunsPassTest, FailsForUnsupportedNativeGateMenu) { - expectSynthesisFailure( - [&] { - return mlir::qc::QCProgramBuilder::build(context.get(), mlir::qc::h); - }, - "not-a-gate"); + expectSynthesisFailure(mlir::qc::h, "not-a-gate"); } TEST_F(FuseTwoQubitUnitaryRunsPassTest, FailsForNativeGateMenuWithoutSingleQEmitter) { - expectSynthesisFailure( - [&] { - return mlir::qc::QCProgramBuilder::build(context.get(), - mlir::qc::singleControlledX); - }, - "cx,cz"); + expectSynthesisFailure(mlir::qc::singleControlledX, "cx,cz"); } TEST_F(FuseTwoQubitUnitaryRunsPassTest, FailsForMultiControlledGateStructure) { - expectSynthesisFailure( - [&] { - return mlir::qc::QCProgramBuilder::build(context.get(), - mlir::qc::multipleControlledX); - }, - "x,sx,rz,cx"); + expectSynthesisFailure(mlir::qc::multipleControlledX, "x,sx,rz,cx"); } TEST_F(FuseTwoQubitUnitaryRunsPassTest, @@ -898,26 +653,84 @@ TEST_F(FuseTwoQubitUnitaryRunsPassTest, return mlir::qc::QCProgramBuilder::build(context.get(), determinismSwap); }; auto firstModule = buildFn(); - runFuseTwoQubitUnitaryRunsPipeline(firstModule, "u,cx"); + runFusePipeline(firstModule, "u,cx"); auto secondModule = buildFn(); - runFuseTwoQubitUnitaryRunsPipeline(secondModule, "u,cx"); - EXPECT_EQ(moduleToString(firstModule), moduleToString(secondModule)); + runFusePipeline(secondModule, "u,cx"); + + std::string first; + std::string second; + llvm::raw_string_ostream osFirst(first); + llvm::raw_string_ostream osSecond(second); + firstModule->print(osFirst); + secondModule->print(osSecond); + EXPECT_EQ(first, second); } TEST_F(FuseTwoQubitUnitaryRunsPassTest, ThreeQubitGhzEquivalentOnCoreProfiles) { - for (const auto& profileCase : coreEquivalenceProfiles()) { + const std::array profiles{{ + {.name = "GhzIbm", + .program = threeQGhz, + .nativeGates = "x,sx,rz,cx", + .isNative = onlyIbmBasicCxOps, + .checkEquivalence = false}, + {.name = "GhzGeneric", + .program = threeQGhz, + .nativeGates = "u,cx", + .isNative = onlyGenericU3CxOps, + .checkEquivalence = false}, + {.name = "GhzIqm", + .program = threeQGhz, + .nativeGates = "r,cz", + .isNative = onlyIqmDefaultOps, + .checkEquivalence = false}, + }}; + for (const ProfileCase& profile : profiles) { auto expected = mlir::qc::QCProgramBuilder::build(context.get(), threeQGhz); runQcToQco(expected); - auto synthesized = mlir::qc::QCProgramBuilder::build(context.get(), threeQGhz); - runFuseTwoQubitUnitaryRunsPipeline(synthesized, profileCase.nativeGates); - EXPECT_TRUE(profileCase.isNative(synthesized)); + runFusePipeline(synthesized, profile.nativeGates); + EXPECT_TRUE(profile.isNative(synthesized)); expectQcoModulesEquivalent(expected, synthesized); } } -// --- Two-qubit window fusion (pass internals) --- +TEST_P(FuseTwoQubitFusionTest, WindowFusionBehavior) { + const FusionCase& c = GetParam(); + if (c.checkTwoQUnitary) { + expectTwoQFusePreservesUnitary(c.program, c.nativeGates); + } + auto module = mlir::qc::QCProgramBuilder::build(context.get(), c.program); + ASSERT_TRUE(module); + runQcToQco(module); + runTwoQFuse(module, c.nativeGates); + if (c.exactCtrlCount) { + EXPECT_EQ(countCtrlOps(module), *c.exactCtrlCount); + } + if (c.minCtrlCount) { + EXPECT_GE(countCtrlOps(module), *c.minCtrlCount); + } +} + +INSTANTIATE_TEST_SUITE_P( + Windows, FuseTwoQubitFusionTest, + testing::Values(FusionCase{"AdjacentCxCancel", fusionCxCx, "u,cx", 0, + std::nullopt, true}, + FusionCase{"InterleavedOneQ", fusionHCxInterleavedTCx, + "u,cx", std::nullopt, std::nullopt, true}, + FusionCase{"DifferentPairBoundary", fusionThreeLineCx, + "u,cx", std::nullopt, 1, false}, + FusionCase{"SharedWireOneQ", fusionCxRSharedOtherPair, + "u,cx", std::nullopt, 2, false}, + FusionCase{"BarrierBoundary", fusionCxBarrierCx, "u,cx", 2, + std::nullopt, false}, + FusionCase{"SwappedWireOrder", fusionSwapCxPattern, "u,cx", + std::nullopt, std::nullopt, true}, + FusionCase{"RzzBlock", fusionHRzzSRzz, "x,sx,rz,rx,rzz,cz", + std::nullopt, std::nullopt, true}), + [](const testing::TestParamInfo& info) { + return info.param.name; + }); TEST_F(FuseTwoQubitUnitaryRunsPassTest, InvalidNativeGatesFailsPass) { auto module = mlir::qc::QCProgramBuilder::build(context.get(), fusionCxCx); @@ -929,45 +742,3 @@ TEST_F(FuseTwoQubitUnitaryRunsPassTest, InvalidNativeGatesFailsPass) { })); EXPECT_TRUE(failed(pm.run(*module))); } - -TEST_F(FuseTwoQubitUnitaryRunsPassTest, AdjacentCxCancel) { - expectTwoQFusePreservesUnitary(context.get(), fusionCxCx, "u,cx"); - - auto module = mlir::qc::QCProgramBuilder::build(context.get(), fusionCxCx); - ASSERT_TRUE(module); - runQcToQco(module); - runTwoQFuse(module, "u,cx"); - EXPECT_EQ(countCtrlOps(module), 0U); -} - -TEST_F(FuseTwoQubitUnitaryRunsPassTest, FusesCxThroughInterleavedOneQOps) { - expectTwoQFusePreservesUnitary(context.get(), fusionHCxInterleavedTCx, - "u,cx"); -} - -TEST_F(FuseTwoQubitUnitaryRunsPassTest, StopsAtDifferentPairBoundary) { - auto module = - mlir::qc::QCProgramBuilder::build(context.get(), fusionThreeLineCx); - ASSERT_TRUE(module); - runQcToQco(module); - runTwoQFuse(module, "u,cx"); - EXPECT_GE(countCtrlOps(module), 1U); -} - -TEST_F(FuseTwoQubitUnitaryRunsPassTest, DoesNotFuseAcrossBarrier) { - auto module = - mlir::qc::QCProgramBuilder::build(context.get(), fusionCxBarrierCx); - ASSERT_TRUE(module); - runQcToQco(module); - runTwoQFuse(module, "u,cx"); - EXPECT_EQ(countCtrlOps(module), 2U); -} - -TEST_F(FuseTwoQubitUnitaryRunsPassTest, HandlesSwappedWireOrder) { - expectTwoQFusePreservesUnitary(context.get(), fusionSwapCxPattern, "u,cx"); -} - -TEST_F(FuseTwoQubitUnitaryRunsPassTest, HandlesRzzBlock) { - expectTwoQFusePreservesUnitary(context.get(), fusionHRzzSRzz, - "x,sx,rz,rx,rzz,cz"); -} diff --git a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp index a2ad8f9bf2..421b123147 100644 --- a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp +++ b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp @@ -404,6 +404,16 @@ TEST(UnitaryMatrix4x4, ScalarLeftMultiply) { EXPECT_TRUE((scalar * swap).isApprox(swap * scalar)); } +TEST(GateMatrixFactories, RemEuclidAndControlledGates) { + EXPECT_DOUBLE_EQ(remEuclid(-1.0, 3.0), 2.0); + EXPECT_TRUE(twoQubitControlledX01().isApprox( + Matrix4x4::fromElements(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0))); + EXPECT_TRUE(twoQubitControlledX10().isApprox( + reorderTwoQubitMatrix(twoQubitControlledX01(), 1, 0))); + EXPECT_TRUE(rxMatrix(0.0).isIdentity()); + EXPECT_TRUE((iPauliX() * iPauliX()).isApprox(-1.0 * Matrix2x2::identity())); +} + TEST(JacobiEigensolver, DiagonalMatrix) { std::array a{}; a[0] = 3.0; diff --git a/mlir/unittests/TestCaseUtils.h b/mlir/unittests/TestCaseUtils.h index 75a9fc325d..7603fda8be 100644 --- a/mlir/unittests/TestCaseUtils.h +++ b/mlir/unittests/TestCaseUtils.h @@ -10,23 +10,18 @@ #pragma once -#include "mlir/Dialect/QCO/Utils/Matrix.h" #include "mlir/Support/PrettyPrinting.h" #include #include -#include #include -#include #include #include -#include #include #include // NOLINT(misc-include-cleaner) #include #include -#include #include #include #include @@ -161,179 +156,6 @@ class DeferredPrinter { std::vector> entries_; }; -// --------------------------------------------------------------------------- -// Gate-matrix factories and two-qubit layout helpers shared by the -// decomposition / native-synthesis unit tests. They build reference unitaries -// for equivalence checks and are intentionally test-only, so they live here -// rather than in the production decomposition headers. -// --------------------------------------------------------------------------- - -/// Hadamard gate (2x2). -[[nodiscard]] inline const mlir::qco::Matrix2x2& hGate() { - constexpr double frac1Sqrt2 = - 0.707106781186547524400844362104849039284835937688474036588L; - static const mlir::qco::Matrix2x2 matrix = mlir::qco::Matrix2x2::fromElements( - frac1Sqrt2, frac1Sqrt2, frac1Sqrt2, -frac1Sqrt2); - return matrix; -} - -/// Logical qubit index within a two-qubit reference matrix. -using QubitId = std::size_t; - -/// `SWAP` gate. -[[nodiscard]] inline const mlir::qco::Matrix4x4& swapGate() { - return mlir::qco::twoQubitSwapMatrix(); -} - -/// Embed a single-qubit matrix into the two-qubit space acting on `qubitId`. -[[nodiscard]] inline mlir::qco::Matrix4x4 -expandToTwoQubits(const mlir::qco::Matrix2x2& singleQubitMatrix, - QubitId qubitId) { - return mlir::qco::embedSingleQubitInTwoQubit(singleQubitMatrix, qubitId); -} - -/// Reorder a two-qubit matrix so it acts on qubits `{0, 1}`. -[[nodiscard]] inline mlir::qco::Matrix4x4 -fixTwoQubitMatrixQubitOrder(const mlir::qco::Matrix4x4& twoQubitMatrix, - const llvm::SmallVector& qubitIds) { - if (qubitIds.size() != 2) { - llvm::reportFatalInternalError( - "Invalid qubit IDs for fixing two-qubit matrix"); - } - return mlir::qco::reorderTwoQubitMatrix(twoQubitMatrix, qubitIds[0], - qubitIds[1]); -} - -constexpr double kSanityCheckPrecision = 1e-12; - -[[nodiscard]] inline bool isUnitaryMatrix(const mlir::qco::Matrix2x2& matrix, - double tolerance = 1e-12) { - return (matrix.adjoint() * matrix).isIdentity(tolerance); -} - -[[nodiscard]] inline double remEuclid(double a, double b) { - if (b == 0.0) { - llvm::reportFatalInternalError("remEuclid expects non-zero divisor"); - } - const auto r = std::fmod(a, b); - return (r < 0.0) ? r + std::abs(b) : r; -} - -[[nodiscard]] inline double traceToFidelity(const std::complex& x) { - const auto xAbs = std::abs(x); - return (4.0 + (xAbs * xAbs)) / 20.0; -} - -[[nodiscard]] inline std::complex -globalPhaseFactor(double globalPhase) { - return std::exp(std::complex{0, 1} * globalPhase); -} - -[[nodiscard]] inline mlir::qco::Matrix4x4 rxxMatrix(double theta) { - const auto cosTheta = std::cos(theta / 2.); - const mlir::qco::Complex misin{0., -std::sin(theta / 2.)}; - return mlir::qco::Matrix4x4::fromElements(cosTheta, 0, 0, misin, // - 0, cosTheta, misin, 0, // - 0, misin, cosTheta, 0, // - misin, 0, 0, cosTheta); -} - -[[nodiscard]] inline mlir::qco::Matrix4x4 ryyMatrix(double theta) { - const auto cosTheta = std::cos(theta / 2.); - const mlir::qco::Complex isin{0., std::sin(theta / 2.)}; - const mlir::qco::Complex misin{0., -std::sin(theta / 2.)}; - return mlir::qco::Matrix4x4::fromElements(cosTheta, 0, 0, isin, // - 0, cosTheta, misin, 0, // - 0, misin, cosTheta, 0, // - isin, 0, 0, cosTheta); -} - -[[nodiscard]] inline mlir::qco::Matrix4x4 rzzMatrix(double theta) { - const auto cosTheta = std::cos(theta / 2.); - const auto sinTheta = std::sin(theta / 2.); - const mlir::qco::Complex em{cosTheta, -sinTheta}; - const mlir::qco::Complex ep{cosTheta, sinTheta}; - return mlir::qco::Matrix4x4::fromElements(em, 0, 0, 0, // - 0, ep, 0, 0, // - 0, 0, ep, 0, // - 0, 0, 0, em); -} - -[[nodiscard]] inline const mlir::qco::Matrix4x4& cxGate01() { - static const mlir::qco::Matrix4x4 matrix = - mlir::qco::Matrix4x4::fromElements(1, 0, 0, 0, // - 0, 1, 0, 0, // - 0, 0, 0, 1, // - 0, 0, 1, 0); - return matrix; -} - -[[nodiscard]] inline const mlir::qco::Matrix4x4& cxGate10() { - static const mlir::qco::Matrix4x4 matrix = - mlir::qco::Matrix4x4::fromElements(1, 0, 0, 0, // - 0, 0, 0, 1, // - 0, 0, 1, 0, // - 0, 1, 0, 0); - return matrix; -} - -[[nodiscard]] inline const mlir::qco::Matrix4x4& czGate() { - static const mlir::qco::Matrix4x4 matrix = - mlir::qco::Matrix4x4::fromElements(1, 0, 0, 0, // - 0, 1, 0, 0, // - 0, 0, 1, 0, // - 0, 0, 0, -1); - return matrix; -} - -[[nodiscard]] inline std::vector> -randomUnitaryData(std::size_t dim, std::mt19937& rng) { - std::normal_distribution normalDist(0.0, 1.0); - std::vector>> columns( - dim, std::vector>(dim)); - for (auto& column : columns) { - for (auto& entry : column) { - entry = std::complex(normalDist(rng), normalDist(rng)); - } - } - for (std::size_t j = 0; j < dim; ++j) { - for (std::size_t k = 0; k < j; ++k) { - std::complex projection{0.0, 0.0}; - for (std::size_t i = 0; i < dim; ++i) { - projection += std::conj(columns[k][i]) * columns[j][i]; - } - for (std::size_t i = 0; i < dim; ++i) { - columns[j][i] -= projection * columns[k][i]; - } - } - double norm = 0.0; - for (std::size_t i = 0; i < dim; ++i) { - norm += std::norm(columns[j][i]); - } - norm = std::sqrt(norm); - for (std::size_t i = 0; i < dim; ++i) { - columns[j][i] /= norm; - } - } - std::vector> data(dim * dim); - for (std::size_t row = 0; row < dim; ++row) { - for (std::size_t col = 0; col < dim; ++col) { - data[(row * dim) + col] = columns[col][row]; - } - } - return data; -} - -[[nodiscard]] inline mlir::qco::Matrix4x4 randomUnitary4x4(std::mt19937& rng) { - const auto data = randomUnitaryData(4, rng); - const mlir::qco::Matrix4x4 unitary = mlir::qco::Matrix4x4::fromElements( - data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], - data[8], data[9], data[10], data[11], data[12], data[13], data[14], - data[15]); - assert((unitary.adjoint() * unitary).isIdentity(1e-12)); - return unitary; -} - } // namespace mqt::test #define MQT_NAMED_BUILDER(fn) ::mqt::test::namedBuilder(#fn, fn) From 41d00771ab7644b24a5a7ec1d9a5d0a3edd66bc4 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Sun, 21 Jun 2026 13:52:59 +0200 Subject: [PATCH 053/122] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20linter=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/Decomposition/Weyl.h | 4 +- .../Decomposition/NativeProfile.cpp | 55 +++++---- .../QCO/Transforms/Decomposition/Weyl.cpp | 28 ++--- .../FuseTwoQubitUnitaryRuns.cpp | 58 +++++----- mlir/lib/Dialect/QCO/Utils/Matrix.cpp | 24 ++-- .../Decomposition/test_weyl_decomposition.cpp | 105 +++++++++--------- .../test_fuse_two_qubit_unitary_runs.cpp | 65 ++++++----- 7 files changed, 168 insertions(+), 171 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h index 503e2d2fa1..a05b1ab2bb 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h @@ -188,8 +188,8 @@ class TwoQubitBasisDecomposer { Matrix2x2 q2r; }; - [[nodiscard]] TwoQubitLocalUnitaryList - decomp0(const TwoQubitWeylDecomposition& target) const; + [[nodiscard]] static TwoQubitLocalUnitaryList + decomp0(const TwoQubitWeylDecomposition& target); [[nodiscard]] TwoQubitLocalUnitaryList decomp1(const TwoQubitWeylDecomposition& target) const; [[nodiscard]] TwoQubitLocalUnitaryList diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp index c604133906..d954355bdc 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp @@ -10,6 +10,7 @@ #include "mlir/Dialect/QCO/Transforms/Decomposition/NativeProfile.h" +#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" @@ -19,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -27,21 +29,16 @@ #include #include +#include #include #include -#include using mlir::qco::Matrix2x2; using mlir::qco::Matrix4x4; -using mlir::qco::twoQubitControlledX01; -using mlir::qco::twoQubitControlledZ; namespace mlir::qco::decomposition { -namespace { - -[[nodiscard]] std::optional -parseGateToken(llvm::StringRef name) { +static std::optional parseGateToken(llvm::StringRef name) { return llvm::StringSwitch>(name) .Case("u", NativeGateKind::U) .Case("x", NativeGateKind::X) @@ -56,7 +53,7 @@ parseGateToken(llvm::StringRef name) { .Default(std::nullopt); } -[[nodiscard]] std::optional> +static std::optional> parseGateSet(llvm::StringRef nativeGates) { llvm::DenseSet gates; llvm::SmallVector parts; @@ -75,17 +72,17 @@ parseGateSet(llvm::StringRef nativeGates) { return gates; } -[[nodiscard]] SingleQubitEmitterSpec +static SingleQubitEmitterSpec makeEmitterSpec(SingleQubitMode mode, AxisPair axisPair = AxisPair::RxRz, bool supportsDirectRx = false) { return { .mode = mode, .axisPair = axisPair, .supportsDirectRx = supportsDirectRx}; } -void addEmitterIfAbsent(llvm::SmallVectorImpl& emitters, - SingleQubitMode mode, - AxisPair axisPair = AxisPair::RxRz, - bool supportsDirectRx = false) { +static void +addEmitterIfAbsent(llvm::SmallVectorImpl& emitters, + SingleQubitMode mode, AxisPair axisPair = AxisPair::RxRz, + bool supportsDirectRx = false) { const bool present = llvm::any_of(emitters, [&](const auto& e) { return e.mode == mode && e.axisPair == axisPair && e.supportsDirectRx == supportsDirectRx; @@ -95,7 +92,7 @@ void addEmitterIfAbsent(llvm::SmallVectorImpl& emitters, } } -[[nodiscard]] llvm::SmallVector +static llvm::SmallVector allowedGatesForEmitter(const SingleQubitEmitterSpec& emitter) { switch (emitter.mode) { case SingleQubitMode::ZSXX: { @@ -124,7 +121,7 @@ allowedGatesForEmitter(const SingleQubitEmitterSpec& emitter) { llvm_unreachable("unknown single-qubit mode"); } -[[nodiscard]] llvm::SmallVector +static llvm::SmallVector allowedGatesForEntangler(EntanglerBasis entangler) { switch (entangler) { case EntanglerBasis::None: @@ -137,7 +134,7 @@ allowedGatesForEntangler(EntanglerBasis entangler) { llvm_unreachable("unknown entangler basis"); } -void populateAllowedGates(NativeProfileSpec& spec) { +static void populateAllowedGates(NativeProfileSpec& spec) { spec.allowedGates.clear(); for (const auto& emitter : spec.singleQubitEmitters) { const auto allowed = allowedGatesForEmitter(emitter); @@ -152,7 +149,7 @@ void populateAllowedGates(NativeProfileSpec& spec) { } } -[[nodiscard]] std::optional +static std::optional selectEntangler(const NativeProfileSpec& spec) { if (llvm::is_contained(spec.entanglerBases, EntanglerBasis::Cx)) { return EntanglerBasis::Cx; @@ -163,12 +160,12 @@ selectEntangler(const NativeProfileSpec& spec) { return std::nullopt; } -[[nodiscard]] Matrix4x4 entanglerMatrix(EntanglerBasis entangler) { - return entangler == EntanglerBasis::Cz ? twoQubitControlledZ() - : twoQubitControlledX01(); +static Matrix4x4 entanglerMatrix(EntanglerBasis entangler) { + return entangler == EntanglerBasis::Cz ? mlir::qco::twoQubitControlledZ() + : mlir::qco::twoQubitControlledX01(); } -[[nodiscard]] std::optional +static std::optional decomposeWithEntangler(const Matrix4x4& target, EntanglerBasis entangler) { auto decomposer = TwoQubitBasisDecomposer::create(entanglerMatrix(entangler), 1.0); @@ -176,24 +173,22 @@ decomposeWithEntangler(const Matrix4x4& target, EntanglerBasis entangler) { return decomposer.twoQubitDecompose(weyl, std::nullopt); } -void emitGPhaseIfNonTrivial(OpBuilder& builder, Location loc, double phase) { +static void emitGPhaseIfNonTrivial(OpBuilder& builder, Location loc, + double phase) { constexpr double epsilon = 1e-12; if (std::abs(phase) > epsilon) { GPhaseOp::create(builder, loc, phase); } } -[[nodiscard]] Value emitSingleQubitMatrix(OpBuilder& builder, Location loc, - Value inQubit, - const Matrix2x2& matrix, - EulerBasis basis) { +static Value emitSingleQubitMatrix(OpBuilder& builder, Location loc, + Value inQubit, const Matrix2x2& matrix, + EulerBasis basis) { return *synthesizeUnitary1QEuler(builder, loc, inQubit, matrix, /*runSize=*/0, /*hasNonBasisGate=*/true, basis); } -} // namespace - EulerBasis emitterEulerBasis(const SingleQubitEmitterSpec& emitter) { switch (emitter.mode) { case SingleQubitMode::ZSXX: @@ -417,8 +412,8 @@ bool assignTwoQubitOpMatrix(Operation* op, Matrix4x4& matrix) { if (!basis) { return false; } - matrix = *basis == EntanglerBasis::Cz ? twoQubitControlledZ() - : twoQubitControlledX01(); + matrix = *basis == EntanglerBasis::Cz ? mlir::qco::twoQubitControlledZ() + : mlir::qco::twoQubitControlledX01(); return true; } auto unitary = llvm::dyn_cast(op); diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp index 245bdfef87..86dd7dd173 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp @@ -41,8 +41,6 @@ namespace mlir::qco::decomposition { using namespace std::complex_literals; -namespace { - enum class Specialization : std::uint8_t { General, IdEquiv, @@ -63,8 +61,8 @@ enum class MagicBasisTransform : std::uint8_t { static constexpr auto DIAGONALIZATION_PRECISION = 1e-13; -[[nodiscard]] Matrix4x4 magicBasisTransform(const Matrix4x4& unitary, - MagicBasisTransform direction) { +static Matrix4x4 magicBasisTransform(const Matrix4x4& unitary, + MagicBasisTransform direction) { const Matrix4x4 bNonNormalized = Matrix4x4::fromElements( // 1, 1i, 0, 0, // 0, 0, 1i, 1, // @@ -84,14 +82,14 @@ static constexpr auto DIAGONALIZATION_PRECISION = 1e-13; llvm::reportFatalInternalError("Unknown MagicBasisTransform direction!"); } -[[nodiscard]] double closestPartialSwap(double a, double b, double c) { +static double 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.); } -[[nodiscard]] std::pair> +static std::pair> diagonalizeComplexSymmetric(const Matrix4x4& m, double precision) { auto state = std::mt19937{2023}; std::normal_distribution dist; @@ -141,7 +139,7 @@ diagonalizeComplexSymmetric(const Matrix4x4& m, double precision) { maxDiagonalizationAttempts, bestErr, precision)); } -[[nodiscard]] std::tuple +static std::tuple decomposeTwoQubitProductGate(const Matrix4x4& specialUnitary) { Matrix2x2 r = Matrix2x2::fromElements(specialUnitary(0, 0), specialUnitary(0, 1), @@ -174,8 +172,8 @@ decomposeTwoQubitProductGate(const Matrix4x4& specialUnitary) { return {l, r, phase}; } -[[nodiscard]] std::complex getTrace(double a, double b, double c, - double ap, double bp, double cp) { +static std::complex 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; @@ -183,7 +181,7 @@ decomposeTwoQubitProductGate(const Matrix4x4& specialUnitary) { std::sin(da) * std::sin(db) * std::sin(dc)}; } -[[nodiscard]] Specialization +static Specialization bestSpecialization(const TwoQubitWeylDecomposition& decomposition, const std::optional& requestedFidelity) { auto isClose = [&](double ap, double bp, double cp) -> bool { @@ -238,8 +236,8 @@ bestSpecialization(const TwoQubitWeylDecomposition& decomposition, return Specialization::General; } -[[nodiscard]] bool relativeEq(double lhs, double rhs, double epsilon, - double maxRelative) { +static bool relativeEq(double lhs, double rhs, double epsilon, + double maxRelative) { if (lhs == rhs) { return true; } @@ -258,8 +256,6 @@ bestSpecialization(const TwoQubitWeylDecomposition& decomposition, return absDiff <= absLhs * maxRelative; } -} // namespace - TwoQubitWeylDecomposition TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, std::optional fidelity) { @@ -891,8 +887,8 @@ TwoQubitBasisDecomposer::twoQubitDecompose( }; } -TwoQubitLocalUnitaryList TwoQubitBasisDecomposer::decomp0( - const TwoQubitWeylDecomposition& target) const { +TwoQubitLocalUnitaryList +TwoQubitBasisDecomposer::decomp0(const TwoQubitWeylDecomposition& target) { return TwoQubitLocalUnitaryList{ target.k1r() * target.k2r(), target.k1l() * target.k2l(), diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp index a7344a665d..0c153a0a30 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -147,6 +148,8 @@ static bool isTwoQubitRunStart(UnitaryOpInterface op) { return isTwoQubitRunMember(op) && !feedsFromSameTwoQubitWindow(op); } +namespace { + struct FusableTwoQubitRun { llvm::SmallVector ops; Matrix4x4 composed = Matrix4x4::identity(); @@ -158,17 +161,16 @@ struct FusableTwoQubitRun { // Replace when off-menu ops must be lowered, or when resynthesis uses fewer // entanglers than the fused window. -static bool shouldApplyTwoQubitRunReplacement(const FusableTwoQubitRun& run, - std::uint8_t numBasisUses) { +bool shouldApplyTwoQubitRunReplacement(const FusableTwoQubitRun& run, + std::uint8_t numBasisUses) { if (run.anyNonNative) { return true; } return numBasisUses < run.numTwoQ; } -static void -absorbTwoQubitIntoRun(FusableTwoQubitRun& run, UnitaryOpInterface op, - const decomposition::NativeProfileSpec& spec) { +void absorbTwoQubitIntoRun(FusableTwoQubitRun& run, UnitaryOpInterface op, + const decomposition::NativeProfileSpec& spec) { Matrix4x4 opMatrix; if (!decomposition::assignTwoQubitOpMatrix(op.getOperation(), opMatrix)) { return; @@ -195,10 +197,9 @@ absorbTwoQubitIntoRun(FusableTwoQubitRun& run, UnitaryOpInterface op, } } -static void absorbOneQubitIntoRun(FusableTwoQubitRun& run, - UnitaryOpInterface op, - const decomposition::NativeProfileSpec& spec, - unsigned wireIndex) { +void absorbOneQubitIntoRun(FusableTwoQubitRun& run, UnitaryOpInterface op, + const decomposition::NativeProfileSpec& spec, + unsigned wireIndex) { Matrix2x2 raw; if (!op.getUnitaryMatrix2x2(raw)) { return; @@ -216,7 +217,7 @@ static void absorbOneQubitIntoRun(FusableTwoQubitRun& run, } } -static FusableTwoQubitRun +FusableTwoQubitRun scanFusableTwoQubitRun(UnitaryOpInterface head, const decomposition::NativeProfileSpec& spec) { FusableTwoQubitRun run; @@ -239,13 +240,16 @@ scanFusableTwoQubitRun(UnitaryOpInterface head, UnitaryOpInterface nextOnA = uniqueUnitaryUser(run.tailA); UnitaryOpInterface nextOnB = uniqueUnitaryUser(run.tailB); - if (nextOnA && nextOnA == nextOnB && nextOnA.isTwoQubit()) { + if (nextOnA && nextOnB && + nextOnA.getOperation() == nextOnB.getOperation() && + nextOnA.isTwoQubit()) { absorbTwoQubitIntoRun(run, nextOnA, spec); continue; } - if (nextOnA && nextOnB && nextOnA != nextOnB && nextOnA.isSingleQubit() && - nextOnB.isSingleQubit()) { + if (nextOnA && nextOnB && + nextOnA.getOperation() != nextOnB.getOperation() && + nextOnA.isSingleQubit() && nextOnB.isSingleQubit()) { if (nextOnA->isBeforeInBlock(nextOnB)) { absorbOneQubitIntoRun(run, nextOnA, spec, /*wireIndex=*/0); continue; @@ -254,12 +258,14 @@ scanFusableTwoQubitRun(UnitaryOpInterface head, continue; } - if (nextOnA && nextOnA.isSingleQubit() && nextOnA != nextOnB) { + if (nextOnA && nextOnA.isSingleQubit() && + (!nextOnB || nextOnA.getOperation() != nextOnB.getOperation())) { absorbOneQubitIntoRun(run, nextOnA, spec, /*wireIndex=*/0); continue; } - if (nextOnB && nextOnB.isSingleQubit() && nextOnB != nextOnA) { + if (nextOnB && nextOnB.isSingleQubit() && + (!nextOnA || nextOnB.getOperation() != nextOnA.getOperation())) { absorbOneQubitIntoRun(run, nextOnB, spec, /*wireIndex=*/1); continue; } @@ -269,8 +275,8 @@ scanFusableTwoQubitRun(UnitaryOpInterface head, return run; } -static void eraseFusableTwoQubitRun(PatternRewriter& rewriter, - const FusableTwoQubitRun& run) { +void eraseFusableTwoQubitRun(PatternRewriter& rewriter, + const FusableTwoQubitRun& run) { for (Operation* op : llvm::reverse(run.ops)) { rewriter.eraseOp(op); } @@ -279,8 +285,8 @@ static void eraseFusableTwoQubitRun(PatternRewriter& rewriter, struct FuseTwoQubitWindowPattern : public OpInterfaceRewritePattern { FuseTwoQubitWindowPattern(MLIRContext* ctx, - const decomposition::NativeProfileSpec& spec) - : OpInterfaceRewritePattern(ctx), spec(spec) {} + decomposition::NativeProfileSpec specIn) + : OpInterfaceRewritePattern(ctx), spec(std::move(specIn)) {} LogicalResult matchAndRewrite(UnitaryOpInterface op, PatternRewriter& rewriter) const override { @@ -320,10 +326,10 @@ struct FuseTwoQubitWindowPattern return success(); } - const decomposition::NativeProfileSpec& spec; + decomposition::NativeProfileSpec spec; }; -static LogicalResult +LogicalResult fuseTwoQubitUnitaryRuns(Operation* root, const decomposition::NativeProfileSpec& spec) { RewritePatternSet patterns(root->getContext()); @@ -335,9 +341,9 @@ struct OneQubitRun { llvm::SmallVector ops; }; -static bool maybeFuseRun(IRRewriter& rewriter, OneQubitRun& run, - const decomposition::EulerBasis basis, - const decomposition::NativeProfileSpec& spec) { +bool maybeFuseRun(IRRewriter& rewriter, OneQubitRun& run, + const decomposition::EulerBasis basis, + const decomposition::NativeProfileSpec& spec) { Matrix2x2 fused = Matrix2x2::identity(); for (UnitaryOpInterface u : run.ops) { Matrix2x2 m; @@ -369,7 +375,7 @@ static bool maybeFuseRun(IRRewriter& rewriter, OneQubitRun& run, return true; } -static UnitaryOpInterface fusibleSingleQubitOp(Operation* op) { +UnitaryOpInterface fusibleSingleQubitOp(Operation* op) { auto unitary = llvm::dyn_cast(op); if (!unitary || !unitary.isSingleQubit()) { return {}; @@ -677,4 +683,6 @@ struct FuseTwoQubitUnitaryRunsPass } }; +} // namespace + } // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Utils/Matrix.cpp b/mlir/lib/Dialect/QCO/Utils/Matrix.cpp index 1d0898ce99..3588bbdae7 100644 --- a/mlir/lib/Dialect/QCO/Utils/Matrix.cpp +++ b/mlir/lib/Dialect/QCO/Utils/Matrix.cpp @@ -845,20 +845,20 @@ Matrix2x2 rzMatrix(const double theta) { } const Matrix2x2& iPauliZ() { - static const Matrix2x2 matrix = + static const Matrix2x2 MATRIX = Matrix2x2::fromElements(Complex{0.0, 1.0}, 0.0, 0.0, Complex{0.0, -1.0}); - return matrix; + return MATRIX; } const Matrix2x2& iPauliY() { - static const Matrix2x2 matrix = Matrix2x2::fromElements(0.0, 1.0, -1.0, 0.0); - return matrix; + static const Matrix2x2 MATRIX = Matrix2x2::fromElements(0.0, 1.0, -1.0, 0.0); + return MATRIX; } const Matrix2x2& iPauliX() { - static const Matrix2x2 matrix = + static const Matrix2x2 MATRIX = Matrix2x2::fromElements(0.0, Complex{0.0, 1.0}, Complex{0.0, 1.0}, 0.0); - return matrix; + return MATRIX; } Matrix4x4 rxxMatrix(const double theta) { @@ -892,27 +892,27 @@ Matrix4x4 rzzMatrix(const double theta) { } const Matrix4x4& twoQubitControlledX01() { - static const Matrix4x4 matrix = Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, // + static const Matrix4x4 MATRIX = Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, // 0.0, 1.0, 0.0, 0.0, // 0.0, 0.0, 0.0, 1.0, // 0.0, 0.0, 1.0, 0.0); - return matrix; + return MATRIX; } const Matrix4x4& twoQubitControlledX10() { - static const Matrix4x4 matrix = Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, // + static const Matrix4x4 MATRIX = Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, // 0.0, 0.0, 0.0, 1.0, // 0.0, 0.0, 1.0, 0.0, // 0.0, 1.0, 0.0, 0.0); - return matrix; + return MATRIX; } const Matrix4x4& twoQubitControlledZ() { - static const Matrix4x4 matrix = Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, // + static const Matrix4x4 MATRIX = Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, // 0.0, 1.0, 0.0, 0.0, // 0.0, 0.0, 1.0, 0.0, // 0.0, 0.0, 0.0, -1.0); - return matrix; + return MATRIX; } } // namespace mlir::qco diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index f3a1793e9b..3d9613e889 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -28,6 +29,7 @@ #include #include #include +#include #include #include @@ -47,19 +49,17 @@ using namespace mlir::qco; using namespace mlir::qco::decomposition; using namespace mqt::test; -constexpr double kSanityCheckPrecision = 1e-12; - -namespace { - using QubitId = std::size_t; -[[nodiscard]] const Matrix2x2& hGate() { - static const Matrix2x2 matrix = Matrix2x2::fromElements( +static constexpr double K_SANITY_CHECK_PRECISION = 1e-12; + +static const Matrix2x2& hGate() { + static const Matrix2x2 MATRIX = Matrix2x2::fromElements( FRAC1_SQRT2, FRAC1_SQRT2, FRAC1_SQRT2, -FRAC1_SQRT2); - return matrix; + return MATRIX; } -[[nodiscard]] Matrix4x4 randomUnitary4x4(std::mt19937& rng) { +static Matrix4x4 randomUnitary4x4(std::mt19937& rng) { std::normal_distribution normalDist(0.0, 1.0); std::vector>> columns( 4, std::vector>(4)); @@ -96,15 +96,15 @@ using QubitId = std::size_t; return unitary; } -Matrix4x4 restoreWeyl(const TwoQubitWeylDecomposition& decomposition) { +static Matrix4x4 restoreWeyl(const TwoQubitWeylDecomposition& decomposition) { return kron(decomposition.k1l(), decomposition.k1r()) * decomposition.getCanonicalMatrix() * kron(decomposition.k2l(), decomposition.k2r()) * globalPhaseFactor(decomposition.globalPhase()); } -Matrix4x4 restoreBasis(const TwoQubitNativeDecomposition& decomposition, - const Matrix4x4& entangler) { +static Matrix4x4 restoreBasis(const TwoQubitNativeDecomposition& decomposition, + const Matrix4x4& entangler) { const auto& factors = decomposition.singleQubitFactors; const auto layer = [&](std::size_t i) { return kron(factors[(2 * i) + 1], factors[2 * i]); @@ -117,14 +117,14 @@ Matrix4x4 restoreBasis(const TwoQubitNativeDecomposition& decomposition, return matrix * globalPhaseFactor(decomposition.globalPhase); } -auto productMatrixCases() { +static auto productMatrixCases() { return ::testing::Values( []() -> Matrix4x4 { return Matrix4x4::identity(); }, []() -> Matrix4x4 { return kron(rzMatrix(1.0), ryMatrix(3.1)); }, []() -> Matrix4x4 { return kron(Matrix2x2::identity(), rxMatrix(0.1)); }); } -auto entangledMatrixCases() { +static auto entangledMatrixCases() { return ::testing::Values( []() -> Matrix4x4 { return rzzMatrix(2.0); }, []() -> Matrix4x4 { @@ -145,14 +145,13 @@ auto entangledMatrixCases() { }); } -auto cxBasisCases() { +static auto cxBasisCases() { return ::testing::Values( []() -> Matrix4x4 { return twoQubitControlledX01(); }, []() -> Matrix4x4 { return twoQubitControlledX10(); }); } -[[nodiscard]] bool extractSingleQubitMatrix(UnitaryOpInterface op, - Matrix2x2& out) { +static bool extractSingleQubitMatrix(UnitaryOpInterface op, Matrix2x2& out) { if (op.getUnitaryMatrix2x2(out)) { return true; } @@ -166,8 +165,7 @@ auto cxBasisCases() { return true; } -[[nodiscard]] bool extractTwoQubitMatrix(UnitaryOpInterface op, - Matrix4x4& out) { +static bool extractTwoQubitMatrix(UnitaryOpInterface op, Matrix4x4& out) { if (auto ctrl = llvm::dyn_cast(op.getOperation())) { if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { return false; @@ -186,7 +184,7 @@ auto cxBasisCases() { return op.getUnitaryMatrix4x4(out); } -[[nodiscard]] std::optional +static std::optional computeTwoQubitUnitaryFromFunc(func::FuncOp funcOp) { Matrix4x4 unitary = Matrix4x4::identity(); std::complex global{1.0, 0.0}; @@ -268,9 +266,10 @@ struct MlirTestContext { [[nodiscard]] MLIRContext* ctx() const { return context.get(); } }; -func::FuncOp synthesize2QIntoFunc(MLIRContext* ctx, const Matrix4x4& target, - const NativeProfileSpec& spec, - OwningOpRef& moduleOut) { +static func::FuncOp synthesize2QIntoFunc(MLIRContext* ctx, + const Matrix4x4& target, + const NativeProfileSpec& spec, + OwningOpRef& moduleOut) { moduleOut = ModuleOp::create(UnknownLoc::get(ctx)); OpBuilder builder(ctx); builder.setInsertionPointToStart(moduleOut->getBody()); @@ -295,8 +294,8 @@ func::FuncOp synthesize2QIntoFunc(MLIRContext* ctx, const Matrix4x4& target, return func; } -void expectSynthesized2QMatrix(MLIRContext* ctx, const Matrix4x4& target, - const NativeProfileSpec& spec) { +static void expectSynthesized2QMatrix(MLIRContext* ctx, const Matrix4x4& target, + const NativeProfileSpec& spec) { OwningOpRef module; const auto func = synthesize2QIntoFunc(ctx, target, spec, module); ASSERT_TRUE(succeeded(verify(module.get()))); @@ -305,8 +304,6 @@ void expectSynthesized2QMatrix(MLIRContext* ctx, const Matrix4x4& target, EXPECT_TRUE(isEquivalentUpToGlobalPhase(*actual, target)); } -} // namespace - TEST(DecompositionHelpersTest, MatrixUtilitySanity) { EXPECT_DOUBLE_EQ(remEuclid(-1.0, 3.0), 2.0); EXPECT_DOUBLE_EQ(traceToFidelity(std::complex{3.0, 4.0}), @@ -316,9 +313,35 @@ TEST(DecompositionHelpersTest, MatrixUtilitySanity) { EXPECT_TRUE(isUnitaryMatrix(Matrix2x2::identity())); } -// NOLINTNEXTLINE(misc-use-internal-linkage) +namespace { + class WeylDecompositionTest : public testing::TestWithParam {}; +class BasisDecomposerTest : public testing::TestWithParam< + std::tuple> { +protected: + void SetUp() override { + basisMatrix = std::get<0>(GetParam())(); + target = std::get<1>(GetParam())(); + targetDecomposition = std::make_unique( + TwoQubitWeylDecomposition::create(target, 1.0)); + } + + Matrix4x4 target; + Matrix4x4 basisMatrix; + std::unique_ptr targetDecomposition; +}; + +struct WeylSynthesisCase { + const char* name; + const char* nativeGates; + Matrix4x4 (*target)(); +}; + +class WeylSynthesisTest : public testing::TestWithParam {}; + +} // namespace + TEST_P(WeylDecompositionTest, ReconstructsWithinRequestedFidelity) { const Matrix4x4 originalMatrix = GetParam()(); for (const double fidelity : {1.0, 1.0 - 1e-12}) { @@ -351,7 +374,7 @@ TEST(WeylDecompositionStandalone, Random) { const auto decomposition = TwoQubitWeylDecomposition::create( originalMatrix, std::optional{1.0 - 1e-12}); EXPECT_TRUE(restoreWeyl(decomposition) - .isApprox(originalMatrix, kSanityCheckPrecision)); + .isApprox(originalMatrix, K_SANITY_CHECK_PRECISION)); } } @@ -360,22 +383,6 @@ INSTANTIATE_TEST_SUITE_P(ProductTwoQubitMatrices, WeylDecompositionTest, INSTANTIATE_TEST_SUITE_P(TwoQubitMatrices, WeylDecompositionTest, entangledMatrixCases()); -// NOLINTNEXTLINE(misc-use-internal-linkage) -class BasisDecomposerTest : public testing::TestWithParam< - std::tuple> { -protected: - void SetUp() override { - basisMatrix = std::get<0>(GetParam())(); - target = std::get<1>(GetParam())(); - targetDecomposition = std::make_unique( - TwoQubitWeylDecomposition::create(target, 1.0)); - } - - Matrix4x4 target; - Matrix4x4 basisMatrix; - std::unique_ptr targetDecomposition; -}; - TEST_P(BasisDecomposerTest, ReconstructsWithinRequestedFidelity) { for (const double fidelity : {1.0, 1.0 - 1e-12}) { const auto decomposer = @@ -403,7 +410,7 @@ TEST(BasisDecomposerTest, Random) { decomposer.twoQubitDecompose(targetDecomposition, std::nullopt); ASSERT_TRUE(decomposed.has_value()); EXPECT_TRUE(restoreBasis(*decomposed, basisMatrix) - .isApprox(originalMatrix, kSanityCheckPrecision)); + .isApprox(originalMatrix, K_SANITY_CHECK_PRECISION)); } } @@ -425,14 +432,6 @@ INSTANTIATE_TEST_SUITE_P(TwoQubitMatrices, BasisDecomposerTest, testing::Combine(cxBasisCases(), entangledMatrixCases())); -struct WeylSynthesisCase { - const char* name; - const char* nativeGates; - Matrix4x4 (*target)(); -}; - -class WeylSynthesisTest : public testing::TestWithParam {}; - TEST_P(WeylSynthesisTest, PreservesTargetUnitary) { MlirTestContext fx; fx.setUp(); diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp index 32a5f1f7c9..98928fa686 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp @@ -19,7 +19,6 @@ #include #include -#include #include #include #include @@ -31,24 +30,26 @@ #include #include #include +#include #include #include +#include #include #include #include #include #include +#include #include #include #include +#include using namespace mlir; using namespace mlir::qco; using namespace mqt::test; -namespace { - using ProgramFn = void (*)(mlir::qc::QCProgramBuilder&); using NativePredicate = bool (*)(OwningOpRef&); @@ -70,7 +71,8 @@ struct FusionCase { }; template -bool onlyTheseOps(OwningOpRef& moduleOp, bool allowCx, bool allowCz) { +static bool onlyTheseOps(OwningOpRef& moduleOp, bool allowCx, + bool allowCz) { bool ok = true; std::ignore = moduleOp->walk([&](UnitaryOpInterface op) { Operation* raw = op.getOperation(); @@ -103,36 +105,36 @@ bool onlyTheseOps(OwningOpRef& moduleOp, bool allowCx, bool allowCz) { return ok; } -bool onlyIbmBasicCxOps(OwningOpRef& m) { +static bool onlyIbmBasicCxOps(OwningOpRef& m) { return onlyTheseOps(m, true, false); } -bool onlyIbmBasicCzOps(OwningOpRef& m) { +static bool onlyIbmBasicCzOps(OwningOpRef& m) { return onlyTheseOps(m, false, true); } -bool onlyGenericU3CxOps(OwningOpRef& m) { +static bool onlyGenericU3CxOps(OwningOpRef& m) { return onlyTheseOps(m, true, false); } -bool onlyIqmDefaultOps(OwningOpRef& m) { +static bool onlyIqmDefaultOps(OwningOpRef& m) { return onlyTheseOps(m, false, true); } -bool onlyIbmFractionalOps(OwningOpRef& m) { +static bool onlyIbmFractionalOps(OwningOpRef& m) { return onlyTheseOps(m, false, true); } -bool onlyAxisPairRxRzCxOps(OwningOpRef& m) { +static bool onlyAxisPairRxRzCxOps(OwningOpRef& m) { return onlyTheseOps(m, true, false); } -bool onlyAxisPairRxRyCxOps(OwningOpRef& m) { +static bool onlyAxisPairRxRyCxOps(OwningOpRef& m) { return onlyTheseOps(m, true, false); } -bool onlyAxisPairRyRzCzOps(OwningOpRef& m) { +static bool onlyAxisPairRyRzCzOps(OwningOpRef& m) { return onlyTheseOps(m, false, true); } -bool onlyGenericU3CxOrCzOps(OwningOpRef& m) { +static bool onlyGenericU3CxOrCzOps(OwningOpRef& m) { return onlyTheseOps(m, true, true); } -[[nodiscard]] std::optional unitaryQubitOperand(UnitaryOpInterface op, - std::size_t index) { +static std::optional unitaryQubitOperand(UnitaryOpInterface op, + std::size_t index) { if (index >= op.getNumQubits()) { return std::nullopt; } @@ -143,8 +145,8 @@ bool onlyGenericU3CxOrCzOps(OwningOpRef& m) { return v; } -[[nodiscard]] std::optional unitaryQubitResult(UnitaryOpInterface op, - std::size_t index) { +static std::optional unitaryQubitResult(UnitaryOpInterface op, + std::size_t index) { if (index >= op.getNumQubits()) { return std::nullopt; } @@ -155,8 +157,7 @@ bool onlyGenericU3CxOrCzOps(OwningOpRef& m) { return v; } -[[nodiscard]] bool extractSingleQubitMatrix(UnitaryOpInterface op, - Matrix2x2& out) { +static bool extractSingleQubitMatrix(UnitaryOpInterface op, Matrix2x2& out) { if (op.getUnitaryMatrix2x2(out)) { return true; } @@ -170,8 +171,7 @@ bool onlyGenericU3CxOrCzOps(OwningOpRef& m) { return true; } -[[nodiscard]] bool extractTwoQubitMatrix(UnitaryOpInterface op, - Matrix4x4& out) { +static bool extractTwoQubitMatrix(UnitaryOpInterface op, Matrix4x4& out) { if (auto ctrl = llvm::dyn_cast(op.getOperation())) { if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { return false; @@ -190,7 +190,7 @@ bool onlyGenericU3CxOrCzOps(OwningOpRef& m) { return op.getUnitaryMatrix4x4(out); } -[[nodiscard]] std::optional +static std::optional computeUnitaryFromQcoModule(const OwningOpRef& moduleOp) { ModuleOp module = moduleOp.get(); if (!module) { @@ -299,7 +299,8 @@ computeUnitaryFromQcoModule(const OwningOpRef& moduleOp) { return unitary; } -// NOLINTNEXTLINE(misc-use-internal-linkage) +namespace { + class FuseTwoQubitUnitaryRunsPassTest : public testing::Test { protected: void SetUp() override { @@ -311,8 +312,7 @@ class FuseTwoQubitUnitaryRunsPassTest : public testing::Test { context->loadAllAvailableDialects(); } - static void runFusePipeline(OwningOpRef& moduleOp, - StringRef nativeGates) { + void runFusePipeline(OwningOpRef& moduleOp, StringRef nativeGates) { PassManager pm(moduleOp->getContext()); pm.addPass(createQCToQCO()); pm.addPass(createFuseTwoQubitUnitaryRuns(FuseTwoQubitUnitaryRunsOptions{ @@ -321,14 +321,13 @@ class FuseTwoQubitUnitaryRunsPassTest : public testing::Test { ASSERT_TRUE(succeeded(pm.run(*moduleOp))); } - static void runQcToQco(OwningOpRef& moduleOp) { + void runQcToQco(OwningOpRef& moduleOp) { PassManager pm(moduleOp->getContext()); pm.addPass(createQCToQCO()); ASSERT_TRUE(succeeded(pm.run(*moduleOp))); } - static void runTwoQFuse(OwningOpRef& moduleOp, - StringRef nativeGates) { + void runTwoQFuse(OwningOpRef& moduleOp, StringRef nativeGates) { PassManager pm(moduleOp->getContext()); pm.addPass(createFuseTwoQubitUnitaryRuns(FuseTwoQubitUnitaryRunsOptions{ .nativeGates = nativeGates.str(), @@ -336,8 +335,8 @@ class FuseTwoQubitUnitaryRunsPassTest : public testing::Test { ASSERT_TRUE(succeeded(pm.run(*moduleOp))); } - static void expectQcoModulesEquivalent(const OwningOpRef& lhs, - const OwningOpRef& rhs) { + void expectQcoModulesEquivalent(const OwningOpRef& lhs, + const OwningOpRef& rhs) { const auto lhsUnitary = computeUnitaryFromQcoModule(lhs); ASSERT_TRUE(lhsUnitary.has_value()); const auto rhsUnitary = computeUnitaryFromQcoModule(rhs); @@ -387,7 +386,7 @@ class FuseTwoQubitUnitaryRunsPassTest : public testing::Test { expectQcoModulesEquivalent(expected, fused); } - static std::size_t countCtrlOps(const OwningOpRef& moduleOp) { + std::size_t countCtrlOps(const OwningOpRef& moduleOp) { std::size_t count = 0; moduleOp.get()->walk([&](CtrlOp) { ++count; }); return count; @@ -404,6 +403,8 @@ class FuseTwoQubitFusionTest : public FuseTwoQubitUnitaryRunsPassTest, public testing::WithParamInterface { }; +} // namespace + static void fusionCxCx(mlir::qc::QCProgramBuilder& b) { const auto q0 = b.allocQubit(); const auto q1 = b.allocQubit(); @@ -590,8 +591,6 @@ static void determinismSwap(mlir::qc::QCProgramBuilder& b) { b.dealloc(q1); } -} // namespace - TEST_P(FuseTwoQubitProfileTest, SynthesizesToNativeMenu) { const ProfileCase& c = GetParam(); if (c.checkEquivalence) { From 7e23b015c1b2762df4b629b18605d66645091577 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Sun, 21 Jun 2026 14:07:52 +0200 Subject: [PATCH 054/122] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20linter=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/Decomposition/Weyl.cpp | 4 + .../FuseTwoQubitUnitaryRuns.cpp | 142 ++++++++++-------- .../Decomposition/test_weyl_decomposition.cpp | 28 ++-- .../test_fuse_two_qubit_unitary_runs.cpp | 48 +++--- 4 files changed, 119 insertions(+), 103 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp index 86dd7dd173..3c23f466e3 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp @@ -41,6 +41,8 @@ namespace mlir::qco::decomposition { using namespace std::complex_literals; +namespace { + enum class Specialization : std::uint8_t { General, IdEquiv, @@ -59,6 +61,8 @@ enum class MagicBasisTransform : std::uint8_t { OutOf, }; +} // namespace + static constexpr auto DIAGONALIZATION_PRECISION = 1e-13; static Matrix4x4 magicBasisTransform(const Matrix4x4& unitary, diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp index 0c153a0a30..41fe85847c 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp @@ -159,18 +159,25 @@ struct FusableTwoQubitRun { Value tailB; }; +struct OneQubitRun { + llvm::SmallVector ops; +}; + +} // namespace + // Replace when off-menu ops must be lowered, or when resynthesis uses fewer // entanglers than the fused window. -bool shouldApplyTwoQubitRunReplacement(const FusableTwoQubitRun& run, - std::uint8_t numBasisUses) { +static bool shouldApplyTwoQubitRunReplacement(const FusableTwoQubitRun& run, + std::uint8_t numBasisUses) { if (run.anyNonNative) { return true; } return numBasisUses < run.numTwoQ; } -void absorbTwoQubitIntoRun(FusableTwoQubitRun& run, UnitaryOpInterface op, - const decomposition::NativeProfileSpec& spec) { +static void +absorbTwoQubitIntoRun(FusableTwoQubitRun& run, UnitaryOpInterface op, + const decomposition::NativeProfileSpec& spec) { Matrix4x4 opMatrix; if (!decomposition::assignTwoQubitOpMatrix(op.getOperation(), opMatrix)) { return; @@ -197,9 +204,10 @@ void absorbTwoQubitIntoRun(FusableTwoQubitRun& run, UnitaryOpInterface op, } } -void absorbOneQubitIntoRun(FusableTwoQubitRun& run, UnitaryOpInterface op, - const decomposition::NativeProfileSpec& spec, - unsigned wireIndex) { +static void absorbOneQubitIntoRun(FusableTwoQubitRun& run, + UnitaryOpInterface op, + const decomposition::NativeProfileSpec& spec, + unsigned wireIndex) { Matrix2x2 raw; if (!op.getUnitaryMatrix2x2(raw)) { return; @@ -217,7 +225,7 @@ void absorbOneQubitIntoRun(FusableTwoQubitRun& run, UnitaryOpInterface op, } } -FusableTwoQubitRun +static FusableTwoQubitRun scanFusableTwoQubitRun(UnitaryOpInterface head, const decomposition::NativeProfileSpec& spec) { FusableTwoQubitRun run; @@ -275,13 +283,67 @@ scanFusableTwoQubitRun(UnitaryOpInterface head, return run; } -void eraseFusableTwoQubitRun(PatternRewriter& rewriter, - const FusableTwoQubitRun& run) { +static void eraseFusableTwoQubitRun(PatternRewriter& rewriter, + const FusableTwoQubitRun& run) { for (Operation* op : llvm::reverse(run.ops)) { rewriter.eraseOp(op); } } +static bool maybeFuseRun(IRRewriter& rewriter, OneQubitRun& run, + const decomposition::EulerBasis basis, + const decomposition::NativeProfileSpec& spec) { + Matrix2x2 fused = Matrix2x2::identity(); + for (UnitaryOpInterface u : run.ops) { + Matrix2x2 m; + if (!u.getUnitaryMatrix2x2(m)) { + return false; + } + fused.premultiplyBy(m); + } + + const bool anyNonNative = llvm::any_of(run.ops, [&](UnitaryOpInterface u) { + return !decomposition::allowsSingleQubitOp(u, spec); + }); + + Operation* firstOp = run.ops.front().getOperation(); + const Value inQubit = run.ops.front().getInputQubit(0); + const Value outQubit = run.ops.back().getOutputQubit(0); + + rewriter.setInsertionPoint(firstOp); + const auto replacement = decomposition::synthesizeUnitary1QEuler( + rewriter, firstOp->getLoc(), inQubit, fused, run.ops.size(), anyNonNative, + basis); + if (!replacement) { + return false; + } + rewriter.replaceAllUsesWith(outQubit, *replacement); + for (auto& op : llvm::reverse(run.ops)) { + rewriter.eraseOp(op.getOperation()); + } + return true; +} + +static UnitaryOpInterface fusibleSingleQubitOp(Operation* op) { + auto unitary = llvm::dyn_cast(op); + if (!unitary || !unitary.isSingleQubit()) { + return {}; + } + if (llvm::isa(op)) { + return {}; + } + if (isExcludedFromTopLevelUnitaryWalk(op)) { + return {}; + } + Matrix2x2 matrix; + if (!unitary.getUnitaryMatrix2x2(matrix)) { + return {}; + } + return unitary; +} + +namespace { + struct FuseTwoQubitWindowPattern : public OpInterfaceRewritePattern { FuseTwoQubitWindowPattern(MLIRContext* ctx, @@ -329,7 +391,9 @@ struct FuseTwoQubitWindowPattern decomposition::NativeProfileSpec spec; }; -LogicalResult +} // namespace + +static LogicalResult fuseTwoQubitUnitaryRuns(Operation* root, const decomposition::NativeProfileSpec& spec) { RewritePatternSet patterns(root->getContext()); @@ -337,61 +401,7 @@ fuseTwoQubitUnitaryRuns(Operation* root, return applyPatternsGreedily(root, std::move(patterns)); } -struct OneQubitRun { - llvm::SmallVector ops; -}; - -bool maybeFuseRun(IRRewriter& rewriter, OneQubitRun& run, - const decomposition::EulerBasis basis, - const decomposition::NativeProfileSpec& spec) { - Matrix2x2 fused = Matrix2x2::identity(); - for (UnitaryOpInterface u : run.ops) { - Matrix2x2 m; - if (!u.getUnitaryMatrix2x2(m)) { - return false; - } - fused.premultiplyBy(m); - } - - const bool anyNonNative = llvm::any_of(run.ops, [&](UnitaryOpInterface u) { - return !decomposition::allowsSingleQubitOp(u, spec); - }); - - Operation* firstOp = run.ops.front().getOperation(); - const Value inQubit = run.ops.front().getInputQubit(0); - const Value outQubit = run.ops.back().getOutputQubit(0); - - rewriter.setInsertionPoint(firstOp); - const auto replacement = decomposition::synthesizeUnitary1QEuler( - rewriter, firstOp->getLoc(), inQubit, fused, run.ops.size(), anyNonNative, - basis); - if (!replacement) { - return false; - } - rewriter.replaceAllUsesWith(outQubit, *replacement); - for (auto& op : llvm::reverse(run.ops)) { - rewriter.eraseOp(op.getOperation()); - } - return true; -} - -UnitaryOpInterface fusibleSingleQubitOp(Operation* op) { - auto unitary = llvm::dyn_cast(op); - if (!unitary || !unitary.isSingleQubit()) { - return {}; - } - if (llvm::isa(op)) { - return {}; - } - if (isExcludedFromTopLevelUnitaryWalk(op)) { - return {}; - } - Matrix2x2 matrix; - if (!unitary.getUnitaryMatrix2x2(matrix)) { - return {}; - } - return unitary; -} +namespace { struct FuseTwoQubitUnitaryRunsPass : impl::FuseTwoQubitUnitaryRunsBase { diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index 3d9613e889..41e82a2726 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -252,20 +252,6 @@ computeTwoQubitUnitaryFromFunc(func::FuncOp funcOp) { return unitary * global; } -struct MlirTestContext { - std::unique_ptr context; - - void setUp() { - DialectRegistry registry; - registry.insert(); - context = std::make_unique(); - context->appendDialectRegistry(registry); - context->loadAllAvailableDialects(); - } - - [[nodiscard]] MLIRContext* ctx() const { return context.get(); } -}; - static func::FuncOp synthesize2QIntoFunc(MLIRContext* ctx, const Matrix4x4& target, const NativeProfileSpec& spec, @@ -315,6 +301,20 @@ TEST(DecompositionHelpersTest, MatrixUtilitySanity) { namespace { +struct MlirTestContext { + std::unique_ptr context; + + void setUp() { + DialectRegistry registry; + registry.insert(); + context = std::make_unique(); + context->appendDialectRegistry(registry); + context->loadAllAvailableDialects(); + } + + [[nodiscard]] MLIRContext* ctx() const { return context.get(); } +}; + class WeylDecompositionTest : public testing::TestWithParam {}; class BasisDecomposerTest : public testing::TestWithParam< diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp index 98928fa686..ddaa13e035 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp @@ -53,23 +53,6 @@ using namespace mqt::test; using ProgramFn = void (*)(mlir::qc::QCProgramBuilder&); using NativePredicate = bool (*)(OwningOpRef&); -struct ProfileCase { - const char* name; - ProgramFn program; - const char* nativeGates; - NativePredicate isNative; - bool checkEquivalence; -}; - -struct FusionCase { - const char* name; - ProgramFn program; - const char* nativeGates; - std::optional exactCtrlCount; - std::optional minCtrlCount; - bool checkTwoQUnitary; -}; - template static bool onlyTheseOps(OwningOpRef& moduleOp, bool allowCx, bool allowCz) { @@ -301,6 +284,23 @@ computeUnitaryFromQcoModule(const OwningOpRef& moduleOp) { namespace { +struct ProfileCase { + const char* name; + ProgramFn program; + const char* nativeGates; + NativePredicate isNative; + bool checkEquivalence; +}; + +struct FusionCase { + const char* name; + ProgramFn program; + const char* nativeGates; + std::optional exactCtrlCount; + std::optional minCtrlCount; + bool checkTwoQUnitary; +}; + class FuseTwoQubitUnitaryRunsPassTest : public testing::Test { protected: void SetUp() override { @@ -312,7 +312,8 @@ class FuseTwoQubitUnitaryRunsPassTest : public testing::Test { context->loadAllAvailableDialects(); } - void runFusePipeline(OwningOpRef& moduleOp, StringRef nativeGates) { + static void runFusePipeline(OwningOpRef& moduleOp, + StringRef nativeGates) { PassManager pm(moduleOp->getContext()); pm.addPass(createQCToQCO()); pm.addPass(createFuseTwoQubitUnitaryRuns(FuseTwoQubitUnitaryRunsOptions{ @@ -321,13 +322,14 @@ class FuseTwoQubitUnitaryRunsPassTest : public testing::Test { ASSERT_TRUE(succeeded(pm.run(*moduleOp))); } - void runQcToQco(OwningOpRef& moduleOp) { + static void runQcToQco(OwningOpRef& moduleOp) { PassManager pm(moduleOp->getContext()); pm.addPass(createQCToQCO()); ASSERT_TRUE(succeeded(pm.run(*moduleOp))); } - void runTwoQFuse(OwningOpRef& moduleOp, StringRef nativeGates) { + static void runTwoQFuse(OwningOpRef& moduleOp, + StringRef nativeGates) { PassManager pm(moduleOp->getContext()); pm.addPass(createFuseTwoQubitUnitaryRuns(FuseTwoQubitUnitaryRunsOptions{ .nativeGates = nativeGates.str(), @@ -335,8 +337,8 @@ class FuseTwoQubitUnitaryRunsPassTest : public testing::Test { ASSERT_TRUE(succeeded(pm.run(*moduleOp))); } - void expectQcoModulesEquivalent(const OwningOpRef& lhs, - const OwningOpRef& rhs) { + static void expectQcoModulesEquivalent(const OwningOpRef& lhs, + const OwningOpRef& rhs) { const auto lhsUnitary = computeUnitaryFromQcoModule(lhs); ASSERT_TRUE(lhsUnitary.has_value()); const auto rhsUnitary = computeUnitaryFromQcoModule(rhs); @@ -386,7 +388,7 @@ class FuseTwoQubitUnitaryRunsPassTest : public testing::Test { expectQcoModulesEquivalent(expected, fused); } - std::size_t countCtrlOps(const OwningOpRef& moduleOp) { + static std::size_t countCtrlOps(const OwningOpRef& moduleOp) { std::size_t count = 0; moduleOp.get()->walk([&](CtrlOp) { ++count; }); return count; From 239ed3452bc85a0102611bc58c416809513768c5 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 22 Jun 2026 10:50:40 +0200 Subject: [PATCH 055/122] =?UTF-8?q?=E2=9C=A8=20Enhance=20Matrix=20function?= =?UTF-8?q?ality=20with=20new=20methods=20and=20optimizations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/include/mlir/Dialect/QCO/Utils/Matrix.h | 196 ++++++ mlir/lib/Dialect/QCO/Utils/Matrix.cpp | 605 +++++++++++++++++- .../Dialect/QCO/Utils/test_unitary_matrix.cpp | 277 +++++++- 3 files changed, 1046 insertions(+), 32 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h index 8f49d372d7..402e86ac4a 100644 --- a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h +++ b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h @@ -183,6 +183,12 @@ struct Matrix2x2 { */ [[nodiscard]] Matrix2x2 adjoint() const; + /** + * @brief Returns the (non-conjugate) transpose of this matrix. + * @return Transposed matrix `A^T`. + */ + [[nodiscard]] Matrix2x2 transpose() const; + /** * @brief Returns the trace of this matrix. * @return Sum of diagonal entries. @@ -195,6 +201,13 @@ struct Matrix2x2 { */ [[nodiscard]] Complex determinant() const; + /** + * @brief Checks whether this matrix is approximately the identity. + * @param tol Maximum allowed complex modulus of each entry difference. + * @return True if every entry is within @p tol of the identity. + */ + [[nodiscard]] bool isIdentity(double tol = MATRIX_TOLERANCE) const; + /** * @brief Checks approximate equality using an absolute entry-wise tolerance. * @@ -322,6 +335,12 @@ struct Matrix4x4 { */ [[nodiscard]] Matrix4x4 adjoint() const; + /** + * @brief Returns the (non-conjugate) transpose of this matrix. + * @return Transposed matrix `A^T`. + */ + [[nodiscard]] Matrix4x4 transpose() const; + /** * @brief Returns the trace of this matrix. * @return Sum of diagonal entries. @@ -334,6 +353,53 @@ struct Matrix4x4 { */ [[nodiscard]] Complex determinant() const; + /** + * @brief Checks whether this matrix is approximately the identity. + * @param tol Maximum allowed complex modulus of each entry difference. + * @return True if every entry is within @p tol of the identity. + */ + [[nodiscard]] bool isIdentity(double tol = MATRIX_TOLERANCE) const; + + /** + * @brief Returns the four diagonal entries `(m00, m11, m22, m33)`. + * @return Array of diagonal entries. + */ + [[nodiscard]] std::array diagonal() const; + + /** + * @brief Builds a diagonal matrix from four diagonal entries. + * @param diagonalEntries Diagonal entries `(m00, m11, m22, m33)`. + * @return Diagonal matrix with the given entries. + */ + [[nodiscard]] static Matrix4x4 + fromDiagonal(const std::array& diagonalEntries); + + /** + * @brief Returns the entries of column @p col, top to bottom. + * @param col Column index in `[0, K_COLS)`. + * @return Array of the four column entries. + */ + [[nodiscard]] std::array column(std::size_t col) const; + + /** + * @brief Overwrites column @p col with @p values. + * @param col Column index in `[0, K_COLS)`. + * @param values New column entries, top to bottom. + */ + void setColumn(std::size_t col, const std::array& values); + + /** + * @brief Returns the element-wise real parts in row-major order. + * @return Real parts of all entries. + */ + [[nodiscard]] std::array realPart() const; + + /** + * @brief Returns the element-wise imaginary parts in row-major order. + * @return Imaginary parts of all entries. + */ + [[nodiscard]] std::array imagPart() const; + /** * @brief Checks approximate equality using an absolute entry-wise tolerance. * @@ -539,6 +605,41 @@ class DynamicMatrix { [[nodiscard]] bool isApprox(const DynamicMatrix& other, double tol = MATRIX_TOLERANCE) const; + /** + * @brief Returns the trace of this matrix. + * @return Sum of diagonal entries. + */ + [[nodiscard]] Complex trace() const; + + /** + * @brief Matrix product `*this * rhs`. + * @param rhs Right-hand factor. + * @return Product of the two matrices. + */ + [[nodiscard]] DynamicMatrix operator*(const DynamicMatrix& rhs) const; + + /** + * @brief Element-wise scaling by a complex scalar. + * @param scalar Factor applied to every matrix entry. + * @return Scaled copy of this matrix. + */ + [[nodiscard]] DynamicMatrix operator*(const Complex& scalar) const; + + /** + * @brief Element-wise in-place scaling by a complex scalar. + * @param scalar Factor applied to every matrix entry. + * @return Reference to this matrix. + */ + DynamicMatrix& operator*=(const Complex& scalar); + + /** + * @brief Checks whether this matrix is approximately the identity. + * @param tol Maximum allowed complex modulus of each off-diagonal entry and + * each diagonal deviation from one. + * @return True when the matrix is close to the identity. + */ + [[nodiscard]] bool isIdentity(double tol = MATRIX_TOLERANCE) const; + private: struct Impl; std::unique_ptr impl_; @@ -558,4 +659,99 @@ inline constexpr bool std::disjunction_v, std::is_same, std::is_same, std::is_same>; + +/** + * @brief Kronecker product `lhs (x) rhs` of two single-qubit matrices. + * + * Uses the computational-basis bit order where the first operand labels the + * high bit, matching `UnitaryOpInterface::getUnitaryMatrix4x4`. + * + * @param lhs Left factor (acts on the high bit / qubit 0). + * @param rhs Right factor (acts on the low bit / qubit 1). + * @return The `4x4` Kronecker product. + */ +[[nodiscard]] Matrix4x4 kron(const Matrix2x2& lhs, const Matrix2x2& rhs); + +/// Scalar-on-the-left multiply `scalar * matrix` (commutes with the member +/// `matrix * scalar`). Provided so generic code can scale a matrix from +/// either side. +[[nodiscard]] Matrix2x2 operator*(const Complex& scalar, + const Matrix2x2& matrix); +/// @copydoc operator*(const Complex&, const Matrix2x2&) +[[nodiscard]] Matrix4x4 operator*(const Complex& scalar, + const Matrix4x4& matrix); +/// @copydoc operator*(const Complex&, const Matrix2x2&) +[[nodiscard]] DynamicMatrix operator*(const Complex& scalar, + const DynamicMatrix& matrix); + +/** + * @brief Eigenvalues and eigenvectors of a real symmetric `4x4` matrix. + * + * `eigenvalues` are sorted ascending and `eigenvectors` holds the + * corresponding orthonormal eigenvectors as columns (column `j` is the + * eigenvector for `eigenvalues[j]`). + */ +struct SymmetricEigen4 { + std::array eigenvalues{}; + Matrix4x4 eigenvectors{}; +}; + +/** + * @brief Computes the eigendecomposition of a real symmetric `4x4` matrix. + * + * Uses Householder tridiagonalization (EISPACK `tred2`) followed by implicit + * QL iteration (`tql2`) on the tridiagonal form. + * + * @pre @p symmetric is real and symmetric: `symmetric[i,j] == symmetric[j,i]` + * for all `i, j`. Only the lower triangle (including the diagonal) is read, + * but supplying a non-symmetric matrix yields undefined numerical results. + * + * @param symmetric Row-major real symmetric `4x4` matrix. + * @return Ascending eigenvalues and matching eigenvectors (as columns). + */ +[[nodiscard]] SymmetricEigen4 +symmetricEigen4(const std::array& symmetric); + +/** + * @brief Computes the eigendecomposition of a real symmetric `4x4` matrix. + * + * @copydoc symmetricEigen4(const std::array&) + * + * @pre Entries of @p symmetric are real (imaginary parts must be negligible). + * The real parts must form a symmetric matrix; imaginary parts are ignored. + */ +[[nodiscard]] SymmetricEigen4 symmetricEigen4(const Matrix4x4& symmetric); + +/** + * @brief Reorder a two-qubit matrix to act on qubits `{0, 1}`. + * + * @param q0Index Wire index of operand 0; @p q1Index wire index of operand 1. + */ +[[nodiscard]] Matrix4x4 reorderTwoQubitMatrix(const Matrix4x4& matrix, + std::size_t q0Index, + std::size_t q1Index); + +/** + * @brief Embed a single-qubit matrix into an @p numQubits-qubit Hilbert space. + * + * Qubit @p qubitIndex uses the same MSB-first convention as @ref kron + * (high bit first operand, low bit second). For each basis pair whose untouched + * wires match, copies @p matrix at the target qubit's row/column bits. + */ +[[nodiscard]] DynamicMatrix embedSingleQubitInNqubit(const Matrix2x2& matrix, + std::size_t numQubits, + std::size_t qubitIndex); + +/** + * @brief Embed a two-qubit matrix into an @p numQubits-qubit Hilbert space. + * + * Operand 0 labels the high bit of the pair and acts on @p q0Index; operand 1 + * labels the low bit and acts on @p q1Index. For each basis pair whose other + * wires match, copies @p matrix at the packed two-qubit row/column indices. + */ +[[nodiscard]] DynamicMatrix embedTwoQubitInNqubit(const Matrix4x4& matrix, + std::size_t numQubits, + std::size_t q0Index, + std::size_t q1Index); + } // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Utils/Matrix.cpp b/mlir/lib/Dialect/QCO/Utils/Matrix.cpp index 8df45840a3..7a53347c04 100644 --- a/mlir/lib/Dialect/QCO/Utils/Matrix.cpp +++ b/mlir/lib/Dialect/QCO/Utils/Matrix.cpp @@ -11,6 +11,7 @@ #include "mlir/Dialect/QCO/Utils/Matrix.h" #include +#include #include #include @@ -81,21 +82,32 @@ assignFromDynamicImpl(const DynamicMatrix& src, } /// Writes the row-major product `lhs * rhs` into @p out (2x2, fully unrolled). -static void -multiply2x2(const std::array& lhs, - const std::array& rhs, - std::array& out) { +static void multiply2x2(const ArrayRef lhs, + const ArrayRef rhs, + const MutableArrayRef out) { + assert(lhs.size() == Matrix2x2::K_SIZE_AT_COMPILE_TIME && + rhs.size() == Matrix2x2::K_SIZE_AT_COMPILE_TIME && + out.size() == Matrix2x2::K_SIZE_AT_COMPILE_TIME); out[0] = lhs[0] * rhs[0] + lhs[1] * rhs[2]; out[1] = lhs[0] * rhs[1] + lhs[1] * rhs[3]; out[2] = lhs[2] * rhs[0] + lhs[3] * rhs[2]; out[3] = lhs[2] * rhs[1] + lhs[3] * rhs[3]; } -/// Writes the row-major product `lhs * rhs` into @p out (4x4, unrolled rows). static void -multiply4x4(const std::array& lhs, - const std::array& rhs, - std::array& out) { +multiply2x2(const std::array& lhs, + const std::array& rhs, + std::array& out) { + multiply2x2(ArrayRef(lhs), ArrayRef(rhs), MutableArrayRef(out)); +} + +/// Writes the row-major product `lhs * rhs` into @p out (4x4, unrolled rows). +static void multiply4x4(const ArrayRef lhs, + const ArrayRef rhs, + const MutableArrayRef out) { + assert(lhs.size() == Matrix4x4::K_SIZE_AT_COMPILE_TIME && + rhs.size() == Matrix4x4::K_SIZE_AT_COMPILE_TIME && + out.size() == Matrix4x4::K_SIZE_AT_COMPILE_TIME); for (std::size_t row = 0; row < Matrix4x4::K_ROWS; ++row) { const std::size_t rowBase = row * Matrix4x4::K_COLS; const Complex& a0 = lhs[rowBase + 0]; @@ -109,6 +121,34 @@ multiply4x4(const std::array& lhs, } } +static void +multiply4x4(const std::array& lhs, + const std::array& rhs, + std::array& out) { + multiply4x4(ArrayRef(lhs), ArrayRef(rhs), MutableArrayRef(out)); +} + +/// Returns true if @p data is approximately the @p dim x @p dim identity +/// matrix. +template +[[nodiscard]] static bool +isIdentityEntries(const std::array& data, + const double tol) { + for (std::size_t row = 0; row < Dim; ++row) { + for (std::size_t col = 0; col < Dim; ++col) { + const Complex& entry = data[(row * Dim) + col]; + if (row == col) { + if (std::abs(entry - Complex{1.0, 0.0}) > tol) { + return false; + } + } else if (std::abs(entry) > tol) { + return false; + } + } + } + return true; +} + /// Returns @p dim as `size_t` after asserting it is non-negative and squarable. [[nodiscard]] static std::size_t checkedDim(const std::int64_t dim) { assert(dim >= 0 && "DynamicMatrix dimension must be non-negative"); @@ -231,12 +271,20 @@ Matrix2x2 Matrix2x2::adjoint() const { std::conj(data[1]), std::conj(data[3])); } +Matrix2x2 Matrix2x2::transpose() const { + return fromElements(data[0], data[2], data[1], data[3]); +} + Complex Matrix2x2::trace() const { return data[0] + data[3]; } Complex Matrix2x2::determinant() const { return data[0] * data[3] - data[1] * data[2]; } +bool Matrix2x2::isIdentity(const double tol) const { + return isIdentityEntries(data, tol); +} + bool Matrix2x2::isApprox(const Matrix2x2& other, const double tol) const { return entriesAreApprox(data, other.data, tol); } @@ -296,6 +344,12 @@ Matrix4x4 Matrix4x4::adjoint() const { return out; } +Matrix4x4 Matrix4x4::transpose() const { + return fromElements(data[0], data[4], data[8], data[12], data[1], data[5], + data[9], data[13], data[2], data[6], data[10], data[14], + data[3], data[7], data[11], data[15]); +} + Complex Matrix4x4::trace() const { return data[0] + data[5] + data[10] + data[15]; } @@ -317,6 +371,52 @@ Complex Matrix4x4::determinant() const { data[12], data[13], data[14]); } +bool Matrix4x4::isIdentity(const double tol) const { + return isIdentityEntries(data, tol); +} + +std::array Matrix4x4::diagonal() const { + return {data[0], data[5], data[10], data[15]}; +} + +Matrix4x4 +Matrix4x4::fromDiagonal(const std::array& diagonalEntries) { + Matrix4x4 out{}; + for (std::size_t i = 0; i < K_ROWS; ++i) { + out.data[(i * K_COLS) + i] = diagonalEntries[i]; + } + return out; +} + +std::array +Matrix4x4::column(const std::size_t col) const { + return {data[col], data[K_COLS + col], data[(2 * K_COLS) + col], + data[(3 * K_COLS) + col]}; +} + +void Matrix4x4::setColumn(const std::size_t col, + const std::array& values) { + for (std::size_t row = 0; row < K_ROWS; ++row) { + data[(row * K_COLS) + col] = values[row]; + } +} + +std::array +Matrix4x4::realPart() const { + return {data[0].real(), data[1].real(), data[2].real(), data[3].real(), + data[4].real(), data[5].real(), data[6].real(), data[7].real(), + data[8].real(), data[9].real(), data[10].real(), data[11].real(), + data[12].real(), data[13].real(), data[14].real(), data[15].real()}; +} + +std::array +Matrix4x4::imagPart() const { + return {data[0].imag(), data[1].imag(), data[2].imag(), data[3].imag(), + data[4].imag(), data[5].imag(), data[6].imag(), data[7].imag(), + data[8].imag(), data[9].imag(), data[10].imag(), data[11].imag(), + data[12].imag(), data[13].imag(), data[14].imag(), data[15].imag()}; +} + bool Matrix4x4::isApprox(const Matrix4x4& other, const double tol) const { return entriesAreApprox(data, other.data, tol); } @@ -453,4 +553,493 @@ bool DynamicMatrix::isApprox(const DynamicMatrix& other, return entriesAreApprox(impl_->data, other.impl_->data, tol); } +Complex DynamicMatrix::trace() const { + Complex sum{0.0, 0.0}; + const auto udim = checkedDim(impl_->dim); + for (std::size_t i = 0; i < udim; ++i) { + sum += impl_->data[(i * udim) + i]; + } + return sum; +} + +DynamicMatrix DynamicMatrix::operator*(const DynamicMatrix& rhs) const { + if (impl_->dim != rhs.impl_->dim) { + llvm::reportFatalInternalError( + "DynamicMatrix multiply requires matching dimensions"); + } + DynamicMatrix out(impl_->dim); + if (impl_->dim == static_cast(Matrix2x2::K_ROWS)) { + multiply2x2(impl_->data, rhs.impl_->data, out.impl_->data); + return out; + } + if (impl_->dim == static_cast(Matrix4x4::K_ROWS)) { + multiply4x4(impl_->data, rhs.impl_->data, out.impl_->data); + return out; + } + + const auto udim = checkedDim(impl_->dim); + for (std::size_t row = 0; row < udim; ++row) { + for (std::size_t col = 0; col < udim; ++col) { + Complex sum{0.0, 0.0}; + for (std::size_t k = 0; k < udim; ++k) { + sum += + impl_->data[(row * udim) + k] * rhs.impl_->data[(k * udim) + col]; + } + out.impl_->data[(row * udim) + col] = sum; + } + } + return out; +} + +DynamicMatrix DynamicMatrix::operator*(const Complex& scalar) const { + DynamicMatrix out(impl_->dim); + for (std::size_t i = 0; i < impl_->data.size(); ++i) { + out.impl_->data[i] = impl_->data[i] * scalar; + } + return out; +} + +DynamicMatrix& DynamicMatrix::operator*=(const Complex& scalar) { + for (Complex& entry : impl_->data) { + entry *= scalar; + } + return *this; +} + +bool DynamicMatrix::isIdentity(const double tol) const { + const auto udim = checkedDim(impl_->dim); + for (std::size_t row = 0; row < udim; ++row) { + for (std::size_t col = 0; col < udim; ++col) { + const Complex& entry = impl_->data[(row * udim) + col]; + if (row == col) { + if (std::abs(entry - Complex{1.0, 0.0}) > tol) { + return false; + } + } else if (std::abs(entry) > tol) { + return false; + } + } + } + return true; +} + +/** + * @brief Returns the @p qubitIndex bit of a computational-basis label. + * + * Qubit 0 is the MSB of @p stateIndex, matching @ref kron and + * @ref embedSingleQubitInNqubit. + */ +[[nodiscard]] static std::size_t qubitBitAt(const std::size_t stateIndex, + const std::size_t numQubits, + const std::size_t qubitIndex) { + return (stateIndex >> (numQubits - 1 - qubitIndex)) & 1U; +} + +/** + * @brief True when row and col agree on every wire except @p skipA and @p + * skipB. + * + * Used when embedding a gate: untouched qubits must match or the matrix entry + * is zero. For a single-qubit embed, pass @p skipB = @p numQubits so only @p + * skipA is skipped. + */ +[[nodiscard]] static bool otherQubitBitsMatch(const std::size_t row, + const std::size_t col, + const std::size_t numQubits, + const std::size_t skipA, + const std::size_t skipB) { + for (std::size_t q = 0; q < numQubits; ++q) { + if (q == skipA || q == skipB) { + continue; + } + if (qubitBitAt(row, numQubits, q) != qubitBitAt(col, numQubits, q)) { + return false; + } + } + return true; +} + +Matrix4x4 kron(const Matrix2x2& lhs, const Matrix2x2& rhs) { + const auto& a = lhs.data; + const auto& b = rhs.data; + return Matrix4x4::fromElements( + a[0] * b[0], a[0] * b[1], a[1] * b[0], a[1] * b[1], a[0] * b[2], + a[0] * b[3], a[1] * b[2], a[1] * b[3], a[2] * b[0], a[2] * b[1], + a[3] * b[0], a[3] * b[1], a[2] * b[2], a[2] * b[3], a[3] * b[2], + a[3] * b[3]); +} + +Matrix2x2 operator*(const Complex& scalar, const Matrix2x2& matrix) { + return matrix * scalar; +} + +Matrix4x4 operator*(const Complex& scalar, const Matrix4x4& matrix) { + return matrix * scalar; +} + +DynamicMatrix operator*(const Complex& scalar, const DynamicMatrix& matrix) { + return matrix * scalar; +} + +[[nodiscard]] static double pythag2(const double a, const double b) { + const double absA = std::abs(a); + const double absB = std::abs(b); + if (absA > absB) { + const double ratio = absB / absA; + return absA * std::sqrt(1.0 + (ratio * ratio)); + } + if (absB == 0.0) { + return 0.0; + } + const double ratio = absA / absB; + return absB * std::sqrt(1.0 + (ratio * ratio)); +} + +[[nodiscard]] static double copySign(const double magnitude, + const double signSource) { + return (signSource >= 0.0) ? std::abs(magnitude) : -std::abs(magnitude); +} + +// Adapted from John Burkardt's MIT-licensed EISPACK C port (`tred2` / `tql2`): +// https://people.sc.fsu.edu/~jburkardt/c_src/eispack/eispack.c +// Specialized to `n = 4`; input is row-major, accumulator `z` is column-major. + +/// EISPACK `tred2` for `n = 4` (column-major `z[row + col*n]`). +static void symmetricTred24(const std::array& input, + std::array& z, + std::array& diag, + std::array& subdiag) { + constexpr std::size_t n = 4; + const auto zAt = [&z](const std::size_t row, + const std::size_t col) -> double& { + return z[row + (col * n)]; + }; + double h = 0.0; + + for (std::size_t col = 0; col < n; ++col) { + for (std::size_t row = col; row < n; ++row) { + zAt(row, col) = input[(row * n) + col]; + } + diag[col] = input[((n - 1) * n) + col]; + } + + for (int i = static_cast(n) - 1; i >= 1; --i) { + const auto ui = static_cast(i); + const std::size_t l = ui - 1; + h = 0.0; + double scale = 0.0; + for (std::size_t k = 0; k <= l; ++k) { + scale += std::abs(diag[k]); + } + if (scale == 0.0) { + subdiag[ui] = diag[l]; + for (std::size_t j = 0; j <= l; ++j) { + diag[j] = zAt(l, j); + zAt(ui, j) = 0.0; + zAt(j, ui) = 0.0; + } + diag[ui] = 0.0; + continue; + } + for (std::size_t k = 0; k <= l; ++k) { + diag[k] /= scale; + } + for (std::size_t k = 0; k <= l; ++k) { + h += diag[k] * diag[k]; + } + const double f = diag[l]; + const double g = -std::sqrt(h) * copySign(1.0, f); + subdiag[ui] = scale * g; + h -= f * g; + diag[l] = f - g; + + for (std::size_t k = 0; k <= l; ++k) { + subdiag[k] = 0.0; + } + for (std::size_t j = 0; j <= l; ++j) { + const double fj = diag[j]; + zAt(j, ui) = fj; + double gj = subdiag[j] + (zAt(j, j) * fj); + for (std::size_t k = j + 1; k <= l; ++k) { + gj += zAt(k, j) * diag[k]; + subdiag[k] += zAt(k, j) * fj; + } + subdiag[j] = gj; + } + double ff = 0.0; + for (std::size_t k = 0; k <= l; ++k) { + subdiag[k] /= h; + ff += subdiag[k] * diag[k]; + } + const double hh = 0.5 * ff / h; + for (std::size_t k = 0; k <= l; ++k) { + subdiag[k] -= hh * diag[k]; + } + for (std::size_t j = 0; j <= l; ++j) { + const double fj = diag[j]; + const double gj = subdiag[j]; + for (std::size_t k = j; k <= l; ++k) { + zAt(k, j) -= (fj * subdiag[k]) + (gj * diag[k]); + } + diag[j] = zAt(l, j); + zAt(ui, j) = 0.0; + } + diag[ui] = h; + } + + for (std::size_t i = 1; i < n; ++i) { + const std::size_t l = i - 1; + zAt(n - 1, l) = zAt(l, l); + zAt(l, l) = 1.0; + h = diag[i]; + if (h != 0.0) { + for (std::size_t k = 0; k <= l; ++k) { + diag[k] = zAt(k, i) / h; + } + for (std::size_t j = 0; j <= l; ++j) { + double g = 0.0; + for (std::size_t k = 0; k <= l; ++k) { + g += zAt(k, i) * zAt(k, j); + } + for (std::size_t k = 0; k <= l; ++k) { + zAt(k, j) -= g * diag[k]; + } + } + } + for (std::size_t k = 0; k <= l; ++k) { + zAt(k, i) = 0.0; + } + } + + for (std::size_t j = 0; j < n; ++j) { + diag[j] = zAt(n - 1, j); + } + for (std::size_t j = 0; j < n - 1; ++j) { + zAt(n - 1, j) = 0.0; + } + zAt(n - 1, n - 1) = 1.0; + subdiag[0] = 0.0; +} + +/// EISPACK `tql2` for `n = 4` (column-major `z[row + col*n]`). +static void symmetricTql24(std::array& diag, + std::array& subdiag, + std::array& z) { + constexpr std::size_t n = 4; + const auto zAt = [&z](const std::size_t row, + const std::size_t col) -> double& { + return z[row + (col * n)]; + }; + + for (std::size_t i = 1; i < n; ++i) { + subdiag[i - 1] = subdiag[i]; + } + double f = 0.0; + double tst1 = 0.0; + subdiag[n - 1] = 0.0; + + for (std::size_t l = 0; l < n; ++l) { + int j = 0; + const double h = std::abs(diag[l]) + std::abs(subdiag[l]); + tst1 = std::max(tst1, h); + + std::size_t m = l; + for (; m < n; ++m) { + const double tst2 = tst1 + std::abs(subdiag[m]); + if (tst2 == tst1) { + break; + } + } + + if (m != l) { + while (true) { + if (j == 30) { + llvm::reportFatalInternalError("symmetricTql2_4: failed to converge"); + } + ++j; + + const std::size_t l1 = l + 1; + const std::size_t l2 = l1 + 1; + const double g = diag[l]; + const double p = (diag[l1] - g) / (2.0 * subdiag[l]); + const double r = pythag2(p, 1.0); + diag[l] = subdiag[l] / (p + copySign(std::abs(r), p)); + diag[l1] = subdiag[l] * (p + copySign(std::abs(r), p)); + const double dl1 = diag[l1]; + const double hh = g - diag[l]; + for (std::size_t i = l2; i < n; ++i) { + diag[i] -= hh; + } + f += hh; + + double pv = diag[m]; + double c = 1.0; + double c2 = c; + const double el1 = subdiag[l1]; + double s = 0.0; + double c3 = 1.0; + double s2 = 0.0; + const std::size_t mml = m - l; + for (std::size_t ii = 1; ii <= mml; ++ii) { + c3 = c2; + c2 = c; + s2 = s; + const std::size_t i = m - ii; + const double gi = c * subdiag[i]; + const double hi = c * pv; + const double ri = pythag2(pv, subdiag[i]); + subdiag[i + 1] = s * ri; + s = subdiag[i] / ri; + c = pv / ri; + pv = (c * diag[i]) - (s * gi); + diag[i + 1] = hi + (s * ((c * gi) + (s * diag[i]))); + for (std::size_t k = 0; k < n; ++k) { + const double zkI1 = zAt(k, i + 1); + zAt(k, i + 1) = (s * zAt(k, i)) + (c * zkI1); + zAt(k, i) = (c * zAt(k, i)) - (s * zkI1); + } + } + pv = -s * s2 * c3 * el1 * subdiag[l] / dl1; + subdiag[l] = s * pv; + diag[l] = c * pv; + const double tst2 = tst1 + std::abs(subdiag[l]); + if (tst2 > tst1) { + continue; + } + break; + } + } + diag[l] += f; + } + + for (std::size_t ii = 1; ii < n; ++ii) { + const std::size_t i = ii - 1; + std::size_t k = i; + double p = diag[i]; + for (std::size_t j = ii; j < n; ++j) { + if (diag[j] < p) { + k = j; + p = diag[j]; + } + } + if (k == i) { + continue; + } + diag[k] = diag[i]; + diag[i] = p; + for (std::size_t j = 0; j < n; ++j) { + const double tmp = zAt(j, i); + zAt(j, i) = zAt(j, k); + zAt(j, k) = tmp; + } + } +} + +SymmetricEigen4 symmetricEigen4(const std::array& symmetric) { + constexpr std::size_t n = 4; + + std::array z{}; + std::array diag{}; + std::array subdiag{}; + symmetricTred24(symmetric, z, diag, subdiag); + symmetricTql24(diag, subdiag, z); + + SymmetricEigen4 result; + for (std::size_t col = 0; col < n; ++col) { + result.eigenvalues[col] = diag[col]; + for (std::size_t row = 0; row < n; ++row) { + result.eigenvectors(row, col) = Complex{z[row + (col * n)], 0.0}; + } + } + return result; +} + +SymmetricEigen4 symmetricEigen4(const Matrix4x4& symmetric) { + return symmetricEigen4(symmetric.realPart()); +} + +static Matrix4x4 embedSingleQubitInTwoQubit(const Matrix2x2& matrix, + const std::size_t qubitIndex) { + if (qubitIndex == 0) { + return kron(matrix, Matrix2x2::identity()); + } + if (qubitIndex == 1) { + return kron(Matrix2x2::identity(), matrix); + } + llvm::reportFatalInternalError("Invalid qubit index for single-qubit embed"); +} + +Matrix4x4 reorderTwoQubitMatrix(const Matrix4x4& matrix, + const std::size_t q0Index, + const std::size_t q1Index) { + if (q0Index == 0 && q1Index == 1) { + return matrix; + } + if (q0Index == 1 && q1Index == 0) { + // Conjugate by SWAP: out[i, j] = matrix[pi(i), pi(j)] with pi swapping |01> + // and |10> (basis indices 1 and 2). + const auto& m = matrix.data; + return Matrix4x4::fromElements(m[0], m[2], m[1], m[3], m[8], m[10], m[9], + m[11], m[4], m[6], m[5], m[7], m[12], m[14], + m[13], m[15]); + } + llvm::reportFatalInternalError("Invalid qubit indices for two-qubit reorder"); +} + +DynamicMatrix embedSingleQubitInNqubit(const Matrix2x2& matrix, + const std::size_t numQubits, + const std::size_t qubitIndex) { + if (qubitIndex >= numQubits) { + llvm::reportFatalInternalError( + "Invalid qubit index for single-qubit embed"); + } + if (numQubits == 2) { + return DynamicMatrix(embedSingleQubitInTwoQubit(matrix, qubitIndex)); + } + const auto dim = static_cast(1ULL << numQubits); + DynamicMatrix out(dim); + const auto udim = static_cast(dim); + for (std::size_t row = 0; row < udim; ++row) { + for (std::size_t col = 0; col < udim; ++col) { + if (!otherQubitBitsMatch(row, col, numQubits, qubitIndex, numQubits)) { + continue; + } + const std::size_t rowBit = qubitBitAt(row, numQubits, qubitIndex); + const std::size_t colBit = qubitBitAt(col, numQubits, qubitIndex); + out(static_cast(row), static_cast(col)) = + matrix(rowBit, colBit); + } + } + return out; +} + +DynamicMatrix embedTwoQubitInNqubit(const Matrix4x4& matrix, + const std::size_t numQubits, + const std::size_t q0Index, + const std::size_t q1Index) { + if (q0Index >= numQubits || q1Index >= numQubits || q0Index == q1Index) { + llvm::reportFatalInternalError("Invalid qubit indices for two-qubit embed"); + } + if (numQubits == 2) { + return DynamicMatrix(reorderTwoQubitMatrix(matrix, q0Index, q1Index)); + } + const auto dim = static_cast(1ULL << numQubits); + DynamicMatrix out(dim); + const auto udim = static_cast(dim); + for (std::size_t row = 0; row < udim; ++row) { + for (std::size_t col = 0; col < udim; ++col) { + if (!otherQubitBitsMatch(row, col, numQubits, q0Index, q1Index)) { + continue; + } + const std::size_t rowPair = (qubitBitAt(row, numQubits, q0Index) << 1) | + qubitBitAt(row, numQubits, q1Index); + const std::size_t colPair = (qubitBitAt(col, numQubits, q0Index) << 1) | + qubitBitAt(col, numQubits, q1Index); + out(static_cast(row), static_cast(col)) = + matrix(rowPair, colPair); + } + } + return out; +} + } // namespace mlir::qco diff --git a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp index afa0792415..22ce0315f7 100644 --- a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp +++ b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp @@ -12,8 +12,11 @@ #include +#include #include #include +#include +#include #include using namespace mlir::qco; @@ -130,6 +133,7 @@ TEST(UnitaryMatrix4x4, MultiplyAdjointTraceDeterminant) { EXPECT_TRUE(scaled.isApprox(swap * 2.0)); EXPECT_EQ(identity.trace(), Complex(4.0, 0.0)); EXPECT_EQ(identity.determinant(), Complex(1.0, 0.0)); + EXPECT_EQ(swap.determinant(), Complex(-1.0, 0.0)); } TEST(UnitaryMatrix4x4, PremultiplyBy) { @@ -186,6 +190,7 @@ TEST(DynamicMatrix, IdentityAndElementAccess) { EXPECT_EQ(identity(0, 0), 1.0); EXPECT_EQ(identity(1, 2), 0.0); EXPECT_EQ(identity(2, 2), 1.0); + EXPECT_TRUE(identity.isIdentity()); DynamicMatrix mutableMatrix = identity; mutableMatrix(1, 1) = 0.5; @@ -202,6 +207,13 @@ TEST(DynamicMatrix, FromAdjoint) { EXPECT_TRUE(DynamicMatrix(swapMatrix()).isApprox(swapMatrix())); } +TEST(DynamicMatrix, ScalarMultiplyAssign) { + DynamicMatrix matrix = DynamicMatrix::identity(2); + matrix *= std::exp(Complex{0.0, 0.5}); + EXPECT_TRUE(matrix.isApprox(DynamicMatrix::identity(2) * + std::exp(Complex{0.0, 0.5}))); +} + TEST(DynamicMatrix, AssignFrom) { DynamicMatrix dynamic; @@ -267,6 +279,38 @@ TEST(DynamicMatrix, IsApproxRejectsMismatchedExtents) { EXPECT_FALSE(DynamicMatrix::identity(1).isApprox(DynamicMatrix::identity(2))); } +TEST(DynamicMatrix, IsApproxOverloads) { + const Matrix1x1 phase = Matrix1x1::fromElements(Complex{0.25, 0.5}); + const Matrix2x2 x = pauliX(); + const Matrix4x4 swap = swapMatrix(); + + DynamicMatrix as1x1; + as1x1.assignFrom(phase); + EXPECT_TRUE(as1x1.isApprox(phase)); + EXPECT_FALSE(as1x1.isApprox(Matrix1x1::fromElements(1.0))); + + DynamicMatrix as2x2; + as2x2.assignFrom(x); + EXPECT_TRUE(as2x2.isApprox(x)); + EXPECT_FALSE(as2x2.isApprox(Matrix2x2::identity())); + + DynamicMatrix as4x4; + as4x4.assignFrom(swap); + EXPECT_TRUE(as4x4.isApprox(swap)); + EXPECT_FALSE(as4x4.isApprox(Matrix4x4::identity())); + + DynamicMatrix wrongDim = DynamicMatrix::identity(3); + EXPECT_FALSE(wrongDim.isApprox(phase)); + EXPECT_FALSE(wrongDim.isApprox(x)); + EXPECT_FALSE(wrongDim.isApprox(swap)); + + const DynamicMatrix a = DynamicMatrix::identity(2); + DynamicMatrix b = a; + b(1, 0) += 1e-15; + EXPECT_TRUE(a.isApprox(b)); + EXPECT_FALSE(a.isApprox(DynamicMatrix::identity(3))); +} + TEST(Matrix1x1, AssignFromDynamicMatrix) { const Matrix1x1 phase = Matrix1x1::fromElements(Complex{0.25, 0.5}); @@ -303,34 +347,219 @@ TEST(Matrix4x4, AssignFromDynamicMatrix) { EXPECT_FALSE(out.assignFrom(DynamicMatrix::identity(2))); } -TEST(DynamicMatrix, IsApproxOverloads) { - const Matrix1x1 phase = Matrix1x1::fromElements(Complex{0.25, 0.5}); +TEST(UnitaryMatrix2x2, TransposeAndIsIdentity) { + const Matrix2x2 m = Matrix2x2::fromElements(1, 2i, 3, 4); + EXPECT_TRUE(m.transpose().isApprox(Matrix2x2::fromElements(1, 3, 2i, 4))); + EXPECT_TRUE(Matrix2x2::identity().isIdentity()); + EXPECT_FALSE(pauliX().isIdentity()); +} + +TEST(UnitaryMatrix4x4, TransposeAndIsIdentity) { + Matrix4x4 m = Matrix4x4::identity(); + m(0, 3) = 2i; + m(3, 0) = 5.0; + const Matrix4x4 t = m.transpose(); + EXPECT_EQ(t(3, 0), 2i); + EXPECT_EQ(t(0, 3), 5.0); + EXPECT_TRUE(Matrix4x4::identity().isIdentity()); + EXPECT_FALSE(swapMatrix().isIdentity()); +} + +TEST(UnitaryMatrix4x4, DiagonalColumnsAndParts) { + Matrix4x4 m = + Matrix4x4::fromElements(Complex{1, 1}, 0, 0, 0, 0, Complex{2, 2}, 0, 0, 0, + 0, Complex{3, 3}, 0, 0, 0, 0, Complex{4, 4}); + const auto diag = m.diagonal(); + EXPECT_EQ(diag[0], (Complex{1, 1})); + EXPECT_EQ(diag[3], (Complex{4, 4})); + EXPECT_TRUE(Matrix4x4::fromDiagonal(diag).isApprox(m)); + + const auto col1 = m.column(1); + EXPECT_EQ(col1[1], (Complex{2, 2})); + Matrix4x4 n = Matrix4x4::identity(); + n.setColumn(2, {1i, 2i, 3i, 4i}); + EXPECT_EQ(n(0, 2), 1i); + EXPECT_EQ(n(3, 2), 4i); + + const auto re = m.realPart(); + const auto im = m.imagPart(); + EXPECT_EQ(re[0], 1.0); + EXPECT_EQ(im[0], 1.0); + EXPECT_EQ(re[15], 4.0); + EXPECT_EQ(im[15], 4.0); +} + +TEST(UnitaryMatrix4x4, KroneckerProduct) { const Matrix2x2 x = pauliX(); - const Matrix4x4 swap = swapMatrix(); + // X (x) I should swap the high bit. + const Matrix4x4 xi = kron(x, Matrix2x2::identity()); + EXPECT_TRUE(xi.isApprox(Matrix4x4::fromElements(0, 0, 1, 0, // row 0 + 0, 0, 0, 1, // row 1 + 1, 0, 0, 0, // row 2 + 0, 1, 0, 0))); + // I (x) X swaps the low bit. + const Matrix4x4 ix = kron(Matrix2x2::identity(), x); + EXPECT_TRUE(ix.isApprox(Matrix4x4::fromElements(0, 1, 0, 0, // row 0 + 1, 0, 0, 0, // row 1 + 0, 0, 0, 1, // row 2 + 0, 0, 1, 0))); +} - DynamicMatrix as1x1; - as1x1.assignFrom(phase); - EXPECT_TRUE(as1x1.isApprox(phase)); - EXPECT_FALSE(as1x1.isApprox(Matrix1x1::fromElements(1.0))); +TEST(UnitaryMatrix4x4, ReorderTwoQubitMatrix) { + const Matrix2x2 x = pauliX(); + const Matrix4x4 onHigh = kron(x, Matrix2x2::identity()); + const Matrix4x4 onLow = kron(Matrix2x2::identity(), x); - DynamicMatrix as2x2; - as2x2.assignFrom(x); - EXPECT_TRUE(as2x2.isApprox(x)); - EXPECT_FALSE(as2x2.isApprox(Matrix2x2::identity())); + EXPECT_TRUE(reorderTwoQubitMatrix(onHigh, 0, 1).isApprox(onHigh)); + EXPECT_TRUE(reorderTwoQubitMatrix(onHigh, 1, 0).isApprox(onLow)); + EXPECT_TRUE(reorderTwoQubitMatrix(onLow, 1, 0).isApprox(onHigh)); +} - DynamicMatrix as4x4; - as4x4.assignFrom(swap); - EXPECT_TRUE(as4x4.isApprox(swap)); - EXPECT_FALSE(as4x4.isApprox(Matrix4x4::identity())); +TEST(UnitaryDynamicMatrix, NQubitEmbedMatchesTwoQubitSpecialization) { + const Matrix2x2 x = pauliX(); + const Matrix4x4 cx = Matrix4x4::fromElements(1, 0, 0, 0, // + 0, 1, 0, 0, // + 0, 0, 0, 1, // + 0, 0, 1, 0); + EXPECT_TRUE(embedSingleQubitInNqubit(x, 2, 0).isApprox( + kron(x, Matrix2x2::identity()))); + EXPECT_TRUE(embedSingleQubitInNqubit(x, 2, 1).isApprox( + kron(Matrix2x2::identity(), x))); + EXPECT_TRUE(embedTwoQubitInNqubit(cx, 2, 0, 1) + .isApprox(reorderTwoQubitMatrix(cx, 0, 1))); + const DynamicMatrix cxOn01 = embedTwoQubitInNqubit(cx, 3, 0, 1); + const DynamicMatrix cxOn12 = embedTwoQubitInNqubit(cx, 3, 1, 2); + EXPECT_EQ(cxOn01.rows(), 8); + EXPECT_EQ(cxOn12.rows(), 8); + EXPECT_FALSE(cxOn01.isApprox(cxOn12)); +} - DynamicMatrix wrongDim = DynamicMatrix::identity(3); - EXPECT_FALSE(wrongDim.isApprox(phase)); - EXPECT_FALSE(wrongDim.isApprox(x)); - EXPECT_FALSE(wrongDim.isApprox(swap)); +TEST(UnitaryDynamicMatrix, EmbedSingleQubitOnMiddleWire) { + const Matrix2x2 x = pauliX(); + const DynamicMatrix embedded = embedSingleQubitInNqubit(x, 3, 1); + EXPECT_EQ(embedded.rows(), 8); + EXPECT_FALSE(embedded.isIdentity()); - const DynamicMatrix a = DynamicMatrix::identity(2); - DynamicMatrix b = a; - b(1, 0) += 1e-15; - EXPECT_TRUE(a.isApprox(b)); - EXPECT_FALSE(a.isApprox(DynamicMatrix::identity(3))); + const DynamicMatrix product = embedded * embedded; + EXPECT_TRUE(product.isIdentity()); + EXPECT_NEAR(product.trace().real(), 8.0, MATRIX_TOLERANCE); +} + +TEST(UnitaryDynamicMatrix, MultiplyTraceAndScalar) { + const Matrix2x2 x = pauliX(); + const DynamicMatrix embedded = embedSingleQubitInNqubit(x, 2, 0); + EXPECT_FALSE(embedded.isIdentity()); + const Complex scalar = std::exp(1i * 0.3); + EXPECT_TRUE((scalar * embedded).isApprox(embedded * scalar)); + const DynamicMatrix product = embedded * embedded; + EXPECT_TRUE(product.isIdentity()); + EXPECT_NEAR(product.trace().real(), 4.0, MATRIX_TOLERANCE); +} + +TEST(DynamicMatrix, MultiplyAdjointTraceAt4) { + const auto swap = DynamicMatrix(swapMatrix()); + EXPECT_EQ(swap.rows(), 4); + + const DynamicMatrix product = swap * swap; + EXPECT_TRUE(product.isIdentity()); + EXPECT_NEAR(product.trace().real(), 4.0, MATRIX_TOLERANCE); + + const DynamicMatrix adjoint = swap.adjoint(); + EXPECT_TRUE(adjoint.isApprox(swapMatrix())); +} + +TEST(UnitaryMatrix2x2, ScalarLeftMultiply) { + const Matrix2x2 x = pauliX(); + const Complex scalar = std::exp(1i * 0.5); + EXPECT_TRUE((scalar * x).isApprox(x * scalar)); +} + +TEST(UnitaryMatrix4x4, ScalarLeftMultiply) { + const Matrix4x4 swap = swapMatrix(); + const Complex scalar = std::exp(1i * 0.25); + EXPECT_TRUE((scalar * swap).isApprox(swap * scalar)); +} + +TEST(SymmetricEigensolver, DiagonalMatrix) { + std::array a{}; + a[0] = 3.0; + a[5] = 1.0; + a[10] = 4.0; + a[15] = 2.0; + const SymmetricEigen4 result = symmetricEigen4(a); + EXPECT_NEAR(result.eigenvalues[0], 1.0, MATRIX_TOLERANCE); + EXPECT_NEAR(result.eigenvalues[1], 2.0, MATRIX_TOLERANCE); + EXPECT_NEAR(result.eigenvalues[2], 3.0, MATRIX_TOLERANCE); + EXPECT_NEAR(result.eigenvalues[3], 4.0, MATRIX_TOLERANCE); +} + +TEST(SymmetricEigensolver, Matrix4x4Overload) { + std::array a{}; + a[0] = 3.0; + a[5] = 1.0; + a[10] = 4.0; + a[15] = 2.0; + Matrix4x4 matrix{}; + for (std::size_t k = 0; k < 16; ++k) { + matrix(k / 4, k % 4) = a[k]; + } + const SymmetricEigen4 fromArray = symmetricEigen4(a); + const SymmetricEigen4 fromMatrix = symmetricEigen4(matrix); + for (std::size_t i = 0; i < 4; ++i) { + EXPECT_NEAR(fromMatrix.eigenvalues[i], fromArray.eigenvalues[i], + MATRIX_TOLERANCE); + } + EXPECT_TRUE(fromMatrix.eigenvectors.isApprox(fromArray.eigenvectors)); +} + +TEST(SymmetricEigensolver, ReconstructsRandomSymmetric) { + std::mt19937 rng(0xC0FFEE); + std::uniform_real_distribution dist(-2.0, 2.0); + for (int trial = 0; trial < 50; ++trial) { + std::array a{}; + for (std::size_t i = 0; i < 4; ++i) { + for (std::size_t j = i; j < 4; ++j) { + const double value = dist(rng); + a[(i * 4) + j] = value; + a[(j * 4) + i] = value; + } + } + const SymmetricEigen4 result = symmetricEigen4(a); + + // Eigenvalues are ascending. + for (std::size_t i = 0; i + 1 < 4; ++i) { + EXPECT_LE(result.eigenvalues[i], + result.eigenvalues[i + 1] + MATRIX_TOLERANCE); + } + + // Eigenvectors are orthonormal: V^T V == I. + const Matrix4x4& v = result.eigenvectors; + EXPECT_TRUE((v.transpose() * v).isIdentity()); + + // Reconstruction: V D V^T == A. + const Matrix4x4 d = + Matrix4x4::fromDiagonal({result.eigenvalues[0], result.eigenvalues[1], + result.eigenvalues[2], result.eigenvalues[3]}); + const Matrix4x4 reconstructed = v * d * v.transpose(); + Matrix4x4 original{}; + for (std::size_t k = 0; k < 16; ++k) { + original(k / 4, k % 4) = a[k]; + } + EXPECT_TRUE(reconstructed.isApprox(original)); + } +} + +TEST(SymmetricEigensolver, HandlesDegenerateSpectrum) { + // A scalar multiple of the identity: every vector is an eigenvector, but the + // returned basis must still be orthonormal. + std::array a{}; + for (std::size_t i = 0; i < 4; ++i) { + a[(i * 4) + i] = 2.5; + } + const SymmetricEigen4 result = symmetricEigen4(a); + for (const double value : result.eigenvalues) { + EXPECT_NEAR(value, 2.5, MATRIX_TOLERANCE); + } + const Matrix4x4& v = result.eigenvectors; + EXPECT_TRUE((v.transpose() * v).isIdentity()); } From f892bafeb8176361b3d685a82870afe2afb0c6b8 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 22 Jun 2026 12:20:11 +0200 Subject: [PATCH 056/122] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20linter=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/lib/Dialect/QCO/Utils/Matrix.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Utils/Matrix.cpp b/mlir/lib/Dialect/QCO/Utils/Matrix.cpp index 7a53347c04..32932d3d6a 100644 --- a/mlir/lib/Dialect/QCO/Utils/Matrix.cpp +++ b/mlir/lib/Dialect/QCO/Utils/Matrix.cpp @@ -568,11 +568,11 @@ DynamicMatrix DynamicMatrix::operator*(const DynamicMatrix& rhs) const { "DynamicMatrix multiply requires matching dimensions"); } DynamicMatrix out(impl_->dim); - if (impl_->dim == static_cast(Matrix2x2::K_ROWS)) { + if (std::cmp_equal(impl_->dim, Matrix2x2::K_ROWS)) { multiply2x2(impl_->data, rhs.impl_->data, out.impl_->data); return out; } - if (impl_->dim == static_cast(Matrix4x4::K_ROWS)) { + if (std::cmp_equal(impl_->dim, Matrix4x4::K_ROWS)) { multiply4x4(impl_->data, rhs.impl_->data, out.impl_->data); return out; } From 7e3fe1aa5905cafebcba2da06f5c28202f8d23c5 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 22 Jun 2026 12:51:02 +0200 Subject: [PATCH 057/122] =?UTF-8?q?=E2=98=82=EF=B8=8F=20Increase=20coverag?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/lib/Dialect/QCO/Utils/Matrix.cpp | 103 ++++++------------ .../Dialect/QCO/Utils/test_unitary_matrix.cpp | 20 ++++ 2 files changed, 56 insertions(+), 67 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Utils/Matrix.cpp b/mlir/lib/Dialect/QCO/Utils/Matrix.cpp index 32932d3d6a..ed55c4a212 100644 --- a/mlir/lib/Dialect/QCO/Utils/Matrix.cpp +++ b/mlir/lib/Dialect/QCO/Utils/Matrix.cpp @@ -27,14 +27,20 @@ namespace mlir::qco { +/// Returns true if @p lhs and @p rhs differ by at most @p tol (complex +/// modulus). +[[nodiscard]] static bool entryIsApprox(const Complex& lhs, const Complex& rhs, + const double tol) { + return std::abs(lhs - rhs) <= tol; +} + /// Returns true if every entry pair differs by at most @p tol (complex /// modulus). [[nodiscard]] static bool entriesAreApprox(ArrayRef lhs, ArrayRef rhs, double tol) { - return std::ranges::equal(lhs, rhs, - [tol](const Complex& a, const Complex& b) { - return std::abs(a - b) <= tol; - }); + return llvm::equal(lhs, rhs, [tol](const Complex& a, const Complex& b) { + return entryIsApprox(a, b, tol); + }); } /// Writes the conjugate transpose of @p in into @p out (square, row-major). @@ -130,15 +136,15 @@ multiply4x4(const std::array& lhs, /// Returns true if @p data is approximately the @p dim x @p dim identity /// matrix. -template -[[nodiscard]] static bool -isIdentityEntries(const std::array& data, - const double tol) { - for (std::size_t row = 0; row < Dim; ++row) { - for (std::size_t col = 0; col < Dim; ++col) { - const Complex& entry = data[(row * Dim) + col]; +[[nodiscard]] static bool isIdentityEntries(ArrayRef data, + const std::size_t dim, + const double tol) { + assert(data.size() >= dim * dim); + for (std::size_t row = 0; row < dim; ++row) { + for (std::size_t col = 0; col < dim; ++col) { + const Complex& entry = data[(row * dim) + col]; if (row == col) { - if (std::abs(entry - Complex{1.0, 0.0}) > tol) { + if (!entryIsApprox(entry, Complex{1.0, 0.0}, tol)) { return false; } } else if (std::abs(entry) > tol) { @@ -206,7 +212,7 @@ Complex Matrix1x1::operator()(const std::size_t row, } bool Matrix1x1::isApprox(const Matrix1x1& other, const double tol) const { - return std::abs(value - other.value) <= tol; + return entryIsApprox(value, other.value, tol); } bool Matrix1x1::assignFrom(const DynamicMatrix& src) { @@ -282,7 +288,7 @@ Complex Matrix2x2::determinant() const { } bool Matrix2x2::isIdentity(const double tol) const { - return isIdentityEntries(data, tol); + return isIdentityEntries(data, K_ROWS, tol); } bool Matrix2x2::isApprox(const Matrix2x2& other, const double tol) const { @@ -372,7 +378,7 @@ Complex Matrix4x4::determinant() const { } bool Matrix4x4::isIdentity(const double tol) const { - return isIdentityEntries(data, tol); + return isIdentityEntries(data, K_ROWS, tol); } std::array Matrix4x4::diagonal() const { @@ -533,7 +539,7 @@ bool DynamicMatrix::isApprox(const Matrix1x1& other, const double tol) const { if (impl_->dim != 1) { return false; } - return std::abs(impl_->data[0] - other.value) <= tol; + return entryIsApprox(impl_->data[0], other.value, tol); } bool DynamicMatrix::isApprox(const Matrix2x2& other, const double tol) const { @@ -563,10 +569,8 @@ Complex DynamicMatrix::trace() const { } DynamicMatrix DynamicMatrix::operator*(const DynamicMatrix& rhs) const { - if (impl_->dim != rhs.impl_->dim) { - llvm::reportFatalInternalError( - "DynamicMatrix multiply requires matching dimensions"); - } + assert(impl_->dim == rhs.impl_->dim && + "DynamicMatrix multiply requires matching dimensions"); DynamicMatrix out(impl_->dim); if (std::cmp_equal(impl_->dim, Matrix2x2::K_ROWS)) { multiply2x2(impl_->data, rhs.impl_->data, out.impl_->data); @@ -607,20 +611,7 @@ DynamicMatrix& DynamicMatrix::operator*=(const Complex& scalar) { } bool DynamicMatrix::isIdentity(const double tol) const { - const auto udim = checkedDim(impl_->dim); - for (std::size_t row = 0; row < udim; ++row) { - for (std::size_t col = 0; col < udim; ++col) { - const Complex& entry = impl_->data[(row * udim) + col]; - if (row == col) { - if (std::abs(entry - Complex{1.0, 0.0}) > tol) { - return false; - } - } else if (std::abs(entry) > tol) { - return false; - } - } - } - return true; + return isIdentityEntries(impl_->data, checkedDim(impl_->dim), tol); } /** @@ -681,25 +672,6 @@ DynamicMatrix operator*(const Complex& scalar, const DynamicMatrix& matrix) { return matrix * scalar; } -[[nodiscard]] static double pythag2(const double a, const double b) { - const double absA = std::abs(a); - const double absB = std::abs(b); - if (absA > absB) { - const double ratio = absB / absA; - return absA * std::sqrt(1.0 + (ratio * ratio)); - } - if (absB == 0.0) { - return 0.0; - } - const double ratio = absA / absB; - return absB * std::sqrt(1.0 + (ratio * ratio)); -} - -[[nodiscard]] static double copySign(const double magnitude, - const double signSource) { - return (signSource >= 0.0) ? std::abs(magnitude) : -std::abs(magnitude); -} - // Adapted from John Burkardt's MIT-licensed EISPACK C port (`tred2` / `tql2`): // https://people.sc.fsu.edu/~jburkardt/c_src/eispack/eispack.c // Specialized to `n = 4`; input is row-major, accumulator `z` is column-major. @@ -748,7 +720,7 @@ static void symmetricTred24(const std::array& input, h += diag[k] * diag[k]; } const double f = diag[l]; - const double g = -std::sqrt(h) * copySign(1.0, f); + const double g = -std::sqrt(h) * std::copysign(1.0, f); subdiag[ui] = scale * g; h -= f * g; diag[l] = f - g; @@ -862,9 +834,9 @@ static void symmetricTql24(std::array& diag, const std::size_t l2 = l1 + 1; const double g = diag[l]; const double p = (diag[l1] - g) / (2.0 * subdiag[l]); - const double r = pythag2(p, 1.0); - diag[l] = subdiag[l] / (p + copySign(std::abs(r), p)); - diag[l1] = subdiag[l] * (p + copySign(std::abs(r), p)); + const double r = std::hypot(p, 1.0); + diag[l] = subdiag[l] / (p + std::copysign(std::abs(r), p)); + diag[l1] = subdiag[l] * (p + std::copysign(std::abs(r), p)); const double dl1 = diag[l1]; const double hh = g - diag[l]; for (std::size_t i = l2; i < n; ++i) { @@ -887,7 +859,7 @@ static void symmetricTql24(std::array& diag, const std::size_t i = m - ii; const double gi = c * subdiag[i]; const double hi = c * pv; - const double ri = pythag2(pv, subdiag[i]); + const double ri = std::hypot(pv, subdiag[i]); subdiag[i + 1] = s * ri; s = subdiag[i] / ri; c = pv / ri; @@ -966,7 +938,7 @@ static Matrix4x4 embedSingleQubitInTwoQubit(const Matrix2x2& matrix, if (qubitIndex == 1) { return kron(Matrix2x2::identity(), matrix); } - llvm::reportFatalInternalError("Invalid qubit index for single-qubit embed"); + assert(false && "Invalid qubit index for single-qubit embed"); } Matrix4x4 reorderTwoQubitMatrix(const Matrix4x4& matrix, @@ -983,16 +955,14 @@ Matrix4x4 reorderTwoQubitMatrix(const Matrix4x4& matrix, m[11], m[4], m[6], m[5], m[7], m[12], m[14], m[13], m[15]); } - llvm::reportFatalInternalError("Invalid qubit indices for two-qubit reorder"); + assert(false && "Invalid qubit indices for two-qubit reorder"); } DynamicMatrix embedSingleQubitInNqubit(const Matrix2x2& matrix, const std::size_t numQubits, const std::size_t qubitIndex) { - if (qubitIndex >= numQubits) { - llvm::reportFatalInternalError( - "Invalid qubit index for single-qubit embed"); - } + assert(qubitIndex < numQubits && + "Invalid qubit index for single-qubit embed"); if (numQubits == 2) { return DynamicMatrix(embedSingleQubitInTwoQubit(matrix, qubitIndex)); } @@ -1017,9 +987,8 @@ DynamicMatrix embedTwoQubitInNqubit(const Matrix4x4& matrix, const std::size_t numQubits, const std::size_t q0Index, const std::size_t q1Index) { - if (q0Index >= numQubits || q1Index >= numQubits || q0Index == q1Index) { - llvm::reportFatalInternalError("Invalid qubit indices for two-qubit embed"); - } + assert(q0Index < numQubits && q1Index < numQubits && q0Index != q1Index && + "Invalid qubit indices for two-qubit embed"); if (numQubits == 2) { return DynamicMatrix(reorderTwoQubitMatrix(matrix, q0Index, q1Index)); } diff --git a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp index 22ce0315f7..946dc1956e 100644 --- a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp +++ b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp @@ -352,6 +352,11 @@ TEST(UnitaryMatrix2x2, TransposeAndIsIdentity) { EXPECT_TRUE(m.transpose().isApprox(Matrix2x2::fromElements(1, 3, 2i, 4))); EXPECT_TRUE(Matrix2x2::identity().isIdentity()); EXPECT_FALSE(pauliX().isIdentity()); + Matrix2x2 nearIdentity = Matrix2x2::identity(); + nearIdentity(0, 1) = 1e-15; + EXPECT_TRUE(nearIdentity.isIdentity()); + nearIdentity(0, 1) = 1.0; + EXPECT_FALSE(nearIdentity.isIdentity()); } TEST(UnitaryMatrix4x4, TransposeAndIsIdentity) { @@ -468,6 +473,21 @@ TEST(DynamicMatrix, MultiplyAdjointTraceAt4) { EXPECT_TRUE(adjoint.isApprox(swapMatrix())); } +TEST(DynamicMatrix, MultiplyAt2) { + const DynamicMatrix x(pauliX()); + EXPECT_EQ(x.rows(), 2); + const DynamicMatrix product = x * x; + EXPECT_TRUE(product.isIdentity()); + EXPECT_NEAR(product.trace().real(), 2.0, MATRIX_TOLERANCE); + EXPECT_TRUE(product.isApprox(pauliX() * pauliX())); +} + +TEST(DynamicMatrix, IsIdentityOffDiagonal) { + DynamicMatrix matrix = DynamicMatrix::identity(2); + matrix(0, 1) = 1.0; + EXPECT_FALSE(matrix.isIdentity()); +} + TEST(UnitaryMatrix2x2, ScalarLeftMultiply) { const Matrix2x2 x = pauliX(); const Complex scalar = std::exp(1i * 0.5); From a2484a18f23ba74f855496216738996e68d7d3d9 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 22 Jun 2026 13:43:15 +0200 Subject: [PATCH 058/122] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Suggestions=20from?= =?UTF-8?q?=20code=20review=20to=20improve=20performance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/include/mlir/Dialect/QCO/Utils/Matrix.h | 187 ++-- mlir/lib/Dialect/QCO/Utils/Matrix.cpp | 865 +++++++++--------- .../Dialect/QCO/Utils/test_unitary_matrix.cpp | 58 +- 3 files changed, 578 insertions(+), 532 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h index 402e86ac4a..369cd2209b 100644 --- a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h +++ b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h @@ -10,6 +10,9 @@ #pragma once +#include +#include + #include #include #include @@ -26,6 +29,8 @@ using Complex = std::complex; inline constexpr double MATRIX_TOLERANCE = 1e-14; class DynamicMatrix; +struct Matrix4x4; +struct SymmetricEigen4; /** * @brief 1x1 matrix for global-phase gates. @@ -228,6 +233,30 @@ struct Matrix2x2 { * @return `true` when @p src is 2x2. */ [[nodiscard]] bool assignFrom(const DynamicMatrix& src); + + /** + * @brief Embed this single-qubit matrix into an @p numQubits-qubit Hilbert + * space. + * + * Wire @p qubitIndex uses the same MSB-first convention as @ref + * Matrix4x4::kron (high bit first operand, low bit second). For each basis + * pair whose untouched wires match, copies this matrix at the target qubit's + * row/column bits. + * + * @param numQubits Number of qubits in the target Hilbert space. + * @param qubitIndex Wire index to act on. + * @return Embedded unitary as a dynamic matrix. + */ + [[nodiscard]] DynamicMatrix embedInNqubit(std::size_t numQubits, + std::size_t qubitIndex) const; + + /** + * @brief Embed this single-qubit matrix into a two-qubit Hilbert space. + * + * @param qubitIndex Wire index (`0` = high bit / MSB, `1` = low bit). + * @return The `4x4` embedded unitary. + */ + [[nodiscard]] Matrix4x4 embedInTwoQubit(std::size_t qubitIndex) const; }; /** @@ -368,11 +397,25 @@ struct Matrix4x4 { /** * @brief Builds a diagonal matrix from four diagonal entries. - * @param diagonalEntries Diagonal entries `(m00, m11, m22, m33)`. + * @param diagonalEntries Diagonal entries `(m00, m11, m22, m33)`; must have + * length `K_ROWS`. * @return Diagonal matrix with the given entries. */ [[nodiscard]] static Matrix4x4 - fromDiagonal(const std::array& diagonalEntries); + fromDiagonal(ArrayRef diagonalEntries); + + /** + * @brief Kronecker product `lhs (x) rhs` of two single-qubit matrices. + * + * Uses the computational-basis bit order where the first operand labels the + * high bit, matching `UnitaryOpInterface::getUnitaryMatrix4x4`. + * + * @param lhs Left factor (acts on the high bit / qubit 0). + * @param rhs Right factor (acts on the low bit / qubit 1). + * @return The `4x4` Kronecker product. + */ + [[nodiscard]] static Matrix4x4 kron(const Matrix2x2& lhs, + const Matrix2x2& rhs); /** * @brief Returns the entries of column @p col, top to bottom. @@ -384,9 +427,23 @@ struct Matrix4x4 { /** * @brief Overwrites column @p col with @p values. * @param col Column index in `[0, K_COLS)`. - * @param values New column entries, top to bottom. + * @param values New column entries, top to bottom; must have length `K_ROWS`. + */ + void setColumn(std::size_t col, ArrayRef values); + + /** + * @brief Returns the entries of row @p row, left to right. + * @param row Row index in `[0, K_ROWS)`. + * @return View over the four row entries. + */ + [[nodiscard]] ArrayRef row(std::size_t row) const; + + /** + * @brief Overwrites row @p row with @p values. + * @param row Row index in `[0, K_ROWS)`. + * @param values New row entries, left to right; must have length `K_COLS`. */ - void setColumn(std::size_t col, const std::array& values); + void setRow(std::size_t row, ArrayRef values); /** * @brief Returns the element-wise real parts in row-major order. @@ -420,6 +477,58 @@ struct Matrix4x4 { * @return `true` when @p src is 4x4. */ [[nodiscard]] bool assignFrom(const DynamicMatrix& src); + + /** + * @brief Embed this two-qubit matrix into an @p numQubits-qubit Hilbert + * space. + * + * Operand 0 labels the high bit of the pair and acts on @p q0Index; operand 1 + * labels the low bit and acts on @p q1Index. For each basis pair whose other + * wires match, copies this matrix at the packed two-qubit row/column indices. + * + * @param numQubits Number of qubits in the target Hilbert space. + * @param q0Index Wire index of operand 0. + * @param q1Index Wire index of operand 1. + * @return Embedded unitary as a dynamic matrix. + */ + [[nodiscard]] DynamicMatrix embedInNqubit(std::size_t numQubits, + std::size_t q0Index, + std::size_t q1Index) const; + + /** + * @brief Reorder this matrix to act on qubits `{0, 1}`. + * + * @param q0Index Wire index of operand 0; @p q1Index wire index of operand 1. + * @return Reordered copy of this matrix. + */ + [[nodiscard]] Matrix4x4 reorderForQubits(std::size_t q0Index, + std::size_t q1Index) const; + + /** + * @brief Computes the eigendecomposition of this real symmetric matrix. + * + * @copydoc Matrix4x4::symmetricEigen4(const std::array&) + * + * @pre Entries are real (imaginary parts must be negligible). The real parts + * must form a symmetric matrix; imaginary parts are ignored. + */ + [[nodiscard]] SymmetricEigen4 symmetricEigen4() const; + + /** + * @brief Computes the eigendecomposition of a real symmetric `4x4` matrix. + * + * Uses Householder tridiagonalization (EISPACK `tred2`) followed by implicit + * QL iteration (`tql2`) on the tridiagonal form. + * + * @pre @p symmetric is real and symmetric: `symmetric[i,j] == symmetric[j,i]` + * for all `i, j`. Only the lower triangle (including the diagonal) is read, + * but supplying a non-symmetric matrix yields undefined numerical results. + * + * @param symmetric Row-major real symmetric `4x4` matrix. + * @return Ascending eigenvalues and matching eigenvectors (as columns). + */ + [[nodiscard]] static SymmetricEigen4 + symmetricEigen4(const std::array& symmetric); }; /** @@ -660,18 +769,6 @@ inline constexpr bool std::is_same, std::is_same>; -/** - * @brief Kronecker product `lhs (x) rhs` of two single-qubit matrices. - * - * Uses the computational-basis bit order where the first operand labels the - * high bit, matching `UnitaryOpInterface::getUnitaryMatrix4x4`. - * - * @param lhs Left factor (acts on the high bit / qubit 0). - * @param rhs Right factor (acts on the low bit / qubit 1). - * @return The `4x4` Kronecker product. - */ -[[nodiscard]] Matrix4x4 kron(const Matrix2x2& lhs, const Matrix2x2& rhs); - /// Scalar-on-the-left multiply `scalar * matrix` (commutes with the member /// `matrix * scalar`). Provided so generic code can scale a matrix from /// either side. @@ -696,62 +793,4 @@ struct SymmetricEigen4 { Matrix4x4 eigenvectors{}; }; -/** - * @brief Computes the eigendecomposition of a real symmetric `4x4` matrix. - * - * Uses Householder tridiagonalization (EISPACK `tred2`) followed by implicit - * QL iteration (`tql2`) on the tridiagonal form. - * - * @pre @p symmetric is real and symmetric: `symmetric[i,j] == symmetric[j,i]` - * for all `i, j`. Only the lower triangle (including the diagonal) is read, - * but supplying a non-symmetric matrix yields undefined numerical results. - * - * @param symmetric Row-major real symmetric `4x4` matrix. - * @return Ascending eigenvalues and matching eigenvectors (as columns). - */ -[[nodiscard]] SymmetricEigen4 -symmetricEigen4(const std::array& symmetric); - -/** - * @brief Computes the eigendecomposition of a real symmetric `4x4` matrix. - * - * @copydoc symmetricEigen4(const std::array&) - * - * @pre Entries of @p symmetric are real (imaginary parts must be negligible). - * The real parts must form a symmetric matrix; imaginary parts are ignored. - */ -[[nodiscard]] SymmetricEigen4 symmetricEigen4(const Matrix4x4& symmetric); - -/** - * @brief Reorder a two-qubit matrix to act on qubits `{0, 1}`. - * - * @param q0Index Wire index of operand 0; @p q1Index wire index of operand 1. - */ -[[nodiscard]] Matrix4x4 reorderTwoQubitMatrix(const Matrix4x4& matrix, - std::size_t q0Index, - std::size_t q1Index); - -/** - * @brief Embed a single-qubit matrix into an @p numQubits-qubit Hilbert space. - * - * Qubit @p qubitIndex uses the same MSB-first convention as @ref kron - * (high bit first operand, low bit second). For each basis pair whose untouched - * wires match, copies @p matrix at the target qubit's row/column bits. - */ -[[nodiscard]] DynamicMatrix embedSingleQubitInNqubit(const Matrix2x2& matrix, - std::size_t numQubits, - std::size_t qubitIndex); - -/** - * @brief Embed a two-qubit matrix into an @p numQubits-qubit Hilbert space. - * - * Operand 0 labels the high bit of the pair and acts on @p q0Index; operand 1 - * labels the low bit and acts on @p q1Index. For each basis pair whose other - * wires match, copies @p matrix at the packed two-qubit row/column indices. - */ -[[nodiscard]] DynamicMatrix embedTwoQubitInNqubit(const Matrix4x4& matrix, - std::size_t numQubits, - std::size_t q0Index, - std::size_t q1Index); - } // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Utils/Matrix.cpp b/mlir/lib/Dialect/QCO/Utils/Matrix.cpp index ed55c4a212..806aaa51ae 100644 --- a/mlir/lib/Dialect/QCO/Utils/Matrix.cpp +++ b/mlir/lib/Dialect/QCO/Utils/Matrix.cpp @@ -100,13 +100,6 @@ static void multiply2x2(const ArrayRef lhs, out[3] = lhs[2] * rhs[1] + lhs[3] * rhs[3]; } -static void -multiply2x2(const std::array& lhs, - const std::array& rhs, - std::array& out) { - multiply2x2(ArrayRef(lhs), ArrayRef(rhs), MutableArrayRef(out)); -} - /// Writes the row-major product `lhs * rhs` into @p out (4x4, unrolled rows). static void multiply4x4(const ArrayRef lhs, const ArrayRef rhs, @@ -127,13 +120,6 @@ static void multiply4x4(const ArrayRef lhs, } } -static void -multiply4x4(const std::array& lhs, - const std::array& rhs, - std::array& out) { - multiply4x4(ArrayRef(lhs), ArrayRef(rhs), MutableArrayRef(out)); -} - /// Returns true if @p data is approximately the @p dim x @p dim identity /// matrix. [[nodiscard]] static bool isIdentityEntries(ArrayRef data, @@ -193,10 +179,41 @@ static void copyBottomRightCorner(const std::int64_t matrixDim, } } -struct DynamicMatrix::Impl { - std::int64_t dim = 0; - SmallVector data; -}; +/** + * @brief Returns the @p qubitIndex bit of a computational-basis label. + * + * Qubit 0 is the MSB of @p stateIndex, matching @ref Matrix4x4::kron and + * @ref Matrix2x2::embedInNqubit. + */ +[[nodiscard]] static std::size_t qubitBitAt(const std::size_t stateIndex, + const std::size_t numQubits, + const std::size_t qubitIndex) { + return (stateIndex >> (numQubits - 1 - qubitIndex)) & 1U; +} + +/** + * @brief True when row and col agree on every wire except @p skipA and @p + * skipB. + * + * Used when embedding a gate: untouched qubits must match or the matrix entry + * is zero. For a single-qubit embed, pass @p skipB = @p numQubits so only @p + * skipA is skipped. + */ +[[nodiscard]] static bool otherQubitBitsMatch(const std::size_t row, + const std::size_t col, + const std::size_t numQubits, + const std::size_t skipA, + const std::size_t skipB) { + for (std::size_t q = 0; q < numQubits; ++q) { + if (q == skipA || q == skipB) { + continue; + } + if (qubitBitAt(row, numQubits, q) != qubitBitAt(col, numQubits, q)) { + return false; + } + } + return true; +} Matrix1x1 Matrix1x1::fromElements(const Complex m00) { return {m00}; } @@ -299,6 +316,40 @@ bool Matrix2x2::assignFrom(const DynamicMatrix& src) { return assignFromDynamicImpl(src, data); } +DynamicMatrix Matrix2x2::embedInNqubit(const std::size_t numQubits, + const std::size_t qubitIndex) const { + assert(qubitIndex < numQubits && + "Invalid qubit index for single-qubit embed"); + if (numQubits == 2) { + return DynamicMatrix(embedInTwoQubit(qubitIndex)); + } + const auto dim = static_cast(1ULL << numQubits); + DynamicMatrix out(dim); + const auto udim = static_cast(dim); + for (std::size_t row = 0; row < udim; ++row) { + for (std::size_t col = 0; col < udim; ++col) { + if (!otherQubitBitsMatch(row, col, numQubits, qubitIndex, numQubits)) { + continue; + } + const std::size_t rowBit = qubitBitAt(row, numQubits, qubitIndex); + const std::size_t colBit = qubitBitAt(col, numQubits, qubitIndex); + out(static_cast(row), static_cast(col)) = + (*this)(rowBit, colBit); + } + } + return out; +} + +Matrix4x4 Matrix2x2::embedInTwoQubit(const std::size_t qubitIndex) const { + if (qubitIndex == 0) { + return Matrix4x4::kron(*this, Matrix2x2::identity()); + } + if (qubitIndex == 1) { + return Matrix4x4::kron(Matrix2x2::identity(), *this); + } + assert(false && "Invalid qubit index for single-qubit embed"); +} + Matrix4x4 Matrix4x4::fromElements(const Complex& m00, const Complex& m01, const Complex& m02, const Complex& m03, const Complex& m10, const Complex& m11, @@ -385,8 +436,9 @@ std::array Matrix4x4::diagonal() const { return {data[0], data[5], data[10], data[15]}; } -Matrix4x4 -Matrix4x4::fromDiagonal(const std::array& diagonalEntries) { +Matrix4x4 Matrix4x4::fromDiagonal(const ArrayRef diagonalEntries) { + assert(diagonalEntries.size() == K_ROWS && + "fromDiagonal requires exactly K_ROWS entries"); Matrix4x4 out{}; for (std::size_t i = 0; i < K_ROWS; ++i) { out.data[(i * K_COLS) + i] = diagonalEntries[i]; @@ -394,6 +446,15 @@ Matrix4x4::fromDiagonal(const std::array& diagonalEntries) { return out; } +Matrix4x4 Matrix4x4::kron(const Matrix2x2& lhs, const Matrix2x2& rhs) { + const auto& a = lhs.data; + const auto& b = rhs.data; + return fromElements(a[0] * b[0], a[0] * b[1], a[1] * b[0], a[1] * b[1], + a[0] * b[2], a[0] * b[3], a[1] * b[2], a[1] * b[3], + a[2] * b[0], a[2] * b[1], a[3] * b[0], a[3] * b[1], + a[2] * b[2], a[2] * b[3], a[3] * b[2], a[3] * b[3]); +} + std::array Matrix4x4::column(const std::size_t col) const { return {data[col], data[K_COLS + col], data[(2 * K_COLS) + col], @@ -401,12 +462,27 @@ Matrix4x4::column(const std::size_t col) const { } void Matrix4x4::setColumn(const std::size_t col, - const std::array& values) { + const ArrayRef values) { + assert(values.size() == K_ROWS && + "setColumn requires exactly K_ROWS entries"); for (std::size_t row = 0; row < K_ROWS; ++row) { data[(row * K_COLS) + col] = values[row]; } } +ArrayRef Matrix4x4::row(const std::size_t row) const { + assert(row < K_ROWS); + return ArrayRef(data).slice(row * K_COLS, K_COLS); +} + +void Matrix4x4::setRow(const std::size_t row, const ArrayRef values) { + assert(row < K_ROWS); + assert(values.size() == K_COLS && "setRow requires exactly K_COLS entries"); + for (std::size_t col = 0; col < K_COLS; ++col) { + data[(row * K_COLS) + col] = values[col]; + } +} + std::array Matrix4x4::realPart() const { return {data[0].real(), data[1].real(), data[2].real(), data[3].real(), @@ -431,366 +507,171 @@ bool Matrix4x4::assignFrom(const DynamicMatrix& src) { return assignFromDynamicImpl(src, data); } -DynamicMatrix::DynamicMatrix() : impl_(std::make_unique()) {} - -DynamicMatrix::DynamicMatrix(const std::int64_t dim) - : impl_(std::make_unique()) { - impl_->dim = dim; - impl_->data.assign(checkedStorageSize(dim), Complex{}); +DynamicMatrix Matrix4x4::embedInNqubit(const std::size_t numQubits, + const std::size_t q0Index, + const std::size_t q1Index) const { + assert(q0Index < numQubits && q1Index < numQubits && q0Index != q1Index && + "Invalid qubit indices for two-qubit embed"); + if (numQubits == 2) { + return DynamicMatrix(reorderForQubits(q0Index, q1Index)); + } + const auto dim = static_cast(1ULL << numQubits); + DynamicMatrix out(dim); + const auto udim = static_cast(dim); + for (std::size_t row = 0; row < udim; ++row) { + for (std::size_t col = 0; col < udim; ++col) { + if (!otherQubitBitsMatch(row, col, numQubits, q0Index, q1Index)) { + continue; + } + const std::size_t rowPair = (qubitBitAt(row, numQubits, q0Index) << 1) | + qubitBitAt(row, numQubits, q1Index); + const std::size_t colPair = (qubitBitAt(col, numQubits, q0Index) << 1) | + qubitBitAt(col, numQubits, q1Index); + out(static_cast(row), static_cast(col)) = + (*this)(rowPair, colPair); + } + } + return out; } -DynamicMatrix::DynamicMatrix(const Matrix2x2& src) - : impl_(std::make_unique()) { - assignFrom(src); +Matrix4x4 Matrix4x4::reorderForQubits(const std::size_t q0Index, + const std::size_t q1Index) const { + if (q0Index == 0 && q1Index == 1) { + return *this; + } + if (q0Index == 1 && q1Index == 0) { + // Conjugate by SWAP: out[i, j] = matrix[pi(i), pi(j)] with pi swapping |01> + // and |10> (basis indices 1 and 2). + const auto& m = data; + return fromElements(m[0], m[2], m[1], m[3], m[8], m[10], m[9], m[11], m[4], + m[6], m[5], m[7], m[12], m[14], m[13], m[15]); + } + assert(false && "Invalid qubit indices for two-qubit reorder"); } -DynamicMatrix::DynamicMatrix(const Matrix4x4& src) - : impl_(std::make_unique()) { - assignFrom(src); +SymmetricEigen4 Matrix4x4::symmetricEigen4() const { + return symmetricEigen4(realPart()); } -DynamicMatrix::DynamicMatrix(const DynamicMatrix& other) - : impl_(std::make_unique(*other.impl_)) {} +// Adapted from John Burkardt's MIT-licensed EISPACK C port (`tred2` / `tql2`): +// https://people.sc.fsu.edu/~jburkardt/c_src/eispack/eispack.c +// Specialized to `n = 4`; input is row-major, accumulator `z` is column-major. -DynamicMatrix::DynamicMatrix(DynamicMatrix&& other) noexcept = default; +/// EISPACK `tred2` for `n = 4` (column-major `z[row + col*n]`). +static void symmetricTred24(const std::array& input, + std::array& z, + std::array& diag, + std::array& subdiag) { + constexpr std::size_t n = 4; + const auto zAt = [&z](const std::size_t row, + const std::size_t col) -> double& { + return z[row + (col * n)]; + }; + double h = 0.0; -DynamicMatrix& DynamicMatrix::operator=(const DynamicMatrix& other) { - if (this != &other) { - *impl_ = *other.impl_; + for (std::size_t col = 0; col < n; ++col) { + for (std::size_t row = col; row < n; ++row) { + zAt(row, col) = input[(row * n) + col]; + } + diag[col] = input[((n - 1) * n) + col]; } - return *this; -} - -DynamicMatrix& -DynamicMatrix::operator=(DynamicMatrix&& other) noexcept = default; - -DynamicMatrix::~DynamicMatrix() = default; -std::int64_t DynamicMatrix::rows() const { return impl_->dim; } - -std::int64_t DynamicMatrix::cols() const { return impl_->dim; } + for (int i = static_cast(n) - 1; i >= 1; --i) { + const auto ui = static_cast(i); + const std::size_t l = ui - 1; + h = 0.0; + double scale = 0.0; + for (std::size_t k = 0; k <= l; ++k) { + scale += std::abs(diag[k]); + } + if (scale == 0.0) { + subdiag[ui] = diag[l]; + for (std::size_t j = 0; j <= l; ++j) { + diag[j] = zAt(l, j); + zAt(ui, j) = 0.0; + zAt(j, ui) = 0.0; + } + diag[ui] = 0.0; + continue; + } + for (std::size_t k = 0; k <= l; ++k) { + diag[k] /= scale; + } + for (std::size_t k = 0; k <= l; ++k) { + h += diag[k] * diag[k]; + } + const double f = diag[l]; + const double g = -std::sqrt(h) * std::copysign(1.0, f); + subdiag[ui] = scale * g; + h -= f * g; + diag[l] = f - g; -DynamicMatrix DynamicMatrix::identity(const std::int64_t dim) { - DynamicMatrix matrix(dim); - const auto udim = checkedDim(dim); - for (std::size_t i = 0; i < udim; ++i) { - matrix.impl_->data[(i * udim) + i] = 1.0; + for (std::size_t k = 0; k <= l; ++k) { + subdiag[k] = 0.0; + } + for (std::size_t j = 0; j <= l; ++j) { + const double fj = diag[j]; + zAt(j, ui) = fj; + double gj = subdiag[j] + (zAt(j, j) * fj); + for (std::size_t k = j + 1; k <= l; ++k) { + gj += zAt(k, j) * diag[k]; + subdiag[k] += zAt(k, j) * fj; + } + subdiag[j] = gj; + } + double ff = 0.0; + for (std::size_t k = 0; k <= l; ++k) { + subdiag[k] /= h; + ff += subdiag[k] * diag[k]; + } + const double hh = 0.5 * ff / h; + for (std::size_t k = 0; k <= l; ++k) { + subdiag[k] -= hh * diag[k]; + } + for (std::size_t j = 0; j <= l; ++j) { + const double fj = diag[j]; + const double gj = subdiag[j]; + for (std::size_t k = j; k <= l; ++k) { + zAt(k, j) -= (fj * subdiag[k]) + (gj * diag[k]); + } + diag[j] = zAt(l, j); + zAt(ui, j) = 0.0; + } + diag[ui] = h; } - return matrix; -} -DynamicMatrix DynamicMatrix::fromAdjoint(const Matrix2x2& src) { - return DynamicMatrix(src.adjoint()); -} + for (std::size_t i = 1; i < n; ++i) { + const std::size_t l = i - 1; + zAt(n - 1, l) = zAt(l, l); + zAt(l, l) = 1.0; + h = diag[i]; + if (h != 0.0) { + for (std::size_t k = 0; k <= l; ++k) { + diag[k] = zAt(k, i) / h; + } + for (std::size_t j = 0; j <= l; ++j) { + double g = 0.0; + for (std::size_t k = 0; k <= l; ++k) { + g += zAt(k, i) * zAt(k, j); + } + for (std::size_t k = 0; k <= l; ++k) { + zAt(k, j) -= g * diag[k]; + } + } + } + for (std::size_t k = 0; k <= l; ++k) { + zAt(k, i) = 0.0; + } + } -Complex& DynamicMatrix::operator()(const std::int64_t row, - const std::int64_t col) { - return impl_->data[static_cast((row * impl_->dim) + col)]; -} - -Complex DynamicMatrix::operator()(const std::int64_t row, - const std::int64_t col) const { - return impl_->data[static_cast((row * impl_->dim) + col)]; -} - -void DynamicMatrix::setBottomRightCorner(const Matrix2x2& block) { - copyBottomRightCorner(impl_->dim, impl_->data, - static_cast(Matrix2x2::K_ROWS), - block.data); -} - -void DynamicMatrix::setBottomRightCorner(const Matrix4x4& block) { - copyBottomRightCorner(impl_->dim, impl_->data, - static_cast(Matrix4x4::K_ROWS), - block.data); -} - -void DynamicMatrix::setBottomRightCorner(const DynamicMatrix& block) { - copyBottomRightCorner(impl_->dim, impl_->data, block.impl_->dim, - block.impl_->data); -} - -DynamicMatrix DynamicMatrix::adjoint() const { - DynamicMatrix out(impl_->dim); - adjointInto(impl_->data, out.impl_->data, checkedDim(impl_->dim)); - return out; -} - -void DynamicMatrix::assignFrom(const Matrix1x1& src) { - impl_->dim = 1; - impl_->data.assign({src.value}); -} - -void DynamicMatrix::assignFrom(const Matrix2x2& src) { - assignFixedImpl( - impl_->dim, impl_->data, src.data); -} - -void DynamicMatrix::assignFrom(const Matrix4x4& src) { - assignFixedImpl( - impl_->dim, impl_->data, src.data); -} - -void DynamicMatrix::assignFrom(const DynamicMatrix& src) { - *impl_ = *src.impl_; -} - -bool DynamicMatrix::isApprox(const Matrix1x1& other, const double tol) const { - if (impl_->dim != 1) { - return false; - } - return entryIsApprox(impl_->data[0], other.value, tol); -} - -bool DynamicMatrix::isApprox(const Matrix2x2& other, const double tol) const { - return isApproxFixedImpl( - impl_->dim, impl_->data, other.data, tol); -} - -bool DynamicMatrix::isApprox(const Matrix4x4& other, const double tol) const { - return isApproxFixedImpl( - impl_->dim, impl_->data, other.data, tol); -} - -bool DynamicMatrix::isApprox(const DynamicMatrix& other, - const double tol) const { - return entriesAreApprox(impl_->data, other.impl_->data, tol); -} - -Complex DynamicMatrix::trace() const { - Complex sum{0.0, 0.0}; - const auto udim = checkedDim(impl_->dim); - for (std::size_t i = 0; i < udim; ++i) { - sum += impl_->data[(i * udim) + i]; - } - return sum; -} - -DynamicMatrix DynamicMatrix::operator*(const DynamicMatrix& rhs) const { - assert(impl_->dim == rhs.impl_->dim && - "DynamicMatrix multiply requires matching dimensions"); - DynamicMatrix out(impl_->dim); - if (std::cmp_equal(impl_->dim, Matrix2x2::K_ROWS)) { - multiply2x2(impl_->data, rhs.impl_->data, out.impl_->data); - return out; - } - if (std::cmp_equal(impl_->dim, Matrix4x4::K_ROWS)) { - multiply4x4(impl_->data, rhs.impl_->data, out.impl_->data); - return out; - } - - const auto udim = checkedDim(impl_->dim); - for (std::size_t row = 0; row < udim; ++row) { - for (std::size_t col = 0; col < udim; ++col) { - Complex sum{0.0, 0.0}; - for (std::size_t k = 0; k < udim; ++k) { - sum += - impl_->data[(row * udim) + k] * rhs.impl_->data[(k * udim) + col]; - } - out.impl_->data[(row * udim) + col] = sum; - } - } - return out; -} - -DynamicMatrix DynamicMatrix::operator*(const Complex& scalar) const { - DynamicMatrix out(impl_->dim); - for (std::size_t i = 0; i < impl_->data.size(); ++i) { - out.impl_->data[i] = impl_->data[i] * scalar; - } - return out; -} - -DynamicMatrix& DynamicMatrix::operator*=(const Complex& scalar) { - for (Complex& entry : impl_->data) { - entry *= scalar; - } - return *this; -} - -bool DynamicMatrix::isIdentity(const double tol) const { - return isIdentityEntries(impl_->data, checkedDim(impl_->dim), tol); -} - -/** - * @brief Returns the @p qubitIndex bit of a computational-basis label. - * - * Qubit 0 is the MSB of @p stateIndex, matching @ref kron and - * @ref embedSingleQubitInNqubit. - */ -[[nodiscard]] static std::size_t qubitBitAt(const std::size_t stateIndex, - const std::size_t numQubits, - const std::size_t qubitIndex) { - return (stateIndex >> (numQubits - 1 - qubitIndex)) & 1U; -} - -/** - * @brief True when row and col agree on every wire except @p skipA and @p - * skipB. - * - * Used when embedding a gate: untouched qubits must match or the matrix entry - * is zero. For a single-qubit embed, pass @p skipB = @p numQubits so only @p - * skipA is skipped. - */ -[[nodiscard]] static bool otherQubitBitsMatch(const std::size_t row, - const std::size_t col, - const std::size_t numQubits, - const std::size_t skipA, - const std::size_t skipB) { - for (std::size_t q = 0; q < numQubits; ++q) { - if (q == skipA || q == skipB) { - continue; - } - if (qubitBitAt(row, numQubits, q) != qubitBitAt(col, numQubits, q)) { - return false; - } - } - return true; -} - -Matrix4x4 kron(const Matrix2x2& lhs, const Matrix2x2& rhs) { - const auto& a = lhs.data; - const auto& b = rhs.data; - return Matrix4x4::fromElements( - a[0] * b[0], a[0] * b[1], a[1] * b[0], a[1] * b[1], a[0] * b[2], - a[0] * b[3], a[1] * b[2], a[1] * b[3], a[2] * b[0], a[2] * b[1], - a[3] * b[0], a[3] * b[1], a[2] * b[2], a[2] * b[3], a[3] * b[2], - a[3] * b[3]); -} - -Matrix2x2 operator*(const Complex& scalar, const Matrix2x2& matrix) { - return matrix * scalar; -} - -Matrix4x4 operator*(const Complex& scalar, const Matrix4x4& matrix) { - return matrix * scalar; -} - -DynamicMatrix operator*(const Complex& scalar, const DynamicMatrix& matrix) { - return matrix * scalar; -} - -// Adapted from John Burkardt's MIT-licensed EISPACK C port (`tred2` / `tql2`): -// https://people.sc.fsu.edu/~jburkardt/c_src/eispack/eispack.c -// Specialized to `n = 4`; input is row-major, accumulator `z` is column-major. - -/// EISPACK `tred2` for `n = 4` (column-major `z[row + col*n]`). -static void symmetricTred24(const std::array& input, - std::array& z, - std::array& diag, - std::array& subdiag) { - constexpr std::size_t n = 4; - const auto zAt = [&z](const std::size_t row, - const std::size_t col) -> double& { - return z[row + (col * n)]; - }; - double h = 0.0; - - for (std::size_t col = 0; col < n; ++col) { - for (std::size_t row = col; row < n; ++row) { - zAt(row, col) = input[(row * n) + col]; - } - diag[col] = input[((n - 1) * n) + col]; - } - - for (int i = static_cast(n) - 1; i >= 1; --i) { - const auto ui = static_cast(i); - const std::size_t l = ui - 1; - h = 0.0; - double scale = 0.0; - for (std::size_t k = 0; k <= l; ++k) { - scale += std::abs(diag[k]); - } - if (scale == 0.0) { - subdiag[ui] = diag[l]; - for (std::size_t j = 0; j <= l; ++j) { - diag[j] = zAt(l, j); - zAt(ui, j) = 0.0; - zAt(j, ui) = 0.0; - } - diag[ui] = 0.0; - continue; - } - for (std::size_t k = 0; k <= l; ++k) { - diag[k] /= scale; - } - for (std::size_t k = 0; k <= l; ++k) { - h += diag[k] * diag[k]; - } - const double f = diag[l]; - const double g = -std::sqrt(h) * std::copysign(1.0, f); - subdiag[ui] = scale * g; - h -= f * g; - diag[l] = f - g; - - for (std::size_t k = 0; k <= l; ++k) { - subdiag[k] = 0.0; - } - for (std::size_t j = 0; j <= l; ++j) { - const double fj = diag[j]; - zAt(j, ui) = fj; - double gj = subdiag[j] + (zAt(j, j) * fj); - for (std::size_t k = j + 1; k <= l; ++k) { - gj += zAt(k, j) * diag[k]; - subdiag[k] += zAt(k, j) * fj; - } - subdiag[j] = gj; - } - double ff = 0.0; - for (std::size_t k = 0; k <= l; ++k) { - subdiag[k] /= h; - ff += subdiag[k] * diag[k]; - } - const double hh = 0.5 * ff / h; - for (std::size_t k = 0; k <= l; ++k) { - subdiag[k] -= hh * diag[k]; - } - for (std::size_t j = 0; j <= l; ++j) { - const double fj = diag[j]; - const double gj = subdiag[j]; - for (std::size_t k = j; k <= l; ++k) { - zAt(k, j) -= (fj * subdiag[k]) + (gj * diag[k]); - } - diag[j] = zAt(l, j); - zAt(ui, j) = 0.0; - } - diag[ui] = h; - } - - for (std::size_t i = 1; i < n; ++i) { - const std::size_t l = i - 1; - zAt(n - 1, l) = zAt(l, l); - zAt(l, l) = 1.0; - h = diag[i]; - if (h != 0.0) { - for (std::size_t k = 0; k <= l; ++k) { - diag[k] = zAt(k, i) / h; - } - for (std::size_t j = 0; j <= l; ++j) { - double g = 0.0; - for (std::size_t k = 0; k <= l; ++k) { - g += zAt(k, i) * zAt(k, j); - } - for (std::size_t k = 0; k <= l; ++k) { - zAt(k, j) -= g * diag[k]; - } - } - } - for (std::size_t k = 0; k <= l; ++k) { - zAt(k, i) = 0.0; - } - } - - for (std::size_t j = 0; j < n; ++j) { - diag[j] = zAt(n - 1, j); - } - for (std::size_t j = 0; j < n - 1; ++j) { - zAt(n - 1, j) = 0.0; - } - zAt(n - 1, n - 1) = 1.0; - subdiag[0] = 0.0; + for (std::size_t j = 0; j < n; ++j) { + diag[j] = zAt(n - 1, j); + } + for (std::size_t j = 0; j < n - 1; ++j) { + zAt(n - 1, j) = 0.0; + } + zAt(n - 1, n - 1) = 1.0; + subdiag[0] = 0.0; } /// EISPACK `tql2` for `n = 4` (column-major `z[row + col*n]`). @@ -907,18 +788,17 @@ static void symmetricTql24(std::array& diag, } } -SymmetricEigen4 symmetricEigen4(const std::array& symmetric) { +SymmetricEigen4 +Matrix4x4::symmetricEigen4(const std::array& symmetric) { constexpr std::size_t n = 4; + SymmetricEigen4 result; std::array z{}; - std::array diag{}; std::array subdiag{}; - symmetricTred24(symmetric, z, diag, subdiag); - symmetricTql24(diag, subdiag, z); + symmetricTred24(symmetric, z, result.eigenvalues, subdiag); + symmetricTql24(result.eigenvalues, subdiag, z); - SymmetricEigen4 result; for (std::size_t col = 0; col < n; ++col) { - result.eigenvalues[col] = diag[col]; for (std::size_t row = 0; row < n; ++row) { result.eigenvectors(row, col) = Complex{z[row + (col * n)], 0.0}; } @@ -926,89 +806,204 @@ SymmetricEigen4 symmetricEigen4(const std::array& symmetric) { return result; } -SymmetricEigen4 symmetricEigen4(const Matrix4x4& symmetric) { - return symmetricEigen4(symmetric.realPart()); +struct DynamicMatrix::Impl { + std::int64_t dim = 0; + SmallVector data; +}; + +DynamicMatrix::DynamicMatrix() : impl_(std::make_unique()) {} + +DynamicMatrix::DynamicMatrix(const std::int64_t dim) + : impl_(std::make_unique()) { + impl_->dim = dim; + impl_->data.assign(checkedStorageSize(dim), Complex{}); +} + +DynamicMatrix::DynamicMatrix(const Matrix2x2& src) + : impl_(std::make_unique()) { + assignFrom(src); +} + +DynamicMatrix::DynamicMatrix(const Matrix4x4& src) + : impl_(std::make_unique()) { + assignFrom(src); } -static Matrix4x4 embedSingleQubitInTwoQubit(const Matrix2x2& matrix, - const std::size_t qubitIndex) { - if (qubitIndex == 0) { - return kron(matrix, Matrix2x2::identity()); +DynamicMatrix::DynamicMatrix(const DynamicMatrix& other) + : impl_(std::make_unique(*other.impl_)) {} + +DynamicMatrix::DynamicMatrix(DynamicMatrix&& other) noexcept = default; + +DynamicMatrix& DynamicMatrix::operator=(const DynamicMatrix& other) { + if (this != &other) { + *impl_ = *other.impl_; } - if (qubitIndex == 1) { - return kron(Matrix2x2::identity(), matrix); + return *this; +} + +DynamicMatrix& +DynamicMatrix::operator=(DynamicMatrix&& other) noexcept = default; + +DynamicMatrix::~DynamicMatrix() = default; + +std::int64_t DynamicMatrix::rows() const { return impl_->dim; } + +std::int64_t DynamicMatrix::cols() const { return impl_->dim; } + +DynamicMatrix DynamicMatrix::identity(const std::int64_t dim) { + DynamicMatrix matrix(dim); + const auto udim = checkedDim(dim); + for (std::size_t i = 0; i < udim; ++i) { + matrix.impl_->data[(i * udim) + i] = 1.0; } - assert(false && "Invalid qubit index for single-qubit embed"); + return matrix; } -Matrix4x4 reorderTwoQubitMatrix(const Matrix4x4& matrix, - const std::size_t q0Index, - const std::size_t q1Index) { - if (q0Index == 0 && q1Index == 1) { - return matrix; +DynamicMatrix DynamicMatrix::fromAdjoint(const Matrix2x2& src) { + return DynamicMatrix(src.adjoint()); +} + +Complex& DynamicMatrix::operator()(const std::int64_t row, + const std::int64_t col) { + return impl_->data[static_cast((row * impl_->dim) + col)]; +} + +Complex DynamicMatrix::operator()(const std::int64_t row, + const std::int64_t col) const { + return impl_->data[static_cast((row * impl_->dim) + col)]; +} + +void DynamicMatrix::setBottomRightCorner(const Matrix2x2& block) { + copyBottomRightCorner(impl_->dim, impl_->data, + static_cast(Matrix2x2::K_ROWS), + block.data); +} + +void DynamicMatrix::setBottomRightCorner(const Matrix4x4& block) { + copyBottomRightCorner(impl_->dim, impl_->data, + static_cast(Matrix4x4::K_ROWS), + block.data); +} + +void DynamicMatrix::setBottomRightCorner(const DynamicMatrix& block) { + copyBottomRightCorner(impl_->dim, impl_->data, block.impl_->dim, + block.impl_->data); +} + +DynamicMatrix DynamicMatrix::adjoint() const { + DynamicMatrix out(impl_->dim); + adjointInto(impl_->data, out.impl_->data, checkedDim(impl_->dim)); + return out; +} + +void DynamicMatrix::assignFrom(const Matrix1x1& src) { + impl_->dim = 1; + impl_->data.assign({src.value}); +} + +void DynamicMatrix::assignFrom(const Matrix2x2& src) { + assignFixedImpl( + impl_->dim, impl_->data, src.data); +} + +void DynamicMatrix::assignFrom(const Matrix4x4& src) { + assignFixedImpl( + impl_->dim, impl_->data, src.data); +} + +void DynamicMatrix::assignFrom(const DynamicMatrix& src) { + *impl_ = *src.impl_; +} + +bool DynamicMatrix::isApprox(const Matrix1x1& other, const double tol) const { + if (impl_->dim != 1) { + return false; } - if (q0Index == 1 && q1Index == 0) { - // Conjugate by SWAP: out[i, j] = matrix[pi(i), pi(j)] with pi swapping |01> - // and |10> (basis indices 1 and 2). - const auto& m = matrix.data; - return Matrix4x4::fromElements(m[0], m[2], m[1], m[3], m[8], m[10], m[9], - m[11], m[4], m[6], m[5], m[7], m[12], m[14], - m[13], m[15]); + return entryIsApprox(impl_->data[0], other.value, tol); +} + +bool DynamicMatrix::isApprox(const Matrix2x2& other, const double tol) const { + return isApproxFixedImpl( + impl_->dim, impl_->data, other.data, tol); +} + +bool DynamicMatrix::isApprox(const Matrix4x4& other, const double tol) const { + return isApproxFixedImpl( + impl_->dim, impl_->data, other.data, tol); +} + +bool DynamicMatrix::isApprox(const DynamicMatrix& other, + const double tol) const { + return entriesAreApprox(impl_->data, other.impl_->data, tol); +} + +Complex DynamicMatrix::trace() const { + Complex sum{0.0, 0.0}; + const auto udim = checkedDim(impl_->dim); + for (std::size_t i = 0; i < udim; ++i) { + sum += impl_->data[(i * udim) + i]; } - assert(false && "Invalid qubit indices for two-qubit reorder"); + return sum; } -DynamicMatrix embedSingleQubitInNqubit(const Matrix2x2& matrix, - const std::size_t numQubits, - const std::size_t qubitIndex) { - assert(qubitIndex < numQubits && - "Invalid qubit index for single-qubit embed"); - if (numQubits == 2) { - return DynamicMatrix(embedSingleQubitInTwoQubit(matrix, qubitIndex)); +DynamicMatrix DynamicMatrix::operator*(const DynamicMatrix& rhs) const { + assert(impl_->dim == rhs.impl_->dim && + "DynamicMatrix multiply requires matching dimensions"); + DynamicMatrix out(impl_->dim); + if (std::cmp_equal(impl_->dim, Matrix2x2::K_ROWS)) { + multiply2x2(impl_->data, rhs.impl_->data, out.impl_->data); + return out; } - const auto dim = static_cast(1ULL << numQubits); - DynamicMatrix out(dim); - const auto udim = static_cast(dim); + if (std::cmp_equal(impl_->dim, Matrix4x4::K_ROWS)) { + multiply4x4(impl_->data, rhs.impl_->data, out.impl_->data); + return out; + } + + const auto udim = checkedDim(impl_->dim); for (std::size_t row = 0; row < udim; ++row) { for (std::size_t col = 0; col < udim; ++col) { - if (!otherQubitBitsMatch(row, col, numQubits, qubitIndex, numQubits)) { - continue; + Complex sum{0.0, 0.0}; + for (std::size_t k = 0; k < udim; ++k) { + sum += + impl_->data[(row * udim) + k] * rhs.impl_->data[(k * udim) + col]; } - const std::size_t rowBit = qubitBitAt(row, numQubits, qubitIndex); - const std::size_t colBit = qubitBitAt(col, numQubits, qubitIndex); - out(static_cast(row), static_cast(col)) = - matrix(rowBit, colBit); + out.impl_->data[(row * udim) + col] = sum; } } return out; } -DynamicMatrix embedTwoQubitInNqubit(const Matrix4x4& matrix, - const std::size_t numQubits, - const std::size_t q0Index, - const std::size_t q1Index) { - assert(q0Index < numQubits && q1Index < numQubits && q0Index != q1Index && - "Invalid qubit indices for two-qubit embed"); - if (numQubits == 2) { - return DynamicMatrix(reorderTwoQubitMatrix(matrix, q0Index, q1Index)); - } - const auto dim = static_cast(1ULL << numQubits); - DynamicMatrix out(dim); - const auto udim = static_cast(dim); - for (std::size_t row = 0; row < udim; ++row) { - for (std::size_t col = 0; col < udim; ++col) { - if (!otherQubitBitsMatch(row, col, numQubits, q0Index, q1Index)) { - continue; - } - const std::size_t rowPair = (qubitBitAt(row, numQubits, q0Index) << 1) | - qubitBitAt(row, numQubits, q1Index); - const std::size_t colPair = (qubitBitAt(col, numQubits, q0Index) << 1) | - qubitBitAt(col, numQubits, q1Index); - out(static_cast(row), static_cast(col)) = - matrix(rowPair, colPair); - } +DynamicMatrix DynamicMatrix::operator*(const Complex& scalar) const { + DynamicMatrix out(impl_->dim); + for (std::size_t i = 0; i < impl_->data.size(); ++i) { + out.impl_->data[i] = impl_->data[i] * scalar; } return out; } +DynamicMatrix& DynamicMatrix::operator*=(const Complex& scalar) { + for (Complex& entry : impl_->data) { + entry *= scalar; + } + return *this; +} + +bool DynamicMatrix::isIdentity(const double tol) const { + return isIdentityEntries(impl_->data, checkedDim(impl_->dim), tol); +} + +Matrix2x2 operator*(const Complex& scalar, const Matrix2x2& matrix) { + return matrix * scalar; +} + +Matrix4x4 operator*(const Complex& scalar, const Matrix4x4& matrix) { + return matrix * scalar; +} + +DynamicMatrix operator*(const Complex& scalar, const DynamicMatrix& matrix) { + return matrix * scalar; +} + } // namespace mlir::qco diff --git a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp index 946dc1956e..c23426918b 100644 --- a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp +++ b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp @@ -370,7 +370,7 @@ TEST(UnitaryMatrix4x4, TransposeAndIsIdentity) { EXPECT_FALSE(swapMatrix().isIdentity()); } -TEST(UnitaryMatrix4x4, DiagonalColumnsAndParts) { +TEST(UnitaryMatrix4x4, DiagonalRowsColumnsAndParts) { Matrix4x4 m = Matrix4x4::fromElements(Complex{1, 1}, 0, 0, 0, 0, Complex{2, 2}, 0, 0, 0, 0, Complex{3, 3}, 0, 0, 0, 0, Complex{4, 4}); @@ -386,6 +386,19 @@ TEST(UnitaryMatrix4x4, DiagonalColumnsAndParts) { EXPECT_EQ(n(0, 2), 1i); EXPECT_EQ(n(3, 2), 4i); + const auto row1 = m.row(1); + ASSERT_EQ(row1.size(), Matrix4x4::K_COLS); + EXPECT_EQ(row1[0], (Complex{0, 0})); + EXPECT_EQ(row1[1], (Complex{2, 2})); + EXPECT_EQ(row1[0], m(1, 0)); + EXPECT_EQ(row1[3], m(1, 3)); + + Matrix4x4 r = Matrix4x4::identity(); + r.setRow(2, {1.0, 2.0, 3.0, 4.0}); + EXPECT_EQ(r(2, 0), 1.0); + EXPECT_EQ(r(2, 3), 4.0); + EXPECT_EQ(r.row(2)[1], 2.0); + const auto re = m.realPart(); const auto im = m.imagPart(); EXPECT_EQ(re[0], 1.0); @@ -397,13 +410,13 @@ TEST(UnitaryMatrix4x4, DiagonalColumnsAndParts) { TEST(UnitaryMatrix4x4, KroneckerProduct) { const Matrix2x2 x = pauliX(); // X (x) I should swap the high bit. - const Matrix4x4 xi = kron(x, Matrix2x2::identity()); + const Matrix4x4 xi = Matrix4x4::kron(x, Matrix2x2::identity()); EXPECT_TRUE(xi.isApprox(Matrix4x4::fromElements(0, 0, 1, 0, // row 0 0, 0, 0, 1, // row 1 1, 0, 0, 0, // row 2 0, 1, 0, 0))); // I (x) X swaps the low bit. - const Matrix4x4 ix = kron(Matrix2x2::identity(), x); + const Matrix4x4 ix = Matrix4x4::kron(Matrix2x2::identity(), x); EXPECT_TRUE(ix.isApprox(Matrix4x4::fromElements(0, 1, 0, 0, // row 0 1, 0, 0, 0, // row 1 0, 0, 0, 1, // row 2 @@ -412,12 +425,12 @@ TEST(UnitaryMatrix4x4, KroneckerProduct) { TEST(UnitaryMatrix4x4, ReorderTwoQubitMatrix) { const Matrix2x2 x = pauliX(); - const Matrix4x4 onHigh = kron(x, Matrix2x2::identity()); - const Matrix4x4 onLow = kron(Matrix2x2::identity(), x); + const Matrix4x4 onHigh = Matrix4x4::kron(x, Matrix2x2::identity()); + const Matrix4x4 onLow = Matrix4x4::kron(Matrix2x2::identity(), x); - EXPECT_TRUE(reorderTwoQubitMatrix(onHigh, 0, 1).isApprox(onHigh)); - EXPECT_TRUE(reorderTwoQubitMatrix(onHigh, 1, 0).isApprox(onLow)); - EXPECT_TRUE(reorderTwoQubitMatrix(onLow, 1, 0).isApprox(onHigh)); + EXPECT_TRUE(onHigh.reorderForQubits(0, 1).isApprox(onHigh)); + EXPECT_TRUE(onHigh.reorderForQubits(1, 0).isApprox(onLow)); + EXPECT_TRUE(onLow.reorderForQubits(1, 0).isApprox(onHigh)); } TEST(UnitaryDynamicMatrix, NQubitEmbedMatchesTwoQubitSpecialization) { @@ -426,14 +439,13 @@ TEST(UnitaryDynamicMatrix, NQubitEmbedMatchesTwoQubitSpecialization) { 0, 1, 0, 0, // 0, 0, 0, 1, // 0, 0, 1, 0); - EXPECT_TRUE(embedSingleQubitInNqubit(x, 2, 0).isApprox( - kron(x, Matrix2x2::identity()))); - EXPECT_TRUE(embedSingleQubitInNqubit(x, 2, 1).isApprox( - kron(Matrix2x2::identity(), x))); - EXPECT_TRUE(embedTwoQubitInNqubit(cx, 2, 0, 1) - .isApprox(reorderTwoQubitMatrix(cx, 0, 1))); - const DynamicMatrix cxOn01 = embedTwoQubitInNqubit(cx, 3, 0, 1); - const DynamicMatrix cxOn12 = embedTwoQubitInNqubit(cx, 3, 1, 2); + EXPECT_TRUE(x.embedInNqubit(2, 0).isApprox( + Matrix4x4::kron(x, Matrix2x2::identity()))); + EXPECT_TRUE(x.embedInNqubit(2, 1).isApprox( + Matrix4x4::kron(Matrix2x2::identity(), x))); + EXPECT_TRUE(cx.embedInNqubit(2, 0, 1).isApprox(cx.reorderForQubits(0, 1))); + const DynamicMatrix cxOn01 = cx.embedInNqubit(3, 0, 1); + const DynamicMatrix cxOn12 = cx.embedInNqubit(3, 1, 2); EXPECT_EQ(cxOn01.rows(), 8); EXPECT_EQ(cxOn12.rows(), 8); EXPECT_FALSE(cxOn01.isApprox(cxOn12)); @@ -441,7 +453,7 @@ TEST(UnitaryDynamicMatrix, NQubitEmbedMatchesTwoQubitSpecialization) { TEST(UnitaryDynamicMatrix, EmbedSingleQubitOnMiddleWire) { const Matrix2x2 x = pauliX(); - const DynamicMatrix embedded = embedSingleQubitInNqubit(x, 3, 1); + const DynamicMatrix embedded = x.embedInNqubit(3, 1); EXPECT_EQ(embedded.rows(), 8); EXPECT_FALSE(embedded.isIdentity()); @@ -452,7 +464,7 @@ TEST(UnitaryDynamicMatrix, EmbedSingleQubitOnMiddleWire) { TEST(UnitaryDynamicMatrix, MultiplyTraceAndScalar) { const Matrix2x2 x = pauliX(); - const DynamicMatrix embedded = embedSingleQubitInNqubit(x, 2, 0); + const DynamicMatrix embedded = x.embedInNqubit(2, 0); EXPECT_FALSE(embedded.isIdentity()); const Complex scalar = std::exp(1i * 0.3); EXPECT_TRUE((scalar * embedded).isApprox(embedded * scalar)); @@ -506,7 +518,7 @@ TEST(SymmetricEigensolver, DiagonalMatrix) { a[5] = 1.0; a[10] = 4.0; a[15] = 2.0; - const SymmetricEigen4 result = symmetricEigen4(a); + const SymmetricEigen4 result = Matrix4x4::symmetricEigen4(a); EXPECT_NEAR(result.eigenvalues[0], 1.0, MATRIX_TOLERANCE); EXPECT_NEAR(result.eigenvalues[1], 2.0, MATRIX_TOLERANCE); EXPECT_NEAR(result.eigenvalues[2], 3.0, MATRIX_TOLERANCE); @@ -523,8 +535,8 @@ TEST(SymmetricEigensolver, Matrix4x4Overload) { for (std::size_t k = 0; k < 16; ++k) { matrix(k / 4, k % 4) = a[k]; } - const SymmetricEigen4 fromArray = symmetricEigen4(a); - const SymmetricEigen4 fromMatrix = symmetricEigen4(matrix); + const SymmetricEigen4 fromArray = Matrix4x4::symmetricEigen4(a); + const SymmetricEigen4 fromMatrix = matrix.symmetricEigen4(); for (std::size_t i = 0; i < 4; ++i) { EXPECT_NEAR(fromMatrix.eigenvalues[i], fromArray.eigenvalues[i], MATRIX_TOLERANCE); @@ -544,7 +556,7 @@ TEST(SymmetricEigensolver, ReconstructsRandomSymmetric) { a[(j * 4) + i] = value; } } - const SymmetricEigen4 result = symmetricEigen4(a); + const SymmetricEigen4 result = Matrix4x4::symmetricEigen4(a); // Eigenvalues are ascending. for (std::size_t i = 0; i + 1 < 4; ++i) { @@ -576,7 +588,7 @@ TEST(SymmetricEigensolver, HandlesDegenerateSpectrum) { for (std::size_t i = 0; i < 4; ++i) { a[(i * 4) + i] = 2.5; } - const SymmetricEigen4 result = symmetricEigen4(a); + const SymmetricEigen4 result = Matrix4x4::symmetricEigen4(a); for (const double value : result.eigenvalues) { EXPECT_NEAR(value, 2.5, MATRIX_TOLERANCE); } From 01cfccbcaa28ed7c232bc0ae88af7731aa7e0a64 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 22 Jun 2026 13:43:31 +0200 Subject: [PATCH 059/122] =?UTF-8?q?=F0=9F=93=9D=20Update=20CHANGELOG=20to?= =?UTF-8?q?=20include=20PR=20#1802?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb58acb8be..614fe3c174 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,7 +42,7 @@ with the exception that minor releases may include breaking changes. [#1569], [#1570], [#1572], [#1573], [#1580], [#1602], [#1620], [#1623], [#1624], [#1626], [#1627], [#1635], [#1638], [#1673], [#1675], [#1700], [#1710], [#1717], [#1728], [#1730], [#1749], [#1751], [#1762], [#1765], - [#1774], [#1780], [#1781], [#1782], [#1787]) + [#1774], [#1780], [#1781], [#1782], [#1787], [#1802]) ([**@burgholzer**], [**@denialhaag**], [**@taminob**], [**@DRovara**], [**@li-mingbao**], [**@Ectras**], [**@MatthiasReumann**], [**@simon1hofmann**]) @@ -598,6 +598,7 @@ changelogs._ +[#1802]: https://github.com/munich-quantum-toolkit/core/pull/1802 [#1787]: https://github.com/munich-quantum-toolkit/core/pull/1787 [#1782]: https://github.com/munich-quantum-toolkit/core/pull/1782 [#1781]: https://github.com/munich-quantum-toolkit/core/pull/1781 From aa593accc2f8245e0422c109a678767a4e6587bc Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 22 Jun 2026 13:54:18 +0200 Subject: [PATCH 060/122] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20linter=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/lib/Dialect/QCO/Utils/Matrix.cpp | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Utils/Matrix.cpp b/mlir/lib/Dialect/QCO/Utils/Matrix.cpp index 806aaa51ae..aeea248840 100644 --- a/mlir/lib/Dialect/QCO/Utils/Matrix.cpp +++ b/mlir/lib/Dialect/QCO/Utils/Matrix.cpp @@ -11,6 +11,7 @@ #include "mlir/Dialect/QCO/Utils/Matrix.h" #include +#include #include #include @@ -271,10 +272,7 @@ Matrix2x2 Matrix2x2::operator*(const Matrix2x2& rhs) const { return out; } -void Matrix2x2::premultiplyBy(const Matrix2x2& lhs) { - const std::array rhs = data; - multiply2x2(lhs.data, rhs, data); -} +void Matrix2x2::premultiplyBy(const Matrix2x2& lhs) { *this = lhs * *this; } Matrix2x2 Matrix2x2::operator*(const Complex& scalar) const { Matrix2x2 out = *this; @@ -377,10 +375,7 @@ Matrix4x4 Matrix4x4::operator*(const Matrix4x4& rhs) const { return out; } -void Matrix4x4::premultiplyBy(const Matrix4x4& lhs) { - const std::array rhs = data; - multiply4x4(lhs.data, rhs, data); -} +void Matrix4x4::premultiplyBy(const Matrix4x4& lhs) { *this = lhs * *this; } Matrix4x4 Matrix4x4::operator*(const Complex& scalar) const { Matrix4x4 out = *this; From 3f38d9c34e1754bfa155fa0232a39b4a066d68f3 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 22 Jun 2026 14:11:05 +0200 Subject: [PATCH 061/122] =?UTF-8?q?=F0=9F=90=87=20Address=20rabbit's=20com?= =?UTF-8?q?ments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/include/mlir/Dialect/QCO/Utils/Matrix.h | 2 ++ mlir/lib/Dialect/QCO/Utils/Matrix.cpp | 20 +++++++++++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h index 369cd2209b..cdea703ed4 100644 --- a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h +++ b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h @@ -789,7 +789,9 @@ inline constexpr bool * eigenvector for `eigenvalues[j]`). */ struct SymmetricEigen4 { + /// Eigenvalues in ascending order. std::array eigenvalues{}; + /// Orthonormal eigenvectors as columns (column `j` matches `eigenvalues[j]`). Matrix4x4 eigenvectors{}; }; diff --git a/mlir/lib/Dialect/QCO/Utils/Matrix.cpp b/mlir/lib/Dialect/QCO/Utils/Matrix.cpp index aeea248840..00b590271b 100644 --- a/mlir/lib/Dialect/QCO/Utils/Matrix.cpp +++ b/mlir/lib/Dialect/QCO/Utils/Matrix.cpp @@ -155,6 +155,14 @@ static void multiply4x4(const ArrayRef lhs, return udim * udim; } +/// Returns `2^numQubits` as `int64_t` after checking it fits. +[[nodiscard]] static std::int64_t +checkedHilbertDim(const std::size_t numQubits) { + assert(numQubits < std::numeric_limits::digits && + "Hilbert-space dimension must fit in int64_t"); + return std::int64_t{static_cast(std::uint64_t{1} << numQubits)}; +} + static void validateCornerDims(const std::int64_t matrixDim, const std::int64_t blockDim) { assert(matrixDim >= 0 && blockDim >= 0 && blockDim <= matrixDim && @@ -321,7 +329,7 @@ DynamicMatrix Matrix2x2::embedInNqubit(const std::size_t numQubits, if (numQubits == 2) { return DynamicMatrix(embedInTwoQubit(qubitIndex)); } - const auto dim = static_cast(1ULL << numQubits); + const auto dim = checkedHilbertDim(numQubits); DynamicMatrix out(dim); const auto udim = static_cast(dim); for (std::size_t row = 0; row < udim; ++row) { @@ -345,7 +353,7 @@ Matrix4x4 Matrix2x2::embedInTwoQubit(const std::size_t qubitIndex) const { if (qubitIndex == 1) { return Matrix4x4::kron(Matrix2x2::identity(), *this); } - assert(false && "Invalid qubit index for single-qubit embed"); + llvm::reportFatalInternalError("Invalid qubit index for single-qubit embed"); } Matrix4x4 Matrix4x4::fromElements(const Complex& m00, const Complex& m01, @@ -452,12 +460,14 @@ Matrix4x4 Matrix4x4::kron(const Matrix2x2& lhs, const Matrix2x2& rhs) { std::array Matrix4x4::column(const std::size_t col) const { + assert(col < K_COLS); return {data[col], data[K_COLS + col], data[(2 * K_COLS) + col], data[(3 * K_COLS) + col]}; } void Matrix4x4::setColumn(const std::size_t col, const ArrayRef values) { + assert(col < K_COLS); assert(values.size() == K_ROWS && "setColumn requires exactly K_ROWS entries"); for (std::size_t row = 0; row < K_ROWS; ++row) { @@ -467,7 +477,7 @@ void Matrix4x4::setColumn(const std::size_t col, ArrayRef Matrix4x4::row(const std::size_t row) const { assert(row < K_ROWS); - return ArrayRef(data).slice(row * K_COLS, K_COLS); + return ArrayRef(data).slice(row * K_COLS, K_COLS); } void Matrix4x4::setRow(const std::size_t row, const ArrayRef values) { @@ -510,7 +520,7 @@ DynamicMatrix Matrix4x4::embedInNqubit(const std::size_t numQubits, if (numQubits == 2) { return DynamicMatrix(reorderForQubits(q0Index, q1Index)); } - const auto dim = static_cast(1ULL << numQubits); + const auto dim = checkedHilbertDim(numQubits); DynamicMatrix out(dim); const auto udim = static_cast(dim); for (std::size_t row = 0; row < udim; ++row) { @@ -541,7 +551,7 @@ Matrix4x4 Matrix4x4::reorderForQubits(const std::size_t q0Index, return fromElements(m[0], m[2], m[1], m[3], m[8], m[10], m[9], m[11], m[4], m[6], m[5], m[7], m[12], m[14], m[13], m[15]); } - assert(false && "Invalid qubit indices for two-qubit reorder"); + llvm::reportFatalInternalError("Invalid qubit indices for two-qubit reorder"); } SymmetricEigen4 Matrix4x4::symmetricEigen4() const { From 7bba64d00007a6255ef260909ba5e12edd747c0a Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 22 Jun 2026 15:22:42 +0200 Subject: [PATCH 062/122] =?UTF-8?q?=F0=9F=8E=A8=20Simplify=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Transforms/Decomposition/NativeProfile.h | 121 +----- .../QCO/Transforms/Decomposition/Weyl.h | 76 ++-- .../Decomposition/NativeProfile.cpp | 350 +++++----------- .../QCO/Transforms/Decomposition/Weyl.cpp | 71 ++-- .../FuseTwoQubitUnitaryRuns.cpp | 375 ++++++++---------- .../Compiler/test_compiler_pipeline.cpp | 4 +- .../Decomposition/test_weyl_decomposition.cpp | 62 ++- 7 files changed, 372 insertions(+), 687 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeProfile.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeProfile.h index f9abed48d7..13542e507b 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeProfile.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeProfile.h @@ -10,13 +10,10 @@ #pragma once -#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" -#include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" #include -#include #include #include #include @@ -30,7 +27,7 @@ namespace mlir::qco::decomposition { /** - * @brief Native gate kinds that may appear in a two-qubit synthesis menu. + * @brief Gate token in a comma-separated native menu (e.g. `"u,cx,rzz"`). */ enum class NativeGateKind : std::uint8_t { U, @@ -45,65 +42,24 @@ enum class NativeGateKind : std::uint8_t { Rzz, }; -/** - * @brief Single-qubit emission strategy resolved from a native-gate menu. - */ -enum class SingleQubitMode : std::uint8_t { - ZSXX, ///< `RZ` / `SX` / `X` via ZYZ decomposition. - U3, ///< Generic `U(theta, phi, lambda)`. - R, ///< `R(theta, phi)` chain (`Rx`/`Ry` as `R`). - AxisPair, ///< Two fixed rotation axes (see @ref AxisPair). -}; - -/** - * @brief Rotation-axis pair for @ref SingleQubitMode::AxisPair emitters. - */ -enum class AxisPair : std::uint8_t { - RxRz, - RxRy, - RyRz, -}; - -/** - * @brief Entangling basis gate for two-qubit Weyl synthesis. - */ -enum class EntanglerBasis : std::uint8_t { - None, - Cx, - Cz, -}; - -struct SingleQubitEmitterSpec { - SingleQubitMode mode = SingleQubitMode::U3; - AxisPair axisPair = AxisPair::RxRz; - bool supportsDirectRx = false; -}; - /** * @brief Resolved native-gate menu for two-qubit Weyl synthesis. + * + * @p gates is the parsed menu. Euler decomposition and entangler choice are + * derived from it with fixed priority (see @ref NativeProfileSpec::eulerBasis). */ struct NativeProfileSpec { - bool allowRzz = false; - llvm::DenseSet allowedGates; - llvm::SmallVector singleQubitEmitters; - llvm::SmallVector entanglerBases; -}; + llvm::DenseSet gates; -/** - * @brief Euler basis used to emit single-qubit factors for @p emitter. - */ -[[nodiscard]] EulerBasis -emitterEulerBasis(const SingleQubitEmitterSpec& emitter); + /** @brief Preferred single-qubit Euler basis for synthesis in this menu. */ + [[nodiscard]] EulerBasis eulerBasis() const; +}; -/** - * @brief Parses a comma-separated native-gate menu (e.g. `"u,cx,rzz"`). - */ +/** @brief Parses a comma-separated native-gate menu (e.g. `"u,cx,rzz"`). */ [[nodiscard]] std::optional parseNativeSpec(StringRef nativeGates); -/** - * @brief Synthesizes a composed two-qubit unitary as gates in @p spec. - */ +/** @brief Synthesizes a two-qubit unitary as gates allowed by @p spec. */ [[nodiscard]] LogicalResult synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, Value qubit0, Value qubit1, const Matrix4x4& target, @@ -111,63 +67,20 @@ synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, Value qubit0, Value& outQubit1); /** - * @brief Number of entangling basis gates required to synthesize @p target. + * @brief Entangling basis gates needed to synthesize @p target under @p spec. * - * @return Entangler count for @p spec, or `std::nullopt` if synthesis fails. + * @return Count for @p spec, or `std::nullopt` when synthesis is impossible. */ [[nodiscard]] std::optional twoQubitEntanglerCount(const Matrix4x4& target, const NativeProfileSpec& spec); /** - * @brief Maps a compile-time single-qubit op to its native-menu gate kind. - * - * Returns `std::nullopt` for non-primitive or unsupported ops. - */ -[[nodiscard]] std::optional -nativeGateKindFor(UnitaryOpInterface op); - -/** - * @brief Returns true when @p op is already on the resolved single-qubit menu. - * - * `BarrierOp` and `GPhaseOp` are always allowed. - */ -[[nodiscard]] bool allowsSingleQubitOp(UnitaryOpInterface op, - const NativeProfileSpec& spec); - -/** - * @brief Entangler basis for a 1-control, 1-target `CtrlOp` with `X`/`Z` body. - */ -[[nodiscard]] std::optional -entanglerBasisForSingleTargetCtrl(CtrlOp ctrl); - -/** @brief Returns true when @p spec lists @p basis as an entangler. */ -[[nodiscard]] bool profileAllowsEntangler(const NativeProfileSpec& spec, - EntanglerBasis basis); - -/** - * @brief Returns true when a 1-control, 1-target `CtrlOp` is on @p spec. - */ -[[nodiscard]] bool allowsSingleTargetCtrl(CtrlOp ctrl, - const NativeProfileSpec& spec); - -/** - * @brief Returns true when a bare two-qubit op (currently `RZZ`) is on @p spec. - */ -[[nodiscard]] bool allowsBareTwoQubitOp(Operation* op, - const NativeProfileSpec& spec); - -/** - * @brief Returns true when @p op is a native two-qubit gate under @p spec. - */ -[[nodiscard]] bool allowsTwoQubitOp(Operation* op, - const NativeProfileSpec& spec); - -/** - * @brief Fills @p matrix with the 4x4 unitary for a two-qubit block op. + * @brief Returns true when @p op is already on the resolved native menu. * - * Handles single-target `CtrlOp` shells and generic two-qubit unitaries. - * Returns false for barriers, global phase, and unsupported shapes. + * Barriers and global phase are always allowed. Single-qubit primitives, + * single-target `CtrlOp` shells (`X`/`Z` bodies), and `RZZ` are checked + * against @p spec.gates. */ -[[nodiscard]] bool assignTwoQubitOpMatrix(Operation* op, Matrix4x4& matrix); +[[nodiscard]] bool allowsOp(Operation* op, const NativeProfileSpec& spec); } // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h index a05b1ab2bb..da48001459 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h @@ -12,7 +12,7 @@ #include "mlir/Dialect/QCO/Utils/Matrix.h" -#include +#include #include #include @@ -22,11 +22,11 @@ namespace mlir::qco::decomposition { /** - * @brief Weyl decomposition of a 2-qubit unitary matrix (4x4). + * @brief Weyl decomposition of a 2-qubit unitary. * - * The result consists of four 2x2 single-qubit matrices (`k1l`, `k2l`, - * `k1r`, `k2r`) and three parameters for a canonical gate (`a`, `b`, `c`). - * The canonical gate is `RXX(-2 * a) * RYY(-2 * b) * RZZ(-2 * c)`. + * A 4x4 unitary is factored as + * `(K1l ⊗ K1r) · U_canon(a,b,c) · (K2l ⊗ K2r)` up to global phase, where + * `U_canon(a,b,c) = RXX(-2a) · RYY(-2b) · RZZ(-2c)`. * * @note Adapted from TwoQubitWeylDecomposition in the IBM Qiskit framework. * (C) Copyright IBM 2023 @@ -42,16 +42,8 @@ namespace mlir::qco::decomposition { */ class TwoQubitWeylDecomposition { public: - static TwoQubitWeylDecomposition create(const Matrix4x4& unitaryMatrix, - std::optional fidelity); - - ~TwoQubitWeylDecomposition() = default; - TwoQubitWeylDecomposition() = default; - TwoQubitWeylDecomposition(const TwoQubitWeylDecomposition&) = default; - TwoQubitWeylDecomposition(TwoQubitWeylDecomposition&&) = default; - TwoQubitWeylDecomposition& - operator=(const TwoQubitWeylDecomposition&) = default; - TwoQubitWeylDecomposition& operator=(TwoQubitWeylDecomposition&&) = default; + [[nodiscard]] static TwoQubitWeylDecomposition + create(const Matrix4x4& unitaryMatrix, std::optional fidelity); [[nodiscard]] Matrix4x4 getCanonicalMatrix() const { return getCanonicalMatrix(a_, b_, c_); @@ -68,38 +60,20 @@ class TwoQubitWeylDecomposition { * ``` * q1 - k2r - C - k1r - * A - * q0 - k2l - N - *k1l* - + * q0 - k2l - N - k1l - * ``` */ [[nodiscard]] const Matrix2x2& k1l() const { return k1l_; } /** * @brief Left single-qubit factor before the canonical gate. - * - * ``` - * q1 - k2r - C - k1r - - * A - * q0 - *k2l* - N - k1l - - * ``` */ [[nodiscard]] const Matrix2x2& k2l() const { return k2l_; } /** * @brief Right single-qubit factor after the canonical gate. - * - * ``` - * q1 - k2r - C - *k1r* - - * A - * q0 - k2l - N - k1l - - * ``` */ [[nodiscard]] const Matrix2x2& k1r() const { return k1r_; } /** * @brief Right single-qubit factor before the canonical gate. - * - * ``` - * q1 - *k2r* - C - k1r - - * A - * q0 - k2l - N - k1l - - * ``` */ [[nodiscard]] const Matrix2x2& k2r() const { return k2r_; } @@ -107,7 +81,7 @@ class TwoQubitWeylDecomposition { double c); private: - bool applySpecialization(); + bool applySpecialization(const std::optional& requestedFidelity); double a_{}; double b_{}; @@ -117,32 +91,25 @@ class TwoQubitWeylDecomposition { Matrix2x2 k2l_; Matrix2x2 k1r_; Matrix2x2 k2r_; - std::uint8_t specializationKind_{0}; - std::optional requestedFidelity; }; -using TwoQubitLocalUnitaryList = llvm::SmallVector; - /** - * @brief Result of a two-qubit basis decomposition as single-qubit factors and - * entangler uses. + * @brief Single-qubit factors and entangler count for basis-gate synthesis. * * Factors are stored in emission order. For `i` in `[0, numBasisUses)` the * pair `(singleQubitFactors[2*i], singleQubitFactors[2*i + 1])` is applied to * qubits `1` and `0` respectively, followed by one entangler. The final pair * `(singleQubitFactors[2*numBasisUses], singleQubitFactors[2*numBasisUses+1])` - * is applied after the last entangler. The list therefore has length - * `2 * (numBasisUses + 1)`. + * is applied after the last entangler. */ struct TwoQubitNativeDecomposition { std::uint8_t numBasisUses = 0; - TwoQubitLocalUnitaryList singleQubitFactors; + SmallVector singleQubitFactors; double globalPhase = 0.0; }; /** - * @brief Decomposer initialized with a two-qubit basis gate for canonical-gate - * (RXX+RYY+RZZ) synthesis. + * @brief Decomposer for a fixed two-qubit basis gate (e.g. CX/CZ). * * @note Adapted from TwoQubitBasisDecomposer in the IBM Qiskit framework. * (C) Copyright IBM 2023 @@ -188,13 +155,13 @@ class TwoQubitBasisDecomposer { Matrix2x2 q2r; }; - [[nodiscard]] static TwoQubitLocalUnitaryList + [[nodiscard]] static SmallVector decomp0(const TwoQubitWeylDecomposition& target); - [[nodiscard]] TwoQubitLocalUnitaryList + [[nodiscard]] SmallVector decomp1(const TwoQubitWeylDecomposition& target) const; - [[nodiscard]] TwoQubitLocalUnitaryList + [[nodiscard]] SmallVector decomp2Supercontrolled(const TwoQubitWeylDecomposition& target) const; - [[nodiscard]] TwoQubitLocalUnitaryList + [[nodiscard]] SmallVector decomp3Supercontrolled(const TwoQubitWeylDecomposition& target) const; [[nodiscard]] std::array, 4> traces(const TwoQubitWeylDecomposition& target) const; @@ -205,4 +172,13 @@ class TwoQubitBasisDecomposer { SmbPrecomputed smb{}; }; +/** + * @brief Decomposes @p target with respect to a two-qubit basis gate matrix. + */ +[[nodiscard]] std::optional +decomposeTwoQubitWithBasis( + const Matrix4x4& target, const Matrix4x4& basisMatrix, + double basisFidelity = 1.0, + std::optional numBasisUses = std::nullopt); + } // namespace mlir::qco::decomposition diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp index d954355bdc..66e3a2f504 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp @@ -18,7 +18,6 @@ #include #include -#include #include #include #include @@ -37,8 +36,9 @@ using mlir::qco::Matrix2x2; using mlir::qco::Matrix4x4; namespace mlir::qco::decomposition { +namespace { -static std::optional parseGateToken(llvm::StringRef name) { +std::optional parseGateToken(llvm::StringRef name) { return llvm::StringSwitch>(name) .Case("u", NativeGateKind::U) .Case("x", NativeGateKind::X) @@ -53,10 +53,10 @@ static std::optional parseGateToken(llvm::StringRef name) { .Default(std::nullopt); } -static std::optional> +std::optional> parseGateSet(llvm::StringRef nativeGates) { llvm::DenseSet gates; - llvm::SmallVector parts; + SmallVector parts; nativeGates.split(parts, ',', /*MaxSplit=*/-1, /*KeepEmpty=*/false); for (llvm::StringRef part : parts) { const auto token = part.trim().lower(); @@ -72,202 +72,133 @@ parseGateSet(llvm::StringRef nativeGates) { return gates; } -static SingleQubitEmitterSpec -makeEmitterSpec(SingleQubitMode mode, AxisPair axisPair = AxisPair::RxRz, - bool supportsDirectRx = false) { - return { - .mode = mode, .axisPair = axisPair, .supportsDirectRx = supportsDirectRx}; -} - -static void -addEmitterIfAbsent(llvm::SmallVectorImpl& emitters, - SingleQubitMode mode, AxisPair axisPair = AxisPair::RxRz, - bool supportsDirectRx = false) { - const bool present = llvm::any_of(emitters, [&](const auto& e) { - return e.mode == mode && e.axisPair == axisPair && - e.supportsDirectRx == supportsDirectRx; - }); - if (!present) { - emitters.push_back(makeEmitterSpec(mode, axisPair, supportsDirectRx)); - } -} - -static llvm::SmallVector -allowedGatesForEmitter(const SingleQubitEmitterSpec& emitter) { - switch (emitter.mode) { - case SingleQubitMode::ZSXX: { - llvm::SmallVector gates{ - NativeGateKind::X, NativeGateKind::Sx, NativeGateKind::Rz}; - if (emitter.supportsDirectRx) { - gates.push_back(NativeGateKind::Rx); - } - return gates; - } - case SingleQubitMode::U3: - return {NativeGateKind::U}; - case SingleQubitMode::R: - return {NativeGateKind::R}; - case SingleQubitMode::AxisPair: - switch (emitter.axisPair) { - case AxisPair::RxRz: - return {NativeGateKind::Rx, NativeGateKind::Rz}; - case AxisPair::RxRy: - return {NativeGateKind::Rx, NativeGateKind::Ry}; - case AxisPair::RyRz: - return {NativeGateKind::Ry, NativeGateKind::Rz}; - } - break; - } - llvm_unreachable("unknown single-qubit mode"); -} - -static llvm::SmallVector -allowedGatesForEntangler(EntanglerBasis entangler) { - switch (entangler) { - case EntanglerBasis::None: - return {}; - case EntanglerBasis::Cx: - return {NativeGateKind::Cx}; - case EntanglerBasis::Cz: - return {NativeGateKind::Cz}; - } - llvm_unreachable("unknown entangler basis"); -} - -static void populateAllowedGates(NativeProfileSpec& spec) { - spec.allowedGates.clear(); - for (const auto& emitter : spec.singleQubitEmitters) { - const auto allowed = allowedGatesForEmitter(emitter); - spec.allowedGates.insert(allowed.begin(), allowed.end()); +bool hasSingleQubitStrategy(const llvm::DenseSet& gates) { + const auto has = [&](NativeGateKind kind) { return gates.contains(kind); }; + if (has(NativeGateKind::U)) { + return true; } - for (const auto entangler : spec.entanglerBases) { - const auto allowed = allowedGatesForEntangler(entangler); - spec.allowedGates.insert(allowed.begin(), allowed.end()); + if (has(NativeGateKind::X) && has(NativeGateKind::Sx) && + has(NativeGateKind::Rz)) { + return true; } - if (spec.allowRzz) { - spec.allowedGates.insert(NativeGateKind::Rzz); + if (has(NativeGateKind::R)) { + return true; } + return (has(NativeGateKind::Rx) && has(NativeGateKind::Rz)) || + (has(NativeGateKind::Rx) && has(NativeGateKind::Ry)) || + (has(NativeGateKind::Ry) && has(NativeGateKind::Rz)); } -static std::optional -selectEntangler(const NativeProfileSpec& spec) { - if (llvm::is_contained(spec.entanglerBases, EntanglerBasis::Cx)) { - return EntanglerBasis::Cx; +std::optional selectEntangler(const NativeProfileSpec& spec) { + if (spec.gates.contains(NativeGateKind::Cx)) { + return NativeGateKind::Cx; } - if (llvm::is_contained(spec.entanglerBases, EntanglerBasis::Cz)) { - return EntanglerBasis::Cz; + if (spec.gates.contains(NativeGateKind::Cz)) { + return NativeGateKind::Cz; } return std::nullopt; } -static Matrix4x4 entanglerMatrix(EntanglerBasis entangler) { - return entangler == EntanglerBasis::Cz ? mlir::qco::twoQubitControlledZ() +Matrix4x4 entanglerMatrix(NativeGateKind entangler) { + return entangler == NativeGateKind::Cz ? mlir::qco::twoQubitControlledZ() : mlir::qco::twoQubitControlledX01(); } -static std::optional -decomposeWithEntangler(const Matrix4x4& target, EntanglerBasis entangler) { - auto decomposer = - TwoQubitBasisDecomposer::create(entanglerMatrix(entangler), 1.0); - auto weyl = TwoQubitWeylDecomposition::create(target, std::nullopt); - return decomposer.twoQubitDecompose(weyl, std::nullopt); +std::optional +decomposeForProfile(const Matrix4x4& target, const NativeProfileSpec& spec) { + const auto entangler = selectEntangler(spec); + if (!entangler) { + return std::nullopt; + } + return decomposeTwoQubitWithBasis(target, entanglerMatrix(*entangler)); } -static void emitGPhaseIfNonTrivial(OpBuilder& builder, Location loc, - double phase) { +void emitGPhaseIfNonTrivial(OpBuilder& builder, Location loc, double phase) { constexpr double epsilon = 1e-12; if (std::abs(phase) > epsilon) { GPhaseOp::create(builder, loc, phase); } } -static Value emitSingleQubitMatrix(OpBuilder& builder, Location loc, - Value inQubit, const Matrix2x2& matrix, - EulerBasis basis) { +Value emitSingleQubitMatrix(OpBuilder& builder, Location loc, Value inQubit, + const Matrix2x2& matrix, EulerBasis basis) { return *synthesizeUnitary1QEuler(builder, loc, inQubit, matrix, /*runSize=*/0, /*hasNonBasisGate=*/true, basis); } -EulerBasis emitterEulerBasis(const SingleQubitEmitterSpec& emitter) { - switch (emitter.mode) { - case SingleQubitMode::ZSXX: - return EulerBasis::ZSXX; - case SingleQubitMode::U3: - return EulerBasis::U; - case SingleQubitMode::R: - return EulerBasis::R; - case SingleQubitMode::AxisPair: - switch (emitter.axisPair) { - case AxisPair::RxRz: - return EulerBasis::XZX; - case AxisPair::RxRy: - return EulerBasis::XYX; - case AxisPair::RyRz: - return EulerBasis::ZYZ; - } - break; +std::optional gateKindFor(UnitaryOpInterface op) { + Operation* raw = op.getOperation(); + if (llvm::isa(raw)) { + return NativeGateKind::U; + } + if (llvm::isa(raw)) { + return NativeGateKind::X; + } + if (llvm::isa(raw)) { + return NativeGateKind::Sx; + } + if (llvm::isa(raw)) { + return NativeGateKind::Rz; + } + if (llvm::isa(raw)) { + return NativeGateKind::Rx; } - llvm_unreachable("unknown single-qubit mode"); + if (llvm::isa(raw)) { + return NativeGateKind::Ry; + } + if (llvm::isa(raw)) { + return NativeGateKind::R; + } + return std::nullopt; } -std::optional parseNativeSpec(llvm::StringRef nativeGates) { - const auto gates = parseGateSet(nativeGates); - if (!gates || gates->empty()) { +std::optional entanglerKindFor(CtrlOp ctrl) { + if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { return std::nullopt; } - const auto has = [&](NativeGateKind kind) { return gates->contains(kind); }; + Operation* body = ctrl.getBodyUnitary(0).getOperation(); + if (llvm::isa(body)) { + return NativeGateKind::Cx; + } + if (llvm::isa(body)) { + return NativeGateKind::Cz; + } + return std::nullopt; +} - NativeProfileSpec spec; +} // namespace +EulerBasis NativeProfileSpec::eulerBasis() const { + const auto has = [&](NativeGateKind kind) { return gates.contains(kind); }; if (has(NativeGateKind::U)) { - addEmitterIfAbsent(spec.singleQubitEmitters, SingleQubitMode::U3); + return EulerBasis::U; } - const bool hasXSxRz = has(NativeGateKind::X) && has(NativeGateKind::Sx) && - has(NativeGateKind::Rz); - if (hasXSxRz) { - addEmitterIfAbsent(spec.singleQubitEmitters, SingleQubitMode::ZSXX, - AxisPair::RxRz, - /*supportsDirectRx=*/has(NativeGateKind::Rx)); + if (has(NativeGateKind::X) && has(NativeGateKind::Sx) && + has(NativeGateKind::Rz)) { + return EulerBasis::ZSXX; } if (has(NativeGateKind::R)) { - addEmitterIfAbsent(spec.singleQubitEmitters, SingleQubitMode::R); + return EulerBasis::R; } - struct AxisPairRule { - AxisPair axis; - NativeGateKind left; - NativeGateKind right; - }; - for (const auto& rule : { - AxisPairRule{.axis = AxisPair::RxRz, - .left = NativeGateKind::Rx, - .right = NativeGateKind::Rz}, - AxisPairRule{.axis = AxisPair::RxRy, - .left = NativeGateKind::Rx, - .right = NativeGateKind::Ry}, - AxisPairRule{.axis = AxisPair::RyRz, - .left = NativeGateKind::Ry, - .right = NativeGateKind::Rz}, - }) { - if (has(rule.left) && has(rule.right)) { - addEmitterIfAbsent(spec.singleQubitEmitters, SingleQubitMode::AxisPair, - rule.axis); - } + if (has(NativeGateKind::Rx) && has(NativeGateKind::Rz)) { + return EulerBasis::XZX; } - if (spec.singleQubitEmitters.empty()) { - return std::nullopt; + if (has(NativeGateKind::Rx) && has(NativeGateKind::Ry)) { + return EulerBasis::XYX; } - - if (has(NativeGateKind::Cx)) { - spec.entanglerBases.push_back(EntanglerBasis::Cx); + if (has(NativeGateKind::Ry) && has(NativeGateKind::Rz)) { + return EulerBasis::ZYZ; } - if (has(NativeGateKind::Cz)) { - spec.entanglerBases.push_back(EntanglerBasis::Cz); - } - spec.allowRzz = has(NativeGateKind::Rzz); + llvm_unreachable("parseNativeSpec guarantees a synthesizable basis"); +} - populateAllowedGates(spec); - return spec; +std::optional parseNativeSpec(llvm::StringRef nativeGates) { + const auto gates = parseGateSet(nativeGates); + if (!gates || gates->empty() || !hasSingleQubitStrategy(*gates)) { + return std::nullopt; + } + return NativeProfileSpec{.gates = std::move(*gates)}; } LogicalResult synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, @@ -279,11 +210,11 @@ LogicalResult synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, if (!entangler) { return failure(); } - const auto native = decomposeWithEntangler(target, *entangler); + const auto native = decomposeForProfile(target, spec); if (!native) { return failure(); } - const auto basis = emitterEulerBasis(spec.singleQubitEmitters.front()); + const auto basis = spec.eulerBasis(); emitGPhaseIfNonTrivial(builder, loc, native->globalPhase); @@ -297,8 +228,8 @@ LogicalResult synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, const auto emitEntangler = [&]() { auto ctrlOp = CtrlOp::create( builder, loc, ValueRange{wire0}, ValueRange{wire1}, - [&](ValueRange targetArgs) -> llvm::SmallVector { - if (*entangler == EntanglerBasis::Cz) { + [&](ValueRange targetArgs) -> SmallVector { + if (*entangler == NativeGateKind::Cz) { return {ZOp::create(builder, loc, targetArgs[0]).getOutputQubit(0)}; } return {XOp::create(builder, loc, targetArgs[0]).getOutputQubit(0)}; @@ -322,105 +253,30 @@ LogicalResult synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, std::optional twoQubitEntanglerCount(const Matrix4x4& target, const NativeProfileSpec& spec) { - const auto entangler = selectEntangler(spec); - if (!entangler) { - return std::nullopt; - } - const auto native = decomposeWithEntangler(target, *entangler); + const auto native = decomposeForProfile(target, spec); if (!native) { return std::nullopt; } return native->numBasisUses; } -std::optional nativeGateKindFor(UnitaryOpInterface op) { - Operation* raw = op.getOperation(); - if (llvm::isa(raw)) { - return NativeGateKind::U; - } - if (llvm::isa(raw)) { - return NativeGateKind::X; - } - if (llvm::isa(raw)) { - return NativeGateKind::Sx; - } - if (llvm::isa(raw)) { - return NativeGateKind::Rz; - } - if (llvm::isa(raw)) { - return NativeGateKind::Rx; - } - if (llvm::isa(raw)) { - return NativeGateKind::Ry; - } - if (llvm::isa(raw)) { - return NativeGateKind::R; - } - return std::nullopt; -} - -bool allowsSingleQubitOp(UnitaryOpInterface op, const NativeProfileSpec& spec) { - if (llvm::isa(op.getOperation())) { +bool allowsOp(Operation* op, const NativeProfileSpec& spec) { + if (llvm::isa(op)) { return true; } - const auto gate = nativeGateKindFor(op); - return gate && spec.allowedGates.contains(*gate); -} - -std::optional entanglerBasisForSingleTargetCtrl(CtrlOp ctrl) { - if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { - return std::nullopt; - } - Operation* body = ctrl.getBodyUnitary(0).getOperation(); - if (llvm::isa(body)) { - return EntanglerBasis::Cx; - } - if (llvm::isa(body)) { - return EntanglerBasis::Cz; - } - return std::nullopt; -} - -bool profileAllowsEntangler(const NativeProfileSpec& spec, - EntanglerBasis basis) { - return llvm::is_contained(spec.entanglerBases, basis); -} - -bool allowsSingleTargetCtrl(CtrlOp ctrl, const NativeProfileSpec& spec) { - const auto basis = entanglerBasisForSingleTargetCtrl(ctrl); - return basis && profileAllowsEntangler(spec, *basis); -} - -bool allowsBareTwoQubitOp(Operation* op, const NativeProfileSpec& spec) { - return spec.allowRzz && llvm::isa(op) && - spec.allowedGates.contains(NativeGateKind::Rzz); -} - -bool allowsTwoQubitOp(Operation* op, const NativeProfileSpec& spec) { if (auto ctrl = llvm::dyn_cast(op)) { - return allowsSingleTargetCtrl(ctrl, spec); + const auto kind = entanglerKindFor(ctrl); + return kind && spec.gates.contains(*kind); } - return allowsBareTwoQubitOp(op, spec); -} - -bool assignTwoQubitOpMatrix(Operation* op, Matrix4x4& matrix) { - if (llvm::isa(op)) { - return false; - } - if (auto ctrl = llvm::dyn_cast(op)) { - const auto basis = entanglerBasisForSingleTargetCtrl(ctrl); - if (!basis) { - return false; - } - matrix = *basis == EntanglerBasis::Cz ? mlir::qco::twoQubitControlledZ() - : mlir::qco::twoQubitControlledX01(); - return true; + if (llvm::isa(op)) { + return spec.gates.contains(NativeGateKind::Rzz); } auto unitary = llvm::dyn_cast(op); - if (!unitary || !unitary.isTwoQubit()) { + if (!unitary || !unitary.isSingleQubit()) { return false; } - return unitary.getUnitaryMatrix4x4(matrix); + const auto gate = gateKindFor(unitary); + return gate && spec.gates.contains(*gate); } } // namespace mlir::qco::decomposition diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp index be4756bf64..2bdc6d5c0c 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp @@ -56,17 +56,12 @@ enum class Specialization : std::uint8_t { FSimabmbEquiv, }; -enum class MagicBasisTransform : std::uint8_t { - Into, - OutOf, -}; - } // namespace static constexpr auto DIAGONALIZATION_PRECISION = 1e-13; static Matrix4x4 magicBasisTransform(const Matrix4x4& unitary, - MagicBasisTransform direction) { + bool outOfMagicBasis) { const Matrix4x4 bNonNormalized = Matrix4x4::fromElements( // 1, 1i, 0, 0, // 0, 0, 1i, 1, // @@ -77,13 +72,10 @@ static Matrix4x4 magicBasisTransform(const Matrix4x4& unitary, Complex{0.0, -0.5}, 0, 0, Complex{0.0, 0.5}, // 0, Complex{0.0, -0.5}, Complex{0.0, -0.5}, 0, // 0, 0.5, -0.5, 0); - if (direction == MagicBasisTransform::OutOf) { + if (outOfMagicBasis) { return bNonNormalizedDagger * unitary * bNonNormalized; } - if (direction == MagicBasisTransform::Into) { - return bNonNormalized * unitary * bNonNormalizedDagger; - } - llvm::reportFatalInternalError("Unknown MagicBasisTransform direction!"); + return bNonNormalized * unitary * bNonNormalizedDagger; } static double closestPartialSwap(double a, double b, double c) { @@ -287,7 +279,7 @@ TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, // 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); + auto uP = magicBasisTransform(u, /*outOfMagicBasis=*/true); const Matrix4x4 m2 = uP.transpose() * uP; // diagonalization yields eigenvectors (p) and eigenvalues (d); @@ -360,14 +352,14 @@ TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, // residual rather than the (much tighter) default matrix tolerance. assert((k1.transpose() * k1).isIdentity(SANITY_CHECK_PRECISION)); assert(k1.determinant().real() > 0.0); - k1 = magicBasisTransform(k1, MagicBasisTransform::Into); + k1 = magicBasisTransform(k1, /*outOfMagicBasis=*/false); // combined matrix k2 of 1q gates before canonical gate Matrix4x4 k2 = p.adjoint(); // k2 must be orthogonal; see the tolerance note on the k1 check above. assert((k2.transpose() * k2).isIdentity(SANITY_CHECK_PRECISION)); assert(k2.determinant().real() > 0.0); - k2 = magicBasisTransform(k2, MagicBasisTransform::Into); + k2 = magicBasisTransform(k2, /*outOfMagicBasis=*/false); // ensure k1 and k2 are correct (when combined with the canonical gate // parameters in-between, they are equivalent to u) @@ -377,7 +369,7 @@ TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, } assert((k1 * magicBasisTransform(Matrix4x4::fromDiagonal(tempConjDiag), - MagicBasisTransform::Into) * + /*outOfMagicBasis=*/false) * k2) .isApprox(u, SANITY_CHECK_PRECISION)); @@ -455,9 +447,6 @@ TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, decomposition.k2l_ = K2l; decomposition.k1r_ = K1r; decomposition.k2r_ = K2r; - decomposition.specializationKind_ = - static_cast(Specialization::General); - decomposition.requestedFidelity = fidelity; // make sure decomposition is equal to input assert((Matrix4x4::kron(K1l, K1r) * decomposition.getCanonicalMatrix() * @@ -466,7 +455,7 @@ TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, // determine actual specialization of canonical gate so that the 1q // matrices can potentially be simplified - auto flippedFromOriginal = decomposition.applySpecialization(); + auto flippedFromOriginal = decomposition.applySpecialization(fidelity); auto getTraceValue = [&]() { if (flippedFromOriginal) { @@ -483,12 +472,11 @@ TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, // 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 && - calculatedFidelity + 1.0e-13 < *decomposition.requestedFidelity) { + if (fidelity && calculatedFidelity + 1.0e-13 < *fidelity) { llvm::reportFatalInternalError(llvm::formatv( "TwoQubitWeylDecomposition: Calculated fidelity of " "specialization is worse than requested fidelity ({0:F4} vs {1:F4})!", - calculatedFidelity, *decomposition.requestedFidelity)); + calculatedFidelity, *fidelity)); } decomposition.globalPhase_ += std::arg(trace); @@ -516,13 +504,8 @@ Matrix4x4 TwoQubitWeylDecomposition::getCanonicalMatrix(double a, double b, return zz * yy * xx; } -bool TwoQubitWeylDecomposition::applySpecialization() { - if (specializationKind_ != - static_cast(Specialization::General)) { - llvm::reportFatalInternalError( - "Application of specialization only works on " - "general Weyl decompositions!"); - } +bool TwoQubitWeylDecomposition::applySpecialization( + const std::optional& requestedFidelity) { bool flippedFromOriginal = false; const auto newSpecialization = bestSpecialization(*this, requestedFidelity); if (newSpecialization == Specialization::General) { @@ -532,7 +515,6 @@ bool TwoQubitWeylDecomposition::applySpecialization() { // make the single-qubit pre-/post-gates canonical. return flippedFromOriginal; } - specializationKind_ = static_cast(newSpecialization); if (newSpecialization == Specialization::IdEquiv) { // :math:`U \sim U_d(0,0,0)` @@ -865,7 +847,7 @@ TwoQubitBasisDecomposer::twoQubitDecompose( llvm::Twine(bestNbasis) + ")!"); llvm_unreachable(""); }; - TwoQubitLocalUnitaryList factors = chooseDecomposition(); + SmallVector factors = chooseDecomposition(); #ifndef NDEBUG for (const auto& factor : factors) { assert(isUnitaryMatrix(factor, 1e-12)); @@ -892,18 +874,18 @@ TwoQubitBasisDecomposer::twoQubitDecompose( }; } -TwoQubitLocalUnitaryList +SmallVector TwoQubitBasisDecomposer::decomp0(const TwoQubitWeylDecomposition& target) { - return TwoQubitLocalUnitaryList{ + return SmallVector{ target.k1r() * target.k2r(), target.k1l() * target.k2l(), }; } -TwoQubitLocalUnitaryList TwoQubitBasisDecomposer::decomp1( +SmallVector TwoQubitBasisDecomposer::decomp1( const TwoQubitWeylDecomposition& target) const { // may not work for z != 0 and c != 0 (not always in Weyl chamber) - return TwoQubitLocalUnitaryList{ + return SmallVector{ basisDecomposer.k2r().adjoint() * target.k2r(), basisDecomposer.k2l().adjoint() * target.k2l(), target.k1r() * basisDecomposer.k1r().adjoint(), @@ -911,14 +893,14 @@ TwoQubitLocalUnitaryList TwoQubitBasisDecomposer::decomp1( }; } -TwoQubitLocalUnitaryList TwoQubitBasisDecomposer::decomp2Supercontrolled( +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 TwoQubitLocalUnitaryList{ + return SmallVector{ smb.q2r * target.k2r(), smb.q2l * target.k2l(), smb.q1ra * rzMatrix(2. * target.b()) * smb.q1rb, @@ -928,14 +910,14 @@ TwoQubitLocalUnitaryList TwoQubitBasisDecomposer::decomp2Supercontrolled( }; } -TwoQubitLocalUnitaryList TwoQubitBasisDecomposer::decomp3Supercontrolled( +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 TwoQubitLocalUnitaryList{ + return SmallVector{ smb.u3r * target.k2r(), smb.u3l * target.k2l(), smb.u2ra * rzMatrix(2. * target.b()) * smb.u2rb, @@ -976,4 +958,15 @@ TwoQubitBasisDecomposer::traces(const TwoQubitWeylDecomposition& target) const { }; } +std::optional +decomposeTwoQubitWithBasis(const Matrix4x4& target, + const Matrix4x4& basisMatrix, + const double basisFidelity, + const std::optional numBasisUses) { + const auto decomposer = + TwoQubitBasisDecomposer::create(basisMatrix, basisFidelity); + const auto weyl = TwoQubitWeylDecomposition::create(target, std::nullopt); + return decomposer.twoQubitDecompose(weyl, numBasisUses); +} + } // namespace mlir::qco::decomposition diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp index bed7355287..1d6d155d3b 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp @@ -18,9 +18,7 @@ #include #include #include -#include #include -#include #include #include #include @@ -41,17 +39,23 @@ namespace mlir::qco { #define GEN_PASS_DEF_FUSETWOQUBITUNITARYRUNS #include "mlir/Dialect/QCO/Transforms/Passes.h.inc" +namespace { + /// Skips unitaries nested under `ctrl`/`inv` bodies (handled on the shell op). -static bool isExcludedFromTopLevelUnitaryWalk(Operation* op) { +bool isExcludedFromTopLevelUnitaryWalk(Operation* op) { if (op->getParentOfType()) { return true; } return !llvm::isa(op) && op->getParentOfType(); } -static void -collectUnitaryOpsInPreOrder(Operation* root, - llvm::SmallVectorImpl& ops) { +bool isWalkableUnitaryShell(Operation* op) { + return !llvm::isa(op) && + !isExcludedFromTopLevelUnitaryWalk(op); +} + +void collectUnitaryOpsInPreOrder(Operation* root, + SmallVectorImpl& ops) { root->walk([&](Operation* op) { if (isExcludedFromTopLevelUnitaryWalk(op)) { return; @@ -62,43 +66,67 @@ collectUnitaryOpsInPreOrder(Operation* root, }); } -static Value emitSingleQubitMatrix(IRRewriter& rewriter, Location loc, - Value inQubit, const Matrix2x2& matrix, - const decomposition::EulerBasis basis) { +Value emitSingleQubitMatrix(IRRewriter& rewriter, Location loc, Value inQubit, + const Matrix2x2& matrix, + const decomposition::EulerBasis basis) { return *decomposition::synthesizeUnitary1QEuler( rewriter, loc, inQubit, matrix, /*runSize=*/0, /*hasNonBasisGate=*/true, basis); } -static bool isTwoQubitRunMember(UnitaryOpInterface unitary) { - if (!unitary || !unitary.isTwoQubit()) { +bool assignTwoQubitOpMatrix(Operation* op, Matrix4x4& matrix) { + if (llvm::isa(op)) { return false; } - if (llvm::isa(unitary.getOperation())) { + if (auto ctrl = llvm::dyn_cast(op)) { + if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { + return false; + } + Operation* body = ctrl.getBodyUnitary(0).getOperation(); + if (llvm::isa(body)) { + matrix = twoQubitControlledX01(); + return true; + } + if (llvm::isa(body)) { + matrix = twoQubitControlledZ(); + return true; + } return false; } - if (isExcludedFromTopLevelUnitaryWalk(unitary.getOperation())) { + auto unitary = llvm::dyn_cast(op); + if (!unitary || !unitary.isTwoQubit()) { return false; } - Matrix4x4 matrix; - return decomposition::assignTwoQubitOpMatrix(unitary.getOperation(), matrix); + return unitary.getUnitaryMatrix4x4(matrix); } -static bool isOneQubitWindowMember(UnitaryOpInterface unitary) { - if (!unitary || !unitary.isSingleQubit()) { +bool isOneQubitWindowMember(UnitaryOpInterface unitary) { + if (!unitary || !unitary.isSingleQubit() || + !isWalkableUnitaryShell(unitary.getOperation())) { return false; } - if (llvm::isa(unitary.getOperation())) { + Matrix2x2 matrix; + return unitary.getUnitaryMatrix2x2(matrix); +} + +bool isTwoQubitRunMember(UnitaryOpInterface unitary) { + if (!unitary || !unitary.isTwoQubit() || + !isWalkableUnitaryShell(unitary.getOperation())) { return false; } - if (isExcludedFromTopLevelUnitaryWalk(unitary.getOperation())) { - return false; + Matrix4x4 matrix; + return assignTwoQubitOpMatrix(unitary.getOperation(), matrix); +} + +UnitaryOpInterface fusibleSingleQubitOp(Operation* op) { + if (llvm::isa(op)) { + return {}; } - Matrix2x2 matrix; - return unitary.getUnitaryMatrix2x2(matrix); + auto unitary = llvm::dyn_cast(op); + return isOneQubitWindowMember(unitary) ? unitary : UnitaryOpInterface{}; } -static UnitaryOpInterface uniqueUnitaryUser(Value wire) { +UnitaryOpInterface uniqueUnitaryUser(Value wire) { if (!wire.hasOneUse()) { return {}; } @@ -115,7 +143,7 @@ static UnitaryOpInterface uniqueUnitaryUser(Value wire) { return {}; } -static Operation* twoQubitGateAtEndOfOneQChain(Value wire) { +Operation* twoQubitGateAtEndOfOneQChain(Value wire) { Value cur = wire; while (Operation* def = cur.getDefiningOp()) { auto unitary = llvm::dyn_cast(def); @@ -133,7 +161,7 @@ static Operation* twoQubitGateAtEndOfOneQChain(Value wire) { return nullptr; } -static bool feedsFromSameTwoQubitWindow(UnitaryOpInterface op) { +bool feedsFromSameTwoQubitWindow(UnitaryOpInterface op) { const Value in0 = op.getInputQubit(0); const Value in1 = op.getInputQubit(1); if (!in0.hasOneUse() || !in1.hasOneUse()) { @@ -144,14 +172,12 @@ static bool feedsFromSameTwoQubitWindow(UnitaryOpInterface op) { return gate0 != nullptr && gate0 == gate1; } -static bool isTwoQubitRunStart(UnitaryOpInterface op) { +bool isTwoQubitRunStart(UnitaryOpInterface op) { return isTwoQubitRunMember(op) && !feedsFromSameTwoQubitWindow(op); } -namespace { - struct FusableTwoQubitRun { - llvm::SmallVector ops; + SmallVector ops; Matrix4x4 composed = Matrix4x4::identity(); unsigned numTwoQ = 0; bool anyNonNative = false; @@ -160,31 +186,35 @@ struct FusableTwoQubitRun { }; struct OneQubitRun { - llvm::SmallVector ops; + SmallVector ops; }; -} // namespace +void markNonNativeIfNeeded(FusableTwoQubitRun& run, Operation* op, + const decomposition::NativeProfileSpec& spec) { + if (!decomposition::allowsOp(op, spec)) { + run.anyNonNative = true; + } +} // Replace when off-menu ops must be lowered, or when resynthesis uses fewer // entanglers than the fused window. -static bool shouldApplyTwoQubitRunReplacement(const FusableTwoQubitRun& run, - std::uint8_t numBasisUses) { +bool shouldApplyTwoQubitRunReplacement(const FusableTwoQubitRun& run, + std::uint8_t numBasisUses) { if (run.anyNonNative) { return true; } return numBasisUses < run.numTwoQ; } -static void -absorbTwoQubitIntoRun(FusableTwoQubitRun& run, UnitaryOpInterface op, - const decomposition::NativeProfileSpec& spec) { +void absorbTwoQubitIntoRun(FusableTwoQubitRun& run, UnitaryOpInterface op, + const decomposition::NativeProfileSpec& spec) { Matrix4x4 opMatrix; - if (!decomposition::assignTwoQubitOpMatrix(op.getOperation(), opMatrix)) { + if (!assignTwoQubitOpMatrix(op.getOperation(), opMatrix)) { return; } const Value in0 = op.getInputQubit(0); const Value in1 = op.getInputQubit(1); - llvm::SmallVector ids; + SmallVector ids; if (in0 == run.tailA && in1 == run.tailB) { ids = {0, 1}; run.tailA = op.getOutputQubit(0); @@ -199,15 +229,12 @@ absorbTwoQubitIntoRun(FusableTwoQubitRun& run, UnitaryOpInterface op, run.composed = opMatrix.reorderForQubits(ids[0], ids[1]) * run.composed; run.ops.push_back(op.getOperation()); ++run.numTwoQ; - if (!decomposition::allowsTwoQubitOp(op.getOperation(), spec)) { - run.anyNonNative = true; - } + markNonNativeIfNeeded(run, op.getOperation(), spec); } -static void absorbOneQubitIntoRun(FusableTwoQubitRun& run, - UnitaryOpInterface op, - const decomposition::NativeProfileSpec& spec, - unsigned wireIndex) { +void absorbOneQubitIntoRun(FusableTwoQubitRun& run, UnitaryOpInterface op, + const decomposition::NativeProfileSpec& spec, + unsigned wireIndex) { Matrix2x2 raw; if (!op.getUnitaryMatrix2x2(raw)) { return; @@ -215,9 +242,7 @@ static void absorbOneQubitIntoRun(FusableTwoQubitRun& run, const auto pad = raw.embedInTwoQubit(wireIndex); run.composed = pad * run.composed; run.ops.push_back(op.getOperation()); - if (!decomposition::allowsSingleQubitOp(op, spec)) { - run.anyNonNative = true; - } + markNonNativeIfNeeded(run, op.getOperation(), spec); if (wireIndex == 0) { run.tailA = op.getOutputQubit(0); } else { @@ -225,7 +250,7 @@ static void absorbOneQubitIntoRun(FusableTwoQubitRun& run, } } -static FusableTwoQubitRun +FusableTwoQubitRun scanFusableTwoQubitRun(UnitaryOpInterface head, const decomposition::NativeProfileSpec& spec) { FusableTwoQubitRun run; @@ -233,16 +258,13 @@ scanFusableTwoQubitRun(UnitaryOpInterface head, run.tailB = head.getOutputQubit(1); run.ops.push_back(head.getOperation()); run.numTwoQ = 1; - if (!decomposition::assignTwoQubitOpMatrix(head.getOperation(), - run.composed)) { + if (!assignTwoQubitOpMatrix(head.getOperation(), run.composed)) { run.composed = Matrix4x4::identity(); run.numTwoQ = 0; run.ops.clear(); return run; } - if (!decomposition::allowsTwoQubitOp(head.getOperation(), spec)) { - run.anyNonNative = true; - } + markNonNativeIfNeeded(run, head.getOperation(), spec); while (true) { UnitaryOpInterface nextOnA = uniqueUnitaryUser(run.tailA); @@ -283,16 +305,16 @@ scanFusableTwoQubitRun(UnitaryOpInterface head, return run; } -static void eraseFusableTwoQubitRun(PatternRewriter& rewriter, - const FusableTwoQubitRun& run) { +void eraseFusableTwoQubitRun(PatternRewriter& rewriter, + const FusableTwoQubitRun& run) { for (Operation* op : llvm::reverse(run.ops)) { rewriter.eraseOp(op); } } -static bool maybeFuseRun(IRRewriter& rewriter, OneQubitRun& run, - const decomposition::EulerBasis basis, - const decomposition::NativeProfileSpec& spec) { +bool maybeFuseRun(IRRewriter& rewriter, OneQubitRun& run, + const decomposition::EulerBasis basis, + const decomposition::NativeProfileSpec& spec) { Matrix2x2 fused = Matrix2x2::identity(); for (UnitaryOpInterface u : run.ops) { Matrix2x2 m; @@ -303,7 +325,7 @@ static bool maybeFuseRun(IRRewriter& rewriter, OneQubitRun& run, } const bool anyNonNative = llvm::any_of(run.ops, [&](UnitaryOpInterface u) { - return !decomposition::allowsSingleQubitOp(u, spec); + return !decomposition::allowsOp(u.getOperation(), spec); }); Operation* firstOp = run.ops.front().getOperation(); @@ -324,25 +346,53 @@ static bool maybeFuseRun(IRRewriter& rewriter, OneQubitRun& run, return true; } -static UnitaryOpInterface fusibleSingleQubitOp(Operation* op) { - auto unitary = llvm::dyn_cast(op); - if (!unitary || !unitary.isSingleQubit()) { - return {}; - } - if (llvm::isa(op)) { - return {}; - } - if (isExcludedFromTopLevelUnitaryWalk(op)) { - return {}; - } - Matrix2x2 matrix; - if (!unitary.getUnitaryMatrix2x2(matrix)) { - return {}; - } - return unitary; +bool hasNonNativeOps(Operation* root, + const decomposition::NativeProfileSpec& spec, + bool singleQubitOnly) { + const mlir::WalkResult walkResult = root->walk([&](Operation* op) { + if (!isWalkableUnitaryShell(op)) { + return mlir::WalkResult::advance(); + } + if (singleQubitOnly) { + auto unitary = llvm::dyn_cast(op); + if (!unitary || !unitary.isSingleQubit()) { + return mlir::WalkResult::advance(); + } + } else if (!llvm::isa(op) && !llvm::isa(op)) { + return mlir::WalkResult::advance(); + } + if (!decomposition::allowsOp(op, spec)) { + return mlir::WalkResult::interrupt(); + } + return mlir::WalkResult::advance(); + }); + return walkResult.wasInterrupted(); } -namespace { +LogicalResult synthesizeTwoQubitOp(IRRewriter& rewriter, Operation* op, + Location loc, Value in0, Value in1, + const decomposition::NativeProfileSpec& spec, + llvm::Twine matrixErrorMsg, + llvm::Twine synthesisErrorMsg) { + if (decomposition::allowsOp(op, spec)) { + return success(); + } + Matrix4x4 matrix; + if (!assignTwoQubitOpMatrix(op, matrix)) { + op->emitError(matrixErrorMsg); + return failure(); + } + rewriter.setInsertionPoint(op); + Value out0; + Value out1; + if (failed(decomposition::synthesizeUnitary2QWeyl( + rewriter, loc, in0, in1, matrix, spec, out0, out1))) { + op->emitError(synthesisErrorMsg); + return failure(); + } + rewriter.replaceOp(op, ValueRange{out0, out1}); + return success(); +} struct FuseTwoQubitWindowPattern : public OpInterfaceRewritePattern { @@ -391,9 +441,7 @@ struct FuseTwoQubitWindowPattern decomposition::NativeProfileSpec spec; }; -} // namespace - -static LogicalResult +LogicalResult fuseTwoQubitUnitaryRuns(Operation* root, const decomposition::NativeProfileSpec& spec) { RewritePatternSet patterns(root->getContext()); @@ -401,8 +449,6 @@ fuseTwoQubitUnitaryRuns(Operation* root, return applyPatternsGreedily(root, std::move(patterns)); } -namespace { - struct FuseTwoQubitUnitaryRunsPass : impl::FuseTwoQubitUnitaryRunsBase { FuseTwoQubitUnitaryRunsPass() = default; @@ -424,8 +470,7 @@ struct FuseTwoQubitUnitaryRunsPass return; } const auto& spec = *specOpt; - const decomposition::EulerBasis oneQubitBasis = - decomposition::emitterEulerBasis(spec.singleQubitEmitters.front()); + const decomposition::EulerBasis oneQubitBasis = spec.eulerBasis(); IRRewriter rewriter(&getContext()); @@ -440,11 +485,11 @@ struct FuseTwoQubitUnitaryRunsPass signalPassFailure(); return; } - if (!hasNonNativeSingleQubitOps(spec)) { + if (!hasNonNativeOps(getOperation(), spec, /*singleQubitOnly=*/true)) { break; } } - if (hasNonNativeSingleQubitOps(spec)) { + if (hasNonNativeOps(getOperation(), spec, /*singleQubitOnly=*/true)) { getOperation().emitError() << "native gate synthesis did not converge within " << kMaxSynthesisSweeps @@ -455,14 +500,15 @@ struct FuseTwoQubitUnitaryRunsPass fuseOneQubitRuns(rewriter, spec, oneQubitBasis); constexpr unsigned kPostMenuCleanupSweeps = 4; unsigned postMenuSweepsRemaining = kPostMenuCleanupSweeps; - while (hasNonNativeMenuOps(spec) && postMenuSweepsRemaining-- > 0) { + while (hasNonNativeOps(getOperation(), spec, /*singleQubitOnly=*/false) && + postMenuSweepsRemaining-- > 0) { if (failed(synthesizeRemainingOps(rewriter, spec, oneQubitBasis))) { signalPassFailure(); return; } fuseOneQubitRuns(rewriter, spec, oneQubitBasis); } - if (hasNonNativeMenuOps(spec)) { + if (hasNonNativeOps(getOperation(), spec, /*singleQubitOnly=*/false)) { getOperation().emitError() << "native gate synthesis: operations remain outside the native menu " "after final cleanup"; @@ -471,69 +517,11 @@ struct FuseTwoQubitUnitaryRunsPass } } - bool hasNonNativeMenuOps(const decomposition::NativeProfileSpec& spec) { - const mlir::WalkResult walkResult = - getOperation()->walk([&](Operation* op) { - if (llvm::isa(op)) { - return mlir::WalkResult::advance(); - } - if (isExcludedFromTopLevelUnitaryWalk(op)) { - return mlir::WalkResult::advance(); - } - if (auto ctrl = llvm::dyn_cast(op)) { - if (!decomposition::allowsSingleTargetCtrl(ctrl, spec)) { - return mlir::WalkResult::interrupt(); - } - return mlir::WalkResult::advance(); - } - auto unitary = llvm::dyn_cast(op); - if (!unitary) { - return mlir::WalkResult::advance(); - } - if (unitary.isSingleQubit()) { - if (!decomposition::allowsSingleQubitOp(unitary, spec)) { - return mlir::WalkResult::interrupt(); - } - return mlir::WalkResult::advance(); - } - if (unitary.isTwoQubit()) { - if (!decomposition::allowsBareTwoQubitOp(op, spec)) { - return mlir::WalkResult::interrupt(); - } - return mlir::WalkResult::advance(); - } - return mlir::WalkResult::interrupt(); - }); - return walkResult.wasInterrupted(); - } - - bool - hasNonNativeSingleQubitOps(const decomposition::NativeProfileSpec& spec) { - const mlir::WalkResult walkResult = - getOperation()->walk([&](Operation* op) { - if (llvm::isa(op)) { - return mlir::WalkResult::advance(); - } - if (isExcludedFromTopLevelUnitaryWalk(op)) { - return mlir::WalkResult::advance(); - } - auto unitary = llvm::dyn_cast(op); - if (!unitary || !unitary.isSingleQubit()) { - return mlir::WalkResult::advance(); - } - if (!decomposition::allowsSingleQubitOp(unitary, spec)) { - return mlir::WalkResult::interrupt(); - } - return mlir::WalkResult::advance(); - }); - return walkResult.wasInterrupted(); - } - private: void fuseOneQubitRuns(IRRewriter& rewriter, const decomposition::NativeProfileSpec& spec, const decomposition::EulerBasis basis) { - llvm::SmallVector runs; + SmallVector runs; llvm::DenseMap tailOpToRun; // Require single-use tail output so fan-out wires are not fused away. @@ -571,7 +559,7 @@ struct FuseTwoQubitUnitaryRunsPass synthesizeRemainingOps(IRRewriter& rewriter, const decomposition::NativeProfileSpec& spec, const decomposition::EulerBasis basis) { - llvm::SmallVector ops; + SmallVector ops; collectUnitaryOpsInPreOrder(getOperation(), ops); llvm::DenseSet erasedOps; @@ -579,19 +567,28 @@ struct FuseTwoQubitUnitaryRunsPass if (erasedOps.contains(op)) { continue; } - if (isExcludedFromTopLevelUnitaryWalk(op)) { + if (!isWalkableUnitaryShell(op)) { continue; } - if (llvm::isa(op)) { + + if (auto ctrl = llvm::dyn_cast(op)) { + const bool wasAlreadyNative = decomposition::allowsOp(op, spec); + if (failed(synthesizeControlled(rewriter, ctrl, spec))) { + return failure(); + } + if (!wasAlreadyNative) { + erasedOps.insert(op); + } continue; } + auto unitary = llvm::dyn_cast(op); if (!unitary) { continue; } if (unitary.isSingleQubit()) { - if (!decomposition::allowsSingleQubitOp(unitary, spec)) { + if (!decomposition::allowsOp(op, spec)) { if (failed(rewriteSingleQubit(rewriter, op, unitary, basis))) { return failure(); } @@ -600,24 +597,11 @@ struct FuseTwoQubitUnitaryRunsPass continue; } - if (auto ctrl = llvm::dyn_cast(op)) { - const bool wasAlreadyNative = - decomposition::allowsSingleTargetCtrl(ctrl, spec); - if (failed(rewriteControlled(rewriter, ctrl, spec))) { - return failure(); - } - if (!wasAlreadyNative) { - erasedOps.insert(op); - } - continue; - } - if (unitary.isTwoQubit()) { - if (failed(rewriteTwoQubit(rewriter, op, unitary, spec))) { + if (failed(synthesizeBareTwoQubit(rewriter, op, unitary, spec))) { return failure(); } erasedOps.insert(op); - continue; } } return success(); @@ -642,54 +626,25 @@ struct FuseTwoQubitUnitaryRunsPass } static LogicalResult - rewriteControlled(IRRewriter& rewriter, CtrlOp ctrl, - const decomposition::NativeProfileSpec& spec) { - if (decomposition::allowsSingleTargetCtrl(ctrl, spec)) { - return success(); - } - Matrix4x4 matrix; - if (!decomposition::assignTwoQubitOpMatrix(ctrl.getOperation(), matrix)) { - ctrl.emitError( - "native synthesis: cannot build a constant 4x4 matrix for this " - "controlled gate (unsupported body or non-constant parameters)"); - return failure(); - } - rewriter.setInsertionPoint(ctrl); - Value out0; - Value out1; - if (failed(decomposition::synthesizeUnitary2QWeyl( - rewriter, ctrl.getLoc(), ctrl.getInputControl(0), - ctrl.getInputTarget(0), matrix, spec, out0, out1))) { - ctrl.emitError("controlled gate not allowed by selected profile"); - return failure(); - } - rewriter.replaceOp(ctrl, ValueRange{out0, out1}); - return success(); + synthesizeControlled(IRRewriter& rewriter, CtrlOp ctrl, + const decomposition::NativeProfileSpec& spec) { + return synthesizeTwoQubitOp( + rewriter, ctrl.getOperation(), ctrl.getLoc(), ctrl.getInputControl(0), + ctrl.getInputTarget(0), spec, + "native synthesis: cannot build a constant 4x4 matrix for this " + "controlled gate (unsupported body or non-constant parameters)", + "controlled gate not allowed by selected profile"); } static LogicalResult - rewriteTwoQubit(IRRewriter& rewriter, Operation* op, - UnitaryOpInterface unitary, - const decomposition::NativeProfileSpec& spec) { - if (decomposition::allowsBareTwoQubitOp(op, spec)) { - return success(); - } - Matrix4x4 matrix; - if (!decomposition::assignTwoQubitOpMatrix(op, matrix)) { - op->emitError("unsupported two-qubit operation for selected profile"); - return failure(); - } - rewriter.setInsertionPoint(op); - Value out0; - Value out1; - if (failed(decomposition::synthesizeUnitary2QWeyl( - rewriter, op->getLoc(), unitary.getInputQubit(0), - unitary.getInputQubit(1), matrix, spec, out0, out1))) { - op->emitError("unsupported two-qubit operation for selected profile"); - return failure(); - } - rewriter.replaceOp(op, ValueRange{out0, out1}); - return success(); + synthesizeBareTwoQubit(IRRewriter& rewriter, Operation* op, + UnitaryOpInterface unitary, + const decomposition::NativeProfileSpec& spec) { + const llvm::Twine error = + "unsupported two-qubit operation for selected profile"; + return synthesizeTwoQubitOp(rewriter, op, op->getLoc(), + unitary.getInputQubit(0), + unitary.getInputQubit(1), spec, error, error); } }; diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index f5599ad71d..0f22e0e245 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -28,7 +28,6 @@ #include #include -#include #include #include #include @@ -43,6 +42,7 @@ #include #include #include +#include #include #include @@ -820,7 +820,7 @@ computeStaticTwoQubitUnitary(mlir::ModuleOp module) { } else if (!op.getUnitaryMatrix4x4(twoQ)) { return std::nullopt; } - const llvm::SmallVector ids{*q0, *q1}; + const mlir::SmallVector ids{*q0, *q1}; unitary = twoQ.reorderForQubits(ids[0], ids[1]) * unitary; qubitIds[op.getOutputQubit(0)] = *q0; qubitIds[op.getOutputQubit(1)] = *q1; diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index e09c2c8532..8e71268d98 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -19,7 +19,6 @@ #include #include #include -#include #include #include #include @@ -60,9 +59,8 @@ static const Matrix2x2& hGate() { } static Matrix4x4 randomUnitary4x4(std::mt19937& rng) { - std::normal_distribution normalDist(0.0, 1.0); - std::vector>> columns( - 4, std::vector>(4)); + std::normal_distribution normalDist(0.0, 1.0); + std::vector columns(4, std::vector(4, std::complex{0.0, 0.0})); for (auto& column : columns) { for (auto& entry : column) { entry = std::complex(normalDist(rng), normalDist(rng)); @@ -119,40 +117,33 @@ static Matrix4x4 restoreBasis(const TwoQubitNativeDecomposition& decomposition, static auto productMatrixCases() { return ::testing::Values( - []() -> Matrix4x4 { return Matrix4x4::identity(); }, - []() -> Matrix4x4 { - return Matrix4x4::kron(rzMatrix(1.0), ryMatrix(3.1)); - }, - []() -> Matrix4x4 { - return Matrix4x4::kron(Matrix2x2::identity(), rxMatrix(0.1)); - }); + []() { return Matrix4x4::identity(); }, + []() { return Matrix4x4::kron(rzMatrix(1.0), ryMatrix(3.1)); }, + []() { return Matrix4x4::kron(Matrix2x2::identity(), rxMatrix(0.1)); }); } static auto entangledMatrixCases() { return ::testing::Values( - []() -> Matrix4x4 { return rzzMatrix(2.0); }, - []() -> Matrix4x4 { - return ryyMatrix(1.0) * rzzMatrix(3.0) * rxxMatrix(2.0); - }, - []() -> Matrix4x4 { + []() { return rzzMatrix(2.0); }, + []() { return ryyMatrix(1.0) * rzzMatrix(3.0) * rxxMatrix(2.0); }, + []() { return TwoQubitWeylDecomposition::getCanonicalMatrix(1.5, -0.2, 0.0) * Matrix4x4::kron(rxMatrix(1.0), Matrix2x2::identity()); }, - []() -> Matrix4x4 { + []() { return Matrix4x4::kron(rxMatrix(1.0), ryMatrix(1.0)) * TwoQubitWeylDecomposition::getCanonicalMatrix(1.1, 0.2, 3.0) * Matrix4x4::kron(rxMatrix(1.0), Matrix2x2::identity()); }, - []() -> Matrix4x4 { + []() { return Matrix4x4::kron(hGate(), iPauliZ()) * twoQubitControlledX01() * Matrix4x4::kron(iPauliX(), iPauliY()); }); } static auto cxBasisCases() { - return ::testing::Values( - []() -> Matrix4x4 { return twoQubitControlledX01(); }, - []() -> Matrix4x4 { return twoQubitControlledX10(); }); + return ::testing::Values([]() { return twoQubitControlledX01(); }, + []() { return twoQubitControlledX10(); }); } static bool extractSingleQubitMatrix(UnitaryOpInterface op, Matrix2x2& out) { @@ -246,7 +237,7 @@ computeTwoQubitUnitaryFromFunc(func::FuncOp funcOp) { if (!extractTwoQubitMatrix(op, twoQ)) { return std::nullopt; } - const llvm::SmallVector ids{*q0id, *q1id}; + const SmallVector ids{*q0id, *q1id}; unitary = twoQ.reorderForQubits(ids[0], ids[1]) * unitary; wireIds[op->getResult(0)] = *q0id; wireIds[op->getResult(1)] = *q1id; @@ -400,8 +391,8 @@ TEST_P(BasisDecomposerTest, ReconstructsWithinRequestedFidelity) { TEST(BasisDecomposerTest, Random) { std::mt19937 rng{123456UL}; - const llvm::SmallVector basisMatrices{twoQubitControlledX01(), - twoQubitControlledX10()}; + const SmallVector basisMatrices{twoQubitControlledX01(), + twoQubitControlledX10()}; std::uniform_int_distribution distBasisGate{0, 1}; for (int i = 0; i < 2000; ++i) { @@ -478,7 +469,8 @@ TEST(WeylSynthesisTest, FailsWithoutEntanglerInSpec) { fx.setUp(); const auto spec = parseNativeSpec("u"); ASSERT_TRUE(spec); - EXPECT_TRUE(spec->entanglerBases.empty()); + EXPECT_FALSE(spec->gates.contains(NativeGateKind::Cx)); + EXPECT_FALSE(spec->gates.contains(NativeGateKind::Cz)); OpBuilder builder(fx.ctx()); const auto qubitTy = QubitType::get(fx.ctx()); const auto funcTy = @@ -497,29 +489,29 @@ TEST(WeylSynthesisTest, FailsWithoutEntanglerInSpec) { TEST(NativeSpecTest, ParsesAndRejectsMenus) { const auto ibm = parseNativeSpec("x,sx,rz,cx"); ASSERT_TRUE(ibm); - EXPECT_TRUE(ibm->allowedGates.contains(NativeGateKind::Cx)); - EXPECT_TRUE(ibm->allowedGates.contains(NativeGateKind::X)); - EXPECT_FALSE(ibm->allowRzz); + EXPECT_TRUE(ibm->gates.contains(NativeGateKind::Cx)); + EXPECT_TRUE(ibm->gates.contains(NativeGateKind::X)); + EXPECT_FALSE(ibm->gates.contains(NativeGateKind::Rzz)); EXPECT_FALSE(parseNativeSpec("x,sx,rz,not-a-gate").has_value()); const auto pMenu = parseNativeSpec("x,sx,p,cx"); const auto rzMenu = parseNativeSpec("x,sx,rz,cx"); ASSERT_TRUE(pMenu); ASSERT_TRUE(rzMenu); - EXPECT_EQ(pMenu->allowedGates, rzMenu->allowedGates); + EXPECT_EQ(pMenu->gates, rzMenu->gates); const auto cxOnly = parseNativeSpec("u,cx"); ASSERT_TRUE(cxOnly); - EXPECT_TRUE(llvm::is_contained(cxOnly->entanglerBases, EntanglerBasis::Cx)); - EXPECT_FALSE(llvm::is_contained(cxOnly->entanglerBases, EntanglerBasis::Cz)); + EXPECT_TRUE(cxOnly->gates.contains(NativeGateKind::Cx)); + EXPECT_FALSE(cxOnly->gates.contains(NativeGateKind::Cz)); const auto both = parseNativeSpec("u,cx,cz"); ASSERT_TRUE(both); - EXPECT_TRUE(llvm::is_contained(both->entanglerBases, EntanglerBasis::Cx)); - EXPECT_TRUE(llvm::is_contained(both->entanglerBases, EntanglerBasis::Cz)); + EXPECT_TRUE(both->gates.contains(NativeGateKind::Cx)); + EXPECT_TRUE(both->gates.contains(NativeGateKind::Cz)); const auto generic = parseNativeSpec("u,cx"); ASSERT_TRUE(generic); - EXPECT_TRUE(generic->allowedGates.contains(NativeGateKind::U)); - EXPECT_FALSE(generic->allowedGates.contains(NativeGateKind::X)); + EXPECT_TRUE(generic->gates.contains(NativeGateKind::U)); + EXPECT_FALSE(generic->gates.contains(NativeGateKind::X)); } From c853e08620cbacdb9c2295b694599c9e1a5ca6fd Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 22 Jun 2026 16:22:52 +0200 Subject: [PATCH 063/122] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20linter=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Decomposition/NativeProfile.cpp | 34 +++--- .../FuseTwoQubitUnitaryRuns.cpp | 102 ++++++++++-------- .../Decomposition/test_weyl_decomposition.cpp | 2 +- 3 files changed, 75 insertions(+), 63 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp index 66e3a2f504..0a4dea0eef 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp @@ -31,14 +31,14 @@ #include #include #include +#include using mlir::qco::Matrix2x2; using mlir::qco::Matrix4x4; namespace mlir::qco::decomposition { -namespace { -std::optional parseGateToken(llvm::StringRef name) { +static std::optional parseGateToken(llvm::StringRef name) { return llvm::StringSwitch>(name) .Case("u", NativeGateKind::U) .Case("x", NativeGateKind::X) @@ -53,7 +53,7 @@ std::optional parseGateToken(llvm::StringRef name) { .Default(std::nullopt); } -std::optional> +static std::optional> parseGateSet(llvm::StringRef nativeGates) { llvm::DenseSet gates; SmallVector parts; @@ -72,7 +72,8 @@ parseGateSet(llvm::StringRef nativeGates) { return gates; } -bool hasSingleQubitStrategy(const llvm::DenseSet& gates) { +static bool +hasSingleQubitStrategy(const llvm::DenseSet& gates) { const auto has = [&](NativeGateKind kind) { return gates.contains(kind); }; if (has(NativeGateKind::U)) { return true; @@ -89,7 +90,8 @@ bool hasSingleQubitStrategy(const llvm::DenseSet& gates) { (has(NativeGateKind::Ry) && has(NativeGateKind::Rz)); } -std::optional selectEntangler(const NativeProfileSpec& spec) { +static std::optional +selectEntangler(const NativeProfileSpec& spec) { if (spec.gates.contains(NativeGateKind::Cx)) { return NativeGateKind::Cx; } @@ -99,12 +101,12 @@ std::optional selectEntangler(const NativeProfileSpec& spec) { return std::nullopt; } -Matrix4x4 entanglerMatrix(NativeGateKind entangler) { +static Matrix4x4 entanglerMatrix(NativeGateKind entangler) { return entangler == NativeGateKind::Cz ? mlir::qco::twoQubitControlledZ() : mlir::qco::twoQubitControlledX01(); } -std::optional +static std::optional decomposeForProfile(const Matrix4x4& target, const NativeProfileSpec& spec) { const auto entangler = selectEntangler(spec); if (!entangler) { @@ -113,21 +115,23 @@ decomposeForProfile(const Matrix4x4& target, const NativeProfileSpec& spec) { return decomposeTwoQubitWithBasis(target, entanglerMatrix(*entangler)); } -void emitGPhaseIfNonTrivial(OpBuilder& builder, Location loc, double phase) { +static void emitGPhaseIfNonTrivial(OpBuilder& builder, Location loc, + double phase) { constexpr double epsilon = 1e-12; if (std::abs(phase) > epsilon) { GPhaseOp::create(builder, loc, phase); } } -Value emitSingleQubitMatrix(OpBuilder& builder, Location loc, Value inQubit, - const Matrix2x2& matrix, EulerBasis basis) { +static Value emitSingleQubitMatrix(OpBuilder& builder, Location loc, + Value inQubit, const Matrix2x2& matrix, + EulerBasis basis) { return *synthesizeUnitary1QEuler(builder, loc, inQubit, matrix, /*runSize=*/0, /*hasNonBasisGate=*/true, basis); } -std::optional gateKindFor(UnitaryOpInterface op) { +static std::optional gateKindFor(UnitaryOpInterface op) { Operation* raw = op.getOperation(); if (llvm::isa(raw)) { return NativeGateKind::U; @@ -153,7 +157,7 @@ std::optional gateKindFor(UnitaryOpInterface op) { return std::nullopt; } -std::optional entanglerKindFor(CtrlOp ctrl) { +static std::optional entanglerKindFor(CtrlOp ctrl) { if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { return std::nullopt; } @@ -167,8 +171,6 @@ std::optional entanglerKindFor(CtrlOp ctrl) { return std::nullopt; } -} // namespace - EulerBasis NativeProfileSpec::eulerBasis() const { const auto has = [&](NativeGateKind kind) { return gates.contains(kind); }; if (has(NativeGateKind::U)) { @@ -194,11 +196,11 @@ EulerBasis NativeProfileSpec::eulerBasis() const { } std::optional parseNativeSpec(llvm::StringRef nativeGates) { - const auto gates = parseGateSet(nativeGates); + auto gates = parseGateSet(nativeGates); if (!gates || gates->empty() || !hasSingleQubitStrategy(*gates)) { return std::nullopt; } - return NativeProfileSpec{.gates = std::move(*gates)}; + return NativeProfileSpec{.gates = std::move(gates).value()}; } LogicalResult synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp index 1d6d155d3b..2513cbd485 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp @@ -39,23 +39,21 @@ namespace mlir::qco { #define GEN_PASS_DEF_FUSETWOQUBITUNITARYRUNS #include "mlir/Dialect/QCO/Transforms/Passes.h.inc" -namespace { - /// Skips unitaries nested under `ctrl`/`inv` bodies (handled on the shell op). -bool isExcludedFromTopLevelUnitaryWalk(Operation* op) { +static bool isExcludedFromTopLevelUnitaryWalk(Operation* op) { if (op->getParentOfType()) { return true; } return !llvm::isa(op) && op->getParentOfType(); } -bool isWalkableUnitaryShell(Operation* op) { +static bool isWalkableUnitaryShell(Operation* op) { return !llvm::isa(op) && !isExcludedFromTopLevelUnitaryWalk(op); } -void collectUnitaryOpsInPreOrder(Operation* root, - SmallVectorImpl& ops) { +static void collectUnitaryOpsInPreOrder(Operation* root, + SmallVectorImpl& ops) { root->walk([&](Operation* op) { if (isExcludedFromTopLevelUnitaryWalk(op)) { return; @@ -66,15 +64,15 @@ void collectUnitaryOpsInPreOrder(Operation* root, }); } -Value emitSingleQubitMatrix(IRRewriter& rewriter, Location loc, Value inQubit, - const Matrix2x2& matrix, - const decomposition::EulerBasis basis) { +static Value emitSingleQubitMatrix(IRRewriter& rewriter, Location loc, + Value inQubit, const Matrix2x2& matrix, + const decomposition::EulerBasis basis) { return *decomposition::synthesizeUnitary1QEuler( rewriter, loc, inQubit, matrix, /*runSize=*/0, /*hasNonBasisGate=*/true, basis); } -bool assignTwoQubitOpMatrix(Operation* op, Matrix4x4& matrix) { +static bool assignTwoQubitOpMatrix(Operation* op, Matrix4x4& matrix) { if (llvm::isa(op)) { return false; } @@ -100,7 +98,7 @@ bool assignTwoQubitOpMatrix(Operation* op, Matrix4x4& matrix) { return unitary.getUnitaryMatrix4x4(matrix); } -bool isOneQubitWindowMember(UnitaryOpInterface unitary) { +static bool isOneQubitWindowMember(UnitaryOpInterface unitary) { if (!unitary || !unitary.isSingleQubit() || !isWalkableUnitaryShell(unitary.getOperation())) { return false; @@ -109,7 +107,7 @@ bool isOneQubitWindowMember(UnitaryOpInterface unitary) { return unitary.getUnitaryMatrix2x2(matrix); } -bool isTwoQubitRunMember(UnitaryOpInterface unitary) { +static bool isTwoQubitRunMember(UnitaryOpInterface unitary) { if (!unitary || !unitary.isTwoQubit() || !isWalkableUnitaryShell(unitary.getOperation())) { return false; @@ -118,7 +116,7 @@ bool isTwoQubitRunMember(UnitaryOpInterface unitary) { return assignTwoQubitOpMatrix(unitary.getOperation(), matrix); } -UnitaryOpInterface fusibleSingleQubitOp(Operation* op) { +static UnitaryOpInterface fusibleSingleQubitOp(Operation* op) { if (llvm::isa(op)) { return {}; } @@ -126,7 +124,7 @@ UnitaryOpInterface fusibleSingleQubitOp(Operation* op) { return isOneQubitWindowMember(unitary) ? unitary : UnitaryOpInterface{}; } -UnitaryOpInterface uniqueUnitaryUser(Value wire) { +static UnitaryOpInterface uniqueUnitaryUser(Value wire) { if (!wire.hasOneUse()) { return {}; } @@ -143,7 +141,7 @@ UnitaryOpInterface uniqueUnitaryUser(Value wire) { return {}; } -Operation* twoQubitGateAtEndOfOneQChain(Value wire) { +static Operation* twoQubitGateAtEndOfOneQChain(Value wire) { Value cur = wire; while (Operation* def = cur.getDefiningOp()) { auto unitary = llvm::dyn_cast(def); @@ -161,7 +159,7 @@ Operation* twoQubitGateAtEndOfOneQChain(Value wire) { return nullptr; } -bool feedsFromSameTwoQubitWindow(UnitaryOpInterface op) { +static bool feedsFromSameTwoQubitWindow(UnitaryOpInterface op) { const Value in0 = op.getInputQubit(0); const Value in1 = op.getInputQubit(1); if (!in0.hasOneUse() || !in1.hasOneUse()) { @@ -172,10 +170,12 @@ bool feedsFromSameTwoQubitWindow(UnitaryOpInterface op) { return gate0 != nullptr && gate0 == gate1; } -bool isTwoQubitRunStart(UnitaryOpInterface op) { +static bool isTwoQubitRunStart(UnitaryOpInterface op) { return isTwoQubitRunMember(op) && !feedsFromSameTwoQubitWindow(op); } +namespace { + struct FusableTwoQubitRun { SmallVector ops; Matrix4x4 composed = Matrix4x4::identity(); @@ -189,8 +189,11 @@ struct OneQubitRun { SmallVector ops; }; -void markNonNativeIfNeeded(FusableTwoQubitRun& run, Operation* op, - const decomposition::NativeProfileSpec& spec) { +} // namespace + +static void +markNonNativeIfNeeded(FusableTwoQubitRun& run, Operation* op, + const decomposition::NativeProfileSpec& spec) { if (!decomposition::allowsOp(op, spec)) { run.anyNonNative = true; } @@ -198,16 +201,17 @@ void markNonNativeIfNeeded(FusableTwoQubitRun& run, Operation* op, // Replace when off-menu ops must be lowered, or when resynthesis uses fewer // entanglers than the fused window. -bool shouldApplyTwoQubitRunReplacement(const FusableTwoQubitRun& run, - std::uint8_t numBasisUses) { +static bool shouldApplyTwoQubitRunReplacement(const FusableTwoQubitRun& run, + std::uint8_t numBasisUses) { if (run.anyNonNative) { return true; } return numBasisUses < run.numTwoQ; } -void absorbTwoQubitIntoRun(FusableTwoQubitRun& run, UnitaryOpInterface op, - const decomposition::NativeProfileSpec& spec) { +static void +absorbTwoQubitIntoRun(FusableTwoQubitRun& run, UnitaryOpInterface op, + const decomposition::NativeProfileSpec& spec) { Matrix4x4 opMatrix; if (!assignTwoQubitOpMatrix(op.getOperation(), opMatrix)) { return; @@ -232,9 +236,10 @@ void absorbTwoQubitIntoRun(FusableTwoQubitRun& run, UnitaryOpInterface op, markNonNativeIfNeeded(run, op.getOperation(), spec); } -void absorbOneQubitIntoRun(FusableTwoQubitRun& run, UnitaryOpInterface op, - const decomposition::NativeProfileSpec& spec, - unsigned wireIndex) { +static void absorbOneQubitIntoRun(FusableTwoQubitRun& run, + UnitaryOpInterface op, + const decomposition::NativeProfileSpec& spec, + unsigned wireIndex) { Matrix2x2 raw; if (!op.getUnitaryMatrix2x2(raw)) { return; @@ -250,7 +255,7 @@ void absorbOneQubitIntoRun(FusableTwoQubitRun& run, UnitaryOpInterface op, } } -FusableTwoQubitRun +static FusableTwoQubitRun scanFusableTwoQubitRun(UnitaryOpInterface head, const decomposition::NativeProfileSpec& spec) { FusableTwoQubitRun run; @@ -305,16 +310,16 @@ scanFusableTwoQubitRun(UnitaryOpInterface head, return run; } -void eraseFusableTwoQubitRun(PatternRewriter& rewriter, - const FusableTwoQubitRun& run) { +static void eraseFusableTwoQubitRun(PatternRewriter& rewriter, + const FusableTwoQubitRun& run) { for (Operation* op : llvm::reverse(run.ops)) { rewriter.eraseOp(op); } } -bool maybeFuseRun(IRRewriter& rewriter, OneQubitRun& run, - const decomposition::EulerBasis basis, - const decomposition::NativeProfileSpec& spec) { +static bool maybeFuseRun(IRRewriter& rewriter, OneQubitRun& run, + const decomposition::EulerBasis basis, + const decomposition::NativeProfileSpec& spec) { Matrix2x2 fused = Matrix2x2::identity(); for (UnitaryOpInterface u : run.ops) { Matrix2x2 m; @@ -346,9 +351,9 @@ bool maybeFuseRun(IRRewriter& rewriter, OneQubitRun& run, return true; } -bool hasNonNativeOps(Operation* root, - const decomposition::NativeProfileSpec& spec, - bool singleQubitOnly) { +static bool hasNonNativeOps(Operation* root, + const decomposition::NativeProfileSpec& spec, + bool singleQubitOnly) { const mlir::WalkResult walkResult = root->walk([&](Operation* op) { if (!isWalkableUnitaryShell(op)) { return mlir::WalkResult::advance(); @@ -369,11 +374,10 @@ bool hasNonNativeOps(Operation* root, return walkResult.wasInterrupted(); } -LogicalResult synthesizeTwoQubitOp(IRRewriter& rewriter, Operation* op, - Location loc, Value in0, Value in1, - const decomposition::NativeProfileSpec& spec, - llvm::Twine matrixErrorMsg, - llvm::Twine synthesisErrorMsg) { +static LogicalResult synthesizeTwoQubitOp( + IRRewriter& rewriter, Operation* op, Location loc, Value in0, Value in1, + const decomposition::NativeProfileSpec& spec, llvm::Twine matrixErrorMsg, + llvm::Twine synthesisErrorMsg) { if (decomposition::allowsOp(op, spec)) { return success(); } @@ -394,6 +398,8 @@ LogicalResult synthesizeTwoQubitOp(IRRewriter& rewriter, Operation* op, return success(); } +namespace { + struct FuseTwoQubitWindowPattern : public OpInterfaceRewritePattern { FuseTwoQubitWindowPattern(MLIRContext* ctx, @@ -441,7 +447,9 @@ struct FuseTwoQubitWindowPattern decomposition::NativeProfileSpec spec; }; -LogicalResult +} // namespace + +static LogicalResult fuseTwoQubitUnitaryRuns(Operation* root, const decomposition::NativeProfileSpec& spec) { RewritePatternSet patterns(root->getContext()); @@ -449,6 +457,8 @@ fuseTwoQubitUnitaryRuns(Operation* root, return applyPatternsGreedily(root, std::move(patterns)); } +namespace { + struct FuseTwoQubitUnitaryRunsPass : impl::FuseTwoQubitUnitaryRunsBase { FuseTwoQubitUnitaryRunsPass() = default; @@ -640,11 +650,11 @@ struct FuseTwoQubitUnitaryRunsPass synthesizeBareTwoQubit(IRRewriter& rewriter, Operation* op, UnitaryOpInterface unitary, const decomposition::NativeProfileSpec& spec) { - const llvm::Twine error = - "unsupported two-qubit operation for selected profile"; - return synthesizeTwoQubitOp(rewriter, op, op->getLoc(), - unitary.getInputQubit(0), - unitary.getInputQubit(1), spec, error, error); + return synthesizeTwoQubitOp( + rewriter, op, op->getLoc(), unitary.getInputQubit(0), + unitary.getInputQubit(1), spec, + "unsupported two-qubit operation for selected profile", + "unsupported two-qubit operation for selected profile"); } }; diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index 8e71268d98..0debdb32ec 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -18,7 +18,7 @@ #include #include -#include +#include #include #include #include From 3ca7f74c40500f3ef9bc50243e8958a2e0446757 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 22 Jun 2026 18:22:28 +0200 Subject: [PATCH 064/122] =?UTF-8?q?=E2=9C=A8=20Weyl=20decomposition?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/Decomposition/Euler.h | 25 + .../QCO/Transforms/Decomposition/Weyl.h | 187 ++++ mlir/include/mlir/Dialect/QCO/Utils/Matrix.h | 27 + .../QCO/Transforms/Decomposition/Euler.cpp | 25 +- .../QCO/Transforms/Decomposition/Weyl.cpp | 977 ++++++++++++++++++ mlir/lib/Dialect/QCO/Utils/Matrix.cpp | 67 ++ .../Transforms/Decomposition/CMakeLists.txt | 18 +- .../test_euler_decomposition.cpp | 12 - .../Decomposition/test_weyl_decomposition.cpp | 256 +++++ 9 files changed, 1553 insertions(+), 41 deletions(-) create mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h create mode 100644 mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp create mode 100644 mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h index 8fb0018b28..7cea37b3a6 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h @@ -42,6 +42,31 @@ enum class EulerBasis : std::uint8_t { */ [[nodiscard]] std::optional parseEulerBasis(StringRef basis); +/** + * @brief Euler angles `(theta, phi, lambda)` and global phase for a 2x2 + * unitary. + * + * The decomposition obeys `matrix == e^{i*phase} * K(phi) * A(theta) * + * K(lambda)` where `(K, A)` are the rotation axes of the chosen @ref + * EulerBasis. + */ +struct EulerAngles { + double theta = 0.0; ///< Middle rotation angle. + double phi = 0.0; ///< First outer rotation angle. + double lambda = 0.0; ///< Second outer rotation angle. + double phase = 0.0; ///< Global phase in radians. +}; + +/** + * @brief Extracts `(theta, phi, lambda, phase)` of @p matrix in @p basis. + * + * @param matrix The single-qubit unitary to decompose. + * @param basis The target Euler basis. + * @return The extracted Euler angles and global phase. + */ +[[nodiscard]] EulerAngles anglesFromUnitary(const Matrix2x2& matrix, + EulerBasis basis); + /** * @brief Synthesizes a composed single-qubit unitary as gates in @p basis. * diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h new file mode 100644 index 0000000000..ac7ef42cee --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h @@ -0,0 +1,187 @@ +/* + * 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/Utils/Matrix.h" + +#include + +#include +#include +#include +#include + +namespace mlir::qco::decomposition { + +/** Numeric tolerance for Weyl internal matrix checks. */ +inline constexpr double WEYL_TOLERANCE = 100 * MATRIX_TOLERANCE; + +/** + * @brief Weyl decomposition of a 2-qubit unitary. + * + * A 4x4 unitary is factored as + * `(K1l ⊗ K1r) · U_canon(a,b,c) · (K2l ⊗ K2r)` up to global phase, where + * `U_canon(a,b,c) = RXX(-2a) · RYY(-2b) · RZZ(-2c)`. + * + * @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 + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Any modifications or derivative works of this code must retain this + * copyright notice, and modified files need to carry a notice + * indicating that they have been altered from the originals. + */ +class TwoQubitWeylDecomposition { +public: + [[nodiscard]] static TwoQubitWeylDecomposition + create(const Matrix4x4& unitaryMatrix, std::optional fidelity); + + [[nodiscard]] Matrix4x4 getCanonicalMatrix() const { + return getCanonicalMatrix(a_, b_, c_); + } + + [[nodiscard]] double a() const { return a_; } + [[nodiscard]] double b() const { return b_; } + [[nodiscard]] double c() const { return c_; } + [[nodiscard]] double globalPhase() const { return globalPhase_; } + + /** + * @brief Left single-qubit factor after the canonical gate. + * + * ``` + * q1 - k2r - C - k1r - + * A + * q0 - k2l - N - k1l - + * ``` + */ + [[nodiscard]] const Matrix2x2& k1l() const { return k1l_; } + /** + * @brief Left single-qubit factor before the canonical gate. + */ + [[nodiscard]] const Matrix2x2& k2l() const { return k2l_; } + /** + * @brief Right single-qubit factor after the canonical gate. + */ + [[nodiscard]] const Matrix2x2& k1r() const { return k1r_; } + /** + * @brief Right single-qubit factor before the canonical gate. + */ + [[nodiscard]] const Matrix2x2& k2r() const { return k2r_; } + + [[nodiscard]] static Matrix4x4 getCanonicalMatrix(double a, double b, + double c); + +private: + bool applySpecialization(const std::optional& requestedFidelity); + + double a_{}; + double b_{}; + double c_{}; + double globalPhase_{}; + Matrix2x2 k1l_; + Matrix2x2 k2l_; + Matrix2x2 k1r_; + Matrix2x2 k2r_; +}; + +/** + * @brief Single-qubit factors and entangler count for basis-gate synthesis. + * + * Factors are stored in emission order. For `i` in `[0, numBasisUses)` the + * pair `(singleQubitFactors[2*i], singleQubitFactors[2*i + 1])` is applied to + * qubits `1` and `0` respectively, followed by one entangler. The final pair + * `(singleQubitFactors[2*numBasisUses], singleQubitFactors[2*numBasisUses+1])` + * is applied after the last entangler. + */ +struct TwoQubitNativeDecomposition { + std::uint8_t numBasisUses = 0; + SmallVector singleQubitFactors; + double globalPhase = 0.0; +}; + +/** + * @brief Decomposer for a fixed two-qubit basis gate (e.g. CX/CZ). + * + * @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 + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Any modifications or derivative works of this code must retain this + * copyright notice, and modified files need to carry a notice + * indicating that they have been altered from the originals. + */ +class TwoQubitBasisDecomposer { +public: + [[nodiscard]] static TwoQubitBasisDecomposer + create(const Matrix4x4& basisMatrix, double basisFidelity); + + [[nodiscard]] std::optional + twoQubitDecompose(const TwoQubitWeylDecomposition& targetDecomposition, + std::optional numBasisGateUses) const; + +private: + struct SmbPrecomputed { + Matrix2x2 u0l; + Matrix2x2 u0r; + Matrix2x2 u1l; + Matrix2x2 u1ra; + Matrix2x2 u1rb; + Matrix2x2 u2la; + Matrix2x2 u2lb; + Matrix2x2 u2ra; + Matrix2x2 u2rb; + Matrix2x2 u3l; + Matrix2x2 u3r; + Matrix2x2 q0l; + Matrix2x2 q0r; + Matrix2x2 q1la; + Matrix2x2 q1lb; + Matrix2x2 q1ra; + Matrix2x2 q1rb; + Matrix2x2 q2l; + Matrix2x2 q2r; + }; + + [[nodiscard]] static SmallVector + decomp0(const TwoQubitWeylDecomposition& target); + [[nodiscard]] SmallVector + decomp1(const TwoQubitWeylDecomposition& target) const; + [[nodiscard]] SmallVector + decomp2Supercontrolled(const TwoQubitWeylDecomposition& target) const; + [[nodiscard]] SmallVector + decomp3Supercontrolled(const TwoQubitWeylDecomposition& target) const; + [[nodiscard]] std::array, 4> + traces(const TwoQubitWeylDecomposition& target) const; + + double basisFidelity{}; + TwoQubitWeylDecomposition basisDecomposer; + bool isSuperControlled{}; + SmbPrecomputed smb{}; +}; + +/** + * @brief Decomposes @p target with respect to a two-qubit basis gate matrix. + */ +[[nodiscard]] std::optional +decomposeTwoQubitWithBasis( + const Matrix4x4& target, const Matrix4x4& basisMatrix, + double basisFidelity = 1.0, + std::optional numBasisUses = std::nullopt); + +} // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h index cdea703ed4..60324d3873 100644 --- a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h +++ b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h @@ -795,4 +795,31 @@ struct SymmetricEigen4 { Matrix4x4 eigenvectors{}; }; +/** @brief Constant-folded `RX(theta)` unitary. */ +[[nodiscard]] Matrix2x2 rxMatrix(double theta); + +/** @brief Constant-folded `RY(theta)` unitary. */ +[[nodiscard]] Matrix2x2 ryMatrix(double theta); + +/** @brief Constant-folded `RZ(theta)` unitary. */ +[[nodiscard]] Matrix2x2 rzMatrix(double theta); + +/** @brief `i` times Pauli-X. */ +[[nodiscard]] const Matrix2x2& iPauliX(); + +/** @brief `i` times Pauli-Y. */ +[[nodiscard]] const Matrix2x2& iPauliY(); + +/** @brief `i` times Pauli-Z. */ +[[nodiscard]] const Matrix2x2& iPauliZ(); + +/** @brief Constant-folded two-qubit `RXX(theta)` unitary. */ +[[nodiscard]] Matrix4x4 rxxMatrix(double theta); + +/** @brief Constant-folded two-qubit `RYY(theta)` unitary. */ +[[nodiscard]] Matrix4x4 ryyMatrix(double theta); + +/** @brief Constant-folded two-qubit `RZZ(theta)` unitary. */ +[[nodiscard]] Matrix4x4 rzzMatrix(double theta); + } // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index 085d493abf..077cdde0e1 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -102,21 +102,6 @@ static void emitGPhaseIfNeeded(OpBuilder& builder, Location loc, double phase) { // Euler decomposition (angles) //===----------------------------------------------------------------------===// -/** - * @brief Euler angles `(theta, phi, lambda)` and global phase for a 2x2 - * unitary. - */ -namespace { - -struct EulerAngles { - double theta = 0.0; ///< Middle rotation angle. - double phi = 0.0; ///< First outer rotation angle. - double lambda = 0.0; ///< Second outer rotation angle. - double phase = 0.0; ///< Global phase in radians. -}; - -} // namespace - /** * @brief Z-Y-Z Euler angles and global phase for a 2x2 unitary. * @@ -197,15 +182,7 @@ struct EulerAngles { .phase = phase - (0.5 * (phi + lambda))}; } -/** - * @brief Extracts `(theta, phi, lambda, phase)` for all Euler bases. - * - * @param matrix The single-qubit unitary to decompose. - * @param basis The target Euler basis. - * @return The extracted Euler angles and global phase. - */ -[[nodiscard]] static EulerAngles anglesFromUnitary(const Matrix2x2& matrix, - const EulerBasis basis) { +EulerAngles anglesFromUnitary(const Matrix2x2& matrix, const EulerBasis basis) { switch (basis) { case EulerBasis::ZYZ: case EulerBasis::ZSXX: diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp new file mode 100644 index 0000000000..8b47c02c87 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp @@ -0,0 +1,977 @@ +/* + * 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/Weyl.h" + +#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mlir::qco::decomposition { + +using namespace std::complex_literals; + +namespace { + +enum class Specialization : std::uint8_t { + General, + IdEquiv, + SWAPEquiv, + PartialSWAPEquiv, + PartialSWAPFlipEquiv, + ControlledEquiv, + MirrorControlledEquiv, + FSimaabEquiv, + FSimabbEquiv, + FSimabmbEquiv, +}; + +} // namespace + +static constexpr auto DIAGONALIZATION_PRECISION = WEYL_TOLERANCE / 10; + +/** Non-negative remainder of @p a modulo @p b (always in `[0, |b|)`). */ +static double remEuclid(const double a, const double b) { + if (b == 0.0) { + llvm::reportFatalInternalError("remEuclid expects non-zero divisor"); + } + const auto r = std::fmod(a, b); + return (r < 0.0) ? r + std::abs(b) : r; +} + +/** Maps `|tr(U^dag V)|` to average two-qubit gate fidelity. */ +static double traceToFidelity(const Complex& trace) { + const auto traceAbs = std::abs(trace); + return (4.0 + (traceAbs * traceAbs)) / 20.0; +} + +static constexpr double INV_SQRT2 = 1.0 / std::numbers::sqrt2; + +static Matrix4x4 magicBasisTransform(const Matrix4x4& unitary, + bool outOfMagicBasis) { + const Matrix4x4 bNonNormalized = Matrix4x4::fromElements( // + 1, 1i, 0, 0, // + 0, 0, 1i, 1, // + 0, 0, 1i, -1, // + 1, -1i, 0, 0); + const Matrix4x4 bNonNormalizedDagger = Matrix4x4::fromElements( // + 0.5, 0, 0, 0.5, // + Complex{0.0, -0.5}, 0, 0, Complex{0.0, 0.5}, // + 0, Complex{0.0, -0.5}, Complex{0.0, -0.5}, 0, // + 0, 0.5, -0.5, 0); + if (outOfMagicBasis) { + return bNonNormalizedDagger * unitary * bNonNormalized; + } + return bNonNormalized * unitary * bNonNormalizedDagger; +} + +static double 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.); +} + +static std::pair> +diagonalizeComplexSymmetric(const Matrix4x4& m, double precision) { + auto state = std::mt19937{2023}; + std::normal_distribution dist; + + const auto mReal = m.realPart(); + const auto mImag = m.imagPart(); + + double bestErr = 1e300; + constexpr auto maxDiagonalizationAttempts = 100; + for (int i = 0; i < maxDiagonalizationAttempts; ++i) { + double randA{}; + double randB{}; + if (i == 0) { + randA = 1.2602066112249388; + randB = 0.22317849046722027; + } else { + randA = dist(state); + randB = dist(state); + } + std::array m2Real{}; + for (std::size_t k = 0; k < m2Real.size(); ++k) { + m2Real[k] = (randA * mReal[k]) + (randB * mImag[k]); + } + const Matrix4x4 p = Matrix4x4::symmetricEigen4(m2Real).eigenvectors; + const std::array d = (p.transpose() * m * p).diagonal(); + + const auto compare = p * Matrix4x4::fromDiagonal(d) * p.transpose(); + { + double err = 0.0; + for (std::size_t r = 0; r < 4; ++r) { + for (std::size_t cc = 0; cc < 4; ++cc) { + err = std::max(err, std::abs(compare(r, cc) - m(r, cc))); + } + } + bestErr = std::min(bestErr, err); + } + if (compare.isApprox(m, precision)) { + assert((p.transpose() * p).isIdentity(WEYL_TOLERANCE)); + assert(std::abs(Matrix4x4::fromDiagonal(d).determinant() - 1.0) < + WEYL_TOLERANCE); + return std::make_pair(p, d); + } + } + llvm::reportFatalInternalError(llvm::formatv( + "TwoQubitWeylDecomposition: failed to diagonalize M2 ({0} iterations). " + "best error = {1:e}, precision = {2:e}", + maxDiagonalizationAttempts, bestErr, precision)); +} + +static std::tuple +decomposeTwoQubitProductGate(const Matrix4x4& specialUnitary) { + Matrix2x2 r = + Matrix2x2::fromElements(specialUnitary(0, 0), specialUnitary(0, 1), + specialUnitary(1, 0), specialUnitary(1, 1)); + auto detR = r.determinant(); + if (std::abs(detR) < 0.1) { + r = Matrix2x2::fromElements(specialUnitary(2, 0), specialUnitary(2, 1), + specialUnitary(3, 0), specialUnitary(3, 1)); + detR = r.determinant(); + } + if (std::abs(detR) < 0.1) { + llvm::reportFatalInternalError( + "decomposeTwoQubitProductGate: unable to decompose: det_r < 0.1"); + } + r *= (1.0 / std::sqrt(detR)); + const Matrix2x2 rTConj = r.adjoint(); + + Matrix4x4 temp = + specialUnitary * Matrix4x4::kron(Matrix2x2::identity(), rTConj); + + Matrix2x2 l = + Matrix2x2::fromElements(temp(0, 0), temp(0, 2), temp(2, 0), temp(2, 2)); + auto detL = l.determinant(); + if (std::abs(detL) < 0.9) { + llvm::reportFatalInternalError( + "decomposeTwoQubitProductGate: unable to decompose: detL < 0.9"); + } + l *= (1.0 / std::sqrt(detL)); + auto phase = std::arg(detL) / 2.; + + return {l, r, phase}; +} + +static std::complex 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)}; +} + +static Specialization +bestSpecialization(const TwoQubitWeylDecomposition& decomposition, + const std::optional& requestedFidelity) { + auto isClose = [&](double ap, double bp, double cp) -> bool { + auto tr = getTrace(decomposition.a(), decomposition.b(), decomposition.c(), + ap, bp, cp); + if (requestedFidelity) { + return traceToFidelity(tr) >= *requestedFidelity; + } + return false; + }; + + auto closestAbc = closestPartialSwap(decomposition.a(), decomposition.b(), + decomposition.c()); + auto closestAbMinusC = closestPartialSwap( + decomposition.a(), decomposition.b(), -decomposition.c()); + + if (isClose(0., 0., 0.)) { + return Specialization::IdEquiv; + } + if (isClose((std::numbers::pi / 4.0), (std::numbers::pi / 4.0), + (std::numbers::pi / 4.0)) || + isClose((std::numbers::pi / 4.0), (std::numbers::pi / 4.0), + -(std::numbers::pi / 4.0))) { + return Specialization::SWAPEquiv; + } + if (isClose(closestAbc, closestAbc, closestAbc)) { + return Specialization::PartialSWAPEquiv; + } + if (isClose(closestAbMinusC, closestAbMinusC, -closestAbMinusC)) { + return Specialization::PartialSWAPFlipEquiv; + } + if (isClose(decomposition.a(), 0., 0.)) { + return Specialization::ControlledEquiv; + } + if (isClose((std::numbers::pi / 4.0), (std::numbers::pi / 4.0), + decomposition.c())) { + return Specialization::MirrorControlledEquiv; + } + if (isClose((decomposition.a() + decomposition.b()) / 2., + (decomposition.a() + decomposition.b()) / 2., + decomposition.c())) { + return Specialization::FSimaabEquiv; + } + if (isClose(decomposition.a(), (decomposition.b() + decomposition.c()) / 2., + (decomposition.b() + decomposition.c()) / 2.)) { + return Specialization::FSimabbEquiv; + } + if (isClose(decomposition.a(), (decomposition.b() - decomposition.c()) / 2., + (decomposition.c() - decomposition.b()) / 2.)) { + return Specialization::FSimabmbEquiv; + } + return Specialization::General; +} + +static bool relativeEq(double lhs, double rhs, double epsilon, + double maxRelative) { + if (lhs == rhs) { + return true; + } + if (std::isinf(lhs) || std::isinf(rhs)) { + return false; + } + auto absDiff = std::abs(lhs - rhs); + 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; +} + +TwoQubitWeylDecomposition +TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, + std::optional fidelity) { + auto u = unitaryMatrix; + auto detU = u.determinant(); + // Project into SU(4) by dividing out the fourth root of det(U): for a 4x4 + // unitary, |det(U)| == 1 so `det^{-1/4}` both enforces det == 1 and removes + // the global phase. The extracted phase is tracked separately in + // `globalPhase` (quarter of arg(det) to match the fourth-root choice) so the + // caller can reconstruct the original matrix exactly if needed. + auto detPow = std::pow(detU, -0.25); + u *= detPow; // remove global phase from unitary matrix + auto globalPhase = std::arg(detU) / 4.; + + // Numerical drift can still leave tiny determinant errors after root + // normalization. Re-normalize once more instead of aborting. + auto detNormalized = u.determinant(); + if (std::abs(detNormalized - Complex{1.0, 0.0}) > WEYL_TOLERANCE && + std::abs(detNormalized) > WEYL_TOLERANCE) { + u *= std::pow(detNormalized, -0.25); + } + + // 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, /*outOfMagicBasis=*/true); + const Matrix4x4 m2 = uP.transpose() * uP; + + // diagonalization yields eigenvectors (p) and eigenvalues (d); + // p is used to calculate K1/K2 (and thus the single-qubit gates + // surrounding the canonical gate); d is 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) + constexpr double pi = std::numbers::pi; + std::array dReal{}; + for (std::size_t i = 0; i < d.size(); ++i) { + dReal[i] = -std::arg(d[i]) / 2.0; + } + dReal[3] = -dReal[0] - dReal[1] - dReal[2]; + std::array cs{}; + for (std::size_t i = 0; i < cs.size(); ++i) { + cs[i] = remEuclid((dReal[i] + dReal[3]) / 2.0, 2.0 * pi); + } + + // Reorder coordinates according to min(a, pi/2 - a) with + // a = x mod pi/2 for each Weyl coordinate x + std::array cstemp{}; + for (std::size_t i = 0; i < cs.size(); ++i) { + const auto tmp = remEuclid(cs[i], pi / 2.0); + cstemp[i] = std::min(tmp, (pi / 2.0) - tmp); + } + std::array order{0, 1, 2}; + std::ranges::stable_sort( + order, [&](auto a, auto b) { return cstemp[a] < cstemp[b]; }); + order = {order[1], order[2], order[0]}; + cs = {cs[order[0]], cs[order[1]], cs[order[2]]}; + { + const std::array reordered{dReal[order[0]], dReal[order[1]], + dReal[order[2]]}; + dReal[0] = reordered[0]; + dReal[1] = reordered[1]; + dReal[2] = reordered[2]; + } + + // update eigenvectors (columns of p) according to new order of + // weyl coordinates + const Matrix4x4 pOrig = p; + for (std::size_t i = 0; i < order.size(); ++i) { + p.setColumn(i, pOrig.column(order[i])); + } + // apply correction for determinant if necessary + if (p.determinant().real() < 0.0) { + auto lastColumn = p.column(3); + for (auto& entry : lastColumn) { + entry = -entry; + } + p.setColumn(3, lastColumn); + } + assert(std::abs(p.determinant() - 1.0) < WEYL_TOLERANCE); + + // re-create complex eigenvalue matrix; this matrix contains the + // parameters of the canonical gate which is later used in the + // verification. Since the matrix is diagonal, the matrix exponential is + // equivalent to the element-wise exponential function. + std::array tempDiag{}; + for (std::size_t k = 0; k < tempDiag.size(); ++k) { + tempDiag[k] = std::exp(1i * dReal[k]); + } + const Matrix4x4 temp = Matrix4x4::fromDiagonal(tempDiag); + + // combined matrix k1 of 1q gates after canonical gate + Matrix4x4 k1 = uP * p * temp; + // k1 must be orthogonal; the tolerance matches the iterative diagonalization + // residual rather than the (much tighter) default matrix tolerance. + assert((k1.transpose() * k1).isIdentity(WEYL_TOLERANCE)); + assert(k1.determinant().real() > 0.0); + k1 = magicBasisTransform(k1, /*outOfMagicBasis=*/false); + + // combined matrix k2 of 1q gates before canonical gate + Matrix4x4 k2 = p.adjoint(); + // k2 must be orthogonal; see the tolerance note on the k1 check above. + assert((k2.transpose() * k2).isIdentity(WEYL_TOLERANCE)); + assert(k2.determinant().real() > 0.0); + k2 = magicBasisTransform(k2, /*outOfMagicBasis=*/false); + + // ensure k1 and k2 are correct (when combined with the canonical gate + // parameters in-between, they are equivalent to u) + std::array tempConjDiag{}; + for (std::size_t k = 0; k < tempConjDiag.size(); ++k) { + tempConjDiag[k] = std::conj(tempDiag[k]); + } + assert((k1 * + magicBasisTransform(Matrix4x4::fromDiagonal(tempConjDiag), + /*outOfMagicBasis=*/false) * + k2) + .isApprox(u, WEYL_TOLERANCE)); + + // calculate k1 = K1l ⊗ K1r + auto [K1l, K1r, phaseL] = decomposeTwoQubitProductGate(k1); + // decompose k2 = K2l ⊗ K2r + auto [K2l, K2r, phaseR] = decomposeTwoQubitProductGate(k2); + assert(Matrix4x4::kron(K1l, K1r).isApprox(k1, WEYL_TOLERANCE)); + assert(Matrix4x4::kron(K2l, K2r).isApprox(k2, WEYL_TOLERANCE)); + // accumulate global phase + globalPhase += phaseL + phaseR; + + // Flip into Weyl chamber + if (cs[0] > (pi / 2.0)) { + cs[0] -= 3.0 * (pi / 2.0); + K1l = K1l * iPauliY(); + K1r = K1r * iPauliY(); + globalPhase += (pi / 2.0); + } + if (cs[1] > (pi / 2.0)) { + cs[1] -= 3.0 * (pi / 2.0); + K1l = K1l * iPauliX(); + K1r = K1r * iPauliX(); + globalPhase += (pi / 2.0); + } + auto conjs = 0; + if (cs[0] > (pi / 4.0)) { + cs[0] = (pi / 2.0) - cs[0]; + K1l = K1l * iPauliY(); + K2r = iPauliY() * K2r; + conjs += 1; + globalPhase -= (pi / 2.0); + } + if (cs[1] > (pi / 4.0)) { + cs[1] = (pi / 2.0) - cs[1]; + K1l = K1l * iPauliX(); + K2r = iPauliX() * K2r; + conjs += 1; + globalPhase += (pi / 2.0); + if (conjs == 1) { + globalPhase -= pi; + } + } + if (cs[2] > (pi / 2.0)) { + cs[2] -= 3.0 * (pi / 2.0); + K1l = K1l * iPauliZ(); + K1r = K1r * iPauliZ(); + globalPhase += (pi / 2.0); + if (conjs == 1) { + globalPhase -= pi; + } + } + if (conjs == 1) { + cs[2] = (pi / 2.0) - cs[2]; + K1l = K1l * iPauliZ(); + K2r = iPauliZ() * K2r; + globalPhase += (pi / 2.0); + } + if (cs[2] > (pi / 4.0)) { + cs[2] -= (pi / 2.0); + K1l = K1l * iPauliZ(); + K1r = K1r * iPauliZ(); + globalPhase -= (pi / 2.0); + } + + // 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; + + // make sure decomposition is equal to input + assert((Matrix4x4::kron(K1l, K1r) * decomposition.getCanonicalMatrix() * + Matrix4x4::kron(K2l, K2r) * std::exp(Complex{0.0, 1.0} * globalPhase)) + .isApprox(unitaryMatrix, WEYL_TOLERANCE)); + + // determine actual specialization of canonical gate so that the 1q + // matrices can potentially be simplified + auto flippedFromOriginal = decomposition.applySpecialization(fidelity); + + auto getTraceValue = [&]() { + if (flippedFromOriginal) { + return getTrace((pi / 2.0) - a, b, -c, decomposition.a_, decomposition.b_, + decomposition.c_); + } + return getTrace(a, b, c, decomposition.a_, decomposition.b_, + decomposition.c_); + }; + // use trace to calculate fidelity of applied specialization and + // adjust global phase + auto trace = getTraceValue(); + const double calculatedFidelity = 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 (fidelity && calculatedFidelity + 1.0e-13 < *fidelity) { + llvm::reportFatalInternalError(llvm::formatv( + "TwoQubitWeylDecomposition: Calculated fidelity of " + "specialization is worse than requested fidelity ({0:F4} vs {1:F4})!", + calculatedFidelity, *fidelity)); + } + decomposition.globalPhase_ += std::arg(trace); + + // final check if decomposition is still valid after specialization + assert((Matrix4x4::kron(decomposition.k1l_, decomposition.k1r_) * + decomposition.getCanonicalMatrix() * + Matrix4x4::kron(decomposition.k2l_, decomposition.k2r_) * + std::exp(Complex{0.0, 1.0} * decomposition.globalPhase_)) + .isApprox(unitaryMatrix, WEYL_TOLERANCE)); + + return decomposition; +} + +Matrix4x4 TwoQubitWeylDecomposition::getCanonicalMatrix(double a, double b, + double c) { + // Canonical gate `U_d(a, b, c) = exp(i * (a*XX + b*YY + c*ZZ))`. XX/YY/ZZ + // commute pairwise, so any product order is equivalent; the order below is + // chosen to match common Qiskit/QuantumFlow references. The negated rotation + // angles (`-2 * a`, ...) compensate for the `RXX/RYY/RZZ` convention + // `exp(-i * theta/2 * XX)`, so that the factored angles sum back to the + // intended `+a`, `+b`, `+c`. + const auto xx = rxxMatrix(-2.0 * a); + const auto yy = ryyMatrix(-2.0 * b); + const auto zz = rzzMatrix(-2.0 * c); + return zz * yy * xx; +} + +bool TwoQubitWeylDecomposition::applySpecialization( + const std::optional& requestedFidelity) { + bool flippedFromOriginal = false; + const auto newSpecialization = bestSpecialization(*this, requestedFidelity); + 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; + } + + 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_ = Matrix2x2::identity(); + k1r_ = k1r_ * k2r_; + k2r_ = Matrix2x2::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_ = Matrix2x2::identity(); + k2r_ = Matrix2x2::identity(); + } else { + flippedFromOriginal = true; + + globalPhase_ += (std::numbers::pi / 2.0); + k1l_ = k1l_ * iPauliZ() * k2r_; + k1r_ = k1r_ * iPauliZ() * k2l_; + k2l_ = Matrix2x2::identity(); + k2r_ = Matrix2x2::identity(); + } + a_ = (std::numbers::pi / 4.0); + b_ = (std::numbers::pi / 4.0); + c_ = (std::numbers::pi / 4.0); + } 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_.adjoint(); + + a_ = closest; + b_ = closest; + c_ = closest; + // unmodified global phase + k1l_ = k1l_ * k2l_; + k1r_ = k1r_ * k2l_; + k2r_ = k2lDagger * k2r_; + k2l_ = Matrix2x2::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_.adjoint(); + + a_ = closest; + b_ = closest; + c_ = -closest; + // unmodified global phase + k1l_ = k1l_ * k2l_; + k1r_ = k1r_ * iPauliZ() * k2l_ * iPauliZ(); + k2r_ = iPauliZ() * k2lDagger * iPauliZ() * k2r_; + k2l_ = Matrix2x2::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)` + const EulerBasis eulerBasis = EulerBasis::XYX; + const auto [k2ltheta, k2lphi, k2llambda, k2lphase] = + anglesFromUnitary(k2l_, eulerBasis); + const auto [k2rtheta, k2rphi, k2rlambda, k2rphase] = + anglesFromUnitary(k2r_, EulerBasis::XYX); + // 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); + } 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)` + const auto [k2ltheta, k2lphi, k2llambda, k2lphase] = + anglesFromUnitary(k2l_, EulerBasis::ZYZ); + const auto [k2rtheta, k2rphi, k2rlambda, k2rphase] = + anglesFromUnitary(k2r_, EulerBasis::ZYZ); + a_ = (std::numbers::pi / 4.0); + b_ = (std::numbers::pi / 4.0); + // 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] = + 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] = + 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_; + } 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] = + 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_ * iPauliZ() * rxMatrix(k2lphi) * iPauliZ(); + k2r_ = iPauliZ() * rxMatrix(-k2lphi) * iPauliZ() * k2r_; + } else { + llvm::reportFatalInternalError( + "Unknown specialization for Weyl decomposition!"); + } + return flippedFromOriginal; +} + +TwoQubitBasisDecomposer +TwoQubitBasisDecomposer::create(const Matrix4x4& basisMatrix, + double basisFidelity) { + const Matrix2x2 k12RArr = Matrix2x2::fromElements( + 1i * INV_SQRT2, INV_SQRT2, -INV_SQRT2, -1i * INV_SQRT2); + const Matrix2x2 k12LArr = + Matrix2x2::fromElements(Complex{0.5, 0.5}, Complex{0.5, 0.5}, + Complex{-0.5, 0.5}, Complex{0.5, -0.5}); + + const auto basisDecomposer = + TwoQubitWeylDecomposition::create(basisMatrix, basisFidelity); + const auto isSuperControlled = + relativeEq(basisDecomposer.a(), std::numbers::pi / 4.0, 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(); + Complex temp{0.5, -0.5}; + const Matrix2x2 k11l = Matrix2x2::fromElements( + temp * (-1i * std::exp(-1i * b)), temp * std::exp(-1i * b), + temp * (-1i * std::exp(1i * b)), temp * -std::exp(1i * b)); + const Matrix2x2 k11r = Matrix2x2::fromElements( + INV_SQRT2 * (1i * std::exp(-1i * b)), INV_SQRT2 * -std::exp(-1i * b), + INV_SQRT2 * std::exp(1i * b), INV_SQRT2 * (-1i * std::exp(1i * b))); + const Matrix2x2 k32lK21l = Matrix2x2::fromElements( + INV_SQRT2 * Complex{1., std::cos(2. * b)}, + INV_SQRT2 * (1i * std::sin(2. * b)), INV_SQRT2 * (1i * std::sin(2. * b)), + INV_SQRT2 * Complex{1., -std::cos(2. * b)}); + temp = Complex{0.5, 0.5}; + const Matrix2x2 k21r = Matrix2x2::fromElements( + temp * (-1i * std::exp(-2i * b)), temp * std::exp(-2i * b), + temp * (1i * std::exp(2i * b)), temp * std::exp(2i * b)); + const Matrix2x2 k22l = + Matrix2x2::fromElements(INV_SQRT2, -INV_SQRT2, INV_SQRT2, INV_SQRT2); + const Matrix2x2 k22r = Matrix2x2::fromElements(0, 1, -1, 0); + const Matrix2x2 k31l = Matrix2x2::fromElements( + INV_SQRT2 * std::exp(-1i * b), INV_SQRT2 * std::exp(-1i * b), + INV_SQRT2 * -std::exp(1i * b), INV_SQRT2 * std::exp(1i * b)); + const Matrix2x2 k31r = Matrix2x2::fromElements(1i * std::exp(1i * b), 0, 0, + -1i * std::exp(-1i * b)); + const Matrix2x2 k32r = Matrix2x2::fromElements( + 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().adjoint(); + auto k1rDagger = basisDecomposer.k1r().adjoint(); + auto k2lDagger = basisDecomposer.k2l().adjoint(); + auto k2rDagger = basisDecomposer.k2r().adjoint(); + // 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.adjoint() * k1lDagger; + auto q0r = k12RArr.adjoint() * iPauliZ() * k1rDagger; + auto q1la = k2lDagger * k11l.adjoint(); + auto q1lb = k11l * k1lDagger; + auto q1ra = k2rDagger * iPauliZ() * k11r.adjoint(); + auto q1rb = k11r * k1rDagger; + auto q2l = k2lDagger * k12LArr; + auto q2r = k2rDagger * k12RArr; + + TwoQubitBasisDecomposer decomposer; + decomposer.basisFidelity = basisFidelity; + decomposer.basisDecomposer = basisDecomposer; + decomposer.isSuperControlled = isSuperControlled; + decomposer.smb.u0l = u0l; + decomposer.smb.u0r = u0r; + decomposer.smb.u1l = u1l; + decomposer.smb.u1ra = u1ra; + decomposer.smb.u1rb = u1rb; + decomposer.smb.u2la = u2la; + decomposer.smb.u2lb = u2lb; + decomposer.smb.u2ra = u2ra; + decomposer.smb.u2rb = u2rb; + decomposer.smb.u3l = u3l; + decomposer.smb.u3r = u3r; + decomposer.smb.q0l = q0l; + decomposer.smb.q0r = q0r; + decomposer.smb.q1la = q1la; + decomposer.smb.q1lb = q1lb; + decomposer.smb.q1ra = q1ra; + decomposer.smb.q1rb = q1rb; + decomposer.smb.q2l = q2l; + decomposer.smb.q2r = q2r; + return decomposer; +} + +std::optional +TwoQubitBasisDecomposer::twoQubitDecompose( + const TwoQubitWeylDecomposition& targetDecomposition, + std::optional numBasisGateUses) const { + auto traces = this->traces(targetDecomposition); + auto getDefaultNbasis = [&]() -> std::uint8_t { + // Pick the number of basis gate uses `i ∈ {0, 1, 2, 3}` that maximizes + // expected_fidelity(i) = traceToFidelity(traces[i]) * basisFidelity^i. + auto bestValue = std::numeric_limits::lowest(); + auto bestIndex = -1; + for (int i = 0; std::cmp_less(i, traces.size()); ++i) { + auto value = traceToFidelity(traces[i]) * std::pow(basisFidelity, i); + if (std::isnan(value)) { + continue; + } + if (value > bestValue) { + bestIndex = i; + bestValue = value; + } + } + if (bestIndex < 0) { + llvm::reportFatalInternalError("Unable to select basis-gate count: all " + "candidate fidelities are NaN"); + } + 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(""); + }; + SmallVector factors = chooseDecomposition(); + + double globalPhase = targetDecomposition.globalPhase(); + globalPhase -= bestNbasis * basisDecomposer.globalPhase(); + if (bestNbasis == 2) { + // The two-basis (2x CX/CZ) template in `decomp2Supercontrolled` produces + // a sequence whose global phase is off by `pi` relative to the target; + // compensate here so the emitted sequence reproduces the target unitary + // exactly, not just up to sign. + globalPhase += std::numbers::pi; + } + // large global phases can be generated by the decomposition, thus limit + // it to [0, +2*pi) + globalPhase = remEuclid(globalPhase, 2.0 * std::numbers::pi); + + return TwoQubitNativeDecomposition{ + .numBasisUses = bestNbasis, + .singleQubitFactors = std::move(factors), + .globalPhase = globalPhase, + }; +} + +SmallVector +TwoQubitBasisDecomposer::decomp0(const TwoQubitWeylDecomposition& target) { + return SmallVector{ + target.k1r() * target.k2r(), + target.k1l() * target.k2l(), + }; +} + +SmallVector TwoQubitBasisDecomposer::decomp1( + const TwoQubitWeylDecomposition& target) const { + // may not work for z != 0 and c != 0 (not always in Weyl chamber) + return SmallVector{ + basisDecomposer.k2r().adjoint() * target.k2r(), + basisDecomposer.k2l().adjoint() * target.k2l(), + target.k1r() * basisDecomposer.k1r().adjoint(), + target.k1l() * basisDecomposer.k1l().adjoint(), + }; +} + +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 SmallVector{ + smb.q2r * target.k2r(), + smb.q2l * target.k2l(), + smb.q1ra * rzMatrix(2. * target.b()) * smb.q1rb, + smb.q1la * rzMatrix(-2. * target.a()) * smb.q1lb, + target.k1r() * smb.q0r, + target.k1l() * smb.q0l, + }; +} + +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 SmallVector{ + smb.u3r * target.k2r(), + smb.u3l * target.k2l(), + smb.u2ra * rzMatrix(2. * target.b()) * smb.u2rb, + smb.u2la * rzMatrix(-2. * target.a()) * smb.u2lb, + smb.u1ra * rzMatrix(-2. * target.c()) * smb.u1rb, + smb.u1l, + target.k1r() * smb.u0r, + target.k1l() * smb.u0l, + }; +} + +std::array, 4> +TwoQubitBasisDecomposer::traces(const TwoQubitWeylDecomposition& target) const { + // Returns the Hilbert-Schmidt traces between the target canonical gate and + // the best candidate reachable with `0, 1, 2, 3` uses of the basis gate, + // respectively. Fed into `traceToFidelity` by `getDefaultNbasis` to pick + // the best basis-gate count. The closed-form expressions specialize + // `TwoQubitWeylDecomposition::getTrace(a, b, c, ap, bp, cp)` for: + // i == 0: no basis gate (ap == bp == cp == 0) + // i == 1: one basis use (ap == pi/4, bp == basis.b, cp == 0) + // i == 2: two basis uses (ap == 0, bp == 0, cp == -target.c) + // i == 3: three basis uses (target reachable exactly -> trace == 4) + // so the array has length 4 and is indexed by the number of basis uses. + 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((std::numbers::pi / 4.0) - target.a()) * + std::cos(basisDecomposer.b() - target.b()) * + std::cos(target.c()), + std::sin((std::numbers::pi / 4.0) - target.a()) * + std::sin(basisDecomposer.b() - target.b()) * + std::sin(target.c())}, + std::complex{4. * std::cos(target.c()), 0.}, + std::complex{4., 0.}, + }; +} + +std::optional +decomposeTwoQubitWithBasis(const Matrix4x4& target, + const Matrix4x4& basisMatrix, + const double basisFidelity, + const std::optional numBasisUses) { + const auto decomposer = + TwoQubitBasisDecomposer::create(basisMatrix, basisFidelity); + const auto weyl = TwoQubitWeylDecomposition::create(target, std::nullopt); + return decomposer.twoQubitDecompose(weyl, numBasisUses); +} + +} // namespace mlir::qco::decomposition diff --git a/mlir/lib/Dialect/QCO/Utils/Matrix.cpp b/mlir/lib/Dialect/QCO/Utils/Matrix.cpp index 50d524daa1..274b3417e3 100644 --- a/mlir/lib/Dialect/QCO/Utils/Matrix.cpp +++ b/mlir/lib/Dialect/QCO/Utils/Matrix.cpp @@ -1015,4 +1015,71 @@ DynamicMatrix operator*(const Complex& scalar, const DynamicMatrix& matrix) { return matrix * scalar; } +Matrix2x2 rxMatrix(const double theta) { + const auto halfTheta = theta / 2.0; + const Complex cos{std::cos(halfTheta), 0.0}; + const Complex isin{0.0, -std::sin(halfTheta)}; + return Matrix2x2::fromElements(cos, isin, isin, cos); +} + +Matrix2x2 ryMatrix(const double theta) { + const auto halfTheta = theta / 2.0; + const Complex cos{std::cos(halfTheta), 0.0}; + const Complex sin{std::sin(halfTheta), 0.0}; + return Matrix2x2::fromElements(cos, -sin, sin, cos); +} + +Matrix2x2 rzMatrix(const double theta) { + return Matrix2x2::fromElements( + Complex{std::cos(theta / 2.0), -std::sin(theta / 2.0)}, 0.0, 0.0, + Complex{std::cos(theta / 2.0), std::sin(theta / 2.0)}); +} + +const Matrix2x2& iPauliX() { + static const Matrix2x2 MATRIX = + Matrix2x2::fromElements(0.0, Complex{0.0, 1.0}, Complex{0.0, 1.0}, 0.0); + return MATRIX; +} + +const Matrix2x2& iPauliY() { + static const Matrix2x2 MATRIX = Matrix2x2::fromElements(0.0, 1.0, -1.0, 0.0); + return MATRIX; +} + +const Matrix2x2& iPauliZ() { + static const Matrix2x2 MATRIX = + Matrix2x2::fromElements(Complex{0.0, 1.0}, 0.0, 0.0, Complex{0.0, -1.0}); + return MATRIX; +} + +Matrix4x4 rxxMatrix(const double theta) { + const auto cosTheta = std::cos(theta / 2.0); + const Complex misin{0.0, -std::sin(theta / 2.0)}; + return Matrix4x4::fromElements(cosTheta, 0.0, 0.0, misin, // + 0.0, cosTheta, misin, 0.0, // + 0.0, misin, cosTheta, 0.0, // + misin, 0.0, 0.0, cosTheta); +} + +Matrix4x4 ryyMatrix(const double theta) { + const auto cosTheta = std::cos(theta / 2.0); + const Complex isin{0.0, std::sin(theta / 2.0)}; + const Complex misin{0.0, -std::sin(theta / 2.0)}; + return Matrix4x4::fromElements(cosTheta, 0.0, 0.0, isin, // + 0.0, cosTheta, misin, 0.0, // + 0.0, misin, cosTheta, 0.0, // + isin, 0.0, 0.0, cosTheta); +} + +Matrix4x4 rzzMatrix(const double theta) { + const auto cosTheta = std::cos(theta / 2.0); + const auto sinTheta = std::sin(theta / 2.0); + const Complex em{cosTheta, -sinTheta}; + const Complex ep{cosTheta, sinTheta}; + return Matrix4x4::fromElements(em, 0.0, 0.0, 0.0, // + 0.0, ep, 0.0, 0.0, // + 0.0, 0.0, ep, 0.0, // + 0.0, 0.0, 0.0, em); +} + } // namespace mlir::qco diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt index f493bb9e4d..c6160821cd 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt @@ -7,12 +7,20 @@ # Licensed under the MIT License set(target_name mqt-core-mlir-unittest-decomposition) -add_executable(${target_name} test_euler_decomposition.cpp) +add_executable(${target_name} test_euler_decomposition.cpp test_weyl_decomposition.cpp) -target_link_libraries(${target_name} PRIVATE GTest::gtest_main MLIRQCOProgramBuilder - MLIRQCOTransforms) -target_link_libraries(${target_name} PRIVATE MLIRPass MLIRFuncDialect MLIRArithDialect MLIRIR - MLIRSupport MLIRQTensorDialect) +target_link_libraries( + ${target_name} + PRIVATE GTest::gtest_main + MLIRQCOProgramBuilder + MLIRQCOTransforms + MLIRQCOUtils + MLIRPass + MLIRFuncDialect + MLIRArithDialect + MLIRIR + MLIRSupport + MLIRQTensorDialect) mqt_mlir_configure_unittest_target(${target_name}) diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp index fa9ba1d4ea..454012d489 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -104,18 +104,6 @@ class EulerSynthesisExactTest // Euler synthesis support //===----------------------------------------------------------------------===// -[[nodiscard]] static Matrix2x2 rzMatrix(const double theta) { - const auto m00 = std::polar(1.0, -theta / 2.0); - const auto m11 = std::polar(1.0, theta / 2.0); - return Matrix2x2::fromElements(m00, 0, 0, m11); -} - -[[nodiscard]] static Matrix2x2 ryMatrix(const double theta) { - const auto m00 = std::cos(theta / 2.0); - const auto m01 = -std::sin(theta / 2.0); - return Matrix2x2::fromElements(m00, m01, -m01, m00); -} - [[nodiscard]] static Matrix2x2 randomUnitaryMatrix(std::mt19937& rng) { std::uniform_real_distribution dist(-std::numbers::pi, std::numbers::pi); const Matrix2x2 su2 = 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..d25df3c2da --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -0,0 +1,256 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace mlir::qco; +using namespace mlir::qco::decomposition; + +static Complex globalPhaseFactor(const double phase) { + return std::exp(Complex{0.0, 1.0} * phase); +} + +static const Matrix4x4& twoQubitControlledX01() { + static const Matrix4x4 MATRIX = Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, // + 0.0, 1.0, 0.0, 0.0, // + 0.0, 0.0, 0.0, 1.0, // + 0.0, 0.0, 1.0, 0.0); + return MATRIX; +} + +static const Matrix4x4& twoQubitControlledX10() { + static const Matrix4x4 MATRIX = Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, // + 0.0, 0.0, 0.0, 1.0, // + 0.0, 0.0, 1.0, 0.0, // + 0.0, 1.0, 0.0, 0.0); + return MATRIX; +} + +static bool isUnitaryMatrix(const Matrix2x2& matrix, + const double tolerance = MATRIX_TOLERANCE) { + return (matrix.adjoint() * matrix).isIdentity(tolerance); +} + +static Matrix4x4 randomUnitary4x4(std::mt19937& rng) { + std::normal_distribution normalDist(0.0, 1.0); + std::vector columns(4, std::vector(4, std::complex{0.0, 0.0})); + for (auto& column : columns) { + for (auto& entry : column) { + entry = std::complex(normalDist(rng), normalDist(rng)); + } + } + for (std::size_t j = 0; j < 4; ++j) { + for (std::size_t k = 0; k < j; ++k) { + std::complex projection{0.0, 0.0}; + for (std::size_t i = 0; i < 4; ++i) { + projection += std::conj(columns[k][i]) * columns[j][i]; + } + for (std::size_t i = 0; i < 4; ++i) { + columns[j][i] -= projection * columns[k][i]; + } + } + double norm = 0.0; + for (std::size_t i = 0; i < 4; ++i) { + norm += std::norm(columns[j][i]); + } + norm = std::sqrt(norm); + for (std::size_t i = 0; i < 4; ++i) { + columns[j][i] /= norm; + } + } + const Matrix4x4 unitary = Matrix4x4::fromElements( + columns[0][0], columns[1][0], columns[2][0], columns[3][0], columns[0][1], + columns[1][1], columns[2][1], columns[3][1], columns[0][2], columns[1][2], + columns[2][2], columns[3][2], columns[0][3], columns[1][3], columns[2][3], + columns[3][3]); + assert((unitary.adjoint() * unitary).isIdentity(WEYL_TOLERANCE)); + return unitary; +} + +static Matrix4x4 restoreWeyl(const TwoQubitWeylDecomposition& decomposition) { + return Matrix4x4::kron(decomposition.k1l(), decomposition.k1r()) * + decomposition.getCanonicalMatrix() * + Matrix4x4::kron(decomposition.k2l(), decomposition.k2r()) * + globalPhaseFactor(decomposition.globalPhase()); +} + +static Matrix4x4 restoreBasis(const TwoQubitNativeDecomposition& decomposition, + const Matrix4x4& entangler) { + const auto& factors = decomposition.singleQubitFactors; + const auto layer = [&](std::size_t i) { + return Matrix4x4::kron(factors[(2 * i) + 1], factors[2 * i]); + }; + Matrix4x4 matrix = layer(0); + for (std::uint8_t i = 0; i < decomposition.numBasisUses; ++i) { + matrix = entangler * matrix; + matrix = layer(static_cast(i) + 1) * matrix; + } + return matrix * globalPhaseFactor(decomposition.globalPhase); +} + +static auto productMatrixCases() { + return ::testing::Values( + []() { return Matrix4x4::identity(); }, + []() { return Matrix4x4::kron(rzMatrix(1.0), ryMatrix(3.1)); }, + []() { return Matrix4x4::kron(Matrix2x2::identity(), rxMatrix(0.1)); }); +} + +static auto entangledMatrixCases() { + return ::testing::Values( + []() { return rzzMatrix(2.0); }, + []() { return ryyMatrix(1.0) * rzzMatrix(3.0) * rxxMatrix(2.0); }, + []() { + return TwoQubitWeylDecomposition::getCanonicalMatrix(1.5, -0.2, 0.0) * + Matrix4x4::kron(rxMatrix(1.0), Matrix2x2::identity()); + }, + []() { + return Matrix4x4::kron(rxMatrix(1.0), ryMatrix(1.0)) * + TwoQubitWeylDecomposition::getCanonicalMatrix(1.1, 0.2, 3.0) * + Matrix4x4::kron(rxMatrix(1.0), Matrix2x2::identity()); + }, + []() { + return Matrix4x4::kron(HOp::getUnitaryMatrix(), iPauliZ()) * + twoQubitControlledX01() * Matrix4x4::kron(iPauliX(), iPauliY()); + }); +} + +static auto cxBasisCases() { + return ::testing::Values([]() { return twoQubitControlledX01(); }, + []() { return twoQubitControlledX10(); }); +} + +TEST(DecompositionHelpersTest, MatrixUtilitySanity) { + EXPECT_NEAR(std::abs(globalPhaseFactor(1.25)), 1.0, 1e-14); + EXPECT_FALSE(isUnitaryMatrix(Matrix2x2::fromElements(2.0, 0.0, 0.0, 2.0))); + EXPECT_TRUE(isUnitaryMatrix(Matrix2x2::identity())); +} + +class WeylDecompositionTest : public testing::TestWithParam {}; + +class BasisDecomposerTest : public testing::TestWithParam< + std::tuple> { +protected: + void SetUp() override { + basisMatrix = std::get<0>(GetParam())(); + target = std::get<1>(GetParam())(); + targetDecomposition = std::make_unique( + TwoQubitWeylDecomposition::create(target, 1.0)); + } + + Matrix4x4 target; + Matrix4x4 basisMatrix; + std::unique_ptr targetDecomposition; +}; + +TEST_P(WeylDecompositionTest, ReconstructsWithinRequestedFidelity) { + const Matrix4x4 originalMatrix = GetParam()(); + for (const double fidelity : {1.0, 1.0 - 1e-12}) { + const auto decomposition = + TwoQubitWeylDecomposition::create(originalMatrix, fidelity); + EXPECT_TRUE(restoreWeyl(decomposition).isApprox(originalMatrix)); + } +} + +TEST(WeylDecompositionStandalone, + CnotProducesValidWeylParametersAndUnitaryLocals) { + const Matrix4x4 cnot = + Matrix4x4::fromElements(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0); + const auto decomp = TwoQubitWeylDecomposition::create(cnot, std::nullopt); + constexpr double piOver4 = 0.7853981633974483; + for (const double angle : {decomp.a(), decomp.b(), decomp.c()}) { + EXPECT_GE(angle, -1e-10); + EXPECT_LE(angle, piOver4 + 1e-10); + } + EXPECT_TRUE(isUnitaryMatrix(decomp.k1l())); + EXPECT_TRUE(isUnitaryMatrix(decomp.k2l())); + EXPECT_TRUE(isUnitaryMatrix(decomp.k1r())); + EXPECT_TRUE(isUnitaryMatrix(decomp.k2r())); +} + +TEST(WeylDecompositionStandalone, Random) { + std::mt19937 rng{1234567UL}; + for (int i = 0; i < 5000; ++i) { + const Matrix4x4 originalMatrix = randomUnitary4x4(rng); + const auto decomposition = TwoQubitWeylDecomposition::create( + originalMatrix, std::optional{1.0 - 1e-12}); + EXPECT_TRUE( + restoreWeyl(decomposition).isApprox(originalMatrix, WEYL_TOLERANCE)); + } +} + +INSTANTIATE_TEST_SUITE_P(ProductTwoQubitMatrices, WeylDecompositionTest, + productMatrixCases()); +INSTANTIATE_TEST_SUITE_P(TwoQubitMatrices, WeylDecompositionTest, + entangledMatrixCases()); + +TEST_P(BasisDecomposerTest, ReconstructsWithinRequestedFidelity) { + for (const double fidelity : {1.0, 1.0 - 1e-12}) { + const auto decomposer = + TwoQubitBasisDecomposer::create(basisMatrix, fidelity); + const auto decomposed = + decomposer.twoQubitDecompose(*targetDecomposition, std::nullopt); + ASSERT_TRUE(decomposed.has_value()); + EXPECT_TRUE(restoreBasis(*decomposed, basisMatrix).isApprox(target)); + } +} + +TEST(BasisDecomposerTest, Random) { + std::mt19937 rng{123456UL}; + const mlir::SmallVector basisMatrices{twoQubitControlledX01(), + twoQubitControlledX10()}; + std::uniform_int_distribution distBasisGate{0, 1}; + + for (int i = 0; i < 2000; ++i) { + const Matrix4x4 originalMatrix = randomUnitary4x4(rng); + const auto targetDecomposition = TwoQubitWeylDecomposition::create( + originalMatrix, std::optional{1.0}); + const Matrix4x4 basisMatrix = basisMatrices[distBasisGate(rng)]; + const auto decomposer = TwoQubitBasisDecomposer::create(basisMatrix, 1.0); + const auto decomposed = + decomposer.twoQubitDecompose(targetDecomposition, std::nullopt); + ASSERT_TRUE(decomposed.has_value()); + EXPECT_TRUE(restoreBasis(*decomposed, basisMatrix) + .isApprox(originalMatrix, WEYL_TOLERANCE)); + } +} + +TEST(BasisDecomposerNumBasisTest, ForcesZeroBasisUsesForIdentityTarget) { + const Matrix4x4 basis = twoQubitControlledX01(); + const auto decomposer = TwoQubitBasisDecomposer::create(basis, 1.0); + const Matrix4x4 target = Matrix4x4::identity(); + const auto weyl = TwoQubitWeylDecomposition::create(target, 1.0); + const auto decomposed = decomposer.twoQubitDecompose(weyl, std::uint8_t{0}); + ASSERT_TRUE(decomposed.has_value()); + EXPECT_EQ(decomposed->numBasisUses, 0); + EXPECT_TRUE(restoreBasis(*decomposed, basis).isApprox(target)); +} + +INSTANTIATE_TEST_SUITE_P(ProductTwoQubitMatrices, BasisDecomposerTest, + testing::Combine(cxBasisCases(), + productMatrixCases())); +INSTANTIATE_TEST_SUITE_P(TwoQubitMatrices, BasisDecomposerTest, + testing::Combine(cxBasisCases(), + entangledMatrixCases())); From 2f5ef130541474e3524677a0df0b33f8af04a005 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 22 Jun 2026 18:31:20 +0200 Subject: [PATCH 065/122] =?UTF-8?q?=E2=9C=A8=20Add=20specialized=20matrix?= =?UTF-8?q?=20test=20cases=20for=20Weyl=20decomposition=20and=20improve=20?= =?UTF-8?q?tolerance=20checks=20in=20existing=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Decomposition/test_weyl_decomposition.cpp | 49 +++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index d25df3c2da..8a5f34d17f 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -142,12 +142,50 @@ static auto cxBasisCases() { []() { return twoQubitControlledX10(); }); } +static auto specializedMatrixCases() { + return ::testing::Values( + []() { + return twoQubitControlledX01() * twoQubitControlledX10() * + twoQubitControlledX01(); + }, + []() { + return TwoQubitWeylDecomposition::getCanonicalMatrix(0.5, 0.5, 0.5); + }, + []() { + return TwoQubitWeylDecomposition::getCanonicalMatrix(0.5, 0.5, -0.5); + }, + []() { return twoQubitControlledX01() * twoQubitControlledX10(); }, + []() { + return TwoQubitWeylDecomposition::getCanonicalMatrix(0.5, 0.5, 0.1); + }, + []() { + return TwoQubitWeylDecomposition::getCanonicalMatrix(0.5, 0.1, 0.1); + }, + []() { + return TwoQubitWeylDecomposition::getCanonicalMatrix(0.5, 0.1, -0.1); + }); +} + TEST(DecompositionHelpersTest, MatrixUtilitySanity) { EXPECT_NEAR(std::abs(globalPhaseFactor(1.25)), 1.0, 1e-14); EXPECT_FALSE(isUnitaryMatrix(Matrix2x2::fromElements(2.0, 0.0, 0.0, 2.0))); EXPECT_TRUE(isUnitaryMatrix(Matrix2x2::identity())); } +TEST(DecompositionHelpersTest, GateMatrixFactoriesMatchCanonicalForm) { + for (const double theta : {0.0, 0.25, 1.0, 2.5, -1.3}) { + EXPECT_TRUE(rxxMatrix(theta).isApprox( + TwoQubitWeylDecomposition::getCanonicalMatrix(-theta / 2.0, 0.0, 0.0), + WEYL_TOLERANCE)); + EXPECT_TRUE(ryyMatrix(theta).isApprox( + TwoQubitWeylDecomposition::getCanonicalMatrix(0.0, -theta / 2.0, 0.0), + WEYL_TOLERANCE)); + EXPECT_TRUE(rzzMatrix(theta).isApprox( + TwoQubitWeylDecomposition::getCanonicalMatrix(0.0, 0.0, -theta / 2.0), + WEYL_TOLERANCE)); + } +} + class WeylDecompositionTest : public testing::TestWithParam {}; class BasisDecomposerTest : public testing::TestWithParam< @@ -170,7 +208,8 @@ TEST_P(WeylDecompositionTest, ReconstructsWithinRequestedFidelity) { for (const double fidelity : {1.0, 1.0 - 1e-12}) { const auto decomposition = TwoQubitWeylDecomposition::create(originalMatrix, fidelity); - EXPECT_TRUE(restoreWeyl(decomposition).isApprox(originalMatrix)); + EXPECT_TRUE( + restoreWeyl(decomposition).isApprox(originalMatrix, WEYL_TOLERANCE)); } } @@ -205,6 +244,8 @@ INSTANTIATE_TEST_SUITE_P(ProductTwoQubitMatrices, WeylDecompositionTest, productMatrixCases()); INSTANTIATE_TEST_SUITE_P(TwoQubitMatrices, WeylDecompositionTest, entangledMatrixCases()); +INSTANTIATE_TEST_SUITE_P(SpecializedMatrices, WeylDecompositionTest, + specializedMatrixCases()); TEST_P(BasisDecomposerTest, ReconstructsWithinRequestedFidelity) { for (const double fidelity : {1.0, 1.0 - 1e-12}) { @@ -213,7 +254,8 @@ TEST_P(BasisDecomposerTest, ReconstructsWithinRequestedFidelity) { const auto decomposed = decomposer.twoQubitDecompose(*targetDecomposition, std::nullopt); ASSERT_TRUE(decomposed.has_value()); - EXPECT_TRUE(restoreBasis(*decomposed, basisMatrix).isApprox(target)); + EXPECT_TRUE(restoreBasis(*decomposed, basisMatrix) + .isApprox(target, WEYL_TOLERANCE)); } } @@ -245,7 +287,8 @@ TEST(BasisDecomposerNumBasisTest, ForcesZeroBasisUsesForIdentityTarget) { const auto decomposed = decomposer.twoQubitDecompose(weyl, std::uint8_t{0}); ASSERT_TRUE(decomposed.has_value()); EXPECT_EQ(decomposed->numBasisUses, 0); - EXPECT_TRUE(restoreBasis(*decomposed, basis).isApprox(target)); + EXPECT_TRUE( + restoreBasis(*decomposed, basis).isApprox(target, WEYL_TOLERANCE)); } INSTANTIATE_TEST_SUITE_P(ProductTwoQubitMatrices, BasisDecomposerTest, From 61ed24571cc367ab0fe74519be9e7d965dd3511c Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 22 Jun 2026 18:36:44 +0200 Subject: [PATCH 066/122] =?UTF-8?q?=F0=9F=93=9D=20Simplify=20comments=20an?= =?UTF-8?q?d=20docstrings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/Decomposition/Weyl.h | 36 +--- mlir/include/mlir/Dialect/QCO/Utils/Matrix.h | 12 +- .../QCO/Transforms/Decomposition/Weyl.cpp | 165 ++---------------- 3 files changed, 31 insertions(+), 182 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h index ac7ef42cee..9b59eafaae 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h @@ -57,27 +57,13 @@ class TwoQubitWeylDecomposition { [[nodiscard]] double c() const { return c_; } [[nodiscard]] double globalPhase() const { return globalPhase_; } - /** - * @brief Left single-qubit factor after the canonical gate. - * - * ``` - * q1 - k2r - C - k1r - - * A - * q0 - k2l - N - k1l - - * ``` - */ + /** @brief Single-qubit factor on qubit 0 after the canonical gate. */ [[nodiscard]] const Matrix2x2& k1l() const { return k1l_; } - /** - * @brief Left single-qubit factor before the canonical gate. - */ + /** @brief Single-qubit factor on qubit 0 before the canonical gate. */ [[nodiscard]] const Matrix2x2& k2l() const { return k2l_; } - /** - * @brief Right single-qubit factor after the canonical gate. - */ + /** @brief Single-qubit factor on qubit 1 after the canonical gate. */ [[nodiscard]] const Matrix2x2& k1r() const { return k1r_; } - /** - * @brief Right single-qubit factor before the canonical gate. - */ + /** @brief Single-qubit factor on qubit 1 before the canonical gate. */ [[nodiscard]] const Matrix2x2& k2r() const { return k2r_; } [[nodiscard]] static Matrix4x4 getCanonicalMatrix(double a, double b, @@ -97,13 +83,11 @@ class TwoQubitWeylDecomposition { }; /** - * @brief Single-qubit factors and entangler count for basis-gate synthesis. + * @brief Native two-qubit synthesis result. * - * Factors are stored in emission order. For `i` in `[0, numBasisUses)` the - * pair `(singleQubitFactors[2*i], singleQubitFactors[2*i + 1])` is applied to - * qubits `1` and `0` respectively, followed by one entangler. The final pair - * `(singleQubitFactors[2*numBasisUses], singleQubitFactors[2*numBasisUses+1])` - * is applied after the last entangler. + * Emission order: for each `i`, apply `singleQubitFactors[2*i+1]` on q0 and + * `singleQubitFactors[2*i]` on q1, then the basis gate (except after the last + * pair). */ struct TwoQubitNativeDecomposition { std::uint8_t numBasisUses = 0; @@ -175,9 +159,7 @@ class TwoQubitBasisDecomposer { SmbPrecomputed smb{}; }; -/** - * @brief Decomposes @p target with respect to a two-qubit basis gate matrix. - */ +/** @brief Weyl-decomposes @p target using @p basisMatrix as entangler. */ [[nodiscard]] std::optional decomposeTwoQubitWithBasis( const Matrix4x4& target, const Matrix4x4& basisMatrix, diff --git a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h index 60324d3873..d9d46a714e 100644 --- a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h +++ b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h @@ -795,13 +795,13 @@ struct SymmetricEigen4 { Matrix4x4 eigenvectors{}; }; -/** @brief Constant-folded `RX(theta)` unitary. */ +/** @brief `RX(theta)` unitary matrix. */ [[nodiscard]] Matrix2x2 rxMatrix(double theta); -/** @brief Constant-folded `RY(theta)` unitary. */ +/** @brief `RY(theta)` unitary matrix. */ [[nodiscard]] Matrix2x2 ryMatrix(double theta); -/** @brief Constant-folded `RZ(theta)` unitary. */ +/** @brief `RZ(theta)` unitary matrix. */ [[nodiscard]] Matrix2x2 rzMatrix(double theta); /** @brief `i` times Pauli-X. */ @@ -813,13 +813,13 @@ struct SymmetricEigen4 { /** @brief `i` times Pauli-Z. */ [[nodiscard]] const Matrix2x2& iPauliZ(); -/** @brief Constant-folded two-qubit `RXX(theta)` unitary. */ +/** @brief `RXX(theta)` unitary matrix. */ [[nodiscard]] Matrix4x4 rxxMatrix(double theta); -/** @brief Constant-folded two-qubit `RYY(theta)` unitary. */ +/** @brief `RYY(theta)` unitary matrix. */ [[nodiscard]] Matrix4x4 ryyMatrix(double theta); -/** @brief Constant-folded two-qubit `RZZ(theta)` unitary. */ +/** @brief `RZZ(theta)` unitary matrix. */ [[nodiscard]] Matrix4x4 rzzMatrix(double theta); } // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp index 8b47c02c87..397f114113 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp @@ -269,37 +269,23 @@ TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, std::optional fidelity) { auto u = unitaryMatrix; auto detU = u.determinant(); - // Project into SU(4) by dividing out the fourth root of det(U): for a 4x4 - // unitary, |det(U)| == 1 so `det^{-1/4}` both enforces det == 1 and removes - // the global phase. The extracted phase is tracked separately in - // `globalPhase` (quarter of arg(det) to match the fourth-root choice) so the - // caller can reconstruct the original matrix exactly if needed. + // Project into SU(4): det^{-1/4} removes global phase; arg(det)/4 is tracked. auto detPow = std::pow(detU, -0.25); - u *= detPow; // remove global phase from unitary matrix + u *= detPow; auto globalPhase = std::arg(detU) / 4.; - // Numerical drift can still leave tiny determinant errors after root - // normalization. Re-normalize once more instead of aborting. auto detNormalized = u.determinant(); if (std::abs(detNormalized - Complex{1.0, 0.0}) > WEYL_TOLERANCE && std::abs(detNormalized) > WEYL_TOLERANCE) { u *= std::pow(detNormalized, -0.25); } - // 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, /*outOfMagicBasis=*/true); const Matrix4x4 m2 = uP.transpose() * uP; - // diagonalization yields eigenvectors (p) and eigenvalues (d); - // p is used to calculate K1/K2 (and thus the single-qubit gates - // surrounding the canonical gate); d is 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) + // Map eigenvalue phases to Weyl coordinates in [0, 2*pi). constexpr double pi = std::numbers::pi; std::array dReal{}; for (std::size_t i = 0; i < d.size(); ++i) { @@ -311,8 +297,7 @@ TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, cs[i] = remEuclid((dReal[i] + dReal[3]) / 2.0, 2.0 * pi); } - // Reorder coordinates according to min(a, pi/2 - a) with - // a = x mod pi/2 for each Weyl coordinate x + // Sort coordinates by min(x mod pi/2, pi/2 - x mod pi/2). std::array cstemp{}; for (std::size_t i = 0; i < cs.size(); ++i) { const auto tmp = remEuclid(cs[i], pi / 2.0); @@ -331,13 +316,10 @@ TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, dReal[2] = reordered[2]; } - // update eigenvectors (columns of p) according to new order of - // weyl coordinates const Matrix4x4 pOrig = p; for (std::size_t i = 0; i < order.size(); ++i) { p.setColumn(i, pOrig.column(order[i])); } - // apply correction for determinant if necessary if (p.determinant().real() < 0.0) { auto lastColumn = p.column(3); for (auto& entry : lastColumn) { @@ -347,33 +329,23 @@ TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, } assert(std::abs(p.determinant() - 1.0) < WEYL_TOLERANCE); - // re-create complex eigenvalue matrix; this matrix contains the - // parameters of the canonical gate which is later used in the - // verification. Since the matrix is diagonal, the matrix exponential is - // equivalent to the element-wise exponential function. std::array tempDiag{}; for (std::size_t k = 0; k < tempDiag.size(); ++k) { tempDiag[k] = std::exp(1i * dReal[k]); } const Matrix4x4 temp = Matrix4x4::fromDiagonal(tempDiag); - // combined matrix k1 of 1q gates after canonical gate Matrix4x4 k1 = uP * p * temp; - // k1 must be orthogonal; the tolerance matches the iterative diagonalization - // residual rather than the (much tighter) default matrix tolerance. + // Orthogonality checks use WEYL_TOLERANCE (diagonalization residual scale). assert((k1.transpose() * k1).isIdentity(WEYL_TOLERANCE)); assert(k1.determinant().real() > 0.0); k1 = magicBasisTransform(k1, /*outOfMagicBasis=*/false); - // combined matrix k2 of 1q gates before canonical gate Matrix4x4 k2 = p.adjoint(); - // k2 must be orthogonal; see the tolerance note on the k1 check above. assert((k2.transpose() * k2).isIdentity(WEYL_TOLERANCE)); assert(k2.determinant().real() > 0.0); k2 = magicBasisTransform(k2, /*outOfMagicBasis=*/false); - // ensure k1 and k2 are correct (when combined with the canonical gate - // parameters in-between, they are equivalent to u) std::array tempConjDiag{}; for (std::size_t k = 0; k < tempConjDiag.size(); ++k) { tempConjDiag[k] = std::conj(tempDiag[k]); @@ -384,16 +356,13 @@ TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, k2) .isApprox(u, WEYL_TOLERANCE)); - // calculate k1 = K1l ⊗ K1r auto [K1l, K1r, phaseL] = decomposeTwoQubitProductGate(k1); - // decompose k2 = K2l ⊗ K2r auto [K2l, K2r, phaseR] = decomposeTwoQubitProductGate(k2); assert(Matrix4x4::kron(K1l, K1r).isApprox(k1, WEYL_TOLERANCE)); assert(Matrix4x4::kron(K2l, K2r).isApprox(k2, WEYL_TOLERANCE)); - // accumulate global phase globalPhase += phaseL + phaseR; - // Flip into Weyl chamber + // Map into the Weyl chamber. if (cs[0] > (pi / 2.0)) { cs[0] -= 3.0 * (pi / 2.0); K1l = K1l * iPauliY(); @@ -446,7 +415,7 @@ TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, globalPhase -= (pi / 2.0); } - // bind weyl coordinates as parameters of canonical gate + // Bind Weyl coordinates to the canonical gate. auto [a, b, c] = std::tie(cs[1], cs[0], cs[2]); TwoQubitWeylDecomposition decomposition; @@ -459,13 +428,10 @@ TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, decomposition.k1r_ = K1r; decomposition.k2r_ = K2r; - // make sure decomposition is equal to input assert((Matrix4x4::kron(K1l, K1r) * decomposition.getCanonicalMatrix() * Matrix4x4::kron(K2l, K2r) * std::exp(Complex{0.0, 1.0} * globalPhase)) .isApprox(unitaryMatrix, WEYL_TOLERANCE)); - // determine actual specialization of canonical gate so that the 1q - // matrices can potentially be simplified auto flippedFromOriginal = decomposition.applySpecialization(fidelity); auto getTraceValue = [&]() { @@ -476,13 +442,8 @@ TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, return getTrace(a, b, c, decomposition.a_, decomposition.b_, decomposition.c_); }; - // use trace to calculate fidelity of applied specialization and - // adjust global phase - auto trace = getTraceValue(); + const auto trace = getTraceValue(); const double calculatedFidelity = 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 (fidelity && calculatedFidelity + 1.0e-13 < *fidelity) { llvm::reportFatalInternalError(llvm::formatv( "TwoQubitWeylDecomposition: Calculated fidelity of " @@ -491,7 +452,6 @@ TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, } decomposition.globalPhase_ += std::arg(trace); - // final check if decomposition is still valid after specialization assert((Matrix4x4::kron(decomposition.k1l_, decomposition.k1r_) * decomposition.getCanonicalMatrix() * Matrix4x4::kron(decomposition.k2l_, decomposition.k2r_) * @@ -503,12 +463,8 @@ TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, Matrix4x4 TwoQubitWeylDecomposition::getCanonicalMatrix(double a, double b, double c) { - // Canonical gate `U_d(a, b, c) = exp(i * (a*XX + b*YY + c*ZZ))`. XX/YY/ZZ - // commute pairwise, so any product order is equivalent; the order below is - // chosen to match common Qiskit/QuantumFlow references. The negated rotation - // angles (`-2 * a`, ...) compensate for the `RXX/RYY/RZZ` convention - // `exp(-i * theta/2 * XX)`, so that the factored angles sum back to the - // intended `+a`, `+b`, `+c`. + // U_d(a,b,c) = exp(i(a XX + b YY + c ZZ)); `-2*a/b/c` matches RXX/RYY/RZZ + // gate convention; product order matches Qiskit (RZZ · RYY · RXX). const auto xx = rxxMatrix(-2.0 * a); const auto yy = ryyMatrix(-2.0 * b); const auto zz = rzzMatrix(-2.0 * c); @@ -520,37 +476,19 @@ bool TwoQubitWeylDecomposition::applySpecialization( bool flippedFromOriginal = false; const auto newSpecialization = bestSpecialization(*this, requestedFidelity); 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; } 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_ = Matrix2x2::identity(); k1r_ = k1r_ * k2r_; k2r_ = Matrix2x2::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_ = Matrix2x2::identity(); @@ -568,58 +506,33 @@ bool TwoQubitWeylDecomposition::applySpecialization( b_ = (std::numbers::pi / 4.0); c_ = (std::numbers::pi / 4.0); } 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_.adjoint(); a_ = closest; b_ = closest; c_ = closest; - // unmodified global phase k1l_ = k1l_ * k2l_; k1r_ = k1r_ * k2l_; k2r_ = k2lDagger * k2r_; k2l_ = Matrix2x2::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_.adjoint(); a_ = closest; b_ = closest; c_ = -closest; - // unmodified global phase k1l_ = k1l_ * k2l_; k1r_ = k1r_ * iPauliZ() * k2l_ * iPauliZ(); k2r_ = iPauliZ() * k2lDagger * iPauliZ() * k2r_; k2l_ = Matrix2x2::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)` const EulerBasis eulerBasis = EulerBasis::XYX; const auto [k2ltheta, k2lphi, k2llambda, k2lphase] = anglesFromUnitary(k2l_, eulerBasis); const auto [k2rtheta, k2rphi, k2rlambda, k2rphase] = anglesFromUnitary(k2r_, EulerBasis::XYX); - // unmodified parameter a b_ = 0.; c_ = 0.; globalPhase_ = globalPhase_ + k2lphase + k2rphase; @@ -628,55 +541,35 @@ bool TwoQubitWeylDecomposition::applySpecialization( k1r_ = k1r_ * rxMatrix(k2rphi); k2r_ = ryMatrix(k2rtheta) * rxMatrix(k2rlambda); } 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)` const auto [k2ltheta, k2lphi, k2llambda, k2lphase] = anglesFromUnitary(k2l_, EulerBasis::ZYZ); const auto [k2rtheta, k2rphi, k2rlambda, k2rphase] = anglesFromUnitary(k2r_, EulerBasis::ZYZ); a_ = (std::numbers::pi / 4.0); b_ = (std::numbers::pi / 4.0); - // 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] = 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] = anglesFromUnitary(k2l_, eulerBasis); auto bc = (b_ + c_) / 2.; - // unmodified parameter a b_ = bc; c_ = bc; globalPhase_ = globalPhase_ + k2lphase; @@ -685,17 +578,11 @@ bool TwoQubitWeylDecomposition::applySpecialization( k1r_ = k1r_ * rxMatrix(k2lphi); k2r_ = rxMatrix(-k2lphi) * k2r_; } 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] = anglesFromUnitary(k2l_, eulerBasis); auto bc = (b_ - c_) / 2.; - // unmodified parameter a b_ = bc; c_ = -bc; globalPhase_ = globalPhase_ + k2lphase; @@ -725,8 +612,6 @@ TwoQubitBasisDecomposer::create(const Matrix4x4& basisMatrix, relativeEq(basisDecomposer.a(), std::numbers::pi / 4.0, 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(); Complex temp{0.5, -0.5}; const Matrix2x2 k11l = Matrix2x2::fromElements( @@ -758,7 +643,6 @@ TwoQubitBasisDecomposer::create(const Matrix4x4& basisMatrix, auto k1rDagger = basisDecomposer.k1r().adjoint(); auto k2lDagger = basisDecomposer.k2l().adjoint(); auto k2rDagger = basisDecomposer.k2r().adjoint(); - // 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; @@ -770,7 +654,6 @@ TwoQubitBasisDecomposer::create(const Matrix4x4& basisMatrix, 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.adjoint() * k1lDagger; auto q0r = k12RArr.adjoint() * iPauliZ() * k1rDagger; auto q1la = k2lDagger * k11l.adjoint(); @@ -812,8 +695,7 @@ TwoQubitBasisDecomposer::twoQubitDecompose( std::optional numBasisGateUses) const { auto traces = this->traces(targetDecomposition); auto getDefaultNbasis = [&]() -> std::uint8_t { - // Pick the number of basis gate uses `i ∈ {0, 1, 2, 3}` that maximizes - // expected_fidelity(i) = traceToFidelity(traces[i]) * basisFidelity^i. + // Maximize traceToFidelity(traces[i]) * basisFidelity^i over i in {0..3}. auto bestValue = std::numeric_limits::lowest(); auto bestIndex = -1; for (int i = 0; std::cmp_less(i, traces.size()); ++i) { @@ -832,11 +714,9 @@ TwoQubitBasisDecomposer::twoQubitDecompose( } 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 + // More than one basis gate requires a super-controlled basis (e.g. CX). return std::nullopt; } auto chooseDecomposition = [&]() { @@ -862,14 +742,9 @@ TwoQubitBasisDecomposer::twoQubitDecompose( double globalPhase = targetDecomposition.globalPhase(); globalPhase -= bestNbasis * basisDecomposer.globalPhase(); if (bestNbasis == 2) { - // The two-basis (2x CX/CZ) template in `decomp2Supercontrolled` produces - // a sequence whose global phase is off by `pi` relative to the target; - // compensate here so the emitted sequence reproduces the target unitary - // exactly, not just up to sign. + // Two-basis template is exact up to a pi global-phase offset. globalPhase += std::numbers::pi; } - // large global phases can be generated by the decomposition, thus limit - // it to [0, +2*pi) globalPhase = remEuclid(globalPhase, 2.0 * std::numbers::pi); return TwoQubitNativeDecomposition{ @@ -889,7 +764,7 @@ TwoQubitBasisDecomposer::decomp0(const TwoQubitWeylDecomposition& target) { SmallVector TwoQubitBasisDecomposer::decomp1( const TwoQubitWeylDecomposition& target) const { - // may not work for z != 0 and c != 0 (not always in Weyl chamber) + // One basis gate; reliable only when the target is in the Weyl chamber. return SmallVector{ basisDecomposer.k2r().adjoint() * target.k2r(), basisDecomposer.k2l().adjoint() * target.k2l(), @@ -936,16 +811,8 @@ SmallVector TwoQubitBasisDecomposer::decomp3Supercontrolled( std::array, 4> TwoQubitBasisDecomposer::traces(const TwoQubitWeylDecomposition& target) const { - // Returns the Hilbert-Schmidt traces between the target canonical gate and - // the best candidate reachable with `0, 1, 2, 3` uses of the basis gate, - // respectively. Fed into `traceToFidelity` by `getDefaultNbasis` to pick - // the best basis-gate count. The closed-form expressions specialize - // `TwoQubitWeylDecomposition::getTrace(a, b, c, ap, bp, cp)` for: - // i == 0: no basis gate (ap == bp == cp == 0) - // i == 1: one basis use (ap == pi/4, bp == basis.b, cp == 0) - // i == 2: two basis uses (ap == 0, bp == 0, cp == -target.c) - // i == 3: three basis uses (target reachable exactly -> trace == 4) - // so the array has length 4 and is indexed by the number of basis uses. + // Hilbert-Schmidt traces for 0..3 basis uses; index i == numBasisUses. + // i=0: identity; i=1: one basis gate; i=2: two uses; i=3: exact (trace=4). return { 4. * std::complex{std::cos(target.a()) * std::cos(target.b()) * std::cos(target.c()), From 5d27f045550474c900eae234e69009ca16f1299e Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 22 Jun 2026 22:02:36 +0200 Subject: [PATCH 067/122] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20linter=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/Decomposition/test_weyl_decomposition.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index 8a5f34d17f..8a60d7a32b 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -186,6 +186,8 @@ TEST(DecompositionHelpersTest, GateMatrixFactoriesMatchCanonicalForm) { } } +namespace { + class WeylDecompositionTest : public testing::TestWithParam {}; class BasisDecomposerTest : public testing::TestWithParam< @@ -203,6 +205,8 @@ class BasisDecomposerTest : public testing::TestWithParam< std::unique_ptr targetDecomposition; }; +} // namespace + TEST_P(WeylDecompositionTest, ReconstructsWithinRequestedFidelity) { const Matrix4x4 originalMatrix = GetParam()(); for (const double fidelity : {1.0, 1.0 - 1e-12}) { From d57dab29dda1af607aca750028667b4bb430fef6 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 22 Jun 2026 22:02:54 +0200 Subject: [PATCH 068/122] =?UTF-8?q?=F0=9F=93=9D=20Add=20comment=20about=20?= =?UTF-8?q?fixed=20perturbation=20coefficients=20for=20deterministic=20beh?= =?UTF-8?q?avior=20in=20Weyl=20decomposition=20diagonalization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp index 397f114113..5aef8af884 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp @@ -109,6 +109,10 @@ diagonalizeComplexSymmetric(const Matrix4x4& m, double precision) { for (int i = 0; i < maxDiagonalizationAttempts; ++i) { double randA{}; double randB{}; + // Fixed perturbation coefficients for the first diagonalization attempt, + // carried over from Qiskit's two-qubit Weyl decomposition (legacy Python + // RNG values). The loop usually succeeds on this trial; fixing randA/randB + // keeps behavior deterministic while later attempts sample the distribution. if (i == 0) { randA = 1.2602066112249388; randB = 0.22317849046722027; From eaa10973998f924c6e3abc7d4bc7cfcd21b7265e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 20:04:44 +0000 Subject: [PATCH 069/122] =?UTF-8?q?=F0=9F=8E=A8=20pre-commit=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp index 5aef8af884..1a042db1b2 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp @@ -112,7 +112,8 @@ diagonalizeComplexSymmetric(const Matrix4x4& m, double precision) { // Fixed perturbation coefficients for the first diagonalization attempt, // carried over from Qiskit's two-qubit Weyl decomposition (legacy Python // RNG values). The loop usually succeeds on this trial; fixing randA/randB - // keeps behavior deterministic while later attempts sample the distribution. + // keeps behavior deterministic while later attempts sample the + // distribution. if (i == 0) { randA = 1.2602066112249388; randB = 0.22317849046722027; From 362692c29c9e58cd6ca263346233a22ad7739073 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 23 Jun 2026 12:52:46 +0200 Subject: [PATCH 070/122] =?UTF-8?q?=E2=9C=A8=20Add=20project=20options=20f?= =?UTF-8?q?or=20MLIRQCOTransforms=20in=20CMake=20configuration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt index 3564b55dc5..77f594db17 100644 --- a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt +++ b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt @@ -22,6 +22,8 @@ add_mlir_library( DEPENDS MLIRQCOTransformsIncGen) +mqt_mlir_target_use_project_options(MLIRQCOTransforms) + # collect header files file(GLOB_RECURSE PASSES_HEADERS_SOURCE ${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Dialect/QCO/Transforms/*.h) From 425a03d21a85b39f1b4525bf2a83d31d8fb4a389 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 23 Jun 2026 13:34:11 +0200 Subject: [PATCH 071/122] =?UTF-8?q?=E2=98=82=EF=B8=8F=20Increase=20coverag?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Decomposition/test_weyl_decomposition.cpp | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index 8a60d7a32b..075724d040 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -295,6 +296,80 @@ TEST(BasisDecomposerNumBasisTest, ForcesZeroBasisUsesForIdentityTarget) { restoreBasis(*decomposed, basis).isApprox(target, WEYL_TOLERANCE)); } +TEST(BasisDecomposerTest, DecomposeTwoQubitWithBasisReconstructsTarget) { + const Matrix4x4 basis = twoQubitControlledX01(); + const Matrix4x4 target = + Matrix4x4::kron(rxMatrix(0.4), ryMatrix(0.6)) * + TwoQubitWeylDecomposition::getCanonicalMatrix(0.3, 0.2, 0.1) * + Matrix4x4::kron(rzMatrix(0.2), Matrix2x2::identity()); + const auto decomposed = decomposeTwoQubitWithBasis(target, basis); + ASSERT_TRUE(decomposed.has_value()); + EXPECT_TRUE( + restoreBasis(*decomposed, basis).isApprox(target, WEYL_TOLERANCE)); +} + +TEST(BasisDecomposerTest, RejectsMultipleBasisUsesForNonSuperControlledBasis) { + const Matrix4x4 basis = rzzMatrix(1.0); + const auto decomposer = TwoQubitBasisDecomposer::create(basis, 1.0); + const auto weyl = + TwoQubitWeylDecomposition::create(Matrix4x4::identity(), 1.0); + EXPECT_FALSE(decomposer.twoQubitDecompose(weyl, std::uint8_t{2}).has_value()); +} + +TEST(BasisDecomposerForcedCountTest, OneBasisUseProducesFactors) { + const Matrix4x4 basis = twoQubitControlledX01(); + const auto decomposer = TwoQubitBasisDecomposer::create(basis, 1.0); + const auto weyl = + TwoQubitWeylDecomposition::create(twoQubitControlledX01(), 1.0); + const auto decomposed = decomposer.twoQubitDecompose(weyl, std::uint8_t{1}); + ASSERT_TRUE(decomposed.has_value()); + EXPECT_EQ(decomposed->numBasisUses, 1); + EXPECT_EQ(decomposed->singleQubitFactors.size(), 4U); +} + +TEST(BasisDecomposerForcedCountTest, TwoBasisUsesProducesFactors) { + const Matrix4x4 basis = twoQubitControlledX01(); + const auto decomposer = TwoQubitBasisDecomposer::create(basis, 1.0); + const auto weyl = + TwoQubitWeylDecomposition::create(twoQubitControlledX01(), 1.0); + const auto decomposed = decomposer.twoQubitDecompose(weyl, std::uint8_t{2}); + ASSERT_TRUE(decomposed.has_value()); + EXPECT_EQ(decomposed->numBasisUses, 2); + EXPECT_EQ(decomposed->singleQubitFactors.size(), 6U); +} + +TEST(BasisDecomposerForcedCountTest, ThreeBasisUsesProducesFactors) { + const Matrix4x4 basis = twoQubitControlledX01(); + const auto decomposer = TwoQubitBasisDecomposer::create(basis, 1.0); + const auto weyl = + TwoQubitWeylDecomposition::create(twoQubitControlledX01(), 1.0); + const auto decomposed = decomposer.twoQubitDecompose(weyl, std::uint8_t{3}); + ASSERT_TRUE(decomposed.has_value()); + EXPECT_EQ(decomposed->numBasisUses, 3); + EXPECT_EQ(decomposed->singleQubitFactors.size(), 8U); +} + +TEST(WeylDecompositionStandalone, SwapNegativeCSpecializationReconstructs) { + constexpr double piOver4 = std::numbers::pi / 4.0; + const Matrix4x4 swapNegativeC = + TwoQubitWeylDecomposition::getCanonicalMatrix(piOver4, piOver4, -piOver4); + const auto decomposition = + TwoQubitWeylDecomposition::create(swapNegativeC, 1.0); + EXPECT_TRUE( + restoreWeyl(decomposition).isApprox(swapNegativeC, WEYL_TOLERANCE)); +} + +TEST(WeylDecompositionStandalone, ControlledSpecializationReconstructs) { + const Matrix4x4 controlledLike = + Matrix4x4::kron(rxMatrix(0.3), ryMatrix(0.4)) * + TwoQubitWeylDecomposition::getCanonicalMatrix(0.6, 0.0, 0.0) * + Matrix4x4::kron(Matrix2x2::identity(), rzMatrix(0.2)); + const auto decomposition = + TwoQubitWeylDecomposition::create(controlledLike, 1.0); + EXPECT_TRUE( + restoreWeyl(decomposition).isApprox(controlledLike, WEYL_TOLERANCE)); +} + INSTANTIATE_TEST_SUITE_P(ProductTwoQubitMatrices, BasisDecomposerTest, testing::Combine(cxBasisCases(), productMatrixCases())); From 426f3002479b745f24248c94d410fed677b8e0ff Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 23 Jun 2026 13:48:44 +0200 Subject: [PATCH 072/122] =?UTF-8?q?=E2=98=82=EF=B8=8F=20Increase=20coverag?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_euler_decomposition.cpp | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp index 454012d489..da86f990d9 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -42,6 +42,7 @@ #include #include #include +#include #include #include #include @@ -441,6 +442,80 @@ TEST(EulerSynthesisTest, RandomReconstructionAllBases) { } } +TEST(EulerAnglesCoverageTest, ParamsZYZUsesOffDiagonal01When10IsNearZero) { + Matrix2x2 matrix = rxMatrix(0.4); + matrix(1, 0) = Complex{0.0, 0.0}; + ASSERT_GT(std::abs(matrix(0, 1)), mlir::utils::TOLERANCE); + const EulerAngles angles = anglesFromUnitary(matrix, ZYZ); + EXPECT_TRUE(std::isfinite(angles.theta)); + EXPECT_TRUE(std::isfinite(angles.phi)); + EXPECT_TRUE(std::isfinite(angles.lambda)); +} + +TEST(EulerAnglesDeathTest, InvalidBasisIsUnreachable) { + EXPECT_DEATH(static_cast(anglesFromUnitary( + Matrix2x2::identity(), static_cast(99))), + "invalid Euler basis"); +} + +TEST(EulerAnglesCoverageTest, PhaseOnlyDecompositionSkipsRotationGates) { + TestFixture fx; + fx.setUp(); + constexpr double scale = 1.0 + 1e-10; + const Matrix2x2 matrix = Matrix2x2::fromElements(scale, 0, 0, scale); + ASSERT_FALSE(matrix.isApprox(Matrix2x2::identity())); + const EulerAngles angles = anglesFromUnitary(matrix, ZYZ); + EXPECT_LE(std::abs(angles.theta), mlir::utils::TOLERANCE); + EXPECT_LE(std::abs(angles.phi), mlir::utils::TOLERANCE); + EXPECT_LE(std::abs(angles.lambda), mlir::utils::TOLERANCE); + const auto circuit = synthesizeMatrix(fx.ctx(), matrix, ZYZ); + ASSERT_TRUE(succeeded(verify(*circuit.mlirModule))); + EXPECT_EQ(countZYZGates(circuit.func), 0U); +} + +TEST(EulerAnglesCoverageTest, UBasisZeroThetaEmitsSingleUGate) { + TestFixture fx; + fx.setUp(); + const Matrix2x2 matrix = rzMatrix(0.7); + expectSynthesizedMatrix(fx.ctx(), matrix, U, + [](func::FuncOp funcOp, const Matrix2x2& /*matrix*/) { + EXPECT_EQ(countOps(funcOp), 1U); + EXPECT_EQ(countZYZGates(funcOp), 0U); + }); +} + +TEST(EulerAnglesCoverageTest, UBasisNonzeroThetaEmitsSingleUGate) { + TestFixture fx; + fx.setUp(); + const Matrix2x2 matrix = ryMatrix(1.2); + expectSynthesizedMatrix(fx.ctx(), matrix, U, + [](func::FuncOp funcOp, const Matrix2x2& /*matrix*/) { + EXPECT_EQ(countOps(funcOp), 1U); + EXPECT_EQ(countZYZGates(funcOp), 0U); + }); +} + +TEST(EulerAnglesCoverageTest, Mod2PiMapsPiBoundaryThroughSynthesis) { + TestFixture fx; + fx.setUp(); + constexpr double eps = 0.5 * mlir::utils::TOLERANCE; + const Complex global = std::polar(1.0, std::numbers::pi - eps); + const Matrix2x2 matrix = Matrix2x2::fromElements(global, 0, 0, global); + expectSynthesizedMatrix(fx.ctx(), matrix, U, + [](func::FuncOp funcOp, const Matrix2x2& /*matrix*/) { + EXPECT_EQ(countOps(funcOp), 1U); + EXPECT_EQ(countOps(funcOp), 1U); + }); +} + +TEST(EulerAnglesCoverageTest, Mod2PiPreservesNonFinitePhase) { + TestFixture fx; + fx.setUp(); + const Matrix2x2 matrix = Matrix2x2::fromElements( + Complex{std::numeric_limits::quiet_NaN(), 0}, 0, 0, 1); + EXPECT_NO_FATAL_FAILURE(synthesizeMatrix(fx.ctx(), matrix, ZYZ)); +} + //===----------------------------------------------------------------------===// // FuseSingleQubitUnitaryRuns support //===----------------------------------------------------------------------===// From 392f6086a13fd53b555dc6e5f26c80733bdc8be4 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 23 Jun 2026 14:36:34 +0200 Subject: [PATCH 073/122] =?UTF-8?q?=F0=9F=8E=A8=20Align=20implementation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/Decomposition/Weyl.cpp | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp index 1a042db1b2..d4459e2788 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp @@ -52,7 +52,11 @@ enum class Specialization : std::uint8_t { } // namespace -static constexpr auto DIAGONALIZATION_PRECISION = WEYL_TOLERANCE / 10; +/** Default fidelity for Weyl specialization. */ +static constexpr double DEFAULT_FIDELITY = 1.0 - 1e-12; + +/** M2 diagonalization tolerance. */ +static constexpr double DIAGONALIZATION_PRECISION = 1e-13; /** Non-negative remainder of @p a modulo @p b (always in `[0, |b|)`). */ static double remEuclid(const double a, const double b) { @@ -279,12 +283,6 @@ TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, u *= detPow; auto globalPhase = std::arg(detU) / 4.; - auto detNormalized = u.determinant(); - if (std::abs(detNormalized - Complex{1.0, 0.0}) > WEYL_TOLERANCE && - std::abs(detNormalized) > WEYL_TOLERANCE) { - u *= std::pow(detNormalized, -0.25); - } - auto uP = magicBasisTransform(u, /*outOfMagicBasis=*/true); const Matrix4x4 m2 = uP.transpose() * uP; @@ -612,7 +610,7 @@ TwoQubitBasisDecomposer::create(const Matrix4x4& basisMatrix, Complex{-0.5, 0.5}, Complex{0.5, -0.5}); const auto basisDecomposer = - TwoQubitWeylDecomposition::create(basisMatrix, basisFidelity); + TwoQubitWeylDecomposition::create(basisMatrix, DEFAULT_FIDELITY); const auto isSuperControlled = relativeEq(basisDecomposer.a(), std::numbers::pi / 4.0, 1e-13, 1e-09) && relativeEq(basisDecomposer.c(), 0.0, 1e-13, 1e-09); @@ -842,7 +840,7 @@ decomposeTwoQubitWithBasis(const Matrix4x4& target, const std::optional numBasisUses) { const auto decomposer = TwoQubitBasisDecomposer::create(basisMatrix, basisFidelity); - const auto weyl = TwoQubitWeylDecomposition::create(target, std::nullopt); + const auto weyl = TwoQubitWeylDecomposition::create(target, DEFAULT_FIDELITY); return decomposer.twoQubitDecompose(weyl, numBasisUses); } From 65408d6d0222437c60b705dbf3ba09b33c7bcbc7 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 23 Jun 2026 14:42:42 +0200 Subject: [PATCH 074/122] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20linter=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Transforms/Decomposition/test_euler_decomposition.cpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp index da86f990d9..aedc27d80f 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -452,12 +452,6 @@ TEST(EulerAnglesCoverageTest, ParamsZYZUsesOffDiagonal01When10IsNearZero) { EXPECT_TRUE(std::isfinite(angles.lambda)); } -TEST(EulerAnglesDeathTest, InvalidBasisIsUnreachable) { - EXPECT_DEATH(static_cast(anglesFromUnitary( - Matrix2x2::identity(), static_cast(99))), - "invalid Euler basis"); -} - TEST(EulerAnglesCoverageTest, PhaseOnlyDecompositionSkipsRotationGates) { TestFixture fx; fx.setUp(); From 846a4edcfee77522cc963f9adc7604afecda5ff5 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 23 Jun 2026 15:11:15 +0200 Subject: [PATCH 075/122] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Enhance=20TwoQubit?= =?UTF-8?q?BasisDecomposer=20with=20caching=20functionality=20for=20target?= =?UTF-8?q?=20decompositions.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/Decomposition/Weyl.h | 30 +++++++++++++-- .../QCO/Transforms/Decomposition/Weyl.cpp | 38 ++++++++++++------- .../Decomposition/test_weyl_decomposition.cpp | 23 +++++++++++ 3 files changed, 74 insertions(+), 17 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h index 9b59eafaae..3b51ce902f 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h @@ -32,7 +32,7 @@ inline constexpr double WEYL_TOLERANCE = 100 * MATRIX_TOLERANCE; * `U_canon(a,b,c) = RXX(-2a) · RYY(-2b) · RZZ(-2c)`. * * @note Adapted from TwoQubitWeylDecomposition in the IBM Qiskit framework. - * (C) Copyright IBM 2023 + * (C) Copyright IBM 2026 * * 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 @@ -99,7 +99,7 @@ struct TwoQubitNativeDecomposition { * @brief Decomposer for a fixed two-qubit basis gate (e.g. CX/CZ). * * @note Adapted from TwoQubitBasisDecomposer in the IBM Qiskit framework. - * (C) Copyright IBM 2023 + * (C) Copyright IBM 2026 * * 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 @@ -112,6 +112,14 @@ struct TwoQubitNativeDecomposition { */ class TwoQubitBasisDecomposer { public: + /** + * @brief Precomputes basis-gate data for repeated target decompositions. + * + * Creation performs a full Weyl decomposition of @p basisMatrix and + * precomputes the single-qubit templates used by @ref twoQubitDecompose. + * Reuse one instance for many targets that share the same entangler + * (e.g. CX) via @ref decomposeTarget. + */ [[nodiscard]] static TwoQubitBasisDecomposer create(const Matrix4x4& basisMatrix, double basisFidelity); @@ -119,6 +127,16 @@ class TwoQubitBasisDecomposer { twoQubitDecompose(const TwoQubitWeylDecomposition& targetDecomposition, std::optional numBasisGateUses) const; + /** + * @brief Decomposes @p targetUnitary using this cached basis decomposer. + * + * Only the target undergoes Weyl decomposition; basis precomputation from + * @ref create is reused. + */ + [[nodiscard]] std::optional decomposeTarget( + const Matrix4x4& targetUnitary, + std::optional numBasisGateUses = std::nullopt) const; + private: struct SmbPrecomputed { Matrix2x2 u0l; @@ -159,7 +177,13 @@ class TwoQubitBasisDecomposer { SmbPrecomputed smb{}; }; -/** @brief Weyl-decomposes @p target using @p basisMatrix as entangler. */ +/** + * @brief Convenience wrapper that builds a fresh basis decomposer per call. + * + * For a fixed basis gate decomposed many times, prefer caching + * `TwoQubitBasisDecomposer::create(basisMatrix, basisFidelity)` and calling + * `TwoQubitBasisDecomposer::decomposeTarget` for each target. + */ [[nodiscard]] std::optional decomposeTwoQubitWithBasis( const Matrix4x4& target, const Matrix4x4& basisMatrix, diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp index d4459e2788..f2c09d181d 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp @@ -75,22 +75,24 @@ static double traceToFidelity(const Complex& trace) { static constexpr double INV_SQRT2 = 1.0 / std::numbers::sqrt2; +static const Matrix4x4 kMagicBasisNonNormalized = Matrix4x4::fromElements( // + 1, 1i, 0, 0, // + 0, 0, 1i, 1, // + 0, 0, 1i, -1, // + 1, -1i, 0, 0); +static const Matrix4x4 kMagicBasisNonNormalizedDagger = + Matrix4x4::fromElements( // + 0.5, 0, 0, 0.5, // + Complex{0.0, -0.5}, 0, 0, Complex{0.0, 0.5}, // + 0, Complex{0.0, -0.5}, Complex{0.0, -0.5}, 0, // + 0, 0.5, -0.5, 0); + static Matrix4x4 magicBasisTransform(const Matrix4x4& unitary, bool outOfMagicBasis) { - const Matrix4x4 bNonNormalized = Matrix4x4::fromElements( // - 1, 1i, 0, 0, // - 0, 0, 1i, 1, // - 0, 0, 1i, -1, // - 1, -1i, 0, 0); - const Matrix4x4 bNonNormalizedDagger = Matrix4x4::fromElements( // - 0.5, 0, 0, 0.5, // - Complex{0.0, -0.5}, 0, 0, Complex{0.0, 0.5}, // - 0, Complex{0.0, -0.5}, Complex{0.0, -0.5}, 0, // - 0, 0.5, -0.5, 0); if (outOfMagicBasis) { - return bNonNormalizedDagger * unitary * bNonNormalized; + return kMagicBasisNonNormalizedDagger * unitary * kMagicBasisNonNormalized; } - return bNonNormalized * unitary * bNonNormalizedDagger; + return kMagicBasisNonNormalized * unitary * kMagicBasisNonNormalizedDagger; } static double closestPartialSwap(double a, double b, double c) { @@ -692,6 +694,15 @@ TwoQubitBasisDecomposer::create(const Matrix4x4& basisMatrix, return decomposer; } +std::optional +TwoQubitBasisDecomposer::decomposeTarget( + const Matrix4x4& targetUnitary, + const std::optional numBasisGateUses) const { + const auto targetWeyl = + TwoQubitWeylDecomposition::create(targetUnitary, DEFAULT_FIDELITY); + return twoQubitDecompose(targetWeyl, numBasisGateUses); +} + std::optional TwoQubitBasisDecomposer::twoQubitDecompose( const TwoQubitWeylDecomposition& targetDecomposition, @@ -840,8 +851,7 @@ decomposeTwoQubitWithBasis(const Matrix4x4& target, const std::optional numBasisUses) { const auto decomposer = TwoQubitBasisDecomposer::create(basisMatrix, basisFidelity); - const auto weyl = TwoQubitWeylDecomposition::create(target, DEFAULT_FIDELITY); - return decomposer.twoQubitDecompose(weyl, numBasisUses); + return decomposer.decomposeTarget(target, numBasisUses); } } // namespace mlir::qco::decomposition diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index 075724d040..bcdc627c60 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -308,6 +308,29 @@ TEST(BasisDecomposerTest, DecomposeTwoQubitWithBasisReconstructsTarget) { restoreBasis(*decomposed, basis).isApprox(target, WEYL_TOLERANCE)); } +TEST(BasisDecomposerTest, CachedDecomposerMatchesOneShotAcrossTargets) { + const Matrix4x4 basis = twoQubitControlledX01(); + const auto cachedDecomposer = TwoQubitBasisDecomposer::create(basis, 1.0); + const mlir::SmallVector targets{ + Matrix4x4::identity(), + twoQubitControlledX01(), + Matrix4x4::kron(rxMatrix(0.2), ryMatrix(0.3)) * + TwoQubitWeylDecomposition::getCanonicalMatrix(0.1, 0.2, 0.3) * + Matrix4x4::kron(rzMatrix(0.1), Matrix2x2::identity()), + }; + for (const Matrix4x4& target : targets) { + const auto oneShot = decomposeTwoQubitWithBasis(target, basis); + const auto cached = cachedDecomposer.decomposeTarget(target); + ASSERT_TRUE(oneShot.has_value()); + ASSERT_TRUE(cached.has_value()); + EXPECT_TRUE(restoreBasis(*oneShot, basis).isApprox(target, WEYL_TOLERANCE)); + EXPECT_TRUE(restoreBasis(*cached, basis).isApprox(target, WEYL_TOLERANCE)); + EXPECT_EQ(cached->numBasisUses, oneShot->numBasisUses); + EXPECT_EQ(cached->singleQubitFactors.size(), + oneShot->singleQubitFactors.size()); + } +} + TEST(BasisDecomposerTest, RejectsMultipleBasisUsesForNonSuperControlledBasis) { const Matrix4x4 basis = rzzMatrix(1.0); const auto decomposer = TwoQubitBasisDecomposer::create(basis, 1.0); From 7e4e69acad077b080b5b3408c9e16210c5235de5 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 23 Jun 2026 15:48:23 +0200 Subject: [PATCH 076/122] =?UTF-8?q?=E2=9C=A8=20Move=20basis=20decompositio?= =?UTF-8?q?n=20into=20TwoQubitBasisDecomposer=20and=20enhance=20Weyl=20dec?= =?UTF-8?q?omposition=20with=20new=20specialization=20phase=20finalization?= =?UTF-8?q?=20and=20improved=20fidelity=20calculations.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/Decomposition/Weyl.h | 8 +- .../Decomposition/BasisDecomposer.cpp | 307 +++++++++ .../QCO/Transforms/Decomposition/Weyl.cpp | 633 ++++++------------ 3 files changed, 512 insertions(+), 436 deletions(-) create mode 100644 mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h index 3b51ce902f..5a0483628f 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h @@ -72,6 +72,12 @@ class TwoQubitWeylDecomposition { private: bool applySpecialization(const std::optional& requestedFidelity); + void finalizeSpecializationPhase(bool flippedFromOriginal, + double preSpecializationA, + double preSpecializationB, + double preSpecializationC, + const std::optional& fidelity); + double a_{}; double b_{}; double c_{}; @@ -172,7 +178,7 @@ class TwoQubitBasisDecomposer { traces(const TwoQubitWeylDecomposition& target) const; double basisFidelity{}; - TwoQubitWeylDecomposition basisDecomposer; + TwoQubitWeylDecomposition basisWeyl; bool isSuperControlled{}; SmbPrecomputed smb{}; }; 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..30c5f5709b --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp @@ -0,0 +1,307 @@ +/* + * 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/Weyl.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mlir::qco::decomposition { + +using namespace std::complex_literals; + +static constexpr double DEFAULT_WEYL_FIDELITY = 1.0 - 1e-12; +static constexpr double PI = std::numbers::pi; +static constexpr double PI_OVER_4 = PI / 4.0; +static constexpr double INV_SQRT2 = 1.0 / std::numbers::sqrt2; + +static const Matrix2x2 K12_R_ARR = Matrix2x2::fromElements( + 1i * INV_SQRT2, INV_SQRT2, -INV_SQRT2, -1i * INV_SQRT2); +static const Matrix2x2 K12_L_ARR = + Matrix2x2::fromElements(Complex{0.5, 0.5}, Complex{0.5, 0.5}, + Complex{-0.5, 0.5}, Complex{0.5, -0.5}); +static const Matrix2x2 K22_L = + Matrix2x2::fromElements(INV_SQRT2, -INV_SQRT2, INV_SQRT2, INV_SQRT2); +static const Matrix2x2 K22_R = Matrix2x2::fromElements(0, 1, -1, 0); + +static double remEuclid(const double a, const double b) { + if (b == 0.0) { + llvm::reportFatalInternalError("remEuclid expects non-zero divisor"); + } + const auto r = std::fmod(a, b); + return (r < 0.0) ? r + std::abs(b) : r; +} + +static bool relativeEq(double lhs, double rhs, double epsilon, + double maxRelative) { + if (lhs == rhs) { + return true; + } + if (std::isinf(lhs) || std::isinf(rhs)) { + return false; + } + const auto absDiff = std::abs(lhs - rhs); + if (absDiff <= epsilon) { + return true; + } + const auto absLhs = std::abs(lhs); + const auto absRhs = std::abs(rhs); + if (absRhs > absLhs) { + return absDiff <= absRhs * maxRelative; + } + return absDiff <= absLhs * maxRelative; +} + +static double traceToFidelity(const Complex& trace) { + const auto traceAbs = std::abs(trace); + return (4.0 + (traceAbs * traceAbs)) / 20.0; +} + +//===----------------------------------------------------------------------===// +// TwoQubitBasisDecomposer +//===----------------------------------------------------------------------===// + +TwoQubitBasisDecomposer +TwoQubitBasisDecomposer::create(const Matrix4x4& basisMatrix, + double basisFidelity) { + const auto basisWeyl = + TwoQubitWeylDecomposition::create(basisMatrix, DEFAULT_WEYL_FIDELITY); + const auto isSuperControlled = + relativeEq(basisWeyl.a(), PI_OVER_4, 1e-13, 1e-09) && + relativeEq(basisWeyl.c(), 0.0, 1e-13, 1e-09); + + const auto b = basisWeyl.b(); + Complex temp{0.5, -0.5}; + const Matrix2x2 k11l = Matrix2x2::fromElements( + temp * (-1i * std::exp(-1i * b)), temp * std::exp(-1i * b), + temp * (-1i * std::exp(1i * b)), temp * -std::exp(1i * b)); + const Matrix2x2 k11r = Matrix2x2::fromElements( + INV_SQRT2 * (1i * std::exp(-1i * b)), INV_SQRT2 * -std::exp(-1i * b), + INV_SQRT2 * std::exp(1i * b), INV_SQRT2 * (-1i * std::exp(1i * b))); + const Matrix2x2 k32lK21l = Matrix2x2::fromElements( + INV_SQRT2 * Complex{1., std::cos(2. * b)}, + INV_SQRT2 * (1i * std::sin(2. * b)), INV_SQRT2 * (1i * std::sin(2. * b)), + INV_SQRT2 * Complex{1., -std::cos(2. * b)}); + temp = Complex{0.5, 0.5}; + const Matrix2x2 k21r = Matrix2x2::fromElements( + temp * (-1i * std::exp(-2i * b)), temp * std::exp(-2i * b), + temp * (1i * std::exp(2i * b)), temp * std::exp(2i * b)); + const Matrix2x2 k31l = Matrix2x2::fromElements( + INV_SQRT2 * std::exp(-1i * b), INV_SQRT2 * std::exp(-1i * b), + INV_SQRT2 * -std::exp(1i * b), INV_SQRT2 * std::exp(1i * b)); + const Matrix2x2 k31r = Matrix2x2::fromElements(1i * std::exp(1i * b), 0, 0, + -1i * std::exp(-1i * b)); + const Matrix2x2 k32r = Matrix2x2::fromElements( + temp * std::exp(1i * b), temp * -std::exp(-1i * b), + temp * (-1i * std::exp(1i * b)), temp * (-1i * std::exp(-1i * b))); + + const auto k1lDagger = basisWeyl.k1l().adjoint(); + const auto k1rDagger = basisWeyl.k1r().adjoint(); + const auto k2lDagger = basisWeyl.k2l().adjoint(); + const auto k2rDagger = basisWeyl.k2r().adjoint(); + + TwoQubitBasisDecomposer decomposer; + decomposer.basisFidelity = basisFidelity; + decomposer.basisWeyl = basisWeyl; + decomposer.isSuperControlled = isSuperControlled; + decomposer.smb = TwoQubitBasisDecomposer::SmbPrecomputed{ + .u0l = k31l * k1lDagger, + .u0r = k31r * k1rDagger, + .u1l = k2lDagger * k32lK21l * k1lDagger, + .u1ra = k2rDagger * k32r, + .u1rb = k21r * k1rDagger, + .u2la = k2lDagger * K22_L, + .u2lb = k11l * k1lDagger, + .u2ra = k2rDagger * K22_R, + .u2rb = k11r * k1rDagger, + .u3l = k2lDagger * K12_L_ARR, + .u3r = k2rDagger * K12_R_ARR, + .q0l = K12_L_ARR.adjoint() * k1lDagger, + .q0r = K12_R_ARR.adjoint() * iPauliZ() * k1rDagger, + .q1la = k2lDagger * k11l.adjoint(), + .q1lb = k11l * k1lDagger, + .q1ra = k2rDagger * iPauliZ() * k11r.adjoint(), + .q1rb = k11r * k1rDagger, + .q2l = k2lDagger * K12_L_ARR, + .q2r = k2rDagger * K12_R_ARR, + }; + return decomposer; +} + +std::optional +TwoQubitBasisDecomposer::decomposeTarget( + const Matrix4x4& targetUnitary, + const std::optional numBasisGateUses) const { + const auto targetWeyl = + TwoQubitWeylDecomposition::create(targetUnitary, DEFAULT_WEYL_FIDELITY); + return twoQubitDecompose(targetWeyl, numBasisGateUses); +} + +std::optional +TwoQubitBasisDecomposer::twoQubitDecompose( + const TwoQubitWeylDecomposition& targetDecomposition, + std::optional numBasisGateUses) const { + const auto traceValues = traces(targetDecomposition); + auto getDefaultNbasis = [&]() -> std::uint8_t { + auto bestValue = std::numeric_limits::lowest(); + auto bestIndex = -1; + double fidelityPower = 1.0; + for (int i = 0; std::cmp_less(i, traceValues.size()); ++i) { + const auto value = traceToFidelity(traceValues[i]) * fidelityPower; + fidelityPower *= basisFidelity; + if (std::isnan(value)) { + continue; + } + if (value > bestValue) { + bestIndex = i; + bestValue = value; + } + } + if (bestIndex < 0) { + llvm::reportFatalInternalError("Unable to select basis-gate count: all " + "candidate fidelities are NaN"); + } + return static_cast(bestIndex); + }; + + const auto bestNbasis = numBasisGateUses.value_or(getDefaultNbasis()); + if (bestNbasis > 1 && !isSuperControlled) { + return std::nullopt; + } + + SmallVector factors; + switch (bestNbasis) { + case 0: + factors = decomp0(targetDecomposition); + break; + case 1: + factors = decomp1(targetDecomposition); + break; + case 2: + factors = decomp2Supercontrolled(targetDecomposition); + break; + case 3: + factors = decomp3Supercontrolled(targetDecomposition); + break; + default: + llvm::reportFatalInternalError(llvm::formatv( + "Invalid number of basis gates to use in basis decomposition ({0})!", + bestNbasis)); + llvm_unreachable(""); + } + + double globalPhase = targetDecomposition.globalPhase(); + globalPhase -= bestNbasis * basisWeyl.globalPhase(); + if (bestNbasis == 2) { + globalPhase += PI; + } + globalPhase = remEuclid(globalPhase, 2.0 * PI); + + return TwoQubitNativeDecomposition{ + .numBasisUses = bestNbasis, + .singleQubitFactors = std::move(factors), + .globalPhase = globalPhase, + }; +} + +SmallVector +TwoQubitBasisDecomposer::decomp0(const TwoQubitWeylDecomposition& target) { + return SmallVector{ + target.k1r() * target.k2r(), + target.k1l() * target.k2l(), + }; +} + +SmallVector TwoQubitBasisDecomposer::decomp1( + const TwoQubitWeylDecomposition& target) const { + return SmallVector{ + basisWeyl.k2r().adjoint() * target.k2r(), + basisWeyl.k2l().adjoint() * target.k2l(), + target.k1r() * basisWeyl.k1r().adjoint(), + target.k1l() * basisWeyl.k1l().adjoint(), + }; +} + +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 SmallVector{ + smb.q2r * target.k2r(), + smb.q2l * target.k2l(), + smb.q1ra * rzMatrix(2. * target.b()) * smb.q1rb, + smb.q1la * rzMatrix(-2. * target.a()) * smb.q1lb, + target.k1r() * smb.q0r, + target.k1l() * smb.q0l, + }; +} + +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 SmallVector{ + smb.u3r * target.k2r(), + smb.u3l * target.k2l(), + smb.u2ra * rzMatrix(2. * target.b()) * smb.u2rb, + smb.u2la * rzMatrix(-2. * target.a()) * smb.u2lb, + smb.u1ra * rzMatrix(-2. * target.c()) * smb.u1rb, + smb.u1l, + target.k1r() * smb.u0r, + target.k1l() * smb.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(PI_OVER_4 - target.a()) * + std::cos(basisWeyl.b() - target.b()) * + std::cos(target.c()), + std::sin(PI_OVER_4 - target.a()) * + std::sin(basisWeyl.b() - target.b()) * + std::sin(target.c())}, + std::complex{4. * std::cos(target.c()), 0.}, + std::complex{4., 0.}, + }; +} + +std::optional +decomposeTwoQubitWithBasis(const Matrix4x4& target, + const Matrix4x4& basisMatrix, + const double basisFidelity, + const std::optional numBasisUses) { + const auto decomposer = + TwoQubitBasisDecomposer::create(basisMatrix, basisFidelity); + return decomposer.decomposeTarget(target, numBasisUses); +} + +} // namespace mlir::qco::decomposition diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp index f2c09d181d..0ba70c6b9a 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp @@ -24,7 +24,6 @@ #include #include #include -#include #include #include #include @@ -50,15 +49,10 @@ enum class Specialization : std::uint8_t { FSimabmbEquiv, }; -} // namespace - -/** Default fidelity for Weyl specialization. */ -static constexpr double DEFAULT_FIDELITY = 1.0 - 1e-12; +static constexpr double kDiagonalizationPrecision = 1e-13; +static constexpr double kPi = std::numbers::pi; +static constexpr double kPiOver4 = kPi / 4.0; -/** M2 diagonalization tolerance. */ -static constexpr double DIAGONALIZATION_PRECISION = 1e-13; - -/** Non-negative remainder of @p a modulo @p b (always in `[0, |b|)`). */ static double remEuclid(const double a, const double b) { if (b == 0.0) { llvm::reportFatalInternalError("remEuclid expects non-zero divisor"); @@ -73,8 +67,6 @@ static double traceToFidelity(const Complex& trace) { return (4.0 + (traceAbs * traceAbs)) / 20.0; } -static constexpr double INV_SQRT2 = 1.0 / std::numbers::sqrt2; - static const Matrix4x4 kMagicBasisNonNormalized = Matrix4x4::fromElements( // 1, 1i, 0, 0, // 0, 0, 1i, 1, // @@ -96,9 +88,9 @@ static Matrix4x4 magicBasisTransform(const Matrix4x4& unitary, } static double 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}; + const auto m = (a + b + c) / 3.; + const auto [am, bm, cm] = std::array{a - m, b - m, c - m}; + const 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.); } @@ -175,7 +167,7 @@ decomposeTwoQubitProductGate(const Matrix4x4& specialUnitary) { r *= (1.0 / std::sqrt(detR)); const Matrix2x2 rTConj = r.adjoint(); - Matrix4x4 temp = + const Matrix4x4 temp = specialUnitary * Matrix4x4::kron(Matrix2x2::identity(), rTConj); Matrix2x2 l = @@ -186,16 +178,16 @@ decomposeTwoQubitProductGate(const Matrix4x4& specialUnitary) { "decomposeTwoQubitProductGate: unable to decompose: detL < 0.9"); } l *= (1.0 / std::sqrt(detL)); - auto phase = std::arg(detL) / 2.; + const auto phase = std::arg(detL) / 2.; return {l, r, phase}; } static std::complex 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; + const auto da = a - ap; + const auto db = b - bp; + const 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)}; } @@ -204,26 +196,24 @@ static Specialization bestSpecialization(const TwoQubitWeylDecomposition& decomposition, const std::optional& requestedFidelity) { auto isClose = [&](double ap, double bp, double cp) -> bool { - auto tr = getTrace(decomposition.a(), decomposition.b(), decomposition.c(), - ap, bp, cp); + const auto tr = getTrace(decomposition.a(), decomposition.b(), + decomposition.c(), ap, bp, cp); if (requestedFidelity) { return traceToFidelity(tr) >= *requestedFidelity; } return false; }; - auto closestAbc = closestPartialSwap(decomposition.a(), decomposition.b(), - decomposition.c()); - auto closestAbMinusC = closestPartialSwap( + const auto closestAbc = closestPartialSwap( + decomposition.a(), decomposition.b(), decomposition.c()); + const auto closestAbMinusC = closestPartialSwap( decomposition.a(), decomposition.b(), -decomposition.c()); if (isClose(0., 0., 0.)) { return Specialization::IdEquiv; } - if (isClose((std::numbers::pi / 4.0), (std::numbers::pi / 4.0), - (std::numbers::pi / 4.0)) || - isClose((std::numbers::pi / 4.0), (std::numbers::pi / 4.0), - -(std::numbers::pi / 4.0))) { + if (isClose(kPiOver4, kPiOver4, kPiOver4) || + isClose(kPiOver4, kPiOver4, -kPiOver4)) { return Specialization::SWAPEquiv; } if (isClose(closestAbc, closestAbc, closestAbc)) { @@ -235,8 +225,7 @@ bestSpecialization(const TwoQubitWeylDecomposition& decomposition, if (isClose(decomposition.a(), 0., 0.)) { return Specialization::ControlledEquiv; } - if (isClose((std::numbers::pi / 4.0), (std::numbers::pi / 4.0), - decomposition.c())) { + if (isClose(kPiOver4, kPiOver4, decomposition.c())) { return Specialization::MirrorControlledEquiv; } if (isClose((decomposition.a() + decomposition.b()) / 2., @@ -255,58 +244,48 @@ bestSpecialization(const TwoQubitWeylDecomposition& decomposition, return Specialization::General; } -static bool relativeEq(double lhs, double rhs, double epsilon, - double maxRelative) { - if (lhs == rhs) { - return true; - } - if (std::isinf(lhs) || std::isinf(rhs)) { - return false; - } - auto absDiff = std::abs(lhs - rhs); - 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; +struct ChamberState { + std::array cs{}; + Matrix2x2 k1l; + Matrix2x2 k1r; + Matrix2x2 k2l; + Matrix2x2 k2r; + double globalPhase{}; + double a{}; + double b{}; + double c{}; +}; + +static std::pair projectToSU4(const Matrix4x4& unitary) { + auto u = unitary; + const auto detU = u.determinant(); + u *= std::pow(detU, -0.25); + return {u, std::arg(detU) / 4.0}; } -TwoQubitWeylDecomposition -TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, - std::optional fidelity) { - auto u = unitaryMatrix; - auto detU = u.determinant(); - // Project into SU(4): det^{-1/4} removes global phase; arg(det)/4 is tracked. - auto detPow = std::pow(detU, -0.25); - u *= detPow; - auto globalPhase = std::arg(detU) / 4.; - - auto uP = magicBasisTransform(u, /*outOfMagicBasis=*/true); +static std::tuple, + std::array> +computeOrderedWeylCoordinates(const Matrix4x4& u) { + const auto uP = magicBasisTransform(u, /*outOfMagicBasis=*/true); const Matrix4x4 m2 = uP.transpose() * uP; + auto [p, d] = diagonalizeComplexSymmetric(m2, kDiagonalizationPrecision); - auto [p, d] = diagonalizeComplexSymmetric(m2, DIAGONALIZATION_PRECISION); - - // Map eigenvalue phases to Weyl coordinates in [0, 2*pi). - constexpr double pi = std::numbers::pi; std::array dReal{}; for (std::size_t i = 0; i < d.size(); ++i) { dReal[i] = -std::arg(d[i]) / 2.0; } dReal[3] = -dReal[0] - dReal[1] - dReal[2]; + std::array cs{}; for (std::size_t i = 0; i < cs.size(); ++i) { - cs[i] = remEuclid((dReal[i] + dReal[3]) / 2.0, 2.0 * pi); + cs[i] = remEuclid((dReal[i] + dReal[3]) / 2.0, 2.0 * kPi); } // Sort coordinates by min(x mod pi/2, pi/2 - x mod pi/2). std::array cstemp{}; for (std::size_t i = 0; i < cs.size(); ++i) { - const auto tmp = remEuclid(cs[i], pi / 2.0); - cstemp[i] = std::min(tmp, (pi / 2.0) - tmp); + const auto tmp = remEuclid(cs[i], kPi / 2.0); + cstemp[i] = std::min(tmp, (kPi / 2.0) - tmp); } std::array order{0, 1, 2}; std::ranges::stable_sort( @@ -334,6 +313,13 @@ TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, } assert(std::abs(p.determinant() - 1.0) < WEYL_TOLERANCE); + return {uP, p, cs, dReal}; +} + +static ChamberState buildChamberState(const Matrix4x4& u, const Matrix4x4& uP, + Matrix4x4 p, std::array cs, + const std::array& dReal, + double globalPhase) { std::array tempDiag{}; for (std::size_t k = 0; k < tempDiag.size(); ++k) { tempDiag[k] = std::exp(1i * dReal[k]); @@ -361,93 +347,93 @@ TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, k2) .isApprox(u, WEYL_TOLERANCE)); - auto [K1l, K1r, phaseL] = decomposeTwoQubitProductGate(k1); - auto [K2l, K2r, phaseR] = decomposeTwoQubitProductGate(k2); - assert(Matrix4x4::kron(K1l, K1r).isApprox(k1, WEYL_TOLERANCE)); - assert(Matrix4x4::kron(K2l, K2r).isApprox(k2, WEYL_TOLERANCE)); + auto [k1l, k1r, phaseL] = decomposeTwoQubitProductGate(k1); + auto [k2l, k2r, phaseR] = decomposeTwoQubitProductGate(k2); + assert(Matrix4x4::kron(k1l, k1r).isApprox(k1, WEYL_TOLERANCE)); + assert(Matrix4x4::kron(k2l, k2r).isApprox(k2, WEYL_TOLERANCE)); globalPhase += phaseL + phaseR; - // Map into the Weyl chamber. - if (cs[0] > (pi / 2.0)) { - cs[0] -= 3.0 * (pi / 2.0); - K1l = K1l * iPauliY(); - K1r = K1r * iPauliY(); - globalPhase += (pi / 2.0); + if (cs[0] > (kPi / 2.0)) { + cs[0] -= 3.0 * (kPi / 2.0); + k1l = k1l * iPauliY(); + k1r = k1r * iPauliY(); + globalPhase += (kPi / 2.0); } - if (cs[1] > (pi / 2.0)) { - cs[1] -= 3.0 * (pi / 2.0); - K1l = K1l * iPauliX(); - K1r = K1r * iPauliX(); - globalPhase += (pi / 2.0); + if (cs[1] > (kPi / 2.0)) { + cs[1] -= 3.0 * (kPi / 2.0); + k1l = k1l * iPauliX(); + k1r = k1r * iPauliX(); + globalPhase += (kPi / 2.0); } auto conjs = 0; - if (cs[0] > (pi / 4.0)) { - cs[0] = (pi / 2.0) - cs[0]; - K1l = K1l * iPauliY(); - K2r = iPauliY() * K2r; + if (cs[0] > kPiOver4) { + cs[0] = (kPi / 2.0) - cs[0]; + k1l = k1l * iPauliY(); + k2r = iPauliY() * k2r; conjs += 1; - globalPhase -= (pi / 2.0); + globalPhase -= (kPi / 2.0); } - if (cs[1] > (pi / 4.0)) { - cs[1] = (pi / 2.0) - cs[1]; - K1l = K1l * iPauliX(); - K2r = iPauliX() * K2r; + if (cs[1] > kPiOver4) { + cs[1] = (kPi / 2.0) - cs[1]; + k1l = k1l * iPauliX(); + k2r = iPauliX() * k2r; conjs += 1; - globalPhase += (pi / 2.0); + globalPhase += (kPi / 2.0); if (conjs == 1) { - globalPhase -= pi; + globalPhase -= kPi; } } - if (cs[2] > (pi / 2.0)) { - cs[2] -= 3.0 * (pi / 2.0); - K1l = K1l * iPauliZ(); - K1r = K1r * iPauliZ(); - globalPhase += (pi / 2.0); + if (cs[2] > (kPi / 2.0)) { + cs[2] -= 3.0 * (kPi / 2.0); + k1l = k1l * iPauliZ(); + k1r = k1r * iPauliZ(); + globalPhase += (kPi / 2.0); if (conjs == 1) { - globalPhase -= pi; + globalPhase -= kPi; } } if (conjs == 1) { - cs[2] = (pi / 2.0) - cs[2]; - K1l = K1l * iPauliZ(); - K2r = iPauliZ() * K2r; - globalPhase += (pi / 2.0); - } - if (cs[2] > (pi / 4.0)) { - cs[2] -= (pi / 2.0); - K1l = K1l * iPauliZ(); - K1r = K1r * iPauliZ(); - globalPhase -= (pi / 2.0); - } - - // Bind Weyl coordinates to the 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; - - assert((Matrix4x4::kron(K1l, K1r) * decomposition.getCanonicalMatrix() * - Matrix4x4::kron(K2l, K2r) * std::exp(Complex{0.0, 1.0} * globalPhase)) - .isApprox(unitaryMatrix, WEYL_TOLERANCE)); + cs[2] = (kPi / 2.0) - cs[2]; + k1l = k1l * iPauliZ(); + k2r = iPauliZ() * k2r; + globalPhase += (kPi / 2.0); + } + if (cs[2] > kPiOver4) { + cs[2] -= (kPi / 2.0); + k1l = k1l * iPauliZ(); + k1r = k1r * iPauliZ(); + globalPhase -= (kPi / 2.0); + } + + ChamberState chamber; + chamber.cs = cs; + chamber.k1l = k1l; + chamber.k1r = k1r; + chamber.k2l = k2l; + chamber.k2r = k2r; + chamber.globalPhase = globalPhase; + chamber.a = cs[1]; + chamber.b = cs[0]; + chamber.c = cs[2]; + return chamber; +} - auto flippedFromOriginal = decomposition.applySpecialization(fidelity); +} // namespace - auto getTraceValue = [&]() { - if (flippedFromOriginal) { - return getTrace((pi / 2.0) - a, b, -c, decomposition.a_, decomposition.b_, - decomposition.c_); - } - return getTrace(a, b, c, decomposition.a_, decomposition.b_, - decomposition.c_); - }; - const auto trace = getTraceValue(); +//===----------------------------------------------------------------------===// +// TwoQubitWeylDecomposition +//===----------------------------------------------------------------------===// + +void TwoQubitWeylDecomposition::finalizeSpecializationPhase( + bool flippedFromOriginal, double preSpecializationA, + double preSpecializationB, double preSpecializationC, + const std::optional& fidelity) { + const auto trace = + flippedFromOriginal + ? getTrace((kPi / 2.0) - preSpecializationA, preSpecializationB, + -preSpecializationC, a_, b_, c_) + : getTrace(preSpecializationA, preSpecializationB, preSpecializationC, + a_, b_, c_); const double calculatedFidelity = traceToFidelity(trace); if (fidelity && calculatedFidelity + 1.0e-13 < *fidelity) { llvm::reportFatalInternalError(llvm::formatv( @@ -455,7 +441,34 @@ TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, "specialization is worse than requested fidelity ({0:F4} vs {1:F4})!", calculatedFidelity, *fidelity)); } - decomposition.globalPhase_ += std::arg(trace); + globalPhase_ += std::arg(trace); +} + +TwoQubitWeylDecomposition +TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, + std::optional fidelity) { + const auto [u, globalPhase0] = projectToSU4(unitaryMatrix); + auto [uP, p, cs, dReal] = computeOrderedWeylCoordinates(u); + const auto chamber = buildChamberState(u, uP, p, cs, dReal, globalPhase0); + TwoQubitWeylDecomposition decomposition; + decomposition.a_ = chamber.a; + decomposition.b_ = chamber.b; + decomposition.c_ = chamber.c; + decomposition.globalPhase_ = chamber.globalPhase; + decomposition.k1l_ = chamber.k1l; + decomposition.k2l_ = chamber.k2l; + decomposition.k1r_ = chamber.k1r; + decomposition.k2r_ = chamber.k2r; + + assert((Matrix4x4::kron(decomposition.k1l_, decomposition.k1r_) * + decomposition.getCanonicalMatrix() * + Matrix4x4::kron(decomposition.k2l_, decomposition.k2r_) * + std::exp(Complex{0.0, 1.0} * decomposition.globalPhase_)) + .isApprox(unitaryMatrix, WEYL_TOLERANCE)); + + const bool flippedFromOriginal = decomposition.applySpecialization(fidelity); + decomposition.finalizeSpecializationPhase(flippedFromOriginal, chamber.a, + chamber.b, chamber.c, fidelity); assert((Matrix4x4::kron(decomposition.k1l_, decomposition.k1r_) * decomposition.getCanonicalMatrix() * @@ -468,12 +481,7 @@ TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, Matrix4x4 TwoQubitWeylDecomposition::getCanonicalMatrix(double a, double b, double c) { - // U_d(a,b,c) = exp(i(a XX + b YY + c ZZ)); `-2*a/b/c` matches RXX/RYY/RZZ - // gate convention; product order matches Qiskit (RZZ · RYY · RXX). - const auto xx = rxxMatrix(-2.0 * a); - const auto yy = ryyMatrix(-2.0 * b); - const auto zz = rzzMatrix(-2.0 * c); - return zz * yy * xx; + return rzzMatrix(-2.0 * c) * ryyMatrix(-2.0 * b) * rxxMatrix(-2.0 * a); } bool TwoQubitWeylDecomposition::applySpecialization( @@ -484,7 +492,8 @@ bool TwoQubitWeylDecomposition::applySpecialization( return flippedFromOriginal; } - if (newSpecialization == Specialization::IdEquiv) { + switch (newSpecialization) { + case Specialization::IdEquiv: a_ = 0.; b_ = 0.; c_ = 0.; @@ -492,7 +501,8 @@ bool TwoQubitWeylDecomposition::applySpecialization( k2l_ = Matrix2x2::identity(); k1r_ = k1r_ * k2r_; k2r_ = Matrix2x2::identity(); - } else if (newSpecialization == Specialization::SWAPEquiv) { + break; + case Specialization::SWAPEquiv: if (c_ > 0.) { k1l_ = k1l_ * k2r_; k1r_ = k1r_ * k2l_; @@ -500,20 +510,19 @@ bool TwoQubitWeylDecomposition::applySpecialization( k2r_ = Matrix2x2::identity(); } else { flippedFromOriginal = true; - - globalPhase_ += (std::numbers::pi / 2.0); + globalPhase_ += (kPi / 2.0); k1l_ = k1l_ * iPauliZ() * k2r_; k1r_ = k1r_ * iPauliZ() * k2l_; k2l_ = Matrix2x2::identity(); k2r_ = Matrix2x2::identity(); } - a_ = (std::numbers::pi / 4.0); - b_ = (std::numbers::pi / 4.0); - c_ = (std::numbers::pi / 4.0); - } else if (newSpecialization == Specialization::PartialSWAPEquiv) { - auto closest = closestPartialSwap(a_, b_, c_); - auto k2lDagger = k2l_.adjoint(); - + a_ = kPiOver4; + b_ = kPiOver4; + c_ = kPiOver4; + break; + case Specialization::PartialSWAPEquiv: { + const auto closest = closestPartialSwap(a_, b_, c_); + const auto k2lDagger = k2l_.adjoint(); a_ = closest; b_ = closest; c_ = closest; @@ -521,10 +530,11 @@ bool TwoQubitWeylDecomposition::applySpecialization( k1r_ = k1r_ * k2l_; k2r_ = k2lDagger * k2r_; k2l_ = Matrix2x2::identity(); - } else if (newSpecialization == Specialization::PartialSWAPFlipEquiv) { - auto closest = closestPartialSwap(a_, b_, -c_); - auto k2lDagger = k2l_.adjoint(); - + break; + } + case Specialization::PartialSWAPFlipEquiv: { + const auto closest = closestPartialSwap(a_, b_, -c_); + const auto k2lDagger = k2l_.adjoint(); a_ = closest; b_ = closest; c_ = -closest; @@ -532,10 +542,11 @@ bool TwoQubitWeylDecomposition::applySpecialization( k1r_ = k1r_ * iPauliZ() * k2l_ * iPauliZ(); k2r_ = iPauliZ() * k2lDagger * iPauliZ() * k2r_; k2l_ = Matrix2x2::identity(); - } else if (newSpecialization == Specialization::ControlledEquiv) { - const EulerBasis eulerBasis = EulerBasis::XYX; + break; + } + case Specialization::ControlledEquiv: { const auto [k2ltheta, k2lphi, k2llambda, k2lphase] = - anglesFromUnitary(k2l_, eulerBasis); + anglesFromUnitary(k2l_, EulerBasis::XYX); const auto [k2rtheta, k2rphi, k2rlambda, k2rphase] = anglesFromUnitary(k2r_, EulerBasis::XYX); b_ = 0.; @@ -545,23 +556,26 @@ bool TwoQubitWeylDecomposition::applySpecialization( k2l_ = ryMatrix(k2ltheta) * rxMatrix(k2llambda); k1r_ = k1r_ * rxMatrix(k2rphi); k2r_ = ryMatrix(k2rtheta) * rxMatrix(k2rlambda); - } else if (newSpecialization == Specialization::MirrorControlledEquiv) { + break; + } + case Specialization::MirrorControlledEquiv: { const auto [k2ltheta, k2lphi, k2llambda, k2lphase] = anglesFromUnitary(k2l_, EulerBasis::ZYZ); const auto [k2rtheta, k2rphi, k2rlambda, k2rphase] = anglesFromUnitary(k2r_, EulerBasis::ZYZ); - a_ = (std::numbers::pi / 4.0); - b_ = (std::numbers::pi / 4.0); + a_ = kPiOver4; + b_ = kPiOver4; 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) { - auto [k2ltheta, k2lphi, k2llambda, k2lphase] = + break; + } + case Specialization::FSimaabEquiv: { + const auto [k2ltheta, k2lphi, k2llambda, k2lphase] = anglesFromUnitary(k2l_, EulerBasis::ZYZ); - auto ab = (a_ + b_) / 2.; - + const auto ab = (a_ + b_) / 2.; a_ = ab; b_ = ab; globalPhase_ = globalPhase_ + k2lphase; @@ -569,12 +583,12 @@ bool TwoQubitWeylDecomposition::applySpecialization( k2l_ = ryMatrix(k2ltheta) * rzMatrix(k2llambda); k1r_ = k1r_ * rzMatrix(k2lphi); k2r_ = rzMatrix(-k2lphi) * k2r_; - } else if (newSpecialization == Specialization::FSimabbEquiv) { - auto eulerBasis = EulerBasis::XYX; - auto [k2ltheta, k2lphi, k2llambda, k2lphase] = - anglesFromUnitary(k2l_, eulerBasis); - auto bc = (b_ + c_) / 2.; - + break; + } + case Specialization::FSimabbEquiv: { + const auto [k2ltheta, k2lphi, k2llambda, k2lphase] = + anglesFromUnitary(k2l_, EulerBasis::XYX); + const auto bc = (b_ + c_) / 2.; b_ = bc; c_ = bc; globalPhase_ = globalPhase_ + k2lphase; @@ -582,12 +596,12 @@ bool TwoQubitWeylDecomposition::applySpecialization( k2l_ = ryMatrix(k2ltheta) * rxMatrix(k2llambda); k1r_ = k1r_ * rxMatrix(k2lphi); k2r_ = rxMatrix(-k2lphi) * k2r_; - } else if (newSpecialization == Specialization::FSimabmbEquiv) { - auto eulerBasis = EulerBasis::XYX; - auto [k2ltheta, k2lphi, k2llambda, k2lphase] = - anglesFromUnitary(k2l_, eulerBasis); - auto bc = (b_ - c_) / 2.; - + break; + } + case Specialization::FSimabmbEquiv: { + const auto [k2ltheta, k2lphi, k2llambda, k2lphase] = + anglesFromUnitary(k2l_, EulerBasis::XYX); + const auto bc = (b_ - c_) / 2.; b_ = bc; c_ = -bc; globalPhase_ = globalPhase_ + k2lphase; @@ -595,263 +609,12 @@ bool TwoQubitWeylDecomposition::applySpecialization( k2l_ = ryMatrix(k2ltheta) * rxMatrix(k2llambda); k1r_ = k1r_ * iPauliZ() * rxMatrix(k2lphi) * iPauliZ(); k2r_ = iPauliZ() * rxMatrix(-k2lphi) * iPauliZ() * k2r_; - } else { - llvm::reportFatalInternalError( - "Unknown specialization for Weyl decomposition!"); + break; } - return flippedFromOriginal; -} - -TwoQubitBasisDecomposer -TwoQubitBasisDecomposer::create(const Matrix4x4& basisMatrix, - double basisFidelity) { - const Matrix2x2 k12RArr = Matrix2x2::fromElements( - 1i * INV_SQRT2, INV_SQRT2, -INV_SQRT2, -1i * INV_SQRT2); - const Matrix2x2 k12LArr = - Matrix2x2::fromElements(Complex{0.5, 0.5}, Complex{0.5, 0.5}, - Complex{-0.5, 0.5}, Complex{0.5, -0.5}); - - const auto basisDecomposer = - TwoQubitWeylDecomposition::create(basisMatrix, DEFAULT_FIDELITY); - const auto isSuperControlled = - relativeEq(basisDecomposer.a(), std::numbers::pi / 4.0, 1e-13, 1e-09) && - relativeEq(basisDecomposer.c(), 0.0, 1e-13, 1e-09); - - auto b = basisDecomposer.b(); - Complex temp{0.5, -0.5}; - const Matrix2x2 k11l = Matrix2x2::fromElements( - temp * (-1i * std::exp(-1i * b)), temp * std::exp(-1i * b), - temp * (-1i * std::exp(1i * b)), temp * -std::exp(1i * b)); - const Matrix2x2 k11r = Matrix2x2::fromElements( - INV_SQRT2 * (1i * std::exp(-1i * b)), INV_SQRT2 * -std::exp(-1i * b), - INV_SQRT2 * std::exp(1i * b), INV_SQRT2 * (-1i * std::exp(1i * b))); - const Matrix2x2 k32lK21l = Matrix2x2::fromElements( - INV_SQRT2 * Complex{1., std::cos(2. * b)}, - INV_SQRT2 * (1i * std::sin(2. * b)), INV_SQRT2 * (1i * std::sin(2. * b)), - INV_SQRT2 * Complex{1., -std::cos(2. * b)}); - temp = Complex{0.5, 0.5}; - const Matrix2x2 k21r = Matrix2x2::fromElements( - temp * (-1i * std::exp(-2i * b)), temp * std::exp(-2i * b), - temp * (1i * std::exp(2i * b)), temp * std::exp(2i * b)); - const Matrix2x2 k22l = - Matrix2x2::fromElements(INV_SQRT2, -INV_SQRT2, INV_SQRT2, INV_SQRT2); - const Matrix2x2 k22r = Matrix2x2::fromElements(0, 1, -1, 0); - const Matrix2x2 k31l = Matrix2x2::fromElements( - INV_SQRT2 * std::exp(-1i * b), INV_SQRT2 * std::exp(-1i * b), - INV_SQRT2 * -std::exp(1i * b), INV_SQRT2 * std::exp(1i * b)); - const Matrix2x2 k31r = Matrix2x2::fromElements(1i * std::exp(1i * b), 0, 0, - -1i * std::exp(-1i * b)); - const Matrix2x2 k32r = Matrix2x2::fromElements( - 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().adjoint(); - auto k1rDagger = basisDecomposer.k1r().adjoint(); - auto k2lDagger = basisDecomposer.k2l().adjoint(); - auto k2rDagger = basisDecomposer.k2r().adjoint(); - 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; - auto q0l = k12LArr.adjoint() * k1lDagger; - auto q0r = k12RArr.adjoint() * iPauliZ() * k1rDagger; - auto q1la = k2lDagger * k11l.adjoint(); - auto q1lb = k11l * k1lDagger; - auto q1ra = k2rDagger * iPauliZ() * k11r.adjoint(); - auto q1rb = k11r * k1rDagger; - auto q2l = k2lDagger * k12LArr; - auto q2r = k2rDagger * k12RArr; - - TwoQubitBasisDecomposer decomposer; - decomposer.basisFidelity = basisFidelity; - decomposer.basisDecomposer = basisDecomposer; - decomposer.isSuperControlled = isSuperControlled; - decomposer.smb.u0l = u0l; - decomposer.smb.u0r = u0r; - decomposer.smb.u1l = u1l; - decomposer.smb.u1ra = u1ra; - decomposer.smb.u1rb = u1rb; - decomposer.smb.u2la = u2la; - decomposer.smb.u2lb = u2lb; - decomposer.smb.u2ra = u2ra; - decomposer.smb.u2rb = u2rb; - decomposer.smb.u3l = u3l; - decomposer.smb.u3r = u3r; - decomposer.smb.q0l = q0l; - decomposer.smb.q0r = q0r; - decomposer.smb.q1la = q1la; - decomposer.smb.q1lb = q1lb; - decomposer.smb.q1ra = q1ra; - decomposer.smb.q1rb = q1rb; - decomposer.smb.q2l = q2l; - decomposer.smb.q2r = q2r; - return decomposer; -} - -std::optional -TwoQubitBasisDecomposer::decomposeTarget( - const Matrix4x4& targetUnitary, - const std::optional numBasisGateUses) const { - const auto targetWeyl = - TwoQubitWeylDecomposition::create(targetUnitary, DEFAULT_FIDELITY); - return twoQubitDecompose(targetWeyl, numBasisGateUses); -} - -std::optional -TwoQubitBasisDecomposer::twoQubitDecompose( - const TwoQubitWeylDecomposition& targetDecomposition, - std::optional numBasisGateUses) const { - auto traces = this->traces(targetDecomposition); - auto getDefaultNbasis = [&]() -> std::uint8_t { - // Maximize traceToFidelity(traces[i]) * basisFidelity^i over i in {0..3}. - auto bestValue = std::numeric_limits::lowest(); - auto bestIndex = -1; - for (int i = 0; std::cmp_less(i, traces.size()); ++i) { - auto value = traceToFidelity(traces[i]) * std::pow(basisFidelity, i); - if (std::isnan(value)) { - continue; - } - if (value > bestValue) { - bestIndex = i; - bestValue = value; - } - } - if (bestIndex < 0) { - llvm::reportFatalInternalError("Unable to select basis-gate count: all " - "candidate fidelities are NaN"); - } - return static_cast(bestIndex); - }; - auto bestNbasis = numBasisGateUses.value_or(getDefaultNbasis()); - if (bestNbasis > 1 && !isSuperControlled) { - // More than one basis gate requires a super-controlled basis (e.g. CX). - 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(""); - }; - SmallVector factors = chooseDecomposition(); - - double globalPhase = targetDecomposition.globalPhase(); - globalPhase -= bestNbasis * basisDecomposer.globalPhase(); - if (bestNbasis == 2) { - // Two-basis template is exact up to a pi global-phase offset. - globalPhase += std::numbers::pi; + case Specialization::General: + llvm_unreachable("unreachable specialization"); } - globalPhase = remEuclid(globalPhase, 2.0 * std::numbers::pi); - - return TwoQubitNativeDecomposition{ - .numBasisUses = bestNbasis, - .singleQubitFactors = std::move(factors), - .globalPhase = globalPhase, - }; -} - -SmallVector -TwoQubitBasisDecomposer::decomp0(const TwoQubitWeylDecomposition& target) { - return SmallVector{ - target.k1r() * target.k2r(), - target.k1l() * target.k2l(), - }; -} - -SmallVector TwoQubitBasisDecomposer::decomp1( - const TwoQubitWeylDecomposition& target) const { - // One basis gate; reliable only when the target is in the Weyl chamber. - return SmallVector{ - basisDecomposer.k2r().adjoint() * target.k2r(), - basisDecomposer.k2l().adjoint() * target.k2l(), - target.k1r() * basisDecomposer.k1r().adjoint(), - target.k1l() * basisDecomposer.k1l().adjoint(), - }; -} - -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 SmallVector{ - smb.q2r * target.k2r(), - smb.q2l * target.k2l(), - smb.q1ra * rzMatrix(2. * target.b()) * smb.q1rb, - smb.q1la * rzMatrix(-2. * target.a()) * smb.q1lb, - target.k1r() * smb.q0r, - target.k1l() * smb.q0l, - }; -} - -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 SmallVector{ - smb.u3r * target.k2r(), - smb.u3l * target.k2l(), - smb.u2ra * rzMatrix(2. * target.b()) * smb.u2rb, - smb.u2la * rzMatrix(-2. * target.a()) * smb.u2lb, - smb.u1ra * rzMatrix(-2. * target.c()) * smb.u1rb, - smb.u1l, - target.k1r() * smb.u0r, - target.k1l() * smb.u0l, - }; -} - -std::array, 4> -TwoQubitBasisDecomposer::traces(const TwoQubitWeylDecomposition& target) const { - // Hilbert-Schmidt traces for 0..3 basis uses; index i == numBasisUses. - // i=0: identity; i=1: one basis gate; i=2: two uses; i=3: exact (trace=4). - 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((std::numbers::pi / 4.0) - target.a()) * - std::cos(basisDecomposer.b() - target.b()) * - std::cos(target.c()), - std::sin((std::numbers::pi / 4.0) - target.a()) * - std::sin(basisDecomposer.b() - target.b()) * - std::sin(target.c())}, - std::complex{4. * std::cos(target.c()), 0.}, - std::complex{4., 0.}, - }; -} - -std::optional -decomposeTwoQubitWithBasis(const Matrix4x4& target, - const Matrix4x4& basisMatrix, - const double basisFidelity, - const std::optional numBasisUses) { - const auto decomposer = - TwoQubitBasisDecomposer::create(basisMatrix, basisFidelity); - return decomposer.decomposeTarget(target, numBasisUses); + return flippedFromOriginal; } } // namespace mlir::qco::decomposition From 341cfc88cc3b612451c3677dccd5ca9985653ac7 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 23 Jun 2026 15:58:16 +0200 Subject: [PATCH 077/122] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20linter=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/Decomposition/Weyl.cpp | 122 +++++++++--------- 1 file changed, 62 insertions(+), 60 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp index 0ba70c6b9a..32a0ba391f 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp @@ -49,9 +49,23 @@ enum class Specialization : std::uint8_t { FSimabmbEquiv, }; -static constexpr double kDiagonalizationPrecision = 1e-13; -static constexpr double kPi = std::numbers::pi; -static constexpr double kPiOver4 = kPi / 4.0; +struct ChamberState { + std::array cs{}; + Matrix2x2 k1l; + Matrix2x2 k1r; + Matrix2x2 k2l; + Matrix2x2 k2r; + double globalPhase{}; + double a{}; + double b{}; + double c{}; +}; + +} // namespace + +static constexpr double DIAGONALIZATION_PRECISION = 1e-13; +static constexpr double PI = std::numbers::pi; +static constexpr double PI_OVER_4 = PI / 4.0; static double remEuclid(const double a, const double b) { if (b == 0.0) { @@ -67,12 +81,12 @@ static double traceToFidelity(const Complex& trace) { return (4.0 + (traceAbs * traceAbs)) / 20.0; } -static const Matrix4x4 kMagicBasisNonNormalized = Matrix4x4::fromElements( // - 1, 1i, 0, 0, // - 0, 0, 1i, 1, // - 0, 0, 1i, -1, // +static const Matrix4x4 MAGIC_BASIS_NON_NORMALIZED = Matrix4x4::fromElements( // + 1, 1i, 0, 0, // + 0, 0, 1i, 1, // + 0, 0, 1i, -1, // 1, -1i, 0, 0); -static const Matrix4x4 kMagicBasisNonNormalizedDagger = +static const Matrix4x4 MAGIC_BASIS_NON_NORMALIZED_DAGGER = Matrix4x4::fromElements( // 0.5, 0, 0, 0.5, // Complex{0.0, -0.5}, 0, 0, Complex{0.0, 0.5}, // @@ -82,9 +96,11 @@ static const Matrix4x4 kMagicBasisNonNormalizedDagger = static Matrix4x4 magicBasisTransform(const Matrix4x4& unitary, bool outOfMagicBasis) { if (outOfMagicBasis) { - return kMagicBasisNonNormalizedDagger * unitary * kMagicBasisNonNormalized; + return MAGIC_BASIS_NON_NORMALIZED_DAGGER * unitary * + MAGIC_BASIS_NON_NORMALIZED; } - return kMagicBasisNonNormalized * unitary * kMagicBasisNonNormalizedDagger; + return MAGIC_BASIS_NON_NORMALIZED * unitary * + MAGIC_BASIS_NON_NORMALIZED_DAGGER; } static double closestPartialSwap(double a, double b, double c) { @@ -212,8 +228,8 @@ bestSpecialization(const TwoQubitWeylDecomposition& decomposition, if (isClose(0., 0., 0.)) { return Specialization::IdEquiv; } - if (isClose(kPiOver4, kPiOver4, kPiOver4) || - isClose(kPiOver4, kPiOver4, -kPiOver4)) { + if (isClose(PI_OVER_4, PI_OVER_4, PI_OVER_4) || + isClose(PI_OVER_4, PI_OVER_4, -PI_OVER_4)) { return Specialization::SWAPEquiv; } if (isClose(closestAbc, closestAbc, closestAbc)) { @@ -225,7 +241,7 @@ bestSpecialization(const TwoQubitWeylDecomposition& decomposition, if (isClose(decomposition.a(), 0., 0.)) { return Specialization::ControlledEquiv; } - if (isClose(kPiOver4, kPiOver4, decomposition.c())) { + if (isClose(PI_OVER_4, PI_OVER_4, decomposition.c())) { return Specialization::MirrorControlledEquiv; } if (isClose((decomposition.a() + decomposition.b()) / 2., @@ -244,18 +260,6 @@ bestSpecialization(const TwoQubitWeylDecomposition& decomposition, return Specialization::General; } -struct ChamberState { - std::array cs{}; - Matrix2x2 k1l; - Matrix2x2 k1r; - Matrix2x2 k2l; - Matrix2x2 k2r; - double globalPhase{}; - double a{}; - double b{}; - double c{}; -}; - static std::pair projectToSU4(const Matrix4x4& unitary) { auto u = unitary; const auto detU = u.determinant(); @@ -268,7 +272,7 @@ static std::tuple, computeOrderedWeylCoordinates(const Matrix4x4& u) { const auto uP = magicBasisTransform(u, /*outOfMagicBasis=*/true); const Matrix4x4 m2 = uP.transpose() * uP; - auto [p, d] = diagonalizeComplexSymmetric(m2, kDiagonalizationPrecision); + auto [p, d] = diagonalizeComplexSymmetric(m2, DIAGONALIZATION_PRECISION); std::array dReal{}; for (std::size_t i = 0; i < d.size(); ++i) { @@ -278,14 +282,14 @@ computeOrderedWeylCoordinates(const Matrix4x4& u) { std::array cs{}; for (std::size_t i = 0; i < cs.size(); ++i) { - cs[i] = remEuclid((dReal[i] + dReal[3]) / 2.0, 2.0 * kPi); + cs[i] = remEuclid((dReal[i] + dReal[3]) / 2.0, 2.0 * PI); } // Sort coordinates by min(x mod pi/2, pi/2 - x mod pi/2). std::array cstemp{}; for (std::size_t i = 0; i < cs.size(); ++i) { - const auto tmp = remEuclid(cs[i], kPi / 2.0); - cstemp[i] = std::min(tmp, (kPi / 2.0) - tmp); + const auto tmp = remEuclid(cs[i], PI / 2.0); + cstemp[i] = std::min(tmp, (PI / 2.0) - tmp); } std::array order{0, 1, 2}; std::ranges::stable_sort( @@ -353,56 +357,56 @@ static ChamberState buildChamberState(const Matrix4x4& u, const Matrix4x4& uP, assert(Matrix4x4::kron(k2l, k2r).isApprox(k2, WEYL_TOLERANCE)); globalPhase += phaseL + phaseR; - if (cs[0] > (kPi / 2.0)) { - cs[0] -= 3.0 * (kPi / 2.0); + if (cs[0] > (PI / 2.0)) { + cs[0] -= 3.0 * (PI / 2.0); k1l = k1l * iPauliY(); k1r = k1r * iPauliY(); - globalPhase += (kPi / 2.0); + globalPhase += (PI / 2.0); } - if (cs[1] > (kPi / 2.0)) { - cs[1] -= 3.0 * (kPi / 2.0); + if (cs[1] > (PI / 2.0)) { + cs[1] -= 3.0 * (PI / 2.0); k1l = k1l * iPauliX(); k1r = k1r * iPauliX(); - globalPhase += (kPi / 2.0); + globalPhase += (PI / 2.0); } auto conjs = 0; - if (cs[0] > kPiOver4) { - cs[0] = (kPi / 2.0) - cs[0]; + if (cs[0] > PI_OVER_4) { + cs[0] = (PI / 2.0) - cs[0]; k1l = k1l * iPauliY(); k2r = iPauliY() * k2r; conjs += 1; - globalPhase -= (kPi / 2.0); + globalPhase -= (PI / 2.0); } - if (cs[1] > kPiOver4) { - cs[1] = (kPi / 2.0) - cs[1]; + if (cs[1] > PI_OVER_4) { + cs[1] = (PI / 2.0) - cs[1]; k1l = k1l * iPauliX(); k2r = iPauliX() * k2r; conjs += 1; - globalPhase += (kPi / 2.0); + globalPhase += (PI / 2.0); if (conjs == 1) { - globalPhase -= kPi; + globalPhase -= PI; } } - if (cs[2] > (kPi / 2.0)) { - cs[2] -= 3.0 * (kPi / 2.0); + if (cs[2] > (PI / 2.0)) { + cs[2] -= 3.0 * (PI / 2.0); k1l = k1l * iPauliZ(); k1r = k1r * iPauliZ(); - globalPhase += (kPi / 2.0); + globalPhase += (PI / 2.0); if (conjs == 1) { - globalPhase -= kPi; + globalPhase -= PI; } } if (conjs == 1) { - cs[2] = (kPi / 2.0) - cs[2]; + cs[2] = (PI / 2.0) - cs[2]; k1l = k1l * iPauliZ(); k2r = iPauliZ() * k2r; - globalPhase += (kPi / 2.0); + globalPhase += (PI / 2.0); } - if (cs[2] > kPiOver4) { - cs[2] -= (kPi / 2.0); + if (cs[2] > PI_OVER_4) { + cs[2] -= (PI / 2.0); k1l = k1l * iPauliZ(); k1r = k1r * iPauliZ(); - globalPhase -= (kPi / 2.0); + globalPhase -= (PI / 2.0); } ChamberState chamber; @@ -418,8 +422,6 @@ static ChamberState buildChamberState(const Matrix4x4& u, const Matrix4x4& uP, return chamber; } -} // namespace - //===----------------------------------------------------------------------===// // TwoQubitWeylDecomposition //===----------------------------------------------------------------------===// @@ -430,7 +432,7 @@ void TwoQubitWeylDecomposition::finalizeSpecializationPhase( const std::optional& fidelity) { const auto trace = flippedFromOriginal - ? getTrace((kPi / 2.0) - preSpecializationA, preSpecializationB, + ? getTrace((PI / 2.0) - preSpecializationA, preSpecializationB, -preSpecializationC, a_, b_, c_) : getTrace(preSpecializationA, preSpecializationB, preSpecializationC, a_, b_, c_); @@ -510,15 +512,15 @@ bool TwoQubitWeylDecomposition::applySpecialization( k2r_ = Matrix2x2::identity(); } else { flippedFromOriginal = true; - globalPhase_ += (kPi / 2.0); + globalPhase_ += (PI / 2.0); k1l_ = k1l_ * iPauliZ() * k2r_; k1r_ = k1r_ * iPauliZ() * k2l_; k2l_ = Matrix2x2::identity(); k2r_ = Matrix2x2::identity(); } - a_ = kPiOver4; - b_ = kPiOver4; - c_ = kPiOver4; + a_ = PI_OVER_4; + b_ = PI_OVER_4; + c_ = PI_OVER_4; break; case Specialization::PartialSWAPEquiv: { const auto closest = closestPartialSwap(a_, b_, c_); @@ -563,8 +565,8 @@ bool TwoQubitWeylDecomposition::applySpecialization( anglesFromUnitary(k2l_, EulerBasis::ZYZ); const auto [k2rtheta, k2rphi, k2rlambda, k2rphase] = anglesFromUnitary(k2r_, EulerBasis::ZYZ); - a_ = kPiOver4; - b_ = kPiOver4; + a_ = PI_OVER_4; + b_ = PI_OVER_4; globalPhase_ = globalPhase_ + k2lphase + k2rphase; k1l_ = k1l_ * rzMatrix(k2rphi); k2l_ = ryMatrix(k2ltheta) * rzMatrix(k2llambda); From 8114583941829d0548273ea126b5e82d92f61024 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 23 Jun 2026 16:34:43 +0200 Subject: [PATCH 078/122] =?UTF-8?q?=F0=9F=90=87=20Address=20rabbit's=20com?= =?UTF-8?q?ments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/Decomposition/Weyl.h | 15 ++++++++++++ .../Decomposition/BasisDecomposer.cpp | 13 +++++++---- .../QCO/Transforms/Decomposition/Weyl.cpp | 23 +++++++++++++++---- .../Decomposition/test_weyl_decomposition.cpp | 8 +++++++ 4 files changed, 50 insertions(+), 9 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h index 5a0483628f..d5db42398d 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h @@ -129,6 +129,19 @@ class TwoQubitBasisDecomposer { [[nodiscard]] static TwoQubitBasisDecomposer create(const Matrix4x4& basisMatrix, double basisFidelity); + /** + * @brief Decomposes a target Weyl decomposition into single-qubit factors and + * basis-gate uses. + * + * @param targetDecomposition Weyl decomposition of the target unitary. + * @param numBasisGateUses Requested number of basis-gate applications in + * `{0, 1, 2, 3}`. Pass `std::nullopt` to pick the count that + * maximizes `traceToFidelity(trace[i]) * basisFidelity^i` over + * `i ∈ {0, 1, 2, 3}`. + * @return A native decomposition on success, or `std::nullopt` when the + * requested count is unsupported (e.g. more than one basis gate for a + * non-super-controlled basis, or a value outside `{0, 1, 2, 3}`). + */ [[nodiscard]] std::optional twoQubitDecompose(const TwoQubitWeylDecomposition& targetDecomposition, std::optional numBasisGateUses) const; @@ -177,6 +190,8 @@ class TwoQubitBasisDecomposer { [[nodiscard]] std::array, 4> traces(const TwoQubitWeylDecomposition& target) const; + TwoQubitBasisDecomposer() = default; + double basisFidelity{}; TwoQubitWeylDecomposition basisWeyl; bool isSuperControlled{}; diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp index 30c5f5709b..814702544d 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/BasisDecomposer.cpp @@ -82,6 +82,14 @@ static double traceToFidelity(const Complex& trace) { TwoQubitBasisDecomposer TwoQubitBasisDecomposer::create(const Matrix4x4& basisMatrix, double basisFidelity) { + if (!std::isfinite(basisFidelity) || basisFidelity < 0.0 || + basisFidelity > 1.0) { + llvm::reportFatalInternalError(llvm::formatv( + "TwoQubitBasisDecomposer: basisFidelity must be finite and in [0, 1] " + "(got {0})", + basisFidelity)); + } + const auto basisWeyl = TwoQubitWeylDecomposition::create(basisMatrix, DEFAULT_WEYL_FIDELITY); const auto isSuperControlled = @@ -202,10 +210,7 @@ TwoQubitBasisDecomposer::twoQubitDecompose( factors = decomp3Supercontrolled(targetDecomposition); break; default: - llvm::reportFatalInternalError(llvm::formatv( - "Invalid number of basis gates to use in basis decomposition ({0})!", - bestNbasis)); - llvm_unreachable(""); + return std::nullopt; } double globalPhase = targetDecomposition.globalPhase(); diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp index 32a0ba391f..a857fdc9cc 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp @@ -449,6 +449,14 @@ void TwoQubitWeylDecomposition::finalizeSpecializationPhase( TwoQubitWeylDecomposition TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, std::optional fidelity) { + if (fidelity && + (!std::isfinite(*fidelity) || *fidelity < 0.0 || *fidelity > 1.0)) { + llvm::reportFatalInternalError(llvm::formatv( + "TwoQubitWeylDecomposition: fidelity must be finite and in [0, 1] " + "(got {0})", + *fidelity)); + } + const auto [u, globalPhase0] = projectToSU4(unitaryMatrix); auto [uP, p, cs, dReal] = computeOrderedWeylCoordinates(u); const auto chamber = buildChamberState(u, uP, p, cs, dReal, globalPhase0); @@ -472,11 +480,16 @@ TwoQubitWeylDecomposition::create(const Matrix4x4& unitaryMatrix, decomposition.finalizeSpecializationPhase(flippedFromOriginal, chamber.a, chamber.b, chamber.c, fidelity); - assert((Matrix4x4::kron(decomposition.k1l_, decomposition.k1r_) * - decomposition.getCanonicalMatrix() * - Matrix4x4::kron(decomposition.k2l_, decomposition.k2r_) * - std::exp(Complex{0.0, 1.0} * decomposition.globalPhase_)) - .isApprox(unitaryMatrix, WEYL_TOLERANCE)); + const auto reconstructed = + Matrix4x4::kron(decomposition.k1l_, decomposition.k1r_) * + decomposition.getCanonicalMatrix() * + Matrix4x4::kron(decomposition.k2l_, decomposition.k2r_) * + std::exp(Complex{0.0, 1.0} * decomposition.globalPhase_); + if (!reconstructed.isApprox(unitaryMatrix, WEYL_TOLERANCE)) { + llvm::reportFatalInternalError( + "TwoQubitWeylDecomposition: failed to reconstruct unitary after " + "specialization"); + } return decomposition; } diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index bcdc627c60..bb11c6fb2b 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -339,6 +339,14 @@ TEST(BasisDecomposerTest, RejectsMultipleBasisUsesForNonSuperControlledBasis) { EXPECT_FALSE(decomposer.twoQubitDecompose(weyl, std::uint8_t{2}).has_value()); } +TEST(BasisDecomposerTest, RejectsInvalidBasisGateUseCount) { + const Matrix4x4 basis = twoQubitControlledX01(); + const auto decomposer = TwoQubitBasisDecomposer::create(basis, 1.0); + const auto weyl = + TwoQubitWeylDecomposition::create(twoQubitControlledX01(), 1.0); + EXPECT_FALSE(decomposer.twoQubitDecompose(weyl, std::uint8_t{4}).has_value()); +} + TEST(BasisDecomposerForcedCountTest, OneBasisUseProducesFactors) { const Matrix4x4 basis = twoQubitControlledX01(); const auto decomposer = TwoQubitBasisDecomposer::create(basis, 1.0); From 004ee4041879296e88a84a5385a3d2360d1bbadd Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 23 Jun 2026 16:49:59 +0200 Subject: [PATCH 079/122] =?UTF-8?q?=F0=9F=93=9C=20Update=20CHANGELOG.md=20?= =?UTF-8?q?to=20include=20PR=20#1803?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 614fe3c174..2a2888002c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,7 +42,7 @@ with the exception that minor releases may include breaking changes. [#1569], [#1570], [#1572], [#1573], [#1580], [#1602], [#1620], [#1623], [#1624], [#1626], [#1627], [#1635], [#1638], [#1673], [#1675], [#1700], [#1710], [#1717], [#1728], [#1730], [#1749], [#1751], [#1762], [#1765], - [#1774], [#1780], [#1781], [#1782], [#1787], [#1802]) + [#1774], [#1780], [#1781], [#1782], [#1787], [#1802], [#1803]) ([**@burgholzer**], [**@denialhaag**], [**@taminob**], [**@DRovara**], [**@li-mingbao**], [**@Ectras**], [**@MatthiasReumann**], [**@simon1hofmann**]) @@ -598,6 +598,7 @@ changelogs._ +[#1803]: https://github.com/munich-quantum-toolkit/core/pull/1803 [#1802]: https://github.com/munich-quantum-toolkit/core/pull/1802 [#1787]: https://github.com/munich-quantum-toolkit/core/pull/1787 [#1782]: https://github.com/munich-quantum-toolkit/core/pull/1782 From 325b66964d29688adb2f13202525f2c25228041e Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 24 Jun 2026 09:41:11 +0200 Subject: [PATCH 080/122] =?UTF-8?q?=F0=9F=8E=A8=20Clean=20up?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Transforms/Decomposition/NativeProfile.h | 14 ++--- mlir/include/mlir/Dialect/QCO/Utils/Matrix.h | 27 -------- .../Decomposition/NativeProfile.cpp | 62 +++++++++---------- mlir/lib/Dialect/QCO/Utils/Matrix.cpp | 29 --------- .../Decomposition/test_weyl_decomposition.cpp | 31 +++++----- .../Dialect/QCO/Utils/test_unitary_matrix.cpp | 3 +- 6 files changed, 56 insertions(+), 110 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeProfile.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeProfile.h index 13542e507b..cc07a03966 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeProfile.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeProfile.h @@ -32,14 +32,14 @@ namespace mlir::qco::decomposition { enum class NativeGateKind : std::uint8_t { U, X, - Sx, - Rz, - Rx, - Ry, + SX, + RZ, + RX, + RY, R, - Cx, - Cz, - Rzz, + CX, + CZ, + RZZ, }; /** diff --git a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h index 893899d04c..5593cd3939 100644 --- a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h +++ b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h @@ -795,33 +795,6 @@ struct SymmetricEigen4 { Matrix4x4 eigenvectors{}; }; -/// `SWAP` on two qubits. -[[nodiscard]] const Matrix4x4& twoQubitSwapMatrix(); - -inline constexpr double FRAC1_SQRT2 = - 0.707106781186547524400844362104849039284835937688474036588; - -/** - * @brief Non-negative remainder of @p a modulo @p b. - * - * Unlike `std::fmod`, the result is always in `[0, |b|)`. - */ -[[nodiscard]] double remEuclid(double a, double b); - -/** - * @brief Average two-qubit gate fidelity from a Hilbert-Schmidt trace. - * - * Maps `|tr(U^dag V)|` to fidelity via `(4 + |tr|^2) / 20`. - */ -[[nodiscard]] double traceToFidelity(const Complex& trace); - -/** @brief Unit-modulus global phase factor `exp(i * phase)`. */ -[[nodiscard]] Complex globalPhaseFactor(double phase); - -/** @brief Returns true when @p matrix is unitary within @p tolerance. */ -[[nodiscard]] bool isUnitaryMatrix(const Matrix2x2& matrix, - double tolerance = MATRIX_TOLERANCE); - [[nodiscard]] Matrix2x2 rxMatrix(double theta); [[nodiscard]] Matrix2x2 ryMatrix(double theta); [[nodiscard]] Matrix2x2 rzMatrix(double theta); diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp index b49b301dfe..2e126c895d 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp @@ -42,14 +42,14 @@ static std::optional parseGateToken(llvm::StringRef name) { return llvm::StringSwitch>(name) .Case("u", NativeGateKind::U) .Case("x", NativeGateKind::X) - .Case("sx", NativeGateKind::Sx) - .Cases("rz", "p", NativeGateKind::Rz) - .Case("rx", NativeGateKind::Rx) - .Case("ry", NativeGateKind::Ry) + .Case("sx", NativeGateKind::SX) + .Cases("rz", "p", NativeGateKind::RZ) + .Case("rx", NativeGateKind::RX) + .Case("ry", NativeGateKind::RY) .Case("r", NativeGateKind::R) - .Case("cx", NativeGateKind::Cx) - .Case("cz", NativeGateKind::Cz) - .Case("rzz", NativeGateKind::Rzz) + .Case("cx", NativeGateKind::CX) + .Case("cz", NativeGateKind::CZ) + .Case("rzz", NativeGateKind::RZZ) .Default(std::nullopt); } @@ -78,25 +78,25 @@ hasSingleQubitStrategy(const llvm::DenseSet& gates) { if (has(NativeGateKind::U)) { return true; } - if (has(NativeGateKind::X) && has(NativeGateKind::Sx) && - has(NativeGateKind::Rz)) { + if (has(NativeGateKind::X) && has(NativeGateKind::SX) && + has(NativeGateKind::RZ)) { return true; } if (has(NativeGateKind::R)) { return true; } - return (has(NativeGateKind::Rx) && has(NativeGateKind::Rz)) || - (has(NativeGateKind::Rx) && has(NativeGateKind::Ry)) || - (has(NativeGateKind::Ry) && has(NativeGateKind::Rz)); + return (has(NativeGateKind::RX) && has(NativeGateKind::RZ)) || + (has(NativeGateKind::RX) && has(NativeGateKind::RY)) || + (has(NativeGateKind::RY) && has(NativeGateKind::RZ)); } static std::optional selectEntangler(const NativeProfileSpec& spec) { - if (spec.gates.contains(NativeGateKind::Cx)) { - return NativeGateKind::Cx; + if (spec.gates.contains(NativeGateKind::CX)) { + return NativeGateKind::CX; } - if (spec.gates.contains(NativeGateKind::Cz)) { - return NativeGateKind::Cz; + if (spec.gates.contains(NativeGateKind::CZ)) { + return NativeGateKind::CZ; } return std::nullopt; } @@ -104,13 +104,13 @@ selectEntangler(const NativeProfileSpec& spec) { static const TwoQubitBasisDecomposer& cachedBasisDecomposer(NativeGateKind entangler) { switch (entangler) { - case NativeGateKind::Cx: { + case NativeGateKind::CX: { static const TwoQubitBasisDecomposer DECOMPOSER = TwoQubitBasisDecomposer::create(mlir::qco::twoQubitControlledX01(), 1.0); return DECOMPOSER; } - case NativeGateKind::Cz: { + case NativeGateKind::CZ: { static const TwoQubitBasisDecomposer DECOMPOSER = TwoQubitBasisDecomposer::create(mlir::qco::twoQubitControlledZ(), 1.0); return DECOMPOSER; @@ -154,16 +154,16 @@ static std::optional gateKindFor(UnitaryOpInterface op) { return NativeGateKind::X; } if (llvm::isa(raw)) { - return NativeGateKind::Sx; + return NativeGateKind::SX; } if (llvm::isa(raw)) { - return NativeGateKind::Rz; + return NativeGateKind::RZ; } if (llvm::isa(raw)) { - return NativeGateKind::Rx; + return NativeGateKind::RX; } if (llvm::isa(raw)) { - return NativeGateKind::Ry; + return NativeGateKind::RY; } if (llvm::isa(raw)) { return NativeGateKind::R; @@ -177,10 +177,10 @@ static std::optional entanglerKindFor(CtrlOp ctrl) { } Operation* body = ctrl.getBodyUnitary(0).getOperation(); if (llvm::isa(body)) { - return NativeGateKind::Cx; + return NativeGateKind::CX; } if (llvm::isa(body)) { - return NativeGateKind::Cz; + return NativeGateKind::CZ; } return std::nullopt; } @@ -190,20 +190,20 @@ EulerBasis NativeProfileSpec::eulerBasis() const { if (has(NativeGateKind::U)) { return EulerBasis::U; } - if (has(NativeGateKind::X) && has(NativeGateKind::Sx) && - has(NativeGateKind::Rz)) { + if (has(NativeGateKind::X) && has(NativeGateKind::SX) && + has(NativeGateKind::RZ)) { return EulerBasis::ZSXX; } if (has(NativeGateKind::R)) { return EulerBasis::R; } - if (has(NativeGateKind::Rx) && has(NativeGateKind::Rz)) { + if (has(NativeGateKind::RX) && has(NativeGateKind::RZ)) { return EulerBasis::XZX; } - if (has(NativeGateKind::Rx) && has(NativeGateKind::Ry)) { + if (has(NativeGateKind::RX) && has(NativeGateKind::RY)) { return EulerBasis::XYX; } - if (has(NativeGateKind::Ry) && has(NativeGateKind::Rz)) { + if (has(NativeGateKind::RY) && has(NativeGateKind::RZ)) { return EulerBasis::ZYZ; } llvm_unreachable("parseNativeSpec guarantees a synthesizable basis"); @@ -245,7 +245,7 @@ LogicalResult synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, auto ctrlOp = CtrlOp::create( builder, loc, ValueRange{wire0}, ValueRange{wire1}, [&](ValueRange targetArgs) -> SmallVector { - if (*entangler == NativeGateKind::Cz) { + if (*entangler == NativeGateKind::CZ) { return {ZOp::create(builder, loc, targetArgs[0]).getOutputQubit(0)}; } return {XOp::create(builder, loc, targetArgs[0]).getOutputQubit(0)}; @@ -285,7 +285,7 @@ bool allowsOp(Operation* op, const NativeProfileSpec& spec) { return kind && spec.gates.contains(*kind); } if (llvm::isa(op)) { - return spec.gates.contains(NativeGateKind::Rzz); + return spec.gates.contains(NativeGateKind::RZZ); } auto unitary = llvm::dyn_cast(op); if (!unitary || !unitary.isSingleQubit()) { diff --git a/mlir/lib/Dialect/QCO/Utils/Matrix.cpp b/mlir/lib/Dialect/QCO/Utils/Matrix.cpp index ae31a37788..22f74061a5 100644 --- a/mlir/lib/Dialect/QCO/Utils/Matrix.cpp +++ b/mlir/lib/Dialect/QCO/Utils/Matrix.cpp @@ -1015,35 +1015,6 @@ DynamicMatrix operator*(const Complex& scalar, const DynamicMatrix& matrix) { return matrix * scalar; } -const Matrix4x4& twoQubitSwapMatrix() { - static const Matrix4x4 MATRIX = Matrix4x4::fromElements(1, 0, 0, 0, // - 0, 0, 1, 0, // - 0, 1, 0, 0, // - 0, 0, 0, 1); - return MATRIX; -} - -double remEuclid(double a, double b) { - if (b == 0.0) { - llvm::reportFatalInternalError("remEuclid expects non-zero divisor"); - } - const auto r = std::fmod(a, b); - return (r < 0.0) ? r + std::abs(b) : r; -} - -double traceToFidelity(const Complex& trace) { - const auto traceAbs = std::abs(trace); - return (4.0 + (traceAbs * traceAbs)) / 20.0; -} - -Complex globalPhaseFactor(double phase) { - return std::exp(Complex{0.0, 1.0} * phase); -} - -bool isUnitaryMatrix(const Matrix2x2& matrix, const double tolerance) { - return (matrix.adjoint() * matrix).isIdentity(tolerance); -} - Matrix2x2 rxMatrix(const double theta) { const auto halfTheta = theta / 2.0; const Complex cos{std::cos(halfTheta), 0.0}; diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index d28f769996..345758b228 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -52,6 +52,8 @@ using namespace mqt::test; using QubitId = std::size_t; static constexpr double K_SANITY_CHECK_PRECISION = 1e-12; +inline constexpr double FRAC1_SQRT2 = + 0.707106781186547524400844362104849039284835937688474036588; static const Matrix2x2& hGate() { static const Matrix2x2 MATRIX = Matrix2x2::fromElements( @@ -99,7 +101,7 @@ static Matrix4x4 restoreWeyl(const TwoQubitWeylDecomposition& decomposition) { return Matrix4x4::kron(decomposition.k1l(), decomposition.k1r()) * decomposition.getCanonicalMatrix() * Matrix4x4::kron(decomposition.k2l(), decomposition.k2r()) * - globalPhaseFactor(decomposition.globalPhase()); + std::exp(Complex{0.0, 1.0} * decomposition.globalPhase()); } static Matrix4x4 restoreBasis(const TwoQubitNativeDecomposition& decomposition, @@ -113,7 +115,7 @@ static Matrix4x4 restoreBasis(const TwoQubitNativeDecomposition& decomposition, matrix = entangler * matrix; matrix = layer(static_cast(i) + 1) * matrix; } - return matrix * globalPhaseFactor(decomposition.globalPhase); + return matrix * std::exp(Complex{0.0, 1.0} * decomposition.globalPhase); } static auto productMatrixCases() { @@ -286,11 +288,12 @@ static void expectSynthesized2QMatrix(MLIRContext* ctx, const Matrix4x4& target, EXPECT_TRUE(isEquivalentUpToGlobalPhase(*actual, target)); } +static bool isUnitaryMatrix(const Matrix2x2& matrix) { + return (matrix.adjoint() * matrix).isIdentity(WEYL_TOLERANCE); +} + TEST(DecompositionHelpersTest, MatrixUtilitySanity) { - EXPECT_DOUBLE_EQ(remEuclid(-1.0, 3.0), 2.0); - EXPECT_DOUBLE_EQ(traceToFidelity(std::complex{3.0, 4.0}), - (4.0 + 25.0) / 20.0); - EXPECT_NEAR(std::abs(globalPhaseFactor(1.25)), 1.0, 1e-14); + EXPECT_NEAR(std::abs(std::exp(Complex{0.0, 1.0} * 1.25)), 1.0, 1e-14); EXPECT_FALSE(isUnitaryMatrix(Matrix2x2::fromElements(2.0, 0.0, 0.0, 2.0))); EXPECT_TRUE(isUnitaryMatrix(Matrix2x2::identity())); } @@ -590,8 +593,8 @@ TEST(WeylSynthesisTest, FailsWithoutEntanglerInSpec) { fx.setUp(); const auto spec = parseNativeSpec("u"); ASSERT_TRUE(spec); - EXPECT_FALSE(spec->gates.contains(NativeGateKind::Cx)); - EXPECT_FALSE(spec->gates.contains(NativeGateKind::Cz)); + EXPECT_FALSE(spec->gates.contains(NativeGateKind::CX)); + EXPECT_FALSE(spec->gates.contains(NativeGateKind::CZ)); OpBuilder builder(fx.ctx()); const auto qubitTy = QubitType::get(fx.ctx()); const auto funcTy = @@ -610,9 +613,9 @@ TEST(WeylSynthesisTest, FailsWithoutEntanglerInSpec) { TEST(NativeSpecTest, ParsesAndRejectsMenus) { const auto ibm = parseNativeSpec("x,sx,rz,cx"); ASSERT_TRUE(ibm); - EXPECT_TRUE(ibm->gates.contains(NativeGateKind::Cx)); + EXPECT_TRUE(ibm->gates.contains(NativeGateKind::CX)); EXPECT_TRUE(ibm->gates.contains(NativeGateKind::X)); - EXPECT_FALSE(ibm->gates.contains(NativeGateKind::Rzz)); + EXPECT_FALSE(ibm->gates.contains(NativeGateKind::RZZ)); EXPECT_FALSE(parseNativeSpec("x,sx,rz,not-a-gate").has_value()); const auto pMenu = parseNativeSpec("x,sx,p,cx"); @@ -623,13 +626,13 @@ TEST(NativeSpecTest, ParsesAndRejectsMenus) { const auto cxOnly = parseNativeSpec("u,cx"); ASSERT_TRUE(cxOnly); - EXPECT_TRUE(cxOnly->gates.contains(NativeGateKind::Cx)); - EXPECT_FALSE(cxOnly->gates.contains(NativeGateKind::Cz)); + EXPECT_TRUE(cxOnly->gates.contains(NativeGateKind::CX)); + EXPECT_FALSE(cxOnly->gates.contains(NativeGateKind::CZ)); const auto both = parseNativeSpec("u,cx,cz"); ASSERT_TRUE(both); - EXPECT_TRUE(both->gates.contains(NativeGateKind::Cx)); - EXPECT_TRUE(both->gates.contains(NativeGateKind::Cz)); + EXPECT_TRUE(both->gates.contains(NativeGateKind::CX)); + EXPECT_TRUE(both->gates.contains(NativeGateKind::CZ)); const auto generic = parseNativeSpec("u,cx"); ASSERT_TRUE(generic); diff --git a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp index 35b0ee080a..cecbda7ca0 100644 --- a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp +++ b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp @@ -596,8 +596,7 @@ TEST(SymmetricEigensolver, HandlesDegenerateSpectrum) { EXPECT_TRUE((v.transpose() * v).isIdentity()); } -TEST(GateMatrixFactories, RemEuclidAndControlledGates) { - EXPECT_DOUBLE_EQ(remEuclid(-1.0, 3.0), 2.0); +TEST(GateMatrixFactories, ControlledGates) { EXPECT_TRUE(twoQubitControlledX01().isApprox( Matrix4x4::fromElements(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0))); EXPECT_TRUE(twoQubitControlledX10().isApprox( From e36422425ee59c821c7ece8db64930e063c4b980 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 24 Jun 2026 10:19:04 +0200 Subject: [PATCH 081/122] =?UTF-8?q?=E2=98=82=EF=B8=8F=20Increase=20coverag?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Decomposition/test_weyl_decomposition.cpp | 122 ++++++++++++++++-- .../test_fuse_two_qubit_unitary_runs.cpp | 54 ++++++++ .../Dialect/QCO/Utils/test_unitary_matrix.cpp | 2 + 3 files changed, 170 insertions(+), 8 deletions(-) diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index 345758b228..3f1eba26b9 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -149,6 +149,30 @@ static auto cxBasisCases() { []() { return twoQubitControlledX10(); }); } +static auto specializedMatrixCases() { + return ::testing::Values( + []() { + return twoQubitControlledX01() * twoQubitControlledX10() * + twoQubitControlledX01(); + }, + []() { + return TwoQubitWeylDecomposition::getCanonicalMatrix(0.5, 0.5, 0.5); + }, + []() { + return TwoQubitWeylDecomposition::getCanonicalMatrix(0.5, 0.5, -0.5); + }, + []() { return twoQubitControlledX01() * twoQubitControlledX10(); }, + []() { + return TwoQubitWeylDecomposition::getCanonicalMatrix(0.5, 0.5, 0.1); + }, + []() { + return TwoQubitWeylDecomposition::getCanonicalMatrix(0.5, 0.1, 0.1); + }, + []() { + return TwoQubitWeylDecomposition::getCanonicalMatrix(0.5, 0.1, -0.1); + }); +} + static bool extractSingleQubitMatrix(UnitaryOpInterface op, Matrix2x2& out) { if (op.getUnitaryMatrix2x2(out)) { return true; @@ -366,8 +390,7 @@ TEST_P(WeylDecompositionTest, ReconstructsWithinRequestedFidelity) { TEST(WeylDecompositionStandalone, CnotProducesValidWeylParametersAndUnitaryLocals) { - const Matrix4x4 cnot = - Matrix4x4::fromElements(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0); + const Matrix4x4 cnot = twoQubitControlledX01(); const auto decomp = TwoQubitWeylDecomposition::create(cnot, std::nullopt); constexpr double piOver4 = 0.7853981633974483; for (const double angle : {decomp.a(), decomp.b(), decomp.c()}) { @@ -395,6 +418,8 @@ INSTANTIATE_TEST_SUITE_P(ProductTwoQubitMatrices, WeylDecompositionTest, productMatrixCases()); INSTANTIATE_TEST_SUITE_P(TwoQubitMatrices, WeylDecompositionTest, entangledMatrixCases()); +INSTANTIATE_TEST_SUITE_P(SpecializedMatrices, WeylDecompositionTest, + specializedMatrixCases()); TEST_P(BasisDecomposerTest, ReconstructsWithinRequestedFidelity) { for (const double fidelity : {1.0, 1.0 - 1e-12}) { @@ -575,17 +600,25 @@ INSTANTIATE_TEST_SUITE_P( Matrix2x2::identity()) * twoQubitControlledX01() * Matrix4x4::kron(rzMatrix(0.2), ryMatrix(0.1)); - }}), + }}, + WeylSynthesisCase{"CzGeneric", "u,cz", + [] { return twoQubitControlledZ(); }}), [](const testing::TestParamInfo& info) { return info.param.name; }); TEST(WeylSynthesisTest, IdentityRequiresNoEntanglers) { - const auto spec = parseNativeSpec("u,cx"); - ASSERT_TRUE(spec); - const auto count = twoQubitEntanglerCount(Matrix4x4::identity(), *spec); - ASSERT_TRUE(count.has_value()); - EXPECT_EQ(*count, 0U); + const auto cxSpec = parseNativeSpec("u,cx"); + ASSERT_TRUE(cxSpec); + const auto cxCount = twoQubitEntanglerCount(Matrix4x4::identity(), *cxSpec); + ASSERT_TRUE(cxCount.has_value()); + EXPECT_EQ(*cxCount, 0U); + + const auto czSpec = parseNativeSpec("u,cz"); + ASSERT_TRUE(czSpec); + const auto czCount = twoQubitEntanglerCount(Matrix4x4::identity(), *czSpec); + ASSERT_TRUE(czCount.has_value()); + EXPECT_EQ(*czCount, 0U); } TEST(WeylSynthesisTest, FailsWithoutEntanglerInSpec) { @@ -639,3 +672,76 @@ TEST(NativeSpecTest, ParsesAndRejectsMenus) { EXPECT_TRUE(generic->gates.contains(NativeGateKind::U)); EXPECT_FALSE(generic->gates.contains(NativeGateKind::X)); } + +TEST(NativeSpecTest, ResolvesEulerBasisFromMenu) { + const auto uMenu = parseNativeSpec("u,cx"); + ASSERT_TRUE(uMenu); + EXPECT_EQ(uMenu->eulerBasis(), EulerBasis::U); + + const auto zsxx = parseNativeSpec("x,sx,rz,cx"); + ASSERT_TRUE(zsxx); + EXPECT_EQ(zsxx->eulerBasis(), EulerBasis::ZSXX); + + const auto rMenu = parseNativeSpec("r,cz"); + ASSERT_TRUE(rMenu); + EXPECT_EQ(rMenu->eulerBasis(), EulerBasis::R); + + const auto xzx = parseNativeSpec("rx,rz,cz"); + ASSERT_TRUE(xzx); + EXPECT_EQ(xzx->eulerBasis(), EulerBasis::XZX); + + const auto xyx = parseNativeSpec("rx,ry,cz"); + ASSERT_TRUE(xyx); + EXPECT_EQ(xyx->eulerBasis(), EulerBasis::XYX); + + const auto zyz = parseNativeSpec("ry,rz,cz"); + ASSERT_TRUE(zyz); + EXPECT_EQ(zyz->eulerBasis(), EulerBasis::ZYZ); +} + +TEST(NativeSpecTest, AllowsOpMatchesMenu) { + MlirTestContext fx; + fx.setUp(); + const auto spec = parseNativeSpec("u,cx,rzz"); + ASSERT_TRUE(spec); + + OpBuilder builder(fx.ctx()); + const Location loc = UnknownLoc::get(fx.ctx()); + const auto qubitTy = QubitType::get(fx.ctx()); + const auto funcTy = + builder.getFunctionType({qubitTy, qubitTy}, {qubitTy, qubitTy}); + auto func = func::FuncOp::create(builder, loc, "allows_op", funcTy); + auto* entry = func.addEntryBlock(); + builder.setInsertionPointToStart(entry); + Value q0 = entry->getArgument(0); + Value q1 = entry->getArgument(1); + + EXPECT_TRUE(allowsOp( + BarrierOp::create(builder, loc, ValueRange{q0, q1}).getOperation(), + *spec)); + EXPECT_TRUE( + allowsOp(GPhaseOp::create(builder, loc, 0.1).getOperation(), *spec)); + EXPECT_TRUE(allowsOp( + UOp::create(builder, loc, q0, 0.1, 0.2, 0.3).getOperation(), *spec)); + EXPECT_TRUE( + allowsOp(RZZOp::create(builder, loc, q0, q1, 0.4).getOperation(), *spec)); + + auto cx = CtrlOp::create( + builder, loc, ValueRange{q0}, ValueRange{q1}, + [&](ValueRange targets) -> SmallVector { + return {XOp::create(builder, loc, targets[0]).getOutputQubit(0)}; + }); + EXPECT_TRUE(allowsOp(cx.getOperation(), *spec)); + + EXPECT_FALSE(allowsOp(XOp::create(builder, loc, q0).getOperation(), *spec)); + + const auto czSpec = parseNativeSpec("u,cz"); + ASSERT_TRUE(czSpec); + auto cz = CtrlOp::create( + builder, loc, ValueRange{q0}, ValueRange{q1}, + [&](ValueRange targets) -> SmallVector { + return {ZOp::create(builder, loc, targets[0]).getOutputQubit(0)}; + }); + EXPECT_TRUE(allowsOp(cz.getOperation(), *czSpec)); + EXPECT_FALSE(allowsOp(cx.getOperation(), *czSpec)); +} diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp index 34e16164a2..2b57860065 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp @@ -466,6 +466,31 @@ static void fusionHRzzSRzz(mlir::qc::QCProgramBuilder& b) { b.rzz(0.17, q0, q1); } +static void fusionOffMenuGateInWindow(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.cx(q0, q1); + b.h(q0); + b.cx(q0, q1); +} + +static void fusionDualWireOneQBetweenCx(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.cx(q0, q1); + b.rz(0.11, q0); + b.ry(0.22, q1); + b.cx(q0, q1); +} + +static void inverseTwoX(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + b.inv({q0}, [&](mlir::ValueRange qubits) { + b.x(qubits[0]); + b.x(qubits[0]); + }); +} + static void broadOneQThenCz(mlir::qc::QCProgramBuilder& b) { const auto q0 = b.allocQubit(); const auto q1 = b.allocQubit(); @@ -634,6 +659,30 @@ INSTANTIATE_TEST_SUITE_P( return info.param.name; }); +TEST_F(FuseTwoQubitUnitaryRunsPassTest, EmptyNativeGatesSkipsPass) { + auto module = mlir::qc::QCProgramBuilder::build(context.get(), fusionCxCx); + ASSERT_TRUE(module); + runQcToQco(module); + std::string before; + llvm::raw_string_ostream osBefore(before); + module->print(osBefore); + + PassManager pm(module->getContext()); + pm.addPass(createFuseTwoQubitUnitaryRuns(FuseTwoQubitUnitaryRunsOptions{ + .nativeGates = "", + })); + ASSERT_TRUE(succeeded(pm.run(*module))); + + std::string after; + llvm::raw_string_ostream osAfter(after); + module->print(osAfter); + EXPECT_EQ(before, after); +} + +TEST_F(FuseTwoQubitUnitaryRunsPassTest, InverseWrappedOpsSynthesize) { + expectNativeAfterSynthesis(inverseTwoX, "x,sx,rz,cx", onlyIbmBasicCxOps); +} + TEST_F(FuseTwoQubitUnitaryRunsPassTest, FailsForUnsupportedNativeGateMenu) { expectSynthesisFailure(mlir::qc::h, "not-a-gate"); } @@ -727,6 +776,11 @@ INSTANTIATE_TEST_SUITE_P( FusionCase{"SwappedWireOrder", fusionSwapCxPattern, "u,cx", std::nullopt, std::nullopt, true}, FusionCase{"RzzBlock", fusionHRzzSRzz, "x,sx,rz,rx,rzz,cz", + std::nullopt, std::nullopt, true}, + FusionCase{"OffMenuGateInWindow", fusionOffMenuGateInWindow, + "u,cx", std::nullopt, std::nullopt, true}, + FusionCase{"DualWireOneQBetweenCx", + fusionDualWireOneQBetweenCx, "u,cx", std::nullopt, std::nullopt, true}), [](const testing::TestParamInfo& info) { return info.param.name; diff --git a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp index cecbda7ca0..671956c44e 100644 --- a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp +++ b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp @@ -601,6 +601,8 @@ TEST(GateMatrixFactories, ControlledGates) { Matrix4x4::fromElements(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0))); EXPECT_TRUE(twoQubitControlledX10().isApprox( twoQubitControlledX01().reorderForQubits(1, 0))); + EXPECT_TRUE(twoQubitControlledZ().isApprox(Matrix4x4::fromElements( + 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, -1))); EXPECT_TRUE(rxMatrix(0.0).isIdentity()); EXPECT_TRUE((iPauliX() * iPauliX()).isApprox(-1.0 * Matrix2x2::identity())); } From 5668e4223bfbc343cb9938b022c00b1cee803c4c Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 24 Jun 2026 10:20:34 +0200 Subject: [PATCH 082/122] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20linter=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/Decomposition/test_euler_decomposition.cpp | 1 + .../QCO/Transforms/Decomposition/test_weyl_decomposition.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp index a397251a14..32e5b1dde9 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -37,6 +37,7 @@ #include #include +#include #include #include #include diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index 3f1eba26b9..a51e743a0a 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -18,7 +18,6 @@ #include #include -#include #include #include #include @@ -30,6 +29,7 @@ #include #include #include +#include #include #include From 27201b5ca390c572d9c8a7935d923ea26447df2e Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 24 Jun 2026 12:47:48 +0200 Subject: [PATCH 083/122] =?UTF-8?q?=E2=9C=A8=20Add=20composeSingleQubitBod?= =?UTF-8?q?yMatrix=20function=20and=20integrate=20into=20CtrlOp=20and=20In?= =?UTF-8?q?vOp=20for=20unitary=20matrix=20composition.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/include/mlir/Dialect/QCO/QCOUtils.h | 14 ++++ mlir/include/mlir/Dialect/QCO/Utils/Matrix.h | 1 - mlir/lib/Dialect/QCO/IR/CMakeLists.txt | 1 + mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp | 13 ++-- mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp | 51 ++------------- mlir/lib/Dialect/QCO/IR/QCOUtils.cpp | 64 +++++++++++++++++++ .../Decomposition/NativeProfile.cpp | 8 ++- .../FuseTwoQubitUnitaryRuns.cpp | 12 +--- mlir/lib/Dialect/QCO/Utils/Matrix.cpp | 8 --- .../Dialect/QCO/IR/test_qco_ir_matrix.cpp | 34 ++++++++++ .../Decomposition/test_weyl_decomposition.cpp | 17 +++++ .../test_fuse_two_qubit_unitary_runs.cpp | 16 ++--- .../Dialect/QCO/Utils/test_unitary_matrix.cpp | 2 - mlir/unittests/programs/qc_programs.cpp | 9 +++ mlir/unittests/programs/qc_programs.h | 4 ++ mlir/unittests/programs/qco_programs.cpp | 9 +++ mlir/unittests/programs/qco_programs.h | 4 ++ 17 files changed, 185 insertions(+), 82 deletions(-) create mode 100644 mlir/lib/Dialect/QCO/IR/QCOUtils.cpp diff --git a/mlir/include/mlir/Dialect/QCO/QCOUtils.h b/mlir/include/mlir/Dialect/QCO/QCOUtils.h index 4b944d81d6..2ecaa0a9c8 100644 --- a/mlir/include/mlir/Dialect/QCO/QCOUtils.h +++ b/mlir/include/mlir/Dialect/QCO/QCOUtils.h @@ -11,15 +11,29 @@ #pragma once #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" #include #include +#include #include #include #include +#include +#include + namespace mlir::qco { +/** + * @brief Composes compile-time single-qubit unitaries in a modifier body. + * + * @return The composed 2x2 target unitary in program order, or `std::nullopt` + * when the body cannot be composed. + */ +[[nodiscard]] std::optional +composeSingleQubitBodyMatrix(Block& block); + /** * @brief Check whether two parameter values match. * diff --git a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h index 5593cd3939..3fdddce9ed 100644 --- a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h +++ b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h @@ -820,7 +820,6 @@ struct SymmetricEigen4 { * Same entangling content as @ref twoQubitControlledX01 but with wires * reversed; useful when parametrizing basis-decomposer tests. */ -[[nodiscard]] const Matrix4x4& twoQubitControlledX10(); [[nodiscard]] const Matrix4x4& twoQubitControlledZ(); } // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/IR/CMakeLists.txt b/mlir/lib/Dialect/QCO/IR/CMakeLists.txt index cb7c30a4ee..3aa4a31031 100644 --- a/mlir/lib/Dialect/QCO/IR/CMakeLists.txt +++ b/mlir/lib/Dialect/QCO/IR/CMakeLists.txt @@ -14,6 +14,7 @@ file(GLOB_RECURSE SCF "${CMAKE_CURRENT_SOURCE_DIR}/SCF/*.cpp") add_mlir_dialect_library( MLIRQCODialect QCOOps.cpp + QCOUtils.cpp ${MODIFIERS} ${OPERATIONS} ${QUBIT_MANAGEMENT} diff --git a/mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp b/mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp index 0bec883fae..746c710f9e 100644 --- a/mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp @@ -11,6 +11,7 @@ #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/QCOUtils.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" #include "mlir/Dialect/Utils/Utils.h" @@ -301,11 +302,15 @@ bool CtrlOp::hasCompileTimeKnownUnitaryMatrix() { } std::optional CtrlOp::getUnitaryMatrix() { - auto bodyUnitary = utils::getSoleBodyUnitary(*getBody()); - if (!bodyUnitary) { - return std::nullopt; + std::optional targetMatrix; + if (auto bodyUnitary = + utils::getSoleBodyUnitary(*getBody())) { + targetMatrix = bodyUnitary.getUnitaryMatrix(); + } else if (getNumTargets() == 1) { + if (const auto composed = composeSingleQubitBodyMatrix(*getBody())) { + targetMatrix = DynamicMatrix(*composed); + } } - const auto targetMatrix = bodyUnitary.getUnitaryMatrix(); if (!targetMatrix) { return std::nullopt; } diff --git a/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp b/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp index c6d6a36679..62f767642a 100644 --- a/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp @@ -11,6 +11,7 @@ #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/QCOUtils.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" #include "mlir/Dialect/Utils/Utils.h" @@ -412,51 +413,6 @@ bool InvOp::hasCompileTimeKnownUnitaryMatrix() { }); } -/** - * @brief Composes compile-time single-qubit unitaries and returns the inverse. - */ -[[nodiscard]] static std::optional -composeInvertedSingleQubitBodyMatrix(Block& block) { - Matrix2x2 acc = Matrix2x2::identity(); - Complex global{1.0, 0.0}; - bool found = false; - for (Operation& op : block.without_terminator()) { - if (!TypeSwitch(&op) - .Case([](auto) { return true; }) - .Case([&](GPhaseOp gphase) { - const auto matrix = gphase.getUnitaryMatrix(); - if (!matrix) { - return false; - } - global *= (*matrix)(0, 0); - return true; - }) - .Case([&](UnitaryOpInterface unitary) { - Matrix2x2 matrix; - if (!unitary.getUnitaryMatrix2x2(matrix)) { - return false; - } - acc.premultiplyBy(matrix); - found = true; - return true; - }) - .Default([](Operation* operation) { - const auto usesQubit = [](Value value) { - return isa(value.getType()); - }; - return !llvm::any_of(operation->getOperands(), usesQubit) && - !llvm::any_of(operation->getResults(), usesQubit); - })) { - return std::nullopt; - } - } - if (!found && std::abs(global - Complex{1.0, 0.0}) <= utils::TOLERANCE) { - return std::nullopt; - } - acc *= global; - return DynamicMatrix::fromAdjoint(acc); -} - std::optional InvOp::getUnitaryMatrix() { if (getNumBodyUnitaries() == 0) { return DynamicMatrix::identity(1LL << getNumTargets()); @@ -474,5 +430,8 @@ std::optional InvOp::getUnitaryMatrix() { if (getNumTargets() != 1) { return std::nullopt; } - return composeInvertedSingleQubitBodyMatrix(*getBody()); + if (const auto composed = composeSingleQubitBodyMatrix(*getBody())) { + return DynamicMatrix::fromAdjoint(*composed); + } + return std::nullopt; } diff --git a/mlir/lib/Dialect/QCO/IR/QCOUtils.cpp b/mlir/lib/Dialect/QCO/IR/QCOUtils.cpp new file mode 100644 index 0000000000..4c82fd9757 --- /dev/null +++ b/mlir/lib/Dialect/QCO/IR/QCOUtils.cpp @@ -0,0 +1,64 @@ +/* + * 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/QCOUtils.h" + +#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" + +#include +#include + +#include +#include + +namespace mlir::qco { + +std::optional composeSingleQubitBodyMatrix(Block& block) { + Matrix2x2 acc = Matrix2x2::identity(); + Complex global{1.0, 0.0}; + bool found = false; + for (Operation& op : block.without_terminator()) { + if (!TypeSwitch(&op) + .Case([](auto) { return true; }) + .Case([&](GPhaseOp gphase) { + const auto matrix = gphase.getUnitaryMatrix(); + if (!matrix) { + return false; + } + global *= (*matrix)(0, 0); + return true; + }) + .Case([&](UnitaryOpInterface unitary) { + Matrix2x2 matrix; + if (!unitary.getUnitaryMatrix2x2(matrix)) { + return false; + } + acc.premultiplyBy(matrix); + found = true; + return true; + }) + .Default([](Operation* operation) { + const auto usesQubit = [](Value value) { + return isa(value.getType()); + }; + return !llvm::any_of(operation->getOperands(), usesQubit) && + !llvm::any_of(operation->getResults(), usesQubit); + })) { + return std::nullopt; + } + } + if (!found && std::abs(global - Complex{1.0, 0.0}) <= utils::TOLERANCE) { + return std::nullopt; + } + acc *= global; + return acc; +} + +} // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp index 2e126c895d..47f9349ee3 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp @@ -15,6 +15,7 @@ #include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" +#include "mlir/Dialect/Utils/Utils.h" #include #include @@ -175,7 +176,12 @@ static std::optional entanglerKindFor(CtrlOp ctrl) { if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { return std::nullopt; } - Operation* body = ctrl.getBodyUnitary(0).getOperation(); + auto bodyUnitary = + utils::getSoleBodyUnitary(*ctrl.getBody()); + if (!bodyUnitary) { + return std::nullopt; + } + Operation* body = bodyUnitary.getOperation(); if (llvm::isa(body)) { return NativeGateKind::CX; } diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp index 2513cbd485..28dc940aad 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp @@ -80,16 +80,8 @@ static bool assignTwoQubitOpMatrix(Operation* op, Matrix4x4& matrix) { if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { return false; } - Operation* body = ctrl.getBodyUnitary(0).getOperation(); - if (llvm::isa(body)) { - matrix = twoQubitControlledX01(); - return true; - } - if (llvm::isa(body)) { - matrix = twoQubitControlledZ(); - return true; - } - return false; + return llvm::cast(ctrl.getOperation()) + .getUnitaryMatrix4x4(matrix); } auto unitary = llvm::dyn_cast(op); if (!unitary || !unitary.isTwoQubit()) { diff --git a/mlir/lib/Dialect/QCO/Utils/Matrix.cpp b/mlir/lib/Dialect/QCO/Utils/Matrix.cpp index 22f74061a5..a42eaddf18 100644 --- a/mlir/lib/Dialect/QCO/Utils/Matrix.cpp +++ b/mlir/lib/Dialect/QCO/Utils/Matrix.cpp @@ -1090,14 +1090,6 @@ const Matrix4x4& twoQubitControlledX01() { return MATRIX; } -const Matrix4x4& twoQubitControlledX10() { - static const Matrix4x4 MATRIX = Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, // - 0.0, 0.0, 0.0, 1.0, // - 0.0, 0.0, 1.0, 0.0, // - 0.0, 1.0, 0.0, 0.0); - return MATRIX; -} - const Matrix4x4& twoQubitControlledZ() { static const Matrix4x4 MATRIX = Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, // 0.0, 1.0, 0.0, 0.0, // diff --git a/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp b/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp index e196b6c1b8..68a97956e9 100644 --- a/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp +++ b/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp @@ -93,6 +93,40 @@ TEST_F(QCOMatrixTest, CXOpMatrix) { ASSERT_TRUE(matrix->isApprox(expected)); } + +TEST_F(QCOMatrixTest, ControlledHOpMatrix) { + auto moduleOp = QCOProgramBuilder::build(context.get(), singleControlledH); + ASSERT_TRUE(moduleOp); + + auto funcOp = *moduleOp->getBody()->getOps().begin(); + auto ctrlOp = *funcOp.getBody().getOps().begin(); + auto matrix = ctrlOp.getUnitaryMatrix(); + + const auto ch = qc::StandardOperation(1, 0, qc::OpType::H); + const auto dd = std::make_unique(2); + const auto chDD = dd::getDD(ch, *dd); + const auto definition = chDD.getMatrix(2); + + const Matrix4x4 expected = matrix4FromDefinition(definition); + + ASSERT_TRUE(matrix->isApprox(expected)); +} + +TEST_F(QCOMatrixTest, ControlledXHOpMatrix) { + auto moduleOp = QCOProgramBuilder::build(context.get(), controlledXH); + ASSERT_TRUE(moduleOp); + + auto funcOp = *moduleOp->getBody()->getOps().begin(); + auto ctrlOp = *funcOp.getBody().getOps().begin(); + auto matrix = ctrlOp.getUnitaryMatrix(); + ASSERT_TRUE(matrix); + + DynamicMatrix expected = DynamicMatrix::identity(4); + expected.setBottomRightCorner(HOp::getUnitaryMatrix() * + XOp::getUnitaryMatrix()); + + ASSERT_TRUE(matrix->isApprox(expected)); +} /// @} /// \name QCO/Modifiers/InvOp.cpp diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index a51e743a0a..7dfecf1aa2 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -12,6 +12,7 @@ #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/NativeProfile.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" @@ -144,6 +145,14 @@ static auto entangledMatrixCases() { }); } +static const Matrix4x4& twoQubitControlledX10() { + static const Matrix4x4 MATRIX = Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, // + 0.0, 0.0, 0.0, 1.0, // + 0.0, 0.0, 1.0, 0.0, // + 0.0, 1.0, 0.0, 0.0); + return MATRIX; +} + static auto cxBasisCases() { return ::testing::Values([]() { return twoQubitControlledX01(); }, []() { return twoQubitControlledX10(); }); @@ -733,6 +742,14 @@ TEST(NativeSpecTest, AllowsOpMatchesMenu) { }); EXPECT_TRUE(allowsOp(cx.getOperation(), *spec)); + auto cxWithInterleavedH = CtrlOp::create( + builder, loc, ValueRange{q0}, ValueRange{q1}, + [&](ValueRange targets) -> SmallVector { + auto wire = XOp::create(builder, loc, targets[0]).getOutputQubit(0); + return {HOp::create(builder, loc, wire).getOutputQubit(0)}; + }); + EXPECT_FALSE(allowsOp(cxWithInterleavedH.getOperation(), *spec)); + EXPECT_FALSE(allowsOp(XOp::create(builder, loc, q0).getOperation(), *spec)); const auto czSpec = parseNativeSpec("u,cz"); diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp index 2b57860065..ce984eb843 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp @@ -159,16 +159,7 @@ static bool extractTwoQubitMatrix(UnitaryOpInterface op, Matrix4x4& out) { if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { return false; } - Operation* body = ctrl.getBodyUnitary(0).getOperation(); - if (llvm::isa(body)) { - out = twoQubitControlledX01(); - return true; - } - if (llvm::isa(body)) { - out = twoQubitControlledZ(); - return true; - } - return false; + return op.getUnitaryMatrix4x4(out); } return op.getUnitaryMatrix4x4(out); } @@ -683,6 +674,11 @@ TEST_F(FuseTwoQubitUnitaryRunsPassTest, InverseWrappedOpsSynthesize) { expectNativeAfterSynthesis(inverseTwoX, "x,sx,rz,cx", onlyIbmBasicCxOps); } +TEST_F(FuseTwoQubitUnitaryRunsPassTest, ControlledXHSynthesizesToNativeMenu) { + expectEquivalentAndNativeAfterSynthesis(mlir::qc::controlledXH, "x,sx,rz,cx", + onlyIbmBasicCxOps); +} + TEST_F(FuseTwoQubitUnitaryRunsPassTest, FailsForUnsupportedNativeGateMenu) { expectSynthesisFailure(mlir::qc::h, "not-a-gate"); } diff --git a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp index 671956c44e..cc738f610c 100644 --- a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp +++ b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp @@ -599,8 +599,6 @@ TEST(SymmetricEigensolver, HandlesDegenerateSpectrum) { TEST(GateMatrixFactories, ControlledGates) { EXPECT_TRUE(twoQubitControlledX01().isApprox( Matrix4x4::fromElements(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0))); - EXPECT_TRUE(twoQubitControlledX10().isApprox( - twoQubitControlledX01().reorderForQubits(1, 0))); EXPECT_TRUE(twoQubitControlledZ().isApprox(Matrix4x4::fromElements( 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, -1))); EXPECT_TRUE(rxMatrix(0.0).isIdentity()); diff --git a/mlir/unittests/programs/qc_programs.cpp b/mlir/unittests/programs/qc_programs.cpp index 8410afd7a8..8ae72de487 100644 --- a/mlir/unittests/programs/qc_programs.cpp +++ b/mlir/unittests/programs/qc_programs.cpp @@ -389,6 +389,15 @@ void singleControlledH(QCProgramBuilder& b) { b.ch(q[0], q[1]); } +void controlledXH(QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.ctrl(q0, {q1}, [&](ValueRange targets) { + b.x(targets[0]); + b.h(targets[0]); + }); +} + void multipleControlledH(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.mch({q[0], q[1]}, q[2]); diff --git a/mlir/unittests/programs/qc_programs.h b/mlir/unittests/programs/qc_programs.h index 25d76718cc..9f9b5d198c 100644 --- a/mlir/unittests/programs/qc_programs.h +++ b/mlir/unittests/programs/qc_programs.h @@ -237,6 +237,10 @@ void h(QCProgramBuilder& b); /// Creates a circuit with a single controlled H gate. void singleControlledH(QCProgramBuilder& b); +/// Creates a circuit with a control modifier applied to an X-H sequence in the +/// ctrl body. +void controlledXH(QCProgramBuilder& b); + /// Creates a circuit with a multi-controlled H gate. void multipleControlledH(QCProgramBuilder& b); diff --git a/mlir/unittests/programs/qco_programs.cpp b/mlir/unittests/programs/qco_programs.cpp index b384aa20a1..65f6ececfa 100644 --- a/mlir/unittests/programs/qco_programs.cpp +++ b/mlir/unittests/programs/qco_programs.cpp @@ -320,6 +320,15 @@ void controlledTwoX(QCOProgramBuilder& b) { }); } +void controlledXH(QCOProgramBuilder& b) { + auto q = b.allocQubitRegister(2); + b.ctrl(q[0], q[1], [&](ValueRange targets) { + auto wire = b.x(targets[0]); + wire = b.h(wire); + return SmallVector{wire}; + }); +} + void inverseTwoX(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.inv(q[0], [&](ValueRange qubits) { diff --git a/mlir/unittests/programs/qco_programs.h b/mlir/unittests/programs/qco_programs.h index b568b4b02f..a771a80cbe 100644 --- a/mlir/unittests/programs/qco_programs.h +++ b/mlir/unittests/programs/qco_programs.h @@ -173,6 +173,10 @@ void twoX(QCOProgramBuilder& b); /// Creates a circuit with a control modifier applied to two subsequent X gates. void controlledTwoX(QCOProgramBuilder& b); +/// Creates a circuit with a control modifier applied to an X-H sequence in the +/// ctrl body. +void controlledXH(QCOProgramBuilder& b); + /// Creates a circuit with an inverse modifier applied to two subsequent X /// gates. void inverseTwoX(QCOProgramBuilder& b); From 33fc33a9cd9d833d377ce3519a20b162d5e3f107 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 25 Jun 2026 13:21:50 +0200 Subject: [PATCH 084/122] =?UTF-8?q?=E2=9C=A8=20Add=20whitespace=20for=20im?= =?UTF-8?q?proved=20readability=20in=20UOp's=20getUnitaryMatrix=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/lib/Dialect/QCO/IR/Operations/StandardGates/UOp.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/UOp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/UOp.cpp index 2e5d3aec10..b74ffb542c 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/UOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/UOp.cpp @@ -137,6 +137,7 @@ std::optional UOp::getUnitaryMatrix() { const auto halfTheta = *theta / 2; const auto c = std::cos(halfTheta); const auto s = std::sin(halfTheta); + const auto m01 = s * std::exp(1i * (*lambda + std::numbers::pi)); const auto m10 = s * std::exp(1i * (*phi)); const auto m11 = c * std::exp(1i * (*phi + *lambda)); From e1c49150742249447dc223f3bf181ee6dd64c77b Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 25 Jun 2026 13:50:03 +0200 Subject: [PATCH 085/122] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20linter=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp | 1 - mlir/lib/Dialect/QCO/IR/QCOUtils.cpp | 9 ++++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp b/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp index 62f767642a..ab634f6d7c 100644 --- a/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp @@ -31,7 +31,6 @@ #include #include -#include #include #include #include diff --git a/mlir/lib/Dialect/QCO/IR/QCOUtils.cpp b/mlir/lib/Dialect/QCO/IR/QCOUtils.cpp index 4c82fd9757..6e3ed48689 100644 --- a/mlir/lib/Dialect/QCO/IR/QCOUtils.cpp +++ b/mlir/lib/Dialect/QCO/IR/QCOUtils.cpp @@ -11,11 +11,18 @@ #include "mlir/Dialect/QCO/QCOUtils.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" +#include "mlir/Dialect/Utils/Utils.h" #include #include +#include +#include +#include +#include -#include +#include #include namespace mlir::qco { From 51cff236f7d952aa11c56ecb9de53659792a470a Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 30 Jun 2026 13:46:14 +0200 Subject: [PATCH 086/122] =?UTF-8?q?=F0=9F=94=A5=20Remove=20unused=20cmath?= =?UTF-8?q?=20include=20from=20QCOUtils.h?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/include/mlir/Dialect/QCO/QCOUtils.h | 1 - 1 file changed, 1 deletion(-) diff --git a/mlir/include/mlir/Dialect/QCO/QCOUtils.h b/mlir/include/mlir/Dialect/QCO/QCOUtils.h index 2ecaa0a9c8..f08db99293 100644 --- a/mlir/include/mlir/Dialect/QCO/QCOUtils.h +++ b/mlir/include/mlir/Dialect/QCO/QCOUtils.h @@ -20,7 +20,6 @@ #include #include -#include #include namespace mlir::qco { From 12ab2e49c2ec7cdc542808b33ee10b060cd3f903 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 30 Jun 2026 13:54:11 +0200 Subject: [PATCH 087/122] =?UTF-8?q?=F0=9F=A7=B9=20Clean=20up=20QCO=20matri?= =?UTF-8?q?x=20tests=20by=20removing=20unused=20controlledXH=20test=20and?= =?UTF-8?q?=20simplifying=20expected=20matrix=20calculations=20for=20Contr?= =?UTF-8?q?olledXH=20and=20ControlledInverseHT=20tests.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Dialect/QCO/IR/test_qco_ir_matrix.cpp | 52 ++----------------- 1 file changed, 3 insertions(+), 49 deletions(-) diff --git a/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp b/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp index 14c6fe9ccd..38cf586946 100644 --- a/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp +++ b/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp @@ -64,15 +64,6 @@ expectedMatrixFromComputation(const Fn& build, dd::buildFunctionality(comp, *package).getMatrix(numQubits)); } -static void controlledXH(QCOProgramBuilder& b) { - auto q = b.allocQubitRegister(2); - b.ctrl(q[0], q[1], [&](ValueRange targets) { - auto wire = b.x(targets[0]); - wire = b.h(wire); - return SmallVector{wire}; - }); -} - static void controlledInverseHT(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.ctrl(q[0], q[1], [&](ValueRange targets) { @@ -155,12 +146,9 @@ TEST_F(QCOMatrixTest, ControlledXHOpMatrix) { auto matrix = ctrlOp.getUnitaryMatrix(); ASSERT_TRUE(matrix); - const Matrix4x4 expected = - expectedMatrixFromComputation([](qc::QuantumComputation& comp) { - comp.addQubitRegister(2, "q"); - comp.cx(1, 0); - comp.ch(1, 0); - }); + DynamicMatrix expected = DynamicMatrix::identity(4); + expected.setBottomRightCorner(HOp::getUnitaryMatrix() * + XOp::getUnitaryMatrix()); ASSERT_TRUE(matrix->isApprox(expected)); } @@ -186,40 +174,6 @@ TEST_F(QCOMatrixTest, ControlledInverseHTOpMatrix) { ASSERT_TRUE(matrix->isApprox(expected)); } - -TEST_F(QCOMatrixTest, ControlledHOpMatrix) { - auto moduleOp = QCOProgramBuilder::build(context.get(), singleControlledH); - ASSERT_TRUE(moduleOp); - - auto funcOp = *moduleOp->getBody()->getOps().begin(); - auto ctrlOp = *funcOp.getBody().getOps().begin(); - auto matrix = ctrlOp.getUnitaryMatrix(); - - const auto ch = qc::StandardOperation(1, 0, qc::OpType::H); - const auto dd = std::make_unique(2); - const auto chDD = dd::getDD(ch, *dd); - const auto definition = chDD.getMatrix(2); - - const Matrix4x4 expected = matrix4FromDefinition(definition); - - ASSERT_TRUE(matrix->isApprox(expected)); -} - -TEST_F(QCOMatrixTest, ControlledXHOpMatrix) { - auto moduleOp = QCOProgramBuilder::build(context.get(), controlledXH); - ASSERT_TRUE(moduleOp); - - auto funcOp = *moduleOp->getBody()->getOps().begin(); - auto ctrlOp = *funcOp.getBody().getOps().begin(); - auto matrix = ctrlOp.getUnitaryMatrix(); - ASSERT_TRUE(matrix); - - DynamicMatrix expected = DynamicMatrix::identity(4); - expected.setBottomRightCorner(HOp::getUnitaryMatrix() * - XOp::getUnitaryMatrix()); - - ASSERT_TRUE(matrix->isApprox(expected)); -} /// @} /// \name QCO/Modifiers/InvOp.cpp From bbb3987e64dc154f54750ca66851fe6c37d66c10 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 30 Jun 2026 21:38:30 +0200 Subject: [PATCH 088/122] =?UTF-8?q?=F0=9F=A7=B9=20Refactor=20QCO=20matrix?= =?UTF-8?q?=20utilities=20by=20removing=20unused=20functions=20and=20intro?= =?UTF-8?q?ducing=20canonical=20controlled=20gates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mlir/Dialect/QCO/Transforms/Passes.h | 2 - mlir/include/mlir/Dialect/QCO/Utils/Matrix.h | 27 ------ .../Decomposition/NativeProfile.cpp | 23 ++++- mlir/lib/Dialect/QCO/Utils/Matrix.cpp | 83 ------------------- .../Compiler/test_compiler_pipeline.cpp | 14 +--- .../Dialect/QCO/IR/test_qco_ir_matrix.cpp | 9 ++ .../Decomposition/test_weyl_decomposition.cpp | 32 +++---- .../test_fuse_two_qubit_unitary_runs.cpp | 11 ++- .../Dialect/QCO/Utils/test_unitary_matrix.cpp | 9 -- mlir/unittests/programs/qc_programs.cpp | 9 -- mlir/unittests/programs/qc_programs.h | 4 - mlir/unittests/programs/qco_programs.cpp | 9 -- mlir/unittests/programs/qco_programs.h | 4 - 13 files changed, 52 insertions(+), 184 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h index 1919b50b0c..7444438a88 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.h @@ -14,8 +14,6 @@ #include #include -#include - namespace mlir::qco { #define GEN_PASS_DECL diff --git a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h index f99d58a4d2..83f3b3d66d 100644 --- a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h +++ b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h @@ -802,31 +802,4 @@ struct SymmetricEigen4 { Matrix4x4 eigenvectors{}; }; -[[nodiscard]] Matrix2x2 rxMatrix(double theta); -[[nodiscard]] Matrix2x2 ryMatrix(double theta); -[[nodiscard]] Matrix2x2 rzMatrix(double theta); - -[[nodiscard]] const Matrix2x2& iPauliX(); -[[nodiscard]] const Matrix2x2& iPauliY(); -[[nodiscard]] const Matrix2x2& iPauliZ(); - -[[nodiscard]] Matrix4x4 rxxMatrix(double theta); -[[nodiscard]] Matrix4x4 ryyMatrix(double theta); -[[nodiscard]] Matrix4x4 rzzMatrix(double theta); - -/** - * @brief Controlled-`X` with control on qubit 0 (MSB) and target on qubit 1. - * - * Operand order matches @ref Matrix2x2::embedInTwoQubit and QCO unitaries. - */ -[[nodiscard]] const Matrix4x4& twoQubitControlledX01(); - -/** - * @brief Controlled-`X` with control on qubit 1 and target on qubit 0. - * - * Same entangling content as @ref twoQubitControlledX01 but with wires - * reversed; useful when parametrizing basis-decomposer tests. - */ -[[nodiscard]] const Matrix4x4& twoQubitControlledZ(); - } // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp index 47f9349ee3..fe5caf4162 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp @@ -37,6 +37,24 @@ using mlir::qco::Matrix2x2; using mlir::qco::Matrix4x4; +namespace { + +const Matrix4x4& canonicalControlledX() { + static const Matrix4x4 matrix = + Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, 1.0, 0.0); + return matrix; +} + +const Matrix4x4& canonicalControlledZ() { + static const Matrix4x4 matrix = + Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, + 1.0, 0.0, 0.0, 0.0, 0.0, -1.0); + return matrix; +} + +} // namespace + namespace mlir::qco::decomposition { static std::optional parseGateToken(llvm::StringRef name) { @@ -107,13 +125,12 @@ cachedBasisDecomposer(NativeGateKind entangler) { switch (entangler) { case NativeGateKind::CX: { static const TwoQubitBasisDecomposer DECOMPOSER = - TwoQubitBasisDecomposer::create(mlir::qco::twoQubitControlledX01(), - 1.0); + TwoQubitBasisDecomposer::create(canonicalControlledX(), 1.0); return DECOMPOSER; } case NativeGateKind::CZ: { static const TwoQubitBasisDecomposer DECOMPOSER = - TwoQubitBasisDecomposer::create(mlir::qco::twoQubitControlledZ(), 1.0); + TwoQubitBasisDecomposer::create(canonicalControlledZ(), 1.0); return DECOMPOSER; } default: diff --git a/mlir/lib/Dialect/QCO/Utils/Matrix.cpp b/mlir/lib/Dialect/QCO/Utils/Matrix.cpp index d325f8d544..9169388fc7 100644 --- a/mlir/lib/Dialect/QCO/Utils/Matrix.cpp +++ b/mlir/lib/Dialect/QCO/Utils/Matrix.cpp @@ -996,87 +996,4 @@ DynamicMatrix operator*(const Complex& scalar, const DynamicMatrix& matrix) { return matrix * scalar; } -Matrix2x2 rxMatrix(const double theta) { - const auto halfTheta = theta / 2.0; - const Complex cos{std::cos(halfTheta), 0.0}; - const Complex isin{0.0, -std::sin(halfTheta)}; - return Matrix2x2::fromElements(cos, isin, isin, cos); -} - -Matrix2x2 ryMatrix(const double theta) { - const auto halfTheta = theta / 2.0; - const Complex cos{std::cos(halfTheta), 0.0}; - const Complex sin{std::sin(halfTheta), 0.0}; - return Matrix2x2::fromElements(cos, -sin, sin, cos); -} - -Matrix2x2 rzMatrix(const double theta) { - return Matrix2x2::fromElements( - Complex{std::cos(theta / 2.0), -std::sin(theta / 2.0)}, 0.0, 0.0, - Complex{std::cos(theta / 2.0), std::sin(theta / 2.0)}); -} - -const Matrix2x2& iPauliZ() { - static const Matrix2x2 MATRIX = - Matrix2x2::fromElements(Complex{0.0, 1.0}, 0.0, 0.0, Complex{0.0, -1.0}); - return MATRIX; -} - -const Matrix2x2& iPauliY() { - static const Matrix2x2 MATRIX = Matrix2x2::fromElements(0.0, 1.0, -1.0, 0.0); - return MATRIX; -} - -const Matrix2x2& iPauliX() { - static const Matrix2x2 MATRIX = - Matrix2x2::fromElements(0.0, Complex{0.0, 1.0}, Complex{0.0, 1.0}, 0.0); - return MATRIX; -} - -Matrix4x4 rxxMatrix(const double theta) { - const auto cosTheta = std::cos(theta / 2.0); - const Complex misin{0.0, -std::sin(theta / 2.0)}; - return Matrix4x4::fromElements(cosTheta, 0.0, 0.0, misin, // - 0.0, cosTheta, misin, 0.0, // - 0.0, misin, cosTheta, 0.0, // - misin, 0.0, 0.0, cosTheta); -} - -Matrix4x4 ryyMatrix(const double theta) { - const auto cosTheta = std::cos(theta / 2.0); - const Complex isin{0.0, std::sin(theta / 2.0)}; - const Complex misin{0.0, -std::sin(theta / 2.0)}; - return Matrix4x4::fromElements(cosTheta, 0.0, 0.0, isin, // - 0.0, cosTheta, misin, 0.0, // - 0.0, misin, cosTheta, 0.0, // - isin, 0.0, 0.0, cosTheta); -} - -Matrix4x4 rzzMatrix(const double theta) { - const auto cosTheta = std::cos(theta / 2.0); - const auto sinTheta = std::sin(theta / 2.0); - const Complex em{cosTheta, -sinTheta}; - const Complex ep{cosTheta, sinTheta}; - return Matrix4x4::fromElements(em, 0.0, 0.0, 0.0, // - 0.0, ep, 0.0, 0.0, // - 0.0, 0.0, ep, 0.0, // - 0.0, 0.0, 0.0, em); -} - -const Matrix4x4& twoQubitControlledX01() { - static const Matrix4x4 MATRIX = Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, // - 0.0, 1.0, 0.0, 0.0, // - 0.0, 0.0, 0.0, 1.0, // - 0.0, 0.0, 1.0, 0.0); - return MATRIX; -} - -const Matrix4x4& twoQubitControlledZ() { - static const Matrix4x4 MATRIX = Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, // - 0.0, 1.0, 0.0, 0.0, // - 0.0, 0.0, 1.0, 0.0, // - 0.0, 0.0, 0.0, -1.0); - return MATRIX; -} - } // namespace mlir::qco diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 0f22e0e245..b4a850f7e7 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -805,19 +805,7 @@ computeStaticTwoQubitUnitary(mlir::ModuleOp module) { return std::nullopt; } mlir::qco::Matrix4x4 twoQ; - if (auto ctrl = llvm::dyn_cast(&rawOp)) { - if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { - return std::nullopt; - } - auto* body = ctrl.getBodyUnitary(0).getOperation(); - if (llvm::isa(body)) { - twoQ = mlir::qco::twoQubitControlledX01(); - } else if (llvm::isa(body)) { - twoQ = mlir::qco::twoQubitControlledZ(); - } else { - return std::nullopt; - } - } else if (!op.getUnitaryMatrix4x4(twoQ)) { + if (!op.getUnitaryMatrix4x4(twoQ)) { return std::nullopt; } const mlir::SmallVector ids{*q0, *q1}; diff --git a/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp b/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp index 38cf586946..a9eb5d918f 100644 --- a/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp +++ b/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp @@ -76,6 +76,15 @@ static void controlledInverseHT(QCOProgramBuilder& b) { }); } +static void controlledXH(QCOProgramBuilder& b) { + auto q = b.allocQubitRegister(2); + b.ctrl(q[0], q[1], [&](ValueRange targets) { + auto wire = b.x(targets[0]); + wire = b.h(wire); + return SmallVector{wire}; + }); +} + namespace { struct QCOMatrixTestCase { diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index 8e796502c7..3ba71cc40d 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -12,6 +12,7 @@ #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/NativeProfile.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" @@ -63,6 +64,12 @@ static constexpr Matrix4x4 TWO_QUBIT_CONTROLLED_X10 = 0.0, 0.0, 1.0, 0.0, // 0.0, 1.0, 0.0, 0.0); +static constexpr Matrix4x4 TWO_QUBIT_CONTROLLED_Z = + Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, // + 0.0, 1.0, 0.0, 0.0, // + 0.0, 0.0, 1.0, 0.0, // + 0.0, 0.0, 0.0, -1.0); + template static bool isUnitaryMatrix(const MatrixT& matrix, const double tolerance = MATRIX_TOLERANCE) { @@ -443,21 +450,6 @@ static bool extractSingleQubitMatrix(UnitaryOpInterface op, Matrix2x2& out) { } static bool extractTwoQubitMatrix(UnitaryOpInterface op, Matrix4x4& out) { - if (auto ctrl = llvm::dyn_cast(op.getOperation())) { - if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { - return false; - } - Operation* body = ctrl.getBodyUnitary(0).getOperation(); - if (llvm::isa(body)) { - out = twoQubitControlledX01(); - return true; - } - if (llvm::isa(body)) { - out = twoQubitControlledZ(); - return true; - } - return false; - } return op.getUnitaryMatrix4x4(out); } @@ -516,7 +508,7 @@ computeTwoQubitUnitaryFromFunc(func::FuncOp funcOp) { return std::nullopt; } Matrix4x4 twoQ; - if (!extractTwoQubitMatrix(op, twoQ)) { + if (!op.getUnitaryMatrix4x4(twoQ);) { return std::nullopt; } unitary = twoQ.reorderForQubits(*q0id, *q1id) * unitary; @@ -604,7 +596,7 @@ INSTANTIATE_TEST_SUITE_P( Profiles, WeylSynthesisTest, testing::Values( WeylSynthesisCase{"CxGeneric", "u,cx", - [] { return twoQubitControlledX01(); }}, + [] { return TWO_QUBIT_CONTROLLED_X01; }}, WeylSynthesisCase{"ProductGeneric", "u,cx", [] { return Matrix4x4::kron(RZOp::unitaryMatrix(1.0), @@ -614,12 +606,12 @@ INSTANTIATE_TEST_SUITE_P( [] { return Matrix4x4::kron(HOp::getUnitaryMatrix(), Matrix2x2::identity()) * - twoQubitControlledX01() * + TWO_QUBIT_CONTROLLED_X01 * Matrix4x4::kron(RZOp::unitaryMatrix(0.2), RYOp::unitaryMatrix(0.1)); }}, WeylSynthesisCase{"CzGeneric", "u,cz", - [] { return twoQubitControlledZ(); }}), + [] { return TWO_QUBIT_CONTROLLED_Z; }}), [](const testing::TestParamInfo& info) { return info.param.name; }); @@ -657,7 +649,7 @@ TEST(WeylSynthesisTest, FailsWithoutEntanglerInSpec) { Value out1; EXPECT_TRUE(failed(synthesizeUnitary2QWeyl( builder, func.getLoc(), entry->getArgument(0), entry->getArgument(1), - twoQubitControlledX01(), *spec, out0, out1))); + TWO_QUBIT_CONTROLLED_X01, *spec, out0, out1))); } TEST(NativeSpecTest, ParsesAndRejectsMenus) { diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp index ce984eb843..38e283a7b6 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp @@ -53,6 +53,15 @@ using namespace mqt::test; using ProgramFn = void (*)(mlir::qc::QCProgramBuilder&); using NativePredicate = bool (*)(OwningOpRef&); +static void controlledXH(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.ctrl(q0, {q1}, [&](ValueRange targets) { + b.x(targets[0]); + b.h(targets[0]); + }); +} + template static bool onlyTheseOps(OwningOpRef& moduleOp, bool allowCx, bool allowCz) { @@ -675,7 +684,7 @@ TEST_F(FuseTwoQubitUnitaryRunsPassTest, InverseWrappedOpsSynthesize) { } TEST_F(FuseTwoQubitUnitaryRunsPassTest, ControlledXHSynthesizesToNativeMenu) { - expectEquivalentAndNativeAfterSynthesis(mlir::qc::controlledXH, "x,sx,rz,cx", + expectEquivalentAndNativeAfterSynthesis(controlledXH, "x,sx,rz,cx", onlyIbmBasicCxOps); } diff --git a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp index 74b9f8c31d..76e50a997d 100644 --- a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp +++ b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp @@ -595,12 +595,3 @@ TEST(SymmetricEigensolver, HandlesDegenerateSpectrum) { const Matrix4x4& v = result.eigenvectors; EXPECT_TRUE((v.transpose() * v).isIdentity()); } - -TEST(GateMatrixFactories, ControlledGates) { - EXPECT_TRUE(twoQubitControlledX01().isApprox( - Matrix4x4::fromElements(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0))); - EXPECT_TRUE(twoQubitControlledZ().isApprox(Matrix4x4::fromElements( - 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, -1))); - EXPECT_TRUE(rxMatrix(0.0).isIdentity()); - EXPECT_TRUE((iPauliX() * iPauliX()).isApprox(-1.0 * Matrix2x2::identity())); -} diff --git a/mlir/unittests/programs/qc_programs.cpp b/mlir/unittests/programs/qc_programs.cpp index 8ae72de487..8410afd7a8 100644 --- a/mlir/unittests/programs/qc_programs.cpp +++ b/mlir/unittests/programs/qc_programs.cpp @@ -389,15 +389,6 @@ void singleControlledH(QCProgramBuilder& b) { b.ch(q[0], q[1]); } -void controlledXH(QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.ctrl(q0, {q1}, [&](ValueRange targets) { - b.x(targets[0]); - b.h(targets[0]); - }); -} - void multipleControlledH(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.mch({q[0], q[1]}, q[2]); diff --git a/mlir/unittests/programs/qc_programs.h b/mlir/unittests/programs/qc_programs.h index 9f9b5d198c..25d76718cc 100644 --- a/mlir/unittests/programs/qc_programs.h +++ b/mlir/unittests/programs/qc_programs.h @@ -237,10 +237,6 @@ void h(QCProgramBuilder& b); /// Creates a circuit with a single controlled H gate. void singleControlledH(QCProgramBuilder& b); -/// Creates a circuit with a control modifier applied to an X-H sequence in the -/// ctrl body. -void controlledXH(QCProgramBuilder& b); - /// Creates a circuit with a multi-controlled H gate. void multipleControlledH(QCProgramBuilder& b); diff --git a/mlir/unittests/programs/qco_programs.cpp b/mlir/unittests/programs/qco_programs.cpp index 65f6ececfa..b384aa20a1 100644 --- a/mlir/unittests/programs/qco_programs.cpp +++ b/mlir/unittests/programs/qco_programs.cpp @@ -320,15 +320,6 @@ void controlledTwoX(QCOProgramBuilder& b) { }); } -void controlledXH(QCOProgramBuilder& b) { - auto q = b.allocQubitRegister(2); - b.ctrl(q[0], q[1], [&](ValueRange targets) { - auto wire = b.x(targets[0]); - wire = b.h(wire); - return SmallVector{wire}; - }); -} - void inverseTwoX(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.inv(q[0], [&](ValueRange qubits) { diff --git a/mlir/unittests/programs/qco_programs.h b/mlir/unittests/programs/qco_programs.h index a771a80cbe..b568b4b02f 100644 --- a/mlir/unittests/programs/qco_programs.h +++ b/mlir/unittests/programs/qco_programs.h @@ -173,10 +173,6 @@ void twoX(QCOProgramBuilder& b); /// Creates a circuit with a control modifier applied to two subsequent X gates. void controlledTwoX(QCOProgramBuilder& b); -/// Creates a circuit with a control modifier applied to an X-H sequence in the -/// ctrl body. -void controlledXH(QCOProgramBuilder& b); - /// Creates a circuit with an inverse modifier applied to two subsequent X /// gates. void inverseTwoX(QCOProgramBuilder& b); From 73f05d9e3f091354c80cb92ca0ce5d3ae6af7be4 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 30 Jun 2026 21:47:32 +0200 Subject: [PATCH 089/122] =?UTF-8?q?=F0=9F=8E=A8=20Move=20`isEquivalentUpTo?= =?UTF-8?q?GlobalPhase`=20function=20to=20`Matrix.h`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/include/mlir/Dialect/QCO/Utils/Matrix.h | 28 +++++++++++++++++++ .../Compiler/test_compiler_pipeline.cpp | 2 -- .../Decomposition/test_weyl_decomposition.cpp | 2 +- .../Dialect/QCO/Utils/test_unitary_matrix.cpp | 8 ++++++ mlir/unittests/TestCaseUtils.h | 22 --------------- 5 files changed, 37 insertions(+), 25 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h index 83f3b3d66d..8766637c9b 100644 --- a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h +++ b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -776,6 +777,33 @@ inline constexpr bool std::is_same, std::is_same>; +/** + * @brief Checks whether two unitaries are equal up to a global phase. + * + * Uses `trace(rhs.adjoint() * lhs)` to infer a unit-modulus phase factor, then + * compares `lhs` to `factor * rhs` with @ref isApprox. A near-zero overlap + * (`|trace| <= tol`) is treated as not equivalent to avoid dividing by a tiny + * number. + * + * @tparam Matrix Any supported fixed- or dynamic-size matrix type. + * @param lhs Left-hand unitary. + * @param rhs Right-hand unitary. + * @param tol Absolute tolerance for overlap and entry-wise comparison. + * @return True when @p lhs and @p rhs differ only by a global phase. + */ +template + requires is_supported_matrix_v +[[nodiscard]] bool isEquivalentUpToGlobalPhase(const Matrix& lhs, + const Matrix& rhs, + double tol = MATRIX_TOLERANCE) { + const auto overlap = (rhs.adjoint() * lhs).trace(); + if (std::abs(overlap) <= tol) { + return false; + } + const auto factor = overlap / std::abs(overlap); + return lhs.isApprox(factor * rhs, tol); +} + /// Scalar-on-the-left multiply `scalar * matrix` (commutes with the member /// `matrix * scalar`). Provided so generic code can scale a matrix from /// either side. diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index b4a850f7e7..aae2ab6f87 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -744,8 +744,6 @@ class CompilerPipelineNativeSynthesisConfigTest : public testing::Test { } // namespace -using mqt::test::isEquivalentUpToGlobalPhase; - static std::optional computeStaticTwoQubitUnitary(mlir::ModuleOp module) { if (module == nullptr) { diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index 3ba71cc40d..722f848ddf 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -508,7 +508,7 @@ computeTwoQubitUnitaryFromFunc(func::FuncOp funcOp) { return std::nullopt; } Matrix4x4 twoQ; - if (!op.getUnitaryMatrix4x4(twoQ);) { + if (!op.getUnitaryMatrix4x4(twoQ)) { return std::nullopt; } unitary = twoQ.reorderForQubits(*q0id, *q1id) * unitary; diff --git a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp index 76e50a997d..60a1be555e 100644 --- a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp +++ b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp @@ -595,3 +595,11 @@ TEST(SymmetricEigensolver, HandlesDegenerateSpectrum) { const Matrix4x4& v = result.eigenvectors; EXPECT_TRUE((v.transpose() * v).isIdentity()); } + +TEST(GlobalPhaseEquivalence, IsEquivalentUpToGlobalPhase) { + const Matrix2x2 pauliX = Matrix2x2::fromElements(0, 1, 1, 0); + const Complex phase{0.0, 1.0}; + EXPECT_TRUE(isEquivalentUpToGlobalPhase(pauliX, phase * pauliX)); + EXPECT_TRUE(isEquivalentUpToGlobalPhase(pauliX, pauliX)); + EXPECT_FALSE(isEquivalentUpToGlobalPhase(pauliX, Matrix2x2::identity())); +} diff --git a/mlir/unittests/TestCaseUtils.h b/mlir/unittests/TestCaseUtils.h index 7603fda8be..570c86c87f 100644 --- a/mlir/unittests/TestCaseUtils.h +++ b/mlir/unittests/TestCaseUtils.h @@ -18,8 +18,6 @@ #include #include -#include -#include // NOLINT(misc-include-cleaner) #include #include #include @@ -28,26 +26,6 @@ namespace mqt::test { -/** - * Check whether two unitary matrices are equal up to a single unit-modulus - * global phase factor. - * - * The comparison is symmetric and numerically stable in the sense that a near - * zero overlap (``|trace(rhs^H * lhs)| <= atol``) is treated as "not - * equivalent" to avoid division by a tiny number. - */ -template -[[nodiscard]] bool isEquivalentUpToGlobalPhase(const Matrix& lhs, - const Matrix& rhs, - double atol = 1e-10) { - const auto overlap = (rhs.adjoint() * lhs).trace(); - if (std::abs(overlap) <= atol) { - return false; - } - const auto factor = overlap / std::abs(overlap); - return lhs.isApprox(factor * rhs, atol); -} - template struct NamedBuilder { const char* name = nullptr; void (*fn)(BuilderT&) = nullptr; From 20861025129bfc324187499d6c25b15e4fed49d7 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 1 Jul 2026 09:35:45 +0200 Subject: [PATCH 090/122] =?UTF-8?q?=E2=9C=A8=20Add=20native=20gate=20profi?= =?UTF-8?q?le=20parsing=20and=20Euler=20R-basis=20synthesis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce NativeProfileSpec for parsing native gate menus and driving two-qubit Weyl synthesis, extend Euler decomposition with an R-basis for IQM-style menus, and add isEquivalentUpToGlobalPhase for unitary checks. Assisted-by: Claude Sonnet 4.6 via Cursor Co-authored-by: Cursor --- .../QCO/Transforms/Decomposition/Euler.h | 1 + .../Transforms/Decomposition/NativeProfile.h | 86 +++++ mlir/include/mlir/Dialect/QCO/Utils/Matrix.h | 28 ++ .../lib/Dialect/QCO/Transforms/CMakeLists.txt | 10 +- .../QCO/Transforms/Decomposition/Euler.cpp | 33 +- .../Decomposition/NativeProfile.cpp | 321 ++++++++++++++++ .../test_euler_decomposition.cpp | 4 + .../Decomposition/test_weyl_decomposition.cpp | 356 ++++++++++++++++++ .../Dialect/QCO/Utils/test_unitary_matrix.cpp | 8 + 9 files changed, 841 insertions(+), 6 deletions(-) create mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeProfile.h create mode 100644 mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h index 7cea37b3a6..5d02898c3b 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h @@ -32,6 +32,7 @@ enum class EulerBasis : std::uint8_t { XYX = 3, ///< `RX(phi) * RY(theta) * RX(lambda)`. U = 4, ///< `U(theta, phi, lambda)`. ZSXX = 5, ///< `RZ` / `SX` / `X` synthesis via ZYZ decomposition. + R = 6, ///< `R(.,0) * R(.,pi/2) * R(.,0)` (XYX with `Rx`/`Ry` as `R`). }; /** diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeProfile.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeProfile.h new file mode 100644 index 0000000000..cc07a03966 --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeProfile.h @@ -0,0 +1,86 @@ +/* + * 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/Euler.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace mlir::qco::decomposition { + +/** + * @brief Gate token in a comma-separated native menu (e.g. `"u,cx,rzz"`). + */ +enum class NativeGateKind : std::uint8_t { + U, + X, + SX, + RZ, + RX, + RY, + R, + CX, + CZ, + RZZ, +}; + +/** + * @brief Resolved native-gate menu for two-qubit Weyl synthesis. + * + * @p gates is the parsed menu. Euler decomposition and entangler choice are + * derived from it with fixed priority (see @ref NativeProfileSpec::eulerBasis). + */ +struct NativeProfileSpec { + llvm::DenseSet gates; + + /** @brief Preferred single-qubit Euler basis for synthesis in this menu. */ + [[nodiscard]] EulerBasis eulerBasis() const; +}; + +/** @brief Parses a comma-separated native-gate menu (e.g. `"u,cx,rzz"`). */ +[[nodiscard]] std::optional +parseNativeSpec(StringRef nativeGates); + +/** @brief Synthesizes a two-qubit unitary as gates allowed by @p spec. */ +[[nodiscard]] LogicalResult +synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, Value qubit0, + Value qubit1, const Matrix4x4& target, + const NativeProfileSpec& spec, Value& outQubit0, + Value& outQubit1); + +/** + * @brief Entangling basis gates needed to synthesize @p target under @p spec. + * + * @return Count for @p spec, or `std::nullopt` when synthesis is impossible. + */ +[[nodiscard]] std::optional +twoQubitEntanglerCount(const Matrix4x4& target, const NativeProfileSpec& spec); + +/** + * @brief Returns true when @p op is already on the resolved native menu. + * + * Barriers and global phase are always allowed. Single-qubit primitives, + * single-target `CtrlOp` shells (`X`/`Z` bodies), and `RZZ` are checked + * against @p spec.gates. + */ +[[nodiscard]] bool allowsOp(Operation* op, const NativeProfileSpec& spec); + +} // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h index 83f3b3d66d..8766637c9b 100644 --- a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h +++ b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -776,6 +777,33 @@ inline constexpr bool std::is_same, std::is_same>; +/** + * @brief Checks whether two unitaries are equal up to a global phase. + * + * Uses `trace(rhs.adjoint() * lhs)` to infer a unit-modulus phase factor, then + * compares `lhs` to `factor * rhs` with @ref isApprox. A near-zero overlap + * (`|trace| <= tol`) is treated as not equivalent to avoid dividing by a tiny + * number. + * + * @tparam Matrix Any supported fixed- or dynamic-size matrix type. + * @param lhs Left-hand unitary. + * @param rhs Right-hand unitary. + * @param tol Absolute tolerance for overlap and entry-wise comparison. + * @return True when @p lhs and @p rhs differ only by a global phase. + */ +template + requires is_supported_matrix_v +[[nodiscard]] bool isEquivalentUpToGlobalPhase(const Matrix& lhs, + const Matrix& rhs, + double tol = MATRIX_TOLERANCE) { + const auto overlap = (rhs.adjoint() * lhs).trace(); + if (std::abs(overlap) <= tol) { + return false; + } + const auto factor = overlap / std::abs(overlap); + return lhs.isApprox(factor * rhs, tol); +} + /// Scalar-on-the-left multiply `scalar * matrix` (commutes with the member /// `matrix * scalar`). Provided so generic code can scale a matrix from /// either side. diff --git a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt index 77f594db17..77d847d3ba 100644 --- a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt +++ b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt @@ -24,11 +24,11 @@ add_mlir_library( mqt_mlir_target_use_project_options(MLIRQCOTransforms) -# collect header files -file(GLOB_RECURSE PASSES_HEADERS_SOURCE - ${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Dialect/QCO/Transforms/*.h) -file(GLOB_RECURSE PASSES_HEADERS_BUILD - ${MQT_MLIR_BUILD_INCLUDE_DIR}/mlir/Dialect/QCO/Transforms/*.inc) +# collect header files (subdirs: Decomposition/, …) +file(GLOB_RECURSE PASSES_HEADERS_SOURCE CONFIGURE_DEPENDS + "${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Dialect/QCO/Transforms/*.h") +file(GLOB_RECURSE PASSES_HEADERS_BUILD CONFIGURE_DEPENDS + "${MQT_MLIR_BUILD_INCLUDE_DIR}/mlir/Dialect/QCO/Transforms/*.inc") # add public headers using file sets target_sources( diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index 7d0ce0288d..55576cd342 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -192,6 +192,9 @@ EulerAngles anglesFromUnitary(const Matrix2x2& matrix, const EulerBasis basis) { case EulerBasis::XZX: return paramsXZX(matrix); case EulerBasis::XYX: + case EulerBasis::R: + // The `R` basis reuses the X-Y-X angles and lowers `Rx`/`Ry` to the native + // `R(theta, phi)` gate (`Rx(a) == R(a, 0)`, `Ry(a) == R(a, pi/2)`). return paramsXYX(matrix); case EulerBasis::U: return paramsU(matrix); @@ -212,7 +215,7 @@ namespace { * `RZ`/`RY`/`RX` use @p theta as the rotation angle; `U` uses all three angles. */ struct SynthesisStep { - enum class Kind : std::uint8_t { RZ, RY, RX, SX, X, U }; + enum class Kind : std::uint8_t { RZ, RY, RX, SX, X, U, R }; Kind kind = Kind::RZ; double theta = 0.0; @@ -241,6 +244,19 @@ struct Unitary1QEulerPlan { } } + /** + * @brief Appends a native `R(angle, axis)` step for non-negligible angles. + * + * @param angle The rotation angle in radians. + * @param axis The rotation axis in the XY-plane (`0` for `Rx`, `pi/2` for + * `Ry`). + */ + void appendRStep(const double angle, const double axis) { + if (!isNearZeroRotationAngle(angle)) { + steps.emplace_back(SynthesisStep::Kind::R, angle, axis); + } + } + /** * @brief Appends the decomposition for @p basis based on @p angles. * @@ -267,6 +283,9 @@ struct Unitary1QEulerPlan { case EulerBasis::XYX: appendRotation(SynthesisStep::Kind::RX, angles.phi + angles.lambda); break; + case EulerBasis::R: + appendRStep(angles.phi + angles.lambda, 0.0); + break; case EulerBasis::U: steps.emplace_back(SynthesisStep::Kind::U, 0.0, angles.phi, angles.lambda); @@ -301,6 +320,14 @@ struct Unitary1QEulerPlan { appendRotation(SynthesisStep::Kind::RX, angles.phi); phase = angles.phase; break; + case EulerBasis::R: + // X-Y-X with `Rx(a) == R(a, 0)` and `Ry(a) == R(a, pi/2)`. + appendRStep(angles.lambda, 0.0); + steps.emplace_back(SynthesisStep::Kind::R, angles.theta, + std::numbers::pi / 2.0); + appendRStep(angles.phi, 0.0); + phase = angles.phase; + break; case EulerBasis::U: steps.emplace_back(SynthesisStep::Kind::U, angles.theta, angles.phi, angles.lambda); @@ -390,6 +417,9 @@ emitUnitary1QEulerPlan(OpBuilder& builder, Location loc, Value qubit, qubit = UOp::create(builder, loc, qubit, theta, phi, lambda).getQubitOut(); break; + case SynthesisStep::Kind::R: + qubit = ROp::create(builder, loc, qubit, theta, phi).getQubitOut(); + break; } } emitGPhaseIfNeeded(builder, loc, plan.phase); @@ -404,6 +434,7 @@ std::optional parseEulerBasis(StringRef basis) { .Case("xyx", EulerBasis::XYX) .Case("u", EulerBasis::U) .Case("zsxx", EulerBasis::ZSXX) + .Case("r", EulerBasis::R) .Default(std::nullopt); } diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp new file mode 100644 index 0000000000..fe5caf4162 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp @@ -0,0 +1,321 @@ +/* + * 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/NativeProfile.h" + +#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" +#include "mlir/Dialect/Utils/Utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +using mlir::qco::Matrix2x2; +using mlir::qco::Matrix4x4; + +namespace { + +const Matrix4x4& canonicalControlledX() { + static const Matrix4x4 matrix = + Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, 1.0, 0.0); + return matrix; +} + +const Matrix4x4& canonicalControlledZ() { + static const Matrix4x4 matrix = + Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, + 1.0, 0.0, 0.0, 0.0, 0.0, -1.0); + return matrix; +} + +} // namespace + +namespace mlir::qco::decomposition { + +static std::optional parseGateToken(llvm::StringRef name) { + return llvm::StringSwitch>(name) + .Case("u", NativeGateKind::U) + .Case("x", NativeGateKind::X) + .Case("sx", NativeGateKind::SX) + .Cases("rz", "p", NativeGateKind::RZ) + .Case("rx", NativeGateKind::RX) + .Case("ry", NativeGateKind::RY) + .Case("r", NativeGateKind::R) + .Case("cx", NativeGateKind::CX) + .Case("cz", NativeGateKind::CZ) + .Case("rzz", NativeGateKind::RZZ) + .Default(std::nullopt); +} + +static std::optional> +parseGateSet(llvm::StringRef nativeGates) { + llvm::DenseSet gates; + SmallVector parts; + nativeGates.split(parts, ',', /*MaxSplit=*/-1, /*KeepEmpty=*/false); + for (llvm::StringRef part : parts) { + const auto token = part.trim().lower(); + if (token.empty()) { + continue; + } + const auto gate = parseGateToken(token); + if (!gate) { + return std::nullopt; + } + gates.insert(*gate); + } + return gates; +} + +static bool +hasSingleQubitStrategy(const llvm::DenseSet& gates) { + const auto has = [&](NativeGateKind kind) { return gates.contains(kind); }; + if (has(NativeGateKind::U)) { + return true; + } + if (has(NativeGateKind::X) && has(NativeGateKind::SX) && + has(NativeGateKind::RZ)) { + return true; + } + if (has(NativeGateKind::R)) { + return true; + } + return (has(NativeGateKind::RX) && has(NativeGateKind::RZ)) || + (has(NativeGateKind::RX) && has(NativeGateKind::RY)) || + (has(NativeGateKind::RY) && has(NativeGateKind::RZ)); +} + +static std::optional +selectEntangler(const NativeProfileSpec& spec) { + if (spec.gates.contains(NativeGateKind::CX)) { + return NativeGateKind::CX; + } + if (spec.gates.contains(NativeGateKind::CZ)) { + return NativeGateKind::CZ; + } + return std::nullopt; +} + +static const TwoQubitBasisDecomposer& +cachedBasisDecomposer(NativeGateKind entangler) { + switch (entangler) { + case NativeGateKind::CX: { + static const TwoQubitBasisDecomposer DECOMPOSER = + TwoQubitBasisDecomposer::create(canonicalControlledX(), 1.0); + return DECOMPOSER; + } + case NativeGateKind::CZ: { + static const TwoQubitBasisDecomposer DECOMPOSER = + TwoQubitBasisDecomposer::create(canonicalControlledZ(), 1.0); + return DECOMPOSER; + } + default: + llvm_unreachable("only CX/CZ are valid entanglers"); + } +} + +static std::optional +decomposeForProfile(const Matrix4x4& target, const NativeProfileSpec& spec) { + const auto entangler = selectEntangler(spec); + if (!entangler) { + return std::nullopt; + } + return cachedBasisDecomposer(*entangler).decomposeTarget(target); +} + +static void emitGPhaseIfNonTrivial(OpBuilder& builder, Location loc, + double phase) { + constexpr double epsilon = 1e-12; + if (std::abs(phase) > epsilon) { + GPhaseOp::create(builder, loc, phase); + } +} + +static Value emitSingleQubitMatrix(OpBuilder& builder, Location loc, + Value inQubit, const Matrix2x2& matrix, + EulerBasis basis) { + return *synthesizeUnitary1QEuler(builder, loc, inQubit, matrix, + /*runSize=*/0, /*hasNonBasisGate=*/true, + basis); +} + +static std::optional gateKindFor(UnitaryOpInterface op) { + Operation* raw = op.getOperation(); + if (llvm::isa(raw)) { + return NativeGateKind::U; + } + if (llvm::isa(raw)) { + return NativeGateKind::X; + } + if (llvm::isa(raw)) { + return NativeGateKind::SX; + } + if (llvm::isa(raw)) { + return NativeGateKind::RZ; + } + if (llvm::isa(raw)) { + return NativeGateKind::RX; + } + if (llvm::isa(raw)) { + return NativeGateKind::RY; + } + if (llvm::isa(raw)) { + return NativeGateKind::R; + } + return std::nullopt; +} + +static std::optional entanglerKindFor(CtrlOp ctrl) { + if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { + return std::nullopt; + } + auto bodyUnitary = + utils::getSoleBodyUnitary(*ctrl.getBody()); + if (!bodyUnitary) { + return std::nullopt; + } + Operation* body = bodyUnitary.getOperation(); + if (llvm::isa(body)) { + return NativeGateKind::CX; + } + if (llvm::isa(body)) { + return NativeGateKind::CZ; + } + return std::nullopt; +} + +EulerBasis NativeProfileSpec::eulerBasis() const { + const auto has = [&](NativeGateKind kind) { return gates.contains(kind); }; + if (has(NativeGateKind::U)) { + return EulerBasis::U; + } + if (has(NativeGateKind::X) && has(NativeGateKind::SX) && + has(NativeGateKind::RZ)) { + return EulerBasis::ZSXX; + } + if (has(NativeGateKind::R)) { + return EulerBasis::R; + } + if (has(NativeGateKind::RX) && has(NativeGateKind::RZ)) { + return EulerBasis::XZX; + } + if (has(NativeGateKind::RX) && has(NativeGateKind::RY)) { + return EulerBasis::XYX; + } + if (has(NativeGateKind::RY) && has(NativeGateKind::RZ)) { + return EulerBasis::ZYZ; + } + llvm_unreachable("parseNativeSpec guarantees a synthesizable basis"); +} + +std::optional parseNativeSpec(llvm::StringRef nativeGates) { + auto gates = parseGateSet(nativeGates); + if (!gates || gates->empty() || !hasSingleQubitStrategy(*gates)) { + return std::nullopt; + } + return NativeProfileSpec{.gates = std::move(gates).value()}; +} + +LogicalResult synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, + Value qubit0, Value qubit1, + const Matrix4x4& target, + const NativeProfileSpec& spec, + Value& outQubit0, Value& outQubit1) { + const auto entangler = selectEntangler(spec); + if (!entangler) { + return failure(); + } + const auto native = decomposeForProfile(target, spec); + if (!native) { + return failure(); + } + const auto basis = spec.eulerBasis(); + + emitGPhaseIfNonTrivial(builder, loc, native->globalPhase); + + Value wire0 = qubit0; + Value wire1 = qubit1; + const auto& factors = native->singleQubitFactors; + const std::uint8_t numBasisUses = native->numBasisUses; + const auto emitFactor = [&](Value& wire, std::size_t index) { + wire = emitSingleQubitMatrix(builder, loc, wire, factors[index], basis); + }; + const auto emitEntangler = [&]() { + auto ctrlOp = CtrlOp::create( + builder, loc, ValueRange{wire0}, ValueRange{wire1}, + [&](ValueRange targetArgs) -> SmallVector { + if (*entangler == NativeGateKind::CZ) { + return {ZOp::create(builder, loc, targetArgs[0]).getOutputQubit(0)}; + } + return {XOp::create(builder, loc, targetArgs[0]).getOutputQubit(0)}; + }); + wire0 = ctrlOp.getOutputControl(0); + wire1 = ctrlOp.getOutputTarget(0); + }; + + for (std::uint8_t i = 0; i < numBasisUses; ++i) { + emitFactor(wire1, static_cast(2 * i)); + emitFactor(wire0, static_cast((2 * i) + 1)); + emitEntangler(); + } + emitFactor(wire1, static_cast(2 * numBasisUses)); + emitFactor(wire0, static_cast((2 * numBasisUses) + 1)); + + outQubit0 = wire0; + outQubit1 = wire1; + return success(); +} + +std::optional +twoQubitEntanglerCount(const Matrix4x4& target, const NativeProfileSpec& spec) { + const auto native = decomposeForProfile(target, spec); + if (!native) { + return std::nullopt; + } + return native->numBasisUses; +} + +bool allowsOp(Operation* op, const NativeProfileSpec& spec) { + if (llvm::isa(op)) { + return true; + } + if (auto ctrl = llvm::dyn_cast(op)) { + const auto kind = entanglerKindFor(ctrl); + return kind && spec.gates.contains(*kind); + } + if (llvm::isa(op)) { + return spec.gates.contains(NativeGateKind::RZZ); + } + auto unitary = llvm::dyn_cast(op); + if (!unitary || !unitary.isSingleQubit()) { + return false; + } + const auto gate = gateKindFor(unitary); + return gate && spec.gates.contains(*gate); +} + +} // namespace mlir::qco::decomposition diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp index 65a31991f2..8e4cc3327e 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -254,6 +254,8 @@ template return countOps(funcOp); case ZSXX: return countZSXXGates(funcOp); + case R: + return countOps(funcOp); } return 0; } @@ -537,6 +539,8 @@ TEST(EulerAnglesCoverageTest, Mod2PiPreservesNonFinitePhase) { return isa(op); case ZSXX: return isa(op); + case R: + return isa(op); } return false; } diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index ad960c4b3b..722f848ddf 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -8,12 +8,30 @@ * Licensed under the MIT License */ +#include "TestCaseUtils.h" +#include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/NativeProfile.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include #include #include @@ -27,8 +45,12 @@ #include #include +using namespace mlir; using namespace mlir::qco; using namespace mlir::qco::decomposition; +using namespace mqt::test; + +using QubitId = std::size_t; static constexpr Matrix4x4 TWO_QUBIT_CONTROLLED_X01 = Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, // @@ -42,6 +64,12 @@ static constexpr Matrix4x4 TWO_QUBIT_CONTROLLED_X10 = 0.0, 0.0, 1.0, 0.0, // 0.0, 1.0, 0.0, 0.0); +static constexpr Matrix4x4 TWO_QUBIT_CONTROLLED_Z = + Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, // + 0.0, 1.0, 0.0, 0.0, // + 0.0, 0.0, 1.0, 0.0, // + 0.0, 0.0, 0.0, -1.0); + template static bool isUnitaryMatrix(const MatrixT& matrix, const double tolerance = MATRIX_TOLERANCE) { @@ -406,3 +434,331 @@ INSTANTIATE_TEST_SUITE_P(ProductTwoQubitMatrices, BasisDecomposerTest, INSTANTIATE_TEST_SUITE_P(TwoQubitMatrices, BasisDecomposerTest, testing::Combine(cxBasisCases(), entangledMatrixCases())); + +static bool extractSingleQubitMatrix(UnitaryOpInterface op, Matrix2x2& out) { + if (op.getUnitaryMatrix2x2(out)) { + return true; + } + DynamicMatrix dynamic; + if (!op.getUnitaryMatrixDynamic(dynamic) || dynamic.rows() != 2 || + dynamic.cols() != 2) { + return false; + } + out = Matrix2x2::fromElements(dynamic(0, 0), dynamic(0, 1), dynamic(1, 0), + dynamic(1, 1)); + return true; +} + +static bool extractTwoQubitMatrix(UnitaryOpInterface op, Matrix4x4& out) { + return op.getUnitaryMatrix4x4(out); +} + +static std::optional +computeTwoQubitUnitaryFromFunc(func::FuncOp funcOp) { + Matrix4x4 unitary = Matrix4x4::identity(); + std::complex global{1.0, 0.0}; + llvm::DenseMap wireIds; + wireIds[funcOp.getArgument(0)] = 0; + wireIds[funcOp.getArgument(1)] = 1; + + auto wireId = [&](Value qubit) -> std::optional { + const auto it = wireIds.find(qubit); + if (it == wireIds.end()) { + return std::nullopt; + } + return it->second; + }; + + for (Operation& rawOp : funcOp.getBody().front()) { + if (llvm::isa(&rawOp)) { + continue; + } + if (auto gphase = llvm::dyn_cast(&rawOp)) { + if (const auto matrix = gphase.getUnitaryMatrix()) { + global *= (*matrix)(0, 0); + } + continue; + } + auto op = llvm::dyn_cast(&rawOp); + if (!op) { + continue; + } + + if (op.isSingleQubit()) { + const auto qIn = op->getOperand(0); + const auto qid = wireId(qIn); + if (!qid) { + return std::nullopt; + } + Matrix2x2 oneQ; + if (!extractSingleQubitMatrix(op, oneQ)) { + return std::nullopt; + } + unitary = oneQ.embedInTwoQubit(*qid) * unitary; + wireIds[op->getResult(0)] = *qid; + continue; + } + + if (op.isTwoQubit()) { + const auto q0In = op->getOperand(0); + const auto q1In = op->getOperand(1); + const auto q0id = wireId(q0In); + const auto q1id = wireId(q1In); + if (!q0id || !q1id) { + return std::nullopt; + } + Matrix4x4 twoQ; + if (!op.getUnitaryMatrix4x4(twoQ)) { + return std::nullopt; + } + unitary = twoQ.reorderForQubits(*q0id, *q1id) * unitary; + wireIds[op->getResult(0)] = *q0id; + wireIds[op->getResult(1)] = *q1id; + } + } + + return unitary * global; +} + +static func::FuncOp synthesize2QIntoFunc(MLIRContext* ctx, + const Matrix4x4& target, + const NativeProfileSpec& spec, + OwningOpRef& moduleOut) { + moduleOut = ModuleOp::create(UnknownLoc::get(ctx)); + OpBuilder builder(ctx); + builder.setInsertionPointToStart(moduleOut->getBody()); + + const auto qubitTy = QubitType::get(ctx); + const auto funcTy = + builder.getFunctionType({qubitTy, qubitTy}, {qubitTy, qubitTy}); + const Location loc = moduleOut->getLoc(); + auto func = func::FuncOp::create(builder, loc, "main", funcTy); + auto* entry = func.addEntryBlock(); + + builder.setInsertionPointToStart(entry); + Value out0; + Value out1; + if (failed(synthesizeUnitary2QWeyl(builder, loc, entry->getArgument(0), + entry->getArgument(1), target, spec, out0, + out1))) { + llvm::report_fatal_error( + "synthesizeUnitary2QWeyl failed during test synthesis"); + } + func::ReturnOp::create(builder, loc, ValueRange{out0, out1}); + return func; +} + +static void expectSynthesized2QMatrix(MLIRContext* ctx, const Matrix4x4& target, + const NativeProfileSpec& spec) { + OwningOpRef module; + const auto func = synthesize2QIntoFunc(ctx, target, spec, module); + ASSERT_TRUE(succeeded(verify(module.get()))); + const auto actual = computeTwoQubitUnitaryFromFunc(func); + ASSERT_TRUE(actual.has_value()); + EXPECT_TRUE(isEquivalentUpToGlobalPhase(*actual, target)); +} + +namespace { + +struct MlirTestContext { + std::unique_ptr context; + + void setUp() { + DialectRegistry registry; + registry.insert(); + context = std::make_unique(); + context->appendDialectRegistry(registry); + context->loadAllAvailableDialects(); + } + + [[nodiscard]] MLIRContext* ctx() const { return context.get(); } +}; + +struct WeylSynthesisCase { + const char* name; + const char* nativeGates; + Matrix4x4 (*target)(); +}; + +class WeylSynthesisTest : public testing::TestWithParam {}; + +} // namespace + +TEST_P(WeylSynthesisTest, PreservesTargetUnitary) { + MlirTestContext fx; + fx.setUp(); + const auto spec = parseNativeSpec(GetParam().nativeGates); + ASSERT_TRUE(spec); + expectSynthesized2QMatrix(fx.ctx(), GetParam().target(), *spec); +} + +INSTANTIATE_TEST_SUITE_P( + Profiles, WeylSynthesisTest, + testing::Values( + WeylSynthesisCase{"CxGeneric", "u,cx", + [] { return TWO_QUBIT_CONTROLLED_X01; }}, + WeylSynthesisCase{"ProductGeneric", "u,cx", + [] { + return Matrix4x4::kron(RZOp::unitaryMatrix(1.0), + RYOp::unitaryMatrix(0.3)); + }}, + WeylSynthesisCase{"IbmBasic", "x,sx,rz,cx", + [] { + return Matrix4x4::kron(HOp::getUnitaryMatrix(), + Matrix2x2::identity()) * + TWO_QUBIT_CONTROLLED_X01 * + Matrix4x4::kron(RZOp::unitaryMatrix(0.2), + RYOp::unitaryMatrix(0.1)); + }}, + WeylSynthesisCase{"CzGeneric", "u,cz", + [] { return TWO_QUBIT_CONTROLLED_Z; }}), + [](const testing::TestParamInfo& info) { + return info.param.name; + }); + +TEST(WeylSynthesisTest, IdentityRequiresNoEntanglers) { + const auto cxSpec = parseNativeSpec("u,cx"); + ASSERT_TRUE(cxSpec); + const auto cxCount = twoQubitEntanglerCount(Matrix4x4::identity(), *cxSpec); + ASSERT_TRUE(cxCount.has_value()); + EXPECT_EQ(*cxCount, 0U); + + const auto czSpec = parseNativeSpec("u,cz"); + ASSERT_TRUE(czSpec); + const auto czCount = twoQubitEntanglerCount(Matrix4x4::identity(), *czSpec); + ASSERT_TRUE(czCount.has_value()); + EXPECT_EQ(*czCount, 0U); +} + +TEST(WeylSynthesisTest, FailsWithoutEntanglerInSpec) { + MlirTestContext fx; + fx.setUp(); + const auto spec = parseNativeSpec("u"); + ASSERT_TRUE(spec); + EXPECT_FALSE(spec->gates.contains(NativeGateKind::CX)); + EXPECT_FALSE(spec->gates.contains(NativeGateKind::CZ)); + OpBuilder builder(fx.ctx()); + const auto qubitTy = QubitType::get(fx.ctx()); + const auto funcTy = + builder.getFunctionType({qubitTy, qubitTy}, {qubitTy, qubitTy}); + auto func = + func::FuncOp::create(builder, UnknownLoc::get(fx.ctx()), "main", funcTy); + auto* entry = func.addEntryBlock(); + builder.setInsertionPointToStart(entry); + Value out0; + Value out1; + EXPECT_TRUE(failed(synthesizeUnitary2QWeyl( + builder, func.getLoc(), entry->getArgument(0), entry->getArgument(1), + TWO_QUBIT_CONTROLLED_X01, *spec, out0, out1))); +} + +TEST(NativeSpecTest, ParsesAndRejectsMenus) { + const auto ibm = parseNativeSpec("x,sx,rz,cx"); + ASSERT_TRUE(ibm); + EXPECT_TRUE(ibm->gates.contains(NativeGateKind::CX)); + EXPECT_TRUE(ibm->gates.contains(NativeGateKind::X)); + EXPECT_FALSE(ibm->gates.contains(NativeGateKind::RZZ)); + EXPECT_FALSE(parseNativeSpec("x,sx,rz,not-a-gate").has_value()); + + const auto pMenu = parseNativeSpec("x,sx,p,cx"); + const auto rzMenu = parseNativeSpec("x,sx,rz,cx"); + ASSERT_TRUE(pMenu); + ASSERT_TRUE(rzMenu); + EXPECT_EQ(pMenu->gates, rzMenu->gates); + + const auto cxOnly = parseNativeSpec("u,cx"); + ASSERT_TRUE(cxOnly); + EXPECT_TRUE(cxOnly->gates.contains(NativeGateKind::CX)); + EXPECT_FALSE(cxOnly->gates.contains(NativeGateKind::CZ)); + + const auto both = parseNativeSpec("u,cx,cz"); + ASSERT_TRUE(both); + EXPECT_TRUE(both->gates.contains(NativeGateKind::CX)); + EXPECT_TRUE(both->gates.contains(NativeGateKind::CZ)); + + const auto generic = parseNativeSpec("u,cx"); + ASSERT_TRUE(generic); + EXPECT_TRUE(generic->gates.contains(NativeGateKind::U)); + EXPECT_FALSE(generic->gates.contains(NativeGateKind::X)); +} + +TEST(NativeSpecTest, ResolvesEulerBasisFromMenu) { + const auto uMenu = parseNativeSpec("u,cx"); + ASSERT_TRUE(uMenu); + EXPECT_EQ(uMenu->eulerBasis(), EulerBasis::U); + + const auto zsxx = parseNativeSpec("x,sx,rz,cx"); + ASSERT_TRUE(zsxx); + EXPECT_EQ(zsxx->eulerBasis(), EulerBasis::ZSXX); + + const auto rMenu = parseNativeSpec("r,cz"); + ASSERT_TRUE(rMenu); + EXPECT_EQ(rMenu->eulerBasis(), EulerBasis::R); + + const auto xzx = parseNativeSpec("rx,rz,cz"); + ASSERT_TRUE(xzx); + EXPECT_EQ(xzx->eulerBasis(), EulerBasis::XZX); + + const auto xyx = parseNativeSpec("rx,ry,cz"); + ASSERT_TRUE(xyx); + EXPECT_EQ(xyx->eulerBasis(), EulerBasis::XYX); + + const auto zyz = parseNativeSpec("ry,rz,cz"); + ASSERT_TRUE(zyz); + EXPECT_EQ(zyz->eulerBasis(), EulerBasis::ZYZ); +} + +TEST(NativeSpecTest, AllowsOpMatchesMenu) { + MlirTestContext fx; + fx.setUp(); + const auto spec = parseNativeSpec("u,cx,rzz"); + ASSERT_TRUE(spec); + + OpBuilder builder(fx.ctx()); + const Location loc = UnknownLoc::get(fx.ctx()); + const auto qubitTy = QubitType::get(fx.ctx()); + const auto funcTy = + builder.getFunctionType({qubitTy, qubitTy}, {qubitTy, qubitTy}); + auto func = func::FuncOp::create(builder, loc, "allows_op", funcTy); + auto* entry = func.addEntryBlock(); + builder.setInsertionPointToStart(entry); + Value q0 = entry->getArgument(0); + Value q1 = entry->getArgument(1); + + EXPECT_TRUE(allowsOp( + BarrierOp::create(builder, loc, ValueRange{q0, q1}).getOperation(), + *spec)); + EXPECT_TRUE( + allowsOp(GPhaseOp::create(builder, loc, 0.1).getOperation(), *spec)); + EXPECT_TRUE(allowsOp( + UOp::create(builder, loc, q0, 0.1, 0.2, 0.3).getOperation(), *spec)); + EXPECT_TRUE( + allowsOp(RZZOp::create(builder, loc, q0, q1, 0.4).getOperation(), *spec)); + + auto cx = CtrlOp::create( + builder, loc, ValueRange{q0}, ValueRange{q1}, + [&](ValueRange targets) -> SmallVector { + return {XOp::create(builder, loc, targets[0]).getOutputQubit(0)}; + }); + EXPECT_TRUE(allowsOp(cx.getOperation(), *spec)); + + auto cxWithInterleavedH = CtrlOp::create( + builder, loc, ValueRange{q0}, ValueRange{q1}, + [&](ValueRange targets) -> SmallVector { + auto wire = XOp::create(builder, loc, targets[0]).getOutputQubit(0); + return {HOp::create(builder, loc, wire).getOutputQubit(0)}; + }); + EXPECT_FALSE(allowsOp(cxWithInterleavedH.getOperation(), *spec)); + + EXPECT_FALSE(allowsOp(XOp::create(builder, loc, q0).getOperation(), *spec)); + + const auto czSpec = parseNativeSpec("u,cz"); + ASSERT_TRUE(czSpec); + auto cz = CtrlOp::create( + builder, loc, ValueRange{q0}, ValueRange{q1}, + [&](ValueRange targets) -> SmallVector { + return {ZOp::create(builder, loc, targets[0]).getOutputQubit(0)}; + }); + EXPECT_TRUE(allowsOp(cz.getOperation(), *czSpec)); + EXPECT_FALSE(allowsOp(cx.getOperation(), *czSpec)); +} diff --git a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp index 76e50a997d..60a1be555e 100644 --- a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp +++ b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp @@ -595,3 +595,11 @@ TEST(SymmetricEigensolver, HandlesDegenerateSpectrum) { const Matrix4x4& v = result.eigenvectors; EXPECT_TRUE((v.transpose() * v).isIdentity()); } + +TEST(GlobalPhaseEquivalence, IsEquivalentUpToGlobalPhase) { + const Matrix2x2 pauliX = Matrix2x2::fromElements(0, 1, 1, 0); + const Complex phase{0.0, 1.0}; + EXPECT_TRUE(isEquivalentUpToGlobalPhase(pauliX, phase * pauliX)); + EXPECT_TRUE(isEquivalentUpToGlobalPhase(pauliX, pauliX)); + EXPECT_FALSE(isEquivalentUpToGlobalPhase(pauliX, Matrix2x2::identity())); +} From 3d5eafb021f98d7ddea33d2850e726595d870724 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 1 Jul 2026 09:45:14 +0200 Subject: [PATCH 091/122] =?UTF-8?q?=F0=9F=94=A5=20Remove=20isEquivalentUpT?= =?UTF-8?q?oGlobalPhase=20function=20and=20update=20related=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/include/mlir/Dialect/QCO/Utils/Matrix.h | 27 ------------------- .../Decomposition/test_weyl_decomposition.cpp | 2 +- .../Dialect/QCO/Utils/test_unitary_matrix.cpp | 8 ------ 3 files changed, 1 insertion(+), 36 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h index 8766637c9b..f0d269de45 100644 --- a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h +++ b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h @@ -777,33 +777,6 @@ inline constexpr bool std::is_same, std::is_same>; -/** - * @brief Checks whether two unitaries are equal up to a global phase. - * - * Uses `trace(rhs.adjoint() * lhs)` to infer a unit-modulus phase factor, then - * compares `lhs` to `factor * rhs` with @ref isApprox. A near-zero overlap - * (`|trace| <= tol`) is treated as not equivalent to avoid dividing by a tiny - * number. - * - * @tparam Matrix Any supported fixed- or dynamic-size matrix type. - * @param lhs Left-hand unitary. - * @param rhs Right-hand unitary. - * @param tol Absolute tolerance for overlap and entry-wise comparison. - * @return True when @p lhs and @p rhs differ only by a global phase. - */ -template - requires is_supported_matrix_v -[[nodiscard]] bool isEquivalentUpToGlobalPhase(const Matrix& lhs, - const Matrix& rhs, - double tol = MATRIX_TOLERANCE) { - const auto overlap = (rhs.adjoint() * lhs).trace(); - if (std::abs(overlap) <= tol) { - return false; - } - const auto factor = overlap / std::abs(overlap); - return lhs.isApprox(factor * rhs, tol); -} - /// Scalar-on-the-left multiply `scalar * matrix` (commutes with the member /// `matrix * scalar`). Provided so generic code can scale a matrix from /// either side. diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index 722f848ddf..23a235d059 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -555,7 +555,7 @@ static void expectSynthesized2QMatrix(MLIRContext* ctx, const Matrix4x4& target, ASSERT_TRUE(succeeded(verify(module.get()))); const auto actual = computeTwoQubitUnitaryFromFunc(func); ASSERT_TRUE(actual.has_value()); - EXPECT_TRUE(isEquivalentUpToGlobalPhase(*actual, target)); + EXPECT_TRUE(actual->isApprox(target, WEYL_TOLERANCE)); } namespace { diff --git a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp index 60a1be555e..76e50a997d 100644 --- a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp +++ b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp @@ -595,11 +595,3 @@ TEST(SymmetricEigensolver, HandlesDegenerateSpectrum) { const Matrix4x4& v = result.eigenvectors; EXPECT_TRUE((v.transpose() * v).isIdentity()); } - -TEST(GlobalPhaseEquivalence, IsEquivalentUpToGlobalPhase) { - const Matrix2x2 pauliX = Matrix2x2::fromElements(0, 1, 1, 0); - const Complex phase{0.0, 1.0}; - EXPECT_TRUE(isEquivalentUpToGlobalPhase(pauliX, phase * pauliX)); - EXPECT_TRUE(isEquivalentUpToGlobalPhase(pauliX, pauliX)); - EXPECT_FALSE(isEquivalentUpToGlobalPhase(pauliX, Matrix2x2::identity())); -} From f0eaa65e803f6396f8bf98901955e2531ac7a80d Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 1 Jul 2026 11:06:20 +0300 Subject: [PATCH 092/122] =?UTF-8?q?=E2=9C=A8=20Move=20emitGPhaseIfNeeded?= =?UTF-8?q?=20function=20to=20handle=20global=20phase=20emission?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mlir/Dialect/QCO/Transforms/Decomposition/Euler.h | 10 ++++++++++ .../lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp | 2 +- .../QCO/Transforms/Decomposition/NativeProfile.cpp | 10 +--------- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h index 5d02898c3b..4130967cde 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h @@ -89,4 +89,14 @@ synthesizeUnitary1QEuler(OpBuilder& builder, Location loc, Value qubit, const Matrix2x2& composed, std::size_t runSize, bool hasNonBasisGate, EulerBasis basis); +/** + * @brief Emits `qco.gphase` when @p phase is outside tolerance (after + * `mod2pi`). + * + * @param builder Builder for the operation. + * @param loc Location of the operation. + * @param phase Global phase in radians. + */ +void emitGPhaseIfNeeded(OpBuilder& builder, Location loc, double phase); + } // namespace mlir::qco::decomposition diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index 55576cd342..757719f6c4 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -91,7 +91,7 @@ namespace mlir::qco::decomposition { * @param loc Location of the operation. * @param phase Global phase in radians. */ -static void emitGPhaseIfNeeded(OpBuilder& builder, Location loc, double phase) { +void emitGPhaseIfNeeded(OpBuilder& builder, Location loc, const double phase) { if (isNearZeroRotationAngle(mod2pi(phase))) { return; } diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp index fe5caf4162..2d9c8223ec 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp @@ -147,14 +147,6 @@ decomposeForProfile(const Matrix4x4& target, const NativeProfileSpec& spec) { return cachedBasisDecomposer(*entangler).decomposeTarget(target); } -static void emitGPhaseIfNonTrivial(OpBuilder& builder, Location loc, - double phase) { - constexpr double epsilon = 1e-12; - if (std::abs(phase) > epsilon) { - GPhaseOp::create(builder, loc, phase); - } -} - static Value emitSingleQubitMatrix(OpBuilder& builder, Location loc, Value inQubit, const Matrix2x2& matrix, EulerBasis basis) { @@ -255,7 +247,7 @@ LogicalResult synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, } const auto basis = spec.eulerBasis(); - emitGPhaseIfNonTrivial(builder, loc, native->globalPhase); + emitGPhaseIfNeeded(builder, loc, native->globalPhase); Value wire0 = qubit0; Value wire1 = qubit1; From 537787e3a3a241dc17a817985a88fa4c7e9d63b7 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 1 Jul 2026 12:20:12 +0300 Subject: [PATCH 093/122] =?UTF-8?q?=E2=9C=A8=20Enhance=20QCO=20transformat?= =?UTF-8?q?ions=20with=20Euler=20basis=20support=20and=20code=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Transforms/Decomposition/NativeProfile.h | 18 +- .../mlir/Dialect/QCO/Transforms/Passes.td | 5 +- mlir/include/mlir/Dialect/QCO/Utils/Matrix.h | 1 - .../QCO/Transforms/Decomposition/Euler.cpp | 7 - .../Decomposition/NativeProfile.cpp | 219 ++++++++---------- .../FuseSingleQubitUnitaryRuns.cpp | 4 +- .../test_euler_decomposition.cpp | 5 +- .../Decomposition/test_weyl_decomposition.cpp | 30 ++- 8 files changed, 129 insertions(+), 160 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeProfile.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeProfile.h index cc07a03966..3e9cc47767 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeProfile.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeProfile.h @@ -46,12 +46,19 @@ enum class NativeGateKind : std::uint8_t { * @brief Resolved native-gate menu for two-qubit Weyl synthesis. * * @p gates is the parsed menu. Euler decomposition and entangler choice are - * derived from it with fixed priority (see @ref NativeProfileSpec::eulerBasis). + * derived from it with fixed priority (see @ref NativeProfileSpec::eulerBasis + * and @ref parseNativeSpec). Menus must include a supported single-qubit + * strategy and at least one of `cx` or `cz`; when both are present, `cx` is + * used for synthesis. */ struct NativeProfileSpec { llvm::DenseSet gates; - /** @brief Preferred single-qubit Euler basis for synthesis in this menu. */ + /** + * @brief Preferred single-qubit Euler basis for synthesis in this menu. + * + * Only valid for specs returned by @ref parseNativeSpec. + */ [[nodiscard]] EulerBasis eulerBasis() const; }; @@ -77,9 +84,10 @@ twoQubitEntanglerCount(const Matrix4x4& target, const NativeProfileSpec& spec); /** * @brief Returns true when @p op is already on the resolved native menu. * - * Barriers and global phase are always allowed. Single-qubit primitives, - * single-target `CtrlOp` shells (`X`/`Z` bodies), and `RZZ` are checked - * against @p spec.gates. + * Barriers and global phase are always allowed. Single-qubit primitives and + * single-target `CtrlOp` shells (`X`/`Z` bodies) are checked against + * @p spec.gates. `RZZ` is allowed when listed, but two-qubit synthesis uses + * only `cx` or `cz` entanglers. */ [[nodiscard]] bool allowsOp(Operation* op, const NativeProfileSpec& spec); diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index fafb906a77..971dc18f71 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -62,8 +62,9 @@ def FuseSingleQubitUnitaryRuns Currently, only operations whose unitary matrix can be obtained at compile time are fused. }]; - let options = [Option<"basis", "basis", "std::string", "\"zyz\"", - "Target Euler basis (zyz, zxz, xzx, xyx, u, zsxx).">]; + let options = [Option< + "basis", "basis", "std::string", "\"zyz\"", + "Target Euler basis (zyz, zxz, xzx, xyx, u, zsxx, r).">]; } def QuantumLoopUnroll diff --git a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h index f0d269de45..83f3b3d66d 100644 --- a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h +++ b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h @@ -14,7 +14,6 @@ #include #include -#include #include #include #include diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index 757719f6c4..7c9d470845 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -84,13 +84,6 @@ namespace mlir::qco::decomposition { return std::abs(angle) <= utils::TOLERANCE; } -/** - * @brief Emits `qco.gphase` when `phase` is outside tolerance. - * - * @param builder Builder for the operation. - * @param loc Location of the operation. - * @param phase Global phase in radians. - */ void emitGPhaseIfNeeded(OpBuilder& builder, Location loc, const double phase) { if (isNearZeroRotationAngle(mod2pi(phase))) { return; diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp index 2d9c8223ec..3ef3a2ebba 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp @@ -17,10 +17,8 @@ #include "mlir/Dialect/QCO/Utils/Matrix.h" #include "mlir/Dialect/Utils/Utils.h" -#include -#include #include -#include +#include #include #include #include @@ -28,30 +26,22 @@ #include #include -#include #include #include #include #include -using mlir::qco::Matrix2x2; -using mlir::qco::Matrix4x4; - namespace { -const Matrix4x4& canonicalControlledX() { - static const Matrix4x4 matrix = - Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 1.0, 0.0, 0.0, 1.0, 0.0); - return matrix; -} +using mlir::qco::Matrix4x4; -const Matrix4x4& canonicalControlledZ() { - static const Matrix4x4 matrix = - Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, - 1.0, 0.0, 0.0, 0.0, 0.0, -1.0); - return matrix; -} +constexpr Matrix4x4 kCanonicalControlledX = + Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, 1.0, 0.0); + +constexpr Matrix4x4 kCanonicalControlledZ = + Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, + 1.0, 0.0, 0.0, 0.0, 0.0, -1.0); } // namespace @@ -91,30 +81,48 @@ parseGateSet(llvm::StringRef nativeGates) { return gates; } -static bool -hasSingleQubitStrategy(const llvm::DenseSet& gates) { +/** + * @brief Resolves the preferred single-qubit Euler basis for a parsed menu. + * + * Returns `std::nullopt` when no supported single-qubit synthesis strategy is + * present. Priority matches @ref NativeProfileSpec::eulerBasis. + */ +[[nodiscard]] static std::optional +resolveEulerBasis(const llvm::DenseSet& gates) { const auto has = [&](NativeGateKind kind) { return gates.contains(kind); }; if (has(NativeGateKind::U)) { - return true; + return EulerBasis::U; } if (has(NativeGateKind::X) && has(NativeGateKind::SX) && has(NativeGateKind::RZ)) { - return true; + return EulerBasis::ZSXX; } if (has(NativeGateKind::R)) { - return true; + return EulerBasis::R; + } + if (has(NativeGateKind::RX) && has(NativeGateKind::RZ)) { + return EulerBasis::XZX; + } + if (has(NativeGateKind::RX) && has(NativeGateKind::RY)) { + return EulerBasis::XYX; + } + if (has(NativeGateKind::RY) && has(NativeGateKind::RZ)) { + return EulerBasis::ZYZ; } - return (has(NativeGateKind::RX) && has(NativeGateKind::RZ)) || - (has(NativeGateKind::RX) && has(NativeGateKind::RY)) || - (has(NativeGateKind::RY) && has(NativeGateKind::RZ)); + return std::nullopt; } -static std::optional -selectEntangler(const NativeProfileSpec& spec) { - if (spec.gates.contains(NativeGateKind::CX)) { +/** + * @brief Picks the two-qubit entangler for Weyl synthesis. + * + * When both `cx` and `cz` appear in the menu, `cx` is preferred. + */ +[[nodiscard]] static std::optional +selectEntangler(const llvm::DenseSet& gates) { + if (gates.contains(NativeGateKind::CX)) { return NativeGateKind::CX; } - if (spec.gates.contains(NativeGateKind::CZ)) { + if (gates.contains(NativeGateKind::CZ)) { return NativeGateKind::CZ; } return std::nullopt; @@ -125,12 +133,12 @@ cachedBasisDecomposer(NativeGateKind entangler) { switch (entangler) { case NativeGateKind::CX: { static const TwoQubitBasisDecomposer DECOMPOSER = - TwoQubitBasisDecomposer::create(canonicalControlledX(), 1.0); + TwoQubitBasisDecomposer::create(kCanonicalControlledX, 1.0); return DECOMPOSER; } case NativeGateKind::CZ: { static const TwoQubitBasisDecomposer DECOMPOSER = - TwoQubitBasisDecomposer::create(canonicalControlledZ(), 1.0); + TwoQubitBasisDecomposer::create(kCanonicalControlledZ, 1.0); return DECOMPOSER; } default: @@ -138,47 +146,17 @@ cachedBasisDecomposer(NativeGateKind entangler) { } } -static std::optional -decomposeForProfile(const Matrix4x4& target, const NativeProfileSpec& spec) { - const auto entangler = selectEntangler(spec); - if (!entangler) { - return std::nullopt; - } - return cachedBasisDecomposer(*entangler).decomposeTarget(target); -} - -static Value emitSingleQubitMatrix(OpBuilder& builder, Location loc, - Value inQubit, const Matrix2x2& matrix, - EulerBasis basis) { - return *synthesizeUnitary1QEuler(builder, loc, inQubit, matrix, - /*runSize=*/0, /*hasNonBasisGate=*/true, - basis); -} - static std::optional gateKindFor(UnitaryOpInterface op) { - Operation* raw = op.getOperation(); - if (llvm::isa(raw)) { - return NativeGateKind::U; - } - if (llvm::isa(raw)) { - return NativeGateKind::X; - } - if (llvm::isa(raw)) { - return NativeGateKind::SX; - } - if (llvm::isa(raw)) { - return NativeGateKind::RZ; - } - if (llvm::isa(raw)) { - return NativeGateKind::RX; - } - if (llvm::isa(raw)) { - return NativeGateKind::RY; - } - if (llvm::isa(raw)) { - return NativeGateKind::R; - } - return std::nullopt; + return TypeSwitch>( + op.getOperation()) + .Case([](UOp) { return NativeGateKind::U; }) + .Case([](XOp) { return NativeGateKind::X; }) + .Case([](SXOp) { return NativeGateKind::SX; }) + .Case([](auto) { return NativeGateKind::RZ; }) + .Case([](RXOp) { return NativeGateKind::RX; }) + .Case([](RYOp) { return NativeGateKind::RY; }) + .Case([](ROp) { return NativeGateKind::R; }) + .Default([](Operation*) { return std::nullopt; }); } static std::optional entanglerKindFor(CtrlOp ctrl) { @@ -190,46 +168,24 @@ static std::optional entanglerKindFor(CtrlOp ctrl) { if (!bodyUnitary) { return std::nullopt; } - Operation* body = bodyUnitary.getOperation(); - if (llvm::isa(body)) { - return NativeGateKind::CX; - } - if (llvm::isa(body)) { - return NativeGateKind::CZ; - } - return std::nullopt; + return TypeSwitch>( + bodyUnitary.getOperation()) + .Case([](XOp) { return NativeGateKind::CX; }) + .Case([](ZOp) { return NativeGateKind::CZ; }) + .Default([](Operation*) { return std::nullopt; }); } EulerBasis NativeProfileSpec::eulerBasis() const { - const auto has = [&](NativeGateKind kind) { return gates.contains(kind); }; - if (has(NativeGateKind::U)) { - return EulerBasis::U; - } - if (has(NativeGateKind::X) && has(NativeGateKind::SX) && - has(NativeGateKind::RZ)) { - return EulerBasis::ZSXX; - } - if (has(NativeGateKind::R)) { - return EulerBasis::R; - } - if (has(NativeGateKind::RX) && has(NativeGateKind::RZ)) { - return EulerBasis::XZX; - } - if (has(NativeGateKind::RX) && has(NativeGateKind::RY)) { - return EulerBasis::XYX; - } - if (has(NativeGateKind::RY) && has(NativeGateKind::RZ)) { - return EulerBasis::ZYZ; - } - llvm_unreachable("parseNativeSpec guarantees a synthesizable basis"); + // Valid only for specs returned by @ref parseNativeSpec. + return *resolveEulerBasis(gates); } std::optional parseNativeSpec(llvm::StringRef nativeGates) { auto gates = parseGateSet(nativeGates); - if (!gates || gates->empty() || !hasSingleQubitStrategy(*gates)) { + if (!gates || !resolveEulerBasis(*gates) || !selectEntangler(*gates)) { return std::nullopt; } - return NativeProfileSpec{.gates = std::move(gates).value()}; + return NativeProfileSpec{.gates = std::move(*gates)}; } LogicalResult synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, @@ -237,15 +193,14 @@ LogicalResult synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, const Matrix4x4& target, const NativeProfileSpec& spec, Value& outQubit0, Value& outQubit1) { - const auto entangler = selectEntangler(spec); + const auto entangler = selectEntangler(spec.gates); if (!entangler) { return failure(); } - const auto native = decomposeForProfile(target, spec); + const auto native = cachedBasisDecomposer(*entangler).decomposeTarget(target); if (!native) { return failure(); } - const auto basis = spec.eulerBasis(); emitGPhaseIfNeeded(builder, loc, native->globalPhase); @@ -253,14 +208,22 @@ LogicalResult synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, Value wire1 = qubit1; const auto& factors = native->singleQubitFactors; const std::uint8_t numBasisUses = native->numBasisUses; + const EulerBasis basis = spec.eulerBasis(); + const bool emitCz = (*entangler == NativeGateKind::CZ); const auto emitFactor = [&](Value& wire, std::size_t index) { - wire = emitSingleQubitMatrix(builder, loc, wire, factors[index], basis); + const auto synthesized = synthesizeUnitary1QEuler( + builder, loc, wire, factors[index], /*runSize=*/0, + /*hasNonBasisGate=*/true, basis); + if (!synthesized) { + llvm_unreachable("forced full synthesis must succeed"); + } + wire = *synthesized; }; const auto emitEntangler = [&]() { auto ctrlOp = CtrlOp::create( builder, loc, ValueRange{wire0}, ValueRange{wire1}, [&](ValueRange targetArgs) -> SmallVector { - if (*entangler == NativeGateKind::CZ) { + if (emitCz) { return {ZOp::create(builder, loc, targetArgs[0]).getOutputQubit(0)}; } return {XOp::create(builder, loc, targetArgs[0]).getOutputQubit(0)}; @@ -284,7 +247,11 @@ LogicalResult synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, std::optional twoQubitEntanglerCount(const Matrix4x4& target, const NativeProfileSpec& spec) { - const auto native = decomposeForProfile(target, spec); + const auto entangler = selectEntangler(spec.gates); + if (!entangler) { + return std::nullopt; + } + const auto native = cachedBasisDecomposer(*entangler).decomposeTarget(target); if (!native) { return std::nullopt; } @@ -292,22 +259,22 @@ twoQubitEntanglerCount(const Matrix4x4& target, const NativeProfileSpec& spec) { } bool allowsOp(Operation* op, const NativeProfileSpec& spec) { - if (llvm::isa(op)) { - return true; - } - if (auto ctrl = llvm::dyn_cast(op)) { - const auto kind = entanglerKindFor(ctrl); - return kind && spec.gates.contains(*kind); - } - if (llvm::isa(op)) { - return spec.gates.contains(NativeGateKind::RZZ); - } - auto unitary = llvm::dyn_cast(op); - if (!unitary || !unitary.isSingleQubit()) { - return false; - } - const auto gate = gateKindFor(unitary); - return gate && spec.gates.contains(*gate); + return TypeSwitch(op) + .Case([](auto) { return true; }) + .Case([&](CtrlOp ctrl) { + const auto kind = entanglerKindFor(ctrl); + return kind && spec.gates.contains(*kind); + }) + .Case( + [&](RZZOp) { return spec.gates.contains(NativeGateKind::RZZ); }) + .Case([&](UnitaryOpInterface unitary) { + if (!unitary.isSingleQubit()) { + return false; + } + const auto gate = gateKindFor(unitary); + return gate && spec.gates.contains(*gate); + }) + .Default([](Operation*) { return false; }); } } // namespace mlir::qco::decomposition diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp index 9a91146384..73fe887dfb 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp @@ -80,6 +80,7 @@ static bool isTargetBasisGate(Operation* op, }) .Case([&](auto) { return basis == EulerBasis::U; }) .Case([&](auto) { return basis == EulerBasis::ZSXX; }) + .Case([&](auto) { return basis == EulerBasis::R; }) .Default([](auto) { return false; }); } @@ -200,7 +201,8 @@ struct FuseSingleQubitUnitaryRunsPass final const auto parsed = decomposition::parseEulerBasis(basis); if (!parsed) { module.emitError() << "Invalid Euler basis '" << basis - << "'. Expected one of: zyz, zxz, xzx, xyx, u, zsxx."; + << "'. Expected one of: zyz, zxz, xzx, xyx, u, zsxx, " + "r."; signalPassFailure(); return; } diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp index 8e4cc3327e..b637abd290 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -134,12 +134,13 @@ template } template static void forEachBasis(Fn fn) { - const std::array bases = {"zyz", "zxz", "xzx", - "xyx", "u", "zsxx"}; + const std::array bases = {"zyz", "zxz", "xzx", "xyx", + "u", "zsxx", "r"}; for (const char* basis : bases) { fn(StringRef{basis}); } } + [[nodiscard]] static WalkResult failMissingUnitaryMatrix(Operation* op, bool& failed) { ADD_FAILURE() << "Expected constant unitary matrix for op: " diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index 23a235d059..ea22c23228 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -20,7 +20,6 @@ #include #include #include -#include #include #include #include @@ -449,10 +448,6 @@ static bool extractSingleQubitMatrix(UnitaryOpInterface op, Matrix2x2& out) { return true; } -static bool extractTwoQubitMatrix(UnitaryOpInterface op, Matrix4x4& out) { - return op.getUnitaryMatrix4x4(out); -} - static std::optional computeTwoQubitUnitaryFromFunc(func::FuncOp funcOp) { Matrix4x4 unitary = Matrix4x4::identity(); @@ -538,11 +533,12 @@ static func::FuncOp synthesize2QIntoFunc(MLIRContext* ctx, builder.setInsertionPointToStart(entry); Value out0; Value out1; - if (failed(synthesizeUnitary2QWeyl(builder, loc, entry->getArgument(0), - entry->getArgument(1), target, spec, out0, - out1))) { - llvm::report_fatal_error( - "synthesizeUnitary2QWeyl failed during test synthesis"); + const auto synthResult = + synthesizeUnitary2QWeyl(builder, loc, entry->getArgument(0), + entry->getArgument(1), target, spec, out0, out1); + if (failed(synthResult)) { + ADD_FAILURE() << "synthesizeUnitary2QWeyl failed during test synthesis"; + return func; } func::ReturnOp::create(builder, loc, ValueRange{out0, out1}); return func; @@ -630,13 +626,14 @@ TEST(WeylSynthesisTest, IdentityRequiresNoEntanglers) { EXPECT_EQ(*czCount, 0U); } -TEST(WeylSynthesisTest, FailsWithoutEntanglerInSpec) { +TEST(WeylSynthesisTest, RejectsMenuWithoutEntangler) { + EXPECT_FALSE(parseNativeSpec("u").has_value()); +} + +TEST(WeylSynthesisTest, SynthesisFailsWithoutEntangler) { MlirTestContext fx; fx.setUp(); - const auto spec = parseNativeSpec("u"); - ASSERT_TRUE(spec); - EXPECT_FALSE(spec->gates.contains(NativeGateKind::CX)); - EXPECT_FALSE(spec->gates.contains(NativeGateKind::CZ)); + const NativeProfileSpec spec{.gates = {NativeGateKind::U}}; OpBuilder builder(fx.ctx()); const auto qubitTy = QubitType::get(fx.ctx()); const auto funcTy = @@ -649,7 +646,7 @@ TEST(WeylSynthesisTest, FailsWithoutEntanglerInSpec) { Value out1; EXPECT_TRUE(failed(synthesizeUnitary2QWeyl( builder, func.getLoc(), entry->getArgument(0), entry->getArgument(1), - TWO_QUBIT_CONTROLLED_X01, *spec, out0, out1))); + TWO_QUBIT_CONTROLLED_X01, spec, out0, out1))); } TEST(NativeSpecTest, ParsesAndRejectsMenus) { @@ -659,6 +656,7 @@ TEST(NativeSpecTest, ParsesAndRejectsMenus) { EXPECT_TRUE(ibm->gates.contains(NativeGateKind::X)); EXPECT_FALSE(ibm->gates.contains(NativeGateKind::RZZ)); EXPECT_FALSE(parseNativeSpec("x,sx,rz,not-a-gate").has_value()); + EXPECT_FALSE(parseNativeSpec("u").has_value()); const auto pMenu = parseNativeSpec("x,sx,p,cx"); const auto rzMenu = parseNativeSpec("x,sx,rz,cx"); From 170e4ab60606544cf795704c1a40c8e91261cc52 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 1 Jul 2026 12:41:23 +0300 Subject: [PATCH 094/122] =?UTF-8?q?=F0=9F=94=A7=20Revert=20header=20file?= =?UTF-8?q?=20collection=20in=20QCO=20transforms=20CMakeLists.txt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt index 77d847d3ba..77f594db17 100644 --- a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt +++ b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt @@ -24,11 +24,11 @@ add_mlir_library( mqt_mlir_target_use_project_options(MLIRQCOTransforms) -# collect header files (subdirs: Decomposition/, …) -file(GLOB_RECURSE PASSES_HEADERS_SOURCE CONFIGURE_DEPENDS - "${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Dialect/QCO/Transforms/*.h") -file(GLOB_RECURSE PASSES_HEADERS_BUILD CONFIGURE_DEPENDS - "${MQT_MLIR_BUILD_INCLUDE_DIR}/mlir/Dialect/QCO/Transforms/*.inc") +# collect header files +file(GLOB_RECURSE PASSES_HEADERS_SOURCE + ${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Dialect/QCO/Transforms/*.h) +file(GLOB_RECURSE PASSES_HEADERS_BUILD + ${MQT_MLIR_BUILD_INCLUDE_DIR}/mlir/Dialect/QCO/Transforms/*.inc) # add public headers using file sets target_sources( From f1a2a7661f5aa9e8105c9f2b001bc53db14a62a3 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 1 Jul 2026 13:50:50 +0300 Subject: [PATCH 095/122] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20linter=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp | 3 --- .../QCO/Transforms/Decomposition/NativeProfile.cpp | 8 ++++---- .../Transforms/Decomposition/test_weyl_decomposition.cpp | 2 -- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index 7c9d470845..484c55998d 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -186,8 +186,6 @@ EulerAngles anglesFromUnitary(const Matrix2x2& matrix, const EulerBasis basis) { return paramsXZX(matrix); case EulerBasis::XYX: case EulerBasis::R: - // The `R` basis reuses the X-Y-X angles and lowers `Rx`/`Ry` to the native - // `R(theta, phi)` gate (`Rx(a) == R(a, 0)`, `Ry(a) == R(a, pi/2)`). return paramsXYX(matrix); case EulerBasis::U: return paramsU(matrix); @@ -314,7 +312,6 @@ struct Unitary1QEulerPlan { phase = angles.phase; break; case EulerBasis::R: - // X-Y-X with `Rx(a) == R(a, 0)` and `Ry(a) == R(a, pi/2)`. appendRStep(angles.lambda, 0.0); steps.emplace_back(SynthesisStep::Kind::R, angles.theta, std::numbers::pi / 2.0); diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp index 3ef3a2ebba..fa8b19c364 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp @@ -35,11 +35,11 @@ namespace { using mlir::qco::Matrix4x4; -constexpr Matrix4x4 kCanonicalControlledX = +constexpr Matrix4x4 CANONICAL_CONTROLLED_X = Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0); -constexpr Matrix4x4 kCanonicalControlledZ = +constexpr Matrix4x4 CANONICAL_CONTROLLED_Z = Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, -1.0); @@ -133,12 +133,12 @@ cachedBasisDecomposer(NativeGateKind entangler) { switch (entangler) { case NativeGateKind::CX: { static const TwoQubitBasisDecomposer DECOMPOSER = - TwoQubitBasisDecomposer::create(kCanonicalControlledX, 1.0); + TwoQubitBasisDecomposer::create(CANONICAL_CONTROLLED_X, 1.0); return DECOMPOSER; } case NativeGateKind::CZ: { static const TwoQubitBasisDecomposer DECOMPOSER = - TwoQubitBasisDecomposer::create(kCanonicalControlledZ, 1.0); + TwoQubitBasisDecomposer::create(CANONICAL_CONTROLLED_Z, 1.0); return DECOMPOSER; } default: diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index ea22c23228..9a08bb45dc 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -8,7 +8,6 @@ * Licensed under the MIT License */ -#include "TestCaseUtils.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" @@ -47,7 +46,6 @@ using namespace mlir; using namespace mlir::qco; using namespace mlir::qco::decomposition; -using namespace mqt::test; using QubitId = std::size_t; From a8b0b5debc775cb23c702f25ab836619d103dd69 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 2 Jul 2026 10:12:13 +0200 Subject: [PATCH 096/122] =?UTF-8?q?=E2=98=82=EF=B8=8F=20Increase=20coverag?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_euler_decomposition.cpp | 14 ++++++ .../Decomposition/test_weyl_decomposition.cpp | 45 +++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp index b637abd290..90041fbccd 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -493,6 +493,8 @@ TEST(EulerAnglesCoverageTest, UBasisNonzeroThetaEmitsSingleUGate) { TestFixture fx; fx.setUp(); const Matrix2x2 matrix = RYOp::unitaryMatrix(1.2); + const EulerAngles angles = anglesFromUnitary(matrix, U); + ASSERT_GT(std::abs(angles.theta), mlir::utils::TOLERANCE); expectSynthesizedMatrix(fx.ctx(), matrix, U, [](func::FuncOp funcOp, const Matrix2x2& /*matrix*/) { EXPECT_EQ(countOps(funcOp), 1U); @@ -500,6 +502,18 @@ TEST(EulerAnglesCoverageTest, UBasisNonzeroThetaEmitsSingleUGate) { }); } +TEST(EulerAnglesCoverageTest, RBasisNonzeroThetaEmitsThreeRGates) { + TestFixture fx; + fx.setUp(); + const Matrix2x2 matrix = HOp::getUnitaryMatrix(); + const EulerAngles angles = anglesFromUnitary(matrix, R); + ASSERT_GT(std::abs(angles.theta), mlir::utils::TOLERANCE); + expectSynthesizedMatrix(fx.ctx(), matrix, R, + [](func::FuncOp funcOp, const Matrix2x2& /*matrix*/) { + EXPECT_EQ(countOps(funcOp), 3U); + }); +} + TEST(EulerAnglesCoverageTest, Mod2PiMapsPiBoundaryThroughSynthesis) { TestFixture fx; fx.setUp(); diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index 9a08bb45dc..a98e24f8c2 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -647,6 +647,11 @@ TEST(WeylSynthesisTest, SynthesisFailsWithoutEntangler) { TWO_QUBIT_CONTROLLED_X01, spec, out0, out1))); } +TEST(WeylSynthesisTest, EntanglerCountFailsWithoutEntangler) { + const NativeProfileSpec spec{.gates = {NativeGateKind::U}}; + EXPECT_FALSE(twoQubitEntanglerCount(Matrix4x4::identity(), spec).has_value()); +} + TEST(NativeSpecTest, ParsesAndRejectsMenus) { const auto ibm = parseNativeSpec("x,sx,rz,cx"); ASSERT_TRUE(ibm); @@ -656,6 +661,11 @@ TEST(NativeSpecTest, ParsesAndRejectsMenus) { EXPECT_FALSE(parseNativeSpec("x,sx,rz,not-a-gate").has_value()); EXPECT_FALSE(parseNativeSpec("u").has_value()); + const auto whitespaceToken = parseNativeSpec("u, ,cx"); + ASSERT_TRUE(whitespaceToken); + EXPECT_TRUE(whitespaceToken->gates.contains(NativeGateKind::U)); + EXPECT_TRUE(whitespaceToken->gates.contains(NativeGateKind::CX)); + const auto pMenu = parseNativeSpec("x,sx,p,cx"); const auto rzMenu = parseNativeSpec("x,sx,rz,cx"); ASSERT_TRUE(pMenu); @@ -678,6 +688,12 @@ TEST(NativeSpecTest, ParsesAndRejectsMenus) { EXPECT_FALSE(generic->gates.contains(NativeGateKind::X)); } +TEST(NativeSpecTest, RejectsMenuWithoutSingleQubitStrategy) { + EXPECT_FALSE(parseNativeSpec("cx").has_value()); + EXPECT_FALSE(parseNativeSpec("cz,rzz").has_value()); + EXPECT_FALSE(parseNativeSpec("rx,cx").has_value()); +} + TEST(NativeSpecTest, ResolvesEulerBasisFromMenu) { const auto uMenu = parseNativeSpec("u,cx"); ASSERT_TRUE(uMenu); @@ -747,6 +763,35 @@ TEST(NativeSpecTest, AllowsOpMatchesMenu) { EXPECT_FALSE(allowsOp(cxWithInterleavedH.getOperation(), *spec)); EXPECT_FALSE(allowsOp(XOp::create(builder, loc, q0).getOperation(), *spec)); + EXPECT_FALSE( + allowsOp(RXXOp::create(builder, loc, q0, q1, 0.2).getOperation(), *spec)); + + const auto pSpec = parseNativeSpec("x,sx,p,cx"); + ASSERT_TRUE(pSpec); + EXPECT_TRUE( + allowsOp(POp::create(builder, loc, q0, 0.3).getOperation(), *pSpec)); + + auto hCtrl = CtrlOp::create( + builder, loc, ValueRange{q0}, ValueRange{q1}, + [&](ValueRange targets) -> SmallVector { + return {HOp::create(builder, loc, targets[0]).getOutputQubit(0)}; + }); + EXPECT_FALSE(allowsOp(hCtrl.getOperation(), *spec)); + + const auto funcTy3 = builder.getFunctionType({qubitTy, qubitTy, qubitTy}, + {qubitTy, qubitTy, qubitTy}); + auto func3 = func::FuncOp::create(builder, loc, "allows_op_ccx", funcTy3); + auto* entry3 = func3.addEntryBlock(); + builder.setInsertionPointToStart(entry3); + Value c0 = entry3->getArgument(0); + Value c1 = entry3->getArgument(1); + Value target = entry3->getArgument(2); + auto ccx = CtrlOp::create( + builder, loc, ValueRange{c0, c1}, ValueRange{target}, + [&](ValueRange targets) -> SmallVector { + return {XOp::create(builder, loc, targets[0]).getOutputQubit(0)}; + }); + EXPECT_FALSE(allowsOp(ccx.getOperation(), *spec)); const auto czSpec = parseNativeSpec("u,cz"); ASSERT_TRUE(czSpec); From 78898f023c6c80bf36211976ea4fa61ab4b07bdc Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 2 Jul 2026 13:20:28 +0200 Subject: [PATCH 097/122] =?UTF-8?q?=F0=9F=8E=A8=20Refactor=20NativeProfile?= =?UTF-8?q?.cpp=20for=20clarity=20and=20correctness?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Decomposition/NativeProfile.cpp | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp index fa8b19c364..f529e24c9e 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp @@ -15,7 +15,6 @@ #include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" -#include "mlir/Dialect/Utils/Utils.h" #include #include @@ -36,12 +35,16 @@ namespace { using mlir::qco::Matrix4x4; constexpr Matrix4x4 CANONICAL_CONTROLLED_X = - Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, - 0.0, 1.0, 0.0, 0.0, 1.0, 0.0); + Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, // row 0 + 0.0, 1.0, 0.0, 0.0, // row 1 + 0.0, 0.0, 0.0, 1.0, // row 2 + 0.0, 0.0, 1.0, 0.0); // row 3 constexpr Matrix4x4 CANONICAL_CONTROLLED_Z = - Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, - 1.0, 0.0, 0.0, 0.0, 0.0, -1.0); + Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, // row 0 + 0.0, 1.0, 0.0, 0.0, // row 1 + 0.0, 0.0, 1.0, 0.0, // row 2 + 0.0, 0.0, 0.0, -1.0); // row 3 } // namespace @@ -160,16 +163,12 @@ static std::optional gateKindFor(UnitaryOpInterface op) { } static std::optional entanglerKindFor(CtrlOp ctrl) { - if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { - return std::nullopt; - } - auto bodyUnitary = - utils::getSoleBodyUnitary(*ctrl.getBody()); - if (!bodyUnitary) { + if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1 || + ctrl.getNumBodyUnitaries() != 1) { return std::nullopt; } return TypeSwitch>( - bodyUnitary.getOperation()) + ctrl.getBodyUnitary(0).getOperation()) .Case([](XOp) { return NativeGateKind::CX; }) .Case([](ZOp) { return NativeGateKind::CZ; }) .Default([](Operation*) { return std::nullopt; }); @@ -232,13 +231,13 @@ LogicalResult synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, wire1 = ctrlOp.getOutputTarget(0); }; - for (std::uint8_t i = 0; i < numBasisUses; ++i) { - emitFactor(wire1, static_cast(2 * i)); - emitFactor(wire0, static_cast((2 * i) + 1)); - emitEntangler(); + for (std::uint8_t layer = 0; layer <= numBasisUses; ++layer) { + emitFactor(wire1, static_cast(2 * layer)); + emitFactor(wire0, static_cast((2 * layer) + 1)); + if (layer < numBasisUses) { + emitEntangler(); + } } - emitFactor(wire1, static_cast(2 * numBasisUses)); - emitFactor(wire0, static_cast((2 * numBasisUses) + 1)); outQubit0 = wire0; outQubit1 = wire1; From 36afb23e14fbb58c65d7858f87451b7c8280748e Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 2 Jul 2026 13:23:46 +0200 Subject: [PATCH 098/122] =?UTF-8?q?=F0=9F=90=87=20Address=20rabbit's=20com?= =?UTF-8?q?ments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp index f529e24c9e..0356ce926c 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp @@ -200,6 +200,10 @@ LogicalResult synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, if (!native) { return failure(); } + const auto basis = resolveEulerBasis(spec.gates); + if (!basis) { + return failure(); + } emitGPhaseIfNeeded(builder, loc, native->globalPhase); @@ -207,12 +211,11 @@ LogicalResult synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, Value wire1 = qubit1; const auto& factors = native->singleQubitFactors; const std::uint8_t numBasisUses = native->numBasisUses; - const EulerBasis basis = spec.eulerBasis(); const bool emitCz = (*entangler == NativeGateKind::CZ); const auto emitFactor = [&](Value& wire, std::size_t index) { const auto synthesized = synthesizeUnitary1QEuler( builder, loc, wire, factors[index], /*runSize=*/0, - /*hasNonBasisGate=*/true, basis); + /*hasNonBasisGate=*/true, *basis); if (!synthesized) { llvm_unreachable("forced full synthesis must succeed"); } From b8003e2ed3ecf2556a6b52b6f9c2d52cd604c5b7 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 2 Jul 2026 13:37:49 +0200 Subject: [PATCH 099/122] =?UTF-8?q?=F0=9F=8E=A8=20Refactor=20Weyl=20decomp?= =?UTF-8?q?osition=20tests=20and=20improve=20matrix=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Decomposition/test_weyl_decomposition.cpp | 315 ++++++++++-------- 1 file changed, 185 insertions(+), 130 deletions(-) diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index a98e24f8c2..bac5d6c4af 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -61,15 +61,12 @@ static constexpr Matrix4x4 TWO_QUBIT_CONTROLLED_X10 = 0.0, 0.0, 1.0, 0.0, // 0.0, 1.0, 0.0, 0.0); -static constexpr Matrix4x4 TWO_QUBIT_CONTROLLED_Z = - Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, // - 0.0, 1.0, 0.0, 0.0, // - 0.0, 0.0, 1.0, 0.0, // - 0.0, 0.0, 0.0, -1.0); +static const Matrix4x4 TWO_QUBIT_CONTROLLED_Z = + Matrix4x4::fromDiagonal({Complex{1.0, 0.0}, Complex{1.0, 0.0}, + Complex{1.0, 0.0}, Complex{-1.0, 0.0}}); -template -static bool isUnitaryMatrix(const MatrixT& matrix, - const double tolerance = MATRIX_TOLERANCE) { +[[nodiscard]] static bool +isUnitaryMatrix(const auto& matrix, const double tolerance = MATRIX_TOLERANCE) { return (matrix.adjoint() * matrix).isIdentity(tolerance); } @@ -100,11 +97,14 @@ static Matrix4x4 randomUnitary4x4(std::mt19937& rng) { columns[j][i] /= norm; } } - const Matrix4x4 unitary = Matrix4x4::fromElements( - columns[0][0], columns[1][0], columns[2][0], columns[3][0], columns[0][1], - columns[1][1], columns[2][1], columns[3][1], columns[0][2], columns[1][2], - columns[2][2], columns[3][2], columns[0][3], columns[1][3], columns[2][3], - columns[3][3]); + const Matrix4x4 unitary = [&] { + Matrix4x4 matrix; + for (std::size_t col = 0; col < 4; ++col) { + matrix.setColumn(col, {columns[0][col], columns[1][col], columns[2][col], + columns[3][col]}); + } + return matrix; + }(); assert(isUnitaryMatrix(unitary, WEYL_TOLERANCE)); return unitary; } @@ -197,11 +197,11 @@ TEST(DecompositionHelpersTest, GateMatrixFactoriesMatchCanonicalForm) { } TEST(DecompositionHelpersTest, CanonicalMatrixMatchesGateProduct) { - for (const auto [a, b, c] : {std::tuple{0.3, 0.2, 0.1}, - {0.5, 0.5, 0.5}, - {0.5, 0.1, -0.1}, - {1.1, 0.2, 3.0}, - {-0.2, 0.3, 0.4}}) { + for (const auto& [a, b, c] : {std::tuple{0.3, 0.2, 0.1}, + {0.5, 0.5, 0.5}, + {0.5, 0.1, -0.1}, + {1.1, 0.2, 3.0}, + {-0.2, 0.3, 0.4}}) { const auto fromGates = RZZOp::unitaryMatrix(-2.0 * c) * RYYOp::unitaryMatrix(-2.0 * b) * RXXOp::unitaryMatrix(-2.0 * a); @@ -243,9 +243,8 @@ TEST_P(WeylDecompositionTest, ReconstructsWithinRequestedFidelity) { TEST(WeylDecompositionStandalone, CnotProducesValidWeylParametersAndUnitaryLocals) { - const Matrix4x4 cnot = - Matrix4x4::fromElements(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0); - const auto decomp = TwoQubitWeylDecomposition::create(cnot, std::nullopt); + const auto decomp = + TwoQubitWeylDecomposition::create(TWO_QUBIT_CONTROLLED_X01, std::nullopt); constexpr double piOver4 = 0.7853981633974483; for (const double angle : {decomp.a(), decomp.b(), decomp.c()}) { EXPECT_GE(angle, -1e-10); @@ -379,7 +378,8 @@ TEST(BasisDecomposerForcedCountTest, OneBasisUseProducesFactors) { const auto decomposed = decomposer.twoQubitDecompose(weyl, std::uint8_t{1}); ASSERT_TRUE(decomposed.has_value()); EXPECT_EQ(decomposed->numBasisUses, 1); - EXPECT_EQ(decomposed->singleQubitFactors.size(), 4U); + EXPECT_EQ(decomposed->singleQubitFactors.size(), + singleQubitFactorCount(decomposed->numBasisUses)); } TEST(BasisDecomposerForcedCountTest, TwoBasisUsesProducesFactors) { @@ -390,7 +390,8 @@ TEST(BasisDecomposerForcedCountTest, TwoBasisUsesProducesFactors) { const auto decomposed = decomposer.twoQubitDecompose(weyl, std::uint8_t{2}); ASSERT_TRUE(decomposed.has_value()); EXPECT_EQ(decomposed->numBasisUses, 2); - EXPECT_EQ(decomposed->singleQubitFactors.size(), 6U); + EXPECT_EQ(decomposed->singleQubitFactors.size(), + singleQubitFactorCount(decomposed->numBasisUses)); } TEST(BasisDecomposerForcedCountTest, ThreeBasisUsesProducesFactors) { @@ -401,7 +402,8 @@ TEST(BasisDecomposerForcedCountTest, ThreeBasisUsesProducesFactors) { const auto decomposed = decomposer.twoQubitDecompose(weyl, std::uint8_t{3}); ASSERT_TRUE(decomposed.has_value()); EXPECT_EQ(decomposed->numBasisUses, 3); - EXPECT_EQ(decomposed->singleQubitFactors.size(), 8U); + EXPECT_EQ(decomposed->singleQubitFactors.size(), + singleQubitFactorCount(decomposed->numBasisUses)); } TEST(WeylDecompositionStandalone, SwapNegativeCSpecializationReconstructs) { @@ -432,122 +434,143 @@ INSTANTIATE_TEST_SUITE_P(TwoQubitMatrices, BasisDecomposerTest, testing::Combine(cxBasisCases(), entangledMatrixCases())); -static bool extractSingleQubitMatrix(UnitaryOpInterface op, Matrix2x2& out) { - if (op.getUnitaryMatrix2x2(out)) { - return true; +[[nodiscard]] static std::optional +lookupWireId(const llvm::DenseMap& wireIds, Value wire) { + if (const auto it = wireIds.find(wire); it != wireIds.end()) { + return it->second; } - DynamicMatrix dynamic; - if (!op.getUnitaryMatrixDynamic(dynamic) || dynamic.rows() != 2 || - dynamic.cols() != 2) { - return false; + return std::nullopt; +} + +[[nodiscard]] static std::optional +embedded1QOnWires(UnitaryOpInterface op, QubitId q0) { + Matrix2x2 matrix; + if (!op.getUnitaryMatrix2x2(matrix)) { + return std::nullopt; } - out = Matrix2x2::fromElements(dynamic(0, 0), dynamic(0, 1), dynamic(1, 0), - dynamic(1, 1)); - return true; + return matrix.embedInTwoQubit(q0); +} + +[[nodiscard]] static std::optional +embedded2QOnWires(UnitaryOpInterface op, QubitId q0, QubitId q1) { + Matrix4x4 matrix; + if (!op.getUnitaryMatrix4x4(matrix)) { + return std::nullopt; + } + return matrix.reorderForQubits(q0, q1); } static std::optional computeTwoQubitUnitaryFromFunc(func::FuncOp funcOp) { Matrix4x4 unitary = Matrix4x4::identity(); - std::complex global{1.0, 0.0}; + Complex global{1.0, 0.0}; llvm::DenseMap wireIds; wireIds[funcOp.getArgument(0)] = 0; wireIds[funcOp.getArgument(1)] = 1; - auto wireId = [&](Value qubit) -> std::optional { - const auto it = wireIds.find(qubit); - if (it == wireIds.end()) { - return std::nullopt; + for (Operation& op : funcOp.getBody().front()) { + if (isa(op)) { + continue; } - return it->second; - }; - for (Operation& rawOp : funcOp.getBody().front()) { - if (llvm::isa(&rawOp)) { + if (auto returnOp = dyn_cast(op)) { + if (returnOp.getNumOperands() != 2) { + return std::nullopt; + } + if (const auto out0 = lookupWireId(wireIds, returnOp.getOperand(0)); + !out0.has_value()) { + return std::nullopt; + } else if (const auto out1 = + lookupWireId(wireIds, returnOp.getOperand(1)); + !out1.has_value() || *out0 != 0 || *out1 != 1) { + return std::nullopt; + } continue; } - if (auto gphase = llvm::dyn_cast(&rawOp)) { + + if (auto gphase = dyn_cast(op)) { if (const auto matrix = gphase.getUnitaryMatrix()) { global *= (*matrix)(0, 0); } continue; } - auto op = llvm::dyn_cast(&rawOp); - if (!op) { - continue; - } - if (op.isSingleQubit()) { - const auto qIn = op->getOperand(0); - const auto qid = wireId(qIn); - if (!qid) { - return std::nullopt; - } - Matrix2x2 oneQ; - if (!extractSingleQubitMatrix(op, oneQ)) { - return std::nullopt; - } - unitary = oneQ.embedInTwoQubit(*qid) * unitary; - wireIds[op->getResult(0)] = *qid; - continue; + auto unitaryOp = dyn_cast(op); + if (!unitaryOp) { + return std::nullopt; } - if (op.isTwoQubit()) { - const auto q0In = op->getOperand(0); - const auto q1In = op->getOperand(1); - const auto q0id = wireId(q0In); - const auto q1id = wireId(q1In); - if (!q0id || !q1id) { + std::optional step; + if (unitaryOp.isSingleQubit()) { + if (const auto qid = lookupWireId(wireIds, unitaryOp.getInputQubit(0)); + !qid.has_value()) { return std::nullopt; + } else { + step = embedded1QOnWires(unitaryOp, *qid); + wireIds[unitaryOp.getOutputQubit(0)] = *qid; } - Matrix4x4 twoQ; - if (!op.getUnitaryMatrix4x4(twoQ)) { + } else if (unitaryOp.isTwoQubit()) { + if (const auto q0id = lookupWireId(wireIds, unitaryOp.getInputQubit(0)); + !q0id.has_value()) { return std::nullopt; + } else if (const auto q1id = + lookupWireId(wireIds, unitaryOp.getInputQubit(1)); + !q1id.has_value()) { + return std::nullopt; + } else { + step = embedded2QOnWires(unitaryOp, *q0id, *q1id); + wireIds[unitaryOp.getOutputQubit(0)] = *q0id; + wireIds[unitaryOp.getOutputQubit(1)] = *q1id; } - unitary = twoQ.reorderForQubits(*q0id, *q1id) * unitary; - wireIds[op->getResult(0)] = *q0id; - wireIds[op->getResult(1)] = *q1id; + } else { + return std::nullopt; } + if (!step) { + return std::nullopt; + } + unitary.premultiplyBy(*step); } - return unitary * global; + unitary *= global; + return unitary; } -static func::FuncOp synthesize2QIntoFunc(MLIRContext* ctx, - const Matrix4x4& target, - const NativeProfileSpec& spec, - OwningOpRef& moduleOut) { - moduleOut = ModuleOp::create(UnknownLoc::get(ctx)); +struct Synthesized2QCircuit { + OwningOpRef mlirModule; + func::FuncOp func; +}; + +[[nodiscard]] static Synthesized2QCircuit +synthesize2QMatrix(MLIRContext* ctx, const Matrix4x4& target, + const NativeProfileSpec& spec) { + OwningOpRef mlirModule = ModuleOp::create(UnknownLoc::get(ctx)); OpBuilder builder(ctx); - builder.setInsertionPointToStart(moduleOut->getBody()); + builder.setInsertionPointToStart(mlirModule->getBody()); const auto qubitTy = QubitType::get(ctx); const auto funcTy = builder.getFunctionType({qubitTy, qubitTy}, {qubitTy, qubitTy}); - const Location loc = moduleOut->getLoc(); + const Location loc = mlirModule->getLoc(); auto func = func::FuncOp::create(builder, loc, "main", funcTy); auto* entry = func.addEntryBlock(); builder.setInsertionPointToStart(entry); Value out0; - Value out1; - const auto synthResult = - synthesizeUnitary2QWeyl(builder, loc, entry->getArgument(0), - entry->getArgument(1), target, spec, out0, out1); - if (failed(synthResult)) { + if (Value out1; failed(synthesizeUnitary2QWeyl( + builder, loc, entry->getArgument(0), entry->getArgument(1), target, + spec, out0, out1))) { ADD_FAILURE() << "synthesizeUnitary2QWeyl failed during test synthesis"; - return func; + } else { + func::ReturnOp::create(builder, loc, ValueRange{out0, out1}); } - func::ReturnOp::create(builder, loc, ValueRange{out0, out1}); - return func; + return {.mlirModule = std::move(mlirModule), .func = func}; } static void expectSynthesized2QMatrix(MLIRContext* ctx, const Matrix4x4& target, const NativeProfileSpec& spec) { - OwningOpRef module; - const auto func = synthesize2QIntoFunc(ctx, target, spec, module); - ASSERT_TRUE(succeeded(verify(module.get()))); - const auto actual = computeTwoQubitUnitaryFromFunc(func); + const auto circuit = synthesize2QMatrix(ctx, target, spec); + ASSERT_TRUE(succeeded(verify(*circuit.mlirModule))); + const auto actual = computeTwoQubitUnitaryFromFunc(circuit.func); ASSERT_TRUE(actual.has_value()); EXPECT_TRUE(actual->isApprox(target, WEYL_TOLERANCE)); } @@ -574,16 +597,26 @@ struct WeylSynthesisCase { Matrix4x4 (*target)(); }; -class WeylSynthesisTest : public testing::TestWithParam {}; +class WeylSynthesisTest : public testing::TestWithParam { +protected: + MlirTestContext mlir; + + void SetUp() override { mlir.setUp(); } +}; + +class NativeProfileMlirTest : public testing::Test { +protected: + MlirTestContext mlir; + + void SetUp() override { mlir.setUp(); } +}; } // namespace TEST_P(WeylSynthesisTest, PreservesTargetUnitary) { - MlirTestContext fx; - fx.setUp(); const auto spec = parseNativeSpec(GetParam().nativeGates); ASSERT_TRUE(spec); - expectSynthesized2QMatrix(fx.ctx(), GetParam().target(), *spec); + expectSynthesized2QMatrix(mlir.ctx(), GetParam().target(), *spec); } INSTANTIATE_TEST_SUITE_P( @@ -611,33 +644,60 @@ INSTANTIATE_TEST_SUITE_P( }); TEST(WeylSynthesisTest, IdentityRequiresNoEntanglers) { - const auto cxSpec = parseNativeSpec("u,cx"); - ASSERT_TRUE(cxSpec); - const auto cxCount = twoQubitEntanglerCount(Matrix4x4::identity(), *cxSpec); - ASSERT_TRUE(cxCount.has_value()); - EXPECT_EQ(*cxCount, 0U); - - const auto czSpec = parseNativeSpec("u,cz"); - ASSERT_TRUE(czSpec); - const auto czCount = twoQubitEntanglerCount(Matrix4x4::identity(), *czSpec); - ASSERT_TRUE(czCount.has_value()); - EXPECT_EQ(*czCount, 0U); + for (const char* menu : {"u,cx", "u,cz"}) { + const auto spec = parseNativeSpec(menu); + ASSERT_TRUE(spec) << menu; + const auto count = twoQubitEntanglerCount(Matrix4x4::identity(), *spec); + ASSERT_TRUE(count.has_value()) << menu; + EXPECT_EQ(*count, 0U) << menu; + } } TEST(WeylSynthesisTest, RejectsMenuWithoutEntangler) { EXPECT_FALSE(parseNativeSpec("u").has_value()); } -TEST(WeylSynthesisTest, SynthesisFailsWithoutEntangler) { - MlirTestContext fx; - fx.setUp(); +TEST_F(NativeProfileMlirTest, ReconstructionRejectsUnhandledOps) { + OpBuilder builder(mlir.ctx()); + const Location loc = UnknownLoc::get(mlir.ctx()); + const auto qubitTy = QubitType::get(mlir.ctx()); + const auto funcTy = + builder.getFunctionType({qubitTy, qubitTy}, {qubitTy, qubitTy}); + auto func = func::FuncOp::create(builder, loc, "main", funcTy); + auto* entry = func.addEntryBlock(); + builder.setInsertionPointToStart(entry); + Value q0 = entry->getArgument(0); + Value q1 = entry->getArgument(1); + BarrierOp::create(builder, loc, ValueRange{q0, q1}); + func::ReturnOp::create(builder, loc, ValueRange{q0, q1}); + EXPECT_FALSE(computeTwoQubitUnitaryFromFunc(func).has_value()); +} + +TEST_F(NativeProfileMlirTest, SynthesisFailsWithoutEulerBasis) { + const NativeProfileSpec spec{.gates = {NativeGateKind::CX}}; + OpBuilder builder(mlir.ctx()); + const auto qubitTy = QubitType::get(mlir.ctx()); + const auto funcTy = + builder.getFunctionType({qubitTy, qubitTy}, {qubitTy, qubitTy}); + auto func = func::FuncOp::create(builder, UnknownLoc::get(mlir.ctx()), "main", + funcTy); + auto* entry = func.addEntryBlock(); + builder.setInsertionPointToStart(entry); + Value out0; + Value out1; + EXPECT_TRUE(failed(synthesizeUnitary2QWeyl( + builder, func.getLoc(), entry->getArgument(0), entry->getArgument(1), + TWO_QUBIT_CONTROLLED_X01, spec, out0, out1))); +} + +TEST_F(NativeProfileMlirTest, SynthesisFailsWithoutEntangler) { const NativeProfileSpec spec{.gates = {NativeGateKind::U}}; - OpBuilder builder(fx.ctx()); - const auto qubitTy = QubitType::get(fx.ctx()); + OpBuilder builder(mlir.ctx()); + const auto qubitTy = QubitType::get(mlir.ctx()); const auto funcTy = builder.getFunctionType({qubitTy, qubitTy}, {qubitTy, qubitTy}); - auto func = - func::FuncOp::create(builder, UnknownLoc::get(fx.ctx()), "main", funcTy); + auto func = func::FuncOp::create(builder, UnknownLoc::get(mlir.ctx()), "main", + funcTy); auto* entry = func.addEntryBlock(); builder.setInsertionPointToStart(entry); Value out0; @@ -674,18 +734,15 @@ TEST(NativeSpecTest, ParsesAndRejectsMenus) { const auto cxOnly = parseNativeSpec("u,cx"); ASSERT_TRUE(cxOnly); + EXPECT_TRUE(cxOnly->gates.contains(NativeGateKind::U)); EXPECT_TRUE(cxOnly->gates.contains(NativeGateKind::CX)); EXPECT_FALSE(cxOnly->gates.contains(NativeGateKind::CZ)); + EXPECT_FALSE(cxOnly->gates.contains(NativeGateKind::X)); const auto both = parseNativeSpec("u,cx,cz"); ASSERT_TRUE(both); EXPECT_TRUE(both->gates.contains(NativeGateKind::CX)); EXPECT_TRUE(both->gates.contains(NativeGateKind::CZ)); - - const auto generic = parseNativeSpec("u,cx"); - ASSERT_TRUE(generic); - EXPECT_TRUE(generic->gates.contains(NativeGateKind::U)); - EXPECT_FALSE(generic->gates.contains(NativeGateKind::X)); } TEST(NativeSpecTest, RejectsMenuWithoutSingleQubitStrategy) { @@ -720,15 +777,13 @@ TEST(NativeSpecTest, ResolvesEulerBasisFromMenu) { EXPECT_EQ(zyz->eulerBasis(), EulerBasis::ZYZ); } -TEST(NativeSpecTest, AllowsOpMatchesMenu) { - MlirTestContext fx; - fx.setUp(); +TEST_F(NativeProfileMlirTest, AllowsOpMatchesMenu) { const auto spec = parseNativeSpec("u,cx,rzz"); ASSERT_TRUE(spec); - OpBuilder builder(fx.ctx()); - const Location loc = UnknownLoc::get(fx.ctx()); - const auto qubitTy = QubitType::get(fx.ctx()); + OpBuilder builder(mlir.ctx()); + const Location loc = UnknownLoc::get(mlir.ctx()); + const auto qubitTy = QubitType::get(mlir.ctx()); const auto funcTy = builder.getFunctionType({qubitTy, qubitTy}, {qubitTy, qubitTy}); auto func = func::FuncOp::create(builder, loc, "allows_op", funcTy); @@ -749,14 +804,14 @@ TEST(NativeSpecTest, AllowsOpMatchesMenu) { auto cx = CtrlOp::create( builder, loc, ValueRange{q0}, ValueRange{q1}, - [&](ValueRange targets) -> SmallVector { + [&builder, &loc](ValueRange targets) -> SmallVector { return {XOp::create(builder, loc, targets[0]).getOutputQubit(0)}; }); EXPECT_TRUE(allowsOp(cx.getOperation(), *spec)); auto cxWithInterleavedH = CtrlOp::create( builder, loc, ValueRange{q0}, ValueRange{q1}, - [&](ValueRange targets) -> SmallVector { + [&builder, &loc](ValueRange targets) -> SmallVector { auto wire = XOp::create(builder, loc, targets[0]).getOutputQubit(0); return {HOp::create(builder, loc, wire).getOutputQubit(0)}; }); @@ -773,7 +828,7 @@ TEST(NativeSpecTest, AllowsOpMatchesMenu) { auto hCtrl = CtrlOp::create( builder, loc, ValueRange{q0}, ValueRange{q1}, - [&](ValueRange targets) -> SmallVector { + [&builder, &loc](ValueRange targets) -> SmallVector { return {HOp::create(builder, loc, targets[0]).getOutputQubit(0)}; }); EXPECT_FALSE(allowsOp(hCtrl.getOperation(), *spec)); @@ -788,7 +843,7 @@ TEST(NativeSpecTest, AllowsOpMatchesMenu) { Value target = entry3->getArgument(2); auto ccx = CtrlOp::create( builder, loc, ValueRange{c0, c1}, ValueRange{target}, - [&](ValueRange targets) -> SmallVector { + [&builder, &loc](ValueRange targets) -> SmallVector { return {XOp::create(builder, loc, targets[0]).getOutputQubit(0)}; }); EXPECT_FALSE(allowsOp(ccx.getOperation(), *spec)); @@ -797,7 +852,7 @@ TEST(NativeSpecTest, AllowsOpMatchesMenu) { ASSERT_TRUE(czSpec); auto cz = CtrlOp::create( builder, loc, ValueRange{q0}, ValueRange{q1}, - [&](ValueRange targets) -> SmallVector { + [&builder, &loc](ValueRange targets) -> SmallVector { return {ZOp::create(builder, loc, targets[0]).getOutputQubit(0)}; }); EXPECT_TRUE(allowsOp(cz.getOperation(), *czSpec)); From 23c2bf83dc54d633f9cba67c5e8fbce830a02557 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 2 Jul 2026 14:05:12 +0200 Subject: [PATCH 100/122] =?UTF-8?q?=F0=9F=8E=A8=20Refactor=20Weyl=20decomp?= =?UTF-8?q?osition=20logic=20for=20improved=20clarity=20and=20efficiency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Decomposition/test_weyl_decomposition.cpp | 81 +++++++++---------- 1 file changed, 36 insertions(+), 45 deletions(-) diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index bac5d6c4af..f5d78bcd64 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -97,14 +97,11 @@ static Matrix4x4 randomUnitary4x4(std::mt19937& rng) { columns[j][i] /= norm; } } - const Matrix4x4 unitary = [&] { - Matrix4x4 matrix; - for (std::size_t col = 0; col < 4; ++col) { - matrix.setColumn(col, {columns[0][col], columns[1][col], columns[2][col], - columns[3][col]}); - } - return matrix; - }(); + const auto unitary = Matrix4x4::fromElements( + columns[0][0], columns[1][0], columns[2][0], columns[3][0], columns[0][1], + columns[1][1], columns[2][1], columns[3][1], columns[0][2], columns[1][2], + columns[2][2], columns[3][2], columns[0][3], columns[1][3], columns[2][3], + columns[3][3]); assert(isUnitaryMatrix(unitary, WEYL_TOLERANCE)); return unitary; } @@ -443,21 +440,23 @@ lookupWireId(const llvm::DenseMap& wireIds, Value wire) { } [[nodiscard]] static std::optional -embedded1QOnWires(UnitaryOpInterface op, QubitId q0) { - Matrix2x2 matrix; - if (!op.getUnitaryMatrix2x2(matrix)) { +embeddedStepOnWires(UnitaryOpInterface op, QubitId q0, + std::optional q1) { + if (op.isSingleQubit()) { + Matrix2x2 matrix; + if (!op.getUnitaryMatrix2x2(matrix)) { + return std::nullopt; + } + return matrix.embedInTwoQubit(q0); + } + if (!q1.has_value()) { return std::nullopt; } - return matrix.embedInTwoQubit(q0); -} - -[[nodiscard]] static std::optional -embedded2QOnWires(UnitaryOpInterface op, QubitId q0, QubitId q1) { Matrix4x4 matrix; if (!op.getUnitaryMatrix4x4(matrix)) { return std::nullopt; } - return matrix.reorderForQubits(q0, q1); + return matrix.reorderForQubits(q0, *q1); } static std::optional @@ -477,12 +476,9 @@ computeTwoQubitUnitaryFromFunc(func::FuncOp funcOp) { if (returnOp.getNumOperands() != 2) { return std::nullopt; } - if (const auto out0 = lookupWireId(wireIds, returnOp.getOperand(0)); - !out0.has_value()) { - return std::nullopt; - } else if (const auto out1 = - lookupWireId(wireIds, returnOp.getOperand(1)); - !out1.has_value() || *out0 != 0 || *out1 != 1) { + const auto out0 = lookupWireId(wireIds, returnOp.getOperand(0)); + const auto out1 = lookupWireId(wireIds, returnOp.getOperand(1)); + if (!out0.has_value() || !out1.has_value() || *out0 != 0 || *out1 != 1) { return std::nullopt; } continue; @@ -500,35 +496,30 @@ computeTwoQubitUnitaryFromFunc(func::FuncOp funcOp) { return std::nullopt; } - std::optional step; - if (unitaryOp.isSingleQubit()) { - if (const auto qid = lookupWireId(wireIds, unitaryOp.getInputQubit(0)); - !qid.has_value()) { - return std::nullopt; - } else { - step = embedded1QOnWires(unitaryOp, *qid); - wireIds[unitaryOp.getOutputQubit(0)] = *qid; - } - } else if (unitaryOp.isTwoQubit()) { - if (const auto q0id = lookupWireId(wireIds, unitaryOp.getInputQubit(0)); - !q0id.has_value()) { - return std::nullopt; - } else if (const auto q1id = - lookupWireId(wireIds, unitaryOp.getInputQubit(1)); - !q1id.has_value()) { + const auto q0 = lookupWireId(wireIds, unitaryOp.getInputQubit(0)); + if (!q0.has_value()) { + return std::nullopt; + } + std::optional q1; + if (unitaryOp.isTwoQubit()) { + q1 = lookupWireId(wireIds, unitaryOp.getInputQubit(1)); + if (!q1.has_value()) { return std::nullopt; - } else { - step = embedded2QOnWires(unitaryOp, *q0id, *q1id); - wireIds[unitaryOp.getOutputQubit(0)] = *q0id; - wireIds[unitaryOp.getOutputQubit(1)] = *q1id; } - } else { + } else if (!unitaryOp.isSingleQubit()) { return std::nullopt; } - if (!step) { + + const auto step = embeddedStepOnWires(unitaryOp, *q0, q1); + if (!step.has_value()) { return std::nullopt; } unitary.premultiplyBy(*step); + + wireIds[unitaryOp.getOutputQubit(0)] = *q0; + if (q1.has_value()) { + wireIds[unitaryOp.getOutputQubit(1)] = *q1; + } } unitary *= global; From c751462c6b90b4dc6644557915b058d4a9eb94b8 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 2 Jul 2026 14:14:16 +0200 Subject: [PATCH 101/122] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20linter=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Decomposition/test_weyl_decomposition.cpp | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index f5d78bcd64..5ceb4d5f56 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -18,7 +18,6 @@ #include #include -#include #include #include #include @@ -41,6 +40,7 @@ #include #include #include +#include #include using namespace mlir; @@ -431,6 +431,15 @@ INSTANTIATE_TEST_SUITE_P(TwoQubitMatrices, BasisDecomposerTest, testing::Combine(cxBasisCases(), entangledMatrixCases())); +namespace { + +struct Synthesized2QCircuit { + OwningOpRef mlirModule; + func::FuncOp func; +}; + +} // namespace + [[nodiscard]] static std::optional lookupWireId(const llvm::DenseMap& wireIds, Value wire) { if (const auto it = wireIds.find(wire); it != wireIds.end()) { @@ -526,11 +535,6 @@ computeTwoQubitUnitaryFromFunc(func::FuncOp funcOp) { return unitary; } -struct Synthesized2QCircuit { - OwningOpRef mlirModule; - func::FuncOp func; -}; - [[nodiscard]] static Synthesized2QCircuit synthesize2QMatrix(MLIRContext* ctx, const Matrix4x4& target, const NativeProfileSpec& spec) { From da8c95a3fc8e991d3613314e41d4e8084770fcde Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 2 Jul 2026 14:18:56 +0200 Subject: [PATCH 102/122] =?UTF-8?q?=F0=9F=93=9D=20Update=20CHANGELOG=20to?= =?UTF-8?q?=20include=20PR=20#1832?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a30e7aec50..32d518f2cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,7 +44,7 @@ releases may include breaking changes. [#1626], [#1627], [#1635], [#1638], [#1673], [#1675], [#1700], [#1710], [#1717], [#1728], [#1730], [#1749], [#1751], [#1762], [#1765], [#1774], [#1780], [#1781], [#1782], [#1787], [#1802], [#1803], [#1806], [#1807], - [#1808], [#1809], [#1823], [#1824], [#1830]) ([**@burgholzer**], + [#1808], [#1809], [#1823], [#1824], [#1830], [#1832]) ([**@burgholzer**], [**@denialhaag**], [**@taminob**], [**@DRovara**], [**@li-mingbao**], [**@Ectras**], [**@MatthiasReumann**], [**@simon1hofmann**]) @@ -592,6 +592,7 @@ changelogs._ +[#1832]: https://github.com/munich-quantum-toolkit/core/pull/1832 [#1830]: https://github.com/munich-quantum-toolkit/core/pull/1830 [#1828]: https://github.com/munich-quantum-toolkit/core/pull/1828 [#1826]: https://github.com/munich-quantum-toolkit/core/pull/1826 From beedf9d26ffb4a7e53d720620574a48ab50a0123 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 7 Jul 2026 09:53:22 +0200 Subject: [PATCH 103/122] =?UTF-8?q?=F0=9F=94=A5=20Remove=20NativeProfile?= =?UTF-8?q?=20header=20and=20implementation=20files,=20refactor=20related?= =?UTF-8?q?=20code=20to=20use=20NativeGateset=20instead?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Transforms/Decomposition/NativeProfile.h | 94 ------ .../Decomposition/NativeProfile.cpp | 282 ------------------ .../FuseTwoQubitUnitaryRuns.cpp | 132 +++++--- .../Compiler/test_compiler_pipeline.cpp | 2 +- .../test_fuse_two_qubit_unitary_runs.cpp | 28 -- 5 files changed, 93 insertions(+), 445 deletions(-) delete mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeProfile.h delete mode 100644 mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeProfile.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeProfile.h deleted file mode 100644 index 3e9cc47767..0000000000 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeProfile.h +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#pragma once - -#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" -#include "mlir/Dialect/QCO/Utils/Matrix.h" - -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -namespace mlir::qco::decomposition { - -/** - * @brief Gate token in a comma-separated native menu (e.g. `"u,cx,rzz"`). - */ -enum class NativeGateKind : std::uint8_t { - U, - X, - SX, - RZ, - RX, - RY, - R, - CX, - CZ, - RZZ, -}; - -/** - * @brief Resolved native-gate menu for two-qubit Weyl synthesis. - * - * @p gates is the parsed menu. Euler decomposition and entangler choice are - * derived from it with fixed priority (see @ref NativeProfileSpec::eulerBasis - * and @ref parseNativeSpec). Menus must include a supported single-qubit - * strategy and at least one of `cx` or `cz`; when both are present, `cx` is - * used for synthesis. - */ -struct NativeProfileSpec { - llvm::DenseSet gates; - - /** - * @brief Preferred single-qubit Euler basis for synthesis in this menu. - * - * Only valid for specs returned by @ref parseNativeSpec. - */ - [[nodiscard]] EulerBasis eulerBasis() const; -}; - -/** @brief Parses a comma-separated native-gate menu (e.g. `"u,cx,rzz"`). */ -[[nodiscard]] std::optional -parseNativeSpec(StringRef nativeGates); - -/** @brief Synthesizes a two-qubit unitary as gates allowed by @p spec. */ -[[nodiscard]] LogicalResult -synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, Value qubit0, - Value qubit1, const Matrix4x4& target, - const NativeProfileSpec& spec, Value& outQubit0, - Value& outQubit1); - -/** - * @brief Entangling basis gates needed to synthesize @p target under @p spec. - * - * @return Count for @p spec, or `std::nullopt` when synthesis is impossible. - */ -[[nodiscard]] std::optional -twoQubitEntanglerCount(const Matrix4x4& target, const NativeProfileSpec& spec); - -/** - * @brief Returns true when @p op is already on the resolved native menu. - * - * Barriers and global phase are always allowed. Single-qubit primitives and - * single-target `CtrlOp` shells (`X`/`Z` bodies) are checked against - * @p spec.gates. `RZZ` is allowed when listed, but two-qubit synthesis uses - * only `cx` or `cz` entanglers. - */ -[[nodiscard]] bool allowsOp(Operation* op, const NativeProfileSpec& spec); - -} // namespace mlir::qco::decomposition diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp deleted file mode 100644 index 0356ce926c..0000000000 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeProfile.cpp +++ /dev/null @@ -1,282 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "mlir/Dialect/QCO/Transforms/Decomposition/NativeProfile.h" - -#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" -#include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" -#include "mlir/Dialect/QCO/Utils/Matrix.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -namespace { - -using mlir::qco::Matrix4x4; - -constexpr Matrix4x4 CANONICAL_CONTROLLED_X = - Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, // row 0 - 0.0, 1.0, 0.0, 0.0, // row 1 - 0.0, 0.0, 0.0, 1.0, // row 2 - 0.0, 0.0, 1.0, 0.0); // row 3 - -constexpr Matrix4x4 CANONICAL_CONTROLLED_Z = - Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, // row 0 - 0.0, 1.0, 0.0, 0.0, // row 1 - 0.0, 0.0, 1.0, 0.0, // row 2 - 0.0, 0.0, 0.0, -1.0); // row 3 - -} // namespace - -namespace mlir::qco::decomposition { - -static std::optional parseGateToken(llvm::StringRef name) { - return llvm::StringSwitch>(name) - .Case("u", NativeGateKind::U) - .Case("x", NativeGateKind::X) - .Case("sx", NativeGateKind::SX) - .Cases("rz", "p", NativeGateKind::RZ) - .Case("rx", NativeGateKind::RX) - .Case("ry", NativeGateKind::RY) - .Case("r", NativeGateKind::R) - .Case("cx", NativeGateKind::CX) - .Case("cz", NativeGateKind::CZ) - .Case("rzz", NativeGateKind::RZZ) - .Default(std::nullopt); -} - -static std::optional> -parseGateSet(llvm::StringRef nativeGates) { - llvm::DenseSet gates; - SmallVector parts; - nativeGates.split(parts, ',', /*MaxSplit=*/-1, /*KeepEmpty=*/false); - for (llvm::StringRef part : parts) { - const auto token = part.trim().lower(); - if (token.empty()) { - continue; - } - const auto gate = parseGateToken(token); - if (!gate) { - return std::nullopt; - } - gates.insert(*gate); - } - return gates; -} - -/** - * @brief Resolves the preferred single-qubit Euler basis for a parsed menu. - * - * Returns `std::nullopt` when no supported single-qubit synthesis strategy is - * present. Priority matches @ref NativeProfileSpec::eulerBasis. - */ -[[nodiscard]] static std::optional -resolveEulerBasis(const llvm::DenseSet& gates) { - const auto has = [&](NativeGateKind kind) { return gates.contains(kind); }; - if (has(NativeGateKind::U)) { - return EulerBasis::U; - } - if (has(NativeGateKind::X) && has(NativeGateKind::SX) && - has(NativeGateKind::RZ)) { - return EulerBasis::ZSXX; - } - if (has(NativeGateKind::R)) { - return EulerBasis::R; - } - if (has(NativeGateKind::RX) && has(NativeGateKind::RZ)) { - return EulerBasis::XZX; - } - if (has(NativeGateKind::RX) && has(NativeGateKind::RY)) { - return EulerBasis::XYX; - } - if (has(NativeGateKind::RY) && has(NativeGateKind::RZ)) { - return EulerBasis::ZYZ; - } - return std::nullopt; -} - -/** - * @brief Picks the two-qubit entangler for Weyl synthesis. - * - * When both `cx` and `cz` appear in the menu, `cx` is preferred. - */ -[[nodiscard]] static std::optional -selectEntangler(const llvm::DenseSet& gates) { - if (gates.contains(NativeGateKind::CX)) { - return NativeGateKind::CX; - } - if (gates.contains(NativeGateKind::CZ)) { - return NativeGateKind::CZ; - } - return std::nullopt; -} - -static const TwoQubitBasisDecomposer& -cachedBasisDecomposer(NativeGateKind entangler) { - switch (entangler) { - case NativeGateKind::CX: { - static const TwoQubitBasisDecomposer DECOMPOSER = - TwoQubitBasisDecomposer::create(CANONICAL_CONTROLLED_X, 1.0); - return DECOMPOSER; - } - case NativeGateKind::CZ: { - static const TwoQubitBasisDecomposer DECOMPOSER = - TwoQubitBasisDecomposer::create(CANONICAL_CONTROLLED_Z, 1.0); - return DECOMPOSER; - } - default: - llvm_unreachable("only CX/CZ are valid entanglers"); - } -} - -static std::optional gateKindFor(UnitaryOpInterface op) { - return TypeSwitch>( - op.getOperation()) - .Case([](UOp) { return NativeGateKind::U; }) - .Case([](XOp) { return NativeGateKind::X; }) - .Case([](SXOp) { return NativeGateKind::SX; }) - .Case([](auto) { return NativeGateKind::RZ; }) - .Case([](RXOp) { return NativeGateKind::RX; }) - .Case([](RYOp) { return NativeGateKind::RY; }) - .Case([](ROp) { return NativeGateKind::R; }) - .Default([](Operation*) { return std::nullopt; }); -} - -static std::optional entanglerKindFor(CtrlOp ctrl) { - if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1 || - ctrl.getNumBodyUnitaries() != 1) { - return std::nullopt; - } - return TypeSwitch>( - ctrl.getBodyUnitary(0).getOperation()) - .Case([](XOp) { return NativeGateKind::CX; }) - .Case([](ZOp) { return NativeGateKind::CZ; }) - .Default([](Operation*) { return std::nullopt; }); -} - -EulerBasis NativeProfileSpec::eulerBasis() const { - // Valid only for specs returned by @ref parseNativeSpec. - return *resolveEulerBasis(gates); -} - -std::optional parseNativeSpec(llvm::StringRef nativeGates) { - auto gates = parseGateSet(nativeGates); - if (!gates || !resolveEulerBasis(*gates) || !selectEntangler(*gates)) { - return std::nullopt; - } - return NativeProfileSpec{.gates = std::move(*gates)}; -} - -LogicalResult synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, - Value qubit0, Value qubit1, - const Matrix4x4& target, - const NativeProfileSpec& spec, - Value& outQubit0, Value& outQubit1) { - const auto entangler = selectEntangler(spec.gates); - if (!entangler) { - return failure(); - } - const auto native = cachedBasisDecomposer(*entangler).decomposeTarget(target); - if (!native) { - return failure(); - } - const auto basis = resolveEulerBasis(spec.gates); - if (!basis) { - return failure(); - } - - emitGPhaseIfNeeded(builder, loc, native->globalPhase); - - Value wire0 = qubit0; - Value wire1 = qubit1; - const auto& factors = native->singleQubitFactors; - const std::uint8_t numBasisUses = native->numBasisUses; - const bool emitCz = (*entangler == NativeGateKind::CZ); - const auto emitFactor = [&](Value& wire, std::size_t index) { - const auto synthesized = synthesizeUnitary1QEuler( - builder, loc, wire, factors[index], /*runSize=*/0, - /*hasNonBasisGate=*/true, *basis); - if (!synthesized) { - llvm_unreachable("forced full synthesis must succeed"); - } - wire = *synthesized; - }; - const auto emitEntangler = [&]() { - auto ctrlOp = CtrlOp::create( - builder, loc, ValueRange{wire0}, ValueRange{wire1}, - [&](ValueRange targetArgs) -> SmallVector { - if (emitCz) { - return {ZOp::create(builder, loc, targetArgs[0]).getOutputQubit(0)}; - } - return {XOp::create(builder, loc, targetArgs[0]).getOutputQubit(0)}; - }); - wire0 = ctrlOp.getOutputControl(0); - wire1 = ctrlOp.getOutputTarget(0); - }; - - for (std::uint8_t layer = 0; layer <= numBasisUses; ++layer) { - emitFactor(wire1, static_cast(2 * layer)); - emitFactor(wire0, static_cast((2 * layer) + 1)); - if (layer < numBasisUses) { - emitEntangler(); - } - } - - outQubit0 = wire0; - outQubit1 = wire1; - return success(); -} - -std::optional -twoQubitEntanglerCount(const Matrix4x4& target, const NativeProfileSpec& spec) { - const auto entangler = selectEntangler(spec.gates); - if (!entangler) { - return std::nullopt; - } - const auto native = cachedBasisDecomposer(*entangler).decomposeTarget(target); - if (!native) { - return std::nullopt; - } - return native->numBasisUses; -} - -bool allowsOp(Operation* op, const NativeProfileSpec& spec) { - return TypeSwitch(op) - .Case([](auto) { return true; }) - .Case([&](CtrlOp ctrl) { - const auto kind = entanglerKindFor(ctrl); - return kind && spec.gates.contains(*kind); - }) - .Case( - [&](RZZOp) { return spec.gates.contains(NativeGateKind::RZZ); }) - .Case([&](UnitaryOpInterface unitary) { - if (!unitary.isSingleQubit()) { - return false; - } - const auto gate = gateKindFor(unitary); - return gate && spec.gates.contains(*gate); - }) - .Default([](Operation*) { return false; }); -} - -} // namespace mlir::qco::decomposition diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp index 28dc940aad..b96b4de99f 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp @@ -11,13 +11,15 @@ #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/NativeProfile.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/NativeGateset.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" #include #include #include +#include #include #include #include @@ -39,6 +41,60 @@ namespace mlir::qco { #define GEN_PASS_DEF_FUSETWOQUBITUNITARYRUNS #include "mlir/Dialect/QCO/Transforms/Passes.h.inc" +namespace { + +using decomposition::NativeGateKind; +using decomposition::NativeGateset; + +std::optional nativeGateKindFor(UnitaryOpInterface op) { + return llvm::TypeSwitch>( + op.getOperation()) + .Case([](UOp) { return NativeGateKind::U; }) + .Case([](XOp) { return NativeGateKind::X; }) + .Case([](SXOp) { return NativeGateKind::SX; }) + .Case([](auto) { return NativeGateKind::RZ; }) + .Case([](RXOp) { return NativeGateKind::RX; }) + .Case([](RYOp) { return NativeGateKind::RY; }) + .Case([](ROp) { return NativeGateKind::R; }) + .Default([](Operation*) { return std::nullopt; }); +} + +std::optional nativeEntanglerKindFor(CtrlOp ctrl) { + if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1 || + ctrl.getNumBodyUnitaries() != 1) { + return std::nullopt; + } + return llvm::TypeSwitch>( + ctrl.getBodyUnitary(0).getOperation()) + .Case([](XOp) { return NativeGateKind::CX; }) + .Case([](ZOp) { return NativeGateKind::CZ; }) + .Default([](Operation*) { return std::nullopt; }); +} + +/// Returns true when @p op is already on the resolved native gateset. +/// +/// Barriers and global phase are always allowed. Single-qubit primitives and +/// single-target `CtrlOp` shells (`X`/`Z` bodies) are checked against +/// @p spec.gates. +bool allowsOp(Operation* op, const NativeGateset& spec) { + return llvm::TypeSwitch(op) + .Case([](auto) { return true; }) + .Case([&](CtrlOp ctrl) { + const auto kind = nativeEntanglerKindFor(ctrl); + return kind && spec.gates.contains(*kind); + }) + .Case([&](UnitaryOpInterface unitary) { + if (!unitary.isSingleQubit()) { + return false; + } + const auto gate = nativeGateKindFor(unitary); + return gate && spec.gates.contains(*gate); + }) + .Default([](Operation*) { return false; }); +} + +} // namespace + /// Skips unitaries nested under `ctrl`/`inv` bodies (handled on the shell op). static bool isExcludedFromTopLevelUnitaryWalk(Operation* op) { if (op->getParentOfType()) { @@ -183,10 +239,9 @@ struct OneQubitRun { } // namespace -static void -markNonNativeIfNeeded(FusableTwoQubitRun& run, Operation* op, - const decomposition::NativeProfileSpec& spec) { - if (!decomposition::allowsOp(op, spec)) { +static void markNonNativeIfNeeded(FusableTwoQubitRun& run, Operation* op, + const decomposition::NativeGateset& spec) { + if (!allowsOp(op, spec)) { run.anyNonNative = true; } } @@ -201,9 +256,9 @@ static bool shouldApplyTwoQubitRunReplacement(const FusableTwoQubitRun& run, return numBasisUses < run.numTwoQ; } -static void -absorbTwoQubitIntoRun(FusableTwoQubitRun& run, UnitaryOpInterface op, - const decomposition::NativeProfileSpec& spec) { +static void absorbTwoQubitIntoRun(FusableTwoQubitRun& run, + UnitaryOpInterface op, + const decomposition::NativeGateset& spec) { Matrix4x4 opMatrix; if (!assignTwoQubitOpMatrix(op.getOperation(), opMatrix)) { return; @@ -230,7 +285,7 @@ absorbTwoQubitIntoRun(FusableTwoQubitRun& run, UnitaryOpInterface op, static void absorbOneQubitIntoRun(FusableTwoQubitRun& run, UnitaryOpInterface op, - const decomposition::NativeProfileSpec& spec, + const decomposition::NativeGateset& spec, unsigned wireIndex) { Matrix2x2 raw; if (!op.getUnitaryMatrix2x2(raw)) { @@ -249,7 +304,7 @@ static void absorbOneQubitIntoRun(FusableTwoQubitRun& run, static FusableTwoQubitRun scanFusableTwoQubitRun(UnitaryOpInterface head, - const decomposition::NativeProfileSpec& spec) { + const decomposition::NativeGateset& spec) { FusableTwoQubitRun run; run.tailA = head.getOutputQubit(0); run.tailB = head.getOutputQubit(1); @@ -311,7 +366,7 @@ static void eraseFusableTwoQubitRun(PatternRewriter& rewriter, static bool maybeFuseRun(IRRewriter& rewriter, OneQubitRun& run, const decomposition::EulerBasis basis, - const decomposition::NativeProfileSpec& spec) { + const decomposition::NativeGateset& spec) { Matrix2x2 fused = Matrix2x2::identity(); for (UnitaryOpInterface u : run.ops) { Matrix2x2 m; @@ -322,7 +377,7 @@ static bool maybeFuseRun(IRRewriter& rewriter, OneQubitRun& run, } const bool anyNonNative = llvm::any_of(run.ops, [&](UnitaryOpInterface u) { - return !decomposition::allowsOp(u.getOperation(), spec); + return !allowsOp(u.getOperation(), spec); }); Operation* firstOp = run.ops.front().getOperation(); @@ -344,7 +399,7 @@ static bool maybeFuseRun(IRRewriter& rewriter, OneQubitRun& run, } static bool hasNonNativeOps(Operation* root, - const decomposition::NativeProfileSpec& spec, + const decomposition::NativeGateset& spec, bool singleQubitOnly) { const mlir::WalkResult walkResult = root->walk([&](Operation* op) { if (!isWalkableUnitaryShell(op)) { @@ -358,7 +413,7 @@ static bool hasNonNativeOps(Operation* root, } else if (!llvm::isa(op) && !llvm::isa(op)) { return mlir::WalkResult::advance(); } - if (!decomposition::allowsOp(op, spec)) { + if (!allowsOp(op, spec)) { return mlir::WalkResult::interrupt(); } return mlir::WalkResult::advance(); @@ -368,9 +423,9 @@ static bool hasNonNativeOps(Operation* root, static LogicalResult synthesizeTwoQubitOp( IRRewriter& rewriter, Operation* op, Location loc, Value in0, Value in1, - const decomposition::NativeProfileSpec& spec, llvm::Twine matrixErrorMsg, + const decomposition::NativeGateset& spec, llvm::Twine matrixErrorMsg, llvm::Twine synthesisErrorMsg) { - if (decomposition::allowsOp(op, spec)) { + if (allowsOp(op, spec)) { return success(); } Matrix4x4 matrix; @@ -395,7 +450,7 @@ namespace { struct FuseTwoQubitWindowPattern : public OpInterfaceRewritePattern { FuseTwoQubitWindowPattern(MLIRContext* ctx, - decomposition::NativeProfileSpec specIn) + decomposition::NativeGateset specIn) : OpInterfaceRewritePattern(ctx), spec(std::move(specIn)) {} LogicalResult matchAndRewrite(UnitaryOpInterface op, @@ -409,10 +464,9 @@ struct FuseTwoQubitWindowPattern return failure(); } - const auto numBasisUses = - decomposition::twoQubitEntanglerCount(run.composed, spec); - if (!numBasisUses || - !shouldApplyTwoQubitRunReplacement(run, *numBasisUses)) { + const auto native = spec.decomposeTarget(run.composed); + if (!native || + !shouldApplyTwoQubitRunReplacement(run, native->numBasisUses)) { return failure(); } @@ -436,14 +490,14 @@ struct FuseTwoQubitWindowPattern return success(); } - decomposition::NativeProfileSpec spec; + decomposition::NativeGateset spec; }; } // namespace static LogicalResult fuseTwoQubitUnitaryRuns(Operation* root, - const decomposition::NativeProfileSpec& spec) { + const decomposition::NativeGateset& spec) { RewritePatternSet patterns(root->getContext()); patterns.add(patterns.getContext(), spec); return applyPatternsGreedily(root, std::move(patterns)); @@ -463,16 +517,15 @@ struct FuseTwoQubitUnitaryRunsPass if (llvm::StringRef(nativeGates).trim().empty()) { return; } - auto specOpt = decomposition::parseNativeSpec(nativeGates); + auto specOpt = decomposition::NativeGateset::parse(nativeGates); if (!specOpt) { - getOperation().emitError() - << "unsupported native gate menu (native-gates='" << nativeGates - << "')"; + getOperation().emitError() << "unsupported native gateset (native-gates='" + << nativeGates << "')"; signalPassFailure(); return; } const auto& spec = *specOpt; - const decomposition::EulerBasis oneQubitBasis = spec.eulerBasis(); + const decomposition::EulerBasis oneQubitBasis = *spec.eulerBasis; IRRewriter rewriter(&getContext()); @@ -521,7 +574,7 @@ struct FuseTwoQubitUnitaryRunsPass private: void fuseOneQubitRuns(IRRewriter& rewriter, - const decomposition::NativeProfileSpec& spec, + const decomposition::NativeGateset& spec, const decomposition::EulerBasis basis) { SmallVector runs; llvm::DenseMap tailOpToRun; @@ -557,10 +610,9 @@ struct FuseTwoQubitUnitaryRunsPass } } - LogicalResult - synthesizeRemainingOps(IRRewriter& rewriter, - const decomposition::NativeProfileSpec& spec, - const decomposition::EulerBasis basis) { + LogicalResult synthesizeRemainingOps(IRRewriter& rewriter, + const decomposition::NativeGateset& spec, + const decomposition::EulerBasis basis) { SmallVector ops; collectUnitaryOpsInPreOrder(getOperation(), ops); llvm::DenseSet erasedOps; @@ -574,7 +626,7 @@ struct FuseTwoQubitUnitaryRunsPass } if (auto ctrl = llvm::dyn_cast(op)) { - const bool wasAlreadyNative = decomposition::allowsOp(op, spec); + const bool wasAlreadyNative = allowsOp(op, spec); if (failed(synthesizeControlled(rewriter, ctrl, spec))) { return failure(); } @@ -590,7 +642,7 @@ struct FuseTwoQubitUnitaryRunsPass } if (unitary.isSingleQubit()) { - if (!decomposition::allowsOp(op, spec)) { + if (!allowsOp(op, spec)) { if (failed(rewriteSingleQubit(rewriter, op, unitary, basis))) { return failure(); } @@ -629,24 +681,24 @@ struct FuseTwoQubitUnitaryRunsPass static LogicalResult synthesizeControlled(IRRewriter& rewriter, CtrlOp ctrl, - const decomposition::NativeProfileSpec& spec) { + const decomposition::NativeGateset& spec) { return synthesizeTwoQubitOp( rewriter, ctrl.getOperation(), ctrl.getLoc(), ctrl.getInputControl(0), ctrl.getInputTarget(0), spec, "native synthesis: cannot build a constant 4x4 matrix for this " "controlled gate (unsupported body or non-constant parameters)", - "controlled gate not allowed by selected profile"); + "controlled gate not allowed by selected gateset"); } static LogicalResult synthesizeBareTwoQubit(IRRewriter& rewriter, Operation* op, UnitaryOpInterface unitary, - const decomposition::NativeProfileSpec& spec) { + const decomposition::NativeGateset& spec) { return synthesizeTwoQubitOp( rewriter, op, op->getLoc(), unitary.getInputQubit(0), unitary.getInputQubit(1), spec, - "unsupported two-qubit operation for selected profile", - "unsupported two-qubit operation for selected profile"); + "unsupported two-qubit operation for selected gateset", + "unsupported two-qubit operation for selected gateset"); } }; diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index aae2ab6f87..673cbbce8f 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -867,7 +867,7 @@ TEST_F(CompilerPipelineNativeSynthesisConfigTest, } TEST_F(CompilerPipelineNativeSynthesisConfigTest, - LeavesIRUnchangedWhenNoNativeProfileIsConfigured) { + LeavesIRUnchangedWhenNoNativeGatesetIsConfigured) { config.nativeGates = ""; const auto record = runPipelineAndExpectSuccess(); diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp index 38e283a7b6..2a205fb599 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp @@ -109,9 +109,6 @@ static bool onlyGenericU3CxOps(OwningOpRef& m) { static bool onlyIqmDefaultOps(OwningOpRef& m) { return onlyTheseOps(m, false, true); } -static bool onlyIbmFractionalOps(OwningOpRef& m) { - return onlyTheseOps(m, false, true); -} static bool onlyAxisPairRxRzCxOps(OwningOpRef& m) { return onlyTheseOps(m, true, false); } @@ -457,15 +454,6 @@ static void fusionSwapCxPattern(mlir::qc::QCProgramBuilder& b) { b.cx(q0, q1); } -static void fusionHRzzSRzz(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.h(q0); - b.rzz(-0.29, q0, q1); - b.s(q1); - b.rzz(0.17, q0, q1); -} - static void fusionOffMenuGateInWindow(mlir::qc::QCProgramBuilder& b) { const auto q0 = b.allocQubit(); const auto q1 = b.allocQubit(); @@ -514,18 +502,6 @@ static void zeroAngleThenCz(mlir::qc::QCProgramBuilder& b) { b.cz(q0, q1); } -static void ibmFractionalGateFamilies(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.h(q0); - b.rx(0.13, q1); - b.cx(q0, q1); - b.cz(q1, q0); - b.swap(q0, q1); - b.rzz(-0.33, q0, q1); - b.rzx(0.41, q0, q1); -} - static void hstycxTwoQ(mlir::qc::QCProgramBuilder& b) { const auto q0 = b.allocQubit(); const auto q1 = b.allocQubit(); @@ -646,8 +622,6 @@ INSTANTIATE_TEST_SUITE_P( ProfileCase{"HCxTIbmCz", hCxTOnQ1, "x,sx,rz,cz", onlyIbmBasicCzOps, false}, ProfileCase{"XYSXCzIqm", xYSXCz, "r,cz", onlyIqmDefaultOps, false}, - ProfileCase{"IbmFractional", ibmFractionalGateFamilies, - "x,sx,rz,rx,rzz,cz", onlyIbmFractionalOps, false}, ProfileCase{"HYCxRxRz", hYCx, "rx,rz,cx", onlyAxisPairRxRzCxOps, false}, ProfileCase{"ZCxRxRy", zCx, "rx,ry,cx", onlyAxisPairRxRyCxOps, false}, ProfileCase{"Hq0Yq1CxSq0", hq0Yq1CxSq0, "u,cx", onlyGenericU3CxOps, @@ -780,8 +754,6 @@ INSTANTIATE_TEST_SUITE_P( std::nullopt, false}, FusionCase{"SwappedWireOrder", fusionSwapCxPattern, "u,cx", std::nullopt, std::nullopt, true}, - FusionCase{"RzzBlock", fusionHRzzSRzz, "x,sx,rz,rx,rzz,cz", - std::nullopt, std::nullopt, true}, FusionCase{"OffMenuGateInWindow", fusionOffMenuGateInWindow, "u,cx", std::nullopt, std::nullopt, true}, FusionCase{"DualWireOneQBetweenCx", From 8824b8e44fd5c8cf241460fda53019db756f443a Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 7 Jul 2026 09:58:51 +0200 Subject: [PATCH 104/122] =?UTF-8?q?=E2=8F=AA=EF=B8=8F=20Revert=20changes?= =?UTF-8?q?=20introduced=20after=20merge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 10 +++---- .../Dialect/QCO/IR/test_qco_ir_matrix.cpp | 27 ++++++++++--------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfd8731a16..c9a46b692f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,11 +50,11 @@ releases may include breaking changes. [#1513], [#1521], [#1542], [#1548], [#1550], [#1554], [#1567], [#1569], [#1570], [#1572], [#1573], [#1580], [#1602], [#1620], [#1623], [#1624], [#1626], [#1627], [#1635], [#1638], [#1673], [#1675], [#1700], [#1710], - [#1717], [#1728], [#1730], [#1749], [#1751], [#1762], [#1765], [#1774], - [#1780], [#1781], [#1782], [#1787], [#1802], [#1803], [#1806], [#1807], - [#1808], [#1809], [#1823], [#1824], [#1830], [#1832]) ([**@burgholzer**], - [**@denialhaag**], [**@taminob**], [**@DRovara**], [**@li-mingbao**], - [**@Ectras**], [**@MatthiasReumann**], [**@simon1hofmann**]) + [#1717], [#1728], [#1730], [#1749], [#1751], [#1762], [#1765], [#1780], + [#1781], [#1782], [#1787], [#1806], [#1807], [#1808], [#1823], [#1824], + [#1830]) ([**@burgholzer**], [**@denialhaag**], [**@taminob**], + [**@DRovara**], [**@li-mingbao**], [**@Ectras**], [**@MatthiasReumann**], + [**@simon1hofmann**]) ### Changed diff --git a/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp b/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp index a9eb5d918f..7e0cd58d31 100644 --- a/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp +++ b/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp @@ -64,23 +64,23 @@ expectedMatrixFromComputation(const Fn& build, dd::buildFunctionality(comp, *package).getMatrix(numQubits)); } -static void controlledInverseHT(QCOProgramBuilder& b) { +static void controlledXH(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.ctrl(q[0], q[1], [&](ValueRange targets) { - auto wire = b.inv({targets[0]}, [&](ValueRange innerTargets) { - auto inner = b.h(innerTargets[0]); - inner = b.t(inner); - return SmallVector{inner}; - })[0]; + auto wire = b.x(targets[0]); + wire = b.h(wire); return SmallVector{wire}; }); } -static void controlledXH(QCOProgramBuilder& b) { +static void controlledInverseHT(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.ctrl(q[0], q[1], [&](ValueRange targets) { - auto wire = b.x(targets[0]); - wire = b.h(wire); + auto wire = b.inv({targets[0]}, [&](ValueRange innerTargets) { + auto inner = b.h(innerTargets[0]); + inner = b.t(inner); + return SmallVector{inner}; + })[0]; return SmallVector{wire}; }); } @@ -155,9 +155,12 @@ TEST_F(QCOMatrixTest, ControlledXHOpMatrix) { auto matrix = ctrlOp.getUnitaryMatrix(); ASSERT_TRUE(matrix); - DynamicMatrix expected = DynamicMatrix::identity(4); - expected.setBottomRightCorner(HOp::getUnitaryMatrix() * - XOp::getUnitaryMatrix()); + const Matrix4x4 expected = + expectedMatrixFromComputation([](qc::QuantumComputation& comp) { + comp.addQubitRegister(2, "q"); + comp.cx(1, 0); + comp.ch(1, 0); + }); ASSERT_TRUE(matrix->isApprox(expected)); } From 23e2715ff0a00a7b6cc7bc27136fec8bfb6af8ca Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 7 Jul 2026 10:02:50 +0200 Subject: [PATCH 105/122] =?UTF-8?q?=E2=9C=A8=20Add=20fuse-two-qubit-unitar?= =?UTF-8?q?y-runs=20pass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a `fuse-two-qubit-unitary-runs` MLIR pass that fuses compile-time two-qubit unitary windows and resynthesizes them to a native gateset via Weyl/KAK decomposition. Includes the pass definition, implementation, unit tests, and CMake wiring. Also add the `isEquivalentUpToGlobalPhase` matrix helper used by the new tests to compare unitaries up to a global phase. Assisted-by: Claude Opus 4.8 via Cursor Co-authored-by: Cursor --- CHANGELOG.md | 5 +- .../mlir/Dialect/QCO/Transforms/Passes.td | 48 ++ mlir/include/mlir/Dialect/QCO/Utils/Matrix.h | 27 + .../lib/Dialect/QCO/Transforms/CMakeLists.txt | 10 +- .../FuseTwoQubitUnitaryRuns.cpp | 707 ++++++++++++++++ .../Dialect/QCO/Transforms/CMakeLists.txt | 1 + .../Transforms/NativeSynthesis/CMakeLists.txt | 30 + .../test_fuse_two_qubit_unitary_runs.cpp | 775 ++++++++++++++++++ .../Dialect/QCO/Utils/test_unitary_matrix.cpp | 8 + 9 files changed, 1605 insertions(+), 6 deletions(-) create mode 100644 mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp create mode 100644 mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt create mode 100644 mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 63090a64e2..c9a46b692f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,10 +12,12 @@ releases may include breaking changes. ### Added +- ✨ Add a `fuse-two-qubit-unitary-runs` pass for fusing compile-time two-qubit + unitary windows via Weyl/KAK resynthesis ([#1655]) ([**@simon1hofmann**]) - ✨ Add support for IQM's `move` gate in the QDMI Qiskit backend converter ([#1844], [#1848]) ([**@burgholzer**], [**@marcelwa**]) - 🚸 Add `const` version of the `CompoundOperation`'s `getOps()` function - ([#1826]) ([**@ystade]) + ([#1826]) ([**@ystade**]) - 🐳 Add dev container configuration for consistent local development environment ([#1786]) ([**@denialhaag**]) - ✨ Add two-qubit Weyl (KAK) decomposition and native-gateset synthesis support @@ -651,6 +653,7 @@ changelogs._ [#1664]: https://github.com/munich-quantum-toolkit/core/pull/1664 [#1662]: https://github.com/munich-quantum-toolkit/core/pull/1662 [#1660]: https://github.com/munich-quantum-toolkit/core/pull/1660 +[#1655]: https://github.com/munich-quantum-toolkit/core/pull/1655 [#1652]: https://github.com/munich-quantum-toolkit/core/pull/1652 [#1638]: https://github.com/munich-quantum-toolkit/core/pull/1638 [#1637]: https://github.com/munich-quantum-toolkit/core/pull/1637 diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index 971dc18f71..8053b2f6a3 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -67,6 +67,54 @@ def FuseSingleQubitUnitaryRuns "Target Euler basis (zyz, zxz, xzx, xyx, u, zsxx, r).">]; } +def FuseTwoQubitUnitaryRuns + : Pass<"fuse-two-qubit-unitary-runs", "mlir::ModuleOp"> { + let dependentDialects = ["mlir::qco::QCODialect"]; + let summary = "Lower QCO unitary gates to a user-specified native gate menu."; + let description = [{ + Rewrites unitaries to match the comma-separated `native-gates` menu. + `qco.barrier` and `qco.gphase` are preserved; `qco.ctrl` must have a + single control and a single target. + + The menu is a comma-separated list of gate tokens (order not significant) + from which the pass builds a profile: a single-qubit synthesis mode + (generic `qco.u` when `u` is present; IBM-style surface gates when all of + `x`, `sx`, and `rz`/`p` are present; IQM-style `qco.r` when `r` is present; + or a supported rotation pair chosen from `rx`, `ry`, `rz`) plus optional + two-qubit entanglers `cx`, `cz`, and optional `rzz`. + + Recognised tokens: `u`, `x`, `sx`, `rz` (or `p`), `rx`, `ry`, `r`, `cx`, + `cz`, `rzz`. An empty or whitespace-only menu is a no-op, which is the + intended pipeline default when synthesis is not needed. An unrecognised + token causes the pass to fail. + + Example menus (each line is one illustrative menu; pick either `cx` or + `cz` as the entangler, or list both if both are native): + - IBM basic (no fractional two-qubit): `x,sx,rz,cx` or `x,sx,rz,cz` + - IBM fractional: `x,sx,rz,rx,rzz,cx` or `x,sx,rz,rx,rzz,cz` + - Generic single-qubit U: `u,cx` or `u,cz` + - IQM default: `r,cz` (or `r,cx` if CX is the native entangler) + - Rotation pair + entangler: `rx,rz,cx`, `rx,ry,cz`, `ry,rz,cx`, etc. + Supported pairs are exactly `rx`+`rz`, `rx`+`ry`, and `ry`+`rz`. + + Stages: fuse single-qubit runs; fuse two-qubit windows; up to four + synthesis sweeps over remaining off-menu ops; fuse 1q seams; up to four + further synthesis and fusion rounds until the full menu holds. The pass + fails if anything remains off-menu after those caps. + + Lowering is deterministic: `cx` is preferred over `cz`, single-qubit + factors use the first emitter's Euler basis, and two-qubit window + replacement uses the minimal entangler count from the synthesizer. + }]; + let options = [Option< + "nativeGates", "native-gates", "std::string", "\"\"", + "Comma-separated native gate menu. Empty or whitespace-only is " + "a no-op. Tokens: u, x, sx, rz (or p), rx, ry, r, cx, cz, rzz. " + "Examples: x,sx,rz,cx; x,sx,rz,rx,rzz,cz; u,cx; r,cz; rx,rz,cx.">]; +} + +//===----------------------------------------------------------------------===// + def QuantumLoopUnroll : InterfacePass<"quantum-loop-unroll", "FunctionOpInterface"> { let dependentDialects = ["mlir::qco::QCODialect", "mlir::scf::SCFDialect"]; diff --git a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h index 38816a9c85..d046e7bb58 100644 --- a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h +++ b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -833,6 +834,32 @@ concept SupportedMatrix = std::same_as || std::same_as || std::same_as || std::same_as; +/** + * @brief Checks whether two unitaries are equal up to a global phase. + * + * Uses `trace(rhs.adjoint() * lhs)` to infer a unit-modulus phase factor, then + * compares `lhs` to `factor * rhs` with @ref isApprox. A near-zero overlap + * (`|trace| <= tol`) is treated as not equivalent to avoid dividing by a tiny + * number. + * + * @tparam Matrix Any supported fixed- or dynamic-size matrix type. + * @param lhs Left-hand unitary. + * @param rhs Right-hand unitary. + * @param tol Absolute tolerance for overlap and entry-wise comparison. + * @return True when @p lhs and @p rhs differ only by a global phase. + */ +template +[[nodiscard]] bool isEquivalentUpToGlobalPhase(const Matrix& lhs, + const Matrix& rhs, + double tol = MATRIX_TOLERANCE) { + const auto overlap = (rhs.adjoint() * lhs).trace(); + if (std::abs(overlap) <= tol) { + return false; + } + const auto factor = overlap / std::abs(overlap); + return lhs.isApprox(factor * rhs, tol); +} + /// Scalar-on-the-left multiply `scalar * matrix` (commutes with the member /// `matrix * scalar`). Provided so generic code can scale a matrix from /// either side. diff --git a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt index 77f594db17..77d847d3ba 100644 --- a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt +++ b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt @@ -24,11 +24,11 @@ add_mlir_library( mqt_mlir_target_use_project_options(MLIRQCOTransforms) -# collect header files -file(GLOB_RECURSE PASSES_HEADERS_SOURCE - ${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Dialect/QCO/Transforms/*.h) -file(GLOB_RECURSE PASSES_HEADERS_BUILD - ${MQT_MLIR_BUILD_INCLUDE_DIR}/mlir/Dialect/QCO/Transforms/*.inc) +# collect header files (subdirs: Decomposition/, …) +file(GLOB_RECURSE PASSES_HEADERS_SOURCE CONFIGURE_DEPENDS + "${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Dialect/QCO/Transforms/*.h") +file(GLOB_RECURSE PASSES_HEADERS_BUILD CONFIGURE_DEPENDS + "${MQT_MLIR_BUILD_INCLUDE_DIR}/mlir/Dialect/QCO/Transforms/*.inc") # add public headers using file sets target_sources( diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp new file mode 100644 index 0000000000..b96b4de99f --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp @@ -0,0 +1,707 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/NativeGateset.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" +#include "mlir/Dialect/QCO/Transforms/Passes.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace mlir::qco { + +#define GEN_PASS_DEF_FUSETWOQUBITUNITARYRUNS +#include "mlir/Dialect/QCO/Transforms/Passes.h.inc" + +namespace { + +using decomposition::NativeGateKind; +using decomposition::NativeGateset; + +std::optional nativeGateKindFor(UnitaryOpInterface op) { + return llvm::TypeSwitch>( + op.getOperation()) + .Case([](UOp) { return NativeGateKind::U; }) + .Case([](XOp) { return NativeGateKind::X; }) + .Case([](SXOp) { return NativeGateKind::SX; }) + .Case([](auto) { return NativeGateKind::RZ; }) + .Case([](RXOp) { return NativeGateKind::RX; }) + .Case([](RYOp) { return NativeGateKind::RY; }) + .Case([](ROp) { return NativeGateKind::R; }) + .Default([](Operation*) { return std::nullopt; }); +} + +std::optional nativeEntanglerKindFor(CtrlOp ctrl) { + if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1 || + ctrl.getNumBodyUnitaries() != 1) { + return std::nullopt; + } + return llvm::TypeSwitch>( + ctrl.getBodyUnitary(0).getOperation()) + .Case([](XOp) { return NativeGateKind::CX; }) + .Case([](ZOp) { return NativeGateKind::CZ; }) + .Default([](Operation*) { return std::nullopt; }); +} + +/// Returns true when @p op is already on the resolved native gateset. +/// +/// Barriers and global phase are always allowed. Single-qubit primitives and +/// single-target `CtrlOp` shells (`X`/`Z` bodies) are checked against +/// @p spec.gates. +bool allowsOp(Operation* op, const NativeGateset& spec) { + return llvm::TypeSwitch(op) + .Case([](auto) { return true; }) + .Case([&](CtrlOp ctrl) { + const auto kind = nativeEntanglerKindFor(ctrl); + return kind && spec.gates.contains(*kind); + }) + .Case([&](UnitaryOpInterface unitary) { + if (!unitary.isSingleQubit()) { + return false; + } + const auto gate = nativeGateKindFor(unitary); + return gate && spec.gates.contains(*gate); + }) + .Default([](Operation*) { return false; }); +} + +} // namespace + +/// Skips unitaries nested under `ctrl`/`inv` bodies (handled on the shell op). +static bool isExcludedFromTopLevelUnitaryWalk(Operation* op) { + if (op->getParentOfType()) { + return true; + } + return !llvm::isa(op) && op->getParentOfType(); +} + +static bool isWalkableUnitaryShell(Operation* op) { + return !llvm::isa(op) && + !isExcludedFromTopLevelUnitaryWalk(op); +} + +static void collectUnitaryOpsInPreOrder(Operation* root, + SmallVectorImpl& ops) { + root->walk([&](Operation* op) { + if (isExcludedFromTopLevelUnitaryWalk(op)) { + return; + } + if (llvm::isa(op)) { + ops.push_back(op); + } + }); +} + +static Value emitSingleQubitMatrix(IRRewriter& rewriter, Location loc, + Value inQubit, const Matrix2x2& matrix, + const decomposition::EulerBasis basis) { + return *decomposition::synthesizeUnitary1QEuler( + rewriter, loc, inQubit, matrix, /*runSize=*/0, + /*hasNonBasisGate=*/true, basis); +} + +static bool assignTwoQubitOpMatrix(Operation* op, Matrix4x4& matrix) { + if (llvm::isa(op)) { + return false; + } + if (auto ctrl = llvm::dyn_cast(op)) { + if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { + return false; + } + return llvm::cast(ctrl.getOperation()) + .getUnitaryMatrix4x4(matrix); + } + auto unitary = llvm::dyn_cast(op); + if (!unitary || !unitary.isTwoQubit()) { + return false; + } + return unitary.getUnitaryMatrix4x4(matrix); +} + +static bool isOneQubitWindowMember(UnitaryOpInterface unitary) { + if (!unitary || !unitary.isSingleQubit() || + !isWalkableUnitaryShell(unitary.getOperation())) { + return false; + } + Matrix2x2 matrix; + return unitary.getUnitaryMatrix2x2(matrix); +} + +static bool isTwoQubitRunMember(UnitaryOpInterface unitary) { + if (!unitary || !unitary.isTwoQubit() || + !isWalkableUnitaryShell(unitary.getOperation())) { + return false; + } + Matrix4x4 matrix; + return assignTwoQubitOpMatrix(unitary.getOperation(), matrix); +} + +static UnitaryOpInterface fusibleSingleQubitOp(Operation* op) { + if (llvm::isa(op)) { + return {}; + } + auto unitary = llvm::dyn_cast(op); + return isOneQubitWindowMember(unitary) ? unitary : UnitaryOpInterface{}; +} + +static UnitaryOpInterface uniqueUnitaryUser(Value wire) { + if (!wire.hasOneUse()) { + return {}; + } + auto unitary = llvm::dyn_cast(*wire.user_begin()); + if (!unitary) { + return {}; + } + if (unitary.isTwoQubit()) { + return isTwoQubitRunMember(unitary) ? unitary : UnitaryOpInterface{}; + } + if (unitary.isSingleQubit()) { + return isOneQubitWindowMember(unitary) ? unitary : UnitaryOpInterface{}; + } + return {}; +} + +static Operation* twoQubitGateAtEndOfOneQChain(Value wire) { + Value cur = wire; + while (Operation* def = cur.getDefiningOp()) { + auto unitary = llvm::dyn_cast(def); + if (!unitary) { + return nullptr; + } + if (unitary.isTwoQubit()) { + return isTwoQubitRunMember(unitary) ? def : nullptr; + } + if (!isOneQubitWindowMember(unitary)) { + return nullptr; + } + cur = unitary.getInputQubit(0); + } + return nullptr; +} + +static bool feedsFromSameTwoQubitWindow(UnitaryOpInterface op) { + const Value in0 = op.getInputQubit(0); + const Value in1 = op.getInputQubit(1); + if (!in0.hasOneUse() || !in1.hasOneUse()) { + return false; + } + Operation* gate0 = twoQubitGateAtEndOfOneQChain(in0); + Operation* gate1 = twoQubitGateAtEndOfOneQChain(in1); + return gate0 != nullptr && gate0 == gate1; +} + +static bool isTwoQubitRunStart(UnitaryOpInterface op) { + return isTwoQubitRunMember(op) && !feedsFromSameTwoQubitWindow(op); +} + +namespace { + +struct FusableTwoQubitRun { + SmallVector ops; + Matrix4x4 composed = Matrix4x4::identity(); + unsigned numTwoQ = 0; + bool anyNonNative = false; + Value tailA; + Value tailB; +}; + +struct OneQubitRun { + SmallVector ops; +}; + +} // namespace + +static void markNonNativeIfNeeded(FusableTwoQubitRun& run, Operation* op, + const decomposition::NativeGateset& spec) { + if (!allowsOp(op, spec)) { + run.anyNonNative = true; + } +} + +// Replace when off-menu ops must be lowered, or when resynthesis uses fewer +// entanglers than the fused window. +static bool shouldApplyTwoQubitRunReplacement(const FusableTwoQubitRun& run, + std::uint8_t numBasisUses) { + if (run.anyNonNative) { + return true; + } + return numBasisUses < run.numTwoQ; +} + +static void absorbTwoQubitIntoRun(FusableTwoQubitRun& run, + UnitaryOpInterface op, + const decomposition::NativeGateset& spec) { + Matrix4x4 opMatrix; + if (!assignTwoQubitOpMatrix(op.getOperation(), opMatrix)) { + return; + } + const Value in0 = op.getInputQubit(0); + const Value in1 = op.getInputQubit(1); + SmallVector ids; + if (in0 == run.tailA && in1 == run.tailB) { + ids = {0, 1}; + run.tailA = op.getOutputQubit(0); + run.tailB = op.getOutputQubit(1); + } else if (in0 == run.tailB && in1 == run.tailA) { + ids = {1, 0}; + run.tailA = op.getOutputQubit(1); + run.tailB = op.getOutputQubit(0); + } else { + return; + } + run.composed = opMatrix.reorderForQubits(ids[0], ids[1]) * run.composed; + run.ops.push_back(op.getOperation()); + ++run.numTwoQ; + markNonNativeIfNeeded(run, op.getOperation(), spec); +} + +static void absorbOneQubitIntoRun(FusableTwoQubitRun& run, + UnitaryOpInterface op, + const decomposition::NativeGateset& spec, + unsigned wireIndex) { + Matrix2x2 raw; + if (!op.getUnitaryMatrix2x2(raw)) { + return; + } + const auto pad = raw.embedInTwoQubit(wireIndex); + run.composed = pad * run.composed; + run.ops.push_back(op.getOperation()); + markNonNativeIfNeeded(run, op.getOperation(), spec); + if (wireIndex == 0) { + run.tailA = op.getOutputQubit(0); + } else { + run.tailB = op.getOutputQubit(0); + } +} + +static FusableTwoQubitRun +scanFusableTwoQubitRun(UnitaryOpInterface head, + const decomposition::NativeGateset& spec) { + FusableTwoQubitRun run; + run.tailA = head.getOutputQubit(0); + run.tailB = head.getOutputQubit(1); + run.ops.push_back(head.getOperation()); + run.numTwoQ = 1; + if (!assignTwoQubitOpMatrix(head.getOperation(), run.composed)) { + run.composed = Matrix4x4::identity(); + run.numTwoQ = 0; + run.ops.clear(); + return run; + } + markNonNativeIfNeeded(run, head.getOperation(), spec); + + while (true) { + UnitaryOpInterface nextOnA = uniqueUnitaryUser(run.tailA); + UnitaryOpInterface nextOnB = uniqueUnitaryUser(run.tailB); + + if (nextOnA && nextOnB && + nextOnA.getOperation() == nextOnB.getOperation() && + nextOnA.isTwoQubit()) { + absorbTwoQubitIntoRun(run, nextOnA, spec); + continue; + } + + if (nextOnA && nextOnB && + nextOnA.getOperation() != nextOnB.getOperation() && + nextOnA.isSingleQubit() && nextOnB.isSingleQubit()) { + if (nextOnA->isBeforeInBlock(nextOnB)) { + absorbOneQubitIntoRun(run, nextOnA, spec, /*wireIndex=*/0); + continue; + } + absorbOneQubitIntoRun(run, nextOnB, spec, /*wireIndex=*/1); + continue; + } + + if (nextOnA && nextOnA.isSingleQubit() && + (!nextOnB || nextOnA.getOperation() != nextOnB.getOperation())) { + absorbOneQubitIntoRun(run, nextOnA, spec, /*wireIndex=*/0); + continue; + } + + if (nextOnB && nextOnB.isSingleQubit() && + (!nextOnA || nextOnB.getOperation() != nextOnA.getOperation())) { + absorbOneQubitIntoRun(run, nextOnB, spec, /*wireIndex=*/1); + continue; + } + + break; + } + return run; +} + +static void eraseFusableTwoQubitRun(PatternRewriter& rewriter, + const FusableTwoQubitRun& run) { + for (Operation* op : llvm::reverse(run.ops)) { + rewriter.eraseOp(op); + } +} + +static bool maybeFuseRun(IRRewriter& rewriter, OneQubitRun& run, + const decomposition::EulerBasis basis, + const decomposition::NativeGateset& spec) { + Matrix2x2 fused = Matrix2x2::identity(); + for (UnitaryOpInterface u : run.ops) { + Matrix2x2 m; + if (!u.getUnitaryMatrix2x2(m)) { + return false; + } + fused.premultiplyBy(m); + } + + const bool anyNonNative = llvm::any_of(run.ops, [&](UnitaryOpInterface u) { + return !allowsOp(u.getOperation(), spec); + }); + + Operation* firstOp = run.ops.front().getOperation(); + const Value inQubit = run.ops.front().getInputQubit(0); + const Value outQubit = run.ops.back().getOutputQubit(0); + + rewriter.setInsertionPoint(firstOp); + const auto replacement = decomposition::synthesizeUnitary1QEuler( + rewriter, firstOp->getLoc(), inQubit, fused, run.ops.size(), anyNonNative, + basis); + if (!replacement) { + return false; + } + rewriter.replaceAllUsesWith(outQubit, *replacement); + for (auto& op : llvm::reverse(run.ops)) { + rewriter.eraseOp(op.getOperation()); + } + return true; +} + +static bool hasNonNativeOps(Operation* root, + const decomposition::NativeGateset& spec, + bool singleQubitOnly) { + const mlir::WalkResult walkResult = root->walk([&](Operation* op) { + if (!isWalkableUnitaryShell(op)) { + return mlir::WalkResult::advance(); + } + if (singleQubitOnly) { + auto unitary = llvm::dyn_cast(op); + if (!unitary || !unitary.isSingleQubit()) { + return mlir::WalkResult::advance(); + } + } else if (!llvm::isa(op) && !llvm::isa(op)) { + return mlir::WalkResult::advance(); + } + if (!allowsOp(op, spec)) { + return mlir::WalkResult::interrupt(); + } + return mlir::WalkResult::advance(); + }); + return walkResult.wasInterrupted(); +} + +static LogicalResult synthesizeTwoQubitOp( + IRRewriter& rewriter, Operation* op, Location loc, Value in0, Value in1, + const decomposition::NativeGateset& spec, llvm::Twine matrixErrorMsg, + llvm::Twine synthesisErrorMsg) { + if (allowsOp(op, spec)) { + return success(); + } + Matrix4x4 matrix; + if (!assignTwoQubitOpMatrix(op, matrix)) { + op->emitError(matrixErrorMsg); + return failure(); + } + rewriter.setInsertionPoint(op); + Value out0; + Value out1; + if (failed(decomposition::synthesizeUnitary2QWeyl( + rewriter, loc, in0, in1, matrix, spec, out0, out1))) { + op->emitError(synthesisErrorMsg); + return failure(); + } + rewriter.replaceOp(op, ValueRange{out0, out1}); + return success(); +} + +namespace { + +struct FuseTwoQubitWindowPattern + : public OpInterfaceRewritePattern { + FuseTwoQubitWindowPattern(MLIRContext* ctx, + decomposition::NativeGateset specIn) + : OpInterfaceRewritePattern(ctx), spec(std::move(specIn)) {} + + LogicalResult matchAndRewrite(UnitaryOpInterface op, + PatternRewriter& rewriter) const override { + if (!isTwoQubitRunStart(op)) { + return failure(); + } + + FusableTwoQubitRun run = scanFusableTwoQubitRun(op, spec); + if (run.ops.size() < 2) { + return failure(); + } + + const auto native = spec.decomposeTarget(run.composed); + if (!native || + !shouldApplyTwoQubitRunReplacement(run, native->numBasisUses)) { + return failure(); + } + + Operation* firstOp = run.ops.front(); + auto firstUnitary = llvm::cast(firstOp); + const Value inA = firstUnitary.getInputQubit(0); + const Value inB = firstUnitary.getInputQubit(1); + + rewriter.setInsertionPoint(firstOp); + Value newA; + Value newB; + if (failed(decomposition::synthesizeUnitary2QWeyl( + rewriter, firstOp->getLoc(), inA, inB, run.composed, spec, newA, + newB))) { + firstOp->emitError("failed to emit synthesized two-qubit gate sequence"); + return failure(); + } + rewriter.replaceAllUsesWith(run.tailA, newA); + rewriter.replaceAllUsesWith(run.tailB, newB); + eraseFusableTwoQubitRun(rewriter, run); + return success(); + } + + decomposition::NativeGateset spec; +}; + +} // namespace + +static LogicalResult +fuseTwoQubitUnitaryRuns(Operation* root, + const decomposition::NativeGateset& spec) { + RewritePatternSet patterns(root->getContext()); + patterns.add(patterns.getContext(), spec); + return applyPatternsGreedily(root, std::move(patterns)); +} + +namespace { + +struct FuseTwoQubitUnitaryRunsPass + : impl::FuseTwoQubitUnitaryRunsBase { + FuseTwoQubitUnitaryRunsPass() = default; + + explicit FuseTwoQubitUnitaryRunsPass(FuseTwoQubitUnitaryRunsOptions options) + : FuseTwoQubitUnitaryRunsBase(std::move(options)) {} + +protected: + void runOnOperation() override { + if (llvm::StringRef(nativeGates).trim().empty()) { + return; + } + auto specOpt = decomposition::NativeGateset::parse(nativeGates); + if (!specOpt) { + getOperation().emitError() << "unsupported native gateset (native-gates='" + << nativeGates << "')"; + signalPassFailure(); + return; + } + const auto& spec = *specOpt; + const decomposition::EulerBasis oneQubitBasis = *spec.eulerBasis; + + IRRewriter rewriter(&getContext()); + + fuseOneQubitRuns(rewriter, spec, oneQubitBasis); + if (failed(fuseTwoQubitUnitaryRuns(getOperation(), spec))) { + signalPassFailure(); + return; + } + constexpr unsigned kMaxSynthesisSweeps = 4; + for (unsigned i = 0; i < kMaxSynthesisSweeps; ++i) { + if (failed(synthesizeRemainingOps(rewriter, spec, oneQubitBasis))) { + signalPassFailure(); + return; + } + if (!hasNonNativeOps(getOperation(), spec, /*singleQubitOnly=*/true)) { + break; + } + } + if (hasNonNativeOps(getOperation(), spec, /*singleQubitOnly=*/true)) { + getOperation().emitError() + << "native gate synthesis did not converge within " + << kMaxSynthesisSweeps + << " sweeps (single-qubit ops remain outside the native menu)"; + signalPassFailure(); + return; + } + fuseOneQubitRuns(rewriter, spec, oneQubitBasis); + constexpr unsigned kPostMenuCleanupSweeps = 4; + unsigned postMenuSweepsRemaining = kPostMenuCleanupSweeps; + while (hasNonNativeOps(getOperation(), spec, /*singleQubitOnly=*/false) && + postMenuSweepsRemaining-- > 0) { + if (failed(synthesizeRemainingOps(rewriter, spec, oneQubitBasis))) { + signalPassFailure(); + return; + } + fuseOneQubitRuns(rewriter, spec, oneQubitBasis); + } + if (hasNonNativeOps(getOperation(), spec, /*singleQubitOnly=*/false)) { + getOperation().emitError() + << "native gate synthesis: operations remain outside the native menu " + "after final cleanup"; + signalPassFailure(); + return; + } + } + +private: + void fuseOneQubitRuns(IRRewriter& rewriter, + const decomposition::NativeGateset& spec, + const decomposition::EulerBasis basis) { + SmallVector runs; + llvm::DenseMap tailOpToRun; + + // Require single-use tail output so fan-out wires are not fused away. + getOperation()->walk([&](Operation* op) { + auto unitary = fusibleSingleQubitOp(op); + if (!unitary) { + return; + } + Value inQubit = unitary.getInputQubit(0); + Operation* defOp = inQubit.getDefiningOp(); + auto it = + (defOp != nullptr) ? tailOpToRun.find(defOp) : tailOpToRun.end(); + const bool canExtend = it != tailOpToRun.end() && inQubit.hasOneUse(); + if (canExtend) { + const size_t runIdx = it->second; + runs[runIdx].ops.push_back(unitary); + tailOpToRun.erase(it); + tailOpToRun[op] = runIdx; + } else { + runs.push_back(OneQubitRun{}); + runs.back().ops.push_back(unitary); + tailOpToRun[op] = runs.size() - 1; + } + }); + + for (auto& run : runs) { + if (run.ops.size() < 2) { + continue; + } + (void)maybeFuseRun(rewriter, run, basis, spec); + } + } + + LogicalResult synthesizeRemainingOps(IRRewriter& rewriter, + const decomposition::NativeGateset& spec, + const decomposition::EulerBasis basis) { + SmallVector ops; + collectUnitaryOpsInPreOrder(getOperation(), ops); + llvm::DenseSet erasedOps; + + for (Operation* op : ops) { + if (erasedOps.contains(op)) { + continue; + } + if (!isWalkableUnitaryShell(op)) { + continue; + } + + if (auto ctrl = llvm::dyn_cast(op)) { + const bool wasAlreadyNative = allowsOp(op, spec); + if (failed(synthesizeControlled(rewriter, ctrl, spec))) { + return failure(); + } + if (!wasAlreadyNative) { + erasedOps.insert(op); + } + continue; + } + + auto unitary = llvm::dyn_cast(op); + if (!unitary) { + continue; + } + + if (unitary.isSingleQubit()) { + if (!allowsOp(op, spec)) { + if (failed(rewriteSingleQubit(rewriter, op, unitary, basis))) { + return failure(); + } + erasedOps.insert(op); + } + continue; + } + + if (unitary.isTwoQubit()) { + if (failed(synthesizeBareTwoQubit(rewriter, op, unitary, spec))) { + return failure(); + } + erasedOps.insert(op); + } + } + return success(); + } + + static LogicalResult + rewriteSingleQubit(IRRewriter& rewriter, Operation* op, + UnitaryOpInterface unitary, + const decomposition::EulerBasis basis) { + rewriter.setInsertionPoint(op); + const Value in = unitary.getInputQubit(0); + Matrix2x2 matrix; + if (!unitary.getUnitaryMatrix2x2(matrix)) { + op->emitError("single-qubit operation with non-constant parameters is " + "not supported for native synthesis"); + return failure(); + } + const Value replaced = + emitSingleQubitMatrix(rewriter, op->getLoc(), in, matrix, basis); + rewriter.replaceOp(op, replaced); + return success(); + } + + static LogicalResult + synthesizeControlled(IRRewriter& rewriter, CtrlOp ctrl, + const decomposition::NativeGateset& spec) { + return synthesizeTwoQubitOp( + rewriter, ctrl.getOperation(), ctrl.getLoc(), ctrl.getInputControl(0), + ctrl.getInputTarget(0), spec, + "native synthesis: cannot build a constant 4x4 matrix for this " + "controlled gate (unsupported body or non-constant parameters)", + "controlled gate not allowed by selected gateset"); + } + + static LogicalResult + synthesizeBareTwoQubit(IRRewriter& rewriter, Operation* op, + UnitaryOpInterface unitary, + const decomposition::NativeGateset& spec) { + return synthesizeTwoQubitOp( + rewriter, op, op->getLoc(), unitary.getInputQubit(0), + unitary.getInputQubit(1), spec, + "unsupported two-qubit operation for selected gateset", + "unsupported two-qubit operation for selected gateset"); + } +}; + +} // namespace + +} // namespace mlir::qco diff --git a/mlir/unittests/Dialect/QCO/Transforms/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/CMakeLists.txt index d59780f461..163412b775 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/CMakeLists.txt +++ b/mlir/unittests/Dialect/QCO/Transforms/CMakeLists.txt @@ -8,4 +8,5 @@ add_subdirectory(Decomposition) add_subdirectory(Mapping) +add_subdirectory(NativeSynthesis) add_subdirectory(Optimizations) diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt new file mode 100644 index 0000000000..0fa87a5daf --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/CMakeLists.txt @@ -0,0 +1,30 @@ +# 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-fuse-two-qubit-unitary-runs) +add_executable(${target_name} test_fuse_two_qubit_unitary_runs.cpp) + +target_link_libraries( + ${target_name} + PRIVATE MLIRParser + GTest::gtest_main + MLIRQCPrograms + MLIRQCOProgramBuilder + MLIRQCOUtils + MLIRQCToQCO + MLIRQCOTransforms + MLIRPass + MLIRFuncDialect + MLIRArithDialect + MLIRIR + MLIRSupport + LLVMSupport) + +mqt_mlir_configure_unittest_target(${target_name}) + +gtest_discover_tests(${target_name} PROPERTIES LABELS mqt-mlir-unittests DISCOVERY_TIMEOUT 60) diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp new file mode 100644 index 0000000000..2a205fb599 --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp @@ -0,0 +1,775 @@ +/* + * 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 "TestCaseUtils.h" +#include "mlir/Conversion/QCToQCO/QCToQCO.h" +#include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Passes.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" +#include "qc_programs.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace mlir; +using namespace mlir::qco; +using namespace mqt::test; + +using ProgramFn = void (*)(mlir::qc::QCProgramBuilder&); +using NativePredicate = bool (*)(OwningOpRef&); + +static void controlledXH(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.ctrl(q0, {q1}, [&](ValueRange targets) { + b.x(targets[0]); + b.h(targets[0]); + }); +} + +template +static bool onlyTheseOps(OwningOpRef& moduleOp, bool allowCx, + bool allowCz) { + bool ok = true; + std::ignore = moduleOp->walk([&](UnitaryOpInterface op) { + Operation* raw = op.getOperation(); + if (llvm::isa_and_present(raw->getParentOp())) { + return WalkResult::advance(); + } + if (llvm::isa(raw)) { + return WalkResult::advance(); + } + if (auto ctrl = llvm::dyn_cast(raw)) { + if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { + ok = false; + return WalkResult::interrupt(); + } + Operation* body = ctrl.getBodyUnitary(0).getOperation(); + const bool isCx = llvm::isa(body); + const bool isCz = llvm::isa(body); + if ((isCx && allowCx) || (isCz && allowCz)) { + return WalkResult::advance(); + } + ok = false; + return WalkResult::interrupt(); + } + if (!llvm::isa(raw)) { + ok = false; + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + return ok; +} + +static bool onlyIbmBasicCxOps(OwningOpRef& m) { + return onlyTheseOps(m, true, false); +} +static bool onlyIbmBasicCzOps(OwningOpRef& m) { + return onlyTheseOps(m, false, true); +} +static bool onlyGenericU3CxOps(OwningOpRef& m) { + return onlyTheseOps(m, true, false); +} +static bool onlyIqmDefaultOps(OwningOpRef& m) { + return onlyTheseOps(m, false, true); +} +static bool onlyAxisPairRxRzCxOps(OwningOpRef& m) { + return onlyTheseOps(m, true, false); +} +static bool onlyAxisPairRxRyCxOps(OwningOpRef& m) { + return onlyTheseOps(m, true, false); +} +static bool onlyAxisPairRyRzCzOps(OwningOpRef& m) { + return onlyTheseOps(m, false, true); +} +static bool onlyGenericU3CxOrCzOps(OwningOpRef& m) { + return onlyTheseOps(m, true, true); +} + +static std::optional unitaryQubitOperand(UnitaryOpInterface op, + std::size_t index) { + if (index >= op.getNumQubits()) { + return std::nullopt; + } + Value v = op->getOperand(index); + if (!llvm::isa(v.getType())) { + return std::nullopt; + } + return v; +} + +static std::optional unitaryQubitResult(UnitaryOpInterface op, + std::size_t index) { + if (index >= op.getNumQubits()) { + return std::nullopt; + } + Value v = op->getResult(index); + if (!llvm::isa(v.getType())) { + return std::nullopt; + } + return v; +} + +static bool extractSingleQubitMatrix(UnitaryOpInterface op, Matrix2x2& out) { + if (op.getUnitaryMatrix2x2(out)) { + return true; + } + DynamicMatrix dynamic; + if (!op.getUnitaryMatrixDynamic(dynamic) || dynamic.rows() != 2 || + dynamic.cols() != 2) { + return false; + } + out = Matrix2x2::fromElements(dynamic(0, 0), dynamic(0, 1), dynamic(1, 0), + dynamic(1, 1)); + return true; +} + +static bool extractTwoQubitMatrix(UnitaryOpInterface op, Matrix4x4& out) { + if (auto ctrl = llvm::dyn_cast(op.getOperation())) { + if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { + return false; + } + return op.getUnitaryMatrix4x4(out); + } + return op.getUnitaryMatrix4x4(out); +} + +static std::optional +computeUnitaryFromQcoModule(const OwningOpRef& moduleOp) { + ModuleOp module = moduleOp.get(); + if (!module) { + return std::nullopt; + } + + llvm::DenseMap qubitIds; + std::size_t nextQubitId = 0; + std::size_t numQubits = 0; + + for (auto func : module.getOps()) { + for (auto& block : func.getBlocks()) { + for (auto& rawOp : block.getOperations()) { + if (auto staticOp = llvm::dyn_cast(&rawOp)) { + const auto index = static_cast(staticOp.getIndex()); + qubitIds.try_emplace(staticOp.getQubit(), index); + numQubits = std::max(numQubits, index + 1); + } else if (auto alloc = llvm::dyn_cast(&rawOp)) { + qubitIds.try_emplace(alloc.getResult(), nextQubitId++); + numQubits = std::max(numQubits, nextQubitId); + } + } + } + } + + if (numQubits == 0) { + return std::nullopt; + } + + DynamicMatrix unitary = + DynamicMatrix::identity(static_cast(1ULL << numQubits)); + + auto getQubitId = [&](Value qubit) -> std::optional { + const auto it = qubitIds.find(qubit); + if (it == qubitIds.end()) { + return std::nullopt; + } + return it->second; + }; + + for (auto func : module.getOps()) { + for (auto& block : func.getBlocks()) { + for (auto& rawOp : block.getOperations()) { + auto op = llvm::dyn_cast(&rawOp); + if (!op) { + continue; + } + if (llvm::isa(op.getOperation())) { + continue; + } + + if (op.isSingleQubit()) { + const auto qIn = unitaryQubitOperand(op, 0); + if (!qIn) { + return std::nullopt; + } + const auto qid = getQubitId(*qIn); + if (!qid) { + return std::nullopt; + } + Matrix2x2 oneQ; + if (!extractSingleQubitMatrix(op, oneQ)) { + return std::nullopt; + } + unitary = oneQ.embedInNqubit(numQubits, *qid) * unitary; + const auto qOut = unitaryQubitResult(op, 0); + if (!qOut) { + return std::nullopt; + } + qubitIds[*qOut] = *qid; + continue; + } + + if (op.isTwoQubit()) { + const auto q0In = unitaryQubitOperand(op, 0); + const auto q1In = unitaryQubitOperand(op, 1); + if (!q0In || !q1In) { + return std::nullopt; + } + const auto q0id = getQubitId(*q0In); + const auto q1id = getQubitId(*q1In); + if (!q0id || !q1id) { + return std::nullopt; + } + Matrix4x4 twoQ; + if (!extractTwoQubitMatrix(op, twoQ)) { + return std::nullopt; + } + unitary = twoQ.embedInNqubit(numQubits, *q0id, *q1id) * unitary; + const auto q0Out = unitaryQubitResult(op, 0); + const auto q1Out = unitaryQubitResult(op, 1); + if (!q0Out || !q1Out) { + return std::nullopt; + } + qubitIds[*q0Out] = *q0id; + qubitIds[*q1Out] = *q1id; + continue; + } + + return std::nullopt; + } + } + } + + return unitary; +} + +namespace { + +struct ProfileCase { + const char* name; + ProgramFn program; + const char* nativeGates; + NativePredicate isNative; + bool checkEquivalence; +}; + +struct FusionCase { + const char* name; + ProgramFn program; + const char* nativeGates; + std::optional exactCtrlCount; + std::optional minCtrlCount; + bool checkTwoQUnitary; +}; + +class FuseTwoQubitUnitaryRunsPassTest : public testing::Test { +protected: + void SetUp() override { + DialectRegistry registry; + registry.insert(); + context = std::make_unique(); + context->appendDialectRegistry(registry); + context->loadAllAvailableDialects(); + } + + static void runFusePipeline(OwningOpRef& moduleOp, + StringRef nativeGates) { + PassManager pm(moduleOp->getContext()); + pm.addPass(createQCToQCO()); + pm.addPass(createFuseTwoQubitUnitaryRuns(FuseTwoQubitUnitaryRunsOptions{ + .nativeGates = nativeGates.str(), + })); + ASSERT_TRUE(succeeded(pm.run(*moduleOp))); + } + + static void runQcToQco(OwningOpRef& moduleOp) { + PassManager pm(moduleOp->getContext()); + pm.addPass(createQCToQCO()); + ASSERT_TRUE(succeeded(pm.run(*moduleOp))); + } + + static void runTwoQFuse(OwningOpRef& moduleOp, + StringRef nativeGates) { + PassManager pm(moduleOp->getContext()); + pm.addPass(createFuseTwoQubitUnitaryRuns(FuseTwoQubitUnitaryRunsOptions{ + .nativeGates = nativeGates.str(), + })); + ASSERT_TRUE(succeeded(pm.run(*moduleOp))); + } + + static void expectQcoModulesEquivalent(const OwningOpRef& lhs, + const OwningOpRef& rhs) { + const auto lhsUnitary = computeUnitaryFromQcoModule(lhs); + ASSERT_TRUE(lhsUnitary.has_value()); + const auto rhsUnitary = computeUnitaryFromQcoModule(rhs); + ASSERT_TRUE(rhsUnitary.has_value()); + EXPECT_TRUE(isEquivalentUpToGlobalPhase(*lhsUnitary, *rhsUnitary)); + } + + void expectNativeAfterSynthesis(ProgramFn program, StringRef nativeGates, + NativePredicate isNative) { + auto moduleOp = mlir::qc::QCProgramBuilder::build(context.get(), program); + runFusePipeline(moduleOp, nativeGates); + EXPECT_TRUE(isNative(moduleOp)); + } + + void expectEquivalentAndNativeAfterSynthesis(ProgramFn program, + StringRef nativeGates, + NativePredicate isNative) { + auto expected = mlir::qc::QCProgramBuilder::build(context.get(), program); + runQcToQco(expected); + auto synthesized = + mlir::qc::QCProgramBuilder::build(context.get(), program); + runFusePipeline(synthesized, nativeGates); + EXPECT_TRUE(isNative(synthesized)); + expectQcoModulesEquivalent(expected, synthesized); + } + + void expectSynthesisFailure(ProgramFn program, StringRef nativeGates) { + auto moduleOp = mlir::qc::QCProgramBuilder::build(context.get(), program); + PassManager pm(moduleOp->getContext()); + pm.addPass(createQCToQCO()); + pm.addPass(createFuseTwoQubitUnitaryRuns(FuseTwoQubitUnitaryRunsOptions{ + .nativeGates = nativeGates.str(), + })); + EXPECT_TRUE(failed(pm.run(*moduleOp))); + } + + void expectTwoQFusePreservesUnitary(ProgramFn program, + StringRef nativeGates) { + auto expected = mlir::qc::QCProgramBuilder::build(context.get(), program); + ASSERT_TRUE(expected); + runQcToQco(expected); + auto fused = mlir::qc::QCProgramBuilder::build(context.get(), program); + ASSERT_TRUE(fused); + runQcToQco(fused); + runTwoQFuse(fused, nativeGates); + ASSERT_TRUE(succeeded(verify(*fused))); + expectQcoModulesEquivalent(expected, fused); + } + + static std::size_t countCtrlOps(const OwningOpRef& moduleOp) { + std::size_t count = 0; + moduleOp.get()->walk([&](CtrlOp) { ++count; }); + return count; + } + + std::unique_ptr context; +}; + +class FuseTwoQubitProfileTest + : public FuseTwoQubitUnitaryRunsPassTest, + public testing::WithParamInterface {}; + +class FuseTwoQubitFusionTest : public FuseTwoQubitUnitaryRunsPassTest, + public testing::WithParamInterface { +}; + +} // namespace + +static void fusionCxCx(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.cx(q0, q1); + b.cx(q0, q1); +} + +static void fusionHCxInterleavedTCx(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.cx(q0, q1); + b.t(q1); + b.s(q0); + b.cx(q0, q1); +} + +static void fusionThreeLineCx(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + const auto q2 = b.allocQubit(); + b.cx(q0, q1); + b.cx(q1, q2); + b.cx(q0, q1); +} + +static void fusionCxRSharedOtherPair(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + const auto q2 = b.allocQubit(); + b.cx(q0, q1); + b.rz(0.17, q1); + b.cx(q1, q2); +} + +static void fusionCxBarrierCx(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.cx(q0, q1); + b.barrier({q0, q1}); + b.cx(q0, q1); +} + +static void fusionSwapCxPattern(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.cx(q0, q1); + b.cx(q1, q0); + b.cx(q0, q1); +} + +static void fusionOffMenuGateInWindow(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.cx(q0, q1); + b.h(q0); + b.cx(q0, q1); +} + +static void fusionDualWireOneQBetweenCx(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.cx(q0, q1); + b.rz(0.11, q0); + b.ry(0.22, q1); + b.cx(q0, q1); +} + +static void inverseTwoX(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + b.inv({q0}, [&](mlir::ValueRange qubits) { + b.x(qubits[0]); + b.x(qubits[0]); + }); +} + +static void broadOneQThenCz(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.x(q0); + b.y(q1); + b.h(q0); + b.sx(q1); + b.rx(0.13, q0); + b.ry(-0.47, q1); + b.rz(0.29, q0); + b.cz(q0, q1); +} + +static void zeroAngleThenCz(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.rx(0.0, q0); + b.ry(0.0, q1); + b.rz(0.0, q0); + b.p(0.0, q1); + b.cz(q0, q1); +} + +static void hstycxTwoQ(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.s(q0); + b.t(q0); + b.y(q0); + b.cx(q0, q1); +} + +static void cxYOnQ1(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.cx(q0, q1); + b.y(q1); +} + +static void hCxTOnQ1(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q1); + b.cx(q0, q1); + b.t(q1); +} + +static void xYSXCz(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.x(q0); + b.y(q0); + b.sx(q0); + b.cz(q0, q1); +} + +static void hYCx(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.y(q0); + b.cx(q0, q1); +} + +static void zCx(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.z(q0); + b.cx(q0, q1); +} + +static void xHCz(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.x(q0); + b.h(q0); + b.cz(q0, q1); +} + +static void hq0Yq1CxSq0(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.y(q1); + b.cx(q0, q1); + b.s(q0); +} + +static void hCxSq1(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.cx(q0, q1); + b.s(q1); +} + +static void threeQGhz(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + const auto q2 = b.allocQubit(); + b.h(q0); + b.cx(q0, q1); + b.cx(q1, q2); +} + +static void determinismSwap(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.swap(q0, q1); + b.dealloc(q0); + b.dealloc(q1); +} + +TEST_P(FuseTwoQubitProfileTest, SynthesizesToNativeMenu) { + const ProfileCase& c = GetParam(); + if (c.checkEquivalence) { + expectEquivalentAndNativeAfterSynthesis(c.program, c.nativeGates, + c.isNative); + } else { + expectNativeAfterSynthesis(c.program, c.nativeGates, c.isNative); + } +} + +INSTANTIATE_TEST_SUITE_P( + Menus, FuseTwoQubitProfileTest, + testing::Values( + ProfileCase{"SwapIbmBasic", mlir::qc::swap, "x,sx,rz,cx", + onlyIbmBasicCxOps, false}, + ProfileCase{"SwapGeneric", mlir::qc::swap, "u,cx", onlyGenericU3CxOps, + false}, + ProfileCase{"SwapIqm", mlir::qc::swap, "r,cz", onlyIqmDefaultOps, + false}, + ProfileCase{"HstycxIbm", hstycxTwoQ, "x,sx,rz,cx", onlyIbmBasicCxOps, + false}, + ProfileCase{"CxYIqm", cxYOnQ1, "r,cz", onlyIqmDefaultOps, false}, + ProfileCase{"BroadOneQIqm", broadOneQThenCz, "r,cz", onlyIqmDefaultOps, + false}, + ProfileCase{"ZeroAngleRyRzCz", zeroAngleThenCz, "ry,rz,cz", + onlyAxisPairRyRzCzOps, false}, + ProfileCase{"HCxTIbmCz", hCxTOnQ1, "x,sx,rz,cz", onlyIbmBasicCzOps, + false}, + ProfileCase{"XYSXCzIqm", xYSXCz, "r,cz", onlyIqmDefaultOps, false}, + ProfileCase{"HYCxRxRz", hYCx, "rx,rz,cx", onlyAxisPairRxRzCxOps, false}, + ProfileCase{"ZCxRxRy", zCx, "rx,ry,cx", onlyAxisPairRxRyCxOps, false}, + ProfileCase{"Hq0Yq1CxSq0", hq0Yq1CxSq0, "u,cx", onlyGenericU3CxOps, + true}, + ProfileCase{"XHCzRyRz", xHCz, "ry,rz,cz", onlyAxisPairRyRzCzOps, true}, + ProfileCase{"HCxSq1MultiEnt", hCxSq1, "u,cx,cz", onlyGenericU3CxOrCzOps, + true}), + [](const testing::TestParamInfo& info) { + return info.param.name; + }); + +TEST_F(FuseTwoQubitUnitaryRunsPassTest, EmptyNativeGatesSkipsPass) { + auto module = mlir::qc::QCProgramBuilder::build(context.get(), fusionCxCx); + ASSERT_TRUE(module); + runQcToQco(module); + std::string before; + llvm::raw_string_ostream osBefore(before); + module->print(osBefore); + + PassManager pm(module->getContext()); + pm.addPass(createFuseTwoQubitUnitaryRuns(FuseTwoQubitUnitaryRunsOptions{ + .nativeGates = "", + })); + ASSERT_TRUE(succeeded(pm.run(*module))); + + std::string after; + llvm::raw_string_ostream osAfter(after); + module->print(osAfter); + EXPECT_EQ(before, after); +} + +TEST_F(FuseTwoQubitUnitaryRunsPassTest, InverseWrappedOpsSynthesize) { + expectNativeAfterSynthesis(inverseTwoX, "x,sx,rz,cx", onlyIbmBasicCxOps); +} + +TEST_F(FuseTwoQubitUnitaryRunsPassTest, ControlledXHSynthesizesToNativeMenu) { + expectEquivalentAndNativeAfterSynthesis(controlledXH, "x,sx,rz,cx", + onlyIbmBasicCxOps); +} + +TEST_F(FuseTwoQubitUnitaryRunsPassTest, FailsForUnsupportedNativeGateMenu) { + expectSynthesisFailure(mlir::qc::h, "not-a-gate"); +} + +TEST_F(FuseTwoQubitUnitaryRunsPassTest, + FailsForNativeGateMenuWithoutSingleQEmitter) { + expectSynthesisFailure(mlir::qc::singleControlledX, "cx,cz"); +} + +TEST_F(FuseTwoQubitUnitaryRunsPassTest, FailsForMultiControlledGateStructure) { + expectSynthesisFailure(mlir::qc::multipleControlledX, "x,sx,rz,cx"); +} + +TEST_F(FuseTwoQubitUnitaryRunsPassTest, + CandidateSelectionIsDeterministicAcrossRuns) { + auto buildFn = [&] { + return mlir::qc::QCProgramBuilder::build(context.get(), determinismSwap); + }; + auto firstModule = buildFn(); + runFusePipeline(firstModule, "u,cx"); + auto secondModule = buildFn(); + runFusePipeline(secondModule, "u,cx"); + + std::string first; + std::string second; + llvm::raw_string_ostream osFirst(first); + llvm::raw_string_ostream osSecond(second); + firstModule->print(osFirst); + secondModule->print(osSecond); + EXPECT_EQ(first, second); +} + +TEST_F(FuseTwoQubitUnitaryRunsPassTest, ThreeQubitGhzEquivalentOnCoreProfiles) { + const std::array profiles{{ + {.name = "GhzIbm", + .program = threeQGhz, + .nativeGates = "x,sx,rz,cx", + .isNative = onlyIbmBasicCxOps, + .checkEquivalence = false}, + {.name = "GhzGeneric", + .program = threeQGhz, + .nativeGates = "u,cx", + .isNative = onlyGenericU3CxOps, + .checkEquivalence = false}, + {.name = "GhzIqm", + .program = threeQGhz, + .nativeGates = "r,cz", + .isNative = onlyIqmDefaultOps, + .checkEquivalence = false}, + }}; + for (const ProfileCase& profile : profiles) { + auto expected = mlir::qc::QCProgramBuilder::build(context.get(), threeQGhz); + runQcToQco(expected); + auto synthesized = + mlir::qc::QCProgramBuilder::build(context.get(), threeQGhz); + runFusePipeline(synthesized, profile.nativeGates); + EXPECT_TRUE(profile.isNative(synthesized)); + expectQcoModulesEquivalent(expected, synthesized); + } +} + +TEST_P(FuseTwoQubitFusionTest, WindowFusionBehavior) { + const FusionCase& c = GetParam(); + if (c.checkTwoQUnitary) { + expectTwoQFusePreservesUnitary(c.program, c.nativeGates); + } + auto module = mlir::qc::QCProgramBuilder::build(context.get(), c.program); + ASSERT_TRUE(module); + runQcToQco(module); + runTwoQFuse(module, c.nativeGates); + if (c.exactCtrlCount) { + EXPECT_EQ(countCtrlOps(module), *c.exactCtrlCount); + } + if (c.minCtrlCount) { + EXPECT_GE(countCtrlOps(module), *c.minCtrlCount); + } +} + +INSTANTIATE_TEST_SUITE_P( + Windows, FuseTwoQubitFusionTest, + testing::Values(FusionCase{"AdjacentCxCancel", fusionCxCx, "u,cx", 0, + std::nullopt, true}, + FusionCase{"InterleavedOneQ", fusionHCxInterleavedTCx, + "u,cx", std::nullopt, std::nullopt, true}, + FusionCase{"DifferentPairBoundary", fusionThreeLineCx, + "u,cx", std::nullopt, 1, false}, + FusionCase{"SharedWireOneQ", fusionCxRSharedOtherPair, + "u,cx", std::nullopt, 2, false}, + FusionCase{"BarrierBoundary", fusionCxBarrierCx, "u,cx", 2, + std::nullopt, false}, + FusionCase{"SwappedWireOrder", fusionSwapCxPattern, "u,cx", + std::nullopt, std::nullopt, true}, + FusionCase{"OffMenuGateInWindow", fusionOffMenuGateInWindow, + "u,cx", std::nullopt, std::nullopt, true}, + FusionCase{"DualWireOneQBetweenCx", + fusionDualWireOneQBetweenCx, "u,cx", + std::nullopt, std::nullopt, true}), + [](const testing::TestParamInfo& info) { + return info.param.name; + }); + +TEST_F(FuseTwoQubitUnitaryRunsPassTest, InvalidNativeGatesFailsPass) { + auto module = mlir::qc::QCProgramBuilder::build(context.get(), fusionCxCx); + ASSERT_TRUE(module); + runQcToQco(module); + PassManager pm(module->getContext()); + pm.addPass(createFuseTwoQubitUnitaryRuns(FuseTwoQubitUnitaryRunsOptions{ + .nativeGates = "not-a-gate", + })); + EXPECT_TRUE(failed(pm.run(*module))); +} diff --git a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp index 618daff0db..2af990979f 100644 --- a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp +++ b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp @@ -806,6 +806,14 @@ TEST(SymmetricEigensolver, HandlesDegenerateSpectrum) { EXPECT_TRUE((v.transpose() * v).isIdentity()); } +TEST(GlobalPhaseEquivalence, IsEquivalentUpToGlobalPhase) { + const Matrix2x2 pauliX = Matrix2x2::fromElements(0, 1, 1, 0); + const Complex phase{0.0, 1.0}; + EXPECT_TRUE(isEquivalentUpToGlobalPhase(pauliX, phase * pauliX)); + EXPECT_TRUE(isEquivalentUpToGlobalPhase(pauliX, pauliX)); + EXPECT_FALSE(isEquivalentUpToGlobalPhase(pauliX, Matrix2x2::identity())); +} + TEST(Eigensolver, EmptyMatrixReturnsNullopt) { EXPECT_FALSE(DynamicMatrix{}.eigenDecomposition().has_value()); } From 9bde3bf1ad1f682b9a9317f13f22393f0780e19e Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 7 Jul 2026 10:14:25 +0200 Subject: [PATCH 106/122] =?UTF-8?q?=F0=9F=94=A5=20Remove=20the=20`isEquiva?= =?UTF-8?q?lentUpToGlobalPhase`=20function=20from=20the=20matrix=20utiliti?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/include/mlir/Dialect/QCO/Utils/Matrix.h | 27 ------------------- .../test_fuse_two_qubit_unitary_runs.cpp | 13 ++++++--- .../Dialect/QCO/Utils/test_unitary_matrix.cpp | 8 ------ 3 files changed, 10 insertions(+), 38 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h index d046e7bb58..38816a9c85 100644 --- a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h +++ b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h @@ -14,7 +14,6 @@ #include #include -#include #include #include #include @@ -834,32 +833,6 @@ concept SupportedMatrix = std::same_as || std::same_as || std::same_as || std::same_as; -/** - * @brief Checks whether two unitaries are equal up to a global phase. - * - * Uses `trace(rhs.adjoint() * lhs)` to infer a unit-modulus phase factor, then - * compares `lhs` to `factor * rhs` with @ref isApprox. A near-zero overlap - * (`|trace| <= tol`) is treated as not equivalent to avoid dividing by a tiny - * number. - * - * @tparam Matrix Any supported fixed- or dynamic-size matrix type. - * @param lhs Left-hand unitary. - * @param rhs Right-hand unitary. - * @param tol Absolute tolerance for overlap and entry-wise comparison. - * @return True when @p lhs and @p rhs differ only by a global phase. - */ -template -[[nodiscard]] bool isEquivalentUpToGlobalPhase(const Matrix& lhs, - const Matrix& rhs, - double tol = MATRIX_TOLERANCE) { - const auto overlap = (rhs.adjoint() * lhs).trace(); - if (std::abs(overlap) <= tol) { - return false; - } - const auto factor = overlap / std::abs(overlap); - return lhs.isApprox(factor * rhs, tol); -} - /// Scalar-on-the-left multiply `scalar * matrix` (commutes with the member /// `matrix * scalar`). Provided so generic code can scale a matrix from /// either side. diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp index 2a205fb599..68571420bd 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp @@ -202,6 +202,7 @@ computeUnitaryFromQcoModule(const OwningOpRef& moduleOp) { DynamicMatrix unitary = DynamicMatrix::identity(static_cast(1ULL << numQubits)); + Complex globalPhase{1.0, 0.0}; auto getQubitId = [&](Value qubit) -> std::optional { const auto it = qubitIds.find(qubit); @@ -218,7 +219,13 @@ computeUnitaryFromQcoModule(const OwningOpRef& moduleOp) { if (!op) { continue; } - if (llvm::isa(op.getOperation())) { + if (llvm::isa(op.getOperation())) { + continue; + } + if (auto gphase = llvm::dyn_cast(op.getOperation())) { + if (const auto matrix = gphase.getUnitaryMatrix()) { + globalPhase *= (*matrix)(0, 0); + } continue; } @@ -275,7 +282,7 @@ computeUnitaryFromQcoModule(const OwningOpRef& moduleOp) { } } - return unitary; + return globalPhase * unitary; } namespace { @@ -339,7 +346,7 @@ class FuseTwoQubitUnitaryRunsPassTest : public testing::Test { ASSERT_TRUE(lhsUnitary.has_value()); const auto rhsUnitary = computeUnitaryFromQcoModule(rhs); ASSERT_TRUE(rhsUnitary.has_value()); - EXPECT_TRUE(isEquivalentUpToGlobalPhase(*lhsUnitary, *rhsUnitary)); + EXPECT_TRUE(lhsUnitary->isApprox(*rhsUnitary)); } void expectNativeAfterSynthesis(ProgramFn program, StringRef nativeGates, diff --git a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp index 2af990979f..618daff0db 100644 --- a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp +++ b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp @@ -806,14 +806,6 @@ TEST(SymmetricEigensolver, HandlesDegenerateSpectrum) { EXPECT_TRUE((v.transpose() * v).isIdentity()); } -TEST(GlobalPhaseEquivalence, IsEquivalentUpToGlobalPhase) { - const Matrix2x2 pauliX = Matrix2x2::fromElements(0, 1, 1, 0); - const Complex phase{0.0, 1.0}; - EXPECT_TRUE(isEquivalentUpToGlobalPhase(pauliX, phase * pauliX)); - EXPECT_TRUE(isEquivalentUpToGlobalPhase(pauliX, pauliX)); - EXPECT_FALSE(isEquivalentUpToGlobalPhase(pauliX, Matrix2x2::identity())); -} - TEST(Eigensolver, EmptyMatrixReturnsNullopt) { EXPECT_FALSE(DynamicMatrix{}.eigenDecomposition().has_value()); } From 408b5143d01bfcea9e8858b1a0f76a28d45ea93f Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 7 Jul 2026 10:14:37 +0200 Subject: [PATCH 107/122] =?UTF-8?q?=F0=9F=93=9D=20Update=20CHANGELOG.md=20?= =?UTF-8?q?to=20reflect=20changes=20in=20the=20`fuse-two-qubit-unitary-run?= =?UTF-8?q?s`=20pass=20and=20remove=20outdated=20PR=20link?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9a46b692f..af4e282f1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ releases may include breaking changes. ### Added - ✨ Add a `fuse-two-qubit-unitary-runs` pass for fusing compile-time two-qubit - unitary windows via Weyl/KAK resynthesis ([#1655]) ([**@simon1hofmann**]) + unitary windows via Weyl/KAK resynthesis ([#1865]) ([**@simon1hofmann**]) - ✨ Add support for IQM's `move` gate in the QDMI Qiskit backend converter ([#1844], [#1848]) ([**@burgholzer**], [**@marcelwa**]) - 🚸 Add `const` version of the `CompoundOperation`'s `getOps()` function @@ -602,6 +602,7 @@ changelogs._ +[#1865]: https://github.com/munich-quantum-toolkit/core/pull/1865 [#1848]: https://github.com/munich-quantum-toolkit/core/pull/1848 [#1844]: https://github.com/munich-quantum-toolkit/core/pull/1844 [#1842]: https://github.com/munich-quantum-toolkit/core/pull/1842 @@ -653,7 +654,6 @@ changelogs._ [#1664]: https://github.com/munich-quantum-toolkit/core/pull/1664 [#1662]: https://github.com/munich-quantum-toolkit/core/pull/1662 [#1660]: https://github.com/munich-quantum-toolkit/core/pull/1660 -[#1655]: https://github.com/munich-quantum-toolkit/core/pull/1655 [#1652]: https://github.com/munich-quantum-toolkit/core/pull/1652 [#1638]: https://github.com/munich-quantum-toolkit/core/pull/1638 [#1637]: https://github.com/munich-quantum-toolkit/core/pull/1637 From 6a23d6b842acf888a9eb1ff2a1bb848db6418906 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 7 Jul 2026 10:22:37 +0200 Subject: [PATCH 108/122] =?UTF-8?q?=F0=9F=94=A7=20Refactor=20CMakeLists.tx?= =?UTF-8?q?t=20to=20simplify=20header=20file=20collection=20for=20QCO=20tr?= =?UTF-8?q?ansforms?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt index 77d847d3ba..77f594db17 100644 --- a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt +++ b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt @@ -24,11 +24,11 @@ add_mlir_library( mqt_mlir_target_use_project_options(MLIRQCOTransforms) -# collect header files (subdirs: Decomposition/, …) -file(GLOB_RECURSE PASSES_HEADERS_SOURCE CONFIGURE_DEPENDS - "${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Dialect/QCO/Transforms/*.h") -file(GLOB_RECURSE PASSES_HEADERS_BUILD CONFIGURE_DEPENDS - "${MQT_MLIR_BUILD_INCLUDE_DIR}/mlir/Dialect/QCO/Transforms/*.inc") +# collect header files +file(GLOB_RECURSE PASSES_HEADERS_SOURCE + ${MQT_MLIR_SOURCE_INCLUDE_DIR}/mlir/Dialect/QCO/Transforms/*.h) +file(GLOB_RECURSE PASSES_HEADERS_BUILD + ${MQT_MLIR_BUILD_INCLUDE_DIR}/mlir/Dialect/QCO/Transforms/*.inc) # add public headers using file sets target_sources( From 66e5465267c0d9d82def9c84b64440be98d138a6 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 7 Jul 2026 11:10:09 +0200 Subject: [PATCH 109/122] =?UTF-8?q?=F0=9F=8E=A8=20Simplify=20FuseTwoQubitU?= =?UTF-8?q?nitaryRuns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/Decomposition/Euler.h | 15 + .../Transforms/Decomposition/NativeGateset.h | 13 + .../mlir/Dialect/QCO/Transforms/Passes.td | 44 +- .../Decomposition/NativeGateset.cpp | 46 ++ .../FuseSingleQubitUnitaryRuns.cpp | 26 +- .../FuseTwoQubitUnitaryRuns.cpp | 585 +++++------------- .../Decomposition/test_weyl_decomposition.cpp | 73 +-- 7 files changed, 282 insertions(+), 520 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h index 4130967cde..952875d972 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h @@ -20,6 +20,10 @@ #include #include +namespace mlir { +class RewritePatternSet; +} // namespace mlir + namespace mlir::qco::decomposition { /** @@ -99,4 +103,15 @@ synthesizeUnitary1QEuler(OpBuilder& builder, Location loc, Value qubit, */ void emitGPhaseIfNeeded(OpBuilder& builder, Location loc, double phase); +/** + * @brief Populates @p patterns with the single-qubit run fusion rewrite for + * @p basis (the reusable core of `fuse-single-qubit-unitary-runs`). + * + * @param skipControlledBodies When set, single-qubit gates nested in `qco.ctrl` + * bodies are left untouched. + */ +void populateFuseSingleQubitUnitaryRunsPatterns( + RewritePatternSet& patterns, EulerBasis basis, + bool skipControlledBodies = false); + } // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeGateset.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeGateset.h index caa11c7c1e..ac73592bae 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeGateset.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/NativeGateset.h @@ -18,6 +18,10 @@ #include #include +namespace mlir { +class Operation; +} // namespace mlir + namespace mlir::qco::decomposition { /** @@ -63,6 +67,15 @@ struct NativeGateset { */ [[nodiscard]] std::optional decomposeTarget(const Matrix4x4& target) const; + + /** + * @brief Whether @p op is already on this native gateset. + * + * `qco.barrier` and `qco.gphase` are always allowed. Single-qubit primitives + * and single-target `qco.ctrl` shells (with an `X`/`Z` body) are checked + * against @p gates. All other ops are rejected. + */ + [[nodiscard]] bool allowsOp(Operation* op) const; }; } // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index 8053b2f6a3..d49a44fa36 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -72,45 +72,45 @@ def FuseTwoQubitUnitaryRuns let dependentDialects = ["mlir::qco::QCODialect"]; let summary = "Lower QCO unitary gates to a user-specified native gate menu."; let description = [{ - Rewrites unitaries to match the comma-separated `native-gates` menu. + Rewrites unitaries to match the comma-separated `native-gates` gateset. `qco.barrier` and `qco.gphase` are preserved; `qco.ctrl` must have a single control and a single target. - The menu is a comma-separated list of gate tokens (order not significant) - from which the pass builds a profile: a single-qubit synthesis mode + The gateset is a comma-separated list of gate tokens (order not + significant) from which the pass resolves a single-qubit Euler basis (generic `qco.u` when `u` is present; IBM-style surface gates when all of `x`, `sx`, and `rz`/`p` are present; IQM-style `qco.r` when `r` is present; - or a supported rotation pair chosen from `rx`, `ry`, `rz`) plus optional - two-qubit entanglers `cx`, `cz`, and optional `rzz`. + or a supported rotation pair chosen from `rx`, `ry`, `rz`) plus the + two-qubit entangler `cx` or `cz`. Recognised tokens: `u`, `x`, `sx`, `rz` (or `p`), `rx`, `ry`, `r`, `cx`, - `cz`, `rzz`. An empty or whitespace-only menu is a no-op, which is the - intended pipeline default when synthesis is not needed. An unrecognised - token causes the pass to fail. - - Example menus (each line is one illustrative menu; pick either `cx` or - `cz` as the entangler, or list both if both are native): - - IBM basic (no fractional two-qubit): `x,sx,rz,cx` or `x,sx,rz,cz` - - IBM fractional: `x,sx,rz,rx,rzz,cx` or `x,sx,rz,rx,rzz,cz` + `cz`. An empty or whitespace-only gateset is a no-op, which is the intended + pipeline default when synthesis is not needed. An unrecognised token causes + the pass to fail. + + Example gatesets (each line is one illustrative gateset; pick either `cx` + or `cz` as the entangler, or list both if both are native): + - IBM: `x,sx,rz,cx` or `x,sx,rz,cz` - Generic single-qubit U: `u,cx` or `u,cz` - IQM default: `r,cz` (or `r,cx` if CX is the native entangler) - Rotation pair + entangler: `rx,rz,cx`, `rx,ry,cz`, `ry,rz,cx`, etc. Supported pairs are exactly `rx`+`rz`, `rx`+`ry`, and `ry`+`rz`. - Stages: fuse single-qubit runs; fuse two-qubit windows; up to four - synthesis sweeps over remaining off-menu ops; fuse 1q seams; up to four - further synthesis and fusion rounds until the full menu holds. The pass - fails if anything remains off-menu after those caps. + Stages: fuse single-qubit runs (reusing `fuse-single-qubit-unitary-runs`, + which also lowers lone off-gateset single-qubit gates); fuse two-qubit + windows and lower any remaining off-gateset two-qubit ops via Weyl + synthesis; fuse the single-qubit seams introduced by that synthesis. The + pass fails if anything remains off the native gateset afterwards. Lowering is deterministic: `cx` is preferred over `cz`, single-qubit - factors use the first emitter's Euler basis, and two-qubit window - replacement uses the minimal entangler count from the synthesizer. + factors use the resolved Euler basis, and two-qubit window replacement uses + the minimal entangler count from the synthesizer. }]; let options = [Option< "nativeGates", "native-gates", "std::string", "\"\"", - "Comma-separated native gate menu. Empty or whitespace-only is " - "a no-op. Tokens: u, x, sx, rz (or p), rx, ry, r, cx, cz, rzz. " - "Examples: x,sx,rz,cx; x,sx,rz,rx,rzz,cz; u,cx; r,cz; rx,rz,cx.">]; + "Comma-separated native gateset. Empty or whitespace-only is " + "a no-op. Tokens: u, x, sx, rz (or p), rx, ry, r, cx, cz. " + "Examples: x,sx,rz,cx; u,cx; r,cz; rx,rz,cx.">]; } //===----------------------------------------------------------------------===// diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeGateset.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeGateset.cpp index 0fdec01791..4baf5cbd95 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeGateset.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeGateset.cpp @@ -10,12 +10,16 @@ #include "mlir/Dialect/QCO/Transforms/Decomposition/NativeGateset.h" +#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Weyl.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" #include +#include #include +#include #include #include @@ -141,6 +145,48 @@ NativeGateset::decomposeTarget(const Matrix4x4& target) const { return cachedNativeBasisDecomposer(*entangler).decomposeTarget(target); } +static std::optional gateKindFor(UnitaryOpInterface op) { + return llvm::TypeSwitch>( + op.getOperation()) + .Case([](UOp) { return NativeGateKind::U; }) + .Case([](XOp) { return NativeGateKind::X; }) + .Case([](SXOp) { return NativeGateKind::SX; }) + .Case([](RZOp) { return NativeGateKind::RZ; }) + .Case([](RXOp) { return NativeGateKind::RX; }) + .Case([](RYOp) { return NativeGateKind::RY; }) + .Case([](ROp) { return NativeGateKind::R; }) + .Default([](Operation*) { return std::nullopt; }); +} + +static std::optional entanglerKindFor(CtrlOp ctrl) { + if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1 || + ctrl.getNumBodyUnitaries() != 1) { + return std::nullopt; + } + return llvm::TypeSwitch>( + ctrl.getBodyUnitary(0).getOperation()) + .Case([](XOp) { return NativeGateKind::CX; }) + .Case([](ZOp) { return NativeGateKind::CZ; }) + .Default([](Operation*) { return std::nullopt; }); +} + +bool NativeGateset::allowsOp(Operation* op) const { + return llvm::TypeSwitch(op) + .Case([](auto) { return true; }) + .Case([&](CtrlOp ctrl) { + const auto kind = entanglerKindFor(ctrl); + return kind && gates.contains(*kind); + }) + .Case([&](UnitaryOpInterface unitary) { + if (!unitary.isSingleQubit()) { + return false; + } + const auto gate = gateKindFor(unitary); + return gate && gates.contains(*gate); + }) + .Default([](Operation*) { return false; }); +} + std::optional NativeGateset::parse(StringRef nativeGates) { auto gates = parseGateSet(nativeGates); if (!gates) { diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp index 73fe887dfb..b6fa901035 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp @@ -137,10 +137,13 @@ namespace { struct FuseSingleQubitUnitaryRunsPattern final : OpInterfaceRewritePattern { FuseSingleQubitUnitaryRunsPattern(MLIRContext* context, - const decomposition::EulerBasis basis) - : OpInterfaceRewritePattern(context), basis(basis) {} + const decomposition::EulerBasis basis, + const bool skipControlledBodies) + : OpInterfaceRewritePattern(context), basis(basis), + skipControlledBodies(skipControlledBodies) {} decomposition::EulerBasis basis; + bool skipControlledBodies; /** * @brief Whether `op` starts a run. @@ -165,6 +168,10 @@ struct FuseSingleQubitUnitaryRunsPattern final */ LogicalResult matchAndRewrite(UnitaryOpInterface op, PatternRewriter& rewriter) const override { + if (skipControlledBodies && + (op.getOperation()->getParentOfType() != nullptr)) { + return failure(); + } if (!isRunStart(op)) { return failure(); } @@ -208,8 +215,8 @@ struct FuseSingleQubitUnitaryRunsPass final } RewritePatternSet patterns(&getContext()); - patterns.add(patterns.getContext(), - *parsed); + decomposition::populateFuseSingleQubitUnitaryRunsPatterns( + patterns, *parsed, /*skipControlledBodies=*/false); if (failed(applyPatternsGreedily(module, std::move(patterns)))) { signalPassFailure(); @@ -220,3 +227,14 @@ struct FuseSingleQubitUnitaryRunsPass final } // namespace } // namespace mlir::qco + +namespace mlir::qco::decomposition { + +void populateFuseSingleQubitUnitaryRunsPatterns( + RewritePatternSet& patterns, const EulerBasis basis, + const bool skipControlledBodies) { + patterns.add(patterns.getContext(), basis, + skipControlledBodies); +} + +} // namespace mlir::qco::decomposition diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp index b96b4de99f..ef3970a617 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp @@ -16,12 +16,9 @@ #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" -#include -#include #include -#include #include -#include +#include #include #include #include @@ -32,7 +29,6 @@ #include #include -#include #include #include @@ -41,59 +37,10 @@ namespace mlir::qco { #define GEN_PASS_DEF_FUSETWOQUBITUNITARYRUNS #include "mlir/Dialect/QCO/Transforms/Passes.h.inc" -namespace { - -using decomposition::NativeGateKind; +using decomposition::EulerBasis; using decomposition::NativeGateset; - -std::optional nativeGateKindFor(UnitaryOpInterface op) { - return llvm::TypeSwitch>( - op.getOperation()) - .Case([](UOp) { return NativeGateKind::U; }) - .Case([](XOp) { return NativeGateKind::X; }) - .Case([](SXOp) { return NativeGateKind::SX; }) - .Case([](auto) { return NativeGateKind::RZ; }) - .Case([](RXOp) { return NativeGateKind::RX; }) - .Case([](RYOp) { return NativeGateKind::RY; }) - .Case([](ROp) { return NativeGateKind::R; }) - .Default([](Operation*) { return std::nullopt; }); -} - -std::optional nativeEntanglerKindFor(CtrlOp ctrl) { - if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1 || - ctrl.getNumBodyUnitaries() != 1) { - return std::nullopt; - } - return llvm::TypeSwitch>( - ctrl.getBodyUnitary(0).getOperation()) - .Case([](XOp) { return NativeGateKind::CX; }) - .Case([](ZOp) { return NativeGateKind::CZ; }) - .Default([](Operation*) { return std::nullopt; }); -} - -/// Returns true when @p op is already on the resolved native gateset. -/// -/// Barriers and global phase are always allowed. Single-qubit primitives and -/// single-target `CtrlOp` shells (`X`/`Z` bodies) are checked against -/// @p spec.gates. -bool allowsOp(Operation* op, const NativeGateset& spec) { - return llvm::TypeSwitch(op) - .Case([](auto) { return true; }) - .Case([&](CtrlOp ctrl) { - const auto kind = nativeEntanglerKindFor(ctrl); - return kind && spec.gates.contains(*kind); - }) - .Case([&](UnitaryOpInterface unitary) { - if (!unitary.isSingleQubit()) { - return false; - } - const auto gate = nativeGateKindFor(unitary); - return gate && spec.gates.contains(*gate); - }) - .Default([](Operation*) { return false; }); -} - -} // namespace +using decomposition::populateFuseSingleQubitUnitaryRunsPatterns; +using decomposition::synthesizeUnitary2QWeyl; /// Skips unitaries nested under `ctrl`/`inv` bodies (handled on the shell op). static bool isExcludedFromTopLevelUnitaryWalk(Operation* op) { @@ -108,26 +55,9 @@ static bool isWalkableUnitaryShell(Operation* op) { !isExcludedFromTopLevelUnitaryWalk(op); } -static void collectUnitaryOpsInPreOrder(Operation* root, - SmallVectorImpl& ops) { - root->walk([&](Operation* op) { - if (isExcludedFromTopLevelUnitaryWalk(op)) { - return; - } - if (llvm::isa(op)) { - ops.push_back(op); - } - }); -} - -static Value emitSingleQubitMatrix(IRRewriter& rewriter, Location loc, - Value inQubit, const Matrix2x2& matrix, - const decomposition::EulerBasis basis) { - return *decomposition::synthesizeUnitary1QEuler( - rewriter, loc, inQubit, matrix, /*runSize=*/0, - /*hasNonBasisGate=*/true, basis); -} - +/// Builds the constant 4x4 matrix for a two-qubit op (bare or single-target +/// `CtrlOp`). Returns false for barriers, global phase, non-two-qubit ops, and +/// ops whose matrix is not known at compile time. static bool assignTwoQubitOpMatrix(Operation* op, Matrix4x4& matrix) { if (llvm::isa(op)) { return false; @@ -164,14 +94,6 @@ static bool isTwoQubitRunMember(UnitaryOpInterface unitary) { return assignTwoQubitOpMatrix(unitary.getOperation(), matrix); } -static UnitaryOpInterface fusibleSingleQubitOp(Operation* op) { - if (llvm::isa(op)) { - return {}; - } - auto unitary = llvm::dyn_cast(op); - return isOneQubitWindowMember(unitary) ? unitary : UnitaryOpInterface{}; -} - static UnitaryOpInterface uniqueUnitaryUser(Value wire) { if (!wire.hasOneUse()) { return {}; @@ -218,10 +140,6 @@ static bool feedsFromSameTwoQubitWindow(UnitaryOpInterface op) { return gate0 != nullptr && gate0 == gate1; } -static bool isTwoQubitRunStart(UnitaryOpInterface op) { - return isTwoQubitRunMember(op) && !feedsFromSameTwoQubitWindow(op); -} - namespace { struct FusableTwoQubitRun { @@ -233,229 +151,114 @@ struct FusableTwoQubitRun { Value tailB; }; -struct OneQubitRun { - SmallVector ops; -}; - } // namespace -static void markNonNativeIfNeeded(FusableTwoQubitRun& run, Operation* op, - const decomposition::NativeGateset& spec) { - if (!allowsOp(op, spec)) { - run.anyNonNative = true; - } -} - -// Replace when off-menu ops must be lowered, or when resynthesis uses fewer -// entanglers than the fused window. -static bool shouldApplyTwoQubitRunReplacement(const FusableTwoQubitRun& run, - std::uint8_t numBasisUses) { - if (run.anyNonNative) { - return true; - } - return numBasisUses < run.numTwoQ; -} - static void absorbTwoQubitIntoRun(FusableTwoQubitRun& run, UnitaryOpInterface op, - const decomposition::NativeGateset& spec) { + const NativeGateset& spec) { Matrix4x4 opMatrix; if (!assignTwoQubitOpMatrix(op.getOperation(), opMatrix)) { return; } const Value in0 = op.getInputQubit(0); const Value in1 = op.getInputQubit(1); - SmallVector ids; + std::size_t id0 = 0; + std::size_t id1 = 1; if (in0 == run.tailA && in1 == run.tailB) { - ids = {0, 1}; run.tailA = op.getOutputQubit(0); run.tailB = op.getOutputQubit(1); } else if (in0 == run.tailB && in1 == run.tailA) { - ids = {1, 0}; + id0 = 1; + id1 = 0; run.tailA = op.getOutputQubit(1); run.tailB = op.getOutputQubit(0); } else { return; } - run.composed = opMatrix.reorderForQubits(ids[0], ids[1]) * run.composed; + run.composed.premultiplyBy(opMatrix.reorderForQubits(id0, id1)); run.ops.push_back(op.getOperation()); ++run.numTwoQ; - markNonNativeIfNeeded(run, op.getOperation(), spec); + run.anyNonNative |= !spec.allowsOp(op.getOperation()); } static void absorbOneQubitIntoRun(FusableTwoQubitRun& run, UnitaryOpInterface op, - const decomposition::NativeGateset& spec, + const NativeGateset& spec, unsigned wireIndex) { Matrix2x2 raw; if (!op.getUnitaryMatrix2x2(raw)) { return; } - const auto pad = raw.embedInTwoQubit(wireIndex); - run.composed = pad * run.composed; + run.composed.premultiplyBy(raw.embedInTwoQubit(wireIndex)); run.ops.push_back(op.getOperation()); - markNonNativeIfNeeded(run, op.getOperation(), spec); - if (wireIndex == 0) { - run.tailA = op.getOutputQubit(0); - } else { - run.tailB = op.getOutputQubit(0); - } + run.anyNonNative |= !spec.allowsOp(op.getOperation()); + (wireIndex == 0 ? run.tailA : run.tailB) = op.getOutputQubit(0); } -static FusableTwoQubitRun -scanFusableTwoQubitRun(UnitaryOpInterface head, - const decomposition::NativeGateset& spec) { +static FusableTwoQubitRun scanFusableTwoQubitRun(UnitaryOpInterface head, + const NativeGateset& spec) { FusableTwoQubitRun run; + if (!assignTwoQubitOpMatrix(head.getOperation(), run.composed)) { + return run; + } run.tailA = head.getOutputQubit(0); run.tailB = head.getOutputQubit(1); run.ops.push_back(head.getOperation()); run.numTwoQ = 1; - if (!assignTwoQubitOpMatrix(head.getOperation(), run.composed)) { - run.composed = Matrix4x4::identity(); - run.numTwoQ = 0; - run.ops.clear(); - return run; - } - markNonNativeIfNeeded(run, head.getOperation(), spec); + run.anyNonNative |= !spec.allowsOp(head.getOperation()); while (true) { UnitaryOpInterface nextOnA = uniqueUnitaryUser(run.tailA); UnitaryOpInterface nextOnB = uniqueUnitaryUser(run.tailB); + const bool sameOp = + nextOnA && nextOnB && nextOnA.getOperation() == nextOnB.getOperation(); - if (nextOnA && nextOnB && - nextOnA.getOperation() == nextOnB.getOperation() && - nextOnA.isTwoQubit()) { + if (sameOp && nextOnA.isTwoQubit()) { absorbTwoQubitIntoRun(run, nextOnA, spec); continue; } - if (nextOnA && nextOnB && - nextOnA.getOperation() != nextOnB.getOperation() && - nextOnA.isSingleQubit() && nextOnB.isSingleQubit()) { - if (nextOnA->isBeforeInBlock(nextOnB)) { - absorbOneQubitIntoRun(run, nextOnA, spec, /*wireIndex=*/0); - continue; - } - absorbOneQubitIntoRun(run, nextOnB, spec, /*wireIndex=*/1); - continue; - } - - if (nextOnA && nextOnA.isSingleQubit() && - (!nextOnB || nextOnA.getOperation() != nextOnB.getOperation())) { + const bool aSingle = nextOnA && nextOnA.isSingleQubit() && !sameOp; + const bool bSingle = nextOnB && nextOnB.isSingleQubit() && !sameOp; + if (aSingle && (!bSingle || nextOnA->isBeforeInBlock(nextOnB))) { absorbOneQubitIntoRun(run, nextOnA, spec, /*wireIndex=*/0); continue; } - - if (nextOnB && nextOnB.isSingleQubit() && - (!nextOnA || nextOnB.getOperation() != nextOnA.getOperation())) { + if (bSingle) { absorbOneQubitIntoRun(run, nextOnB, spec, /*wireIndex=*/1); continue; } - break; } return run; } -static void eraseFusableTwoQubitRun(PatternRewriter& rewriter, - const FusableTwoQubitRun& run) { - for (Operation* op : llvm::reverse(run.ops)) { - rewriter.eraseOp(op); - } -} - -static bool maybeFuseRun(IRRewriter& rewriter, OneQubitRun& run, - const decomposition::EulerBasis basis, - const decomposition::NativeGateset& spec) { - Matrix2x2 fused = Matrix2x2::identity(); - for (UnitaryOpInterface u : run.ops) { - Matrix2x2 m; - if (!u.getUnitaryMatrix2x2(m)) { - return false; +/// Whether any two-qubit or single-qubit unitary op (including `ctrl` shells) +/// remains off the native gateset. Used as the pass' final convergence check. +static bool hasNonNativeOps(Operation* root, const NativeGateset& spec) { + const WalkResult walkResult = root->walk([&](Operation* op) { + if (!isWalkableUnitaryShell(op) || !llvm::isa(op)) { + return WalkResult::advance(); } - fused.premultiplyBy(m); - } - - const bool anyNonNative = llvm::any_of(run.ops, [&](UnitaryOpInterface u) { - return !allowsOp(u.getOperation(), spec); - }); - - Operation* firstOp = run.ops.front().getOperation(); - const Value inQubit = run.ops.front().getInputQubit(0); - const Value outQubit = run.ops.back().getOutputQubit(0); - - rewriter.setInsertionPoint(firstOp); - const auto replacement = decomposition::synthesizeUnitary1QEuler( - rewriter, firstOp->getLoc(), inQubit, fused, run.ops.size(), anyNonNative, - basis); - if (!replacement) { - return false; - } - rewriter.replaceAllUsesWith(outQubit, *replacement); - for (auto& op : llvm::reverse(run.ops)) { - rewriter.eraseOp(op.getOperation()); - } - return true; -} - -static bool hasNonNativeOps(Operation* root, - const decomposition::NativeGateset& spec, - bool singleQubitOnly) { - const mlir::WalkResult walkResult = root->walk([&](Operation* op) { - if (!isWalkableUnitaryShell(op)) { - return mlir::WalkResult::advance(); - } - if (singleQubitOnly) { - auto unitary = llvm::dyn_cast(op); - if (!unitary || !unitary.isSingleQubit()) { - return mlir::WalkResult::advance(); - } - } else if (!llvm::isa(op) && !llvm::isa(op)) { - return mlir::WalkResult::advance(); - } - if (!allowsOp(op, spec)) { - return mlir::WalkResult::interrupt(); - } - return mlir::WalkResult::advance(); + return spec.allowsOp(op) ? WalkResult::advance() : WalkResult::interrupt(); }); return walkResult.wasInterrupted(); } -static LogicalResult synthesizeTwoQubitOp( - IRRewriter& rewriter, Operation* op, Location loc, Value in0, Value in1, - const decomposition::NativeGateset& spec, llvm::Twine matrixErrorMsg, - llvm::Twine synthesisErrorMsg) { - if (allowsOp(op, spec)) { - return success(); - } - Matrix4x4 matrix; - if (!assignTwoQubitOpMatrix(op, matrix)) { - op->emitError(matrixErrorMsg); - return failure(); - } - rewriter.setInsertionPoint(op); - Value out0; - Value out1; - if (failed(decomposition::synthesizeUnitary2QWeyl( - rewriter, loc, in0, in1, matrix, spec, out0, out1))) { - op->emitError(synthesisErrorMsg); - return failure(); - } - rewriter.replaceOp(op, ValueRange{out0, out1}); - return success(); -} - namespace { -struct FuseTwoQubitWindowPattern - : public OpInterfaceRewritePattern { - FuseTwoQubitWindowPattern(MLIRContext* ctx, - decomposition::NativeGateset specIn) +/// Fuses a maximal window of two-qubit gates (plus interleaved single-qubit +/// gates) into a single composed unitary and resynthesizes it to the native +/// gateset when that is beneficial. +struct FuseTwoQubitWindowPattern final + : OpInterfaceRewritePattern { + FuseTwoQubitWindowPattern(MLIRContext* ctx, NativeGateset specIn) : OpInterfaceRewritePattern(ctx), spec(std::move(specIn)) {} LogicalResult matchAndRewrite(UnitaryOpInterface op, PatternRewriter& rewriter) const override { - if (!isTwoQubitRunStart(op)) { + // Only anchor on a run start: a run member not fed by an earlier window. + if (!isTwoQubitRunMember(op) || feedsFromSameTwoQubitWindow(op)) { return failure(); } @@ -464,241 +267,151 @@ struct FuseTwoQubitWindowPattern return failure(); } + // Replace when off-gateset ops must be lowered, or when resynthesis uses + // fewer entanglers than the fused window. const auto native = spec.decomposeTarget(run.composed); - if (!native || - !shouldApplyTwoQubitRunReplacement(run, native->numBasisUses)) { + if (!native || (!run.anyNonNative && native->numBasisUses >= run.numTwoQ)) { return failure(); } - Operation* firstOp = run.ops.front(); - auto firstUnitary = llvm::cast(firstOp); - const Value inA = firstUnitary.getInputQubit(0); - const Value inB = firstUnitary.getInputQubit(1); - + auto firstOp = llvm::cast(run.ops.front()); rewriter.setInsertionPoint(firstOp); Value newA; Value newB; - if (failed(decomposition::synthesizeUnitary2QWeyl( - rewriter, firstOp->getLoc(), inA, inB, run.composed, spec, newA, - newB))) { + if (failed(synthesizeUnitary2QWeyl( + rewriter, firstOp.getLoc(), firstOp.getInputQubit(0), + firstOp.getInputQubit(1), run.composed, spec, newA, newB))) { firstOp->emitError("failed to emit synthesized two-qubit gate sequence"); return failure(); } rewriter.replaceAllUsesWith(run.tailA, newA); rewriter.replaceAllUsesWith(run.tailB, newB); - eraseFusableTwoQubitRun(rewriter, run); + for (Operation* member : llvm::reverse(run.ops)) { + rewriter.eraseOp(member); + } + return success(); + } + + NativeGateset spec; +}; + +/// Lowers a single off-gateset two-qubit op (bare or single-target `CtrlOp`) +/// to the native entangler plus native single-qubit factors via Weyl synthesis. +/// Native two-qubit ops and windows fusable by @ref FuseTwoQubitWindowPattern +/// are left to that pattern. +struct LowerTwoQubitOpPattern final + : OpInterfaceRewritePattern { + LowerTwoQubitOpPattern(MLIRContext* ctx, NativeGateset specIn) + : OpInterfaceRewritePattern(ctx), spec(std::move(specIn)) {} + + LogicalResult matchAndRewrite(UnitaryOpInterface op, + PatternRewriter& rewriter) const override { + Operation* raw = op.getOperation(); + if (!isWalkableUnitaryShell(raw) || spec.allowsOp(raw)) { + return failure(); + } + Matrix4x4 matrix; + if (!assignTwoQubitOpMatrix(raw, matrix)) { + return failure(); + } + + Value in0; + Value in1; + if (auto ctrl = llvm::dyn_cast(raw)) { + in0 = ctrl.getInputControl(0); + in1 = ctrl.getInputTarget(0); + } else { + in0 = op.getInputQubit(0); + in1 = op.getInputQubit(1); + } + + rewriter.setInsertionPoint(raw); + Value out0; + Value out1; + if (failed(synthesizeUnitary2QWeyl(rewriter, raw->getLoc(), in0, in1, + matrix, spec, out0, out1))) { + return failure(); + } + rewriter.replaceOp(raw, ValueRange{out0, out1}); return success(); } - decomposition::NativeGateset spec; + NativeGateset spec; }; } // namespace -static LogicalResult -fuseTwoQubitUnitaryRuns(Operation* root, - const decomposition::NativeGateset& spec) { - RewritePatternSet patterns(root->getContext()); - patterns.add(patterns.getContext(), spec); - return applyPatternsGreedily(root, std::move(patterns)); +/// Fuses single-qubit runs (and lowers lone off-gateset single-qubit ops) by +/// reusing the `fuse-single-qubit-unitary-runs` rewrite. +/// +/// `qco.ctrl` bodies are skipped so the `X`/`Z` bodies of native entanglers are +/// preserved (both before and after two-qubit synthesis). +static LogicalResult fuseSingleQubitRuns(ModuleOp module, + const EulerBasis basis) { + RewritePatternSet patterns(module.getContext()); + populateFuseSingleQubitUnitaryRunsPatterns(patterns, basis, + /*skipControlledBodies=*/true); + return applyPatternsGreedily(module, std::move(patterns)); +} + +/// Fuses two-qubit windows, then lowers any remaining off-gateset two-qubit +/// ops. +static LogicalResult fuseAndLowerTwoQubitOps(ModuleOp module, + const NativeGateset& spec) { + MLIRContext* ctx = module.getContext(); + { + RewritePatternSet windowPatterns(ctx); + windowPatterns.add(ctx, spec); + if (failed(applyPatternsGreedily(module, std::move(windowPatterns)))) { + return failure(); + } + } + RewritePatternSet lowerPatterns(ctx); + lowerPatterns.add(ctx, spec); + return applyPatternsGreedily(module, std::move(lowerPatterns)); } namespace { -struct FuseTwoQubitUnitaryRunsPass +struct FuseTwoQubitUnitaryRunsPass final : impl::FuseTwoQubitUnitaryRunsBase { - FuseTwoQubitUnitaryRunsPass() = default; + using Base::Base; explicit FuseTwoQubitUnitaryRunsPass(FuseTwoQubitUnitaryRunsOptions options) - : FuseTwoQubitUnitaryRunsBase(std::move(options)) {} + : Base(std::move(options)) {} protected: void runOnOperation() override { if (llvm::StringRef(nativeGates).trim().empty()) { return; } - auto specOpt = decomposition::NativeGateset::parse(nativeGates); - if (!specOpt) { + const auto spec = NativeGateset::parse(nativeGates); + if (!spec) { getOperation().emitError() << "unsupported native gateset (native-gates='" << nativeGates << "')"; signalPassFailure(); return; } - const auto& spec = *specOpt; - const decomposition::EulerBasis oneQubitBasis = *spec.eulerBasis; - - IRRewriter rewriter(&getContext()); - - fuseOneQubitRuns(rewriter, spec, oneQubitBasis); - if (failed(fuseTwoQubitUnitaryRuns(getOperation(), spec))) { - signalPassFailure(); - return; - } - constexpr unsigned kMaxSynthesisSweeps = 4; - for (unsigned i = 0; i < kMaxSynthesisSweeps; ++i) { - if (failed(synthesizeRemainingOps(rewriter, spec, oneQubitBasis))) { - signalPassFailure(); - return; - } - if (!hasNonNativeOps(getOperation(), spec, /*singleQubitOnly=*/true)) { - break; - } - } - if (hasNonNativeOps(getOperation(), spec, /*singleQubitOnly=*/true)) { - getOperation().emitError() - << "native gate synthesis did not converge within " - << kMaxSynthesisSweeps - << " sweeps (single-qubit ops remain outside the native menu)"; - signalPassFailure(); - return; - } - fuseOneQubitRuns(rewriter, spec, oneQubitBasis); - constexpr unsigned kPostMenuCleanupSweeps = 4; - unsigned postMenuSweepsRemaining = kPostMenuCleanupSweeps; - while (hasNonNativeOps(getOperation(), spec, /*singleQubitOnly=*/false) && - postMenuSweepsRemaining-- > 0) { - if (failed(synthesizeRemainingOps(rewriter, spec, oneQubitBasis))) { - signalPassFailure(); - return; - } - fuseOneQubitRuns(rewriter, spec, oneQubitBasis); - } - if (hasNonNativeOps(getOperation(), spec, /*singleQubitOnly=*/false)) { - getOperation().emitError() - << "native gate synthesis: operations remain outside the native menu " - "after final cleanup"; + const EulerBasis basis = *spec->eulerBasis; + ModuleOp module = getOperation(); + + // 1. Fuse single-qubit runs (also lowers lone off-gateset single-qubit + // ops). + // 2. Fuse two-qubit windows and lower remaining off-gateset two-qubit ops. + // 3. Fuse the single-qubit seams introduced by two-qubit synthesis. + if (failed(fuseSingleQubitRuns(module, basis)) || + failed(fuseAndLowerTwoQubitOps(module, *spec)) || + failed(fuseSingleQubitRuns(module, basis))) { signalPassFailure(); return; } - } - -private: - void fuseOneQubitRuns(IRRewriter& rewriter, - const decomposition::NativeGateset& spec, - const decomposition::EulerBasis basis) { - SmallVector runs; - llvm::DenseMap tailOpToRun; - - // Require single-use tail output so fan-out wires are not fused away. - getOperation()->walk([&](Operation* op) { - auto unitary = fusibleSingleQubitOp(op); - if (!unitary) { - return; - } - Value inQubit = unitary.getInputQubit(0); - Operation* defOp = inQubit.getDefiningOp(); - auto it = - (defOp != nullptr) ? tailOpToRun.find(defOp) : tailOpToRun.end(); - const bool canExtend = it != tailOpToRun.end() && inQubit.hasOneUse(); - if (canExtend) { - const size_t runIdx = it->second; - runs[runIdx].ops.push_back(unitary); - tailOpToRun.erase(it); - tailOpToRun[op] = runIdx; - } else { - runs.push_back(OneQubitRun{}); - runs.back().ops.push_back(unitary); - tailOpToRun[op] = runs.size() - 1; - } - }); - - for (auto& run : runs) { - if (run.ops.size() < 2) { - continue; - } - (void)maybeFuseRun(rewriter, run, basis, spec); - } - } - - LogicalResult synthesizeRemainingOps(IRRewriter& rewriter, - const decomposition::NativeGateset& spec, - const decomposition::EulerBasis basis) { - SmallVector ops; - collectUnitaryOpsInPreOrder(getOperation(), ops); - llvm::DenseSet erasedOps; - - for (Operation* op : ops) { - if (erasedOps.contains(op)) { - continue; - } - if (!isWalkableUnitaryShell(op)) { - continue; - } - - if (auto ctrl = llvm::dyn_cast(op)) { - const bool wasAlreadyNative = allowsOp(op, spec); - if (failed(synthesizeControlled(rewriter, ctrl, spec))) { - return failure(); - } - if (!wasAlreadyNative) { - erasedOps.insert(op); - } - continue; - } - - auto unitary = llvm::dyn_cast(op); - if (!unitary) { - continue; - } - - if (unitary.isSingleQubit()) { - if (!allowsOp(op, spec)) { - if (failed(rewriteSingleQubit(rewriter, op, unitary, basis))) { - return failure(); - } - erasedOps.insert(op); - } - continue; - } - - if (unitary.isTwoQubit()) { - if (failed(synthesizeBareTwoQubit(rewriter, op, unitary, spec))) { - return failure(); - } - erasedOps.insert(op); - } - } - return success(); - } - static LogicalResult - rewriteSingleQubit(IRRewriter& rewriter, Operation* op, - UnitaryOpInterface unitary, - const decomposition::EulerBasis basis) { - rewriter.setInsertionPoint(op); - const Value in = unitary.getInputQubit(0); - Matrix2x2 matrix; - if (!unitary.getUnitaryMatrix2x2(matrix)) { - op->emitError("single-qubit operation with non-constant parameters is " - "not supported for native synthesis"); - return failure(); + if (hasNonNativeOps(module, *spec)) { + module.emitError() << "native gate synthesis: operations remain outside " + "the native gateset (native-gates='" + << nativeGates << "')"; + signalPassFailure(); } - const Value replaced = - emitSingleQubitMatrix(rewriter, op->getLoc(), in, matrix, basis); - rewriter.replaceOp(op, replaced); - return success(); - } - - static LogicalResult - synthesizeControlled(IRRewriter& rewriter, CtrlOp ctrl, - const decomposition::NativeGateset& spec) { - return synthesizeTwoQubitOp( - rewriter, ctrl.getOperation(), ctrl.getLoc(), ctrl.getInputControl(0), - ctrl.getInputTarget(0), spec, - "native synthesis: cannot build a constant 4x4 matrix for this " - "controlled gate (unsupported body or non-constant parameters)", - "controlled gate not allowed by selected gateset"); - } - - static LogicalResult - synthesizeBareTwoQubit(IRRewriter& rewriter, Operation* op, - UnitaryOpInterface unitary, - const decomposition::NativeGateset& spec) { - return synthesizeTwoQubitOp( - rewriter, op, op->getLoc(), unitary.getInputQubit(0), - unitary.getInputQubit(1), spec, - "unsupported two-qubit operation for selected gateset", - "unsupported two-qubit operation for selected gateset"); } }; diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index c10ea5df1f..8568ce2c30 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -769,48 +769,6 @@ TEST(NativeSpecTest, ResolvesEulerBasisFromGateset) { EXPECT_EQ(*zyz->eulerBasis, EulerBasis::ZYZ); } -static std::optional gateKindFor(UnitaryOpInterface op) { - return llvm::TypeSwitch>( - op.getOperation()) - .Case([](UOp) { return NativeGateKind::U; }) - .Case([](XOp) { return NativeGateKind::X; }) - .Case([](SXOp) { return NativeGateKind::SX; }) - .Case([](RZOp) { return NativeGateKind::RZ; }) - .Case([](RXOp) { return NativeGateKind::RX; }) - .Case([](RYOp) { return NativeGateKind::RY; }) - .Case([](ROp) { return NativeGateKind::R; }) - .Default([](Operation*) { return std::nullopt; }); -} - -static std::optional entanglerKindFor(CtrlOp ctrl) { - if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1 || - ctrl.getNumBodyUnitaries() != 1) { - return std::nullopt; - } - return llvm::TypeSwitch>( - ctrl.getBodyUnitary(0).getOperation()) - .Case([](XOp) { return NativeGateKind::CX; }) - .Case([](ZOp) { return NativeGateKind::CZ; }) - .Default([](Operation*) { return std::nullopt; }); -} - -static bool allowsOp(Operation* op, const NativeGateset& spec) { - return llvm::TypeSwitch(op) - .Case([](auto) { return true; }) - .Case([&](CtrlOp ctrl) { - const auto kind = entanglerKindFor(ctrl); - return kind && spec.gates.contains(*kind); - }) - .Case([&](UnitaryOpInterface unitary) { - if (!unitary.isSingleQubit()) { - return false; - } - const auto gate = gateKindFor(unitary); - return gate && spec.gates.contains(*gate); - }) - .Default([](Operation*) { return false; }); -} - TEST_F(NativeGatesetMlirTest, AllowsOpMatchesGateset) { const auto spec = NativeGateset::parse("u,cx"); ASSERT_TRUE(spec); @@ -826,20 +784,19 @@ TEST_F(NativeGatesetMlirTest, AllowsOpMatchesGateset) { Value q0 = entry->getArgument(0); Value q1 = entry->getArgument(1); - EXPECT_TRUE(allowsOp( - BarrierOp::create(builder, loc, ValueRange{q0, q1}).getOperation(), - *spec)); + EXPECT_TRUE(spec->allowsOp( + BarrierOp::create(builder, loc, ValueRange{q0, q1}).getOperation())); EXPECT_TRUE( - allowsOp(GPhaseOp::create(builder, loc, 0.1).getOperation(), *spec)); - EXPECT_TRUE(allowsOp( - UOp::create(builder, loc, q0, 0.1, 0.2, 0.3).getOperation(), *spec)); + spec->allowsOp(GPhaseOp::create(builder, loc, 0.1).getOperation())); + EXPECT_TRUE(spec->allowsOp( + UOp::create(builder, loc, q0, 0.1, 0.2, 0.3).getOperation())); auto cx = CtrlOp::create( builder, loc, ValueRange{q0}, ValueRange{q1}, [&builder, &loc](ValueRange targets) -> SmallVector { return {XOp::create(builder, loc, targets[0]).getOutputQubit(0)}; }); - EXPECT_TRUE(allowsOp(cx.getOperation(), *spec)); + EXPECT_TRUE(spec->allowsOp(cx.getOperation())); auto cxWithInterleavedH = CtrlOp::create( builder, loc, ValueRange{q0}, ValueRange{q1}, @@ -847,25 +804,25 @@ TEST_F(NativeGatesetMlirTest, AllowsOpMatchesGateset) { auto wire = XOp::create(builder, loc, targets[0]).getOutputQubit(0); return {HOp::create(builder, loc, wire).getOutputQubit(0)}; }); - EXPECT_FALSE(allowsOp(cxWithInterleavedH.getOperation(), *spec)); + EXPECT_FALSE(spec->allowsOp(cxWithInterleavedH.getOperation())); - EXPECT_FALSE(allowsOp(XOp::create(builder, loc, q0).getOperation(), *spec)); + EXPECT_FALSE(spec->allowsOp(XOp::create(builder, loc, q0).getOperation())); EXPECT_FALSE( - allowsOp(RXXOp::create(builder, loc, q0, q1, 0.2).getOperation(), *spec)); + spec->allowsOp(RXXOp::create(builder, loc, q0, q1, 0.2).getOperation())); const auto rzSpec = NativeGateset::parse("x,sx,rz,cx"); ASSERT_TRUE(rzSpec); EXPECT_TRUE( - allowsOp(RZOp::create(builder, loc, q0, 0.3).getOperation(), *rzSpec)); + rzSpec->allowsOp(RZOp::create(builder, loc, q0, 0.3).getOperation())); EXPECT_FALSE( - allowsOp(POp::create(builder, loc, q0, 0.3).getOperation(), *rzSpec)); + rzSpec->allowsOp(POp::create(builder, loc, q0, 0.3).getOperation())); auto hCtrl = CtrlOp::create( builder, loc, ValueRange{q0}, ValueRange{q1}, [&builder, &loc](ValueRange targets) -> SmallVector { return {HOp::create(builder, loc, targets[0]).getOutputQubit(0)}; }); - EXPECT_FALSE(allowsOp(hCtrl.getOperation(), *spec)); + EXPECT_FALSE(spec->allowsOp(hCtrl.getOperation())); const auto funcTy3 = builder.getFunctionType({qubitTy, qubitTy, qubitTy}, {qubitTy, qubitTy, qubitTy}); @@ -880,7 +837,7 @@ TEST_F(NativeGatesetMlirTest, AllowsOpMatchesGateset) { [&builder, &loc](ValueRange targets) -> SmallVector { return {XOp::create(builder, loc, targets[0]).getOutputQubit(0)}; }); - EXPECT_FALSE(allowsOp(ccx.getOperation(), *spec)); + EXPECT_FALSE(spec->allowsOp(ccx.getOperation())); const auto czSpec = NativeGateset::parse("u,cz"); ASSERT_TRUE(czSpec); @@ -889,6 +846,6 @@ TEST_F(NativeGatesetMlirTest, AllowsOpMatchesGateset) { [&builder, &loc](ValueRange targets) -> SmallVector { return {ZOp::create(builder, loc, targets[0]).getOutputQubit(0)}; }); - EXPECT_TRUE(allowsOp(cz.getOperation(), *czSpec)); - EXPECT_FALSE(allowsOp(cx.getOperation(), *czSpec)); + EXPECT_TRUE(czSpec->allowsOp(cz.getOperation())); + EXPECT_FALSE(czSpec->allowsOp(cx.getOperation())); } From 3a1b758c61375e03e3c6818be36f2442dae7fdda Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 7 Jul 2026 12:51:18 +0200 Subject: [PATCH 110/122] =?UTF-8?q?=F0=9F=8E=A8=20Refactor=20unit=20tests?= =?UTF-8?q?=20for=20two-qubit=20unitary=20fusion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_fuse_two_qubit_unitary_runs.cpp | 696 ++++++++---------- 1 file changed, 289 insertions(+), 407 deletions(-) diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp index 68571420bd..7fc3607158 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp @@ -8,11 +8,11 @@ * Licensed under the MIT License */ -#include "TestCaseUtils.h" #include "mlir/Conversion/QCToQCO/QCToQCO.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/NativeGateset.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" #include "qc_programs.h" @@ -48,47 +48,27 @@ using namespace mlir; using namespace mlir::qco; -using namespace mqt::test; using ProgramFn = void (*)(mlir::qc::QCProgramBuilder&); -using NativePredicate = bool (*)(OwningOpRef&); -static void controlledXH(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.ctrl(q0, {q1}, [&](ValueRange targets) { - b.x(targets[0]); - b.h(targets[0]); - }); -} +// --- Native-gateset membership check ------------------------------------- // -template -static bool onlyTheseOps(OwningOpRef& moduleOp, bool allowCx, - bool allowCz) { +/// Returns true when every operation in @p moduleOp is native to the gateset +/// parsed from @p nativeGates. Operations nested inside a controlled shell are +/// validated through the shell itself. +static bool allOpsNative(OwningOpRef& moduleOp, + StringRef nativeGates) { + const auto spec = decomposition::NativeGateset::parse(nativeGates); + if (!spec) { + return false; + } bool ok = true; std::ignore = moduleOp->walk([&](UnitaryOpInterface op) { Operation* raw = op.getOperation(); - if (llvm::isa_and_present(raw->getParentOp())) { + if (isa_and_present(raw->getParentOp())) { return WalkResult::advance(); } - if (llvm::isa(raw)) { - return WalkResult::advance(); - } - if (auto ctrl = llvm::dyn_cast(raw)) { - if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { - ok = false; - return WalkResult::interrupt(); - } - Operation* body = ctrl.getBodyUnitary(0).getOperation(); - const bool isCx = llvm::isa(body); - const bool isCz = llvm::isa(body); - if ((isCx && allowCx) || (isCz && allowCz)) { - return WalkResult::advance(); - } - ok = false; - return WalkResult::interrupt(); - } - if (!llvm::isa(raw)) { + if (!spec->allowsOp(raw)) { ok = false; return WalkResult::interrupt(); } @@ -97,53 +77,24 @@ static bool onlyTheseOps(OwningOpRef& moduleOp, bool allowCx, return ok; } -static bool onlyIbmBasicCxOps(OwningOpRef& m) { - return onlyTheseOps(m, true, false); -} -static bool onlyIbmBasicCzOps(OwningOpRef& m) { - return onlyTheseOps(m, false, true); -} -static bool onlyGenericU3CxOps(OwningOpRef& m) { - return onlyTheseOps(m, true, false); -} -static bool onlyIqmDefaultOps(OwningOpRef& m) { - return onlyTheseOps(m, false, true); -} -static bool onlyAxisPairRxRzCxOps(OwningOpRef& m) { - return onlyTheseOps(m, true, false); -} -static bool onlyAxisPairRxRyCxOps(OwningOpRef& m) { - return onlyTheseOps(m, true, false); -} -static bool onlyAxisPairRyRzCzOps(OwningOpRef& m) { - return onlyTheseOps(m, false, true); -} -static bool onlyGenericU3CxOrCzOps(OwningOpRef& m) { - return onlyTheseOps(m, true, true); -} +// --- Reference unitary reconstruction ------------------------------------ // -static std::optional unitaryQubitOperand(UnitaryOpInterface op, - std::size_t index) { - if (index >= op.getNumQubits()) { - return std::nullopt; - } - Value v = op->getOperand(index); - if (!llvm::isa(v.getType())) { +static std::optional unitaryQubit(Value v, std::size_t index, + std::size_t numQubits) { + if (index >= numQubits || !isa(v.getType())) { return std::nullopt; } return v; } +static std::optional unitaryQubitOperand(UnitaryOpInterface op, + std::size_t index) { + return unitaryQubit(op->getOperand(index), index, op.getNumQubits()); +} + static std::optional unitaryQubitResult(UnitaryOpInterface op, std::size_t index) { - if (index >= op.getNumQubits()) { - return std::nullopt; - } - Value v = op->getResult(index); - if (!llvm::isa(v.getType())) { - return std::nullopt; - } - return v; + return unitaryQubit(op->getResult(index), index, op.getNumQubits()); } static bool extractSingleQubitMatrix(UnitaryOpInterface op, Matrix2x2& out) { @@ -161,11 +112,9 @@ static bool extractSingleQubitMatrix(UnitaryOpInterface op, Matrix2x2& out) { } static bool extractTwoQubitMatrix(UnitaryOpInterface op, Matrix4x4& out) { - if (auto ctrl = llvm::dyn_cast(op.getOperation())) { - if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { - return false; - } - return op.getUnitaryMatrix4x4(out); + if (auto ctrl = dyn_cast(op.getOperation()); + ctrl && (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1)) { + return false; } return op.getUnitaryMatrix4x4(out); } @@ -177,18 +126,18 @@ computeUnitaryFromQcoModule(const OwningOpRef& moduleOp) { return std::nullopt; } - llvm::DenseMap qubitIds; + DenseMap qubitIds; std::size_t nextQubitId = 0; std::size_t numQubits = 0; for (auto func : module.getOps()) { for (auto& block : func.getBlocks()) { for (auto& rawOp : block.getOperations()) { - if (auto staticOp = llvm::dyn_cast(&rawOp)) { + if (auto staticOp = dyn_cast(&rawOp)) { const auto index = static_cast(staticOp.getIndex()); qubitIds.try_emplace(staticOp.getQubit(), index); numQubits = std::max(numQubits, index + 1); - } else if (auto alloc = llvm::dyn_cast(&rawOp)) { + } else if (auto alloc = dyn_cast(&rawOp)) { qubitIds.try_emplace(alloc.getResult(), nextQubitId++); numQubits = std::max(numQubits, nextQubitId); } @@ -215,14 +164,14 @@ computeUnitaryFromQcoModule(const OwningOpRef& moduleOp) { for (auto func : module.getOps()) { for (auto& block : func.getBlocks()) { for (auto& rawOp : block.getOperations()) { - auto op = llvm::dyn_cast(&rawOp); + auto op = dyn_cast(&rawOp); if (!op) { continue; } - if (llvm::isa(op.getOperation())) { + if (isa(op.getOperation())) { continue; } - if (auto gphase = llvm::dyn_cast(op.getOperation())) { + if (auto gphase = dyn_cast(op.getOperation())) { if (const auto matrix = gphase.getUnitaryMatrix()) { globalPhase *= (*matrix)(0, 0); } @@ -285,20 +234,210 @@ computeUnitaryFromQcoModule(const OwningOpRef& moduleOp) { return globalPhase * unitary; } +// --- Expressive circuits -------------------------------------------------- // +// +// A handful of circuits, each covering a distinct pass behavior. They are +// crossed with the gateset table below so that every native basis exercises the +// same structural variety, instead of pairing each circuit with a single menu. + +/// A bare SWAP (three-entangler class), the canonical two-qubit decomposition. +static void swapTwoQ(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.swap(q0, q1); +} + +/// Rich single-qubit variety on both wires, followed by a two-qubit entangler. +static void broadOneQThenCz(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.x(q0); + b.y(q1); + b.h(q0); + b.sx(q1); + b.rx(0.13, q0); + b.ry(-0.47, q1); + b.rz(0.29, q0); + b.cz(q0, q1); +} + +/// Long single-qubit run on one wire, then an entangler (Euler-run fusion). +static void hstycxTwoQ(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.s(q0); + b.t(q0); + b.y(q0); + b.cx(q0, q1); +} + +/// Zero-angle rotations that must canonicalize away before the entangler. +static void zeroAngleThenCz(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.rx(0.0, q0); + b.ry(0.0, q1); + b.rz(0.0, q0); + b.p(0.0, q1); + b.cz(q0, q1); +} + +/// Single-qubit gates surrounding an entangler on both sides. +static void hCxSq1(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.cx(q0, q1); + b.s(q1); +} + +/// Three-qubit program with chained entanglers on overlapping pairs. +static void threeQGhz(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + const auto q2 = b.allocQubit(); + b.h(q0); + b.cx(q0, q1); + b.cx(q1, q2); +} + +/// Single-qubit gates wrapped in an inverse modifier (no entangler). +static void inverseTwoX(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + b.inv({q0}, [&](mlir::ValueRange qubits) { + b.x(qubits[0]); + b.x(qubits[0]); + }); +} + +/// A controlled two-gate body that must be synthesized as a two-qubit unitary. +static void controlledXH(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.ctrl(q0, {q1}, [&](ValueRange targets) { + b.x(targets[0]); + b.h(targets[0]); + }); +} + +// --- Fusion-window circuits ---------------------------------------------- // +// +// These probe window geometry (where fusion starts/stops), so they run on a +// single fixed gateset rather than the full table. + +static void fusionCxCx(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.cx(q0, q1); + b.cx(q0, q1); +} + +static void fusionHCxInterleavedTCx(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.h(q0); + b.cx(q0, q1); + b.t(q1); + b.s(q0); + b.cx(q0, q1); +} + +static void fusionThreeLineCx(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + const auto q2 = b.allocQubit(); + b.cx(q0, q1); + b.cx(q1, q2); + b.cx(q0, q1); +} + +static void fusionCxRSharedOtherPair(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + const auto q2 = b.allocQubit(); + b.cx(q0, q1); + b.rz(0.17, q1); + b.cx(q1, q2); +} + +static void fusionCxBarrierCx(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.cx(q0, q1); + b.barrier({q0, q1}); + b.cx(q0, q1); +} + +static void fusionSwapCxPattern(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.cx(q0, q1); + b.cx(q1, q0); + b.cx(q0, q1); +} + +static void fusionOffMenuGateInWindow(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.cx(q0, q1); + b.h(q0); + b.cx(q0, q1); +} + +static void fusionDualWireOneQBetweenCx(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.cx(q0, q1); + b.rz(0.11, q0); + b.ry(0.22, q1); + b.cx(q0, q1); +} + +static void determinismSwap(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.swap(q0, q1); + b.dealloc(q0); + b.dealloc(q1); +} + namespace { -struct ProfileCase { +/// A named circuit builder, used as a test parameter. +struct NamedProgram { const char* name; ProgramFn program; - const char* nativeGates; - NativePredicate isNative; - bool checkEquivalence; }; +/// Native gatesets spanning every supported single-qubit basis and both +/// entangler families (plus a multi-entangler menu). Because the pass +/// re-synthesizes each two-qubit window into the target basis, every circuit is +/// valid input for every gateset. +constexpr std::array GATESETS = { + // CX entangler family + "x,sx,rz,cx", // ZSXX + "u,cx", // U + "rx,rz,cx", // XZX + "rx,ry,cx", // XYX + // CZ entangler family + "r,cz", // R + "ry,rz,cz", // ZYZ + "x,sx,rz,cz", // ZSXX + "u,cz", // U + // Multiple entanglers (cx preferred) + "u,cx,cz", +}; + +/// Gateset used for the fusion-window suite, which asserts on structure rather +/// than on native-basis coverage. +constexpr StringRef FUSION_GATESET = "u,cx"; + +/// Structural expectations for a fusion-window circuit under @ref +/// FUSION_GATESET. struct FusionCase { const char* name; ProgramFn program; - const char* nativeGates; std::optional exactCtrlCount; std::optional minCtrlCount; bool checkTwoQUnitary; @@ -349,22 +488,14 @@ class FuseTwoQubitUnitaryRunsPassTest : public testing::Test { EXPECT_TRUE(lhsUnitary->isApprox(*rhsUnitary)); } - void expectNativeAfterSynthesis(ProgramFn program, StringRef nativeGates, - NativePredicate isNative) { - auto moduleOp = mlir::qc::QCProgramBuilder::build(context.get(), program); - runFusePipeline(moduleOp, nativeGates); - EXPECT_TRUE(isNative(moduleOp)); - } - void expectEquivalentAndNativeAfterSynthesis(ProgramFn program, - StringRef nativeGates, - NativePredicate isNative) { + StringRef nativeGates) { auto expected = mlir::qc::QCProgramBuilder::build(context.get(), program); runQcToQco(expected); auto synthesized = mlir::qc::QCProgramBuilder::build(context.get(), program); runFusePipeline(synthesized, nativeGates); - EXPECT_TRUE(isNative(synthesized)); + EXPECT_TRUE(allOpsNative(synthesized, nativeGates)); expectQcoModulesEquivalent(expected, synthesized); } @@ -400,9 +531,11 @@ class FuseTwoQubitUnitaryRunsPassTest : public testing::Test { std::unique_ptr context; }; -class FuseTwoQubitProfileTest +using SynthesisParam = std::tuple; + +class FuseTwoQubitSynthesisTest : public FuseTwoQubitUnitaryRunsPassTest, - public testing::WithParamInterface {}; + public testing::WithParamInterface {}; class FuseTwoQubitFusionTest : public FuseTwoQubitUnitaryRunsPassTest, public testing::WithParamInterface { @@ -410,236 +543,74 @@ class FuseTwoQubitFusionTest : public FuseTwoQubitUnitaryRunsPassTest, } // namespace -static void fusionCxCx(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.cx(q0, q1); - b.cx(q0, q1); -} - -static void fusionHCxInterleavedTCx(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.h(q0); - b.cx(q0, q1); - b.t(q1); - b.s(q0); - b.cx(q0, q1); -} - -static void fusionThreeLineCx(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - const auto q2 = b.allocQubit(); - b.cx(q0, q1); - b.cx(q1, q2); - b.cx(q0, q1); -} - -static void fusionCxRSharedOtherPair(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - const auto q2 = b.allocQubit(); - b.cx(q0, q1); - b.rz(0.17, q1); - b.cx(q1, q2); -} - -static void fusionCxBarrierCx(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.cx(q0, q1); - b.barrier({q0, q1}); - b.cx(q0, q1); -} - -static void fusionSwapCxPattern(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.cx(q0, q1); - b.cx(q1, q0); - b.cx(q0, q1); -} - -static void fusionOffMenuGateInWindow(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.cx(q0, q1); - b.h(q0); - b.cx(q0, q1); -} - -static void fusionDualWireOneQBetweenCx(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.cx(q0, q1); - b.rz(0.11, q0); - b.ry(0.22, q1); - b.cx(q0, q1); -} - -static void inverseTwoX(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - b.inv({q0}, [&](mlir::ValueRange qubits) { - b.x(qubits[0]); - b.x(qubits[0]); - }); -} - -static void broadOneQThenCz(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.x(q0); - b.y(q1); - b.h(q0); - b.sx(q1); - b.rx(0.13, q0); - b.ry(-0.47, q1); - b.rz(0.29, q0); - b.cz(q0, q1); -} - -static void zeroAngleThenCz(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.rx(0.0, q0); - b.ry(0.0, q1); - b.rz(0.0, q0); - b.p(0.0, q1); - b.cz(q0, q1); -} - -static void hstycxTwoQ(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.h(q0); - b.s(q0); - b.t(q0); - b.y(q0); - b.cx(q0, q1); -} - -static void cxYOnQ1(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.cx(q0, q1); - b.y(q1); -} - -static void hCxTOnQ1(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.h(q1); - b.cx(q0, q1); - b.t(q1); -} - -static void xYSXCz(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.x(q0); - b.y(q0); - b.sx(q0); - b.cz(q0, q1); -} - -static void hYCx(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.h(q0); - b.y(q0); - b.cx(q0, q1); -} - -static void zCx(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.z(q0); - b.cx(q0, q1); -} - -static void xHCz(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.x(q0); - b.h(q0); - b.cz(q0, q1); -} - -static void hq0Yq1CxSq0(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.h(q0); - b.y(q1); - b.cx(q0, q1); - b.s(q0); -} +// --- Synthesis: every expressive circuit against every gateset ----------- // -static void hCxSq1(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.h(q0); - b.cx(q0, q1); - b.s(q1); +TEST_P(FuseTwoQubitSynthesisTest, IsNativeAndEquivalent) { + const auto& [circuit, gateset] = GetParam(); + expectEquivalentAndNativeAfterSynthesis(circuit.program, gateset); } -static void threeQGhz(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - const auto q2 = b.allocQubit(); - b.h(q0); - b.cx(q0, q1); - b.cx(q1, q2); -} +INSTANTIATE_TEST_SUITE_P( + Circuits, FuseTwoQubitSynthesisTest, + testing::Combine( + testing::Values(NamedProgram{"Swap", swapTwoQ}, + NamedProgram{"BroadOneQThenCz", broadOneQThenCz}, + NamedProgram{"HstyThenCx", hstycxTwoQ}, + NamedProgram{"ZeroAngleThenCz", zeroAngleThenCz}, + NamedProgram{"SurroundedCx", hCxSq1}, + NamedProgram{"ThreeQubitGhz", threeQGhz}, + NamedProgram{"InverseBody", inverseTwoX}, + NamedProgram{"ControlledBody", controlledXH}), + testing::ValuesIn(GATESETS)), + [](const testing::TestParamInfo& info) { + std::string gateset = std::get<1>(info.param); + std::replace(gateset.begin(), gateset.end(), ',', '_'); + return std::string(std::get<0>(info.param).name) + "__" + gateset; + }); -static void determinismSwap(mlir::qc::QCProgramBuilder& b) { - const auto q0 = b.allocQubit(); - const auto q1 = b.allocQubit(); - b.swap(q0, q1); - b.dealloc(q0); - b.dealloc(q1); -} +// --- Fusion windows: structural behavior on a fixed gateset -------------- // -TEST_P(FuseTwoQubitProfileTest, SynthesizesToNativeMenu) { - const ProfileCase& c = GetParam(); - if (c.checkEquivalence) { - expectEquivalentAndNativeAfterSynthesis(c.program, c.nativeGates, - c.isNative); - } else { - expectNativeAfterSynthesis(c.program, c.nativeGates, c.isNative); +TEST_P(FuseTwoQubitFusionTest, WindowFusionBehavior) { + const FusionCase& c = GetParam(); + if (c.checkTwoQUnitary) { + expectTwoQFusePreservesUnitary(c.program, FUSION_GATESET); + } + auto module = mlir::qc::QCProgramBuilder::build(context.get(), c.program); + ASSERT_TRUE(module); + runQcToQco(module); + runTwoQFuse(module, FUSION_GATESET); + if (c.exactCtrlCount) { + EXPECT_EQ(countCtrlOps(module), *c.exactCtrlCount); + } + if (c.minCtrlCount) { + EXPECT_GE(countCtrlOps(module), *c.minCtrlCount); } } INSTANTIATE_TEST_SUITE_P( - Menus, FuseTwoQubitProfileTest, + Windows, FuseTwoQubitFusionTest, testing::Values( - ProfileCase{"SwapIbmBasic", mlir::qc::swap, "x,sx,rz,cx", - onlyIbmBasicCxOps, false}, - ProfileCase{"SwapGeneric", mlir::qc::swap, "u,cx", onlyGenericU3CxOps, - false}, - ProfileCase{"SwapIqm", mlir::qc::swap, "r,cz", onlyIqmDefaultOps, - false}, - ProfileCase{"HstycxIbm", hstycxTwoQ, "x,sx,rz,cx", onlyIbmBasicCxOps, - false}, - ProfileCase{"CxYIqm", cxYOnQ1, "r,cz", onlyIqmDefaultOps, false}, - ProfileCase{"BroadOneQIqm", broadOneQThenCz, "r,cz", onlyIqmDefaultOps, - false}, - ProfileCase{"ZeroAngleRyRzCz", zeroAngleThenCz, "ry,rz,cz", - onlyAxisPairRyRzCzOps, false}, - ProfileCase{"HCxTIbmCz", hCxTOnQ1, "x,sx,rz,cz", onlyIbmBasicCzOps, - false}, - ProfileCase{"XYSXCzIqm", xYSXCz, "r,cz", onlyIqmDefaultOps, false}, - ProfileCase{"HYCxRxRz", hYCx, "rx,rz,cx", onlyAxisPairRxRzCxOps, false}, - ProfileCase{"ZCxRxRy", zCx, "rx,ry,cx", onlyAxisPairRxRyCxOps, false}, - ProfileCase{"Hq0Yq1CxSq0", hq0Yq1CxSq0, "u,cx", onlyGenericU3CxOps, - true}, - ProfileCase{"XHCzRyRz", xHCz, "ry,rz,cz", onlyAxisPairRyRzCzOps, true}, - ProfileCase{"HCxSq1MultiEnt", hCxSq1, "u,cx,cz", onlyGenericU3CxOrCzOps, - true}), - [](const testing::TestParamInfo& info) { + FusionCase{"AdjacentCxCancel", fusionCxCx, 0, std::nullopt, true}, + FusionCase{"InterleavedOneQ", fusionHCxInterleavedTCx, std::nullopt, + std::nullopt, true}, + FusionCase{"DifferentPairBoundary", fusionThreeLineCx, std::nullopt, 1, + false}, + FusionCase{"SharedWireOneQ", fusionCxRSharedOtherPair, std::nullopt, 2, + false}, + FusionCase{"BarrierBoundary", fusionCxBarrierCx, 2, std::nullopt, + false}, + FusionCase{"SwappedWireOrder", fusionSwapCxPattern, std::nullopt, + std::nullopt, true}, + FusionCase{"OffMenuGateInWindow", fusionOffMenuGateInWindow, + std::nullopt, std::nullopt, true}, + FusionCase{"DualWireOneQBetweenCx", fusionDualWireOneQBetweenCx, + std::nullopt, std::nullopt, true}), + [](const testing::TestParamInfo& info) { return info.param.name; }); +// --- Pass edge cases ----------------------------------------------------- // + TEST_F(FuseTwoQubitUnitaryRunsPassTest, EmptyNativeGatesSkipsPass) { auto module = mlir::qc::QCProgramBuilder::build(context.get(), fusionCxCx); ASSERT_TRUE(module); @@ -660,16 +631,7 @@ TEST_F(FuseTwoQubitUnitaryRunsPassTest, EmptyNativeGatesSkipsPass) { EXPECT_EQ(before, after); } -TEST_F(FuseTwoQubitUnitaryRunsPassTest, InverseWrappedOpsSynthesize) { - expectNativeAfterSynthesis(inverseTwoX, "x,sx,rz,cx", onlyIbmBasicCxOps); -} - -TEST_F(FuseTwoQubitUnitaryRunsPassTest, ControlledXHSynthesizesToNativeMenu) { - expectEquivalentAndNativeAfterSynthesis(controlledXH, "x,sx,rz,cx", - onlyIbmBasicCxOps); -} - -TEST_F(FuseTwoQubitUnitaryRunsPassTest, FailsForUnsupportedNativeGateMenu) { +TEST_F(FuseTwoQubitUnitaryRunsPassTest, FailsForInvalidNativeGateMenu) { expectSynthesisFailure(mlir::qc::h, "not-a-gate"); } @@ -700,83 +662,3 @@ TEST_F(FuseTwoQubitUnitaryRunsPassTest, secondModule->print(osSecond); EXPECT_EQ(first, second); } - -TEST_F(FuseTwoQubitUnitaryRunsPassTest, ThreeQubitGhzEquivalentOnCoreProfiles) { - const std::array profiles{{ - {.name = "GhzIbm", - .program = threeQGhz, - .nativeGates = "x,sx,rz,cx", - .isNative = onlyIbmBasicCxOps, - .checkEquivalence = false}, - {.name = "GhzGeneric", - .program = threeQGhz, - .nativeGates = "u,cx", - .isNative = onlyGenericU3CxOps, - .checkEquivalence = false}, - {.name = "GhzIqm", - .program = threeQGhz, - .nativeGates = "r,cz", - .isNative = onlyIqmDefaultOps, - .checkEquivalence = false}, - }}; - for (const ProfileCase& profile : profiles) { - auto expected = mlir::qc::QCProgramBuilder::build(context.get(), threeQGhz); - runQcToQco(expected); - auto synthesized = - mlir::qc::QCProgramBuilder::build(context.get(), threeQGhz); - runFusePipeline(synthesized, profile.nativeGates); - EXPECT_TRUE(profile.isNative(synthesized)); - expectQcoModulesEquivalent(expected, synthesized); - } -} - -TEST_P(FuseTwoQubitFusionTest, WindowFusionBehavior) { - const FusionCase& c = GetParam(); - if (c.checkTwoQUnitary) { - expectTwoQFusePreservesUnitary(c.program, c.nativeGates); - } - auto module = mlir::qc::QCProgramBuilder::build(context.get(), c.program); - ASSERT_TRUE(module); - runQcToQco(module); - runTwoQFuse(module, c.nativeGates); - if (c.exactCtrlCount) { - EXPECT_EQ(countCtrlOps(module), *c.exactCtrlCount); - } - if (c.minCtrlCount) { - EXPECT_GE(countCtrlOps(module), *c.minCtrlCount); - } -} - -INSTANTIATE_TEST_SUITE_P( - Windows, FuseTwoQubitFusionTest, - testing::Values(FusionCase{"AdjacentCxCancel", fusionCxCx, "u,cx", 0, - std::nullopt, true}, - FusionCase{"InterleavedOneQ", fusionHCxInterleavedTCx, - "u,cx", std::nullopt, std::nullopt, true}, - FusionCase{"DifferentPairBoundary", fusionThreeLineCx, - "u,cx", std::nullopt, 1, false}, - FusionCase{"SharedWireOneQ", fusionCxRSharedOtherPair, - "u,cx", std::nullopt, 2, false}, - FusionCase{"BarrierBoundary", fusionCxBarrierCx, "u,cx", 2, - std::nullopt, false}, - FusionCase{"SwappedWireOrder", fusionSwapCxPattern, "u,cx", - std::nullopt, std::nullopt, true}, - FusionCase{"OffMenuGateInWindow", fusionOffMenuGateInWindow, - "u,cx", std::nullopt, std::nullopt, true}, - FusionCase{"DualWireOneQBetweenCx", - fusionDualWireOneQBetweenCx, "u,cx", - std::nullopt, std::nullopt, true}), - [](const testing::TestParamInfo& info) { - return info.param.name; - }); - -TEST_F(FuseTwoQubitUnitaryRunsPassTest, InvalidNativeGatesFailsPass) { - auto module = mlir::qc::QCProgramBuilder::build(context.get(), fusionCxCx); - ASSERT_TRUE(module); - runQcToQco(module); - PassManager pm(module->getContext()); - pm.addPass(createFuseTwoQubitUnitaryRuns(FuseTwoQubitUnitaryRunsOptions{ - .nativeGates = "not-a-gate", - })); - EXPECT_TRUE(failed(pm.run(*module))); -} From f79d12f6b7d5056a8b73befc0858072edf9f9019 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 7 Jul 2026 13:31:12 +0200 Subject: [PATCH 111/122] =?UTF-8?q?=E2=9C=A8=20Enhance=20FuseTwoQubitUnita?= =?UTF-8?q?ryRuns=20pass=20with=20improved=20run=20scanning=20and=20metada?= =?UTF-8?q?ta=20management?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mlir/Dialect/QCO/Transforms/Passes.td | 10 +- .../FuseTwoQubitUnitaryRuns.cpp | 139 +++++++++++------- .../test_fuse_two_qubit_unitary_runs.cpp | 4 +- 3 files changed, 92 insertions(+), 61 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index d49a44fa36..e7383499a3 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -97,13 +97,13 @@ def FuseTwoQubitUnitaryRuns Supported pairs are exactly `rx`+`rz`, `rx`+`ry`, and `ry`+`rz`. Stages: fuse single-qubit runs (reusing `fuse-single-qubit-unitary-runs`, - which also lowers lone off-gateset single-qubit gates); fuse two-qubit - windows and lower any remaining off-gateset two-qubit ops via Weyl - synthesis; fuse the single-qubit seams introduced by that synthesis. The - pass fails if anything remains off the native gateset afterwards. + which also lowers lone off-gateset single-qubit gates); fuse two-qubit runs + and lower any remaining off-gateset two-qubit ops via Weyl synthesis; fuse + the single-qubit seams introduced by that synthesis. The pass fails if + anything remains off the native gateset afterwards. Lowering is deterministic: `cx` is preferred over `cz`, single-qubit - factors use the resolved Euler basis, and two-qubit window replacement uses + factors use the resolved Euler basis, and two-qubit run replacement uses the minimal entangler count from the synthesizer. }]; let options = [Option< diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp index ef3970a617..b22f7c3bdd 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp @@ -42,7 +42,24 @@ using decomposition::NativeGateset; using decomposition::populateFuseSingleQubitUnitaryRunsPatterns; using decomposition::synthesizeUnitary2QWeyl; -/// Skips unitaries nested under `ctrl`/`inv` bodies (handled on the shell op). +namespace { + +/** Composed unitary and metadata for a fusable two-qubit run. */ +struct FusableTwoQubitRun { + SmallVector ops; ///< Members in program order. + Matrix4x4 composed = Matrix4x4::identity(); + unsigned numTwoQ = 0; ///< Number of two-qubit members (entanglers consumed). + bool hasNonNativeGate = false; ///< Any member off the native gateset. + Value tailA; ///< Current output wires of the run's tail. + Value tailB; +}; + +} // namespace + +// --- Run membership ------------------------------------------------------- // + +/// Whether `op` is nested under a `ctrl`/`inv` body. Such unitaries are handled +/// through their shell op, so the top-level walk skips them. static bool isExcludedFromTopLevelUnitaryWalk(Operation* op) { if (op->getParentOfType()) { return true; @@ -50,6 +67,7 @@ static bool isExcludedFromTopLevelUnitaryWalk(Operation* op) { return !llvm::isa(op) && op->getParentOfType(); } +/// Whether `op` is a unitary shell the pass may rewrite at top level. static bool isWalkableUnitaryShell(Operation* op) { return !llvm::isa(op) && !isExcludedFromTopLevelUnitaryWalk(op); @@ -76,7 +94,8 @@ static bool assignTwoQubitOpMatrix(Operation* op, Matrix4x4& matrix) { return unitary.getUnitaryMatrix4x4(matrix); } -static bool isOneQubitWindowMember(UnitaryOpInterface unitary) { +/// Whether `unitary` is a single-qubit gate that can join a run. +static bool isOneQubitRunMember(UnitaryOpInterface unitary) { if (!unitary || !unitary.isSingleQubit() || !isWalkableUnitaryShell(unitary.getOperation())) { return false; @@ -85,6 +104,7 @@ static bool isOneQubitWindowMember(UnitaryOpInterface unitary) { return unitary.getUnitaryMatrix2x2(matrix); } +/// Whether `unitary` is a two-qubit gate that can join a run. static bool isTwoQubitRunMember(UnitaryOpInterface unitary) { if (!unitary || !unitary.isTwoQubit() || !isWalkableUnitaryShell(unitary.getOperation())) { @@ -94,6 +114,10 @@ static bool isTwoQubitRunMember(UnitaryOpInterface unitary) { return assignTwoQubitOpMatrix(unitary.getOperation(), matrix); } +// --- Wire navigation ------------------------------------------------------ // + +/// The sole run-member consumer of `wire`, or a null interface when `wire` has +/// no unique user or its user cannot join a run. static UnitaryOpInterface uniqueUnitaryUser(Value wire) { if (!wire.hasOneUse()) { return {}; @@ -106,11 +130,13 @@ static UnitaryOpInterface uniqueUnitaryUser(Value wire) { return isTwoQubitRunMember(unitary) ? unitary : UnitaryOpInterface{}; } if (unitary.isSingleQubit()) { - return isOneQubitWindowMember(unitary) ? unitary : UnitaryOpInterface{}; + return isOneQubitRunMember(unitary) ? unitary : UnitaryOpInterface{}; } return {}; } +/// Traces `wire` upstream through single-qubit gates to the two-qubit run +/// member terminating the chain, or `nullptr` if the chain is broken. static Operation* twoQubitGateAtEndOfOneQChain(Value wire) { Value cur = wire; while (Operation* def = cur.getDefiningOp()) { @@ -121,7 +147,7 @@ static Operation* twoQubitGateAtEndOfOneQChain(Value wire) { if (unitary.isTwoQubit()) { return isTwoQubitRunMember(unitary) ? def : nullptr; } - if (!isOneQubitWindowMember(unitary)) { + if (!isOneQubitRunMember(unitary)) { return nullptr; } cur = unitary.getInputQubit(0); @@ -129,7 +155,9 @@ static Operation* twoQubitGateAtEndOfOneQChain(Value wire) { return nullptr; } -static bool feedsFromSameTwoQubitWindow(UnitaryOpInterface op) { +/// Whether both input wires of `op` come from one earlier two-qubit run, making +/// `op` a continuation of that run rather than a fresh run start. +static bool feedsFromSameTwoQubitRun(UnitaryOpInterface op) { const Value in0 = op.getInputQubit(0); const Value in1 = op.getInputQubit(1); if (!in0.hasOneUse() || !in1.hasOneUse()) { @@ -140,19 +168,11 @@ static bool feedsFromSameTwoQubitWindow(UnitaryOpInterface op) { return gate0 != nullptr && gate0 == gate1; } -namespace { - -struct FusableTwoQubitRun { - SmallVector ops; - Matrix4x4 composed = Matrix4x4::identity(); - unsigned numTwoQ = 0; - bool anyNonNative = false; - Value tailA; - Value tailB; -}; - -} // namespace +// --- Run scanning --------------------------------------------------------- // +/// Appends a two-qubit gate to `run`, composing its matrix. No-op unless both +/// of `op`'s inputs are the run's current tail wires (in either order), keeping +/// the run confined to a single pair of wires. static void absorbTwoQubitIntoRun(FusableTwoQubitRun& run, UnitaryOpInterface op, const NativeGateset& spec) { @@ -178,9 +198,10 @@ static void absorbTwoQubitIntoRun(FusableTwoQubitRun& run, run.composed.premultiplyBy(opMatrix.reorderForQubits(id0, id1)); run.ops.push_back(op.getOperation()); ++run.numTwoQ; - run.anyNonNative |= !spec.allowsOp(op.getOperation()); + run.hasNonNativeGate |= !spec.allowsOp(op.getOperation()); } +/// Appends a single-qubit gate on run wire `wireIndex` (0 = A, 1 = B). static void absorbOneQubitIntoRun(FusableTwoQubitRun& run, UnitaryOpInterface op, const NativeGateset& spec, @@ -191,10 +212,14 @@ static void absorbOneQubitIntoRun(FusableTwoQubitRun& run, } run.composed.premultiplyBy(raw.embedInTwoQubit(wireIndex)); run.ops.push_back(op.getOperation()); - run.anyNonNative |= !spec.allowsOp(op.getOperation()); + run.hasNonNativeGate |= !spec.allowsOp(op.getOperation()); (wireIndex == 0 ? run.tailA : run.tailB) = op.getOutputQubit(0); } +/// Walks forward from `head`, composing the run's matrix and metadata. Absorbs +/// a following two-qubit gate when it keeps both run wires together, otherwise +/// the single-qubit gate first in program order; stops at the first boundary +/// that would split the run's two wires. static FusableTwoQubitRun scanFusableTwoQubitRun(UnitaryOpInterface head, const NativeGateset& spec) { FusableTwoQubitRun run; @@ -205,7 +230,7 @@ static FusableTwoQubitRun scanFusableTwoQubitRun(UnitaryOpInterface head, run.tailB = head.getOutputQubit(1); run.ops.push_back(head.getOperation()); run.numTwoQ = 1; - run.anyNonNative |= !spec.allowsOp(head.getOperation()); + run.hasNonNativeGate |= !spec.allowsOp(head.getOperation()); while (true) { UnitaryOpInterface nextOnA = uniqueUnitaryUser(run.tailA); @@ -233,8 +258,16 @@ static FusableTwoQubitRun scanFusableTwoQubitRun(UnitaryOpInterface head, return run; } -/// Whether any two-qubit or single-qubit unitary op (including `ctrl` shells) -/// remains off the native gateset. Used as the pass' final convergence check. +/// Erases all run members, successors first so each is dead when erased. +static void eraseFusableRun(PatternRewriter& rewriter, + const FusableTwoQubitRun& run) { + for (Operation* member : llvm::reverse(run.ops)) { + rewriter.eraseOp(member); + } +} + +/// Whether any two-qubit or single-qubit unitary (including `ctrl` shells) +/// remains off the native gateset. Used as the pass' convergence check. static bool hasNonNativeOps(Operation* root, const NativeGateset& spec) { const WalkResult walkResult = root->walk([&](Operation* op) { if (!isWalkableUnitaryShell(op) || !llvm::isa(op)) { @@ -247,18 +280,26 @@ static bool hasNonNativeOps(Operation* root, const NativeGateset& spec) { namespace { -/// Fuses a maximal window of two-qubit gates (plus interleaved single-qubit -/// gates) into a single composed unitary and resynthesizes it to the native -/// gateset when that is beneficial. -struct FuseTwoQubitWindowPattern final +/// Fuses a maximal two-qubit run into one composed unitary and resynthesizes it +/// to the native gateset when beneficial. +struct FuseTwoQubitUnitaryRunsPattern final : OpInterfaceRewritePattern { - FuseTwoQubitWindowPattern(MLIRContext* ctx, NativeGateset specIn) + FuseTwoQubitUnitaryRunsPattern(MLIRContext* ctx, NativeGateset specIn) : OpInterfaceRewritePattern(ctx), spec(std::move(specIn)) {} + NativeGateset spec; + + /// Whether `op` anchors a run: a two-qubit run member whose two wires are not + /// both fed by the same earlier run (which would make it a continuation). + static bool isRunStart(UnitaryOpInterface op) { + return isTwoQubitRunMember(op) && !feedsFromSameTwoQubitRun(op); + } + + /// Fuses the run anchored at `op` if it contains an off-gateset gate or Weyl + /// resynthesis uses fewer entanglers than the run's two-qubit members. LogicalResult matchAndRewrite(UnitaryOpInterface op, PatternRewriter& rewriter) const override { - // Only anchor on a run start: a run member not fed by an earlier window. - if (!isTwoQubitRunMember(op) || feedsFromSameTwoQubitWindow(op)) { + if (!isRunStart(op)) { return failure(); } @@ -267,10 +308,9 @@ struct FuseTwoQubitWindowPattern final return failure(); } - // Replace when off-gateset ops must be lowered, or when resynthesis uses - // fewer entanglers than the fused window. const auto native = spec.decomposeTarget(run.composed); - if (!native || (!run.anyNonNative && native->numBasisUses >= run.numTwoQ)) { + if (!native || + (!run.hasNonNativeGate && native->numBasisUses >= run.numTwoQ)) { return failure(); } @@ -286,24 +326,22 @@ struct FuseTwoQubitWindowPattern final } rewriter.replaceAllUsesWith(run.tailA, newA); rewriter.replaceAllUsesWith(run.tailB, newB); - for (Operation* member : llvm::reverse(run.ops)) { - rewriter.eraseOp(member); - } + eraseFusableRun(rewriter, run); return success(); } - - NativeGateset spec; }; -/// Lowers a single off-gateset two-qubit op (bare or single-target `CtrlOp`) -/// to the native entangler plus native single-qubit factors via Weyl synthesis. -/// Native two-qubit ops and windows fusable by @ref FuseTwoQubitWindowPattern -/// are left to that pattern. +/// Lowers a single off-gateset two-qubit op (bare or single-target `CtrlOp`) to +/// the native entangler plus native single-qubit factors via Weyl synthesis. +/// Native two-qubit ops and fusable runs are left to +/// @ref FuseTwoQubitUnitaryRunsPattern. struct LowerTwoQubitOpPattern final : OpInterfaceRewritePattern { LowerTwoQubitOpPattern(MLIRContext* ctx, NativeGateset specIn) : OpInterfaceRewritePattern(ctx), spec(std::move(specIn)) {} + NativeGateset spec; + LogicalResult matchAndRewrite(UnitaryOpInterface op, PatternRewriter& rewriter) const override { Operation* raw = op.getOperation(); @@ -335,17 +373,13 @@ struct LowerTwoQubitOpPattern final rewriter.replaceOp(raw, ValueRange{out0, out1}); return success(); } - - NativeGateset spec; }; } // namespace /// Fuses single-qubit runs (and lowers lone off-gateset single-qubit ops) by -/// reusing the `fuse-single-qubit-unitary-runs` rewrite. -/// -/// `qco.ctrl` bodies are skipped so the `X`/`Z` bodies of native entanglers are -/// preserved (both before and after two-qubit synthesis). +/// reusing the `fuse-single-qubit-unitary-runs` rewrite. `qco.ctrl` bodies are +/// skipped so the `X`/`Z` bodies of native entanglers are preserved. static LogicalResult fuseSingleQubitRuns(ModuleOp module, const EulerBasis basis) { RewritePatternSet patterns(module.getContext()); @@ -354,15 +388,14 @@ static LogicalResult fuseSingleQubitRuns(ModuleOp module, return applyPatternsGreedily(module, std::move(patterns)); } -/// Fuses two-qubit windows, then lowers any remaining off-gateset two-qubit -/// ops. +/// Fuses two-qubit runs, then lowers any remaining off-gateset two-qubit ops. static LogicalResult fuseAndLowerTwoQubitOps(ModuleOp module, const NativeGateset& spec) { MLIRContext* ctx = module.getContext(); { - RewritePatternSet windowPatterns(ctx); - windowPatterns.add(ctx, spec); - if (failed(applyPatternsGreedily(module, std::move(windowPatterns)))) { + RewritePatternSet runPatterns(ctx); + runPatterns.add(ctx, spec); + if (failed(applyPatternsGreedily(module, std::move(runPatterns)))) { return failure(); } } @@ -397,7 +430,7 @@ struct FuseTwoQubitUnitaryRunsPass final // 1. Fuse single-qubit runs (also lowers lone off-gateset single-qubit // ops). - // 2. Fuse two-qubit windows and lower remaining off-gateset two-qubit ops. + // 2. Fuse two-qubit runs and lower remaining off-gateset two-qubit ops. // 3. Fuse the single-qubit seams introduced by two-qubit synthesis. if (failed(fuseSingleQubitRuns(module, basis)) || failed(fuseAndLowerTwoQubitOps(module, *spec)) || diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp index 7fc3607158..bd2b8e7714 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp @@ -236,9 +236,7 @@ computeUnitaryFromQcoModule(const OwningOpRef& moduleOp) { // --- Expressive circuits -------------------------------------------------- // // -// A handful of circuits, each covering a distinct pass behavior. They are -// crossed with the gateset table below so that every native basis exercises the -// same structural variety, instead of pairing each circuit with a single menu. +// A handful of circuits, which are crossed with the gateset table below. /// A bare SWAP (three-entangler class), the canonical two-qubit decomposition. static void swapTwoQ(mlir::qc::QCProgramBuilder& b) { From cf5b04f2837519dde8eefaf0e3c99cfe5492e5a9 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 7 Jul 2026 13:40:10 +0200 Subject: [PATCH 112/122] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20linter=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/Decomposition/test_weyl_decomposition.cpp | 1 - .../NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index 8568ce2c30..37c175edc8 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -18,7 +18,6 @@ #include #include -#include #include #include #include diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp index bd2b8e7714..409a7eabd5 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp @@ -19,7 +19,6 @@ #include #include -#include #include #include #include @@ -562,7 +561,7 @@ INSTANTIATE_TEST_SUITE_P( testing::ValuesIn(GATESETS)), [](const testing::TestParamInfo& info) { std::string gateset = std::get<1>(info.param); - std::replace(gateset.begin(), gateset.end(), ',', '_'); + std::ranges::replace(gateset, ',', '_'); return std::string(std::get<0>(info.param).name) + "__" + gateset; }); From 0b7922ded8605f5b3848a7274ed65831eabe5f44 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 7 Jul 2026 14:17:10 +0200 Subject: [PATCH 113/122] =?UTF-8?q?=E2=98=82=EF=B8=8F=20Increase=20coverag?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mlir/Dialect/QCO/Transforms/Passes.td | 11 ++- .../FuseTwoQubitUnitaryRuns.cpp | 38 +++++---- .../test_fuse_two_qubit_unitary_runs.cpp | 81 +++++++++++++++++-- 3 files changed, 103 insertions(+), 27 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index e7383499a3..259e033e54 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -73,8 +73,10 @@ def FuseTwoQubitUnitaryRuns let summary = "Lower QCO unitary gates to a user-specified native gate menu."; let description = [{ Rewrites unitaries to match the comma-separated `native-gates` gateset. - `qco.barrier` and `qco.gphase` are preserved; `qco.ctrl` must have a - single control and a single target. + `qco.barrier` and `qco.gphase` are preserved. A `qco.ctrl` is treated as a + two-qubit gate only when it has a single control and a single target; gates + acting on more than two qubits (e.g. multi-controlled `qco.ctrl`) are out of + scope and left untouched for a dedicated multi-controlled synthesis pass. The gateset is a comma-separated list of gate tokens (order not significant) from which the pass resolves a single-qubit Euler basis @@ -99,8 +101,9 @@ def FuseTwoQubitUnitaryRuns Stages: fuse single-qubit runs (reusing `fuse-single-qubit-unitary-runs`, which also lowers lone off-gateset single-qubit gates); fuse two-qubit runs and lower any remaining off-gateset two-qubit ops via Weyl synthesis; fuse - the single-qubit seams introduced by that synthesis. The pass fails if - anything remains off the native gateset afterwards. + the single-qubit seams introduced by that synthesis. The pass fails if any + single- or two-qubit op remains off the native gateset afterwards; gates on + more than two qubits are left untouched and do not cause a failure. Lowering is deterministic: `cx` is preferred over `cz`, single-qubit factors use the resolved Euler basis, and two-qubit run replacement uses diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp index b22f7c3bdd..209a30fbc9 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -28,6 +29,7 @@ #include #include +#include #include #include #include @@ -160,9 +162,9 @@ static Operation* twoQubitGateAtEndOfOneQChain(Value wire) { static bool feedsFromSameTwoQubitRun(UnitaryOpInterface op) { const Value in0 = op.getInputQubit(0); const Value in1 = op.getInputQubit(1); - if (!in0.hasOneUse() || !in1.hasOneUse()) { - return false; - } + assert(in0.hasOneUse() && in1.hasOneUse() && + "qubit values are single-use, so a run member consumes each input " + "exactly once"); Operation* gate0 = twoQubitGateAtEndOfOneQChain(in0); Operation* gate1 = twoQubitGateAtEndOfOneQChain(in1); return gate0 != nullptr && gate0 == gate1; @@ -177,9 +179,9 @@ static void absorbTwoQubitIntoRun(FusableTwoQubitRun& run, UnitaryOpInterface op, const NativeGateset& spec) { Matrix4x4 opMatrix; - if (!assignTwoQubitOpMatrix(op.getOperation(), opMatrix)) { - return; - } + [[maybe_unused]] const bool assigned = + assignTwoQubitOpMatrix(op.getOperation(), opMatrix); + assert(assigned && "a two-qubit run member always exposes a 4x4 matrix"); const Value in0 = op.getInputQubit(0); const Value in1 = op.getInputQubit(1); std::size_t id0 = 0; @@ -193,7 +195,8 @@ static void absorbTwoQubitIntoRun(FusableTwoQubitRun& run, run.tailA = op.getOutputQubit(1); run.tailB = op.getOutputQubit(0); } else { - return; + llvm_unreachable( + "a unique user of both tail wires connects to both of them"); } run.composed.premultiplyBy(opMatrix.reorderForQubits(id0, id1)); run.ops.push_back(op.getOperation()); @@ -207,9 +210,8 @@ static void absorbOneQubitIntoRun(FusableTwoQubitRun& run, const NativeGateset& spec, unsigned wireIndex) { Matrix2x2 raw; - if (!op.getUnitaryMatrix2x2(raw)) { - return; - } + [[maybe_unused]] const bool assigned = op.getUnitaryMatrix2x2(raw); + assert(assigned && "a single-qubit run member always exposes a 2x2 matrix"); run.composed.premultiplyBy(raw.embedInTwoQubit(wireIndex)); run.ops.push_back(op.getOperation()); run.hasNonNativeGate |= !spec.allowsOp(op.getOperation()); @@ -223,9 +225,9 @@ static void absorbOneQubitIntoRun(FusableTwoQubitRun& run, static FusableTwoQubitRun scanFusableTwoQubitRun(UnitaryOpInterface head, const NativeGateset& spec) { FusableTwoQubitRun run; - if (!assignTwoQubitOpMatrix(head.getOperation(), run.composed)) { - return run; - } + [[maybe_unused]] const bool assigned = + assignTwoQubitOpMatrix(head.getOperation(), run.composed); + assert(assigned && "a run head is a two-qubit member with a 4x4 matrix"); run.tailA = head.getOutputQubit(0); run.tailB = head.getOutputQubit(1); run.ops.push_back(head.getOperation()); @@ -266,11 +268,15 @@ static void eraseFusableRun(PatternRewriter& rewriter, } } -/// Whether any two-qubit or single-qubit unitary (including `ctrl` shells) -/// remains off the native gateset. Used as the pass' convergence check. +/// Whether any single- or two-qubit unitary (including `ctrl` shells) remains +/// off the native gateset. Used as the pass' convergence check. Gates acting on +/// more than two qubits are out of scope here (a dedicated multi-controlled +/// synthesis pass possibly lowers those) and are left untouched rather than +/// reported. static bool hasNonNativeOps(Operation* root, const NativeGateset& spec) { const WalkResult walkResult = root->walk([&](Operation* op) { - if (!isWalkableUnitaryShell(op) || !llvm::isa(op)) { + auto unitary = llvm::dyn_cast(op); + if (!unitary || !isWalkableUnitaryShell(op) || unitary.getNumQubits() > 2) { return WalkResult::advance(); } return spec.allowsOp(op) ? WalkResult::advance() : WalkResult::interrupt(); diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp index 409a7eabd5..a9f541826d 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp @@ -52,9 +52,10 @@ using ProgramFn = void (*)(mlir::qc::QCProgramBuilder&); // --- Native-gateset membership check ------------------------------------- // -/// Returns true when every operation in @p moduleOp is native to the gateset -/// parsed from @p nativeGates. Operations nested inside a controlled shell are -/// validated through the shell itself. +/// Returns true when every single- or two-qubit operation in @p moduleOp is +/// native to the gateset parsed from @p nativeGates. Operations nested inside a +/// controlled shell are validated through the shell itself, and gates acting on +/// more than two qubits are out of scope for this pass and thus ignored. static bool allOpsNative(OwningOpRef& moduleOp, StringRef nativeGates) { const auto spec = decomposition::NativeGateset::parse(nativeGates); @@ -64,7 +65,7 @@ static bool allOpsNative(OwningOpRef& moduleOp, bool ok = true; std::ignore = moduleOp->walk([&](UnitaryOpInterface op) { Operation* raw = op.getOperation(); - if (isa_and_present(raw->getParentOp())) { + if (isa_and_present(raw->getParentOp()) || op.getNumQubits() > 2) { return WalkResult::advance(); } if (!spec->allowsOp(raw)) { @@ -168,6 +169,13 @@ computeUnitaryFromQcoModule(const OwningOpRef& moduleOp) { continue; } if (isa(op.getOperation())) { + // A barrier is an identity on its wires, but it still threads qubit + // values, so carry each input's id over to the matching output. + for (std::size_t i = 0; i < op.getNumQubits(); ++i) { + if (const auto id = getQubitId(op->getOperand(i))) { + qubitIds[op->getResult(i)] = *id; + } + } continue; } if (auto gphase = dyn_cast(op.getOperation())) { @@ -366,6 +374,28 @@ static void fusionCxBarrierCx(mlir::qc::QCProgramBuilder& b) { b.cx(q0, q1); } +/// A single-wire barrier between two entanglers: a non-walkable single-qubit +/// shell terminates the run scan on wire A (and breaks the run-start chain of +/// the second entangler), so neither entangler fuses. +static void fusionCxSingleWireBarrierCx(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + b.cx(q0, q1); + b.barrier({q0}); + b.cx(q0, q1); +} + +/// An entangler followed by a three-qubit gate sharing both run wires: the run +/// scan must stop at the wider gate (it is neither a single- nor a two-qubit +/// run member), leaving the non-native three-qubit gate for the pass to reject. +static void fusionCxThenMultiControlledX(mlir::qc::QCProgramBuilder& b) { + const auto q0 = b.allocQubit(); + const auto q1 = b.allocQubit(); + const auto q2 = b.allocQubit(); + b.cx(q0, q1); + b.mcx({q0, q1}, q2); +} + static void fusionSwapCxPattern(mlir::qc::QCProgramBuilder& b) { const auto q0 = b.allocQubit(); const auto q1 = b.allocQubit(); @@ -525,6 +555,18 @@ class FuseTwoQubitUnitaryRunsPassTest : public testing::Test { return count; } + /// Counts unitaries acting on more than two qubits, i.e. gates left untouched + /// for the dedicated multi-controlled synthesis pass. + static std::size_t countWideGates(const OwningOpRef& moduleOp) { + std::size_t count = 0; + moduleOp.get()->walk([&](UnitaryOpInterface op) { + if (op.getNumQubits() > 2) { + ++count; + } + }); + return count; + } + std::unique_ptr context; }; @@ -557,7 +599,9 @@ INSTANTIATE_TEST_SUITE_P( NamedProgram{"SurroundedCx", hCxSq1}, NamedProgram{"ThreeQubitGhz", threeQGhz}, NamedProgram{"InverseBody", inverseTwoX}, - NamedProgram{"ControlledBody", controlledXH}), + NamedProgram{"ControlledBody", controlledXH}, + NamedProgram{"SingleWireBarrier", + fusionCxSingleWireBarrierCx}), testing::ValuesIn(GATESETS)), [](const testing::TestParamInfo& info) { std::string gateset = std::get<1>(info.param); @@ -596,6 +640,8 @@ INSTANTIATE_TEST_SUITE_P( false}, FusionCase{"BarrierBoundary", fusionCxBarrierCx, 2, std::nullopt, false}, + FusionCase{"SingleWireBarrierBoundary", fusionCxSingleWireBarrierCx, 2, + std::nullopt, true}, FusionCase{"SwappedWireOrder", fusionSwapCxPattern, std::nullopt, std::nullopt, true}, FusionCase{"OffMenuGateInWindow", fusionOffMenuGateInWindow, @@ -637,8 +683,29 @@ TEST_F(FuseTwoQubitUnitaryRunsPassTest, expectSynthesisFailure(mlir::qc::singleControlledX, "cx,cz"); } -TEST_F(FuseTwoQubitUnitaryRunsPassTest, FailsForMultiControlledGateStructure) { - expectSynthesisFailure(mlir::qc::multipleControlledX, "x,sx,rz,cx"); +TEST_F(FuseTwoQubitUnitaryRunsPassTest, LeavesMultiControlledGateUntouched) { + // A multi-controlled gate is out of scope for this pass; it is left untouched + // (for a dedicated multi-controlled synthesis pass) and does not fail the + // run. + auto module = mlir::qc::QCProgramBuilder::build( + context.get(), mlir::qc::multipleControlledX); + ASSERT_TRUE(module); + runFusePipeline(module, "x,sx,rz,cx"); + EXPECT_TRUE(allOpsNative(module, "x,sx,rz,cx")); + EXPECT_EQ(countWideGates(module), 1U); +} + +TEST_F(FuseTwoQubitUnitaryRunsPassTest, + LowersTwoQubitRunButLeavesWiderGateBoundary) { + // The `cx` is off-menu for this cz-family gateset, so it is Weyl-synthesized + // even though the run scan stops at the three-qubit boundary; the wider gate + // is left untouched, so the pass succeeds with the two-qubit run lowered. + auto module = mlir::qc::QCProgramBuilder::build(context.get(), + fusionCxThenMultiControlledX); + ASSERT_TRUE(module); + runFusePipeline(module, "u,cz"); + EXPECT_TRUE(allOpsNative(module, "u,cz")); + EXPECT_EQ(countWideGates(module), 1U); } TEST_F(FuseTwoQubitUnitaryRunsPassTest, From c4e09698286b24285d96a76783bb3987d5dd74aa Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 7 Jul 2026 16:06:23 +0200 Subject: [PATCH 114/122] =?UTF-8?q?=F0=9F=90=87=20Address=20rabbit's=20com?= =?UTF-8?q?ments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/include/mlir/Dialect/QCO/Transforms/Passes.td | 6 +++--- .../Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp | 3 +++ .../NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp | 3 ++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index 259e033e54..ebdd8db428 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -81,11 +81,11 @@ def FuseTwoQubitUnitaryRuns The gateset is a comma-separated list of gate tokens (order not significant) from which the pass resolves a single-qubit Euler basis (generic `qco.u` when `u` is present; IBM-style surface gates when all of - `x`, `sx`, and `rz`/`p` are present; IQM-style `qco.r` when `r` is present; + `x`, `sx`, and `rz` are present; IQM-style `qco.r` when `r` is present; or a supported rotation pair chosen from `rx`, `ry`, `rz`) plus the two-qubit entangler `cx` or `cz`. - Recognised tokens: `u`, `x`, `sx`, `rz` (or `p`), `rx`, `ry`, `r`, `cx`, + Recognised tokens: `u`, `x`, `sx`, `rz`, `rx`, `ry`, `r`, `cx`, `cz`. An empty or whitespace-only gateset is a no-op, which is the intended pipeline default when synthesis is not needed. An unrecognised token causes the pass to fail. @@ -112,7 +112,7 @@ def FuseTwoQubitUnitaryRuns let options = [Option< "nativeGates", "native-gates", "std::string", "\"\"", "Comma-separated native gateset. Empty or whitespace-only is " - "a no-op. Tokens: u, x, sx, rz (or p), rx, ry, r, cx, cz. " + "a no-op. Tokens: u, x, sx, rz, rx, ry, r, cx, cz. " "Examples: x,sx,rz,cx; u,cx; r,cz; rx,rz,cx.">]; } diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp index 209a30fbc9..ee8b9103ce 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp @@ -247,6 +247,9 @@ static FusableTwoQubitRun scanFusableTwoQubitRun(UnitaryOpInterface head, const bool aSingle = nextOnA && nextOnA.isSingleQubit() && !sameOp; const bool bSingle = nextOnB && nextOnB.isSingleQubit() && !sameOp; + if (aSingle && bSingle && nextOnA->getBlock() != nextOnB->getBlock()) { + break; + } if (aSingle && (!bSingle || nextOnA->isBeforeInBlock(nextOnB))) { absorbOneQubitIntoRun(run, nextOnA, spec, /*wireIndex=*/0); continue; diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp index a9f541826d..c0691f9fb9 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp @@ -387,7 +387,8 @@ static void fusionCxSingleWireBarrierCx(mlir::qc::QCProgramBuilder& b) { /// An entangler followed by a three-qubit gate sharing both run wires: the run /// scan must stop at the wider gate (it is neither a single- nor a two-qubit -/// run member), leaving the non-native three-qubit gate for the pass to reject. +/// run member) and leave it untouched, since gates on more than two qubits are +/// out of scope for this pass rather than a failure. static void fusionCxThenMultiControlledX(mlir::qc::QCProgramBuilder& b) { const auto q0 = b.allocQubit(); const auto q1 = b.allocQubit(); From 707cd97aa7bb6b762cd7fe3bd72e8fee6c8d493d Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 7 Jul 2026 16:29:53 +0200 Subject: [PATCH 115/122] =?UTF-8?q?=E2=98=82=EF=B8=8F=20Increase=20coverag?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FuseTwoQubitUnitaryRuns.cpp | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp index ee8b9103ce..be038e2413 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp @@ -76,12 +76,10 @@ static bool isWalkableUnitaryShell(Operation* op) { } /// Builds the constant 4x4 matrix for a two-qubit op (bare or single-target -/// `CtrlOp`). Returns false for barriers, global phase, non-two-qubit ops, and -/// ops whose matrix is not known at compile time. +/// `CtrlOp`). Returns false for a `CtrlOp` that is not +/// single-control/single-target, or an op whose matrix is not known at compile +/// time. static bool assignTwoQubitOpMatrix(Operation* op, Matrix4x4& matrix) { - if (llvm::isa(op)) { - return false; - } if (auto ctrl = llvm::dyn_cast(op)) { if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { return false; @@ -89,10 +87,9 @@ static bool assignTwoQubitOpMatrix(Operation* op, Matrix4x4& matrix) { return llvm::cast(ctrl.getOperation()) .getUnitaryMatrix4x4(matrix); } - auto unitary = llvm::dyn_cast(op); - if (!unitary || !unitary.isTwoQubit()) { - return false; - } + auto unitary = llvm::cast(op); + assert(unitary.isTwoQubit() && + "only two-qubit unitary shells are passed to assignTwoQubitOpMatrix"); return unitary.getUnitaryMatrix4x4(matrix); } @@ -118,12 +115,11 @@ static bool isTwoQubitRunMember(UnitaryOpInterface unitary) { // --- Wire navigation ------------------------------------------------------ // -/// The sole run-member consumer of `wire`, or a null interface when `wire` has -/// no unique user or its user cannot join a run. +/// The sole run-member consumer of `wire`, or a null interface when its unique +/// user cannot join a run. `wire` is single-use by qubit linearity. static UnitaryOpInterface uniqueUnitaryUser(Value wire) { - if (!wire.hasOneUse()) { - return {}; - } + assert(wire.hasOneUse() && + "qubit values are single-use, so a run tail has exactly one user"); auto unitary = llvm::dyn_cast(*wire.user_begin()); if (!unitary) { return {}; From 872132bd5ba02dbb92e472b4250b8d7ad9400938 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 7 Jul 2026 16:37:26 +0200 Subject: [PATCH 116/122] =?UTF-8?q?=F0=9F=8E=A8=20Drop=20llvm=20and=20std?= =?UTF-8?q?=20clarifiers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Decomposition/NativeGateset.cpp | 6 ++-- .../FuseTwoQubitUnitaryRuns.cpp | 26 +++++++------- .../test_fuse_two_qubit_unitary_runs.cpp | 34 +++++++++---------- 3 files changed, 33 insertions(+), 33 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeGateset.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeGateset.cpp index 4baf5cbd95..fe3ae7aece 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeGateset.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/NativeGateset.cpp @@ -146,7 +146,7 @@ NativeGateset::decomposeTarget(const Matrix4x4& target) const { } static std::optional gateKindFor(UnitaryOpInterface op) { - return llvm::TypeSwitch>( + return TypeSwitch>( op.getOperation()) .Case([](UOp) { return NativeGateKind::U; }) .Case([](XOp) { return NativeGateKind::X; }) @@ -163,7 +163,7 @@ static std::optional entanglerKindFor(CtrlOp ctrl) { ctrl.getNumBodyUnitaries() != 1) { return std::nullopt; } - return llvm::TypeSwitch>( + return TypeSwitch>( ctrl.getBodyUnitary(0).getOperation()) .Case([](XOp) { return NativeGateKind::CX; }) .Case([](ZOp) { return NativeGateKind::CZ; }) @@ -171,7 +171,7 @@ static std::optional entanglerKindFor(CtrlOp ctrl) { } bool NativeGateset::allowsOp(Operation* op) const { - return llvm::TypeSwitch(op) + return TypeSwitch(op) .Case([](auto) { return true; }) .Case([&](CtrlOp ctrl) { const auto kind = entanglerKindFor(ctrl); diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp index be038e2413..5c96d7851d 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp @@ -66,12 +66,12 @@ static bool isExcludedFromTopLevelUnitaryWalk(Operation* op) { if (op->getParentOfType()) { return true; } - return !llvm::isa(op) && op->getParentOfType(); + return !isa(op) && op->getParentOfType(); } /// Whether `op` is a unitary shell the pass may rewrite at top level. static bool isWalkableUnitaryShell(Operation* op) { - return !llvm::isa(op) && + return !isa(op) && !isExcludedFromTopLevelUnitaryWalk(op); } @@ -80,14 +80,14 @@ static bool isWalkableUnitaryShell(Operation* op) { /// single-control/single-target, or an op whose matrix is not known at compile /// time. static bool assignTwoQubitOpMatrix(Operation* op, Matrix4x4& matrix) { - if (auto ctrl = llvm::dyn_cast(op)) { + if (auto ctrl = dyn_cast(op)) { if (ctrl.getNumControls() != 1 || ctrl.getNumTargets() != 1) { return false; } - return llvm::cast(ctrl.getOperation()) + return cast(ctrl.getOperation()) .getUnitaryMatrix4x4(matrix); } - auto unitary = llvm::cast(op); + auto unitary = cast(op); assert(unitary.isTwoQubit() && "only two-qubit unitary shells are passed to assignTwoQubitOpMatrix"); return unitary.getUnitaryMatrix4x4(matrix); @@ -120,7 +120,7 @@ static bool isTwoQubitRunMember(UnitaryOpInterface unitary) { static UnitaryOpInterface uniqueUnitaryUser(Value wire) { assert(wire.hasOneUse() && "qubit values are single-use, so a run tail has exactly one user"); - auto unitary = llvm::dyn_cast(*wire.user_begin()); + auto unitary = dyn_cast(*wire.user_begin()); if (!unitary) { return {}; } @@ -138,7 +138,7 @@ static UnitaryOpInterface uniqueUnitaryUser(Value wire) { static Operation* twoQubitGateAtEndOfOneQChain(Value wire) { Value cur = wire; while (Operation* def = cur.getDefiningOp()) { - auto unitary = llvm::dyn_cast(def); + auto unitary = dyn_cast(def); if (!unitary) { return nullptr; } @@ -180,8 +180,8 @@ static void absorbTwoQubitIntoRun(FusableTwoQubitRun& run, assert(assigned && "a two-qubit run member always exposes a 4x4 matrix"); const Value in0 = op.getInputQubit(0); const Value in1 = op.getInputQubit(1); - std::size_t id0 = 0; - std::size_t id1 = 1; + size_t id0 = 0; + size_t id1 = 1; if (in0 == run.tailA && in1 == run.tailB) { run.tailA = op.getOutputQubit(0); run.tailB = op.getOutputQubit(1); @@ -274,7 +274,7 @@ static void eraseFusableRun(PatternRewriter& rewriter, /// reported. static bool hasNonNativeOps(Operation* root, const NativeGateset& spec) { const WalkResult walkResult = root->walk([&](Operation* op) { - auto unitary = llvm::dyn_cast(op); + auto unitary = dyn_cast(op); if (!unitary || !isWalkableUnitaryShell(op) || unitary.getNumQubits() > 2) { return WalkResult::advance(); } @@ -319,7 +319,7 @@ struct FuseTwoQubitUnitaryRunsPattern final return failure(); } - auto firstOp = llvm::cast(run.ops.front()); + auto firstOp = cast(run.ops.front()); rewriter.setInsertionPoint(firstOp); Value newA; Value newB; @@ -360,7 +360,7 @@ struct LowerTwoQubitOpPattern final Value in0; Value in1; - if (auto ctrl = llvm::dyn_cast(raw)) { + if (auto ctrl = dyn_cast(raw)) { in0 = ctrl.getInputControl(0); in1 = ctrl.getInputTarget(0); } else { @@ -420,7 +420,7 @@ struct FuseTwoQubitUnitaryRunsPass final protected: void runOnOperation() override { - if (llvm::StringRef(nativeGates).trim().empty()) { + if (StringRef(nativeGates).trim().empty()) { return; } const auto spec = NativeGateset::parse(nativeGates); diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp index c0691f9fb9..207363c98b 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp @@ -79,8 +79,8 @@ static bool allOpsNative(OwningOpRef& moduleOp, // --- Reference unitary reconstruction ------------------------------------ // -static std::optional unitaryQubit(Value v, std::size_t index, - std::size_t numQubits) { +static std::optional unitaryQubit(Value v, size_t index, + size_t numQubits) { if (index >= numQubits || !isa(v.getType())) { return std::nullopt; } @@ -88,12 +88,12 @@ static std::optional unitaryQubit(Value v, std::size_t index, } static std::optional unitaryQubitOperand(UnitaryOpInterface op, - std::size_t index) { + size_t index) { return unitaryQubit(op->getOperand(index), index, op.getNumQubits()); } static std::optional unitaryQubitResult(UnitaryOpInterface op, - std::size_t index) { + size_t index) { return unitaryQubit(op->getResult(index), index, op.getNumQubits()); } @@ -126,15 +126,15 @@ computeUnitaryFromQcoModule(const OwningOpRef& moduleOp) { return std::nullopt; } - DenseMap qubitIds; - std::size_t nextQubitId = 0; - std::size_t numQubits = 0; + DenseMap qubitIds; + size_t nextQubitId = 0; + size_t numQubits = 0; for (auto func : module.getOps()) { for (auto& block : func.getBlocks()) { for (auto& rawOp : block.getOperations()) { if (auto staticOp = dyn_cast(&rawOp)) { - const auto index = static_cast(staticOp.getIndex()); + const auto index = static_cast(staticOp.getIndex()); qubitIds.try_emplace(staticOp.getQubit(), index); numQubits = std::max(numQubits, index + 1); } else if (auto alloc = dyn_cast(&rawOp)) { @@ -150,10 +150,10 @@ computeUnitaryFromQcoModule(const OwningOpRef& moduleOp) { } DynamicMatrix unitary = - DynamicMatrix::identity(static_cast(1ULL << numQubits)); + DynamicMatrix::identity(static_cast(1ULL << numQubits)); Complex globalPhase{1.0, 0.0}; - auto getQubitId = [&](Value qubit) -> std::optional { + auto getQubitId = [&](Value qubit) -> std::optional { const auto it = qubitIds.find(qubit); if (it == qubitIds.end()) { return std::nullopt; @@ -171,7 +171,7 @@ computeUnitaryFromQcoModule(const OwningOpRef& moduleOp) { if (isa(op.getOperation())) { // A barrier is an identity on its wires, but it still threads qubit // values, so carry each input's id over to the matching output. - for (std::size_t i = 0; i < op.getNumQubits(); ++i) { + for (size_t i = 0; i < op.getNumQubits(); ++i) { if (const auto id = getQubitId(op->getOperand(i))) { qubitIds[op->getResult(i)] = *id; } @@ -466,8 +466,8 @@ constexpr StringRef FUSION_GATESET = "u,cx"; struct FusionCase { const char* name; ProgramFn program; - std::optional exactCtrlCount; - std::optional minCtrlCount; + std::optional exactCtrlCount; + std::optional minCtrlCount; bool checkTwoQUnitary; }; @@ -550,16 +550,16 @@ class FuseTwoQubitUnitaryRunsPassTest : public testing::Test { expectQcoModulesEquivalent(expected, fused); } - static std::size_t countCtrlOps(const OwningOpRef& moduleOp) { - std::size_t count = 0; + static size_t countCtrlOps(const OwningOpRef& moduleOp) { + size_t count = 0; moduleOp.get()->walk([&](CtrlOp) { ++count; }); return count; } /// Counts unitaries acting on more than two qubits, i.e. gates left untouched /// for the dedicated multi-controlled synthesis pass. - static std::size_t countWideGates(const OwningOpRef& moduleOp) { - std::size_t count = 0; + static size_t countWideGates(const OwningOpRef& moduleOp) { + size_t count = 0; moduleOp.get()->walk([&](UnitaryOpInterface op) { if (op.getNumQubits() > 2) { ++count; From 2f322eb6e73b77dd19b372d875c225562c34972d Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 7 Jul 2026 16:37:44 +0200 Subject: [PATCH 117/122] =?UTF-8?q?=F0=9F=93=9D=20Update=20CHANGELOG.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af4e282f1e..cdc0b9ccfe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,8 @@ releases may include breaking changes. ### Added - ✨ Add a `fuse-two-qubit-unitary-runs` pass for fusing compile-time two-qubit - unitary windows via Weyl/KAK resynthesis ([#1865]) ([**@simon1hofmann**]) + unitary windows via Weyl/KAK resynthesis ([#1865]) ([**@simon1hofmann**], + [**@burgholzer**]) - ✨ Add support for IQM's `move` gate in the QDMI Qiskit backend converter ([#1844], [#1848]) ([**@burgholzer**], [**@marcelwa**]) - 🚸 Add `const` version of the `CompoundOperation`'s `getOps()` function From 7d117b558b60cfac7e3459da8cb213a94a905dc1 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 7 Jul 2026 16:57:47 +0200 Subject: [PATCH 118/122] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20linter=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp index 5c96d7851d..f8491d70b8 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseTwoQubitUnitaryRuns.cpp @@ -17,7 +17,6 @@ #include "mlir/Dialect/QCO/Utils/Matrix.h" #include -#include #include #include #include From ce8011841b6eac36d9a379f952715e6e877c0458 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 13 Jul 2026 11:43:56 +0200 Subject: [PATCH 119/122] =?UTF-8?q?=F0=9F=94=80=20Merge=20main=20and=20ada?= =?UTF-8?q?pt=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 2 +- .pre-commit-config.yaml | 12 +- .rumdl.toml | 2 +- CHANGELOG.md | 74 +- README.md | 4 +- UPGRADING.md | 39 +- bindings/fomac/fomac.cpp | 185 +- bindings/ir/register_quantum_computation.cpp | 6 +- bindings/na/register_fomac.cpp | 4 +- cmake/ExternalDependencies.cmake | 8 +- docs/qir/index.md | 12 +- include/mqt-core/fomac/FoMaC.hpp | 1093 +++--- include/mqt-core/ir/QuantumComputation.hpp | 3 +- include/mqt-core/na/fomac/Device.hpp | 10 +- include/mqt-core/qasm3/Parser.hpp | 2 +- include/mqt-core/qasm3/Statement.hpp | 9 +- include/mqt-core/qir/runtime/Runtime.hpp | 58 +- .../Conversion/QCToQIR/QIRCommon/QIRCommon.h | 33 + .../Dialect/QC/Builder/QCProgramBuilder.h | 111 +- mlir/include/mlir/Dialect/QC/IR/QCOps.td | 10 +- .../Dialect/QCO/Builder/QCOProgramBuilder.h | 170 +- mlir/include/mlir/Dialect/QCO/IR/QCODialect.h | 34 - mlir/include/mlir/Dialect/QCO/IR/QCOOps.td | 267 +- mlir/include/mlir/Dialect/QCO/QCOUtils.h | 17 +- .../Dialect/QCO/Transforms/Mapping/Mapping.h | 6 +- .../mlir/Dialect/QCO/Utils/Algorithms.h | 38 - mlir/include/mlir/Dialect/QCO/Utils/Drivers.h | 58 - mlir/include/mlir/Dialect/QCO/Utils/Graph.h | 108 + mlir/include/mlir/Dialect/QCO/Utils/Layout.h | 79 + mlir/include/mlir/Dialect/QCO/Utils/Matrix.h | 93 +- mlir/include/mlir/Dialect/QCO/Utils/Qubits.h | 71 - .../Dialect/QIR/Builder/QIRProgramBuilder.h | 51 +- mlir/include/mlir/Dialect/Utils/Utils.h | 34 + mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp | 17 +- .../QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp | 20 +- .../QCToQIR/QIRBase/QCToQIRBase.cpp | 15 +- .../QCToQIR/QIRCommon/QIRCommon.cpp | 54 + .../Dialect/QC/Builder/QCProgramBuilder.cpp | 85 +- mlir/lib/Dialect/QC/IR/Modifiers/CtrlOp.cpp | 53 +- mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp | 42 +- .../QC/Translation/TranslateQASM3ToQC.cpp | 52 +- .../TranslateQuantumComputationToQC.cpp | 18 +- .../Dialect/QCO/Builder/QCOProgramBuilder.cpp | 155 +- mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp | 57 +- mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp | 49 +- mlir/lib/Dialect/QCO/IR/QCOUtils.cpp | 146 +- .../Dialect/QCO/IR/QubitManagement/SinkOp.cpp | 83 +- mlir/lib/Dialect/QCO/IR/SCF/IfOp.cpp | 44 +- .../QCO/Transforms/Decomposition/Weyl.cpp | 9 +- .../QCO/Transforms/Mapping/Mapping.cpp | 1425 +++++--- mlir/lib/Dialect/QCO/Utils/Algorithms.cpp | 46 - mlir/lib/Dialect/QCO/Utils/Graph.cpp | 148 + mlir/lib/Dialect/QCO/Utils/Layout.cpp | 70 + mlir/lib/Dialect/QCO/Utils/Matrix.cpp | 483 +-- mlir/lib/Dialect/QCO/Utils/Qubits.cpp | 77 - .../Dialect/QIR/Builder/QIRProgramBuilder.cpp | 68 +- .../Dialect/QTensor/Utils/TensorIterator.cpp | 6 +- .../Compiler/test_compiler_pipeline.cpp | 543 +-- .../JeffRoundTrip/test_jeff_round_trip.cpp | 11 +- .../Conversion/QCOToQC/test_qco_to_qc.cpp | 11 +- .../Conversion/QCToQCO/test_qc_to_qco.cpp | 14 +- .../test_qc_to_qir_adaptive.cpp | 342 +- .../QCToQIRBase/test_qc_to_qir_base.cpp | 240 +- mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp | 36 +- .../QC/Translation/test_qasm3_translation.cpp | 57 +- .../test_quantum_computation_translation.cpp | 10 +- mlir/unittests/Dialect/QCO/IR/CMakeLists.txt | 11 +- mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp | 132 +- .../Dialect/QCO/IR/test_qco_ir_matrix.cpp | 234 +- .../test_euler_decomposition.cpp | 155 +- .../Decomposition/test_weyl_decomposition.cpp | 40 +- .../QCO/Transforms/Mapping/test_mapping.cpp | 646 +++- .../test_fuse_two_qubit_unitary_runs.cpp | 95 +- .../test_qco_hadamard_lifting.cpp | 34 +- .../Dialect/QCO/Utils/test_drivers.cpp | 53 - .../Dialect/QCO/Utils/test_unitary_matrix.cpp | 108 +- mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp | 404 +-- .../Dialect/QTensor/IR/test_qtensor_ir.cpp | 24 +- mlir/unittests/TestCaseUtils.h | 69 +- mlir/unittests/programs/qasm_programs.cpp | 119 +- mlir/unittests/programs/qc_programs.cpp | 993 ++++-- mlir/unittests/programs/qc_programs.h | 566 ++-- mlir/unittests/programs/qco_programs.cpp | 2941 +++++++++++------ mlir/unittests/programs/qco_programs.h | 733 ++-- mlir/unittests/programs/qir_programs.cpp | 748 ++++- mlir/unittests/programs/qir_programs.h | 351 +- .../programs/quantum_computation_programs.cpp | 113 +- pyproject.toml | 4 +- python/mqt/core/fomac.pyi | 13 +- python/mqt/core/ir/__init__.pyi | 4 +- src/fomac/FoMaC.cpp | 509 +-- src/ir/QuantumComputation.cpp | 6 +- src/na/fomac/Device.cpp | 23 +- src/qasm3/Parser.cpp | 13 +- src/qdmi/devices/dd/Device.cpp | 6 +- src/qdmi/driver/Driver.cpp | 13 +- src/qir/jit/Session.cpp | 29 + src/qir/runner/CMakeLists.txt | 2 +- src/qir/runner/Runner.cpp | 8 +- src/qir/runtime/QIR.cpp | 21 +- src/qir/runtime/Runtime.cpp | 72 +- test/fomac/test_fomac.cpp | 72 +- test/python/fomac/test_fomac.py | 14 + test/qdmi/driver/test_driver.cpp | 8 + test/qir/jit/test_jit_session.cpp | 39 +- test/qir/runtime/test_qir_runtime.cpp | 153 +- uv.lock | 588 ++-- 107 files changed, 11107 insertions(+), 6248 deletions(-) delete mode 100644 mlir/include/mlir/Dialect/QCO/Utils/Algorithms.h create mode 100644 mlir/include/mlir/Dialect/QCO/Utils/Graph.h create mode 100644 mlir/include/mlir/Dialect/QCO/Utils/Layout.h delete mode 100644 mlir/include/mlir/Dialect/QCO/Utils/Qubits.h delete mode 100644 mlir/lib/Dialect/QCO/Utils/Algorithms.cpp create mode 100644 mlir/lib/Dialect/QCO/Utils/Graph.cpp create mode 100644 mlir/lib/Dialect/QCO/Utils/Layout.cpp delete mode 100644 mlir/lib/Dialect/QCO/Utils/Qubits.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e4d9370843..04817ed5bb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -163,7 +163,7 @@ jobs: build-project: true files-changed-only: true setup-python: true - install-pkgs: "nanobind==2.12.0" + install-pkgs: "nanobind==2.13.0" cpp-linter-extra-args: "-std=c++20" setup-mlir: true llvm-version: 22.1.7 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 30dbb0025a..ab8290674d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: ## Check JSON schemata - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.37.3 + rev: 0.37.4 hooks: - id: check-github-workflows priority: 0 @@ -47,14 +47,14 @@ repos: ## Check pyproject.toml file - repo: https://github.com/henryiii/validate-pyproject-schema-store - rev: 2026.06.26 + rev: 2026.06.30 hooks: - id: validate-pyproject priority: 0 ## Ensure uv.lock is up to date - repo: https://github.com/astral-sh/uv-pre-commit - rev: 0.11.25 + rev: 0.11.26 hooks: - id: uv-lock priority: 0 @@ -71,7 +71,7 @@ repos: ## Check for typos - repo: https://github.com/adhtruong/mirrors-typos - rev: v1.47.2 + rev: v1.48.0 hooks: - id: typos priority: 3 @@ -104,7 +104,7 @@ repos: ## Format configuration files with prettier - repo: https://github.com/rbubley/mirrors-prettier - rev: v3.9.1 + rev: v3.9.4 hooks: - id: prettier types_or: [yaml, html, css, scss, javascript, json, json5] @@ -156,7 +156,7 @@ repos: ## Check Python types with ty - repo: https://github.com/astral-sh/ty-pre-commit - rev: v0.0.55 + rev: v0.0.56 hooks: - id: ty args: [--only-dev] diff --git a/.rumdl.toml b/.rumdl.toml index 05e0273bbc..d16914050f 100644 --- a/.rumdl.toml +++ b/.rumdl.toml @@ -3,7 +3,7 @@ "**/pull_request_template.md" = ["MD013", "MD041"] # The docs files may include HTML and do not need to start with a top-level heading "docs/**" = ["MD033", "MD041"] -# The README may include HTML and dooes not need to start with a top-level heading +# The README may include HTML and does not need to start with a top-level heading "README.md" = ["MD033", "MD041"] [per-file-flavor] diff --git a/CHANGELOG.md b/CHANGELOG.md index cdc0b9ccfe..75357fe3fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ releases may include breaking changes. ### Added +- ✨ Add support for custom job parameters to C++ and Python FoMaC library + ([#1887]) ([**@flowerthrower**], [**@burgholzer**]) +- ✨ Add QIR Output Schemas support to the QIR runtime ([#1877]) + ([**@rturrado**]) - ✨ Add a `fuse-two-qubit-unitary-runs` pass for fusing compile-time two-qubit unitary windows via Weyl/KAK resynthesis ([#1865]) ([**@simon1hofmann**], [**@burgholzer**]) @@ -23,15 +27,13 @@ releases may include breaking changes. environment ([#1786]) ([**@denialhaag**]) - ✨ Add two-qubit Weyl (KAK) decomposition and native-gateset synthesis support ([#1803], [#1832]) ([**@simon1hofmann**], [**@burgholzer**]) -- ✨ Extend the QCO unitary matrix library ([#1774], [#1802], [#1809], [#1814]) - ([**@simon1hofmann**], [**@burgholzer**]) +- ✨ Extend the QCO unitary matrix library ([#1774], [#1802], [#1809], [#1814], + [#1850]) ([**@simon1hofmann**], [**@burgholzer**]) - ✨ Add a `fuse-single-qubit-unitary-runs` pass for fusing compile-time single-qubit unitary runs via Euler resynthesis ([#1672]) ([**@simon1hofmann**], [**@burgholzer**]) - ✨ Add QIR program format support to the DDSIM QDMI Device ([#1766]) ([**@rturrado**]) -- 🚸 Add [CMake presets] to provide a standardized and reproducible way to - configure builds ([#1660]) ([**@denialhaag**]) - ✨ Add a `quantum-loop-unroll` pass for unrolling for-loop operations containing quantum operations ([#1718]) ([**@MatthiasReumann**]) - ✨ Add a `hadamard-lifting` pass for lifting Hadamard gates above Pauli gates @@ -43,24 +45,22 @@ releases may include breaking changes. [#1676], [#1706], [#1776]) ([**@denialhaag**], [**@burgholzer**]) - ✨ Add a `place-and-route` pass for mapping circuits to architectures with restricted topologies ([#1537], [#1547], [#1568], [#1581], [#1583], [#1588], - [#1600], [#1664], [#1709], [#1716], [#1748]) ([**@MatthiasReumann**], - [**@burgholzer**]) + [#1600], [#1664], [#1709], [#1716], [#1748], [#1805], [#1870]) + ([**@MatthiasReumann**], [**@burgholzer**]) - ✨ 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], [#1542], [#1548], [#1550], [#1554], [#1567], [#1569], [#1570], [#1572], [#1573], [#1580], [#1602], [#1620], [#1623], [#1624], [#1626], [#1627], [#1635], [#1638], [#1673], [#1675], [#1700], [#1710], - [#1717], [#1728], [#1730], [#1749], [#1751], [#1762], [#1765], [#1780], - [#1781], [#1782], [#1787], [#1806], [#1807], [#1808], [#1823], [#1824], - [#1830]) ([**@burgholzer**], [**@denialhaag**], [**@taminob**], - [**@DRovara**], [**@li-mingbao**], [**@Ectras**], [**@MatthiasReumann**], - [**@simon1hofmann**]) + [#1717], [#1728], [#1730], [#1749], [#1751], [#1755], [#1762], [#1765], + [#1780], [#1781], [#1782], [#1787], [#1806], [#1807], [#1808], [#1823], + [#1824], [#1830], [#1869], [#1872]) ([**@burgholzer**], [**@denialhaag**], + [**@taminob**], [**@DRovara**], [**@li-mingbao**], [**@Ectras**], + [**@MatthiasReumann**], [**@simon1hofmann**]) ### Changed -- ⬆️ Update [munich-quantum-toolkit/workflows] to version `v2.0.1` ([#1660], - [#1737]) ([**@denialhaag**]) - ⬆️ Require LLVM 22.1 for C++ library builds ([#1549]) ([**@burgholzer**], [**@denialhaag**]) - 📦 Build MLIR by default for C++ library builds ([#1356]) ([**@burgholzer**], @@ -68,8 +68,6 @@ releases may include breaking changes. ### Removed -- 📝 Remove support for generating LaTeX documentation ([#1828]) - ([**@denialhaag**]) - 🔥 Remove the density matrix support from the MQT Core DD package ([#1466]) ([**@burgholzer**]) - 🔥 Remove `datastructures` (`ds`) (sub)library from MQT Core ([#1458]) @@ -78,6 +76,36 @@ releases may include breaking changes. ### Fixed - 🐛 Fix QIR function names for adjoint gates ([#1830]) ([**@denialhaag**]) + +## [3.7.0] - 2026-07-09 + +_If you are upgrading: please see [`UPGRADING.md`](UPGRADING.md#370)._ + +### Added + +- ✨ Add support for IQM's `move` gate in the QDMI Qiskit backend converter + ([#1844], [#1848]) ([**@burgholzer**], [**@marcelwa**]) +- 🚸 Add `const` version of the `CompoundOperation`'s `getOps()` function + ([#1826]) ([**@ystade**]) +- 🚸 Add [CMake presets] to provide a standardized and reproducible way to + configure builds ([#1660]) ([**@denialhaag**]) + +### Changed + +- ⬆️ Update QDMI to version 1.3.2 ([#1873]) ([**@denialhaag**]) +- ♻️ Improve implementation and usability of FoMaC classes ([#1849]) + ([**@MatthiasReumann**]) +- ⬆️ Update `nanobind` to version 2.13.0 ([#1817]) +- ⬆️ Update [munich-quantum-toolkit/workflows] to version `v2.0.1` ([#1660], + [#1737]) ([**@denialhaag**]) + +### Removed + +- 📝 Remove support for generating LaTeX documentation ([#1828]) + ([**@denialhaag**]) + +### Fixed + - 🐛 Fix invalid `prop_type` for `QDMI_DEVICE_PROPERTY_COUPLINGMAP` in QDMI SC Device ([#1842]) ([**@MatthiasReumann**]) @@ -146,7 +174,7 @@ _If you are upgrading: please see [`UPGRADING.md`](UPGRADING.md#350)._ ### Changed - ⬆️ Update `nanobind` to version 2.12.0 ([#1528]) -- ⬆️ Update QDMI to `v1.3.0` ([#1652]) ([**@burgholzer**]) +- ⬆️ Update QDMI to version 1.3.0 ([#1652]) ([**@burgholzer**]) - 📦 Switch to component-based installation for the MQT Core Python package ([#1596]) ([**@burgholzer**]) - ⬆️ Update QDMI to latest version from stable `v1.2.x` branch ([#1593]) @@ -582,7 +610,8 @@ changelogs._ -[unreleased]: https://github.com/munich-quantum-toolkit/core/compare/v3.6.1...HEAD +[unreleased]: https://github.com/munich-quantum-toolkit/core/compare/v3.7.0...HEAD +[3.7.0]: https://github.com/munich-quantum-toolkit/core/releases/tag/v3.7.0 [3.6.1]: https://github.com/munich-quantum-toolkit/core/releases/tag/v3.6.1 [3.6.0]: https://github.com/munich-quantum-toolkit/core/releases/tag/v3.6.0 [3.5.1]: https://github.com/munich-quantum-toolkit/core/releases/tag/v3.5.1 @@ -603,7 +632,15 @@ changelogs._ +[#1887]: https://github.com/munich-quantum-toolkit/core/pull/1887 +[#1877]: https://github.com/munich-quantum-toolkit/core/pull/1877 +[#1873]: https://github.com/munich-quantum-toolkit/core/pull/1873 +[#1872]: https://github.com/munich-quantum-toolkit/core/pull/1872 +[#1870]: https://github.com/munich-quantum-toolkit/core/pull/1870 +[#1869]: https://github.com/munich-quantum-toolkit/core/pull/1869 [#1865]: https://github.com/munich-quantum-toolkit/core/pull/1865 +[#1850]: https://github.com/munich-quantum-toolkit/core/pull/1850 +[#1849]: https://github.com/munich-quantum-toolkit/core/pull/1849 [#1848]: https://github.com/munich-quantum-toolkit/core/pull/1848 [#1844]: https://github.com/munich-quantum-toolkit/core/pull/1844 [#1842]: https://github.com/munich-quantum-toolkit/core/pull/1842 @@ -613,11 +650,13 @@ changelogs._ [#1826]: https://github.com/munich-quantum-toolkit/core/pull/1826 [#1824]: https://github.com/munich-quantum-toolkit/core/pull/1824 [#1823]: https://github.com/munich-quantum-toolkit/core/pull/1823 +[#1817]: https://github.com/munich-quantum-toolkit/core/pull/1817 [#1814]: https://github.com/munich-quantum-toolkit/core/pull/1814 [#1809]: https://github.com/munich-quantum-toolkit/core/pull/1809 [#1808]: https://github.com/munich-quantum-toolkit/core/pull/1808 [#1807]: https://github.com/munich-quantum-toolkit/core/pull/1807 [#1806]: https://github.com/munich-quantum-toolkit/core/pull/1806 +[#1805]: https://github.com/munich-quantum-toolkit/core/pull/1805 [#1803]: https://github.com/munich-quantum-toolkit/core/pull/1803 [#1802]: https://github.com/munich-quantum-toolkit/core/pull/1802 [#1787]: https://github.com/munich-quantum-toolkit/core/pull/1787 @@ -630,6 +669,7 @@ changelogs._ [#1766]: https://github.com/munich-quantum-toolkit/core/pull/1766 [#1765]: https://github.com/munich-quantum-toolkit/core/pull/1765 [#1762]: https://github.com/munich-quantum-toolkit/core/pull/1762 +[#1755]: https://github.com/munich-quantum-toolkit/core/pull/1755 [#1751]: https://github.com/munich-quantum-toolkit/core/pull/1751 [#1749]: https://github.com/munich-quantum-toolkit/core/pull/1749 [#1748]: https://github.com/munich-quantum-toolkit/core/pull/1748 diff --git a/README.md b/README.md index 0e8196dbc7..deb61e0b17 100644 --- a/README.md +++ b/README.md @@ -92,13 +92,13 @@ To support this endeavor, please consider: `mqt.core` is available via [PyPI](https://pypi.org/project/mqt.core/). ```console -(.venv) $ pip install mqt.core +uv pip install mqt.core ``` The following code gives an example on the usage: ```python3 -from mqt.core import QuantumComputation +from mqt.core.ir import QuantumComputation qc = QuantumComputation(2, 2) qc.h(0) diff --git a/UPGRADING.md b/UPGRADING.md index 56673a4530..e33dfb48b3 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -54,6 +54,33 @@ Its functionality has only ever been used in [MQT QMAP] since its inception. As a consequence, the code shall be moved to [MQT QMAP] once QMAP adopts an MQT Core version that includes this change. +### Dev container + +A [dev container](https://containers.dev/) configuration is available to provide +a consistent local development environment. Common IDEs like +[CLion](https://www.jetbrains.com/help/clion/dev-containers-starting-page.html) +and [VS Code](https://code.visualstudio.com/docs/devcontainers/containers) can +open the repository directly inside the container. If you are on Windows, we +recommend using Docker Desktop with the WSL 2 backend. + +## [3.7.0] + +The shared library ABI version (`SOVERSION`) is increased from `3.6` to `3.7`. +Thus, consuming libraries need to update their wheel repair configuration for +`cibuildwheel` to ensure the `mqt-core` libraries are properly skipped in the +wheel repair step. + +### `nanobind` updated to version 2.13.0 + +This release updates the `nanobind` dependency to version 2.13.0, which includes +an ABI bump. Any existing code that uses the `mqt-core` Python bindings will +need to be recompiled with the new `nanobind` version. + +### QDMI updated to version 1.3.2 + +While not a breaking change, this release updates the QDMI dependency to version +1.3.2 + ### CMake presets [CMake presets] have been added to provide a standardized and reproducible way @@ -75,15 +102,6 @@ preparation for a `clang-tidy` run. If you are on Windows, use the `debug-windows` and `release-windows` presets. -### Dev container - -A [dev container](https://containers.dev/) configuration is available to provide -a consistent local development environment. Common IDEs like -[CLion](https://www.jetbrains.com/help/clion/dev-containers-starting-page.html) -and [VS Code](https://code.visualstudio.com/docs/devcontainers/containers) can -open the repository directly inside the container. If you are on Windows, we -recommend using Docker Desktop with the WSL 2 backend. - ## [3.6.0] The shared library ABI version (`SOVERSION`) is increased from `3.5` to `3.6`. @@ -378,7 +396,8 @@ It also requires the `uv` library version 0.5.20 or higher. -[unreleased]: https://github.com/munich-quantum-toolkit/core/compare/v3.6.0...HEAD +[unreleased]: https://github.com/munich-quantum-toolkit/core/compare/v3.7.0...HEAD +[3.7.0]: https://github.com/munich-quantum-toolkit/core/compare/v3.6.0...v3.7.0 [3.6.0]: https://github.com/munich-quantum-toolkit/core/compare/v3.5.1...v3.6.0 [3.5.1]: https://github.com/munich-quantum-toolkit/core/compare/v3.5.0...v3.5.1 [3.5.0]: https://github.com/munich-quantum-toolkit/core/compare/v3.4.0...v3.5.0 diff --git a/bindings/fomac/fomac.cpp b/bindings/fomac/fomac.cpp index 3714d91c6a..4239d0d1bd 100644 --- a/bindings/fomac/fomac.cpp +++ b/bindings/fomac/fomac.cpp @@ -19,6 +19,7 @@ #include // NOLINT(misc-include-cleaner) #include // NOLINT(misc-include-cleaner) #include // NOLINT(misc-include-cleaner) +#include // NOLINT(misc-include-cleaner) #include // NOLINT(misc-include-cleaner) #include @@ -117,13 +118,13 @@ All authentication parameters are optional and can be provided as keyword argume List of available devices.)pb"); // Job class - auto job = nb::class_( + auto job = nb::class_( m, "Job", "A job represents a submitted quantum program execution."); - job.def("check", &fomac::Session::Job::check, + job.def("check", &fomac::Job::check, "Returns the current status of the job."); - job.def("wait", &fomac::Session::Job::wait, "timeout"_a = 0, + job.def("wait", &fomac::Job::wait, "timeout"_a = 0, R"pb(Waits for the job to complete. Args: @@ -132,41 +133,38 @@ All authentication parameters are optional and can be provided as keyword argume Returns: True if the job completed within the timeout, False otherwise.)pb"); - job.def("cancel", &fomac::Session::Job::cancel, "Cancels the job."); + job.def("cancel", &fomac::Job::cancel, "Cancels the job."); - job.def("get_shots", &fomac::Session::Job::getShots, + job.def("get_shots", &fomac::Job::getShots, "Returns the raw shot results from the job."); - job.def("get_counts", &fomac::Session::Job::getCounts, + job.def("get_counts", &fomac::Job::getCounts, "Returns the measurement counts from the job."); - job.def("get_dense_statevector", &fomac::Session::Job::getDenseStateVector, + job.def("get_dense_statevector", &fomac::Job::getDenseStateVector, "Returns the dense statevector from the job (typically only " "available from simulator devices)."); - job.def("get_dense_probabilities", - &fomac::Session::Job::getDenseProbabilities, + job.def("get_dense_probabilities", &fomac::Job::getDenseProbabilities, "Returns the dense probabilities from the job (typically only " "available from simulator devices)."); - job.def("get_sparse_statevector", &fomac::Session::Job::getSparseStateVector, + job.def("get_sparse_statevector", &fomac::Job::getSparseStateVector, "Returns the sparse statevector from the job (typically only " "available from simulator devices)."); - job.def("get_sparse_probabilities", - &fomac::Session::Job::getSparseProbabilities, + job.def("get_sparse_probabilities", &fomac::Job::getSparseProbabilities, "Returns the sparse probabilities from the job (typically only " "available from simulator devices)."); - job.def_prop_ro("id", &fomac::Session::Job::getId, "The job ID."); + job.def_prop_ro("id", &fomac::Job::getId, "The job ID."); - job.def_prop_ro("program_format", &fomac::Session::Job::getProgramFormat, + job.def_prop_ro("program_format", &fomac::Job::getProgramFormat, "The format of the submitted program."); - job.def_prop_ro("program", &fomac::Session::Job::getProgram, - "The submitted program."); + job.def_prop_ro("program", &fomac::Job::getProgram, "The submitted program."); - job.def_prop_ro("num_shots", &fomac::Session::Job::getNumShots, + job.def_prop_ro("num_shots", &fomac::Job::getNumShots, "The number of shots."); job.def(nb::self == nb::self, @@ -203,7 +201,7 @@ All authentication parameters are optional and can be provided as keyword argume .value("CUSTOM5", QDMI_PROGRAM_FORMAT_CUSTOM5); // Device class - auto device = nb::class_( + auto device = nb::class_( m, "Device", "A device represents a quantum device with its properties and " "capabilities."); @@ -217,68 +215,69 @@ All authentication parameters are optional and can be provided as keyword argume .value("MAINTENANCE", QDMI_DEVICE_STATUS_MAINTENANCE) .value("CALIBRATION", QDMI_DEVICE_STATUS_CALIBRATION); - device.def("name", &fomac::Session::Device::getName, + device.def("name", &fomac::Device::getName, "Returns the name of the device."); - device.def("version", &fomac::Session::Device::getVersion, + device.def("version", &fomac::Device::getVersion, "Returns the version of the device."); - device.def("status", &fomac::Session::Device::getStatus, + device.def("status", &fomac::Device::getStatus, "Returns the current status of the device."); - device.def("library_version", &fomac::Session::Device::getLibraryVersion, + device.def("library_version", &fomac::Device::getLibraryVersion, "Returns the version of the library used to define the device."); - device.def("qubits_num", &fomac::Session::Device::getQubitsNum, + device.def("qubits_num", &fomac::Device::getQubitsNum, "Returns the number of qubits available on the device."); - device.def("sites", &fomac::Session::Device::getSites, + device.def("sites", &fomac::Device::getSites, "Returns the list of all sites (zone and regular sites) available " "on the device."); - device.def("regular_sites", &fomac::Session::Device::getRegularSites, + device.def("regular_sites", &fomac::Device::getRegularSites, "Returns the list of regular sites (without zone sites) available " "on the device."); - device.def("zones", &fomac::Session::Device::getZones, + device.def("zones", &fomac::Device::getZones, "Returns the list of zone sites (without regular sites) available " "on the device."); - device.def("operations", &fomac::Session::Device::getOperations, + device.def("operations", &fomac::Device::getOperations, "Returns the list of operations supported by the device."); - device.def("coupling_map", &fomac::Session::Device::getCouplingMap, + device.def("coupling_map", &fomac::Device::getCouplingMap, "Returns the coupling map of the device as a list of site pairs."); - device.def("needs_calibration", &fomac::Session::Device::getNeedsCalibration, + device.def("needs_calibration", &fomac::Device::getNeedsCalibration, "Returns whether the device needs calibration."); - device.def("length_unit", &fomac::Session::Device::getLengthUnit, + device.def("length_unit", &fomac::Device::getLengthUnit, "Returns the unit of length used by the device."); - device.def("length_scale_factor", - &fomac::Session::Device::getLengthScaleFactor, + device.def("length_scale_factor", &fomac::Device::getLengthScaleFactor, "Returns the scale factor for length used by the device."); - device.def("duration_unit", &fomac::Session::Device::getDurationUnit, + device.def("duration_unit", &fomac::Device::getDurationUnit, "Returns the unit of duration used by the device."); - device.def("duration_scale_factor", - &fomac::Session::Device::getDurationScaleFactor, + device.def("duration_scale_factor", &fomac::Device::getDurationScaleFactor, "Returns the scale factor for duration used by the device."); - device.def("min_atom_distance", &fomac::Session::Device::getMinAtomDistance, + device.def("min_atom_distance", &fomac::Device::getMinAtomDistance, "Returns the minimum atom distance on the device."); device.def("supported_program_formats", - &fomac::Session::Device::getSupportedProgramFormats, + &fomac::Device::getSupportedProgramFormats, "Returns the list of program formats supported by the device."); - device.def("submit_job", &fomac::Session::Device::submitJob, "program"_a, - "program_format"_a, "num_shots"_a, - nb::rv_policy::reference_internal, "Submits a job to the device."); + device.def("submit_job", &fomac::Device::submitJob, "program"_a, + "program_format"_a, "num_shots"_a, nb::kw_only(), + "custom1"_a = nb::none(), "custom2"_a = nb::none(), + "custom3"_a = nb::none(), "custom4"_a = nb::none(), + "custom5"_a = nb::none(), nb::rv_policy::reference_internal, + "Submits a job to the device."); - device.def("__repr__", [](const fomac::Session::Device& dev) { + device.def("__repr__", [](const fomac::Device& dev) { return ""; }); @@ -288,50 +287,48 @@ All authentication parameters are optional and can be provided as keyword argume nb::sig("def __ne__(self, arg: object, /) -> bool")); // Site class - auto site = nb::class_( + auto site = nb::class_( device, "Site", "A site represents a potential qubit location on a quantum device."); - site.def("index", &fomac::Session::Device::Site::getIndex, - "Returns the index of the site."); + site.def("index", &fomac::Site::getIndex, "Returns the index of the site."); - site.def("t1", &fomac::Session::Device::Site::getT1, + site.def("t1", &fomac::Site::getT1, "Returns the T1 coherence time of the site."); - site.def("t2", &fomac::Session::Device::Site::getT2, + site.def("t2", &fomac::Site::getT2, "Returns the T2 coherence time of the site."); - site.def("name", &fomac::Session::Device::Site::getName, - "Returns the name of the site."); + site.def("name", &fomac::Site::getName, "Returns the name of the site."); - site.def("x_coordinate", &fomac::Session::Device::Site::getXCoordinate, + site.def("x_coordinate", &fomac::Site::getXCoordinate, "Returns the x coordinate of the site."); - site.def("y_coordinate", &fomac::Session::Device::Site::getYCoordinate, + site.def("y_coordinate", &fomac::Site::getYCoordinate, "Returns the y coordinate of the site."); - site.def("z_coordinate", &fomac::Session::Device::Site::getZCoordinate, + site.def("z_coordinate", &fomac::Site::getZCoordinate, "Returns the z coordinate of the site."); - site.def("is_zone", &fomac::Session::Device::Site::isZone, + site.def("is_zone", &fomac::Site::isZone, "Returns whether the site is a zone."); - site.def("x_extent", &fomac::Session::Device::Site::getXExtent, + site.def("x_extent", &fomac::Site::getXExtent, "Returns the x extent of the site."); - site.def("y_extent", &fomac::Session::Device::Site::getYExtent, + site.def("y_extent", &fomac::Site::getYExtent, "Returns the y extent of the site."); - site.def("z_extent", &fomac::Session::Device::Site::getZExtent, + site.def("z_extent", &fomac::Site::getZExtent, "Returns the z extent of the site."); - site.def("module_index", &fomac::Session::Device::Site::getModuleIndex, + site.def("module_index", &fomac::Site::getModuleIndex, "Returns the index of the module the site belongs to."); - site.def("submodule_index", &fomac::Session::Device::Site::getSubmoduleIndex, + site.def("submodule_index", &fomac::Site::getSubmoduleIndex, "Returns the index of the submodule the site belongs to."); - site.def("__repr__", [](const fomac::Session::Device::Site& s) { + site.def("__repr__", [](const fomac::Site& s) { return ""; }); @@ -341,78 +338,68 @@ All authentication parameters are optional and can be provided as keyword argume nb::sig("def __ne__(self, arg: object, /) -> bool")); // Operation class - auto operation = nb::class_( + auto operation = nb::class_( device, "Operation", "An operation represents a quantum operation that can be performed on a " "quantum device."); - operation.def("name", &fomac::Session::Device::Operation::getName, - "sites"_a.sig("...") = - std::vector{}, + operation.def("name", &fomac::Operation::getName, + "sites"_a.sig("...") = std::vector{}, "params"_a.sig("...") = std::vector{}, "Returns the name of the operation."); - operation.def("qubits_num", &fomac::Session::Device::Operation::getQubitsNum, - "sites"_a.sig("...") = - std::vector{}, + operation.def("qubits_num", &fomac::Operation::getQubitsNum, + "sites"_a.sig("...") = std::vector{}, "params"_a.sig("...") = std::vector{}, "Returns the number of qubits the operation acts on."); - operation.def( - "parameters_num", &fomac::Session::Device::Operation::getParametersNum, - "sites"_a.sig("...") = std::vector{}, - "params"_a.sig("...") = std::vector{}, - "Returns the number of parameters the operation has."); + operation.def("parameters_num", &fomac::Operation::getParametersNum, + "sites"_a.sig("...") = std::vector{}, + "params"_a.sig("...") = std::vector{}, + "Returns the number of parameters the operation has."); - operation.def("duration", &fomac::Session::Device::Operation::getDuration, - "sites"_a.sig("...") = - std::vector{}, + operation.def("duration", &fomac::Operation::getDuration, + "sites"_a.sig("...") = std::vector{}, "params"_a.sig("...") = std::vector{}, "Returns the duration of the operation."); - operation.def("fidelity", &fomac::Session::Device::Operation::getFidelity, - "sites"_a.sig("...") = - std::vector{}, + operation.def("fidelity", &fomac::Operation::getFidelity, + "sites"_a.sig("...") = std::vector{}, "params"_a.sig("...") = std::vector{}, "Returns the fidelity of the operation."); - operation.def("interaction_radius", - &fomac::Session::Device::Operation::getInteractionRadius, - "sites"_a.sig("...") = - std::vector{}, + operation.def("interaction_radius", &fomac::Operation::getInteractionRadius, + "sites"_a.sig("...") = std::vector{}, "params"_a.sig("...") = std::vector{}, "Returns the interaction radius of the operation."); - operation.def( - "blocking_radius", &fomac::Session::Device::Operation::getBlockingRadius, - "sites"_a.sig("...") = std::vector{}, - "params"_a.sig("...") = std::vector{}, - "Returns the blocking radius of the operation."); + operation.def("blocking_radius", &fomac::Operation::getBlockingRadius, + "sites"_a.sig("...") = std::vector{}, + "params"_a.sig("...") = std::vector{}, + "Returns the blocking radius of the operation."); - operation.def( - "idling_fidelity", &fomac::Session::Device::Operation::getIdlingFidelity, - "sites"_a.sig("...") = std::vector{}, - "params"_a.sig("...") = std::vector{}, - "Returns the idling fidelity of the operation."); + operation.def("idling_fidelity", &fomac::Operation::getIdlingFidelity, + "sites"_a.sig("...") = std::vector{}, + "params"_a.sig("...") = std::vector{}, + "Returns the idling fidelity of the operation."); - operation.def("is_zoned", &fomac::Session::Device::Operation::isZoned, + operation.def("is_zoned", &fomac::Operation::isZoned, "Returns whether the operation is zoned."); - operation.def("sites", &fomac::Session::Device::Operation::getSites, + operation.def("sites", &fomac::Operation::getSites, "Returns the list of sites the operation can be performed on."); - operation.def("site_pairs", &fomac::Session::Device::Operation::getSitePairs, + operation.def("site_pairs", &fomac::Operation::getSitePairs, "Returns the list of site pairs the local 2-qubit operation " "can be performed on."); operation.def("mean_shuttling_speed", - &fomac::Session::Device::Operation::getMeanShuttlingSpeed, - "sites"_a.sig("...") = - std::vector{}, + &fomac::Operation::getMeanShuttlingSpeed, + "sites"_a.sig("...") = std::vector{}, "params"_a.sig("...") = std::vector{}, "Returns the mean shuttling speed of the operation."); - operation.def("__repr__", [](const fomac::Session::Device::Operation& op) { + operation.def("__repr__", [](const fomac::Operation& op) { return ""; }); @@ -436,7 +423,7 @@ All authentication parameters are optional and can be provided as keyword argume const std::optional& custom3 = std::nullopt, const std::optional& custom4 = std::nullopt, const std::optional& custom5 = - std::nullopt) -> fomac::Session::Device { + std::nullopt) -> fomac::Device { const qdmi::DeviceSessionConfig config{.baseUrl = baseUrl, .token = token, .authFile = authFile, @@ -450,7 +437,7 @@ All authentication parameters are optional and can be provided as keyword argume .custom5 = custom5}; auto* const qdmiDevice = qdmi::Driver::get().addDynamicDeviceLibrary( libraryPath, prefix, config); - return fomac::Session::Device::fromQDMIDevice(qdmiDevice); + return fomac::Session::createSessionlessDevice(qdmiDevice); }, "library_path"_a, "prefix"_a, nb::kw_only(), "base_url"_a = std::nullopt, "token"_a = std::nullopt, "auth_file"_a = std::nullopt, diff --git a/bindings/ir/register_quantum_computation.cpp b/bindings/ir/register_quantum_computation.cpp index 618d52e7e4..3dc239b511 100644 --- a/bindings/ir/register_quantum_computation.cpp +++ b/bindings/ir/register_quantum_computation.cpp @@ -2003,15 +2003,17 @@ This method is equivalent to calling :meth:`measure` multiple times. qubits: The qubits to measure cbits: The classical bits to store the results)pb"); qc.def("measure_all", &qc::QuantumComputation::measureAll, nb::kw_only(), - "add_bits"_a = true, + "add_bits"_a = true, "add_barrier"_a = true, R"pb(Measure all qubits and store the results in classical bits. Details: If `add_bits` is `True`, a new classical register (named "`meas`") with the same size as the number of qubits will be added to the circuit and the results will be stored in it. If `add_bits` is `False`, the classical register must already exist and have a sufficient number of bits to store the results. + If `add_barrier` is `True`, a barrier is added before the measurements. Args: - add_bits: Whether to explicitly add a classical register)pb"); + add_bits: Whether to explicitly add a classical register + add_barrier: Whether to add a barrier before the measurements)pb"); qc.def("reset", nb::overload_cast(&qc::QuantumComputation::reset), "q"_a, R"pb(Add a reset operation to the circuit. diff --git a/bindings/na/register_fomac.cpp b/bindings/na/register_fomac.cpp index d70bc696ba..a35059e772 100644 --- a/bindings/na/register_fomac.cpp +++ b/bindings/na/register_fomac.cpp @@ -41,7 +41,7 @@ void registerFomac(nb::module_& m) { nb::module_::import_("mqt.core.fomac"); - auto device = nb::class_( + auto device = nb::class_( m, "Device", "Represents a device with a lattice of traps."); auto lattice = nb::class_( @@ -122,7 +122,7 @@ void registerFomac(nb::module_& m) { return dev.getDecoherenceTimes().t2; }, "The T2 time of the device."); - device.def("__repr__", [](const fomac::Session::Device& dev) { + device.def("__repr__", [](const fomac::Device& dev) { return ""; }); device.def_static("try_create_from_device", diff --git a/cmake/ExternalDependencies.cmake b/cmake/ExternalDependencies.cmake index 37e646cd0c..40596ed145 100644 --- a/cmake/ExternalDependencies.cmake +++ b/cmake/ExternalDependencies.cmake @@ -86,9 +86,11 @@ if(BUILD_MQT_CORE_TESTS) endif() # cmake-format: off -set(QDMI_VERSION 1.3.0 +set(QDMI_MINIMUM_VERSION 1.3.0 + CACHE STRING "Minimum QDMI version") +set(QDMI_VERSION 1.3.2 CACHE STRING "QDMI version") -set(QDMI_REV "0f7e08c58b72800d1022a01cfb618af67b9a9c30" # v1.3.0 +set(QDMI_REV "d05a0b418f42e54e9585d2e00af8ce23e745fd83" # v1.3.2 CACHE STRING "QDMI identifier (tag, branch or commit hash)") set(QDMI_REPO_OWNER "Munich-Quantum-Software-Stack" CACHE STRING "QDMI repository owner (change when using a fork)") @@ -98,7 +100,7 @@ FetchContent_Declare( qdmi GIT_REPOSITORY https://github.com/${QDMI_REPO_OWNER}/qdmi.git GIT_TAG ${QDMI_REV} - FIND_PACKAGE_ARGS ${QDMI_VERSION}) + FIND_PACKAGE_ARGS ${QDMI_MINIMUM_VERSION}) list(APPEND FETCH_PACKAGES qdmi) set(SPDLOG_VERSION diff --git a/docs/qir/index.md b/docs/qir/index.md index 4f04280af1..8ddac20831 100644 --- a/docs/qir/index.md +++ b/docs/qir/index.md @@ -42,8 +42,16 @@ The `mqt-core-qir-runner` can be used to execute a QIR file (typically with a ./build/bin/mqt-core-qir-runner bell.ll ``` -This will simulate the circuit and print the measurement results to the console. -The runner supports the QIR Base Profile. +The runner prints the program's outputs to the console in one of the two +[QIR Output Schemas][output-schemas] (Labeled or Ordered): the two `HEADER` +records announce the schema, and each shot is wrapped in `START` and `END` +records with a `METADATA\toutput_labeling_schema\t` line inside. + +The active schema is selected by the `output_labeling_schema` function attribute +on the entry-point function of the QIR program. The value `ordered` selects +Ordered; anything else, or a missing attribute, selects Labeled. + +[output-schemas]: https://github.com/qir-alliance/qir-spec/tree/main/specification/output_schemas ### QIR Support in the DDSIM QDMI Device diff --git a/include/mqt-core/fomac/FoMaC.hpp b/include/mqt-core/fomac/FoMaC.hpp index 48dbfa815e..127004e822 100644 --- a/include/mqt-core/fomac/FoMaC.hpp +++ b/include/mqt-core/fomac/FoMaC.hpp @@ -11,6 +11,7 @@ #pragma once #include "qdmi/common/Common.hpp" +#include "qdmi/types.h" #include @@ -19,18 +20,20 @@ #include #include #include -#include #include #include -#include +#include #include #include #include #include #include +#include #include namespace fomac { +using CustomJobParameter = std::variant; + /** * @brief Concept for ranges that are contiguous in memory and can be * constructed with a size. @@ -40,8 +43,7 @@ namespace fomac { */ template concept size_constructible_contiguous_range = - std::ranges::contiguous_range && - std::constructible_from && + std::ranges::contiguous_range && std::constructible_from && requires { typename T::value_type; } && requires(T t) { { t.data() } -> std::same_as; }; @@ -54,8 +56,8 @@ concept size_constructible_contiguous_range = */ template concept value_or_string = - std::integral || std::floating_point || std::is_same_v || - std::is_same_v || std::is_same_v; + std::integral || std::floating_point || std::same_as || + std::same_as || std::same_as; /** * @brief Concept for types that are either value_or_string or @@ -76,7 +78,7 @@ concept value_or_string_or_vector = */ template concept is_optional = requires { typename T::value_type; } && - std::is_same_v>; + std::same_as>; /** * @brief Concept for types that are either std::string or std::optional of @@ -87,8 +89,8 @@ concept is_optional = requires { typename T::value_type; } && */ template concept string_or_optional_string = - std::is_same_v || - (is_optional && std::is_same_v); + std::same_as || + (is_optional && std::same_as); /// @see remove_optional_t template struct remove_optional { @@ -106,8 +108,7 @@ template struct remove_optional> { * with the underlying type of optional without caring about its optionality. * @tparam T The type to strip optional from. */ -template -using remove_optional_t = typename remove_optional::type; +template using remove_optional_t = remove_optional::type; /** * @brief Concept for types that are either size_constructible_contiguous_range @@ -175,6 +176,11 @@ struct SessionConfig { std::optional custom5; }; +class Job; +class Site; +class Device; +class Operation; + /** * @brief Class representing the Session library. * @details This class provides methods to query available devices and @@ -182,553 +188,592 @@ struct SessionConfig { * @see QDMI_Session */ class Session { +public: /** - * @brief Private token class. - * @details Only the Session class can create instances of this class. + * @brief Creates a Device object from a QDMI_Device handle. + * @param device The QDMI_Device handle to wrap. + * @return A Device object wrapping the given handle. + * @note This is a factory method for use in bindings where a + * session is not accessible. + */ + [[nodiscard]] static Device createSessionlessDevice(QDMI_Device device); + + /** + * @brief Constructs a new QDMI Session with optional authentication. + * @param config Optional session configuration containing authentication + * parameters. If not provided, uses default (no authentication). + * @details Creates, allocates, and initializes a new QDMI session. */ - class Token { - public: - Token() = default; - }; + explicit Session(const SessionConfig& config = {}); + + /// @see QDMI_SESSION_PROPERTY_DEVICES + [[nodiscard]] std::vector getDevices(); + +private: + /// Query a session property. + template + [[nodiscard]] T queryProperty(const QDMI_Session_Property prop) const { + using StrippedValueType = remove_optional_t::value_type; + + size_t size = 0; + qdmi::throwIfError(QDMI_session_query_session_property(session_.get(), prop, + 0, nullptr, &size), + std::string("Querying size ") + qdmi::toString(prop)); + remove_optional_t value(size / sizeof(StrippedValueType)); + qdmi::throwIfError(QDMI_session_query_session_property( + session_.get(), prop, size, value.data(), nullptr), + std::string("Querying ") + qdmi::toString(prop)); + return value; + } + + std::unique_ptr session_{ + nullptr, QDMI_session_free}; +}; + +static_assert(!std::is_copy_constructible()); +static_assert(!std::is_copy_assignable()); +static_assert(std::is_move_constructible()); +static_assert(std::is_move_assignable()); +/** + * @brief Class representing a quantum device. + * @details + * This class provides methods to query properties of the device, + * its sites, and its operations. + * + * The class can only be constructed by Session instances. + * + * @see QDMI_Device + */ +class Device { public: + // NOLINTNEXTLINE(google-explicit-constructor, *-explicit-conversions) + operator QDMI_Device() const { return device_; } + + /// @see QDMI_DEVICE_PROPERTY_NAME + [[nodiscard]] std::string getName() const; + + /// @see QDMI_DEVICE_PROPERTY_VERSION + [[nodiscard]] std::string getVersion() const; + + /// @see QDMI_DEVICE_PROPERTY_STATUS + [[nodiscard]] QDMI_Device_Status getStatus() const; + + /// @see QDMI_DEVICE_PROPERTY_LIBRARYVERSION + [[nodiscard]] std::string getLibraryVersion() const; + + /// @see QDMI_DEVICE_PROPERTY_QUBITSNUM + [[nodiscard]] size_t getQubitsNum() const; + + /// @see QDMI_DEVICE_PROPERTY_SITES + [[nodiscard]] std::vector getSites() const; + /** - * @brief Class representing a submitted job. - * @details This class provides methods to query job status and retrieve - * results. - * @see QDMI_Job + * @brief Returns the list of regular sites (without zone sites) available + * on the device. + * @details Filters all sites and only returns regular sites, i.e., where + * `isZone()` yields `false`. These represent actual potential physical + * qubit locations on the device lattice. + * @returns vector of regular sites + * @see QDMI_DEVICE_PROPERTY_SITES */ - class Job { - QDMI_Job job_; - - public: - /** - * @brief Constructs a Job object from a QDMI_Job handle. - * @param job The QDMI_Job handle to wrap. - */ - explicit Job(QDMI_Job job) : job_(job) {} - /** - * @brief Destructor that releases the underlying QDMI_Job resource. - */ - ~Job() { - if (job_ != nullptr) { - QDMI_job_free(job_); - } - } - // Delete copy constructor and copy assignment operator to prevent - // pointer duplication and double-free - Job(const Job&) = delete; - Job& operator=(const Job&) = delete; - // Default move constructor and move assignment operator to allow - // safe ownership transfer - Job(Job&& other) noexcept : job_(other.job_) { other.job_ = nullptr; } - Job& operator=(Job&& other) noexcept { - if (this != &other) { - if (job_ != nullptr) { - QDMI_job_free(job_); - } - job_ = other.job_; - other.job_ = nullptr; - } - return *this; - } - /// @returns the underlying QDMI_Job object. - [[nodiscard]] auto getQDMIJob() const -> QDMI_Job { return job_; } - // NOLINTNEXTLINE(google-explicit-constructor, *-explicit-conversions) - operator QDMI_Job() const { return job_; } - /// @see QDMI_job_check - [[nodiscard]] auto check() const -> QDMI_Job_Status; - /** - * @brief @see QDMI_job_wait - * @param timeout The maximum time to wait in seconds. 0 (default) means - * wait indefinitely. - * @return true if the job completed successfully, false if it timed out - */ - [[nodiscard]] auto wait(size_t timeout = 0) const -> bool; - /// @see QDMI_job_cancel - auto cancel() const -> void; - /// Get the job ID - [[nodiscard]] auto getId() const -> std::string; - /// Get the program format - [[nodiscard]] auto getProgramFormat() const -> QDMI_Program_Format; - /// Get the program to be executed - [[nodiscard]] auto getProgram() const -> std::string; - /// Get the number of shots - [[nodiscard]] auto getNumShots() const -> size_t; - /** - * @brief Returns the measurement shots as a vector of bitstrings. - * @see QDMI_JOB_RESULT_SHOTS - */ - [[nodiscard]] auto getShots() const -> std::vector; - /** - * @brief Returns a map of measurement outcomes to their respective counts. - * @see QDMI_JOB_RESULT_HIST_KEYS - * @see QDMI_JOB_RESULT_HIST_VALUES - */ - [[nodiscard]] auto getCounts() const -> std::map; - /** - * @brief Returns the dense state vector as a vector of complex numbers. - * @see QDMI_JOB_RESULT_STATEVECTOR_DENSE - */ - [[nodiscard]] auto getDenseStateVector() const - -> std::vector>; - /** - * @brief Returns the dense probabilities as a vector of doubles. - * @see QDMI_JOB_RESULT_PROBABILITIES_DENSE - */ - [[nodiscard]] auto getDenseProbabilities() const -> std::vector; - /** - * @brief Returns the sparse state vector as a map of bitstrings to complex - * amplitudes. - * @see QDMI_JOB_RESULT_STATEVECTOR_SPARSE_KEYS - * @see QDMI_JOB_RESULT_STATEVECTOR_SPARSE_VALUES - */ - [[nodiscard]] auto getSparseStateVector() const - -> std::map>; - /** - * @brief Returns the sparse probabilities as a map of bitstrings to - * probabilities. - * @see QDMI_JOB_RESULT_PROBABILITIES_SPARSE_KEYS - * @see QDMI_JOB_RESULT_PROBABILITIES_SPARSE_VALUES - */ - [[nodiscard]] auto getSparseProbabilities() const - -> std::map; - }; + [[nodiscard]] std::vector getRegularSites() const; /** - * @brief Class representing a quantum device. - * @details This class provides methods to query properties of the device, - * its sites, and its operations. - * @see QDMI_Device + * @brief Returns the list of zone sites (without regular sites) available + * on the device. + * @details Filters all sites and only returns zone sites, i.e., where + * `isZone()` yields `true`. These represent a zone, i.e., an extent where + * zoned operations can be performed, not individual qubit locations. + * @returns a vector of zone sites + * @see QDMI_DEVICE_PROPERTY_SITES */ - class Device { - /** - * @brief Private token class. - * @details Only the Device class can create instances of this class. - */ - class Token { - public: - Token() = default; - }; + [[nodiscard]] std::vector getZones() const; + + /// @see QDMI_DEVICE_PROPERTY_OPERATIONS + [[nodiscard]] std::vector getOperations() const; + + /// @see QDMI_DEVICE_PROPERTY_COUPLINGMAP + [[nodiscard]] std::optional>> + getCouplingMap() const; + + /// @see QDMI_DEVICE_PROPERTY_NEEDSCALIBRATION + [[nodiscard]] std::optional getNeedsCalibration() const; - public: - /** - * @brief Class representing a site (qubit) on the device. - * @details This class provides methods to query properties of the site. - * @see QDMI_Site - */ - class Site { - /// @brief The associated QDMI_Device object. - QDMI_Device device_; - /// @brief The underlying QDMI_Site object. - QDMI_Site site_; - - template - [[nodiscard]] auto queryProperty(const QDMI_Site_Property prop) const - -> T { - std::string msg = "Querying "; - msg += qdmi::toString(prop); - if constexpr (string_or_optional_string) { - size_t size = 0; - auto result = QDMI_device_query_site_property(device_, site_, prop, 0, - nullptr, &size); - if constexpr (is_optional) { - if (result == QDMI_ERROR_NOTSUPPORTED) { - return std::nullopt; - } - } - qdmi::throwIfError(result, msg); - std::string value(size - 1, '\0'); - result = QDMI_device_query_site_property(device_, site_, prop, size, - value.data(), nullptr); - qdmi::throwIfError(result, msg); - return value; - } else { - remove_optional_t value{}; - const auto result = QDMI_device_query_site_property( - device_, site_, prop, sizeof(remove_optional_t), &value, - nullptr); - if constexpr (is_optional) { - if (result == QDMI_ERROR_NOTSUPPORTED) { - return std::nullopt; - } - } - qdmi::throwIfError(result, msg); - return value; + /// @see QDMI_DEVICE_PROPERTY_LENGTHUNIT + [[nodiscard]] std::optional getLengthUnit() const; + + /// @see QDMI_DEVICE_PROPERTY_LENGTHSCALEFACTOR + [[nodiscard]] std::optional getLengthScaleFactor() const; + + /// @see QDMI_DEVICE_PROPERTY_DURATIONUNIT + [[nodiscard]] std::optional getDurationUnit() const; + + /// @see QDMI_DEVICE_PROPERTY_DURATIONSCALEFACTOR + [[nodiscard]] std::optional getDurationScaleFactor() const; + + /// @see QDMI_DEVICE_PROPERTY_MINATOMDISTANCE + [[nodiscard]] std::optional getMinAtomDistance() const; + + /// @see QDMI_DEVICE_PROPERTY_SUPPORTEDPROGRAMFORMATS + [[nodiscard]] std::vector + getSupportedProgramFormats() const; + + /// @see QDMI_job_submit + [[nodiscard]] Job submitJob( + const std::string& program, QDMI_Program_Format format, size_t numShots, + const std::optional& custom1 = std::nullopt, + const std::optional& custom2 = std::nullopt, + const std::optional& custom3 = std::nullopt, + const std::optional& custom4 = std::nullopt, + const std::optional& custom5 = std::nullopt) const; + + auto operator<=>(const Device&) const noexcept = default; + +private: + /** + * @brief Constructs a Device object from a QDMI_Device handle. + * @param device The QDMI_Device handle to wrap. + */ + explicit Device(QDMI_Device device) : device_(device) {} + + /// Query a device property. + template + [[nodiscard]] T queryProperty(const QDMI_Device_Property prop) const { + std::string msg = "Querying "; + msg += qdmi::toString(prop); + + if constexpr (string_or_optional_string) { + size_t size = 0; + auto result = + QDMI_device_query_device_property(device_, prop, 0, nullptr, &size); + + if constexpr (is_optional) { + if (result == QDMI_ERROR_NOTSUPPORTED) { + return std::nullopt; } } - public: - /** - * @brief Constructs a Site object from a QDMI_Site handle. - * @param device The associated QDMI_Device handle. - * @param site The QDMI_Site handle to wrap. - */ - Site(Token /* unused */, QDMI_Device device, QDMI_Site site) - : device_(device), site_(site) {} - /// @returns the underlying QDMI_Site object. - [[nodiscard]] auto getQDMISite() const -> QDMI_Site { return site_; } - // NOLINTNEXTLINE(google-explicit-constructor, *-explicit-conversions) - operator QDMI_Site() const { return site_; } - auto operator<=>(const Site&) const = default; - /// @see QDMI_SITE_PROPERTY_INDEX - [[nodiscard]] auto getIndex() const -> size_t; - /// @see QDMI_SITE_PROPERTY_T1 - [[nodiscard]] auto getT1() const -> std::optional; - /// @see QDMI_SITE_PROPERTY_T2 - [[nodiscard]] auto getT2() const -> std::optional; - /// @see QDMI_SITE_PROPERTY_NAME - [[nodiscard]] auto getName() const -> std::optional; - /// @see QDMI_SITE_PROPERTY_XCOORDINATE - [[nodiscard]] auto getXCoordinate() const -> std::optional; - /// @see QDMI_SITE_PROPERTY_YCOORDINATE - [[nodiscard]] auto getYCoordinate() const -> std::optional; - /// @see QDMI_SITE_PROPERTY_ZCOORDINATE - [[nodiscard]] auto getZCoordinate() const -> std::optional; - /// @see QDMI_SITE_PROPERTY_ISZONE - [[nodiscard]] auto isZone() const -> bool; - /// @see QDMI_SITE_PROPERTY_XEXTENT - [[nodiscard]] auto getXExtent() const -> std::optional; - /// @see QDMI_SITE_PROPERTY_YEXTENT - [[nodiscard]] auto getYExtent() const -> std::optional; - /// @see QDMI_SITE_PROPERTY_ZEXTENT - [[nodiscard]] auto getZExtent() const -> std::optional; - /// @see QDMI_SITE_PROPERTY_MODULEINDEX - [[nodiscard]] auto getModuleIndex() const -> std::optional; - /// @see QDMI_SITE_PROPERTY_SUBMODULEINDEX - [[nodiscard]] auto getSubmoduleIndex() const -> std::optional; - }; - /** - * @brief Class representing an operation (gate) supported by the device. - * @details This class provides methods to query properties of the - * operation. - * @see QDMI_Operation - */ - class Operation { - /// @brief The associated QDMI_Device object. - QDMI_Device device_; - /// @brief The underlying QDMI_Operation object. - QDMI_Operation operation_; - - template - [[nodiscard]] auto queryProperty(const QDMI_Operation_Property prop, - const std::vector& sites, - const std::vector& params) const - -> T { - std::string msg = "Querying "; - msg += qdmi::toString(prop); - std::vector qdmiSites; - qdmiSites.reserve(sites.size()); - std::ranges::transform( - sites, std::back_inserter(qdmiSites), - [](const Site& site) -> QDMI_Site { return site; }); - if constexpr (string_or_optional_string) { - size_t size = 0; - auto result = QDMI_device_query_operation_property( - device_, operation_, sites.size(), qdmiSites.data(), - params.size(), params.data(), prop, 0, nullptr, &size); - if constexpr (is_optional) { - if (result == QDMI_ERROR_NOTSUPPORTED) { - return std::nullopt; - } - } - qdmi::throwIfError(result, msg); - std::string value(size - 1, '\0'); - result = QDMI_device_query_operation_property( - device_, operation_, sites.size(), qdmiSites.data(), - params.size(), params.data(), prop, size, value.data(), nullptr); - qdmi::throwIfError(result, msg); - return value; - } else if constexpr (maybe_optional_size_constructible_contiguous_range< - T>) { - size_t size = 0; - auto result = QDMI_device_query_operation_property( - device_, operation_, sites.size(), qdmiSites.data(), - params.size(), params.data(), prop, 0, nullptr, &size); - if constexpr (is_optional) { - if (result == QDMI_ERROR_NOTSUPPORTED) { - return std::nullopt; - } - } - qdmi::throwIfError(result, msg); - remove_optional_t value( - size / sizeof(typename remove_optional_t::value_type)); - result = QDMI_device_query_operation_property( - device_, operation_, sites.size(), qdmiSites.data(), - params.size(), params.data(), prop, size, value.data(), nullptr); - qdmi::throwIfError(result, msg); - return value; - } else { - remove_optional_t value{}; - const auto result = QDMI_device_query_operation_property( - device_, operation_, sites.size(), qdmiSites.data(), - params.size(), params.data(), prop, sizeof(remove_optional_t), - &value, nullptr); - if constexpr (is_optional) { - if (result == QDMI_ERROR_NOTSUPPORTED) { - return std::nullopt; - } - } - qdmi::throwIfError(result, msg); - return value; + qdmi::throwIfError(result, msg); + std::string value(size - 1, '\0'); + result = QDMI_device_query_device_property(device_, prop, size, + value.data(), nullptr); + qdmi::throwIfError(result, msg); + return value; + } else if constexpr (maybe_optional_size_constructible_contiguous_range< + T>) { + size_t size = 0; + auto result = + QDMI_device_query_device_property(device_, prop, 0, nullptr, &size); + + if constexpr (is_optional) { + if (result == QDMI_ERROR_NOTSUPPORTED) { + return std::nullopt; } } - public: - /** - * @brief Constructs an Operation object from a QDMI_Operation handle. - * @param device The associated QDMI_Device handle. - * @param operation The QDMI_Operation handle to wrap. - */ - Operation(Token /* unused */, QDMI_Device device, - QDMI_Operation operation) - : device_(device), operation_(operation) {} - /// @returns the underlying QDMI_Operation object. - [[nodiscard]] auto getQDMIOperation() const -> QDMI_Operation { - return operation_; + qdmi::throwIfError(result, msg); + remove_optional_t value( + size / sizeof(typename remove_optional_t::value_type)); + result = QDMI_device_query_device_property(device_, prop, size, + value.data(), nullptr); + qdmi::throwIfError(result, msg); + return value; + } else { + remove_optional_t value{}; + const auto result = QDMI_device_query_device_property( + device_, prop, sizeof(remove_optional_t), &value, nullptr); + + if constexpr (is_optional) { + if (result == QDMI_ERROR_NOTSUPPORTED) { + return std::nullopt; + } } - // NOLINTNEXTLINE(google-explicit-constructor, *-explicit-conversions) - operator QDMI_Operation() const { return operation_; } - auto operator<=>(const Operation&) const = default; - /// @see QDMI_OPERATION_PROPERTY_NAME - [[nodiscard]] auto getName(const std::vector& sites = {}, - const std::vector& params = {}) const - -> std::string; - /// @see QDMI_OPERATION_PROPERTY_QUBITSNUM - [[nodiscard]] auto - getQubitsNum(const std::vector& sites = {}, - const std::vector& params = {}) const - -> std::optional; - /// @see QDMI_OPERATION_PROPERTY_PARAMETERSNUM - [[nodiscard]] auto - getParametersNum(const std::vector& sites = {}, - const std::vector& params = {}) const -> size_t; - /// @see QDMI_OPERATION_PROPERTY_DURATION - [[nodiscard]] auto - getDuration(const std::vector& sites = {}, - const std::vector& params = {}) const - -> std::optional; - /// @see QDMI_OPERATION_PROPERTY_FIDELITY - [[nodiscard]] auto - getFidelity(const std::vector& sites = {}, - const std::vector& params = {}) const - -> std::optional; - /// @see QDMI_OPERATION_PROPERTY_INTERACTIONRADIUS - [[nodiscard]] auto - getInteractionRadius(const std::vector& sites = {}, - const std::vector& params = {}) const - -> std::optional; - /// @see QDMI_OPERATION_PROPERTY_BLOCKINGRADIUS - [[nodiscard]] auto - getBlockingRadius(const std::vector& sites = {}, - const std::vector& params = {}) const - -> std::optional; - /// @see QDMI_OPERATION_PROPERTY_IDLINGFIDELITY - [[nodiscard]] auto - getIdlingFidelity(const std::vector& sites = {}, - const std::vector& params = {}) const - -> std::optional; - /// @see QDMI_OPERATION_PROPERTY_ISZONED - [[nodiscard]] auto isZoned() const -> bool; - /// @see QDMI_OPERATION_PROPERTY_SITES - [[nodiscard]] auto getSites() const -> std::optional>; - /** - * @brief Returns the list of site pairs the local 2-qubit operation can - * be performed on. - * @details For local 2-qubit operations, this function interprets the - * returned list of sites by QDMI as site pairs according to the QDMI - * specification. Hence, this function facilitates easier iteration over - * supported site pairs. - * @return Optional vector of site pairs if this is a local 2-qubit - * operation, std::nullopt otherwise. - * @see QDMI_OPERATION_PROPERTY_SITES - */ - [[nodiscard]] auto getSitePairs() const - -> std::optional>>; - /// @see QDMI_OPERATION_PROPERTY_MEANSHUTTLINGSPEED - [[nodiscard]] auto - getMeanShuttlingSpeed(const std::vector& sites = {}, - const std::vector& params = {}) const - -> std::optional; - }; - private: - /// @brief The underlying QDMI_Device object. - QDMI_Device device_; - - template - [[nodiscard]] auto queryProperty(const QDMI_Device_Property prop) const - -> T { - std::string msg = "Querying "; - msg += qdmi::toString(prop); - if constexpr (string_or_optional_string) { - size_t size = 0; - auto result = - QDMI_device_query_device_property(device_, prop, 0, nullptr, &size); - if constexpr (is_optional) { - if (result == QDMI_ERROR_NOTSUPPORTED) { - return std::nullopt; - } - } - qdmi::throwIfError(result, msg); - std::string value(size - 1, '\0'); - result = QDMI_device_query_device_property(device_, prop, size, - value.data(), nullptr); - qdmi::throwIfError(result, msg); - return value; - } else if constexpr (maybe_optional_size_constructible_contiguous_range< - T>) { - size_t size = 0; - auto result = - QDMI_device_query_device_property(device_, prop, 0, nullptr, &size); - if constexpr (is_optional) { - if (result == QDMI_ERROR_NOTSUPPORTED) { - return std::nullopt; - } + qdmi::throwIfError(result, msg); + return value; + } + } + + static void setCustomJobParam(QDMI_Job job, QDMI_Job_Parameter param, + const CustomJobParameter& value); + + /// @brief The underlying device pointer. + QDMI_Device device_; + + friend class Session; +}; + +/** + * @brief Class representing a submitted job. + * @details + * This class provides methods to query job status and retrieve + * results. + * + * The class can only be constructed by Device instances. + * + * @see QDMI_Job + */ +class Job { +public: + // NOLINTNEXTLINE(google-explicit-constructor, *-explicit-conversions) + operator QDMI_Job() const { return job_.get(); } + + /// @see QDMI_job_check + [[nodiscard]] QDMI_Job_Status check() const; + + /** + * @brief @see QDMI_job_wait + * @param timeout The maximum time to wait in seconds. 0 (default) means + * wait indefinitely. + * @return true if the job completed successfully, false if it timed out + */ + [[nodiscard]] bool wait(size_t timeout = 0) const; + + /// @see QDMI_job_cancel + void cancel() const; + + /// Get the job ID + [[nodiscard]] std::string getId() const; + + /// Get the program format + [[nodiscard]] QDMI_Program_Format getProgramFormat() const; + + /// Get the program to be executed + [[nodiscard]] std::string getProgram() const; + + /// Get the number of shots + [[nodiscard]] size_t getNumShots() const; + + /** + * @brief Returns the measurement shots as a vector of bitstrings. + * @see QDMI_JOB_RESULT_SHOTS + */ + [[nodiscard]] std::vector getShots() const; + + /** + * @brief Returns a map of measurement outcomes to their respective counts. + * @see QDMI_JOB_RESULT_HIST_KEYS + * @see QDMI_JOB_RESULT_HIST_VALUES + */ + [[nodiscard]] std::map getCounts() const; + + /** + * @brief Returns the dense state vector as a vector of complex numbers. + * @see QDMI_JOB_RESULT_STATEVECTOR_DENSE + */ + [[nodiscard]] std::vector> getDenseStateVector() const; + + /** + * @brief Returns the dense probabilities as a vector of doubles. + * @see QDMI_JOB_RESULT_PROBABILITIES_DENSE + */ + [[nodiscard]] std::vector getDenseProbabilities() const; + + /** + * @brief Returns the sparse state vector as a map of bitstrings to complex + * amplitudes. + * @see QDMI_JOB_RESULT_STATEVECTOR_SPARSE_KEYS + * @see QDMI_JOB_RESULT_STATEVECTOR_SPARSE_VALUES + */ + [[nodiscard]] std::map> + getSparseStateVector() const; + + /** + * @brief Returns the sparse probabilities as a map of bitstrings to + * probabilities. + * @see QDMI_JOB_RESULT_PROBABILITIES_SPARSE_KEYS + * @see QDMI_JOB_RESULT_PROBABILITIES_SPARSE_VALUES + */ + [[nodiscard]] std::map getSparseProbabilities() const; + + auto operator<=>(const Job&) const noexcept = default; + +private: + /** + * @brief Constructs a Job object from a QDMI_Job handle. + * @param job The QDMI_Job handle to wrap. + */ + explicit Job(QDMI_Job job) : job_(job, QDMI_job_free) {} + + std::unique_ptr job_{ + nullptr, QDMI_job_free}; + + friend class Device; +}; + +static_assert(!std::is_copy_constructible()); +static_assert(!std::is_copy_assignable()); +static_assert(std::is_move_constructible()); +static_assert(std::is_move_assignable()); + +/** + * @brief Class representing a site (qubit) on the device. + * @details + * This class provides methods to query properties of the site. + * + * The class can only be constructed by Device and Operation instances. + * + * @see QDMI_Site + */ +class Site { +public: + // NOLINTNEXTLINE(google-explicit-constructor, *-explicit-conversions) + operator QDMI_Site() const { return site_; } + + /// @see QDMI_SITE_PROPERTY_INDEX + [[nodiscard]] size_t getIndex() const; + + /// @see QDMI_SITE_PROPERTY_T1 + [[nodiscard]] std::optional getT1() const; + + /// @see QDMI_SITE_PROPERTY_T2 + [[nodiscard]] std::optional getT2() const; + + /// @see QDMI_SITE_PROPERTY_NAME + [[nodiscard]] std::optional getName() const; + + /// @see QDMI_SITE_PROPERTY_XCOORDINATE + [[nodiscard]] std::optional getXCoordinate() const; + + /// @see QDMI_SITE_PROPERTY_YCOORDINATE + [[nodiscard]] std::optional getYCoordinate() const; + + /// @see QDMI_SITE_PROPERTY_ZCOORDINATE + [[nodiscard]] std::optional getZCoordinate() const; + + /// @see QDMI_SITE_PROPERTY_ISZONE + [[nodiscard]] bool isZone() const; + + /// @see QDMI_SITE_PROPERTY_XEXTENT + [[nodiscard]] std::optional getXExtent() const; + + /// @see QDMI_SITE_PROPERTY_YEXTENT + [[nodiscard]] std::optional getYExtent() const; + + /// @see QDMI_SITE_PROPERTY_ZEXTENT + [[nodiscard]] std::optional getZExtent() const; + + /// @see QDMI_SITE_PROPERTY_MODULEINDEX + [[nodiscard]] std::optional getModuleIndex() const; + + /// @see QDMI_SITE_PROPERTY_SUBMODULEINDEX + [[nodiscard]] std::optional getSubmoduleIndex() const; + + auto operator<=>(const Site&) const noexcept = default; + +private: + /** + * @brief Constructs a Site object from a QDMI_Site handle. + * @param device The device that owns the site. + * @param site The QDMI_Site handle to wrap. + */ + Site(const Device* device, QDMI_Site site) : device_(device), site_(site) {} + + /// Query a site property. + template + [[nodiscard]] T queryProperty(const QDMI_Site_Property prop) const { + if constexpr (string_or_optional_string) { + size_t size = 0; + const auto result = QDMI_device_query_site_property(*device_, site_, prop, + 0, nullptr, &size); + if constexpr (is_optional) { + if (result == QDMI_ERROR_NOTSUPPORTED) { + return std::nullopt; } - qdmi::throwIfError(result, msg); - remove_optional_t value( - size / sizeof(typename remove_optional_t::value_type)); - result = QDMI_device_query_device_property(device_, prop, size, - value.data(), nullptr); - qdmi::throwIfError(result, msg); - return value; - } else { - remove_optional_t value{}; - const auto result = QDMI_device_query_device_property( - device_, prop, sizeof(remove_optional_t), &value, nullptr); - if constexpr (is_optional) { - if (result == QDMI_ERROR_NOTSUPPORTED) { - return std::nullopt; - } + } + qdmi::throwIfError(result, + std::string("Querying size") + qdmi::toString(prop)); + std::string value(size - 1, '\0'); + qdmi::throwIfError(QDMI_device_query_site_property(*device_, site_, prop, + size, value.data(), + nullptr), + std::string("Querying ") + qdmi::toString(prop)); + return value; + } else { + remove_optional_t value{}; + const auto result = QDMI_device_query_site_property( + *device_, site_, prop, sizeof(remove_optional_t), &value, nullptr); + if constexpr (is_optional) { + if (result == QDMI_ERROR_NOTSUPPORTED) { + return std::nullopt; } - qdmi::throwIfError(result, msg); - return value; } + qdmi::throwIfError(result, + std::string("Querying ") + qdmi::toString(prop)); + return value; } + } - public: - /** - * @brief Constructs a Device object from a QDMI_Device handle. - * @param device The QDMI_Device handle to wrap. - */ - Device(Session::Token /* unused */, QDMI_Device device) : device_(device) {} - /** - * @brief Creates a Device object from a QDMI_Device handle. - * @param device The QDMI_Device handle to wrap. - * @return A Device object wrapping the given handle. - * @note This is a factory method for use in bindings where Token - * construction is not accessible. - */ - [[nodiscard]] static auto fromQDMIDevice(QDMI_Device device) -> Device { - return Device(Session::Token{}, device); - } - /// @returns the underlying QDMI_Device object. - [[nodiscard]] auto getQDMIDevice() const -> QDMI_Device { return device_; } - // NOLINTNEXTLINE(google-explicit-constructor, *-explicit-conversions) - operator QDMI_Device() const { return device_; } - auto operator<=>(const Device&) const = default; - /// @see QDMI_DEVICE_PROPERTY_NAME - [[nodiscard]] auto getName() const -> std::string; - /// @see QDMI_DEVICE_PROPERTY_VERSION - [[nodiscard]] auto getVersion() const -> std::string; - /// @see QDMI_DEVICE_PROPERTY_STATUS - [[nodiscard]] auto getStatus() const -> QDMI_Device_Status; - /// @see QDMI_DEVICE_PROPERTY_LIBRARYVERSION - [[nodiscard]] auto getLibraryVersion() const -> std::string; - /// @see QDMI_DEVICE_PROPERTY_QUBITSNUM - [[nodiscard]] auto getQubitsNum() const -> size_t; - /// @see QDMI_DEVICE_PROPERTY_SITES - [[nodiscard]] auto getSites() const -> std::vector; - /** - * @brief Returns the list of regular sites (without zone sites) available - * on the device. - * @details Filters all sites and only returns regular sites, i.e., where - * `isZone()` yields `false`. These represent actual potential physical - * qubit locations on the device lattice. - * @returns vector of regular sites - * @see QDMI_DEVICE_PROPERTY_SITES - */ - [[nodiscard]] auto getRegularSites() const -> std::vector; - /** - * @brief Returns the list of zone sites (without regular sites) available - * on the device. - * @details Filters all sites and only returns zone sites, i.e., where - * `isZone()` yields `true`. These represent a zone, i.e., an extent where - * zoned operations can be performed, not individual qubit locations. - * @returns a vector of zone sites - * @see QDMI_DEVICE_PROPERTY_SITES - */ - [[nodiscard]] auto getZones() const -> std::vector; - /// @see QDMI_DEVICE_PROPERTY_OPERATIONS - [[nodiscard]] auto getOperations() const -> std::vector; - /// @see QDMI_DEVICE_PROPERTY_COUPLINGMAP - [[nodiscard]] auto getCouplingMap() const - -> std::optional>>; - /// @see QDMI_DEVICE_PROPERTY_NEEDSCALIBRATION - [[nodiscard]] auto getNeedsCalibration() const -> std::optional; - /// @see QDMI_DEVICE_PROPERTY_LENGTHUNIT - [[nodiscard]] auto getLengthUnit() const -> std::optional; - /// @see QDMI_DEVICE_PROPERTY_LENGTHSCALEFACTOR - [[nodiscard]] auto getLengthScaleFactor() const -> std::optional; - /// @see QDMI_DEVICE_PROPERTY_DURATIONUNIT - [[nodiscard]] auto getDurationUnit() const -> std::optional; - /// @see QDMI_DEVICE_PROPERTY_DURATIONSCALEFACTOR - [[nodiscard]] auto getDurationScaleFactor() const -> std::optional; - /// @see QDMI_DEVICE_PROPERTY_MINATOMDISTANCE - [[nodiscard]] auto getMinAtomDistance() const -> std::optional; - /// @see QDMI_DEVICE_PROPERTY_SUPPORTEDPROGRAMFORMATS - [[nodiscard]] auto getSupportedProgramFormats() const - -> std::vector; - /// @see QDMI_job_submit - [[nodiscard]] auto submitJob(const std::string& program, - QDMI_Program_Format format, - size_t numShots) const -> Job; - }; + /// @brief A pointer to the device that owns the site. + Device const* device_; -private: - QDMI_Session session_ = nullptr; + /// @brief The underlying QDMI_Site object. + QDMI_Site site_; - template - [[nodiscard]] auto queryProperty(const QDMI_Session_Property prop) const - -> T { - std::string msg = "Querying "; - msg += qdmi::toString(prop); - size_t size = 0; - auto result = - QDMI_session_query_session_property(session_, prop, 0, nullptr, &size); - qdmi::throwIfError(result, msg); - remove_optional_t value( - size / sizeof(typename remove_optional_t::value_type)); - result = QDMI_session_query_session_property(session_, prop, size, - value.data(), nullptr); - qdmi::throwIfError(result, msg); - return value; - } + friend class Device; + friend class Operation; +}; +/** + * @brief Class representing an operation (gate) supported by the device. + * @details + * This class provides methods to query properties of the + * operation. + * + * The class can only be constructed by Device instances. + * + * @see QDMI_Operation + */ +class Operation { public: + // NOLINTNEXTLINE(google-explicit-constructor, *-explicit-conversions) + operator QDMI_Operation() const { return operation_; } + + /// @see QDMI_OPERATION_PROPERTY_NAME + [[nodiscard]] std::string + getName(const std::vector& sites = {}, + const std::vector& params = {}) const; + + /// @see QDMI_OPERATION_PROPERTY_QUBITSNUM + [[nodiscard]] std::optional + getQubitsNum(const std::vector& sites = {}, + const std::vector& params = {}) const; + + /// @see QDMI_OPERATION_PROPERTY_PARAMETERSNUM + [[nodiscard]] size_t + getParametersNum(const std::vector& sites = {}, + const std::vector& params = {}) const; + + /// @see QDMI_OPERATION_PROPERTY_DURATION + [[nodiscard]] std::optional + getDuration(const std::vector& sites = {}, + const std::vector& params = {}) const; + + /// @see QDMI_OPERATION_PROPERTY_FIDELITY + [[nodiscard]] std::optional + getFidelity(const std::vector& sites = {}, + const std::vector& params = {}) const; + + /// @see QDMI_OPERATION_PROPERTY_INTERACTIONRADIUS + [[nodiscard]] std::optional + getInteractionRadius(const std::vector& sites = {}, + const std::vector& params = {}) const; + + /// @see QDMI_OPERATION_PROPERTY_BLOCKINGRADIUS + [[nodiscard]] std::optional + getBlockingRadius(const std::vector& sites = {}, + const std::vector& params = {}) const; + + /// @see QDMI_OPERATION_PROPERTY_IDLINGFIDELITY + [[nodiscard]] std::optional + getIdlingFidelity(const std::vector& sites = {}, + const std::vector& params = {}) const; + + /// @see QDMI_OPERATION_PROPERTY_ISZONED + [[nodiscard]] bool isZoned() const; + + /// @see QDMI_OPERATION_PROPERTY_SITES + [[nodiscard]] std::optional> getSites() const; + /** - * @brief Constructs a new QDMI Session with optional authentication. - * @param config Optional session configuration containing authentication - * parameters. If not provided, uses default (no authentication). - * @details Creates, allocates, and initializes a new QDMI session. + * @brief Returns the list of site pairs the local 2-qubit operation can + * be performed on. + * @details For local 2-qubit operations, this function interprets the + * returned list of sites by QDMI as site pairs according to the QDMI + * specification. Hence, this function facilitates easier iteration over + * supported site pairs. + * @return Optional vector of site pairs if this is a local 2-qubit + * operation, std::nullopt otherwise. + * @see QDMI_OPERATION_PROPERTY_SITES */ - explicit Session(const SessionConfig& config = {}); + [[nodiscard]] std::optional>> + getSitePairs() const; + + /// @see QDMI_OPERATION_PROPERTY_MEANSHUTTLINGSPEED + [[nodiscard]] std::optional + getMeanShuttlingSpeed(const std::vector& sites = {}, + const std::vector& params = {}) const; + + auto operator<=>(const Operation&) const noexcept = default; +private: /** - * @brief Destructor that releases the QDMI session. + * @brief Constructs an Operation object from a QDMI_Operation handle. + * @param device The device that owns the site. + * @param operation The QDMI_Operation handle to wrap. */ - ~Session(); + Operation(const Device* device, QDMI_Operation operation) + : device_(device), operation_(operation) {} + + /// Query an operation property. + template + [[nodiscard]] T queryProperty(const QDMI_Operation_Property prop, + const std::vector& sites, + const std::vector& params) const { + std::string msg = "Querying "; + msg += qdmi::toString(prop); + std::vector qdmiSites; + qdmiSites.reserve(sites.size()); + std::ranges::transform(sites, std::back_inserter(qdmiSites), + [](const Site& site) -> QDMI_Site { return site; }); + if constexpr (string_or_optional_string) { + size_t size = 0; + auto result = QDMI_device_query_operation_property( + *device_, operation_, sites.size(), qdmiSites.data(), params.size(), + params.data(), prop, 0, nullptr, &size); + if constexpr (is_optional) { + if (result == QDMI_ERROR_NOTSUPPORTED) { + return std::nullopt; + } + } + qdmi::throwIfError(result, msg); + std::string value(size - 1, '\0'); + result = QDMI_device_query_operation_property( + *device_, operation_, sites.size(), qdmiSites.data(), params.size(), + params.data(), prop, size, value.data(), nullptr); + qdmi::throwIfError(result, msg); + return value; + } else if constexpr (maybe_optional_size_constructible_contiguous_range< + T>) { + size_t size = 0; + auto result = QDMI_device_query_operation_property( + *device_, operation_, sites.size(), qdmiSites.data(), params.size(), + params.data(), prop, 0, nullptr, &size); + if constexpr (is_optional) { + if (result == QDMI_ERROR_NOTSUPPORTED) { + return std::nullopt; + } + } + qdmi::throwIfError(result, msg); + remove_optional_t value( + size / sizeof(typename remove_optional_t::value_type)); + result = QDMI_device_query_operation_property( + *device_, operation_, sites.size(), qdmiSites.data(), params.size(), + params.data(), prop, size, value.data(), nullptr); + qdmi::throwIfError(result, msg); + return value; + } else { + remove_optional_t value{}; + const auto result = QDMI_device_query_operation_property( + *device_, operation_, sites.size(), qdmiSites.data(), params.size(), + params.data(), prop, sizeof(remove_optional_t), &value, nullptr); + if constexpr (is_optional) { + if (result == QDMI_ERROR_NOTSUPPORTED) { + return std::nullopt; + } + } + qdmi::throwIfError(result, msg); + return value; + } + } - // Delete copy constructors and assignment operators - Session(const Session&) = delete; - Session& operator=(const Session&) = delete; + /// @brief A pointer to the device that owns the operation. + Device const* device_; - // Allow move semantics - Session(Session&&) noexcept; - Session& operator=(Session&&) noexcept; + /// @brief The underlying QDMI_Operation object. + QDMI_Operation operation_; - /// @see QDMI_SESSION_PROPERTY_DEVICES - [[nodiscard]] auto getDevices() -> std::vector; + friend class Device; }; } // namespace fomac diff --git a/include/mqt-core/ir/QuantumComputation.hpp b/include/mqt-core/ir/QuantumComputation.hpp index 68f2e295b1..92e426f982 100644 --- a/include/mqt-core/ir/QuantumComputation.hpp +++ b/include/mqt-core/ir/QuantumComputation.hpp @@ -322,11 +322,12 @@ class QuantumComputation { /** * @brief Add measurements to all qubits * @param addBits Whether to add new classical bits to the circuit + * @param addBarrier Whether to add a barrier before the measurements * @details This function adds measurements to all qubits in the circuit and * appends a new classical register (named "meas") to the circuit if addBits * is true. Otherwise, qubit q is measured into classical bit q. */ - void measureAll(bool addBits = true); + void measureAll(bool addBits = true, bool addBarrier = true); void bridge(const Targets& targets); diff --git a/include/mqt-core/na/fomac/Device.hpp b/include/mqt-core/na/fomac/Device.hpp index 1a6a5e53ab..e5698661d4 100644 --- a/include/mqt-core/na/fomac/Device.hpp +++ b/include/mqt-core/na/fomac/Device.hpp @@ -24,7 +24,7 @@ namespace na { * @brief Class representing the Session library with neutral atom extensions. * @see fomac::Session */ -class Session : public fomac::Session { +class Session { public: /** * @brief Class representing a quantum device with neutral atom extensions. @@ -32,7 +32,7 @@ class Session : public fomac::Session { * @note Since it inherits from @ref na::Device, Device objects can be * converted to `nlohmann::json` objects. */ - class Device : public fomac::Session::Device, na::Device { + class Device : public fomac::Device, na::Device { /** * @brief Initializes the name from the underlying QDMI device. @@ -86,7 +86,8 @@ class Session : public fomac::Session { * class. For their initialization, the corresponding `init*FromDevice` * methods must be called, see @ref tryCreateFromDevice. */ - explicit Device(const fomac::Session::Device& device); + explicit Device(const fomac::Device& device) + : fomac::Device(device), na::Device() {}; public: /// @returns the length unit of the device. @@ -120,8 +121,7 @@ class Session : public fomac::Session { * @return An optional containing the instantiated device if compatible, * std::nullopt otherwise. */ - [[nodiscard]] static auto - tryCreateFromDevice(const fomac::Session::Device& device) + [[nodiscard]] static auto tryCreateFromDevice(const fomac::Device& device) -> std::optional { Device d(device); // The sequence of the following method calls does not matter. diff --git a/include/mqt-core/qasm3/Parser.hpp b/include/mqt-core/qasm3/Parser.hpp index b93f679945..72831862da 100644 --- a/include/mqt-core/qasm3/Parser.hpp +++ b/include/mqt-core/qasm3/Parser.hpp @@ -114,7 +114,7 @@ class Parser final { std::shared_ptr parseBarrierStatement(); - std::shared_ptr parseDeclaration(bool isConst); + std::shared_ptr parseDeclaration(bool isConst, bool isOutput); std::shared_ptr parseGateDefinition(); diff --git a/include/mqt-core/qasm3/Statement.hpp b/include/mqt-core/qasm3/Statement.hpp index 258c7a7900..31f09b5ccb 100644 --- a/include/mqt-core/qasm3/Statement.hpp +++ b/include/mqt-core/qasm3/Statement.hpp @@ -347,15 +347,18 @@ class DeclarationStatement final public std::enable_shared_from_this { public: bool isConst; + bool isOutput; std::variant, std::shared_ptr> type; std::string identifier; std::shared_ptr expression; DeclarationStatement(std::shared_ptr debug, const bool declIsConst, - std::shared_ptr ty, std::string id, + bool declIsOutput, std::shared_ptr ty, + std::string id, std::shared_ptr expr) - : Statement(std::move(debug)), isConst(declIsConst), type(ty), - identifier(std::move(id)), expression(std::move(expr)) {} + : Statement(std::move(debug)), isConst(declIsConst), + isOutput(declIsOutput), type(ty), identifier(std::move(id)), + expression(std::move(expr)) {} void accept(InstVisitor* visitor) override; }; diff --git a/include/mqt-core/qir/runtime/Runtime.hpp b/include/mqt-core/qir/runtime/Runtime.hpp index 2ca9d5639a..756ff1fe1d 100644 --- a/include/mqt-core/qir/runtime/Runtime.hpp +++ b/include/mqt-core/qir/runtime/Runtime.hpp @@ -214,7 +214,7 @@ class Runtime { /// If @c dd is currently populated, the existing package's `decRef` plus /// `garbageCollect` path is used so the package (and its internal caches) /// is kept warm. - /// If @c dd was moved out (e.g. by @ref Runtime::takeState), a new package + /// If @c dd was moved out (e.g., by @ref Runtime::takeState), a new package /// is allocated. auto reset() -> void { if (dd) { @@ -228,6 +228,8 @@ class Runtime { } }; + enum class OutputSchema : uint8_t { Labeled, Ordered }; + private: static constexpr uintptr_t MIN_DYN_QUBIT_ADDRESS = 0x10000; enum class AddressMode : uint8_t { UNKNOWN, DYNAMIC, STATIC }; @@ -245,6 +247,9 @@ class Runtime { QState qState; std::mt19937_64 mt; std::ostream* os = &std::cout; + // The QIR spec does not define a default output schema. + // The runtime picks @c Labeled when a program doesn't declare one. + OutputSchema outputSchema = OutputSchema::Labeled; Runtime(); explicit Runtime(uint64_t randomSeed); @@ -290,6 +295,13 @@ class Runtime { return {targets, Op, paramVec}; } + // Helper function to output a type (bool, int...) to @c os, honoring the + // active @c outputSchema. + // The label is included only in Labeled mode. + // Tab separator between fields, newline at end. + void outputType(const char* type, std::string_view value, + const char* label) const; + public: [[nodiscard]] static auto generateRandomSeed() -> uint64_t; static Runtime& getInstance(); @@ -398,12 +410,6 @@ class Runtime { /// @returns the accumulated measurement string. auto getMeasurements() const -> const std::string&; - /// Emit `label:\n` to the output stream. - auto outputContainer(int64_t elementCount, const char* label) const -> void; - - /// Emit `label: valueStr\n` to the output stream. - auto outputValue(std::string_view valueStr, const char* label) const -> void; - /// Move the quantum state out of the runtime. /// Then reset the runtime to a clean state ready for the next job. /// Intended for use after a @c JitSession constructed with @@ -414,6 +420,44 @@ class Runtime { auto getOstream() const -> std::ostream&; auto setOstream(std::ostream& other) -> void; auto resetOstream() -> void; + + /// Emit `OUTPUT\tRESULT\t<0|1>[\tlabel]\n` to the output stream. + auto outputResult(bool value, const char* label) const -> void; + + /// Emit `OUTPUT\tBOOL\t[\tlabel]\n` to the output stream. + auto outputBool(bool value, const char* label) const -> void; + + /// Emit `OUTPUT\tINT\t[\tlabel]\n` to the output stream. + auto outputInt(int64_t value, const char* label) const -> void; + + /// Emit `OUTPUT\tDOUBLE\t[\tlabel]\n` to the output stream. + auto outputFloat(double value, const char* label) const -> void; + + /// Emit `OUTPUT\tTUPLE\t[\tlabel]\n` to the output stream. + auto outputTuple(int64_t elementCount, const char* label) const -> void; + + /// Emit `OUTPUT\tARRAY\t[\tlabel]\n` to the output stream. + auto outputArray(int64_t elementCount, const char* label) const -> void; + + /// Emit the HEADER records (once per submitted program): + /// `HEADER\tschema_id\t` + /// `HEADER\tschema_version\t2.1` + auto outputProgramHeader() const -> void; + + /// Emit `START\n` followed by + /// `METADATA\toutput_labeling_schema\t\n` (one per shot). + auto outputShotStart() const -> void; + + /// Emit `END\t0\n` (one per shot). + /// The trailing `0` is a spec literal, not a runtime exit code. + auto outputShotEnd() const -> void; + + [[nodiscard]] auto getOutputSchema() const -> OutputSchema; + auto setOutputSchema(OutputSchema schema) -> void; }; +/// Write the schema's spec-mandated literal (`labeled` or `ordered`). +auto operator<<(std::ostream& os, Runtime::OutputSchema schema) + -> std::ostream&; + } // namespace qir diff --git a/mlir/include/mlir/Conversion/QCToQIR/QIRCommon/QIRCommon.h b/mlir/include/mlir/Conversion/QCToQIR/QIRCommon/QIRCommon.h index 2434359f3a..ce1fcde8dc 100644 --- a/mlir/include/mlir/Conversion/QCToQIR/QIRCommon/QIRCommon.h +++ b/mlir/include/mlir/Conversion/QCToQIR/QIRCommon/QIRCommon.h @@ -67,6 +67,15 @@ struct LoweringState { Block* measurementsBlock{}; Block* outputBlock{}; + /// Set of MeasureOps whose results should be recorded in the output. + DenseSet returnedMeasurements; + + /// Set of array register names that should be recorded in the output. + DenseSet recordedArrays; + + /// Set of unnamed result indices that should be recorded in the output. + DenseSet recordedIndices; + /// The qubit allocation mode used in the module AllocationMode allocationMode = AllocationMode::Unset; @@ -152,4 +161,28 @@ void populateQCToQIRPatterns(RewritePatternSet& patterns, void addOutputRecording(LLVM::LLVMFuncOp& main, MLIRContext* ctx, LoweringState& state); +/** + * @brief Strips returned measurement results from function return statements + * + * @details + * Walks all `func::ReturnOp` operations in the module to identify operands + * that are directly defined by a `qc::MeasureOp`. For each such operand: + * - The defining `MeasureOp` is added to `state.returnedMeasurements` so that + * it will be included in the QIR output recording. + * - The operand is removed from the return statement. + * + * Non-measurement return values are preserved. After stripping, the enclosing + * `func::FuncOp` function type is updated to match the new return operands. + * + * This must be called **before** func-to-LLVM conversion, while + * `func::ReturnOp` and `qc::MeasureOp` are still in the IR. + * + * Return values that are indirectly computed from measurement outcomes remain + * unaffected. + * + * @param moduleOp The top-level module operation to walk + * @param state The lowering state; `returnedMeasurements` is populated + */ +void stripReturnedMeasurements(Operation* moduleOp, LoweringState& state); + } // namespace mlir diff --git a/mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h b/mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h index ed94af35bc..dee7248db0 100644 --- a/mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h +++ b/mlir/include/mlir/Dialect/QC/Builder/QCProgramBuilder.h @@ -70,7 +70,8 @@ class QCProgramBuilder final : public ImplicitLocOpBuilder { //===--------------------------------------------------------------------===// /** - * @brief Initialize the builder and prepare for program construction + * @brief Initialize the builder and prepare for program construction, with + * a default return type of i64. * * @details * Creates a main function with an entry_point attribute. Must be called @@ -78,6 +79,23 @@ class QCProgramBuilder final : public ImplicitLocOpBuilder { */ void initialize(); + /** + * @brief Initialize the builder and prepare for program construction + * with specified return types. + * @param returnTypes The return types for the main function + * + * @details + * Creates a main function with an entry_point attribute. Must be called + * before adding operations. + */ + void initialize(TypeRange returnTypes); + + /** + * @brief Modify the return types of the main function after initialization. + * @param returnTypes The new return types for the main function + */ + void retype(TypeRange returnTypes); + //===--------------------------------------------------------------------===// // Constants //===--------------------------------------------------------------------===// @@ -922,6 +940,42 @@ class QCProgramBuilder final : public ImplicitLocOpBuilder { QCProgramBuilder& ctrl(ValueRange controls, ValueRange targets, const function_ref& body); + /** + * @brief Apply a control modifier with a single target and one-qubit body. + * + * @param controls Control qubits + * @param target Target qubit + * @param body Function that builds the body containing the target operation + * @return Reference to this builder for method chaining + * + * @par Example: + * ```c++ + * builder.ctrl({q0_in, q1_in}, q2_in, [&](Value target) { + * builder.x(target); + * }); + * ``` + */ + QCProgramBuilder& ctrl(ValueRange controls, Value target, + const function_ref& body); + + /** + * @brief Apply a control modifier with one control and one target. + * + * @param control Control qubit + * @param target Target qubit + * @param body Function that builds the body containing the target operation + * @return Reference to this builder for method chaining + * + * @par Example: + * ```c++ + * builder.ctrl(q0_in, q1_in, [&](Value target) { + * builder.x(target); + * }); + * ``` + */ + QCProgramBuilder& ctrl(Value control, Value target, + const function_ref& body); + /** * @brief Apply an inverse (i.e., adjoint) operation. * @@ -944,6 +998,23 @@ class QCProgramBuilder final : public ImplicitLocOpBuilder { QCProgramBuilder& inv(ValueRange qubits, const function_ref& body); + /** + * @brief Apply an inverse modifier on a single qubit. + * + * @param qubit Qubit involved in the operation + * @param body Function that builds the body containing the operation to + * invert + * @return Reference to this builder for method chaining + * + * @par Example: + * ```c++ + * builder.inv(q0_in, [&](Value qubit) { + * builder.h(qubit); + * }); + * ``` + */ + QCProgramBuilder& inv(Value qubit, const function_ref& body); + //===--------------------------------------------------------------------===// // Deallocation //===--------------------------------------------------------------------===// @@ -1092,17 +1163,49 @@ class QCProgramBuilder final : public ImplicitLocOpBuilder { OwningOpRef finalize(); /** - * @brief Convenience method for building quantum programs + * @brief Finalize the program with the given return values and return the + * constructed module + * @param returnValues Values representing the return values of the main + * function. + * + * @details + * Automatically deallocates all remaining valid qubits and tensors of qubits, + * adds a return statement with the given return values, and + * transfers ownership of the module to the caller. The builder should not + * be used after calling this method. + * + * The return values must have the types indicated by the function signature + * of the main function, which returns an `i64` by default and can be + * modified by passing different arguments to the `initialize()` method. + * + * @return OwningOpRef containing the constructed quantum program module + */ + OwningOpRef finalize(ValueRange returnValues); + + /** + * @brief Convenience method for building quantum programs. * @param context The MLIR context to use for building the program * @param buildFunc A function that takes a reference to a QCProgramBuilder * and uses it to build the desired quantum program. The builder will be * properly initialized before calling this function, and the resulting module - * will be finalized and returned after this function completes. + * will be finalized using the returned Values after this function completes. + * @return The module containing the quantum program built by buildFunc. + */ + static OwningOpRef + build(MLIRContext* context, + const function_ref(QCProgramBuilder&)>& buildFunc); + + /** + * @brief Convenience method for building quantum programs with one return + * value. + * @param context The MLIR context to use for building the program + * @param buildFunc A function that takes a reference to a QCProgramBuilder + * and returns the single result value of the desired quantum program. * @return The module containing the quantum program built by buildFunc. */ static OwningOpRef build(MLIRContext* context, - const function_ref& buildFunc); + const function_ref& buildFunc); private: enum class AllocationMode : uint8_t { Unset, Static, Dynamic }; diff --git a/mlir/include/mlir/Dialect/QC/IR/QCOps.td b/mlir/include/mlir/Dialect/QC/IR/QCOps.td index 88df7b5586..da46297e8f 100644 --- a/mlir/include/mlir/Dialect/QC/IR/QCOps.td +++ b/mlir/include/mlir/Dialect/QC/IR/QCOps.td @@ -968,7 +968,11 @@ def CtrlOp }]; let builders = [OpBuilder<(ins "ValueRange":$controls, "ValueRange":$targets, - "const function_ref&":$body)>]; + "const function_ref&":$body)>, + OpBuilder<(ins "ValueRange":$controls, "Value":$target, + "const function_ref&":$bodyBuilder)>, + OpBuilder<(ins "Value":$control, "Value":$target, + "const function_ref&":$bodyBuilder)>]; let hasCanonicalizer = 1; let hasVerifier = 1; @@ -1021,7 +1025,9 @@ def InvOp : QCOp<"inv", }]; let builders = [OpBuilder<(ins "ValueRange":$qubits, - "const function_ref&":$body)>]; + "const function_ref&":$body)>, + OpBuilder<(ins "Value":$qubit, + "const function_ref&":$bodyBuilder)>]; let hasCanonicalizer = 1; let hasVerifier = 1; diff --git a/mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h b/mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h index 12d7ef73d1..d88e12dfa7 100644 --- a/mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h +++ b/mlir/include/mlir/Dialect/QCO/Builder/QCOProgramBuilder.h @@ -78,7 +78,8 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { //===--------------------------------------------------------------------===// /** - * @brief Initialize the builder and prepare for program construction + * @brief Initialize the builder and prepare for program construction, with + * a default return type of i64. * * @details * Creates a main function with an entry_point attribute. Must be called @@ -86,6 +87,23 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { */ void initialize(); + /** + * @brief Initialize the builder and prepare for program construction + * with specified return types. + * @param returnTypes The return types for the main function + * + * @details + * Creates a main function with an entry_point attribute. Must be called + * before adding operations. + */ + void initialize(TypeRange returnTypes); + + /** + * @brief Modify the return types of the main function after initialization. + * @param returnTypes The new return types for the main function + */ + void retype(TypeRange returnTypes); + //===--------------------------------------------------------------------===// // Constants //===--------------------------------------------------------------------===// @@ -387,7 +405,7 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { * * @param qubit Input qubit (must be valid/unconsumed) * @param bit The classical bit to record the result - * @return Output qubit value + * @return Pair of (output_qubit, measurement_result) * * @par Example: * ```c++ @@ -397,7 +415,7 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { * %q0_out, %r0 = qco.measure("c", 3, 0) %q0 : !qco.qubit * ``` */ - Value measure(Value qubit, const Bit& bit); + std::pair measure(Value qubit, const Bit& bit); /** * @brief Reset a qubit to |0⟩ state @@ -468,7 +486,7 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { * \ * @par Example: \ * ```c++ \ - * {q0_out, q1_out} = builder.mc##OP_NAME(PARAM, {q0_in, q1_in}); \ + * auto [q0_out, q1_out] = builder.mc##OP_NAME(PARAM, {q0_in, q1_in}); \ * ``` \ * ```mlir \ * %q0_out, %q1_out = qco.ctrl(%q0_in, %q1_in) { \ @@ -519,7 +537,7 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { * \ * @par Example: \ * ```c++ \ - * {q0_out, q1_out} = builder.c##OP_NAME(q0_in, q1_in); \ + * auto [q0_out, q1_out] = builder.c##OP_NAME(q0_in, q1_in); \ * ``` \ * ```mlir \ * %q0_out, %q1_out = qco.ctrl(%q0_in) %q1_in { \ @@ -542,7 +560,8 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { * \ * @par Example: \ * ```c++ \ - * {controls_out, target_out} = builder.mc##OP_NAME({q0_in, q1_in}, q2_in); \ + * auto [controls_out, target_out] = builder.mc##OP_NAME({q0_in, q1_in}, \ + * q2_in); \ * ``` \ * ```mlir \ * %controls_out, %target_out = qco.ctrl(%q0_in, %q1_in) %q2_in { \ @@ -605,7 +624,7 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { * \ * @par Example: \ * ```c++ \ - * {q0_out, q1_out} = builder.c##OP_NAME(PARAM, q0_in, q1_in); \ + * auto [q0_out, q1_out] = builder.c##OP_NAME(PARAM, q0_in, q1_in); \ * ``` \ * ```mlir \ * %q0_out, %q1_out = qco.ctrl(%q0_in) %q1_in { \ @@ -630,8 +649,8 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { * \ * @par Example: \ * ```c++ \ - * {controls_out, target_out} = builder.mc##OP_NAME(PARAM, {q0_in, q1_in}, \ - * q2_in); \ + * auto [controls_out, target_out] = builder.mc##OP_NAME(PARAM, {q0_in, \ + * q1_in}, q2_in); \ * ``` \ * ```mlir \ * %controls_out, %target_out = qco.ctrl(%q0_in, %q1_in) %q2_in { \ @@ -693,7 +712,7 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { * \ * @par Example: \ * ```c++ \ - * {q0_out, q1_out} = builder.c##OP_NAME(PARAM1, PARAM2, q0_in, q1_in); \ + * auto [q0_out, q1_out] = builder.c##OP_NAME(PARAM1, PARAM2, q0_in, q1_in); \ * ``` \ * ```mlir \ * %q0_out, %q1_out = qco.ctrl(%q0_in) %q1_in { \ @@ -722,8 +741,8 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { * \ * @par Example: \ * ```c++ \ - * {controls_out, target_out} = builder.mc##OP_NAME(PARAM1, PARAM2, {q0_in, \ - * q1_in}, q2_in); \ + * auto [controls_out, target_out] = builder.mc##OP_NAME(PARAM1, PARAM2, \ + * {q0_in, q1_in}, q2_in); \ * ``` \ * ```mlir \ * %controls_out, %target_out = qco.ctrl(%q0_in, %q1_in) %q2_in { \ @@ -789,7 +808,7 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { * \ * @par Example: \ * ```c++ \ - * {q0_out, q1_out} = builder.c##OP_NAME(PARAM1, PARAM2, PARAM3, q0_in, \ + * auto [q0_out, q1_out] = builder.c##OP_NAME(PARAM1, PARAM2, PARAM3, q0_in, \ * q1_in); \ * ``` \ * ```mlir \ @@ -821,8 +840,8 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { * \ * @par Example: \ * ```c++ \ - * {controls_out, target_out} = builder.mc##OP_NAME(PARAM1, PARAM2, PARAM3, \ - * {q0_in, q1_in}, q2_in); \ + * auto [controls_out, target_out] = builder.mc##OP_NAME(PARAM1, PARAM2, \ + * PARAM3, {q0_in, q1_in}, q2_in); \ * ``` \ * ```mlir \ * %controls_out, %target_out = qco.ctrl(%q0_in, %q1_in) %q2_in { \ @@ -859,7 +878,7 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { * \ * @par Example: \ * ```c++ \ - * {q0_out, q1_out} = builder.OP_NAME(q0_in, q1_in); \ + * auto [q0_out, q1_out] = builder.OP_NAME(q0_in, q1_in); \ * ``` \ * ```mlir \ * %q0_out, %q1_out = qco.OP_NAME %q0_in, %q1_in : !qco.qubit, !qco.qubit \ @@ -881,7 +900,8 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { * \ * @par Example: \ * ```c++ \ - * {q0_out, {q1_out, q2_out}} = builder.c##OP_NAME(q0_in, q1_in, q2_in); \ + * auto [q0_out, targets_out] = builder.c##OP_NAME(q0_in, q1_in, q2_in); \ + * auto [q1_out, q2_out] = targets_out; \ * ``` \ * ```mlir \ * %q0_out, %q1_out, %q2_out = qco.ctrl(%q0_in) %q1_in, %q2_in { \ @@ -908,8 +928,9 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { * \ * @par Example: \ * ```c++ \ - * {controls_out, {q1_out, q2_out}} = builder.mc##OP_NAME({q0_in, q1_in}, \ + * auto [controls_out, targets_out] = builder.mc##OP_NAME({q0_in, q1_in}, \ * q2_in, q3_in); \ + * auto [q1_out, q2_out] = targets_out; \ * ``` \ * ```mlir \ * %controls_out, %q1_out, %q2_out = qco.ctrl(%q0_in, %q1_in) %q2_in, \ @@ -948,7 +969,7 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { * \ * @par Example: \ * ```c++ \ - * {q0_out, q1_out} = builder.OP_NAME(PARAM, q0_in, q1_in); \ + * auto [q0_out, q1_out] = builder.OP_NAME(PARAM, q0_in, q1_in); \ * ``` \ * ```mlir \ * %q0_out, %q1_out = qco.OP_NAME(%PARAM) %q0_in, %q1_in : !qco.qubit, \ @@ -973,8 +994,9 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { * \ * @par Example: \ * ```c++ \ - * {q0_out, {q1_out, q2_out}} = builder.c##OP_NAME(PARAM, q0_in, q1_in, \ + * auto [q0_out, targets_out] = builder.c##OP_NAME(PARAM, q0_in, q1_in, \ * q2_in); \ + * auto [q1_out, q2_out] = targets_out; \ * ``` \ * ```mlir \ * %q0_out, %q1_out, %q2_out = qco.ctrl(%q0_in) %q1_in, %q2_in { \ @@ -1003,8 +1025,9 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { * \ * @par Example: \ * ```c++ \ - * {controls_out, {q1_out, q2_out}} = builder.mc##OP_NAME(PARAM, {q0_in, \ + * auto [controls_out, targets_out] = builder.mc##OP_NAME(PARAM, {q0_in, \ * q1_in}, q2_in, q3_in); \ + * auto [q1_out, q2_out] = targets_out; \ * ``` \ * ```mlir \ * %controls_out, %q1_out, %q2_out = qco.ctrl(%q0_in, %q1_in) %q2_in, \ @@ -1045,7 +1068,7 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { * \ * @par Example: \ * ```c++ \ - * {q0_out, q1_out} = builder.OP_NAME(PARAM1, PARAM2, q0_in, q1_in); \ + * auto [q0_out, q1_out] = builder.OP_NAME(PARAM1, PARAM2, q0_in, q1_in); \ * ``` \ * ```mlir \ * %q0_out, %q1_out = qco.OP_NAME(%PARAM1, %PARAM2) %q0_in, %q1_in : \ @@ -1071,8 +1094,9 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { * \ * @par Example: \ * ```c++ \ - * {q0_out, {q1_out, q2_out}} = builder.c##OP_NAME(PARAM1, PARAM2, q0_in, \ + * auto [q0_out, targets_out] = builder.c##OP_NAME(PARAM1, PARAM2, q0_in, \ * q1_in, q2_in); \ + * auto [q1_out, q2_out] = targets_out; \ * ``` \ * ```mlir \ * %q0_out, %q1_out, %q2_out = qco.ctrl(%q0_in) %q1_in, %q2_in { \ @@ -1103,8 +1127,9 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { * \ * @par Example: \ * ```c++ \ - * {controls_out, {q1_out, q2_out}} = builder.mc##OP_NAME(PARAM1, PARAM2, \ + * auto [controls_out, targets_out] = builder.mc##OP_NAME(PARAM1, PARAM2, \ * {q0_in, q1_in}, q2_in, q3_in); \ + * auto [q1_out, q2_out] = targets_out; \ * ``` \ * ```mlir \ * %controls_out, %q1_out, %q2_out = qco.ctrl(%q0_in, %q1_in) %q2_in, \ @@ -1159,7 +1184,7 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { * * @par Example: * ```c++ - * {controls_out, targets_out} = + * auto [controls_out, targets_out] = * builder.ctrl(q0_in, q1_in, * [&](ValueRange targets) -> SmallVector { * return {builder.x(targets[0])}; @@ -1176,6 +1201,44 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { ctrl(ValueRange controls, ValueRange targets, function_ref(ValueRange)> body); + /** + * @brief Apply a control modifier with a single target and one-qubit body. + * + * @param controls Control qubits + * @param target Target qubit + * @param body Function that builds the body containing the target operation + * @return Pair of (output_control_qubits, output_target_qubit) + * + * @par Example: + * ```c++ + * auto [controls_out, target_out] = + * builder.ctrl({q0_in, q1_in}, q2_in, [&](Value target) { + * return builder.x(target); + * }); + * ``` + */ + std::pair ctrl(ValueRange controls, Value target, + function_ref body); + + /** + * @brief Apply a control modifier with one control and one target. + * + * @param control Control qubit + * @param target Target qubit + * @param body Function that builds the body containing the target operation + * @return Pair of (output_control_qubit, output_target_qubit) + * + * @par Example: + * ```c++ + * auto [control_out, target_out] = + * builder.ctrl(q0_in, q1_in, [&](Value target) { + * return builder.x(target); + * }); + * ``` + */ + std::pair ctrl(Value control, Value target, + function_ref body); + /** * @brief Apply an inverse operation * @@ -1185,7 +1248,7 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { * * @par Example: * ```c++ - * qubits_out = builder.inv(q0_in, + * auto qubits_out = builder.inv(q0_in, * [&](ValueRange qubits) -> SmallVector { * return {builder.s(qubits[0])}; * } @@ -1201,6 +1264,23 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { ValueRange inv(ValueRange qubits, function_ref(ValueRange)> body); + /** + * @brief Apply an inverse modifier on a single qubit. + * + * @param qubit Qubit involved in the operation + * @param body Function that builds the body containing the operation to + * invert + * @return Output qubit + * + * @par Example: + * ```c++ + * auto qubit_out = builder.inv(q0_in, [&](Value qubit) { + * return builder.s(qubit); + * }); + * ``` + */ + Value inv(Value qubit, function_ref body); + //===--------------------------------------------------------------------===// // Deallocation //===--------------------------------------------------------------------===// @@ -1391,17 +1471,49 @@ class QCOProgramBuilder final : public ImplicitLocOpBuilder { OwningOpRef finalize(); /** - * @brief Convenience method for building quantum programs + * @brief Finalize the program with the given return values and return the + * constructed module + * @param returnValues Values representing the return values of the main + * function. + * + * @details + * Automatically deallocates all remaining valid qubits and tensors of qubits, + * adds a return statement with the given return values, and + * transfers ownership of the module to the caller. The builder should not + * be used after calling this method. + * + * The return values must have the types indicated by the function signature + * of the main function, which returns an `i64` by default and can be + * modified by passing different arguments to the `initialize()` method. + * + * @return OwningOpRef containing the constructed quantum program module + */ + OwningOpRef finalize(ValueRange returnValues); + + /** + * @brief Convenience method for building quantum programs. * @param context The MLIR context to use for building the program * @param buildFunc A function that takes a reference to a QCOProgramBuilder * and uses it to build the desired quantum program. The builder will be * properly initialized before calling this function, and the resulting module - * will be finalized and returned after this function completes. + * will be finalized using the returned Values after this function completes. + * @return The module containing the quantum program built by buildFunc. + */ + static OwningOpRef + build(MLIRContext* context, + const function_ref(QCOProgramBuilder&)>& buildFunc); + + /** + * @brief Convenience method for building quantum programs with one return + * value. + * @param context The MLIR context to use for building the program + * @param buildFunc A function that takes a reference to a QCOProgramBuilder + * and returns the single result value of the desired quantum program. * @return The module containing the quantum program built by buildFunc. */ static OwningOpRef build(MLIRContext* context, - const function_ref& buildFunc); + const function_ref& buildFunc); private: enum class AllocationMode : uint8_t { Unset, Static, Dynamic }; diff --git a/mlir/include/mlir/Dialect/QCO/IR/QCODialect.h b/mlir/include/mlir/Dialect/QCO/IR/QCODialect.h index 1b08a1715a..7c0ea4fab2 100644 --- a/mlir/include/mlir/Dialect/QCO/IR/QCODialect.h +++ b/mlir/include/mlir/Dialect/QCO/IR/QCODialect.h @@ -13,9 +13,6 @@ #include "mlir/Dialect/Utils/Utils.h" #include -#include -#include -#include #include #include #include @@ -142,35 +139,4 @@ template class TargetAndParameterArityTrait { }; }; -/** - * @brief Find the entry point function with entry_point attribute - * - * @details - * Searches for the function marked with the "entry_point" attribute in - * the passthrough attributes. If multiple functions are marked, returns the - * first one encountered. - * - * @param op The module operation to search in. - * @returns the entry point function, or nullptr if not found. - */ -inline func::FuncOp getEntryPoint(ModuleOp op) { - static constexpr StringRef PASSTHROUGH_LABEL = "passthrough"; - static constexpr StringRef ENTRY_POINT_LABEL = "entry_point"; - - const auto isEntry = [](Attribute attr) { - const auto strAttr = dyn_cast(attr); - return strAttr && strAttr.getValue() == ENTRY_POINT_LABEL; - }; - - for (auto func : op.getOps()) { - if (const auto passthrough = - func->getAttrOfType(PASSTHROUGH_LABEL); - passthrough && llvm::any_of(passthrough, isEntry)) { - return func; - } - } - - return nullptr; -} - } // namespace mlir::qco diff --git a/mlir/include/mlir/Dialect/QCO/IR/QCOOps.td b/mlir/include/mlir/Dialect/QCO/IR/QCOOps.td index f75e21e919..a3ee7b405c 100644 --- a/mlir/include/mlir/Dialect/QCO/IR/QCOOps.td +++ b/mlir/include/mlir/Dialect/QCO/IR/QCOOps.td @@ -96,7 +96,7 @@ def StaticOp : QCOOp<"static", [Pure]> { // Measurement and Reset Operations //===----------------------------------------------------------------------===// -def MeasureOp : QCOOp<"measure"> { +def MeasureOp : QCOOp<"measure", [Pure]> { let summary = "Measure a qubit in the computational basis"; let description = [{ Measures a qubit in the computational (Z) basis, collapsing the state @@ -119,8 +119,7 @@ def MeasureOp : QCOOp<"measure"> { ``` }]; - let arguments = (ins Arg:$qubit_in, + let arguments = (ins Arg:$qubit_in, OptionalAttr:$register_name, OptionalAttr>:$register_size, OptionalAttr>:$register_index); @@ -138,7 +137,7 @@ def MeasureOp : QCOOp<"measure"> { let hasVerifier = 1; } -def ResetOp : QCOOp<"reset", [Idempotent, SameOperandsAndResultType]> { +def ResetOp : QCOOp<"reset", [Idempotent, SameOperandsAndResultType, Pure]> { let summary = "Reset a qubit to |0⟩ state"; let description = [{ Resets a qubit to the |0⟩ state, regardless of its current state, @@ -150,8 +149,7 @@ def ResetOp : QCOOp<"reset", [Idempotent, SameOperandsAndResultType]> { ``` }]; - let arguments = - (ins Arg:$qubit_in); + let arguments = (ins Arg:$qubit_in); let results = (outs QubitType:$qubit_out); let assemblyFormat = "$qubit_in attr-dict `:` type($qubit_in) `->` type($qubit_out)"; @@ -209,7 +207,8 @@ def GPhaseOp let hasCanonicalizer = 1; } -def IdOp : QCOOp<"id", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { +def IdOp + : QCOOp<"id", traits = [UnitaryOpInterface, OneTargetZeroParameter, Pure]> { let summary = "Apply an Id gate to a qubit"; let description = [{ Applies an Id gate to a qubit and returns the transformed qubit. @@ -220,7 +219,7 @@ def IdOp : QCOOp<"id", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { ``` }]; - let arguments = (ins Arg:$qubit_in); + let arguments = (ins Arg:$qubit_in); let results = (outs QubitType:$qubit_out); let assemblyFormat = "$qubit_in attr-dict `:` type($qubit_in) `->` type($qubit_out)"; @@ -233,7 +232,8 @@ def IdOp : QCOOp<"id", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { let hasCanonicalizer = 1; } -def XOp : QCOOp<"x", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { +def XOp + : QCOOp<"x", traits = [UnitaryOpInterface, OneTargetZeroParameter, Pure]> { let summary = "Apply an X gate to a qubit"; let description = [{ Applies an X gate to a qubit and returns the transformed qubit. @@ -244,7 +244,7 @@ def XOp : QCOOp<"x", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { ``` }]; - let arguments = (ins Arg:$qubit_in); + let arguments = (ins Arg:$qubit_in); let results = (outs QubitType:$qubit_out); let assemblyFormat = "$qubit_in attr-dict `:` type($qubit_in) `->` type($qubit_out)"; @@ -257,7 +257,8 @@ def XOp : QCOOp<"x", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { let hasCanonicalizer = 1; } -def YOp : QCOOp<"y", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { +def YOp + : QCOOp<"y", traits = [UnitaryOpInterface, OneTargetZeroParameter, Pure]> { let summary = "Apply a Y gate to a qubit"; let description = [{ Applies a Y gate to a qubit and returns the transformed qubit. @@ -268,7 +269,7 @@ def YOp : QCOOp<"y", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { ``` }]; - let arguments = (ins Arg:$qubit_in); + let arguments = (ins Arg:$qubit_in); let results = (outs QubitType:$qubit_out); let assemblyFormat = "$qubit_in attr-dict `:` type($qubit_in) `->` type($qubit_out)"; @@ -281,7 +282,8 @@ def YOp : QCOOp<"y", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { let hasCanonicalizer = 1; } -def ZOp : QCOOp<"z", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { +def ZOp + : QCOOp<"z", traits = [UnitaryOpInterface, OneTargetZeroParameter, Pure]> { let summary = "Apply a Z gate to a qubit"; let description = [{ Applies a Z gate to a qubit and returns the transformed qubit. @@ -292,7 +294,7 @@ def ZOp : QCOOp<"z", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { ``` }]; - let arguments = (ins Arg:$qubit_in); + let arguments = (ins Arg:$qubit_in); let results = (outs QubitType:$qubit_out); let assemblyFormat = "$qubit_in attr-dict `:` type($qubit_in) `->` type($qubit_out)"; @@ -305,7 +307,8 @@ def ZOp : QCOOp<"z", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { let hasCanonicalizer = 1; } -def HOp : QCOOp<"h", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { +def HOp + : QCOOp<"h", traits = [UnitaryOpInterface, OneTargetZeroParameter, Pure]> { let summary = "Apply a H gate to a qubit"; let description = [{ Applies a H gate to a qubit and returns the transformed qubit. @@ -316,7 +319,7 @@ def HOp : QCOOp<"h", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { ``` }]; - let arguments = (ins Arg:$qubit_in); + let arguments = (ins Arg:$qubit_in); let results = (outs QubitType:$qubit_out); let assemblyFormat = "$qubit_in attr-dict `:` type($qubit_in) `->` type($qubit_out)"; @@ -329,7 +332,8 @@ def HOp : QCOOp<"h", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { let hasCanonicalizer = 1; } -def SOp : QCOOp<"s", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { +def SOp + : QCOOp<"s", traits = [UnitaryOpInterface, OneTargetZeroParameter, Pure]> { let summary = "Apply an S gate to a qubit"; let description = [{ Applies an S gate to a qubit and returns the transformed qubit. @@ -340,7 +344,7 @@ def SOp : QCOOp<"s", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { ``` }]; - let arguments = (ins Arg:$qubit_in); + let arguments = (ins Arg:$qubit_in); let results = (outs QubitType:$qubit_out); let assemblyFormat = "$qubit_in attr-dict `:` type($qubit_in) `->` type($qubit_out)"; @@ -353,8 +357,8 @@ def SOp : QCOOp<"s", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { let hasCanonicalizer = 1; } -def SdgOp - : QCOOp<"sdg", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { +def SdgOp : QCOOp<"sdg", + traits = [UnitaryOpInterface, OneTargetZeroParameter, Pure]> { let summary = "Apply an Sdg gate to a qubit"; let description = [{ Applies an Sdg gate to a qubit and returns the transformed qubit. @@ -365,7 +369,7 @@ def SdgOp ``` }]; - let arguments = (ins Arg:$qubit_in); + let arguments = (ins Arg:$qubit_in); let results = (outs QubitType:$qubit_out); let assemblyFormat = "$qubit_in attr-dict `:` type($qubit_in) `->` type($qubit_out)"; @@ -378,7 +382,8 @@ def SdgOp let hasCanonicalizer = 1; } -def TOp : QCOOp<"t", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { +def TOp + : QCOOp<"t", traits = [UnitaryOpInterface, OneTargetZeroParameter, Pure]> { let summary = "Apply a T gate to a qubit"; let description = [{ Applies a T gate to a qubit and returns the transformed qubit. @@ -389,7 +394,7 @@ def TOp : QCOOp<"t", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { ``` }]; - let arguments = (ins Arg:$qubit_in); + let arguments = (ins Arg:$qubit_in); let results = (outs QubitType:$qubit_out); let assemblyFormat = "$qubit_in attr-dict `:` type($qubit_in) `->` type($qubit_out)"; @@ -402,8 +407,8 @@ def TOp : QCOOp<"t", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { let hasCanonicalizer = 1; } -def TdgOp - : QCOOp<"tdg", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { +def TdgOp : QCOOp<"tdg", + traits = [UnitaryOpInterface, OneTargetZeroParameter, Pure]> { let summary = "Apply a Tdg gate to a qubit"; let description = [{ Applies a Tdg gate to a qubit and returns the transformed qubit. @@ -414,7 +419,7 @@ def TdgOp ``` }]; - let arguments = (ins Arg:$qubit_in); + let arguments = (ins Arg:$qubit_in); let results = (outs QubitType:$qubit_out); let assemblyFormat = "$qubit_in attr-dict `:` type($qubit_in) `->` type($qubit_out)"; @@ -427,7 +432,8 @@ def TdgOp let hasCanonicalizer = 1; } -def SXOp : QCOOp<"sx", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { +def SXOp + : QCOOp<"sx", traits = [UnitaryOpInterface, OneTargetZeroParameter, Pure]> { let summary = "Apply an SX gate to a qubit"; let description = [{ Applies an SX gate to a qubit and returns the transformed qubit. @@ -438,7 +444,7 @@ def SXOp : QCOOp<"sx", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { ``` }]; - let arguments = (ins Arg:$qubit_in); + let arguments = (ins Arg:$qubit_in); let results = (outs QubitType:$qubit_out); let assemblyFormat = "$qubit_in attr-dict `:` type($qubit_in) `->` type($qubit_out)"; @@ -451,8 +457,8 @@ def SXOp : QCOOp<"sx", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { let hasCanonicalizer = 1; } -def SXdgOp - : QCOOp<"sxdg", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { +def SXdgOp : QCOOp<"sxdg", traits = [UnitaryOpInterface, OneTargetZeroParameter, + Pure]> { let summary = "Apply an SXdg gate to a qubit"; let description = [{ Applies an SXdg gate to a qubit and returns the transformed qubit. @@ -463,7 +469,7 @@ def SXdgOp ``` }]; - let arguments = (ins Arg:$qubit_in); + let arguments = (ins Arg:$qubit_in); let results = (outs QubitType:$qubit_out); let assemblyFormat = "$qubit_in attr-dict `:` type($qubit_in) `->` type($qubit_out)"; @@ -476,7 +482,8 @@ def SXdgOp let hasCanonicalizer = 1; } -def RXOp : QCOOp<"rx", traits = [UnitaryOpInterface, OneTargetOneParameter]> { +def RXOp + : QCOOp<"rx", traits = [UnitaryOpInterface, OneTargetOneParameter, Pure]> { let summary = "Apply an RX gate to a qubit"; let description = [{ Applies an RX gate to a qubit and returns the transformed qubit. @@ -487,7 +494,7 @@ def RXOp : QCOOp<"rx", traits = [UnitaryOpInterface, OneTargetOneParameter]> { ``` }]; - let arguments = (ins Arg:$qubit_in, + let arguments = (ins Arg:$qubit_in, Arg:$theta); let results = (outs QubitType:$qubit_out); let assemblyFormat = "`(` $theta `)` $qubit_in attr-dict `:` type($qubit_in) " @@ -506,7 +513,8 @@ def RXOp : QCOOp<"rx", traits = [UnitaryOpInterface, OneTargetOneParameter]> { let hasCanonicalizer = 1; } -def RYOp : QCOOp<"ry", traits = [UnitaryOpInterface, OneTargetOneParameter]> { +def RYOp + : QCOOp<"ry", traits = [UnitaryOpInterface, OneTargetOneParameter, Pure]> { let summary = "Apply an RY gate to a qubit"; let description = [{ Applies an RY gate to a qubit and returns the transformed qubit. @@ -517,7 +525,7 @@ def RYOp : QCOOp<"ry", traits = [UnitaryOpInterface, OneTargetOneParameter]> { ``` }]; - let arguments = (ins Arg:$qubit_in, + let arguments = (ins Arg:$qubit_in, Arg:$theta); let results = (outs QubitType:$qubit_out); let assemblyFormat = "`(` $theta `)` $qubit_in attr-dict `:` type($qubit_in) " @@ -536,7 +544,8 @@ def RYOp : QCOOp<"ry", traits = [UnitaryOpInterface, OneTargetOneParameter]> { let hasCanonicalizer = 1; } -def RZOp : QCOOp<"rz", traits = [UnitaryOpInterface, OneTargetOneParameter]> { +def RZOp + : QCOOp<"rz", traits = [UnitaryOpInterface, OneTargetOneParameter, Pure]> { let summary = "Apply an RZ gate to a qubit"; let description = [{ Applies an RZ gate to a qubit and returns the transformed qubit. @@ -547,7 +556,7 @@ def RZOp : QCOOp<"rz", traits = [UnitaryOpInterface, OneTargetOneParameter]> { ``` }]; - let arguments = (ins Arg:$qubit_in, + let arguments = (ins Arg:$qubit_in, Arg:$theta); let results = (outs QubitType:$qubit_out); let assemblyFormat = "`(` $theta `)` $qubit_in attr-dict `:` type($qubit_in) " @@ -566,7 +575,8 @@ def RZOp : QCOOp<"rz", traits = [UnitaryOpInterface, OneTargetOneParameter]> { let hasCanonicalizer = 1; } -def POp : QCOOp<"p", traits = [UnitaryOpInterface, OneTargetOneParameter]> { +def POp + : QCOOp<"p", traits = [UnitaryOpInterface, OneTargetOneParameter, Pure]> { let summary = "Apply a P gate to a qubit"; let description = [{ Applies a P gate to a qubit and returns the transformed qubit. @@ -577,7 +587,7 @@ def POp : QCOOp<"p", traits = [UnitaryOpInterface, OneTargetOneParameter]> { ``` }]; - let arguments = (ins Arg:$qubit_in, + let arguments = (ins Arg:$qubit_in, Arg:$theta); let results = (outs QubitType:$qubit_out); let assemblyFormat = "`(` $theta `)` $qubit_in attr-dict `:` type($qubit_in) " @@ -596,7 +606,8 @@ def POp : QCOOp<"p", traits = [UnitaryOpInterface, OneTargetOneParameter]> { let hasCanonicalizer = 1; } -def ROp : QCOOp<"r", traits = [UnitaryOpInterface, OneTargetTwoParameter]> { +def ROp + : QCOOp<"r", traits = [UnitaryOpInterface, OneTargetTwoParameter, Pure]> { let summary = "Apply an R gate to a qubit"; let description = [{ Applies an R gate to a qubit and returns the transformed qubit. @@ -607,7 +618,7 @@ def ROp : QCOOp<"r", traits = [UnitaryOpInterface, OneTargetTwoParameter]> { ``` }]; - let arguments = (ins Arg:$qubit_in, + let arguments = (ins Arg:$qubit_in, Arg:$theta, Arg:$phi); let results = (outs QubitType:$qubit_out); @@ -627,7 +638,8 @@ def ROp : QCOOp<"r", traits = [UnitaryOpInterface, OneTargetTwoParameter]> { let hasCanonicalizer = 1; } -def U2Op : QCOOp<"u2", traits = [UnitaryOpInterface, OneTargetTwoParameter]> { +def U2Op + : QCOOp<"u2", traits = [UnitaryOpInterface, OneTargetTwoParameter, Pure]> { let summary = "Apply a U2 gate to a qubit"; let description = [{ Applies a U2 gate to a qubit and returns the transformed qubit. @@ -638,7 +650,7 @@ def U2Op : QCOOp<"u2", traits = [UnitaryOpInterface, OneTargetTwoParameter]> { ``` }]; - let arguments = (ins Arg:$qubit_in, + let arguments = (ins Arg:$qubit_in, Arg:$phi, Arg:$lambda); let results = (outs QubitType:$qubit_out); @@ -658,7 +670,8 @@ def U2Op : QCOOp<"u2", traits = [UnitaryOpInterface, OneTargetTwoParameter]> { let hasCanonicalizer = 1; } -def UOp : QCOOp<"u", traits = [UnitaryOpInterface, OneTargetThreeParameter]> { +def UOp + : QCOOp<"u", traits = [UnitaryOpInterface, OneTargetThreeParameter, Pure]> { let summary = "Apply a U gate to a qubit"; let description = [{ Applies a U gate to a qubit and returns the transformed qubit. @@ -669,7 +682,7 @@ def UOp : QCOOp<"u", traits = [UnitaryOpInterface, OneTargetThreeParameter]> { ``` }]; - let arguments = (ins Arg:$qubit_in, + let arguments = (ins Arg:$qubit_in, Arg:$theta, Arg:$phi, Arg:$lambda); @@ -692,8 +705,8 @@ def UOp : QCOOp<"u", traits = [UnitaryOpInterface, OneTargetThreeParameter]> { let hasCanonicalizer = 1; } -def SWAPOp - : QCOOp<"swap", traits = [UnitaryOpInterface, TwoTargetZeroParameter]> { +def SWAPOp : QCOOp<"swap", traits = [UnitaryOpInterface, TwoTargetZeroParameter, + Pure]> { let summary = "Apply a SWAP gate to two qubits"; let description = [{ Applies a SWAP gate to two qubits and returns the transformed qubits. @@ -704,9 +717,8 @@ def SWAPOp ``` }]; - let arguments = - (ins Arg:$qubit0_in, - Arg:$qubit1_in); + let arguments = (ins Arg:$qubit0_in, + Arg:$qubit1_in); let results = (outs QubitType:$qubit0_out, QubitType:$qubit1_out); let assemblyFormat = "$qubit0_in `,` $qubit1_in attr-dict `:` type($qubit0_in) `,` " @@ -720,8 +732,8 @@ def SWAPOp let hasCanonicalizer = 1; } -def iSWAPOp - : QCOOp<"iswap", traits = [UnitaryOpInterface, TwoTargetZeroParameter]> { +def iSWAPOp : QCOOp<"iswap", traits = [UnitaryOpInterface, + TwoTargetZeroParameter, Pure]> { let summary = "Apply a iSWAP gate to two qubits"; let description = [{ Applies a iSWAP gate to two qubits and returns the transformed qubits. @@ -732,9 +744,8 @@ def iSWAPOp ``` }]; - let arguments = - (ins Arg:$qubit0_in, - Arg:$qubit1_in); + let arguments = (ins Arg:$qubit0_in, + Arg:$qubit1_in); let results = (outs QubitType:$qubit0_out, QubitType:$qubit1_out); let assemblyFormat = "$qubit0_in `,` $qubit1_in attr-dict `:` type($qubit0_in) `,` " @@ -746,8 +757,8 @@ def iSWAPOp }]; } -def DCXOp - : QCOOp<"dcx", traits = [UnitaryOpInterface, TwoTargetZeroParameter]> { +def DCXOp : QCOOp<"dcx", + traits = [UnitaryOpInterface, TwoTargetZeroParameter, Pure]> { let summary = "Apply a DCX gate to two qubits"; let description = [{ Applies a DCX gate to two qubits and returns the transformed qubits. @@ -758,9 +769,8 @@ def DCXOp ``` }]; - let arguments = - (ins Arg:$qubit0_in, - Arg:$qubit1_in); + let arguments = (ins Arg:$qubit0_in, + Arg:$qubit1_in); let results = (outs QubitType:$qubit0_out, QubitType:$qubit1_out); let assemblyFormat = "$qubit0_in `,` $qubit1_in attr-dict `:` type($qubit0_in) `,` " @@ -774,8 +784,8 @@ def DCXOp let hasCanonicalizer = 1; } -def ECROp - : QCOOp<"ecr", traits = [UnitaryOpInterface, TwoTargetZeroParameter]> { +def ECROp : QCOOp<"ecr", + traits = [UnitaryOpInterface, TwoTargetZeroParameter, Pure]> { let summary = "Apply an ECR gate to two qubits"; let description = [{ Applies an ECR gate to two qubits and returns the transformed qubits. @@ -786,9 +796,8 @@ def ECROp ``` }]; - let arguments = - (ins Arg:$qubit0_in, - Arg:$qubit1_in); + let arguments = (ins Arg:$qubit0_in, + Arg:$qubit1_in); let results = (outs QubitType:$qubit0_out, QubitType:$qubit1_out); let assemblyFormat = "$qubit0_in `,` $qubit1_in attr-dict `:` type($qubit0_in) `,` " @@ -802,7 +811,8 @@ def ECROp let hasCanonicalizer = 1; } -def RXXOp : QCOOp<"rxx", traits = [UnitaryOpInterface, TwoTargetOneParameter]> { +def RXXOp + : QCOOp<"rxx", traits = [UnitaryOpInterface, TwoTargetOneParameter, Pure]> { let summary = "Apply an RXX gate to two qubits"; let description = [{ Applies an RXX gate to two qubits and returns the transformed qubits. @@ -813,10 +823,9 @@ def RXXOp : QCOOp<"rxx", traits = [UnitaryOpInterface, TwoTargetOneParameter]> { ``` }]; - let arguments = - (ins Arg:$qubit0_in, - Arg:$qubit1_in, - Arg:$theta); + let arguments = (ins Arg:$qubit0_in, + Arg:$qubit1_in, + Arg:$theta); let results = (outs QubitType:$qubit0_out, QubitType:$qubit1_out); let assemblyFormat = "`(` $theta `)` $qubit0_in `,` $qubit1_in attr-dict `:` type($qubit0_in) " @@ -835,7 +844,8 @@ def RXXOp : QCOOp<"rxx", traits = [UnitaryOpInterface, TwoTargetOneParameter]> { let hasCanonicalizer = 1; } -def RYYOp : QCOOp<"ryy", traits = [UnitaryOpInterface, TwoTargetOneParameter]> { +def RYYOp + : QCOOp<"ryy", traits = [UnitaryOpInterface, TwoTargetOneParameter, Pure]> { let summary = "Apply an RYY gate to two qubits"; let description = [{ Applies an RYY gate to two qubits and returns the transformed qubits. @@ -846,10 +856,9 @@ def RYYOp : QCOOp<"ryy", traits = [UnitaryOpInterface, TwoTargetOneParameter]> { ``` }]; - let arguments = - (ins Arg:$qubit0_in, - Arg:$qubit1_in, - Arg:$theta); + let arguments = (ins Arg:$qubit0_in, + Arg:$qubit1_in, + Arg:$theta); let results = (outs QubitType:$qubit0_out, QubitType:$qubit1_out); let assemblyFormat = "`(` $theta `)` $qubit0_in `,` $qubit1_in attr-dict `:` type($qubit0_in) " @@ -868,7 +877,8 @@ def RYYOp : QCOOp<"ryy", traits = [UnitaryOpInterface, TwoTargetOneParameter]> { let hasCanonicalizer = 1; } -def RZXOp : QCOOp<"rzx", traits = [UnitaryOpInterface, TwoTargetOneParameter]> { +def RZXOp + : QCOOp<"rzx", traits = [UnitaryOpInterface, TwoTargetOneParameter, Pure]> { let summary = "Apply an RZX gate to two qubits"; let description = [{ Applies an RZX gate to two qubits and returns the transformed qubits. @@ -879,10 +889,9 @@ def RZXOp : QCOOp<"rzx", traits = [UnitaryOpInterface, TwoTargetOneParameter]> { ``` }]; - let arguments = - (ins Arg:$qubit0_in, - Arg:$qubit1_in, - Arg:$theta); + let arguments = (ins Arg:$qubit0_in, + Arg:$qubit1_in, + Arg:$theta); let results = (outs QubitType:$qubit0_out, QubitType:$qubit1_out); let assemblyFormat = "`(` $theta `)` $qubit0_in `,` $qubit1_in attr-dict `:` type($qubit0_in) " @@ -901,7 +910,8 @@ def RZXOp : QCOOp<"rzx", traits = [UnitaryOpInterface, TwoTargetOneParameter]> { let hasCanonicalizer = 1; } -def RZZOp : QCOOp<"rzz", traits = [UnitaryOpInterface, TwoTargetOneParameter]> { +def RZZOp + : QCOOp<"rzz", traits = [UnitaryOpInterface, TwoTargetOneParameter, Pure]> { let summary = "Apply an RZZ gate to two qubits"; let description = [{ Applies an RZZ gate to two qubits and returns the transformed qubits. @@ -912,10 +922,9 @@ def RZZOp : QCOOp<"rzz", traits = [UnitaryOpInterface, TwoTargetOneParameter]> { ``` }]; - let arguments = - (ins Arg:$qubit0_in, - Arg:$qubit1_in, - Arg:$theta); + let arguments = (ins Arg:$qubit0_in, + Arg:$qubit1_in, + Arg:$theta); let results = (outs QubitType:$qubit0_out, QubitType:$qubit1_out); let assemblyFormat = "`(` $theta `)` $qubit0_in `,` $qubit1_in attr-dict `:` type($qubit0_in) " @@ -934,8 +943,8 @@ def RZZOp : QCOOp<"rzz", traits = [UnitaryOpInterface, TwoTargetOneParameter]> { let hasCanonicalizer = 1; } -def XXPlusYYOp : QCOOp<"xx_plus_yy", - traits = [UnitaryOpInterface, TwoTargetTwoParameter]> { +def XXPlusYYOp : QCOOp<"xx_plus_yy", traits = [UnitaryOpInterface, + TwoTargetTwoParameter, Pure]> { let summary = "Apply an XX+YY gate to two qubits"; let description = [{ Applies an XX+YY gate to two qubits and returns the transformed qubits. @@ -946,11 +955,10 @@ def XXPlusYYOp : QCOOp<"xx_plus_yy", ``` }]; - let arguments = - (ins Arg:$qubit0_in, - Arg:$qubit1_in, - Arg:$theta, - Arg:$beta); + let arguments = (ins Arg:$qubit0_in, + Arg:$qubit1_in, + Arg:$theta, + Arg:$beta); let results = (outs QubitType:$qubit0_out, QubitType:$qubit1_out); let assemblyFormat = "`(` $theta `,` $beta `)` $qubit0_in `,` $qubit1_in " "attr-dict `:` type($qubit0_in) `,` type($qubit1_in) " @@ -970,8 +978,8 @@ def XXPlusYYOp : QCOOp<"xx_plus_yy", let hasCanonicalizer = 1; } -def XXMinusYYOp : QCOOp<"xx_minus_yy", - traits = [UnitaryOpInterface, TwoTargetTwoParameter]> { +def XXMinusYYOp : QCOOp<"xx_minus_yy", traits = [UnitaryOpInterface, + TwoTargetTwoParameter, Pure]> { let summary = "Apply an XX-YY gate to two qubits"; let description = [{ Applies an XX-YY gate to two qubits and returns the transformed qubits. @@ -982,11 +990,10 @@ def XXMinusYYOp : QCOOp<"xx_minus_yy", ``` }]; - let arguments = - (ins Arg:$qubit0_in, - Arg:$qubit1_in, - Arg:$theta, - Arg:$beta); + let arguments = (ins Arg:$qubit0_in, + Arg:$qubit1_in, + Arg:$theta, + Arg:$beta); let results = (outs QubitType:$qubit0_out, QubitType:$qubit1_out); let assemblyFormat = "`(` $theta `,` $beta `)` $qubit0_in `,` $qubit1_in " "attr-dict `:` type($qubit0_in) `,` type($qubit1_in) " @@ -1006,7 +1013,7 @@ def XXMinusYYOp : QCOOp<"xx_minus_yy", let hasCanonicalizer = 1; } -def BarrierOp : QCOOp<"barrier", traits = [UnitaryOpInterface]> { +def BarrierOp : QCOOp<"barrier", traits = [UnitaryOpInterface, Pure]> { let summary = "Apply a barrier gate to a set of qubits"; let description = [{ Applies a barrier gate to a set of qubits and returns the transformed qubits. @@ -1018,7 +1025,7 @@ def BarrierOp : QCOOp<"barrier", traits = [UnitaryOpInterface]> { }]; let arguments = - (ins Arg, "the target qubits", [MemRead]>:$qubits_in); + (ins Arg, "the target qubits">:$qubits_in); let results = (outs Variadic:$qubits_out); let assemblyFormat = "$qubits_in attr-dict `:` type($qubits_in) `->` type($qubits_out)"; @@ -1058,7 +1065,7 @@ def BarrierOp : QCOOp<"barrier", traits = [UnitaryOpInterface]> { // Modifiers //===----------------------------------------------------------------------===// -def YieldOp : QCOOp<"yield", traits = [Terminator, ReturnLike]> { +def YieldOp : QCOOp<"yield", traits = [Terminator, ReturnLike, Pure]> { let summary = "Yield from a modifier region"; let description = [{ Terminates a modifier region, yielding the transformed target qubit and qtensor values back to the enclosing modifier operation. @@ -1081,7 +1088,7 @@ def CtrlOp : QCOOp<"ctrl", traits = [UnitaryOpInterface, AttrSizedOperandSegments, AttrSizedResultSegments, SameOperandsAndResultType, SameOperandsAndResultShape, - SingleBlockImplicitTerminator<"YieldOp">, + SingleBlockImplicitTerminator<"YieldOp">, Pure, RecursiveMemoryEffects]> { let summary = "Add control qubits to a unitary operation"; let description = [{ @@ -1103,9 +1110,9 @@ def CtrlOp : QCOOp<"ctrl", ``` }]; - let arguments = (ins Arg, - "the control qubits", [MemRead]>:$controls_in, - Arg, "the target qubits", [MemRead]>:$targets_in); + let arguments = + (ins Arg, "the control qubits">:$controls_in, + Arg, "the target qubits">:$targets_in); let results = (outs Variadic:$controls_out, Variadic:$targets_out); let regions = (region SizedRegion<1>:$region); @@ -1147,13 +1154,17 @@ def CtrlOp : QCOOp<"ctrl", [[nodiscard]] std::optional getUnitaryMatrix(); }]; - let builders = [OpBuilder<(ins "ValueRange":$controls, "ValueRange":$targets), - [{ - build($_builder, $_state, controls.getTypes(), targets.getTypes(), controls, targets); - }]>, - OpBuilder<(ins "ValueRange":$controls, "ValueRange":$targets, - "function_ref(ValueRange)" - ">":$bodyBuilder)>]; + let builders = + [OpBuilder<(ins "ValueRange":$controls, "ValueRange":$targets), [{ + build($_builder, $_state, controls.getTypes(), targets.getTypes(), + controls, targets); + }]>, + OpBuilder<(ins "ValueRange":$controls, "ValueRange":$targets, + "function_ref(ValueRange)>":$bodyBuilder)>, + OpBuilder<(ins "ValueRange":$controls, "Value":$target, + "function_ref":$bodyBuilder)>, + OpBuilder<(ins "Value":$control, "Value":$target, + "function_ref":$bodyBuilder)>]; let hasCanonicalizer = 1; let hasVerifier = 1; @@ -1161,7 +1172,7 @@ def CtrlOp : QCOOp<"ctrl", def InvOp : QCOOp<"inv", traits = [UnitaryOpInterface, SingleBlockImplicitTerminator<"YieldOp">, - RecursiveMemoryEffects]> { + Pure, RecursiveMemoryEffects]> { let summary = "Invert a unitary operation"; let description = [{ A modifier operation that inverts the unitary operation defined in its body region. @@ -1180,9 +1191,8 @@ def InvOp : QCOOp<"inv", traits = [UnitaryOpInterface, ``` }]; - let arguments = - (ins Arg, - "the qubits involved in the operation", [MemRead]>:$qubits_in); + let arguments = (ins Arg, + "the qubits involved in the operation">:$qubits_in); let results = (outs Variadic:$qubits_out); let regions = (region SizedRegion<1>:$region); let assemblyFormat = [{ @@ -1221,12 +1231,14 @@ def InvOp : QCOOp<"inv", traits = [UnitaryOpInterface, [[nodiscard]] std::optional getUnitaryMatrix(); }]; - let builders = [OpBuilder<(ins "ValueRange":$qubits), [{ - build($_builder, $_state, qubits.getTypes(), qubits); - }]>, - OpBuilder<(ins "ValueRange":$qubits, - "function_ref(ValueRange)" - ">":$bodyBuilder)>]; + let builders = + [OpBuilder<(ins "ValueRange":$qubits), [{ + build($_builder, $_state, qubits.getTypes(), qubits); + }]>, + OpBuilder<(ins "ValueRange":$qubits, + "function_ref(ValueRange)>":$bodyBuilder)>, + OpBuilder<(ins "Value":$qubit, + "function_ref":$bodyBuilder)>]; let hasCanonicalizer = 1; let hasVerifier = 1; @@ -1243,7 +1255,7 @@ def IfOp RegionBranchOpInterface, ["getNumRegionInvocations", "getRegionInvocationBounds", "getEntrySuccessorRegions"]>, - SingleBlock, SingleBlockImplicitTerminator<"YieldOp">, + SingleBlock, SingleBlockImplicitTerminator<"YieldOp">, Pure, RecursiveMemoryEffects]> { let summary = "If-then-else operation for linear (qubit) types"; @@ -1319,6 +1331,13 @@ def IfOp /// Return the yielded value that corresponds to the given argument /// for the else-block, or `nullptr` on failure. OpOperand* getTiedElseYieldedValue(BlockArgument bbArg); + + /// Append the specified additional "qubit" operands: replace this + /// if-op with a new if-op that has the additional qubit operands. + /// The operands can be of qubit or qtensor type. + /// The branch bodies of this if-op are moved over to the new if-op. + /// The newly added qubits are yielded from each branch. + IfOp replaceWithAdditionalQubits(RewriterBase& rewriter, ValueRange addons); }]; let extraClassDefinition = [{ diff --git a/mlir/include/mlir/Dialect/QCO/QCOUtils.h b/mlir/include/mlir/Dialect/QCO/QCOUtils.h index f08db99293..cf781c0b52 100644 --- a/mlir/include/mlir/Dialect/QCO/QCOUtils.h +++ b/mlir/include/mlir/Dialect/QCO/QCOUtils.h @@ -20,18 +20,25 @@ #include #include +#include #include namespace mlir::qco { +/// Maximum number of modifier targets supported by @ref +/// composeBodyMatrix. +inline constexpr size_t kMaxModifierTargetQubits = 10; + /** - * @brief Composes compile-time single-qubit unitaries in a modifier body. + * @brief Composes compile-time unitaries in a modifier body on @p numTargets + * wires. * - * @return The composed 2x2 target unitary in program order, or `std::nullopt` - * when the body cannot be composed. + * @details Block arguments map to wire indices `0..numTargets-1` (MSB-first, + * matching @ref Matrix2x2::embedInNqubit). Returns the composed unitary in + * program order, or `std::nullopt` when the body cannot be composed. */ -[[nodiscard]] std::optional -composeSingleQubitBodyMatrix(Block& block); +[[nodiscard]] std::optional composeBodyMatrix(Block& block, + size_t numTargets); /** * @brief Check whether two parameter values match. diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h b/mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h index 0eead57bd9..544f1872c7 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h @@ -11,7 +11,6 @@ #pragma once #include "mlir/Dialect/QCO/Transforms/Passes.h" -#include "mlir/Dialect/QCO/Utils/Algorithms.h" #include #include @@ -25,7 +24,8 @@ namespace mlir::qco { * @brief Create a mapping pass instance with the given target architecture. * @returns a pass object. */ -std::unique_ptr createMappingPass(size_t nqubits, const Edges& coupling, - MappingPassOptions options); +std::unique_ptr +createMappingPass(const llvm::DenseSet>&, + MappingPassOptions); } // namespace mlir::qco diff --git a/mlir/include/mlir/Dialect/QCO/Utils/Algorithms.h b/mlir/include/mlir/Dialect/QCO/Utils/Algorithms.h deleted file mode 100644 index 77c57585f3..0000000000 --- a/mlir/include/mlir/Dialect/QCO/Utils/Algorithms.h +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#pragma once - -#include -#include - -#include -#include - -namespace mlir::qco { - -using Matrix = SmallVector, 0>; -using Edges = llvm::DenseSet>; - -/** - * @brief Find all shortest paths between two nodes in a graph. - * @details Has a time complexity of O(n^3). - * - * @link Adapted from https://en.wikipedia.org/wiki/Floyd–Warshall_algorithm - * - * @param n The number of nodes in the graph. - * @param edges The set of edges (i, j). - * - * @returns The distance matrix dist, where dist[i, j] is defined as the - * distance between node i and j. - */ -Matrix findAllShortestPaths(size_t n, const Edges& edges); - -} // namespace mlir::qco diff --git a/mlir/include/mlir/Dialect/QCO/Utils/Drivers.h b/mlir/include/mlir/Dialect/QCO/Utils/Drivers.h index 3d360ebb78..f359e3194c 100644 --- a/mlir/include/mlir/Dialect/QCO/Utils/Drivers.h +++ b/mlir/include/mlir/Dialect/QCO/Utils/Drivers.h @@ -12,7 +12,6 @@ #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/Utils/Qubits.h" #include "mlir/Dialect/QCO/Utils/WireIterator.h" #include "mlir/Dialect/QTensor/IR/QTensorOps.h" @@ -35,63 +34,6 @@ namespace mlir::qco { -using WalkProgramFn = function_ref; - -/** - * @brief Perform top-down non-recursive walk of all operations within a - * region of a quantum program and apply a callback function. - * @details The signature of the callback function is: - * - * (Operation*, Qubits& q) -> WalkResult - * - * where the Qubits object tracks the front of qubit SSA values. - * Depending on the template parameter, the callback is executed before or after - * updating the Qubits state. - * @param region The targeted region. - * @param fn The callback function. - * @returns success(), if all operations have been visited. - */ -template -LogicalResult walkProgram(Region& region, const WalkProgramFn& fn) { - Qubits qubits; - for (Operation& curr : region.getOps()) { - if constexpr (Order == WalkOrder::PreOrder) { - if (fn(&curr, qubits).wasInterrupted()) { - return failure(); - } - } - - TypeSwitch(&curr) - .template Case( - [&](StaticOp op) { qubits.add(op.getQubit(), op.getIndex()); }) - .template Case([&](AllocOp op) { qubits.add(op.getResult()); }) - .template Case([&](UnitaryOpInterface& op) { - for (const auto& [prevV, nextV] : - llvm::zip(op.getInputQubits(), op.getOutputQubits())) { - const auto prevQ = cast>(prevV); - const auto nextQ = cast>(nextV); - qubits.remap(prevQ, nextQ); - } - }) - .template Case([&](ResetOp op) { - qubits.remap(op.getQubitIn(), op.getQubitOut()); - }) - .template Case([&](MeasureOp op) { - qubits.remap(op.getQubitIn(), op.getQubitOut()); - }) - .template Case( - [&](SinkOp op) { qubits.remove(op.getQubit()); }); - - if constexpr (Order == WalkOrder::PostOrder) { - if (fn(&curr, qubits).wasInterrupted()) { - return failure(); - } - } - } - - return success(); -} - using ReleasedOps = SmallVector; using PendingWiresMap = DenseMap>; diff --git a/mlir/include/mlir/Dialect/QCO/Utils/Graph.h b/mlir/include/mlir/Dialect/QCO/Utils/Graph.h new file mode 100644 index 0000000000..6cd25c8144 --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Utils/Graph.h @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace mlir::qco { + +/// A directed graph. +class Graph { +public: + class DistanceMatrix { + SmallVector data_; + size_t n_{}; + + public: + /// Initialize distance matrix, where all entries are filled with `v`. + explicit DistanceMatrix(size_t n, size_t v) : n_(n), data_(n * n, v) {} + + /// Return the i-th row. + MutableArrayRef operator[](size_t i) { + assert(i < n_ && "row index out of bounds"); + return MutableArrayRef(data_).slice(i * n_, n_); + } + + /// Return the i-th row. + ArrayRef operator[](size_t i) const { + assert(i < n_ && "row index out of bounds"); + return ArrayRef(data_).slice(i * n_, n_); + } + }; + + /// Construct an empty graph. + Graph() = default; + + /// Construct graph from node identifiers. + explicit Graph(ArrayRef nodes) { + for_each(nodes, [this](const auto u) { std::ignore = adj_[u]; }); + } + + /// Construct graph from edge set. + explicit Graph(const llvm::DenseSet>& edges) { + for_each(edges, [this](const auto& e) { addEdge(e.first, e.second); }); + } + + /// Add a directed edge to the internal representation of the graph. + /// Implicitly adds nodes. + void addEdge(size_t u, size_t v); + + /// Return the neighbours of a node. + [[nodiscard]] ArrayRef getNeighbours(size_t id) const; + + /// Return the nodes. + [[nodiscard]] SmallVector getNodes() const; + + /// Return the number of nodes. + [[nodiscard]] size_t getNumNodes() const { return adj_.size(); } + + /// Return the degree of a node. + [[nodiscard]] size_t getDegree(const size_t id) const { + return adj_.at(id).size(); + } + + /// Return the max degree of the graph. + [[nodiscard]] size_t getMaxDegree() const; + + /// Return true if the graph has no nodes and edges. + [[nodiscard]] bool empty() const { return adj_.empty(); } + + /// Clear the graph. + void clear() { adj_.clear(); } + + /// Remove the edges from the graph. Keep the nodes. + void clearEdges(); + + /// Return the minimum distance matrix of the graph by implementing the + /// Floyd-Warshall Algorithm + /// (https://en.wikipedia.org/wiki/Floyd–Warshall_algorithm) where dist[i][j] + /// denotes the distance between i and j. + [[nodiscard]] Graph::DistanceMatrix getDistMatrix() const; + + /// Return cycle in graph or `std::nullopt` if none exists. + /// Implements an iterative depth-first search inspired by LLVM's SCC + /// utilities. For a cycle [A, B, C, A], the function returns [A, B, C]. + [[nodiscard]] std::optional> findCycle() const; + +private: + llvm::DenseMap> adj_; +}; +} // namespace mlir::qco diff --git a/mlir/include/mlir/Dialect/QCO/Utils/Layout.h b/mlir/include/mlir/Dialect/QCO/Utils/Layout.h new file mode 100644 index 0000000000..f4fc4c3530 --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Utils/Layout.h @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include +#include +#include + +#include +#include +#include + +namespace mlir::qco { + +/// A qubit layout that maps program and hardware indices without +/// storing Values. Used for efficient memory usage when Value tracking isn't +/// needed. +/// +/// Note that we use the terminology "hardware" and "program" qubits +/// here, because "virtual" (opposed to physical) and "static" (opposed to +/// dynamic) are C++ keywords. +class Layout { +public: + /// Construct and return a random layout with size `nqubits`. + static Layout random(size_t nqubits, size_t seed); + + /// Insert program:hardware index mapping. + void add(size_t prog, size_t hw); + + /// Lookup and return program index for a hardware index. + [[nodiscard]] size_t getProgramIndex(size_t hw) const; + + /// Lookup and return hardware index for a program index. + [[nodiscard]] size_t getHardwareIndex(size_t prog) const; + + /// Lookup and return multiple hardware indices at once. + template + requires(sizeof...(ProgIndices) > 0) && + ((std::is_convertible_v) && ...) + [[nodiscard]] auto getHardwareIndices(ProgIndices... progs) const { + return std::tuple{getHardwareIndex(static_cast(progs))...}; + } + + /// Lookup and return multiple program indices at once. + template + requires(sizeof...(HwIndices) > 0) && + ((std::is_convertible_v) && ...) + [[nodiscard]] auto getProgramIndices(HwIndices... hws) const { + return std::tuple{getProgramIndex(static_cast(hws))...}; + } + + /// Swap the mapping to program indices of two hardware indices. + void swap(size_t hwA, size_t hwB); + + /// Return the number of qubits managed by the layout. + [[nodiscard]] size_t nqubits() const; + + /// Return the program to hardware mapping. + [[nodiscard]] ArrayRef getProgramToHardware() const; + +protected: + /// Maps a program qubit index to its hardware index. + SmallVector programToHardware_; + /// Maps a hardware qubit index to its program index. + SmallVector hardwareToProgram_; + +private: + explicit Layout(const size_t nqubits) + : programToHardware_(nqubits), hardwareToProgram_(nqubits) {} +}; +} // namespace mlir::qco diff --git a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h index 38816a9c85..cb99ce6fce 100644 --- a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h +++ b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h @@ -62,7 +62,7 @@ struct Matrix1x1 { * @param col Column index (must be `0`). * @return Reference to the sole matrix entry. */ - [[nodiscard]] Complex& operator()(std::size_t row, std::size_t col); + [[nodiscard]] Complex& operator()(size_t row, size_t col); /** * @brief Const element access with `(row, col)` indexing. @@ -70,7 +70,7 @@ struct Matrix1x1 { * @param col Column index (must be `0`). * @return Copy of the sole matrix entry. */ - [[nodiscard]] Complex operator()(std::size_t row, std::size_t col) const; + [[nodiscard]] Complex operator()(size_t row, size_t col) const; /** * @brief Element-wise scaling by a complex scalar. @@ -125,11 +125,11 @@ struct Matrix1x1 { */ struct Matrix2x2 { /// Number of rows. - static constexpr std::size_t K_ROWS = 2; + static constexpr size_t K_ROWS = 2; /// Number of columns. - static constexpr std::size_t K_COLS = 2; + static constexpr size_t K_COLS = 2; /// Total number of stored elements. - static constexpr std::size_t K_SIZE_AT_COMPILE_TIME = 4; + static constexpr size_t K_SIZE_AT_COMPILE_TIME = 4; /// Flat row-major storage of all matrix entries. std::array data{}; @@ -161,7 +161,7 @@ struct Matrix2x2 { * @param col Column index in `[0, K_COLS)`. * @return Reference to the element at `(row, col)`. */ - [[nodiscard]] Complex& operator()(std::size_t row, std::size_t col); + [[nodiscard]] Complex& operator()(size_t row, size_t col); /** * @brief Const element access. @@ -169,7 +169,7 @@ struct Matrix2x2 { * @param col Column index in `[0, K_COLS)`. * @return Copy of the element at `(row, col)`. */ - [[nodiscard]] Complex operator()(std::size_t row, std::size_t col) const; + [[nodiscard]] Complex operator()(size_t row, size_t col) const; /** * @brief Matrix product `*this * rhs`. @@ -271,8 +271,8 @@ struct Matrix2x2 { * @param qubitIndex Wire index to act on. * @return Embedded unitary as a dynamic matrix. */ - [[nodiscard]] DynamicMatrix embedInNqubit(std::size_t numQubits, - std::size_t qubitIndex) const; + [[nodiscard]] DynamicMatrix embedInNqubit(size_t numQubits, + size_t qubitIndex) const; /** * @brief Embed this single-qubit matrix into a two-qubit Hilbert space. @@ -280,7 +280,7 @@ struct Matrix2x2 { * @param qubitIndex Wire index (`0` = high bit / MSB, `1` = low bit). * @return The `4x4` embedded unitary. */ - [[nodiscard]] Matrix4x4 embedInTwoQubit(std::size_t qubitIndex) const; + [[nodiscard]] Matrix4x4 embedInTwoQubit(size_t qubitIndex) const; }; /** @@ -291,11 +291,11 @@ struct Matrix2x2 { */ struct Matrix4x4 { /// Number of rows. - static constexpr std::size_t K_ROWS = 4; + static constexpr size_t K_ROWS = 4; /// Number of columns. - static constexpr std::size_t K_COLS = 4; + static constexpr size_t K_COLS = 4; /// Total number of stored elements. - static constexpr std::size_t K_SIZE_AT_COMPILE_TIME = 16; + static constexpr size_t K_SIZE_AT_COMPILE_TIME = 16; /// Flat row-major storage of all matrix entries. std::array data{}; @@ -350,7 +350,7 @@ struct Matrix4x4 { * @param col Column index in `[0, K_COLS)`. * @return Reference to the element at `(row, col)`. */ - [[nodiscard]] Complex& operator()(std::size_t row, std::size_t col); + [[nodiscard]] Complex& operator()(size_t row, size_t col); /** * @brief Const element access. @@ -358,7 +358,7 @@ struct Matrix4x4 { * @param col Column index in `[0, K_COLS)`. * @return Copy of the element at `(row, col)`. */ - [[nodiscard]] Complex operator()(std::size_t row, std::size_t col) const; + [[nodiscard]] Complex operator()(size_t row, size_t col) const; /** * @brief Matrix product `*this * rhs`. @@ -473,28 +473,28 @@ struct Matrix4x4 { * @param col Column index in `[0, K_COLS)`. * @return Array of the four column entries. */ - [[nodiscard]] std::array column(std::size_t col) const; + [[nodiscard]] std::array column(size_t col) const; /** * @brief Overwrites column @p col with @p values. * @param col Column index in `[0, K_COLS)`. * @param values New column entries, top to bottom; must have length `K_ROWS`. */ - void setColumn(std::size_t col, ArrayRef values); + void setColumn(size_t col, ArrayRef values); /** * @brief Returns the entries of row @p row, left to right. * @param row Row index in `[0, K_ROWS)`. * @return View over the four row entries. */ - [[nodiscard]] ArrayRef row(std::size_t row) const; + [[nodiscard]] ArrayRef row(size_t row) const; /** * @brief Overwrites row @p row with @p values. * @param row Row index in `[0, K_ROWS)`. * @param values New row entries, left to right; must have length `K_COLS`. */ - void setRow(std::size_t row, ArrayRef values); + void setRow(size_t row, ArrayRef values); /** * @brief Returns the element-wise real parts in row-major order. @@ -574,9 +574,8 @@ struct Matrix4x4 { * @param q1Index Wire index of operand 1. * @return Embedded unitary as a dynamic matrix. */ - [[nodiscard]] DynamicMatrix embedInNqubit(std::size_t numQubits, - std::size_t q0Index, - std::size_t q1Index) const; + [[nodiscard]] DynamicMatrix embedInNqubit(size_t numQubits, size_t q0Index, + size_t q1Index) const; /** * @brief Reorder this matrix to act on qubits `{0, 1}`. @@ -584,8 +583,8 @@ struct Matrix4x4 { * @param q0Index Wire index of operand 0; @p q1Index wire index of operand 1. * @return Reordered copy of this matrix. */ - [[nodiscard]] Matrix4x4 reorderForQubits(std::size_t q0Index, - std::size_t q1Index) const; + [[nodiscard]] Matrix4x4 reorderForQubits(size_t q0Index, + size_t q1Index) const; }; /** @@ -604,7 +603,7 @@ class DynamicMatrix { * @brief Creates a zero-initialized square matrix. * @param dim Side length of the square matrix. */ - explicit DynamicMatrix(std::int64_t dim); + explicit DynamicMatrix(int64_t dim); /** * @brief Creates a dynamic matrix from a fixed 2x2 matrix. @@ -634,7 +633,7 @@ class DynamicMatrix { * @param dim Side length of the identity matrix. * @return Identity matrix with ones on the diagonal. */ - [[nodiscard]] static DynamicMatrix identity(std::int64_t dim); + [[nodiscard]] static DynamicMatrix identity(int64_t dim); /** * @brief Creates a dynamic matrix holding the adjoint of a 2x2 matrix. @@ -647,13 +646,13 @@ class DynamicMatrix { * @brief Returns the number of rows. * @return Matrix dimension. */ - [[nodiscard]] std::int64_t rows() const; + [[nodiscard]] int64_t rows() const; /** * @brief Returns the number of columns. * @return Matrix dimension. */ - [[nodiscard]] std::int64_t cols() const; + [[nodiscard]] int64_t cols() const; /** * @brief Mutable element access. @@ -661,7 +660,7 @@ class DynamicMatrix { * @param col Column index in `[0, dim)`. * @return Reference to the element at `(row, col)`. */ - [[nodiscard]] Complex& operator()(std::int64_t row, std::int64_t col); + [[nodiscard]] Complex& operator()(int64_t row, int64_t col); /** * @brief Const element access. @@ -669,7 +668,7 @@ class DynamicMatrix { * @param col Column index in `[0, dim)`. * @return Copy of the element at `(row, col)`. */ - [[nodiscard]] Complex operator()(std::int64_t row, std::int64_t col) const; + [[nodiscard]] Complex operator()(int64_t row, int64_t col) const; /** * @brief Copies a 2x2 block into the bottom-right corner. @@ -784,6 +783,40 @@ class DynamicMatrix { */ [[nodiscard]] DynamicMatrix operator*(const DynamicMatrix& rhs) const; + /** + * @brief Premultiplies by a matrix: `*this = lhs * *this`. + * @param lhs Left-hand factor. + */ + void premultiplyBy(const DynamicMatrix& lhs); + + /** + * @brief Premultiplies by a single-qubit gate embedded on @p qubitIndex. + * + * Uses the same MSB-first wire convention as @ref Matrix2x2::embedInNqubit. + * + * @param gate Single-qubit unitary. + * @param numQubits Number of qubits in this matrix. + * @param qubitIndex Target wire index. + */ + void premultiplyByEmbedded1Q(const Matrix2x2& gate, size_t numQubits, + size_t qubitIndex); + + /** + * @brief Premultiplies by a two-qubit gate embedded on @p q0Index and @p + * q1Index. + * + * @p gate must already be reordered for wires @p q0Index and @p q1Index when + * @p numQubits is 2. Uses the same MSB-first convention as @ref + * Matrix4x4::embedInNqubit. + * + * @param gate Two-qubit unitary. + * @param numQubits Number of qubits in this matrix. + * @param q0Index First target wire index. + * @param q1Index Second target wire index. + */ + void premultiplyByEmbedded2Q(const Matrix4x4& gate, size_t numQubits, + size_t q0Index, size_t q1Index); + /** * @brief Element-wise scaling by a complex scalar. * @param scalar Factor applied to every matrix entry. diff --git a/mlir/include/mlir/Dialect/QCO/Utils/Qubits.h b/mlir/include/mlir/Dialect/QCO/Utils/Qubits.h deleted file mode 100644 index b08952aa46..0000000000 --- a/mlir/include/mlir/Dialect/QCO/Utils/Qubits.h +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#pragma once - -#include "mlir/Dialect/QCO/IR/QCODialect.h" - -#include -#include - -#include -#include -#include - -namespace mlir::qco { -class Qubits { - /** - * @brief Specifies the qubit "location" (hardware or program). - */ - enum class QubitLocation : std::uint8_t { Hardware, Program }; - -public: - /** - * @brief Add qubit with automatically assigned dynamic index. - */ - void add(TypedValue q); - - /** - * @brief Add qubit with static index. - */ - void add(TypedValue q, std::size_t hw); - - /** - * @brief Remap the qubit value from prev to next. - */ - void remap(TypedValue prev, TypedValue next); - - /** - * @brief Remove the qubit value. - */ - void remove(TypedValue q); - - /** - * @returns the qubit value assigned to a program index. - */ - [[nodiscard]] TypedValue getProgramQubit(std::size_t index) const; - - /** - * @returns the qubit value assigned to a hardware index. - */ - [[nodiscard]] TypedValue getHardwareQubit(std::size_t index) const; - - /** - * @returns the index assigned to the qubit value. - */ - [[nodiscard]] std::size_t getIndex(TypedValue q) const; - -private: - DenseMap> programToValue_; - DenseMap> hardwareToValue_; - DenseMap, std::pair> - valueToIndex_; -}; -} // namespace mlir::qco diff --git a/mlir/include/mlir/Dialect/QIR/Builder/QIRProgramBuilder.h b/mlir/include/mlir/Dialect/QIR/Builder/QIRProgramBuilder.h index 65a4bf7dcf..3b0d025e82 100644 --- a/mlir/include/mlir/Dialect/QIR/Builder/QIRProgramBuilder.h +++ b/mlir/include/mlir/Dialect/QIR/Builder/QIRProgramBuilder.h @@ -100,6 +100,23 @@ class QIRProgramBuilder final : public ImplicitLocOpBuilder { */ void initialize(); + /** + * @brief Initialize the builder and prepare for program construction + * with specified return types. + * @param returnType The return type for the main function + * + * @details + * Creates a main function with an entry_point attribute. Must be called + * before adding operations. + */ + void initialize(Type returnType); + + /** + * @brief Modify the return type of the main function after initialization. + * @param returnType The new return type for the main function + */ + void retype(Type returnType); + //===--------------------------------------------------------------------===// // Constants //===--------------------------------------------------------------------===// @@ -304,6 +321,7 @@ class QIRProgramBuilder final : public ImplicitLocOpBuilder { * * @param qubit The qubit to measure * @param resultIndex The classical bit index for result pointer + * @param record Whether the measurement should be recorded in the output * @return An LLVM pointer to the measurement result * * @par Example: @@ -324,7 +342,7 @@ class QIRProgramBuilder final : public ImplicitLocOpBuilder { * !llvm.ptr) -> () * ``` */ - Value measure(Value qubit, int64_t resultIndex); + Value measure(Value qubit, int64_t resultIndex, bool record = true); /** * @brief Measure a qubit into a classical register @@ -337,6 +355,7 @@ class QIRProgramBuilder final : public ImplicitLocOpBuilder { * * @param qubit The qubit to measure * @param bit The classical bit to store the result + * @param record Whether the measurement should be recorded in the output * @return An LLVM pointer to the measurement result * * @par Example: @@ -360,7 +379,7 @@ class QIRProgramBuilder final : public ImplicitLocOpBuilder { * : (i64, !llvm.ptr, !llvm.ptr) -> () * ``` */ - Value measure(Value qubit, const Bit& bit); + Value measure(Value qubit, const Bit& bit, bool record = true); /** * @brief Reset a qubit to |0⟩ state @@ -1078,6 +1097,25 @@ class QIRProgramBuilder final : public ImplicitLocOpBuilder { */ OwningOpRef finalize(); + /** + * @brief Finalize the program with the given return value and return the + * constructed module + * @param returnValue The return value of the main function + * + * @details + * Automatically deallocates all remaining valid qubits and tensors of qubits, + * adds a return statement with the given return value, and + * transfers ownership of the module to the caller. The builder should not + * be used after calling this method. + * + * The return value must have the type indicated by the function signature + * of the main function, which returns an `i64` by default and can be + * modified by passing different arguments to the `initialize()` method. + * + * @return OwningOpRef containing the constructed quantum program module + */ + OwningOpRef finalize(Value returnValue); + /** * @brief Convenience method for building quantum programs * @param context The MLIR context to use for building the program @@ -1085,11 +1123,12 @@ class QIRProgramBuilder final : public ImplicitLocOpBuilder { * and uses it to build the desired quantum program. The builder will be * properly initialized before calling this function, and the resulting module * will be finalized and returned after this function completes. + * @param profile The profile to use for the program. Defaults to Adaptive. * @return The module containing the quantum program built by buildFunc. */ static OwningOpRef build(MLIRContext* context, - const function_ref& buildFunc, + const function_ref& buildFunc, Profile profile = Profile::Adaptive); private: @@ -1135,6 +1174,12 @@ class QIRProgramBuilder final : public ImplicitLocOpBuilder { /// Map from result index to result pointer for non-register results DenseMap resultPtrs; + /// Set of array register names that should be recorded in the output. + DenseSet recordedArrays; + + /// Set of unnamed result indices that should be recorded in the output. + DenseSet recordedIndices; + /// Map from register to their loaded indices DenseMap> loadedQubits; diff --git a/mlir/include/mlir/Dialect/Utils/Utils.h b/mlir/include/mlir/Dialect/Utils/Utils.h index 4135ca1769..970ec6d90c 100644 --- a/mlir/include/mlir/Dialect/Utils/Utils.h +++ b/mlir/include/mlir/Dialect/Utils/Utils.h @@ -13,8 +13,11 @@ #include #include #include +#include +#include #include #include +#include #include #include #include @@ -268,4 +271,35 @@ inline void inlineModifierBody(Operation* op, Block& body, rewriter.replaceOp(op, results); } +/** + * @brief Find the entry point function with the entry_point attribute + * + * @details + * Searches for the function marked with the "entry_point" attribute in + * the passthrough attributes. If multiple functions are marked, returns the + * first one encountered. + * + * @param op The module operation to search in. + * @returns the entry point function, or nullptr if not found. + */ +inline func::FuncOp getEntryPoint(ModuleOp op) { + static constexpr StringRef PASSTHROUGH_LABEL = "passthrough"; + static constexpr StringRef ENTRY_POINT_LABEL = "entry_point"; + + const auto isEntry = [](Attribute attr) { + const auto strAttr = dyn_cast(attr); + return strAttr && strAttr.getValue() == ENTRY_POINT_LABEL; + }; + + for (auto func : op.getOps()) { + if (const auto passthrough = + func->getAttrOfType(PASSTHROUGH_LABEL); + passthrough && llvm::any_of(passthrough, isEntry)) { + return func; + } + } + + return nullptr; +} + } // namespace mlir::utils diff --git a/mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp b/mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp index e18452a780..0bb434e9e3 100644 --- a/mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp +++ b/mlir/lib/Conversion/QCOToJeff/QCOToJeff.cpp @@ -1194,16 +1194,11 @@ struct ConvertSCFForOpToJeff final : StatefulOpConversionPattern { * * @par Example: * ```mlir - * func.func @main() -> i64 attributes {passthrough = ["entry_point"]} { - * %0 = arith.constant 0 : i64 - * return %0 - * } + * func.func @main() -> i64 attributes {passthrough = ["entry_point"]} { ... } * ``` * is converted to * ```mlir - * func.func @main() -> () { - * return - * } + * func.func @main() -> i64 { ... } * ``` */ struct ConvertQCOMainToJeff final : StatefulOpConversionPattern { @@ -1236,17 +1231,11 @@ struct ConvertQCOMainToJeff final : StatefulOpConversionPattern { getState().entryPointName = op.getSymName(); - // Update function signature and remove passthrough attribute + // Remove passthrough attribute from function signature rewriter.startOpModification(op); - op.setType(FunctionType::get(rewriter.getContext(), {}, {})); op->removeAttr("passthrough"); rewriter.finalizeOpModification(op); - // Replace return operation - rewriter.setInsertionPointToEnd(block); - func::ReturnOp::create(rewriter, returnOp->getLoc()); - rewriter.eraseOp(returnOp); - return success(); } }; diff --git a/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp b/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp index 9b03e4d722..80a30c19bd 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRAdaptive/QCToQIRAdaptive.cpp @@ -373,6 +373,9 @@ struct ConvertQCMeasureOp final : StatefulOpConversionPattern { // Get result pointer Value result; + const bool shouldRecord = + state.returnedMeasurements.contains(op.getOperation()); + if (op.getRegisterName() && op.getRegisterSize() && op.getRegisterIndex()) { const auto registerName = op.getRegisterName().value(); const auto registerSize = @@ -380,6 +383,10 @@ struct ConvertQCMeasureOp final : StatefulOpConversionPattern { const auto registerIndex = static_cast(op.getRegisterIndex().value()); + if (shouldRecord) { + state.recordedArrays.insert(state.stringSaver.save(registerName)); + } + // Create result register if it does not exist yet if (!resultArrays.contains(registerName)) { auto fnSig = LLVM::LLVMFunctionType::get( @@ -415,8 +422,12 @@ struct ConvertQCMeasureOp final : StatefulOpConversionPattern { result = loadedResults.at({registerName, registerIndex}); } else { rewriter.setInsertionPoint(state.entryBlock->getTerminator()); - result = createPointerFromIndex(rewriter, op.getLoc(), resultPtrs.size()); - resultPtrs.try_emplace(resultPtrs.size(), result); + auto index = resultPtrs.size(); + if (shouldRecord) { + state.recordedIndices.insert(index); + } + result = createPointerFromIndex(rewriter, op.getLoc(), index); + resultPtrs.try_emplace(index, result); } rewriter.restoreInsertionPoint(savedInsertionPoint); @@ -620,7 +631,10 @@ struct QCToQIRAdaptive final : impl::QCToQIRAdaptiveBase { } } - // Stage 2: Convert func dialect to LLVM + // Stage 2.0: Strip returned measurements from func::ReturnOp + stripReturnedMeasurements(moduleOp, state); + + // Stage 2.1: Convert func dialect to LLVM { RewritePatternSet funcPatterns(ctx); target.addIllegalDialect(); diff --git a/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp b/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp index c7043422f1..1e5a392a92 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRBase/QCToQIRBase.cpp @@ -220,6 +220,8 @@ struct ConvertQCMeasureOp final : StatefulOpConversionPattern { // Get result pointer Value result; int64_t resultIndex = 0; + const bool shouldRecord = + state.returnedMeasurements.contains(op.getOperation()); const auto nresults = resultPtrs.size(); if (op.getRegisterIndex() && op.getRegisterName() && op.getRegisterSize()) { @@ -234,6 +236,10 @@ struct ConvertQCMeasureOp final : StatefulOpConversionPattern { resultIndex = static_cast(nresults); } + if (shouldRecord) { + state.recordedIndices.insert(resultIndex); + } + if (resultPtrs.contains(resultIndex)) { result = resultPtrs.at(resultIndex); } else { @@ -406,7 +412,12 @@ struct QCToQIRBase final : impl::QCToQIRBaseBase { target.addLegalDialect(); - // Stage 1: Convert func dialect to LLVM + LoweringState state; + + // Stage 1.0: Strip returned measurements from func::ReturnOp + stripReturnedMeasurements(moduleOp, state); + + // Stage 1.1: Convert func dialect to LLVM { RewritePatternSet funcPatterns(ctx); target.addIllegalDialect(); @@ -426,8 +437,6 @@ struct QCToQIRBase final : impl::QCToQIRBaseBase { return; } - LoweringState state; - // Stage 2: Create block structure ensureBlocks(main, state); diff --git a/mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp b/mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp index aea4ae0a02..1c5c70005c 100644 --- a/mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp +++ b/mlir/lib/Conversion/QCToQIR/QIRCommon/QIRCommon.cpp @@ -15,6 +15,7 @@ #include "mlir/Dialect/QC/IR/QCOps.h" #include "mlir/Dialect/QIR/Utils/QIRUtils.h" +#include #include #include #include @@ -22,6 +23,8 @@ #include #include #include +#include +#include #include #include #include @@ -426,6 +429,9 @@ void addOutputRecording(LLVM::LLVMFuncOp& main, MLIRContext* ctx, auto fnDec = getOrCreateFunctionDeclaration(builder, main, QIR_RECORD_OUTPUT, fnSig); for (const auto& [index, ptr] : resultPtrs) { + if (!state.recordedIndices.contains(index)) { + continue; + } auto label = createResultLabel(builder, main, "__unnamed__" + std::to_string(index)) .getResult(); @@ -440,6 +446,9 @@ void addOutputRecording(LLVM::LLVMFuncOp& main, MLIRContext* ctx, auto fnDec = getOrCreateFunctionDeclaration(builder, main, QIR_ARRAY_RECORD_OUTPUT, fnSig); for (const auto& [name, results] : resultArrays) { + if (!state.recordedArrays.contains(name)) { + continue; + } auto size = results.getDefiningOp().getArraySize(); auto label = createResultLabel(builder, main, name).getResult(); LLVM::CallOp::create(builder, main->getLoc(), fnDec, @@ -463,4 +472,49 @@ void populateQCToQIRPatterns(RewritePatternSet& patterns, &state); } +void stripReturnedMeasurements(Operation* moduleOp, LoweringState& state) { + moduleOp->walk([&](func::FuncOp funcOp) { + // First, check if the given function is the main entrypoint or not. + auto passthrough = funcOp->getAttrOfType("passthrough"); + bool isEntryPoint = false; + if (passthrough) { + isEntryPoint = llvm::any_of(passthrough, [](Attribute attr) { + auto strAttr = dyn_cast(attr); + return strAttr && strAttr.getValue() == "entry_point"; + }); + } + if (!isEntryPoint) { + return; + } + + funcOp.walk([&](func::ReturnOp returnOp) { + SmallVector keptOperands; + SmallVector keptReturnTypes; + + for (auto operand : returnOp.getOperands()) { + if (auto measureOp = operand.getDefiningOp()) { + state.returnedMeasurements.insert(measureOp.getOperation()); + } else { + keptOperands.push_back(operand); + keptReturnTypes.push_back(operand.getType()); + } + } + + if (keptOperands.empty() && !returnOp.getOperands().empty()) { + OpBuilder builder(returnOp); + auto zero = + arith::ConstantIntOp::create(builder, returnOp.getLoc(), 0, 64); + keptOperands.push_back(zero); + keptReturnTypes.push_back(zero.getType()); + } + + returnOp.getOperandsMutable().assign(keptOperands); + + funcOp.setFunctionType(FunctionType::get( + funcOp.getContext(), funcOp.getFunctionType().getInputs(), + keptReturnTypes)); + }); + }); +} + } // namespace mlir diff --git a/mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp b/mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp index 03d7acb815..3a06d4b593 100644 --- a/mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp +++ b/mlir/lib/Dialect/QC/Builder/QCProgramBuilder.cpp @@ -47,12 +47,14 @@ QCProgramBuilder::QCProgramBuilder(MLIRContext* context) ctx->loadDialect(); } -void QCProgramBuilder::initialize() { +void QCProgramBuilder::initialize() { initialize({getI64Type()}); } + +void QCProgramBuilder::initialize(TypeRange returnTypes) { // Set insertion point to the module body setInsertionPointToStart(cast(module).getBody()); // Create main function as entry point - auto funcType = getFunctionType({}, {getI64Type()}); + auto funcType = getFunctionType({}, returnTypes); auto mainFunc = func::FuncOp::create(*this, "main", funcType); // Add entry_point attribute to identify the main function @@ -65,6 +67,16 @@ void QCProgramBuilder::initialize() { regionStack.emplace_back(entryBlock.getParent()); } +void QCProgramBuilder::retype(TypeRange returnTypes) { + auto mainFunc = getEntryPoint(mlir::cast(module)); + if (!mainFunc) { + llvm::reportFatalUsageError("Main function not found for retyping"); + } + auto funcType = + getFunctionType(mainFunc.getFunctionType().getInputs(), returnTypes); + mainFunc.setType(funcType); +} + Value QCProgramBuilder::boolConstant(const bool value) { checkFinalized(); return arith::ConstantOp::create(*this, getBoolAttr(value)).getResult(); @@ -245,7 +257,7 @@ DEFINE_ZERO_TARGET_ONE_PARAMETER(GPhaseOp, gphase, theta) QCProgramBuilder& QCProgramBuilder::mc##OP_NAME(ValueRange controls, \ Value target) { \ ctrl(controls, target, \ - [&](ValueRange targets) { OP_CLASS::create(*this, targets[0]); }); \ + [&](Value targetArg) { OP_CLASS::create(*this, targetArg); }); \ return *this; \ } @@ -281,9 +293,8 @@ DEFINE_ONE_TARGET_ZERO_PARAMETER(SXdgOp, sxdg) const std::variant&(PARAM), ValueRange controls, \ Value target) { \ auto param = variantToValue(*this, getLoc(), PARAM); \ - ctrl(controls, target, [&](ValueRange targets) { \ - OP_CLASS::create(*this, targets[0], param); \ - }); \ + ctrl(controls, target, \ + [&](Value targetArg) { OP_CLASS::create(*this, targetArg, param); }); \ return *this; \ } @@ -316,8 +327,8 @@ DEFINE_ONE_TARGET_ONE_PARAMETER(POp, p, theta) Value target) { \ auto param1 = variantToValue(*this, getLoc(), PARAM1); \ auto param2 = variantToValue(*this, getLoc(), PARAM2); \ - ctrl(controls, target, [&](ValueRange targets) { \ - OP_CLASS::create(*this, targets[0], param1, param2); \ + ctrl(controls, target, [&](Value targetArg) { \ + OP_CLASS::create(*this, targetArg, param1, param2); \ }); \ return *this; \ } @@ -354,8 +365,8 @@ DEFINE_ONE_TARGET_TWO_PARAMETER(U2Op, u2, phi, lambda) auto param1 = variantToValue(*this, getLoc(), PARAM1); \ auto param2 = variantToValue(*this, getLoc(), PARAM2); \ auto param3 = variantToValue(*this, getLoc(), PARAM3); \ - ctrl(controls, target, [&](ValueRange targets) { \ - OP_CLASS::create(*this, targets[0], param1, param2, param3); \ + ctrl(controls, target, [&](Value targetArg) { \ + OP_CLASS::create(*this, targetArg, param1, param2, param3); \ }); \ return *this; \ } @@ -476,6 +487,22 @@ QCProgramBuilder::ctrl(ValueRange controls, ValueRange targets, return *this; } +QCProgramBuilder& +QCProgramBuilder::ctrl(ValueRange controls, Value target, + const function_ref& body) { + checkFinalized(); + CtrlOp::create(*this, controls, target, body); + return *this; +} + +QCProgramBuilder& +QCProgramBuilder::ctrl(Value control, Value target, + const function_ref& body) { + checkFinalized(); + CtrlOp::create(*this, control, target, body); + return *this; +} + QCProgramBuilder& QCProgramBuilder::inv(ValueRange qubits, const function_ref& body) { @@ -484,6 +511,13 @@ QCProgramBuilder::inv(ValueRange qubits, return *this; } +QCProgramBuilder& QCProgramBuilder::inv(Value qubit, + const function_ref& body) { + checkFinalized(); + InvOp::create(*this, qubit, body); + return *this; +} + //===----------------------------------------------------------------------===// // SCF operations //===----------------------------------------------------------------------===// @@ -630,6 +664,13 @@ void QCProgramBuilder::ensureAllocationMode( OwningOpRef QCProgramBuilder::finalize() { checkFinalized(); + auto exitCode = intConstant(0); + return finalize({exitCode}); +} + +OwningOpRef QCProgramBuilder::finalize(ValueRange returnValues) { + checkFinalized(); + // Ensure that main function exists and insertion point is valid auto* insertionBlock = getInsertionBlock(); func::FuncOp mainFunc = nullptr; @@ -660,11 +701,8 @@ OwningOpRef QCProgramBuilder::finalize() { } allocatedMemrefs.clear(); - // Create constant 0 for successful exit code - auto exitCode = intConstant(0); - - // Add return statement with exit code 0 to the main function - func::ReturnOp::create(*this, exitCode); + // Add return statement with the given return values to the main function + func::ReturnOp::create(*this, returnValues); // Invalidate context to prevent use-after-finalize ctx = nullptr; @@ -675,11 +713,22 @@ OwningOpRef QCProgramBuilder::finalize() { OwningOpRef QCProgramBuilder::build( MLIRContext* context, - const function_ref& buildFunc) { + const function_ref(QCProgramBuilder&)>& buildFunc) { + QCProgramBuilder builder(context); + builder.initialize(); + auto result = buildFunc(builder); + builder.retype(ValueRange(result).getTypes()); + return builder.finalize(result); +} + +OwningOpRef QCProgramBuilder::build( + MLIRContext* context, + const function_ref& buildFunc) { QCProgramBuilder builder(context); builder.initialize(); - buildFunc(builder); - return builder.finalize(); + auto result = buildFunc(builder); + builder.retype(result.getType()); + return builder.finalize(result); } } // namespace mlir::qc diff --git a/mlir/lib/Dialect/QC/IR/Modifiers/CtrlOp.cpp b/mlir/lib/Dialect/QC/IR/Modifiers/CtrlOp.cpp index 86dd60e8d1..7f5ea5de44 100644 --- a/mlir/lib/Dialect/QC/IR/Modifiers/CtrlOp.cpp +++ b/mlir/lib/Dialect/QC/IR/Modifiers/CtrlOp.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -24,7 +25,9 @@ #include #include +#include #include +#include using namespace mlir; using namespace mlir::qc; @@ -163,6 +166,21 @@ struct EraseEmptyCtrl final : OpRewritePattern { } // namespace +static void +buildModifierBody(OpBuilder& odsBuilder, OperationState& odsState, + const size_t numBlockArgs, + const function_ref& emitBody) { + auto& block = odsState.regions.front()->emplaceBlock(); + const auto qubitType = QubitType::get(odsBuilder.getContext()); + for (size_t i = 0; i < numBlockArgs; ++i) { + block.addArgument(qubitType, odsState.location); + } + + const OpBuilder::InsertionGuard guard(odsBuilder); + odsBuilder.setInsertionPointToStart(&block); + emitBody(odsBuilder, block); +} + size_t CtrlOp::getNumBodyUnitaries() { return utils::getNumBodyUnitaries(*getBody()); } @@ -175,17 +193,34 @@ void CtrlOp::build(OpBuilder& odsBuilder, OperationState& odsState, ValueRange controls, ValueRange targets, const function_ref& body) { build(odsBuilder, odsState, controls, targets); - auto& block = odsState.regions.front()->emplaceBlock(); + buildModifierBody(odsBuilder, odsState, targets.size(), + [&](OpBuilder& builder, Block& block) { + body(block.getArguments()); + YieldOp::create(builder, odsState.location); + }); +} - auto qubitType = QubitType::get(odsBuilder.getContext()); - for (size_t i = 0; i < targets.size(); ++i) { - block.addArgument(qubitType, odsState.location); - } +void CtrlOp::build(OpBuilder& odsBuilder, OperationState& odsState, + ValueRange controls, Value target, + const function_ref& bodyBuilder) { + odsState.addOperands(controls); + odsState.addOperands(target); + llvm::copy( + llvm::ArrayRef({static_cast(controls.size()), 1}), + odsState.getOrAddProperties() + .operandSegmentSizes.begin()); + odsState.addRegion(); + buildModifierBody(odsBuilder, odsState, 1, + [&](OpBuilder& builder, Block& block) { + bodyBuilder(block.getArgument(0)); + YieldOp::create(builder, odsState.location); + }); +} - const OpBuilder::InsertionGuard guard(odsBuilder); - odsBuilder.setInsertionPointToStart(&block); - body(block.getArguments()); - YieldOp::create(odsBuilder, odsState.location); +void CtrlOp::build(OpBuilder& odsBuilder, OperationState& odsState, + Value control, Value target, + const function_ref& bodyBuilder) { + build(odsBuilder, odsState, ValueRange{control}, target, bodyBuilder); } LogicalResult CtrlOp::verify() { diff --git a/mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp b/mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp index b1caa45344..0a6bed0645 100644 --- a/mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp +++ b/mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -25,6 +26,7 @@ #include #include +#include #include #include @@ -300,6 +302,21 @@ struct EraseEmptyInv final : OpRewritePattern { } // namespace +static void +buildModifierBody(OpBuilder& odsBuilder, OperationState& odsState, + const size_t numBlockArgs, + const function_ref& emitBody) { + auto& block = odsState.regions.front()->emplaceBlock(); + const auto qubitType = QubitType::get(odsBuilder.getContext()); + for (size_t i = 0; i < numBlockArgs; ++i) { + block.addArgument(qubitType, odsState.location); + } + + const OpBuilder::InsertionGuard guard(odsBuilder); + odsBuilder.setInsertionPointToStart(&block); + emitBody(odsBuilder, block); +} + size_t InvOp::getNumBodyUnitaries() { return utils::getNumBodyUnitaries(*getBody()); } @@ -312,17 +329,22 @@ void InvOp::build(OpBuilder& odsBuilder, OperationState& odsState, ValueRange qubits, const function_ref& body) { build(odsBuilder, odsState, qubits); - auto& block = odsState.regions.front()->emplaceBlock(); - - auto qubitType = QubitType::get(odsBuilder.getContext()); - for (size_t i = 0; i < qubits.size(); ++i) { - block.addArgument(qubitType, odsState.location); - } + buildModifierBody(odsBuilder, odsState, qubits.size(), + [&](OpBuilder& builder, Block& block) { + body(block.getArguments()); + YieldOp::create(builder, odsState.location); + }); +} - const OpBuilder::InsertionGuard guard(odsBuilder); - odsBuilder.setInsertionPointToStart(&block); - body(block.getArguments()); - YieldOp::create(odsBuilder, odsState.location); +void InvOp::build(OpBuilder& odsBuilder, OperationState& odsState, Value qubit, + const function_ref& bodyBuilder) { + odsState.addOperands(qubit); + odsState.addRegion(); + buildModifierBody(odsBuilder, odsState, 1, + [&](OpBuilder& builder, Block& block) { + bodyBuilder(block.getArgument(0)); + YieldOp::create(builder, odsState.location); + }); } LogicalResult InvOp::verify() { diff --git a/mlir/lib/Dialect/QC/Translation/TranslateQASM3ToQC.cpp b/mlir/lib/Dialect/QC/Translation/TranslateQASM3ToQC.cpp index bfc7bfe989..eab7843753 100644 --- a/mlir/lib/Dialect/QC/Translation/TranslateQASM3ToQC.cpp +++ b/mlir/lib/Dialect/QC/Translation/TranslateQASM3ToQC.cpp @@ -220,7 +220,44 @@ class MLIRQasmImporter final : public qasm3::InstVisitor { } } - OwningOpRef finalize() { return builder.finalize(); } + OwningOpRef finalize() { + if (outputRegisters.empty()) { + outputRegisters = allBitRegisters; + } + + if (outputRegisters.empty()) { + return builder.finalize(); + } + + // Collect measurement results for all output bit registers + SmallVector returnValues; + for (const auto& regName : outputRegisters) { + auto it = bitValues.find(regName); + if (it == bitValues.end()) { + llvm::errs() << "Output register '" << regName + << "' was never measured.\n"; + return nullptr; + } + + auto expectedSize = classicalRegisters[regName].size; + if (it->second.size() < expectedSize) { + llvm::errs() << "Not all bits of output register '" << regName + << "' have been measured.\n"; + return nullptr; + } + for (auto bit : it->second) { + if (!bit) { + llvm::errs() << "Not all bits of output register '" << regName + << "' have been measured.\n"; + return nullptr; + } + returnValues.push_back(bit); + } + } + + builder.retype(ValueRange(returnValues).getTypes()); + return builder.finalize(returnValues); + } private: QCProgramBuilder builder; @@ -238,6 +275,12 @@ class MLIRQasmImporter final : public qasm3::InstVisitor { /// Map from classical-register name to measurement results. llvm::StringMap> bitValues; + /// Names of all bit registers, in declaration order. + SmallVector allBitRegisters; + + /// Names of classical registers declared as output, in declaration order. + SmallVector outputRegisters; + /// Map from gate identifier to OpenQASM 3 definition. llvm::StringMap> gates; @@ -361,6 +404,13 @@ class MLIRQasmImporter final : public qasm3::InstVisitor { case qasm3::Int: case qasm3::Uint: { classicalRegisters[id] = builder.allocClassicalBitRegister(size, id); + if (sizedType->type == qasm3::Bit) { + allBitRegisters.push_back(id); + if (stmt->isOutput || openQASM2CompatMode) { + // We return `output` bits in QASM3, or all named bits in QASM2. + outputRegisters.push_back(id); + } + } break; } default: diff --git a/mlir/lib/Dialect/QC/Translation/TranslateQuantumComputationToQC.cpp b/mlir/lib/Dialect/QC/Translation/TranslateQuantumComputationToQC.cpp index 7a2cf23651..4b798de54b 100644 --- a/mlir/lib/Dialect/QC/Translation/TranslateQuantumComputationToQC.cpp +++ b/mlir/lib/Dialect/QC/Translation/TranslateQuantumComputationToQC.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include @@ -729,7 +730,7 @@ static LogicalResult addIfElseOp(QCProgramBuilder& builder, * @brief Translates an operation from QuantumComputation to QC dialect * * @param builder The QCProgramBuilder used to create operations - * @param quantumComputation The quantum computation to translate + * @param operation The operation to translate * @param state The translation state * @return Success if all supported operations were translated */ @@ -849,7 +850,13 @@ OwningOpRef translateQuantumComputationToQC( MLIRContext* context, const ::qc::QuantumComputation& quantumComputation) { // Create and initialize the builder (creates module and main function) QCProgramBuilder builder(context); - builder.initialize(); + SmallVector resultTypes(quantumComputation.getNcbits(), + builder.getI1Type()); + if (quantumComputation.getNcbits() == 0) { + // Without classical bits, we instead return an exit code 0. + resultTypes.push_back(builder.getI64Type()); + } + builder.initialize(resultTypes); // Allocate quantum registers using the builder const auto qregs = allocateQregs(builder, quantumComputation); @@ -876,6 +883,13 @@ OwningOpRef translateQuantumComputationToQC( // Finalize and return the module (adds return statement and transfers // ownership) + if (quantumComputation.getNcbits() > 0) { + if (llvm::any_of(state.results, [](Value v) { return !v; })) { + llvm::errs() << "Not all classical bits were measured.\n"; + return nullptr; + } + return builder.finalize(state.results); + } return builder.finalize(); } diff --git a/mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp b/mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp index da20f3bfa4..f766602a24 100644 --- a/mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp +++ b/mlir/lib/Dialect/QCO/Builder/QCOProgramBuilder.cpp @@ -35,6 +35,7 @@ #include #include +#include #include #include #include @@ -52,12 +53,14 @@ QCOProgramBuilder::QCOProgramBuilder(MLIRContext* context) ctx->loadDialect(); } -void QCOProgramBuilder::initialize() { +void QCOProgramBuilder::initialize() { initialize({getI64Type()}); } + +void QCOProgramBuilder::initialize(TypeRange returnTypes) { // Set insertion point to the module body setInsertionPointToStart(mlir::cast(module).getBody()); // Create main function as entry point - auto funcType = getFunctionType({}, {getI64Type()}); + auto funcType = getFunctionType({}, returnTypes); auto mainFunc = func::FuncOp::create(*this, "main", funcType); // Add entry_point attribute to identify the main function @@ -69,6 +72,16 @@ void QCOProgramBuilder::initialize() { setInsertionPointToStart(&entryBlock); } +void QCOProgramBuilder::retype(TypeRange returnTypes) { + auto mainFunc = getEntryPoint(mlir::cast(module)); + if (!mainFunc) { + llvm::reportFatalUsageError("Main function not found for retyping"); + } + auto funcType = + getFunctionType(mainFunc.getFunctionType().getInputs(), returnTypes); + mainFunc.setType(funcType); +} + Value QCOProgramBuilder::intConstant(const int64_t value) { checkFinalized(); return arith::ConstantOp::create(*this, getI64IntegerAttr(value)).getResult(); @@ -400,7 +413,8 @@ std::pair QCOProgramBuilder::measure(Value qubit) { return {qubitOut, result}; } -Value QCOProgramBuilder::measure(Value qubit, const Bit& bit) { +std::pair QCOProgramBuilder::measure(Value qubit, + const Bit& bit) { checkFinalized(); auto nameAttr = getStringAttr(bit.registerName); @@ -413,7 +427,7 @@ Value QCOProgramBuilder::measure(Value qubit, const Bit& bit) { // Update tracking updateQubitTracking(qubit, qubitOut); - return qubitOut; + return {qubitOut, measureOp.getResult()}; } Value QCOProgramBuilder::reset(Value qubit) { @@ -480,19 +494,15 @@ DEFINE_ZERO_TARGET_ONE_PARAMETER(GPhaseOp, gphase, theta) Value target) { \ checkFinalized(); \ const auto [controlsOut, targetsOut] = \ - ctrl(control, target, [&](ValueRange targets) -> SmallVector { \ - return {OP_NAME(targets[0])}; \ - }); \ - return {controlsOut[0], targetsOut[0]}; \ + ctrl(control, target, [&](Value target) { return OP_NAME(target); }); \ + return {controlsOut, targetsOut}; \ } \ std::pair QCOProgramBuilder::mc##OP_NAME( \ ValueRange controls, Value target) { \ checkFinalized(); \ const auto [controlsOut, targetsOut] = \ - ctrl(controls, target, [&](ValueRange targets) -> SmallVector { \ - return {OP_NAME(targets[0])}; \ - }); \ - return {controlsOut, targetsOut[0]}; \ + ctrl(controls, target, [&](Value target) { return OP_NAME(target); }); \ + return {controlsOut, targetsOut}; \ } DEFINE_ONE_TARGET_ZERO_PARAMETER(IdOp, id) @@ -526,10 +536,9 @@ DEFINE_ONE_TARGET_ZERO_PARAMETER(SXdgOp, sxdg) checkFinalized(); \ auto param = variantToValue(*this, getLoc(), PARAM); \ const auto [controlsOut, targetsOut] = \ - ctrl(control, target, [&](ValueRange targets) -> SmallVector { \ - return {OP_NAME(param, targets[0])}; \ - }); \ - return {controlsOut[0], targetsOut[0]}; \ + ctrl(control, target, \ + [&](Value target) { return OP_NAME(param, target); }); \ + return {controlsOut, targetsOut}; \ } \ std::pair QCOProgramBuilder::mc##OP_NAME( \ const std::variant&(PARAM), ValueRange controls, \ @@ -537,10 +546,9 @@ DEFINE_ONE_TARGET_ZERO_PARAMETER(SXdgOp, sxdg) checkFinalized(); \ auto param = variantToValue(*this, getLoc(), PARAM); \ const auto [controlsOut, targetsOut] = \ - ctrl(controls, target, [&](ValueRange targets) -> SmallVector { \ - return {OP_NAME(param, targets[0])}; \ - }); \ - return {controlsOut, targetsOut[0]}; \ + ctrl(controls, target, \ + [&](Value target) { return OP_NAME(param, target); }); \ + return {controlsOut, targetsOut}; \ } DEFINE_ONE_TARGET_ONE_PARAMETER(RXOp, rx, theta) @@ -570,10 +578,9 @@ DEFINE_ONE_TARGET_ONE_PARAMETER(POp, p, phi) auto param1 = variantToValue(*this, getLoc(), PARAM1); \ auto param2 = variantToValue(*this, getLoc(), PARAM2); \ const auto [controlsOut, targetsOut] = \ - ctrl(control, target, [&](ValueRange targets) -> SmallVector { \ - return {OP_NAME(param1, param2, targets[0])}; \ - }); \ - return {controlsOut[0], targetsOut[0]}; \ + ctrl(control, target, \ + [&](Value target) { return OP_NAME(param1, param2, target); }); \ + return {controlsOut, targetsOut}; \ } \ std::pair QCOProgramBuilder::mc##OP_NAME( \ const std::variant&(PARAM1), \ @@ -583,10 +590,9 @@ DEFINE_ONE_TARGET_ONE_PARAMETER(POp, p, phi) auto param1 = variantToValue(*this, getLoc(), PARAM1); \ auto param2 = variantToValue(*this, getLoc(), PARAM2); \ const auto [controlsOut, targetsOut] = \ - ctrl(controls, target, [&](ValueRange targets) -> SmallVector { \ - return {OP_NAME(param1, param2, targets[0])}; \ - }); \ - return {controlsOut, targetsOut[0]}; \ + ctrl(controls, target, \ + [&](Value target) { return OP_NAME(param1, param2, target); }); \ + return {controlsOut, targetsOut}; \ } DEFINE_ONE_TARGET_TWO_PARAMETER(ROp, r, theta, phi) @@ -618,10 +624,10 @@ DEFINE_ONE_TARGET_TWO_PARAMETER(U2Op, u2, phi, lambda) auto param2 = variantToValue(*this, getLoc(), PARAM2); \ auto param3 = variantToValue(*this, getLoc(), PARAM3); \ const auto [controlsOut, targetsOut] = \ - ctrl(control, target, [&](ValueRange targets) -> SmallVector { \ - return {OP_NAME(param1, param2, param3, targets[0])}; \ + ctrl(control, target, [&](Value target) { \ + return OP_NAME(param1, param2, param3, target); \ }); \ - return {controlsOut[0], targetsOut[0]}; \ + return {controlsOut, targetsOut}; \ } \ std::pair QCOProgramBuilder::mc##OP_NAME( \ const std::variant&(PARAM1), \ @@ -633,10 +639,10 @@ DEFINE_ONE_TARGET_TWO_PARAMETER(U2Op, u2, phi, lambda) auto param2 = variantToValue(*this, getLoc(), PARAM2); \ auto param3 = variantToValue(*this, getLoc(), PARAM3); \ const auto [controlsOut, targetsOut] = \ - ctrl(controls, target, [&](ValueRange targets) -> SmallVector { \ - return {OP_NAME(param1, param2, param3, targets[0])}; \ + ctrl(controls, target, [&](Value target) { \ + return OP_NAME(param1, param2, param3, target); \ }); \ - return {controlsOut, targetsOut[0]}; \ + return {controlsOut, targetsOut}; \ } DEFINE_ONE_TARGET_THREE_PARAMETER(UOp, u, theta, phi, lambda) @@ -879,6 +885,56 @@ QCOProgramBuilder::inv(ValueRange qubits, return targetsOut; } +std::pair +QCOProgramBuilder::ctrl(ValueRange controls, Value target, + function_ref body) { + checkFinalized(); + + Value innerTargetOut; + auto ctrlOp = + CtrlOp::create(*this, controls, target, [&](Value targetArg) -> Value { + updateQubitTracking(target, targetArg); + innerTargetOut = body(targetArg); + return innerTargetOut; + }); + + const auto& controlsOut = ctrlOp.getControlsOut(); + for (const auto& [control, controlOut] : + llvm::zip_equal(controls, controlsOut)) { + updateQubitTracking(control, controlOut); + } + const auto& targetsOut = ctrlOp.getTargetsOut(); + assert(targetsOut.size() == 1); + updateQubitTracking(innerTargetOut, targetsOut.front()); + + return {controlsOut, targetsOut.front()}; +} + +std::pair +QCOProgramBuilder::ctrl(Value control, Value target, + function_ref body) { + const auto [controlsOut, targetOut] = ctrl(ValueRange{control}, target, body); + assert(controlsOut.size() == 1); + return {controlsOut.front(), targetOut}; +} + +Value QCOProgramBuilder::inv(Value qubit, function_ref body) { + checkFinalized(); + + Value innerQubitOut; + auto invOp = InvOp::create(*this, qubit, [&](Value qubitArg) -> Value { + updateQubitTracking(qubit, qubitArg); + innerQubitOut = body(qubitArg); + return innerQubitOut; + }); + + const auto& qubitsOut = invOp.getQubitsOut(); + assert(qubitsOut.size() == 1); + updateQubitTracking(innerQubitOut, qubitsOut.front()); + + return qubitsOut.front(); +} + //===----------------------------------------------------------------------===// // Deallocation //===----------------------------------------------------------------------===// @@ -1103,6 +1159,13 @@ void QCOProgramBuilder::ensureAllocationMode( OwningOpRef QCOProgramBuilder::finalize() { checkFinalized(); + auto exitCode = intConstant(0); + return finalize({exitCode}); +} + +OwningOpRef QCOProgramBuilder::finalize(ValueRange returnValues) { + checkFinalized(); + // Ensure that main function exists and insertion point is valid auto* insertionBlock = getInsertionBlock(); func::FuncOp mainFunc = nullptr; @@ -1151,11 +1214,8 @@ OwningOpRef QCOProgramBuilder::finalize() { validQubits.clear(); validTensors.clear(); - // Create constant 0 for successful exit code - auto exitCode = intConstant(0); - - // Add return statement with exit code 0 to the main function - func::ReturnOp::create(*this, exitCode); + // Add return statement with the given return values to the main function + func::ReturnOp::create(*this, returnValues); // Invalidate context to prevent use-after-finalize ctx = nullptr; @@ -1165,11 +1225,22 @@ OwningOpRef QCOProgramBuilder::finalize() { OwningOpRef QCOProgramBuilder::build( MLIRContext* context, - const function_ref& buildFunc) { + const function_ref(QCOProgramBuilder&)>& buildFunc) { + QCOProgramBuilder builder(context); + builder.initialize(); + auto result = buildFunc(builder); + builder.retype(ValueRange(result).getTypes()); + return builder.finalize(result); +} + +OwningOpRef QCOProgramBuilder::build( + MLIRContext* context, + const function_ref& buildFunc) { QCOProgramBuilder builder(context); builder.initialize(); - buildFunc(builder); - return builder.finalize(); + auto result = buildFunc(builder); + builder.retype(result.getType()); + return builder.finalize(result); } } // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp b/mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp index 657c90b8a1..f4b6f6b51f 100644 --- a/mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp @@ -195,6 +195,21 @@ struct EraseEmptyCtrl final : OpRewritePattern { } // namespace +static void +buildModifierBody(OpBuilder& odsBuilder, OperationState& odsState, + const size_t numBlockArgs, + const function_ref& emitBody) { + auto& block = odsState.regions.front()->emplaceBlock(); + const auto qubitType = QubitType::get(odsBuilder.getContext()); + for (size_t i = 0; i < numBlockArgs; ++i) { + block.addArgument(qubitType, odsState.location); + } + + const OpBuilder::InsertionGuard guard(odsBuilder); + odsBuilder.setInsertionPointToStart(&block); + emitBody(odsBuilder, block); +} + size_t CtrlOp::getNumBodyUnitaries() { return utils::getNumBodyUnitaries(*getBody()); } @@ -224,17 +239,29 @@ void CtrlOp::build(OpBuilder& odsBuilder, OperationState& odsState, ValueRange controls, ValueRange targets, function_ref(ValueRange)> bodyBuilder) { build(odsBuilder, odsState, controls, targets); - auto& block = odsState.regions.front()->emplaceBlock(); + buildModifierBody(odsBuilder, odsState, targets.size(), + [&](OpBuilder& builder, Block& block) { + YieldOp::create(builder, odsState.location, + bodyBuilder(block.getArguments())); + }); +} - auto qubitType = QubitType::get(odsBuilder.getContext()); - for (size_t i = 0; i < targets.size(); ++i) { - block.addArgument(qubitType, odsState.location); - } +void CtrlOp::build(OpBuilder& odsBuilder, OperationState& odsState, + ValueRange controls, Value target, + function_ref bodyBuilder) { + build(odsBuilder, odsState, controls.getTypes(), target.getType(), controls, + target); + buildModifierBody(odsBuilder, odsState, 1, + [&](OpBuilder& builder, Block& block) { + YieldOp::create(builder, odsState.location, + bodyBuilder(block.getArgument(0))); + }); +} - const OpBuilder::InsertionGuard guard(odsBuilder); - odsBuilder.setInsertionPointToStart(&block); - YieldOp::create(odsBuilder, odsState.location, - bodyBuilder(block.getArguments())); +void CtrlOp::build(OpBuilder& odsBuilder, OperationState& odsState, + Value control, Value target, + function_ref bodyBuilder) { + build(odsBuilder, odsState, ValueRange{control}, target, bodyBuilder); } LogicalResult CtrlOp::verify() { @@ -313,10 +340,10 @@ std::optional CtrlOp::getUnitaryMatrix() { // Build `I_{2^controls} ⊗ U` by placing the target block in the bottom-right // corner of a `2^controls * targetDim` identity. const auto controlledMatrix = - [numControls](const std::int64_t targetDim, + [numControls](const int64_t targetDim, const auto& targetBlock) -> DynamicMatrix { auto matrix = DynamicMatrix::identity(static_cast( - (1ULL << numControls) * static_cast(targetDim))); + (1ULL << numControls) * static_cast(targetDim))); matrix.setBottomRightCorner(targetBlock); return matrix; }; @@ -332,11 +359,9 @@ std::optional CtrlOp::getUnitaryMatrix() { return std::nullopt; } - // Composed single-qubit body (e.g. `ctrl { h; x }`); embed the 2x2 directly. - if (getNumTargets() == 1) { - if (const auto composed = composeSingleQubitBodyMatrix(*getBody())) { - return controlledMatrix(2, *composed); - } + // Composed body (e.g., `ctrl { h; x }` or `ctrl { swap; ry }`) + if (const auto composed = composeBodyMatrix(*getBody(), getNumTargets())) { + return controlledMatrix(composed->rows(), *composed); } return std::nullopt; diff --git a/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp b/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp index 675e835638..c58e63c564 100644 --- a/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp @@ -30,6 +30,7 @@ #include #include +#include #include #include #include @@ -318,6 +319,21 @@ struct EraseEmptyInv final : OpRewritePattern { } // namespace +static void +buildModifierBody(OpBuilder& odsBuilder, OperationState& odsState, + const size_t numBlockArgs, + const function_ref& emitBody) { + auto& block = odsState.regions.front()->emplaceBlock(); + const auto qubitType = QubitType::get(odsBuilder.getContext()); + for (size_t i = 0; i < numBlockArgs; ++i) { + block.addArgument(qubitType, odsState.location); + } + + const OpBuilder::InsertionGuard guard(odsBuilder); + odsBuilder.setInsertionPointToStart(&block); + emitBody(odsBuilder, block); +} + size_t InvOp::getNumBodyUnitaries() { return utils::getNumBodyUnitaries(*getBody()); } @@ -347,17 +363,21 @@ void InvOp::build(OpBuilder& odsBuilder, OperationState& odsState, ValueRange qubits, function_ref(ValueRange)> bodyBuilder) { build(odsBuilder, odsState, qubits); - auto& block = odsState.regions.front()->emplaceBlock(); - - auto qubitType = QubitType::get(odsBuilder.getContext()); - for (size_t i = 0; i < qubits.size(); ++i) { - block.addArgument(qubitType, odsState.location); - } + buildModifierBody(odsBuilder, odsState, qubits.size(), + [&](OpBuilder& builder, Block& block) { + YieldOp::create(builder, odsState.location, + bodyBuilder(block.getArguments())); + }); +} - const OpBuilder::InsertionGuard guard(odsBuilder); - odsBuilder.setInsertionPointToStart(&block); - YieldOp::create(odsBuilder, odsState.location, - bodyBuilder(block.getArguments())); +void InvOp::build(OpBuilder& odsBuilder, OperationState& odsState, Value qubit, + function_ref bodyBuilder) { + build(odsBuilder, odsState, qubit.getType(), qubit); + buildModifierBody(odsBuilder, odsState, 1, + [&](OpBuilder& builder, Block& block) { + YieldOp::create(builder, odsState.location, + bodyBuilder(block.getArgument(0))); + }); } LogicalResult InvOp::verify() { @@ -427,12 +447,9 @@ std::optional InvOp::getUnitaryMatrix() { return std::nullopt; } - // Composed single-qubit body (e.g. `inv { h; t }`). - if (getNumTargets() != 1) { - return std::nullopt; - } - if (const auto composed = composeSingleQubitBodyMatrix(*getBody())) { - return DynamicMatrix::fromAdjoint(*composed); + // Composed body (e.g., `ctrl { h; x }` or `ctrl { swap; ry }`) + if (const auto composed = composeBodyMatrix(*getBody(), getNumTargets())) { + return composed->adjoint(); } return std::nullopt; } diff --git a/mlir/lib/Dialect/QCO/IR/QCOUtils.cpp b/mlir/lib/Dialect/QCO/IR/QCOUtils.cpp index 92f47d6d2b..3fcf3a5a73 100644 --- a/mlir/lib/Dialect/QCO/IR/QCOUtils.cpp +++ b/mlir/lib/Dialect/QCO/IR/QCOUtils.cpp @@ -14,6 +14,7 @@ #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" +#include #include #include #include @@ -22,53 +23,130 @@ #include #include +#include +#include #include namespace mlir::qco { -std::optional composeSingleQubitBodyMatrix(Block& block) { - Matrix2x2 acc = Matrix2x2::identity(); +/// Returns the wire index for @p wire in @p wireIds, or `std::nullopt` if +/// untracked. +[[nodiscard]] static std::optional +lookupWireId(const DenseMap& wireIds, Value wire) { + if (const auto it = wireIds.find(wire); it != wireIds.end()) { + return it->second; + } + return std::nullopt; +} + +/// Propagates wire indices from unitary inputs to outputs via @p wireIds. +static void propagateWireIds(UnitaryOpInterface unitary, + DenseMap& wireIds) { + for (auto [input, output] : + llvm::zip_equal(unitary.getInputQubits(), unitary.getOutputQubits())) { + if (const auto wire = lookupWireId(wireIds, input)) { + wireIds[output] = *wire; + } + } +} + +/// Returns the @p unitary embedded on @p numTargets modifier wires using @p +/// wireIds. +[[nodiscard]] static std::optional +embedUnitaryInBody(UnitaryOpInterface unitary, size_t numTargets, + const DenseMap& wireIds) { + const auto numOpQubits = unitary.getNumQubits(); + if (numOpQubits == 0 || numOpQubits > 2) { + return std::nullopt; + } + + if (numOpQubits == 1) { + const auto wire = lookupWireId(wireIds, unitary.getInputQubit(0)); + if (!wire.has_value()) { + return std::nullopt; + } + return unitary.getUnitaryMatrix()->embedInNqubit(numTargets, + *wire); + } + + const auto q0 = lookupWireId(wireIds, unitary.getInputQubit(0)); + const auto q1 = lookupWireId(wireIds, unitary.getInputQubit(1)); + if (!q0.has_value() || !q1.has_value()) { + return std::nullopt; + } + return unitary.getUnitaryMatrix()->embedInNqubit(numTargets, *q0, + *q1); +} + +std::optional composeBodyMatrix(Block& block, + size_t numTargets) { + if (numTargets == 0 || numTargets > kMaxModifierTargetQubits || + block.getNumArguments() != numTargets) { + return std::nullopt; + } + + std::optional acc; Complex global{1.0, 0.0}; bool found = false; + + DenseMap wireIds; + for (size_t i = 0; i < numTargets; ++i) { + wireIds[block.getArgument(i)] = i; + } + for (Operation& op : block.without_terminator()) { - if (!TypeSwitch(&op) - .Case([](auto) { return true; }) - .Case([&](GPhaseOp gphase) { - const auto matrix = gphase.getUnitaryMatrix(); - if (!matrix) { - return false; - } - global *= (*matrix)(0, 0); - found = true; - return true; - }) - .Case([&](UnitaryOpInterface unitary) { - if (!unitary.isSingleQubit() || - !unitary.hasCompileTimeKnownUnitaryMatrix()) { - return false; - } - Matrix2x2 matrix; - if (!unitary.getUnitaryMatrix2x2(matrix)) { - return false; - } - acc.premultiplyBy(matrix); - found = true; - return true; - }) - .Default([](Operation* operation) { - const auto usesQubit = [](Value value) { - return isa(value.getType()); - }; - return !llvm::any_of(operation->getOperands(), usesQubit) && - !llvm::any_of(operation->getResults(), usesQubit); - })) { + const bool handled = + TypeSwitch(&op) + .Case([&](BarrierOp barrier) { + propagateWireIds(barrier, wireIds); + return true; + }) + .Case([&](GPhaseOp gphase) { + const auto matrix = gphase.getUnitaryMatrix(); + if (!matrix) { + return false; + } + global *= matrix->value; + found = true; + return true; + }) + .Case([&](UnitaryOpInterface unitary) { + if (!unitary.hasCompileTimeKnownUnitaryMatrix()) { + return false; + } + auto embedded = embedUnitaryInBody(unitary, numTargets, wireIds); + if (!embedded.has_value()) { + return false; + } + if (!acc.has_value()) { + acc.swap(embedded); + } else { + acc->premultiplyBy(*embedded); + } + found = true; + propagateWireIds(unitary, wireIds); + return true; + }) + .Default([&](Operation* unknown) { + const auto usesQubit = [](Value value) { + return isa(value.getType()); + }; + return !llvm::any_of(unknown->getOperands(), usesQubit) && + !llvm::any_of(unknown->getResults(), usesQubit); + }); + + if (!handled) { return std::nullopt; } } + if (!found) { return std::nullopt; } - acc *= global; + if (!acc.has_value()) { + acc = DynamicMatrix::identity(static_cast(1ULL << numTargets)); + } + *acc *= global; return acc; } diff --git a/mlir/lib/Dialect/QCO/IR/QubitManagement/SinkOp.cpp b/mlir/lib/Dialect/QCO/IR/QubitManagement/SinkOp.cpp index ec8b33b833..c07fa324aa 100644 --- a/mlir/lib/Dialect/QCO/IR/QubitManagement/SinkOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/QubitManagement/SinkOp.cpp @@ -8,15 +8,40 @@ * Licensed under the MIT License */ +#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include +#include +#include #include +#include #include +#include +#include #include +#include using namespace mlir; using namespace mlir::qco; +/** + * @brief Check if given quantum operation is unused (i.e., only used by + * sinks and no memory effects). + * + * @param op The operation to check. + * @return bool True if the operation is unused, false otherwise. + */ +static bool checkDeadGate(Operation* op) { + if (!isMemoryEffectFree(op)) { + // This ignores operations and regions that have children with memory + // effects, which should never be considered dead. + return false; + } + return llvm::all_of(op->getUsers(), + [](Operation* user) { return isa(user); }); +} + namespace { /** @@ -39,9 +64,65 @@ struct RemoveAllocSinkPair final : OpRewritePattern { } }; +/** + * @brief Remove dead gates. + */ +struct DeadGateElimination final : OpRewritePattern { + + explicit DeadGateElimination(MLIRContext* context) + : OpRewritePattern(context) {} + + LogicalResult matchAndRewrite(SinkOp op, + PatternRewriter& rewriter) const override { + Value currentValue = op.getQubit(); + auto* currentOp = currentValue.getDefiningOp(); + auto success = false; + while (currentOp != nullptr) { + if (!checkDeadGate(currentOp)) { + break; + } + + currentValue = + TypeSwitch(currentOp) + .Case([&](auto measureOp) { + auto newValue = measureOp.getQubitIn(); + rewriter.replaceAllUsesWith(measureOp.getQubitOut(), newValue); + rewriter.eraseOp(measureOp); + return newValue; + }) + .Case([&](auto ifOp) { + auto* tiedQubit = + ifOp.getTiedQubit(cast(currentValue)); + auto newValue = tiedQubit->get(); + rewriter.replaceOp(ifOp, ifOp.getQubits()); + return newValue; + }) + .Case([&](auto resetOp) { + auto newValue = resetOp.getQubitIn(); + rewriter.replaceOp(resetOp, resetOp->getOperands()); + return newValue; + }) + .Case([&](auto unitaryOp) { + auto newValue = unitaryOp.getInputForOutput(currentValue); + rewriter.replaceOp(unitaryOp, unitaryOp.getInputQubits()); + return newValue; + }) + .Default([&](auto) { return nullptr; }); + + if (currentValue == nullptr) { + break; + } + currentOp = currentValue.getDefiningOp(); + success = true; + } + + return llvm::success(success); + } +}; + } // namespace void SinkOp::getCanonicalizationPatterns(RewritePatternSet& results, MLIRContext* context) { - results.add(context); + results.add(context); } diff --git a/mlir/lib/Dialect/QCO/IR/SCF/IfOp.cpp b/mlir/lib/Dialect/QCO/IR/SCF/IfOp.cpp index c377947153..c232728250 100644 --- a/mlir/lib/Dialect/QCO/IR/SCF/IfOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/SCF/IfOp.cpp @@ -313,15 +313,55 @@ BlockArgument IfOp::getTiedElseBlockArgument(OpOperand* qubit) { } OpOperand* IfOp::getTiedThenYieldedValue(BlockArgument bbArg) { - if (bbArg.getDefiningOp() != getOperation()) { + if (bbArg.getOwner()->getParentOp() != getOperation()) { return nullptr; } return &thenYield().getTargetsMutable()[bbArg.getArgNumber()]; } OpOperand* IfOp::getTiedElseYieldedValue(BlockArgument bbArg) { - if (bbArg.getDefiningOp() != getOperation()) { + if (bbArg.getOwner()->getParentOp() != getOperation()) { return nullptr; } return &elseYield().getTargetsMutable()[bbArg.getArgNumber()]; } + +IfOp IfOp::replaceWithAdditionalQubits(RewriterBase& rewriter, + ValueRange addons) { + if (addons.empty()) { + return *this; + } + + SmallVector allQubits; + allQubits.reserve(getQubits().size() + addons.size()); + allQubits.append(getQubits().begin(), getQubits().end()); + allQubits.append(addons.begin(), addons.end()); + const auto allQubitTypes = ValueRange(allQubits).getTypes(); + + auto newIfOp = create(rewriter, getLoc(), getCondition(), allQubits); + + const auto rewriteRegion = [&rewriter, &allQubitTypes, + &addons](Region& oldRegion, Region& newRegion) { + auto* oldBlock = &oldRegion.front(); + const auto numOldArgs = oldBlock->getNumArguments(); + auto* newBlock = rewriter.createBlock(&newRegion, {}, allQubitTypes); + const auto oldArgs = newBlock->getArguments().take_front(numOldArgs); + const auto addonArgs = newBlock->getArguments().drop_front(numOldArgs); + + rewriter.mergeBlocks(oldBlock, newBlock, oldArgs); + + auto yield = cast(newBlock->getTerminator()); + SmallVector yieldedValues; + yieldedValues.reserve(yield.getTargets().size() + addons.size()); + yieldedValues.append(yield.getTargets().begin(), yield.getTargets().end()); + yieldedValues.append(addonArgs.begin(), addonArgs.end()); + rewriter.replaceOpWithNewOp(yield, yieldedValues); + }; + + rewriteRegion(getThenRegion(), newIfOp.getThenRegion()); + rewriteRegion(getElseRegion(), newIfOp.getElseRegion()); + + rewriter.eraseOp(*this); + + return newIfOp; +} diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp index 2b2c9818e3..16d6332612 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Weyl.cpp @@ -705,13 +705,12 @@ LogicalResult synthesizeUnitary2QWeyl(OpBuilder& builder, Location loc, wire = *synthesized; }; const auto emitEntangler = [&]() { - auto ctrlOp = CtrlOp::create( - builder, loc, wire0, wire1, - [&](ValueRange targetArgs) -> SmallVector { + auto ctrlOp = + CtrlOp::create(builder, loc, wire0, wire1, [&](Value targetQubit) { if (emitCz) { - return {ZOp::create(builder, loc, targetArgs[0]).getOutputQubit(0)}; + return ZOp::create(builder, loc, targetQubit).getOutputQubit(0); } - return {XOp::create(builder, loc, targetArgs[0]).getOutputQubit(0)}; + return XOp::create(builder, loc, targetQubit).getOutputQubit(0); }); wire0 = ctrlOp.getOutputControl(0); wire1 = ctrlOp.getOutputTarget(0); diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index 74a6d9e6db..8664675abb 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -10,43 +10,50 @@ #include "mlir/Dialect/QCO/Transforms/Mapping/Mapping.h" -#include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/Utils/Algorithms.h" #include "mlir/Dialect/QCO/Utils/Drivers.h" +#include "mlir/Dialect/QCO/Utils/Graph.h" +#include "mlir/Dialect/QCO/Utils/Layout.h" #include "mlir/Dialect/QCO/Utils/WireIterator.h" #include "mlir/Dialect/QTensor/IR/QTensorOps.h" #include "mlir/Dialect/QTensor/Utils/TensorIterator.h" +#include "mlir/Dialect/Utils/Utils.h" -#include #include #include #include +#include #include #include +#include #include #include +#include #include +#include #include #include +#include #include #include #include #include +#include #include #include #include #include +#include #include #include #include #include #include -#include +#include #include #include -#include #include #include #include @@ -56,6 +63,7 @@ namespace mlir::qco { using namespace mlir::qtensor; +using namespace mlir::utils; #define GEN_PASS_DEF_MAPPINGPASS #include "mlir/Dialect/QCO/Transforms/Passes.h.inc" @@ -64,235 +72,115 @@ namespace { struct MappingPass : impl::MappingPassBase { private: - using IndexType = size_t; - using IndexPairType = std::pair; + using IndexPairType = std::pair; using Window = SmallVector; - using Neighbours = SmallVector>; - - enum class RoutingMode : std::uint8_t { Cold, Hot }; - - /** - * @brief A qubit layout that maps program and hardware indices without - * storing Values. Used for efficient memory usage when Value tracking isn't - * needed. - * - * Note that we use the terminology "hardware" and "program" qubits here, - * because "virtual" (opposed to physical) and "static" (opposed to dynamic) - * are C++ keywords. - */ - class [[nodiscard]] Layout { - public: - /** - * @brief Constructs the identity (i->i) layout. - * @param nqubits The number of qubits. - * @return The identity layout. - */ - static Layout identity(const size_t nqubits) { - Layout layout(nqubits); - for (size_t i = 0; i < nqubits; ++i) { - layout.add(i, i); - } - return layout; - } - - /** - * @brief Constructs a random layout. - * @param nqubits The number of qubits. - * @param seed A seed for randomization. - * @return The random layout. - */ - static Layout random(const size_t nqubits, const size_t seed) { - SmallVector mapping(nqubits); - std::iota(mapping.begin(), mapping.end(), IndexType{0}); - std::ranges::shuffle(mapping, std::mt19937_64{seed}); - - Layout layout(nqubits); - for (const auto [prog, hw] : enumerate(mapping)) { - layout.add(prog, hw); - } - - return layout; - } - - /** - * @brief Insert program:hardware index mapping. - * @param prog The program index. - * @param hw The hardware index. - */ - void add(IndexType prog, IndexType hw) { - assert(prog < programToHardware_.size() && - "add: program index out of bounds"); - assert(hw < hardwareToProgram_.size() && - "add: hardware index out of bounds"); - programToHardware_[prog] = hw; - hardwareToProgram_[hw] = prog; - } - - /** - * @brief Look up program index for a hardware index. - * @param hw The hardware index. - * @return The program index of the respective hardware index. - */ - [[nodiscard]] IndexType getProgramIndex(const IndexType hw) const { - assert(hw < hardwareToProgram_.size() && - "getProgramIndex: hardware index out of bounds"); - return hardwareToProgram_[hw]; - } - - /** - * @brief Look up hardware index for a program index. - * @param prog The program index. - * @return The hardware index of the respective program index. - */ - [[nodiscard]] IndexType getHardwareIndex(const IndexType prog) const { - assert(prog < programToHardware_.size() && - "getHardwareIndex: program index out of bounds"); - return programToHardware_[prog]; - } - - /** - * @brief Convenience function to lookup multiple hardware indices at once. - * @param progs The program indices. - * @return A tuple of hardware indices. - */ - template - requires(sizeof...(ProgIndices) > 0) && - ((std::is_convertible_v) && ...) - [[nodiscard]] auto getHardwareIndices(ProgIndices... progs) const { - return std::tuple{getHardwareIndex(static_cast(progs))...}; - } - - /** - * @brief Convenience function to lookup multiple program indices at once. - * @param hws The hardware indices. - * @return A tuple of program indices. - */ - template - requires(sizeof...(HwIndices) > 0) && - ((std::is_convertible_v) && ...) - [[nodiscard]] auto getProgramIndices(HwIndices... hws) const { - return std::tuple{getProgramIndex(static_cast(hws))...}; - } - - /** - * @brief Swap the mapping to program indices of two hardware indices. - */ - void swap(const IndexType hw0, const IndexType hw1) { - const auto prog0 = hardwareToProgram_[hw0]; - const auto prog1 = hardwareToProgram_[hw1]; - - std::swap(hardwareToProgram_[hw0], hardwareToProgram_[hw1]); - std::swap(programToHardware_[prog0], programToHardware_[prog1]); - } - - /** - * @returns the number of qubits managed by the layout. - */ - [[nodiscard]] size_t nqubits() const { return programToHardware_.size(); } - - /** - * @returns the program to hardware mapping. - */ - [[nodiscard]] ArrayRef getProgramToHardware() const { - return programToHardware_; - } - - protected: - /** - * @brief Maps a program qubit index to its hardware index. - */ - SmallVector programToHardware_; - - /** - * @brief Maps a hardware qubit index to its program index. - */ - SmallVector hardwareToProgram_; + using Wires = SmallVector; + using RecursiveRoutingStackItem = std::pair>; + using RecursiveRoutingStack = SmallVector; - private: - explicit Layout(const size_t nqubits) - : programToHardware_(nqubits), hardwareToProgram_(nqubits) {} - }; + enum class RoutingMode : bool { Cold, Hot }; - class [[nodiscard]] AugmentedDevice { + class AugmentedDevice { public: - AugmentedDevice() = default; - - AugmentedDevice(size_t nqubits, const Edges& coupling) - : nqubits_(nqubits), dist_(findAllShortestPaths(nqubits, coupling)), - coupling_(coupling), neighbours_(nqubits) { - for (const auto& [u, v] : coupling_) { - neighbours_[u].push_back(v); - } - } + explicit AugmentedDevice( + const DenseSet>& couplingSet) + : coupling_(couplingSet), dist_(coupling_.getDistMatrix()) {} - /** - * @returns the device's number of qubits. - */ - [[nodiscard]] size_t nqubits() const { return nqubits_; } + /// Return the device's number of qubits. + [[nodiscard]] size_t nqubits() const { return coupling_.getNumNodes(); } - /** - * @returns true if @p u and @p v are adjacent. - */ - [[nodiscard]] bool areAdjacent(size_t u, size_t v) const { - return coupling_.contains(std::make_pair(u, v)); + /// Return true if two qubits are adjacent. + [[nodiscard]] bool areAdjacent(const size_t u, const size_t v) const { + return dist_[u][v] == 1UL; } - /** - * @returns the length of the shortest path between @p u and @p v. - */ - [[nodiscard]] size_t distanceBetween(size_t u, size_t v) const { - if (dist_[u][v] == UINT64_MAX) { + /// Return the length of the shortest path between two qubits. + [[nodiscard]] size_t distanceBetween(const size_t u, const size_t v) const { + const auto dist = dist_[u][v]; + if (dist == UINT64_MAX) { report_fatal_error("Failed to compute the distance between qubits " + Twine(u) + " and " + Twine(v)); } - return dist_[u][v]; + return dist; } - /** - * @returns all neighbours of @p u. - */ - [[nodiscard]] ArrayRef neighboursOf(size_t u) const { - return neighbours_[u]; + /// Return the qubit identifiers. + [[nodiscard]] SmallVector qubits() const { + return coupling_.getNodes(); } - /** - * @returns the max degree (connectivity) of any qubit of the device. - */ - [[nodiscard]] size_t maxDegree() const { - size_t deg = 0; - for (const auto& nbrs : neighbours_) { - deg = std::max(deg, nbrs.size()); - } - return deg; + /// Return all neighbours of a qubit. + [[nodiscard]] ArrayRef neighboursOf(const size_t u) const { + return coupling_.getNeighbours(u); } + /// Return the max degree (connectivity) of any qubit of the device. + [[nodiscard]] size_t maxDegree() const { return coupling_.getMaxDegree(); } + private: - size_t nqubits_{}; - Matrix dist_; - Edges coupling_; - Neighbours neighbours_; + Graph coupling_; + Graph::DistanceMatrix dist_; }; - struct [[nodiscard]] Trial { - explicit Trial(Layout layout) : layout(std::move(layout)) {} + struct WireInfos { + /// Return the mapped wire index of a program index. + [[nodiscard]] size_t lookupIndex(const size_t prog) const { + return programToIndex_[prog]; + } + + /// Return the mapped program index of a wire index. + [[nodiscard]] size_t lookupProgram(const size_t index) const { + return indexToProgram_[index]; + } - Layout layout; - size_t nswaps{}; - bool success{false}; + /// Bidirectionally map a wire index to a program index. + /// Overwrites existing mappings. + void map(const size_t index, const size_t prog) { + if (index >= indexToProgram_.size()) { + indexToProgram_.resize(index + 1); + } + if (prog >= programToIndex_.size()) { + programToIndex_.resize(prog + 1); + } + indexToProgram_[index] = prog; + programToIndex_[prog] = index; + } + + /// Swap two program indices. + void swap(const size_t prog0, const size_t prog1) { + const auto i0 = lookupIndex(prog0); + const auto i1 = lookupIndex(prog1); + std::swap(programToIndex_[prog0], programToIndex_[prog1]); + std::swap(indexToProgram_[i0], indexToProgram_[i1]); + } + + private: + /// Maps the i-th wire index to a program index. + SmallVector indexToProgram_; + /// Maps a program index to the i-th wire index. + SmallVector programToIndex_; + }; + + /// Statistics collected while routing. + struct Statistics { + size_t nswaps{0}; }; - /** - * @brief Parameters influencing the behavior of the A* search algorithm. - */ - struct [[nodiscard]] Parameters { + /// Parameters influencing the behavior of the A* search algorithm. + struct Parameters { float alpha; float lambda; }; - /** - * @brief Describes a node in the A* search graph. - */ - struct [[nodiscard]] Node { + /// Utility-struct for routing functions. + struct RoutingBundle { + Wires wires; + WireInfos infos; + Layout layout; + }; + + /// Describes a node in the A* search graph. + struct Node { struct ComparePointer { bool operator()(const Node* lhs, const Node* rhs) const { return lhs->f > rhs->f; @@ -305,17 +193,13 @@ struct MappingPass : impl::MappingPassBase { size_t depth; float f; - /** - * @brief Construct a root node with the given layout. Initialize the - * sequence with an empty vector and set the cost to zero. - */ + /// Construct a root node with the given layout. Initialize the + /// sequence with an empty vector and set the cost to zero. explicit Node(Layout layout) : layout(std::move(layout)), parent(nullptr), depth(0), f(0) {} - /** - * @brief Construct a non-root node from its parent node. Apply the given - * swap to the layout of the parent node. - */ + /// Construct a non-root node from its parent node. Apply the given swap to + /// the layout of the parent node. Node(Node* parent, const IndexPairType& swap, const Window& window, const AugmentedDevice& device, const Parameters& params) : layout(parent->layout), swap(swap), parent(parent), @@ -324,36 +208,29 @@ struct MappingPass : impl::MappingPassBase { f = g(params.alpha) + h(window, device, params); // NOLINT } - /** - * @returns true if the current SWAP sequence makes all gates in the front - * executable. - */ + /// Return true, if the current SWAP sequence makes all gates in the front + /// executable. [[nodiscard]] bool isGoal(const IndexPairType& front, const AugmentedDevice& device) const { - return device.areAdjacent(layout.getHardwareIndex(front.first), - layout.getHardwareIndex(front.second)); + const auto [hw0, hw1] = + layout.getHardwareIndices(front.first, front.second); + return device.areAdjacent(hw0, hw1); } private: - /** - * @brief Calculate the path cost for the A* search algorithm. - * - * The path cost function is the weighted sum of the currently required - * SWAPs. - */ + /// Calculate the path cost for the A* search algorithm. + /// The path costs are the weighted sum of the currently required SWAPs. [[nodiscard]] float g(const float alpha) const { return alpha * static_cast(depth); } - /** - * @brief Calculate the heuristic cost for the A* search algorithm. - * - * Computes the minimal number of SWAPs required to route each gate in - * each layer. For each gate, this is determined by the shortest distance - * between its hardware qubits. Intuitively, this is the number of SWAPs - * that a naive router would insert to route the layers (with a constant - * layout). - */ + /// Calculate the heuristic cost for the A* search algorithm. + /// + /// Computes the minimal number of SWAPs required to route each gate in + /// each layer. For each gate, this is determined by the shortest distance + /// between its hardware qubits. Intuitively, this is the number of SWAPs + /// that a naive router would insert to route the layers (with a constant + /// layout). [[nodiscard]] float h(const Window& window, const AugmentedDevice& device, const Parameters& params) const { float costs{0}; @@ -370,26 +247,107 @@ struct MappingPass : impl::MappingPassBase { } }; + /// Describes the graph F of arXiv:1602.05150v3. + struct FGraph { + explicit FGraph(std::shared_ptr device) + : f_(device->qubits()), device_(std::move(device)) {}; + + /// Build F-graph: Add edges to F for each edge in the coupling graph. + /// Note that this assumes that the coupling graph is directed, but + /// symmetric (essentially: undirected). + void construct(const Layout& from, const Layout& to) { + for (const auto u : device_->qubits()) { + for (const auto v : device_->neighboursOf(u)) { + if (shouldAddEdge(u, v, from, to)) { + f_.addEdge(u, v); + } + } + } + } + + /// Try to find a directed cycle in the F graph. If there is one, + /// we can apply a happy swap chain. Note that this happy swap chain + /// does not include the final back edge closing the cycle because the + /// first SWAP changes the token (the qubit) on the target, invalidating + /// the edge in F. + [[nodiscard]] std::optional> + findHappySWAPChain() const { + const auto optCycle = f_.findCycle(); + if (!optCycle) { + return std::nullopt; + } + const auto& cycle = *optCycle; + + SmallVector swaps; + for (size_t i = cycle.size() - 1; i > 0; --i) { + swaps.emplace_back(cycle[i], cycle[i - 1]); + } + return swaps; + } + + /// Find an unhappy SWAP. That is, find an edge (u, v), where exchanging u + /// and v, reduces u's distance to its target location (by one) and + /// increases v's distance from 0 (already at the correct location) to one. + [[nodiscard]] std::optional findUnhappySWAP() const { + for (const auto u : f_.getNodes()) { + for (const auto v : f_.getNeighbours(u)) { + if (f_.getDegree(v) == 0) { + return {{u, v}}; + } + } + } + + return std::nullopt; + } + + /// Reset the F graph for rebuilding. + void reset() { f_.clearEdges(); } + + private: + /// Return true, if moving the program qubit on hardware qubit u to hardware + /// qubit v brings it closer to its destination hardware qubit. + [[nodiscard]] bool shouldAddEdge(const size_t u, const size_t v, + const Layout& from, + const Layout& to) const { + const auto dest = to.getHardwareIndex(from.getProgramIndex(u)); + return device_->distanceBetween(v, dest) < + device_->distanceBetween(u, dest); + } + + Graph f_; + std::shared_ptr device_; + }; + public: + /// Construct default mapping pass. MappingPass() = default; - explicit MappingPass(MappingPassOptions options) : MappingPassBase(options) {} - explicit MappingPass(size_t nqubits, const Edges& coupling, - MappingPassOptions options = {}) - : MappingPassBase(options), device(nqubits, coupling) {} + + /// Construct default mapping pass with options. + explicit MappingPass(const MappingPassOptions& options) + : MappingPassBase(options) {} + + /// Construct mapping from coupling set. + explicit MappingPass(const DenseSet>& couplingSet, + const MappingPassOptions& options) + : MappingPassBase(options), + device(std::make_shared(couplingSet)) {} protected: void runOnOperation() override { - assert(alpha > 0 && "runOnOperation: expected alpha > 0"); - assert(niterations > 0 && "runOnOperation: expected niterations > 0"); - assert(ntrials > 0 && "runOnOperation: expected ntrials > 0"); + assert(alpha > 0 && "expected alpha > 0"); + assert(niterations > 0 && "expected niterations > 0"); + assert(ntrials > 0 && "expected ntrials > 0"); + + if (!device) { + llvm::reportFatalUsageError("No device specified!"); + } - std::mt19937_64 rng{seed}; IRRewriter rewriter(&getContext()); - ModuleOp m = getOperation(); - auto func = getEntryPoint(m); + auto mod = getOperation(); + auto func = getEntryPoint(mod); if (!func) { - m.emitError() << "does not contain an entry point function"; + mod.emitError() << "does not contain an entry point function"; signalPassFailure(); return; } @@ -400,93 +358,125 @@ struct MappingPass : impl::MappingPassBase { return; } - if (comp->size() > device.nqubits()) { - m.emitError() << "requires " + Twine(comp.value().size()) + - " qubits. However, the architecture only supports " + - Twine(device.nqubits()) + "qubits."; + auto& body = func.getFunctionBody(); + auto& [wires, infos] = *comp; + + if (wires.size() > device->nqubits()) { + func.emitError() + << "requires " + Twine(wires.size()) + + " qubits. However, the architecture only supports " + + Twine(device->nqubits()) + "qubits."; signalPassFailure(); return; } - // Create trials for initial layout refining. Currently, this includes - // `ntrials` many random layouts. - SmallVector trials; - trials.reserve(ntrials); - for (size_t i = 0; i < ntrials; ++i) { - trials.emplace_back(Layout::random(device.nqubits(), rng())); - } - - // Execute each of the trials (possibly in parallel). Collect the results - // and find the one with the fewest SWAPs on the final backwards pass. - parallelForEach(&getContext(), trials, [&, this](Trial& trial) { - if (const auto res = refineLayout(*comp, trial.layout); succeeded(res)) { - trial.success = true; - trial.nswaps = *res; - } - }); - - Trial* best = findBestTrial(trials); - if (best == nullptr) { - func.emitError() << "failed to find the best layout trial"; + auto layout = generateLayout(wires, infos); + if (failed(layout)) { + func->emitError() << "failed to refine random initial layouts."; signalPassFailure(); return; } - // Perform placement and hot routing by inserting SWAPs into the IR. - auto placedWires = place(func, best->layout, rewriter); + std::tie(wires, infos) = std::move(place(body, *layout, rewriter)); + + Statistics stats; + RoutingBundle bundle{.wires = std::move(wires), + .infos = std::move(infos), + .layout = std::move(*layout)}; + const auto res = route( - placedWires, best->layout, &rewriter); - if (failed(res)) { - func.emitError() << "failed to map the " << func.getName() << " function"; + bundle, stats, &rewriter); + if (res.failed()) { + func.emitError() << "failed to map the function"; signalPassFailure(); return; } // Collect statistics. - numSwaps += *res; + numSwaps += stats.nswaps; // Fix SSA Dominance issues. - for_each(func.getFunctionBody().getBlocks(), - [](Block& b) { sortTopologically(&b); }); + for_each(body.getBlocks(), [](Block& b) { sortTopologically(&b); }); } private: - /** - * @brief Collect wires of the quantum computation before placement. - * @details - * The mapping pass currently assumes that the quantum computation allocates - * all tensors at the start of the function. The required qubits are extracted - * from these tensors and used for the computation. Finally, the qubits are - * inserted back into the tensors at the end of the function. - * Thus, a valid program has the following structure: - * - * T ⨉ [qtensor::AllocOp] - * → N ⨉ [qtensor::ExtractOp] - * → (Computation) - * → N ⨉ [qtensor::InsertOp] - * → T ⨉ [qtensor::DeallocOp] - * - * @returns a vector of wire iterator, or failure() if any of the above - * assumptions are violated. - */ - static FailureOr> + /// Extend the init arguments of an `scf::ForOp` by adding a given range of + /// additional SSA values. Replaces the existing operation and returns the + /// newly created one. + static scf::ForOp extend(scf::ForOp forOp, ValueRange addons, + IRRewriter& rewriter) { + OpBuilder::InsertionGuard guard(rewriter); + rewriter.setInsertionPoint(forOp); + + const auto res = + forOp.replaceWithAdditionalIterOperands(rewriter, addons, true); + assert(succeeded(res)); + auto newForOp = cast(*res); + + for (const auto [before, after] : llvm::zip_equal( + addons, newForOp.getResults().take_back(addons.size()))) { + rewriter.replaceAllUsesExcept(before, after, newForOp); + } + return newForOp; + } + + /// Extend the qubit arguments of an `IfOp` by adding a given range of + /// additional SSA values. Replaces the existing operation and returns the + /// newly created one. + static IfOp extend(IfOp ifOp, ValueRange addons, IRRewriter& rewriter) { + OpBuilder::InsertionGuard guard(rewriter); + rewriter.setInsertionPoint(ifOp); + + auto newIfOp = ifOp.replaceWithAdditionalQubits(rewriter, addons); + + for (const auto [before, after] : llvm::zip_equal( + addons, newIfOp->getResults().take_back(addons.size()))) { + rewriter.replaceAllUsesExcept(before, after, newIfOp); + } + + return newIfOp; + } + + /// Return the wires of a dynamic computation. + /// The mapping pass currently assumes that + /// - there are no `qco.alloc` operation + /// - there is an "extraction" and "insertion" phase, where the i-th extract + /// defines the i-th program qubit + /// Thus, supported programs have the following structure: + /// + /// T ⨉ [qtensor::AllocOp] + /// → N ⨉ [qtensor::ExtractOp] + /// → (Computation) + /// → N ⨉ [qtensor::InsertOp] + /// → T ⨉ [qtensor::DeallocOp] + /// + /// If any of the above assumptions are violated, the function returns + /// failure. + static FailureOr> getComputation(func::FuncOp func) { if (!func.getOps().empty()) { - func.emitError() << "must not contain qco.alloc operations"; - return failure(); + return func.emitError() << "must not contain qco.alloc operations"; } - SmallVector wires; - for (auto tensor : func.getOps()) { + Wires wires; + WireInfos infos; + + for (auto alloc : func.getOps()) { bool isInitPhase = true; - TensorIterator it(tensor.getResult()); + TensorIterator it(alloc.getResult()); for (; it != std::default_sentinel; ++it) { if (auto extract = dyn_cast(it.operation())) { if (!isInitPhase) { - func.emitError() << "must extract and insert all qubits at once."; - return failure(); + return func.emitError() + << "must extract and insert all qubits at once."; } - wires.emplace_back(extract.getResult()); + + const auto qubit = extract.getResult(); + const auto index = wires.size(); + + wires.emplace_back(qubit); + infos.map(index, index); + continue; } @@ -496,36 +486,36 @@ struct MappingPass : impl::MappingPassBase { } } } - return wires; + + return std::make_pair(wires, infos); } - /** - * @brief Perform placement by replacing dynamic with static qubits. - * @details - * Creates static qubits and replaces the extracted qubits with it. - * Moreover, the function extends the computation with as many static qubits - * as the architecture supports. - * @returns a vector of wire iterators, where the i-th wire points at the i-th - * static program qubit. - */ - static SmallVector - place(func::FuncOp func, const Layout& layout, IRRewriter& rewriter) { + /// Perform placement by + /// - initializing as many hardware qubits as the architecture supports + /// - replacing dynamic with static qubits + /// - extending the inputs of `scf::ForOp` to all hardware qubits. + /// + /// Analogously to the getComputation function, the i-th extract + /// operation defines the i-th program qubit. + static std::pair place(Region& body, const Layout& layout, + IRRewriter& rewriter) { SmallVector staticOps; staticOps.reserve(layout.nqubits()); // Create and save static qubit operations. - rewriter.setInsertionPointToStart(&func.getFunctionBody().front()); + rewriter.setInsertionPointToStart(&body.front()); for (size_t i = 0; i < layout.nqubits(); ++i) { - const auto op = StaticOp::create(rewriter, func.getLoc(), i); + const auto op = StaticOp::create(rewriter, body.getLoc(), i); staticOps.emplace_back(op); rewriter.setInsertionPointAfter(op); } // Replace extract ops and collect in program-qubit order. - SmallVector placedWires(layout.nqubits()); - size_t prog = 0UL; - for (auto alloc : make_early_inc_range(func.getOps())) { + Wires wires; + WireInfos infos; + + for (auto alloc : make_early_inc_range(body.getOps())) { TensorIterator it(alloc.getResult()); while (it != std::default_sentinel) { // Get the operation and early increment to avoid issues after erasure. @@ -534,6 +524,7 @@ struct MappingPass : impl::MappingPassBase { TypeSwitch(curr) .Case([&](auto op) { + const auto prog = wires.size(); const auto hw = layout.getHardwareIndex(prog); const auto qubit = staticOps[hw].getQubit(); @@ -541,8 +532,8 @@ struct MappingPass : impl::MappingPassBase { rewriter.replaceAllUsesWith(op.getOutTensor(), op.getTensor()); rewriter.eraseOp(op); - placedWires[prog] = WireIterator(qubit); - ++prog; + wires.emplace_back(qubit); + infos.map(prog, prog); }) .Case([&](auto op) { rewriter.setInsertionPointAfter(op); @@ -557,84 +548,162 @@ struct MappingPass : impl::MappingPassBase { } // Create sinks for remaining, unused, static qubits. - rewriter.setInsertionPoint(func.getFunctionBody().back().getTerminator()); - for (; prog < layout.nqubits(); ++prog) { + + rewriter.setInsertionPoint(body.back().getTerminator()); + for (size_t prog = wires.size(); prog < layout.nqubits(); ++prog) { const auto hw = layout.getHardwareIndex(prog); const auto qubit = staticOps[hw].getQubit(); - placedWires[prog] = WireIterator(qubit); - SinkOp::create(rewriter, func->getLoc(), qubit); - } - return placedWires; - } + wires.emplace_back(qubit); + infos.map(prog, prog); - /** - * @brief Find the best trial result in terms of the number of SWAPs. - * @returns the best trial result or nullptr if no result is valid. - */ - [[nodiscard]] static Trial* findBestTrial(MutableArrayRef trials) { - Trial* best = nullptr; - for (auto& trial : trials) { - if (!trial.success) { - continue; - } + SinkOp::create(rewriter, body.getLoc(), qubit); + } - if (best == nullptr || best->nswaps > trial.nswaps) { - best = &trial; + // Finally, update the SCF operations such that they take all static qubits + // as input. To handle recursively nested SCF operations, use a stack of + // (region, mapping) pairs. + + SmallVector>> stack; + stack.emplace_back(body, DenseSet{}); + + while (!stack.empty()) { + for (auto [region, qubits] = stack.pop_back_val(); + Operation& op : make_early_inc_range(region.getOps())) { + TypeSwitch(&op) + .Case( + [&](StaticOp staticOp) { qubits.insert(staticOp.getQubit()); }) + .Case([&](UnitaryOpInterface& uOp) { + for (const auto [pred, succ] : llvm::zip_equal( + uOp.getInputQubits(), uOp.getOutputQubits())) { + qubits.insert(succ); + qubits.erase(pred); + } + }) + .Case([&](scf::ForOp forOp) { + assert(qubits.size() == layout.nqubits()); + + llvm::for_each(forOp.getInits(), + [&](Value v) { qubits.erase(v); }); + + auto newForOp = extend(forOp, to_vector(qubits), rewriter); + for (const auto [init, result] : llvm::zip_equal( + newForOp.getInits(), *newForOp.getLoopResults())) { + qubits.insert(result); + qubits.erase(init); + } + + stack.emplace_back( + newForOp.getRegion(), + DenseSet(newForOp.getRegionIterArgs().begin(), + newForOp.getRegionIterArgs().end())); + }) + .Case([&](IfOp ifOp) { + assert(qubits.size() == layout.nqubits()); + + llvm::for_each(ifOp.getQubits(), + [&](Value v) { qubits.erase(v); }); + + auto newIfOp = extend(ifOp, to_vector(qubits), rewriter); + + for (const auto [qubit, result] : + llvm::zip_equal(newIfOp.getQubits(), newIfOp.getResults())) { + qubits.insert(result); + qubits.erase(qubit); + } + + const auto thenArgs = newIfOp.getThenRegion().getArguments(); + const auto elseArgs = newIfOp.getElseRegion().getArguments(); + stack.emplace_back( + newIfOp.getThenRegion(), + DenseSet(thenArgs.begin(), thenArgs.end())); + stack.emplace_back( + newIfOp.getElseRegion(), + DenseSet(elseArgs.begin(), elseArgs.end())); + }) + .Case([&](auto resetOp) { + qubits.insert(resetOp.getQubitOut()); + qubits.erase(resetOp.getQubitIn()); + }) + .Case([&](auto) { + llvm::reportFatalInternalError("unexpected dynamic qubit alloc"); + }); } } - return best; + return {wires, infos}; } - /** - * @brief Refine the trial's layout and count #swaps for the final backwards - * pass. - * @details Use the SABRE Approach to improve the initial layout: - * Traverse the layers from left-to-right-to-left and cold-route - * along the way. Repeat this procedure "niterations" times. - * @returns failure() if routing fails. - */ - FailureOr refineLayout(SmallVector wires, - Layout& layout) { - size_t nswaps{0}; - for (size_t i = 0; i < niterations; ++i) { - if (failed(route(wires, layout))) { - return failure(); + /// Execute `ntrials` many (parallel) initial layout refinement trials and + /// return the heuristically best one. + /// + /// The function uses the SABRE Approach to improve the initial layout: + /// Traverse the layers of the program from left-to-right-to-left and + /// cold-route along the way. Repeat this procedure "niterations" times and + /// finally find the trial with the fewest SWAPs on the final backwards pass + /// and return the respective layout. + FailureOr generateLayout(const Wires& wires, const WireInfos& infos) { + std::mt19937_64 rng{seed}; + + struct Trial { + RoutingBundle bundle; + Statistics stats{}; + bool success{false}; + }; + + SmallVector trials; + trials.reserve(ntrials); + for (size_t i = 0; i < ntrials; ++i) { + trials.emplace_back( + RoutingBundle{.wires = wires, + .infos = infos, + .layout = Layout::random(device->nqubits(), rng())}); + } + + parallelForEach(&getContext(), trials, [&, this](Trial& t) { + for (size_t i = 0; i < niterations; ++i) { + if (route(t.bundle, t.stats).failed()) { + return; + } + t.stats.nswaps = 0; + if (route(t.bundle, t.stats).failed()) { + return; + } } - const auto resB = route(wires, layout); - if (failed(resB)) { - return failure(); + t.success = true; + }); + + Trial* best = nullptr; + for (Trial& t : trials) { + if (t.success && + (best == nullptr || best->stats.nswaps > t.stats.nswaps)) { + best = &t; } - nswaps = *resB; } - return nswaps; + if (best == nullptr) { + return failure(); + } + + return best->bundle.layout; } - /** - * @brief Perform A* search to find a sequence of SWAPs that makes the - * two-qubit operations inside the first layer (the front) executable. - * @details - * The iteration budget is b^{3} node expansions, i.e. roughly a depth-3 - * search in a tree with branching factor b. A hard cap prevents impractical - * runtimes on larger architectures. - * - * The branching factor b of the A* search is the product of the - * architecture's maximum qubit degree and the maximum number of two-qubit - * gates in any layer: - * - * b = maxDegree × ⌈N/2⌉ - * - * @returns a vector of hardware-index pairs (each denoting a SWAP) or - * failure() if A* fails. - */ - [[nodiscard]] FailureOr> - search(const Window& window, const Layout& layout) { + /// Perform A* search to find a sequence of SWAPs that makes all two-qubit ops + /// inside the first layer executable. + /// + /// The iteration budget is b^{3} node expansions, i.e. roughly a depth-3 + /// search in a tree with branching factor b, where b is the product of the + /// architecture's maximum qubit degree and the maximum number of two-qubit + /// gates in any layer: `b = maxDegree × ⌈N/2⌉`. A hard cap prevents + /// impractical runtimes on larger architectures. + /// + /// Returns `failure`, if the A* search fails. + FailureOr> search(const Window& window, + const Layout& layout) const { constexpr size_t cap = 25'000'000UL; - const size_t b = device.maxDegree() * ((device.nqubits() + 1) / 2); + const size_t b = device->maxDegree() * ((device->nqubits() + 1) / 2); const size_t budget = std::min(b * b * b, cap); const Parameters params{.alpha = alpha, .lambda = lambda}; @@ -645,14 +714,14 @@ struct MappingPass : impl::MappingPassBase { // Early exit, if the root node is a goal node already. Node* root = std::construct_at(arena.Allocate(), layout); - if (root->isGoal(window.front(), device)) { + if (root->isGoal(window.front(), *device)) { return SmallVector{}; } frontier.emplace(root); - DenseMap, size_t> bestDepth; - DenseSet expansionSet; + DenseMap, size_t> bestDepth; + SmallVector expansionSet; size_t i = 0; while (!frontier.empty() && i < budget) { @@ -667,8 +736,8 @@ struct MappingPass : impl::MappingPassBase { const auto [it, inserted] = bestDepth.try_emplace( curr->layout.getProgramToHardware(), curr->depth); if (!inserted) { - const auto otherDepth = it->getSecond(); - if (curr->depth >= otherDepth) { + if (const auto otherDepth = it->getSecond(); + curr->depth >= otherDepth) { ++i; continue; } @@ -676,13 +745,13 @@ struct MappingPass : impl::MappingPassBase { it->second = curr->depth; } - // If the currently visited node is a goal node, reconstruct the sequence - // of SWAPs from this node to the root. + // If the currently visited node is a goal node, reconstruct the + // sequence of SWAPs from this node to the root. - if (curr->isGoal(window.front(), device)) { + if (curr->isGoal(window.front(), *device)) { SmallVector seq(curr->depth); size_t j = seq.size() - 1; - for (Node* n = curr; n->parent != nullptr; n = n->parent) { + for (const Node* n = curr; n->parent != nullptr; n = n->parent) { seq[j] = n->swap; --j; } @@ -691,21 +760,21 @@ struct MappingPass : impl::MappingPassBase { } // Given a layout, create child-nodes for each possible SWAP - // between two neighbouring hardware qubits. + // between two neighboring hardware qubits. expansionSet.clear(); - const auto& [q0, q1] = window.front(); - for (const auto prog : {q0, q1}) { + for (const auto& [q0, q1] = window.front(); const auto prog : {q0, q1}) { for (const auto hw0 = curr->layout.getHardwareIndex(prog); - const auto hw1 : device.neighboursOf(hw0)) { + const auto hw1 : device->neighboursOf(hw0)) { // Ensure consistent hashing/comparison. const IndexPairType swap = std::minmax(hw0, hw1); - if (!expansionSet.insert(swap).second) { + if (is_contained(expansionSet, swap)) { continue; } + expansionSet.push_back(swap); frontier.emplace(std::construct_at(arena.Allocate(), curr, swap, - window, device, params)); + window, *device, params)); } } @@ -715,85 +784,171 @@ struct MappingPass : impl::MappingPassBase { return failure(); } - /** - * @brief Skip a qubit-pair block. - * @details Traverses the pair of wire iterators in tandem until a two-qubit - * operation is found. If the two-qubit operation is equivalent, continue. - * Otherwise stop. - */ - template - static void skipQubitPairBlock(WireIterator& w0, WireIterator& w1) { - using Traits = WireTraversalTraits; + /// Return the SWAP sequence to move from one layout to another. + /// Implements the 4-Approximation algorithm described in arXiv:1602.05150v3. + [[nodiscard]] SmallVector restore(const Layout& from, + const Layout& to) const { + Layout curr(from); + FGraph f(device); + SmallVector swaps; - WireIterator curr0(w0); - WireIterator curr1(w1); while (true) { - while (Traits::isActive(curr0)) { - std::ranges::advance(curr0, Traits::stride()); + f.reset(); + f.construct(curr, to); + + if (const auto happy = f.findHappySWAPChain()) { + for (const auto& swap : *happy) { + swaps.emplace_back(swap); + curr.swap(swap.first, swap.second); + } + continue; } - if (curr0 == std::default_sentinel) { - return; + // If there are no happy or unhappy swaps anymore, + // the final placement of every token is reached. + + const auto unhappy = f.findUnhappySWAP(); + if (!unhappy) { + break; } - while (Traits::isActive(curr1)) { - std::ranges::advance(curr1, Traits::stride()); + swaps.emplace_back(*unhappy); + curr.swap(unhappy->first, unhappy->second); + } + + return swaps; + } + + /// Return a pair of SWAP sequences to transform two layouts into each other. + /// Inspired by the 4-Approximation algorithm described in arXiv:1602.05150v3, + /// with the key difference that the goal permutation is not static. + [[nodiscard]] std::pair, + SmallVector> + converge(const Layout& lhs, const Layout& rhs) const { + std::array layouts{Layout(lhs), Layout(rhs)}; + std::array graphs{FGraph(device), FGraph(device)}; + std::array, 2> swaps{}; + + std::mt19937 gen(seed); + std::uniform_int_distribution coin(0, 1); + + while (true) { + size_t i = 0; + for (; i < 2; ++i) { + FGraph& f = graphs[i]; + + f.reset(); + f.construct(layouts[i], layouts[(i + 1) % 2]); + + if (const auto happy = f.findHappySWAPChain()) { + for (const auto& swap : *happy) { + swaps[i].emplace_back(swap); + layouts[i].swap(swap.first, swap.second); + } + break; + } } - if (curr1 == std::default_sentinel) { - return; + // If we exit early from the loop, we've found a happy SWAP chain. + if (i != 2) { + continue; } - if (curr0.operation() != curr1.operation()) { - return; + // Otherwise, we randomly apply an unhappy SWAP to one of the layouts. + // If there is no happy or unhappy swaps anymore, the final placement of + // every token is reached. + + i = coin(gen); + + const auto unhappy = graphs[i].findUnhappySWAP(); + if (!unhappy) { + break; } - // Handle two-qubit barrier edge case explicitly. - if (auto barrier = dyn_cast(curr0.operation())) { - if (barrier.getNumQubits() != 2) { + swaps[i].emplace_back(*unhappy); + layouts[i].swap(unhappy->first, unhappy->second); + } + + return {std::move(swaps[0]), std::move(swaps[1])}; + } + + /// Skip to the end of the two-qubit block for both wire iterators, where + /// initially both must point at the same two-qubit operation. + template + static void skipQubitPairBlock(WireIterator& it0, WireIterator& it1) { + using Traits = WireTraversalTraits; + + // Traverses the pair of wire iterators in tandem until a two-qubit + // operation is found. If the two-qubit operation is equivalent, continue. + // Otherwise, stop. + + std::array block{it0, it1}; + while (true) { + for (auto& it : block) { + while (Traits::isActive(it)) { + std::ranges::advance(it, Traits::stride()); + + if (it.operation() == nullptr) { // isa + return; + } + + if (auto u = dyn_cast(it.operation()); + u && u.getNumQubits() > 1) { + // Handle two-qubit barrier edge case explicitly. + if (isa(u) && u.getNumQubits() != 2) { + return; + } + // Otherwise stop for subsequent two-qubit unitary comparison. + break; + } + } + + if (it == std::default_sentinel) { return; } } - w0 = curr0; - w1 = curr1; + if (block[0].operation() != block[1].operation()) { + return; + } + + it0 = block[0]; + it1 = block[1]; } } - /** - * @brief Build and return window of layers. - * @details Traverses the circuit-layers until the desired window sizes is - * reached. Assumes that wires[i] = i-th program qubit. The size of the window - * is 1 + nlookahead. - * @returns window of layers. - */ + /// Return a window of layers with a maximum size of `1 + nlookahead`. template - Window getWindow(ArrayRef baseWires) { + Window getWindow(Wires wires, const WireInfos& infos) { Window window; window.reserve(1 + nlookahead); - SmallVector wires(baseWires); - std::ignore = walkProgramGraph( + walkProgramGraph( wires, [&](const ReadyRange& ready, ReleasedOps& released) { if (ready.empty()) { return WalkResult::advance(); } - for (const auto& [op, progs] : ready) { - if (isa(op)) { - released.emplace_back(op); - continue; - } + for (const auto& [op, indices] : ready) { + if (auto u = dyn_cast(op)) { + const auto i0 = indices[0]; + const auto i1 = indices[1]; + + const auto prog0 = infos.lookupProgram(i0); + const auto prog1 = infos.lookupProgram(i1); - const auto p0 = progs[0]; - const auto p1 = progs[1]; - window.emplace_back(p0, p1); - if (window.size() == 1 + nlookahead) { - return WalkResult::interrupt(); + window.emplace_back(prog0, prog1); + if (window.size() == 1 + nlookahead) { + return WalkResult::interrupt(); + } + + skipQubitPairBlock(wires[i0], wires[i1]); + released.emplace_back(u); + return WalkResult::advance(); } - skipQubitPairBlock(wires[p0], wires[p1]); - released.emplace_back(wires[p0].operation()); + released.emplace_back(op); + return WalkResult::advance(); } return WalkResult::advance(); @@ -802,145 +957,333 @@ struct MappingPass : impl::MappingPassBase { return window; } - /** - * @brief Advance past all executable gates. - * @details Traverses the multi-qubit gates of the circuit until no more - * executable gates are found. - */ + /// Insert SWAP operations, exchanging two qubits, virtually + /// (`RoutingMode::Cold`) or into the IR (`RoutingMode::Hot`). The function + /// expects that each wire points at the correct insertion point. + template + static void insertSWAPs(ArrayRef swaps, RoutingBundle& bundle, + Statistics& stats, IRRewriter* rewriter) { + auto& [wires, infos, layout] = bundle; + for (const auto& [hw0, hw1] : swaps) { + if constexpr (Mode == RoutingMode::Hot) { + const auto [prog0, prog1] = layout.getProgramIndices(hw0, hw1); + + const auto i0 = infos.lookupIndex(prog0); + const auto i1 = infos.lookupIndex(prog1); + + auto& w0 = wires[i0]; + auto& w1 = wires[i1]; + + const auto in0 = w0.qubit(); + const auto in1 = w1.qubit(); + + rewriter->setInsertionPointAfterValue(in0); // Valid bc. Hot => Forward. + auto swapOp = SWAPOp::create(*rewriter, in0.getLoc(), in0, in1); + + const auto out0 = swapOp.getQubit0Out(); + const auto out1 = swapOp.getQubit1Out(); + + rewriter->replaceAllUsesExcept(in0, out1, swapOp); + rewriter->replaceAllUsesExcept(in1, out0, swapOp); + + infos.swap(prog0, prog1); + + std::advance(w0, 1); // Move to SWAP. + std::advance(w1, 1); + } + + layout.swap(hw0, hw1); + } + + stats.nswaps += swaps.size(); + } + + /// Advance past all executable gates and return operations with nested + /// regions and the respective wire indices. Stops when no more executable + /// gates are found. After the function returns, the wires point at the + /// results of non-executable gates or operations with nested regions. template - void skipExecutableGates(MutableArrayRef wires, - Layout& layout) { - std::ignore = walkProgramGraph( + RecursiveRoutingStack advance(Wires& wires, const WireInfos& infos, + const Layout& layout) { + RecursiveRoutingStack stack; + + // Advance wires past all executable gates and push operations with + // nested regions and the respective wire indices of their inputs onto the + // result stack. + + walkProgramGraph( wires, [&](const ReadyRange& ready, ReleasedOps& released) { if (ready.empty()) { return WalkResult::advance(); } - for (const auto& [op, progs] : ready) { - if (isa(op)) { - released.emplace_back(op); - continue; - } - - const auto [hw0, hw1] = - layout.getHardwareIndices(progs[0], progs[1]); - - if (device.areAdjacent(hw0, hw1)) { - released.emplace_back(op); - } + for (const auto& [readyOp, indices] : ready) { + TypeSwitch(readyOp) + .template Case( + [&](BarrierOp op) { released.emplace_back(op); }) + .template Case([&](UnitaryOpInterface op) { + const auto prog0 = infos.lookupProgram(indices[0]); + const auto prog1 = infos.lookupProgram(indices[1]); + if (const auto [hw0, hw1] = + layout.getHardwareIndices(prog0, prog1); + device->areAdjacent(hw0, hw1)) { + released.emplace_back(op); + } + }) + .template Case( + [&](auto op) { stack.emplace_back(op, indices); }); } - // Stop, if there are no more ready AND executable gates. if (released.empty()) { return WalkResult::interrupt(); } return WalkResult::advance(); }); + + return stack; } - /** - * @brief Route via SWAP insertion. - * @details Iterates over a dynamically computed window of layers and uses A* - * search to find a sequence of SWAPs that makes that layer executable. - * Depending on the template parameter, this function only updates - * (and hence modifies) the layout or also inserts the SWAPs into the IR. - * @returns failure() if A* search isn't able to find a solution, the number - * of SWAPs otherwise. - */ - template - FailureOr route(SmallVector& wires, Layout& layout, - IRRewriter* rewriter = nullptr) { - using Traits = WireTraversalTraits; + /// Processes the recursive stack item by routing the nested operation and + /// inserting epilogue SWAPs. + template + requires(Mode != RoutingMode::Hot || Direction == WireDirection::Forward) + LogicalResult dispatch(const RecursiveRoutingStackItem& item, + RoutingBundle& parent, Statistics& stats, + IRRewriter* rewriter = nullptr) { + const auto& [op, indices] = item; + return TypeSwitch(op) + .template Case([&](scf::ForOp forOp) { + RoutingBundle child{.layout = parent.layout}; + + // Map parent (results) to child values (iter args). Going + // forwards, the recursive routing starts at block + // arguments, while the backwards go starts at the yielded + // values. + + for (size_t i : indices) { + const auto prog = parent.infos.lookupProgram(i); + const auto res = cast(parent.wires[i].qubit()); + const auto arg = forOp.getTiedLoopRegionIterArg(res); + const auto index = child.wires.size(); + + if constexpr (Direction == WireDirection::Forward) { + child.wires.emplace_back(arg); + } else { + const auto yield = forOp.getTiedLoopYieldedValue(arg)->get(); + child.wires.emplace_back(yield); + } + child.infos.map(index, prog); + } - size_t nswaps{0}; - while (true) { - skipExecutableGates(wires, layout); + if (failed(route(child, stats, rewriter))) { + return failure(); + } - const auto window = getWindow(wires); - if (window.empty()) { - break; - } + const auto swaps = restore(child.layout, parent.layout); - if constexpr (mode == RoutingMode::Hot) { + if constexpr (Mode == RoutingMode::Hot) { - // At this point the wire iterators either point to - // std::default_sentinel or a multi-qubit gate (including barriers) of - // the current or subsequent layers. The former must be decremented - // twice (sentinel -> sink -> unitary/static). For the latter we simply - // must ensure the insertion point is before the multi-qubit gates. + // After routing the loop body, all iterators point to + // std::default_sentinel. To move the iterators to the + // correct qubit SSA values for the epilogue SWAPs, + // decrement each twice: (sentinel → yield → + // unitary/block arg). - for (auto& it : wires) { - std::ranges::advance(it, it == std::default_sentinel - ? -2 * Traits::stride() - : -Traits::stride()); + for_each(child.wires, [](auto& it) { std::advance(it, -2); }); + } + + insertSWAPs(swaps, child, stats, rewriter); + + if constexpr (Mode == RoutingMode::Hot) { + sortTopologically(forOp.getBody()); + } + + // Finally, move past the operation with nested regions by + // incrementing the respective global wires. + + for_each(indices, [&](size_t i) { + std::advance(parent.wires[i], + WireTraversalTraits::stride()); + }); + + return success(); + }) + .template Case([&](IfOp ifOp) { + std::array children{RoutingBundle{.layout = parent.layout}, + RoutingBundle{.layout = parent.layout}}; + + // Map parent (results) to child values (qubits). Going + // forwards, the recursive routing starts at block + // arguments, while the backwards go starts at the yielded + // values. + + for (size_t i : indices) { + const auto prog = parent.infos.lookupProgram(i); + const auto res = cast(parent.wires[i].qubit()); + const auto index = children[0].wires.size(); + + OpOperand* qubit = ifOp.getTiedQubit(res); + const std::array args{ifOp.getTiedThenBlockArgument(qubit), + ifOp.getTiedElseBlockArgument(qubit)}; + + if constexpr (Direction == WireDirection::Forward) { + for (size_t j = 0; j < children.size(); ++j) { + children[j].wires.emplace_back(args[j]); + children[j].infos.map(index, prog); + } + } else { + const std::array yields{ + ifOp.getTiedThenYieldedValue(args[0])->get(), + ifOp.getTiedElseYieldedValue(args[1])->get()}; + for (size_t j = 0; j < children.size(); ++j) { + children[j].wires.emplace_back(yields[j]); + children[j].infos.map(index, prog); + } + } + } + + for (auto& child : children) { + if (failed(route(child, stats, rewriter))) { + return failure(); + } + } + + if constexpr (Mode == RoutingMode::Hot) { + + // After routing the branch body, all iterators point to + // std::default_sentinel. To move the iterators to the + // correct qubit SSA values for the epilogue SWAPs, + // decrement each twice: (sentinel → yield → + // unitary/block arg). + + for_each(children[0].wires, [](auto& it) { std::advance(it, -2); }); + for_each(children[1].wires, [](auto& it) { std::advance(it, -2); }); + } + + const auto [fst, snd] = + converge(children[0].layout, children[1].layout); + + insertSWAPs(fst, children[0], stats, rewriter); + insertSWAPs(snd, children[1], stats, rewriter); + + if constexpr (Mode == RoutingMode::Hot) { + + // The IfOp implements the SingleBlockImplicitTerminator trait. + assert(ifOp.getThenRegion().hasOneBlock()); + assert(ifOp.getElseRegion().hasOneBlock()); + + sortTopologically(&ifOp.getThenRegion().getBlocks().front()); + sortTopologically(&ifOp.getElseRegion().getBlocks().front()); + } + + // Finally, move past the operation with nested regions by + // incrementing the respective global wires. + + for_each(indices, [&](size_t i) { + std::advance(parent.wires[i], + WireTraversalTraits::stride()); + }); + + return success(); + }) + .Default([](Operation*) { return failure(); }); + } + + /// Iterates over a dynamically computed window of layers and uses A* search + /// to find a SWAP sequence that makes each layer executable. Depending on + /// the template parameter, this function only updates the layout or also + /// inserts the SWAPs into the IR. The function returns `failure` if A* is + /// unable to find a solution. + template + requires(Mode != RoutingMode::Hot || Direction == WireDirection::Forward) + LogicalResult route(RoutingBundle& bundle, Statistics& stats, + IRRewriter* rewriter = nullptr) { + auto& [wires, infos, layout] = bundle; + + while (true) { + + while (true) { + const auto stack = advance(wires, infos, layout); + if (stack.empty()) { + break; + } + for (const auto& item : stack) { + if (dispatch(item, bundle, stats, rewriter) + .failed()) { + return failure(); + } } } + const auto window = getWindow(wires, infos); + if (window.empty()) { + break; + } + const auto swaps = search(window, layout); if (failed(swaps)) { return failure(); } - for (const auto& [hw0, hw1] : *swaps) { - if constexpr (mode == RoutingMode::Hot) { - const auto& [prog0, prog1] = layout.getProgramIndices(hw0, hw1); - const auto& w0 = wires[prog0]; - const auto& w1 = wires[prog1]; - - assert(!isa(w0.operation())); - assert(!isa(w1.operation())); - - const auto in0 = w0.qubit(); - const auto in1 = w1.qubit(); - - rewriter->setInsertionPointAfter(in0.getDefiningOp()); - auto swapOp = SWAPOp::create(*rewriter, in0.getLoc(), in0, in1); + if constexpr (Mode == RoutingMode::Hot) { - const auto out0 = swapOp.getQubit0Out(); - const auto out1 = swapOp.getQubit1Out(); - - rewriter->replaceAllUsesExcept(in0, out1, swapOp); - rewriter->replaceAllUsesExcept(in1, out0, swapOp); - - // Preserve program-indexed wire semantics. - wires[prog0] = WireIterator(out1); - wires[prog1] = WireIterator(out0); + // At this point the wire iterators either point to + // std::default_sentinel or a multi-qubit gate (incl. barriers) of + // the current or subsequent layers. The former must be decremented + // twice (sentinel → sink → unitary/static). For the latter, we + // must ensure the insertion point is before the multi-qubit gates. - assert(isa(w0.operation())); - assert(isa(w1.operation())); + for (auto& it : wires) { + std::advance(it, it == std::default_sentinel ? -2 : -1); } - layout.swap(hw0, hw1); } - if constexpr (mode == RoutingMode::Hot) { + insertSWAPs(*swaps, bundle, stats, rewriter); + + if constexpr (Mode == RoutingMode::Hot) { // After SWAP insertion, a wire is either untouched by the SWAP - // insertion or pointing at a SWAP operation. If the former is the case, - // incrementing the wire iterator will undo the previous decrement, - // leaving it at the same position as before the SWAP insertion. - // Otherwise, an increment will move the iterator to the multi-qubit op - // of the current or subsequent layer or to a sink (and thus - // std::default_sentinel). - - for_each(wires, - [](auto& it) { std::ranges::advance(it, Traits::stride()); }); + // insertion or pointing at a SWAP operation. If the former is the + // case, incrementing the wire iterator will undo the previous + // decrement, leaving it at the same position as before the SWAP + // insertion. Otherwise, an increment will move the iterator to the + // multi-qubit op of the current or subsequent layer or to a sink (and + // thus std::default_sentinel). + + for_each(wires, [](auto& it) { std::advance(it, 1); }); } - - nswaps += swaps->size(); } - return nswaps; + return success(); } - AugmentedDevice device; + std::shared_ptr device; }; } // namespace -std::unique_ptr createMappingPass(size_t nqubits, const Edges& coupling, - MappingPassOptions options) { - return std::make_unique(nqubits, coupling, options); +std::unique_ptr +createMappingPass(const DenseSet>& couplingSet, + MappingPassOptions options) { + + // Verify the assumption that the coupling set is symmetric: + // For every edge (u, v) in the set, (v, u) must also be present. + + for (const auto& [u, v] : couplingSet) { + if (u == v) { + llvm::reportFatalUsageError("Found an invalid (u, u) edge."); + } + + if (!couplingSet.contains({v, u})) { + llvm::reportFatalUsageError("Expected symmetric coupling set: edge (" + + Twine(u) + ", " + Twine(v) + + ") exists but (" + Twine(v) + ", " + + Twine(u) + ") does not."); + } + } + + return std::make_unique(couplingSet, options); } } // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Utils/Algorithms.cpp b/mlir/lib/Dialect/QCO/Utils/Algorithms.cpp deleted file mode 100644 index c8da5817f0..0000000000 --- a/mlir/lib/Dialect/QCO/Utils/Algorithms.cpp +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "mlir/Dialect/QCO/Utils/Algorithms.h" - -#include -#include - -#include -#include -#include - -namespace mlir::qco { -Matrix findAllShortestPaths(size_t n, const Edges& edges) { - Matrix dist(n, SmallVector(n, UINT64_MAX)); - - for (const auto& [u, v] : edges) { - dist[u][v] = 1; - } - for (std::size_t v = 0; v < n; ++v) { - dist[v][v] = 0; - } - - for (std::size_t k = 0; k < n; ++k) { - for (std::size_t i = 0; i < n; ++i) { - for (std::size_t j = 0; j < n; ++j) { - if (dist[i][k] == UINT64_MAX || dist[k][j] == UINT64_MAX) { - continue; // Avoid overflow with "infinite" distances. - } - - const std::size_t sum = dist[i][k] + dist[k][j]; - dist[i][j] = std::min(dist[i][j], sum); - } - } - } - - return dist; -} -} // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Utils/Graph.cpp b/mlir/lib/Dialect/QCO/Utils/Graph.cpp new file mode 100644 index 0000000000..5e3de25add --- /dev/null +++ b/mlir/lib/Dialect/QCO/Utils/Graph.cpp @@ -0,0 +1,148 @@ +/* + * 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/Utils/Graph.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace mlir::qco { +void Graph::addEdge(size_t u, size_t v) { + adj_[u].emplace_back(v); + std::ignore = adj_[v]; // Ensure v exists in the map. +} + +ArrayRef Graph::getNeighbours(size_t id) const { return adj_.at(id); } + +SmallVector Graph::getNodes() const { return to_vector(adj_.keys()); } + +size_t Graph::getMaxDegree() const { + size_t deg = 0; + for (const auto& [u, nbrs] : adj_) { + deg = std::ranges::max(deg, nbrs.size()); + } + return deg; +} + +void Graph::clearEdges() { + for_each(adj_, [](auto& kv) { kv.second.clear(); }); +} + +Graph::DistanceMatrix Graph::getDistMatrix() const { + const auto n = getNumNodes(); + + Graph::DistanceMatrix dist(n, std::numeric_limits::max()); + for (const auto& [u, nbrs] : adj_) { + for (const auto& v : nbrs) { + dist[u][v] = 1; + } + } + for (size_t v = 0; v < n; ++v) { + dist[v][v] = 0; + } + + for (size_t k = 0; k < n; ++k) { + for (size_t i = 0; i < n; ++i) { + for (size_t j = 0; j < n; ++j) { + if (dist[i][k] == std::numeric_limits::max() || + dist[k][j] == std::numeric_limits::max()) { + continue; // Avoid overflow with "infinite" distances. + } + + const size_t sum = dist[i][k] + dist[k][j]; + dist[i][j] = std::min(dist[i][j], sum); + } + } + } + + return dist; +} + +std::optional> Graph::findCycle() const { + enum struct State : uint8_t { Unseen, Seen, Finished }; + + struct Frame { + size_t id; + size_t neighbourIdx; + }; + + SmallVector stack; + llvm::DenseMap parents; + llvm::DenseMap states; + + // Preparation step: Mark all nodes as unseen. + llvm::for_each(adj_.keys(), [&](size_t id) { states[id] = State::Unseen; }); + + for (const auto initId : adj_.keys()) { + // Only start from unseen nodes. + if (states[initId] != State::Unseen) { + continue; + } + + stack.emplace_back(initId, 0); + + while (!stack.empty()) { + Frame& top = stack.back(); + + // If we haven't seen this node before, mark it as seen. + if (states[top.id] == State::Unseen) { + states[top.id] = State::Seen; + } + + auto it = adj_.find(top.id); + assert(it != adj_.end() && "expected node id in adjacency map"); + const auto nbrs = it->getSecond(); + + // Once all neighbours have been visited (indicated by the index + // exceeding the number of neighbours - 1), set the frame on node to + // finished and pop it from the stack. + if (top.neighbourIdx >= nbrs.size()) { + states[top.id] = State::Finished; + stack.pop_back(); + continue; + } + + // Collect the neighbour and advance the index on the + // frame for the next iteration. + const auto nbrId = nbrs[top.neighbourIdx]; + ++top.neighbourIdx; + + if (states[nbrId] == State::Unseen) { + parents[nbrId] = top.id; + stack.emplace_back(nbrId, 0); + } else if (states[nbrId] == State::Seen) { + SmallVector path; + for (auto curr = top.id; curr != nbrId; curr = parents[curr]) { + path.emplace_back(curr); + } + path.emplace_back(nbrId); + std::ranges::reverse(path); + return path; + } + } + + // Preparse stack for next iteration. + stack.clear(); + } + + return std::nullopt; +} + +} // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Utils/Layout.cpp b/mlir/lib/Dialect/QCO/Utils/Layout.cpp new file mode 100644 index 0000000000..9f0a3b4209 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Utils/Layout.cpp @@ -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 + */ + +#include "mlir/Dialect/QCO/Utils/Layout.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace mlir::qco { +Layout Layout::random(const size_t nqubits, const size_t seed) { + SmallVector mapping(nqubits); + std::iota(mapping.begin(), mapping.end(), size_t{0}); + std::ranges::shuffle(mapping, std::mt19937_64{seed}); + + Layout layout(nqubits); + for (const auto [prog, hw] : enumerate(mapping)) { + layout.add(prog, hw); + } + + return layout; +} + +void Layout::add(const size_t prog, const size_t hw) { + assert(prog < programToHardware_.size() && "program index out of bounds"); + assert(hw < hardwareToProgram_.size() && "hardware index out of bounds"); + programToHardware_[prog] = hw; + hardwareToProgram_[hw] = prog; +} + +size_t Layout::getProgramIndex(const size_t hw) const { + assert(hw < hardwareToProgram_.size() && "hardware index out of bounds"); + return hardwareToProgram_[hw]; +} + +size_t Layout::getHardwareIndex(const size_t prog) const { + assert(prog < programToHardware_.size() && "program index out of bounds"); + return programToHardware_[prog]; +} + +void Layout::swap(const size_t hwA, const size_t hwB) { + assert(hwA < hardwareToProgram_.size() && "hardware index out of bounds"); + assert(hwB < hardwareToProgram_.size() && "hardware index out of bounds"); + const auto progA = hardwareToProgram_[hwA]; + const auto progB = hardwareToProgram_[hwB]; + + std::swap(hardwareToProgram_[hwA], hardwareToProgram_[hwB]); + std::swap(programToHardware_[progA], programToHardware_[progB]); +} + +size_t Layout::nqubits() const { return programToHardware_.size(); } + +ArrayRef Layout::getProgramToHardware() const { + return programToHardware_; +} + +} // namespace mlir::qco diff --git a/mlir/lib/Dialect/QCO/Utils/Matrix.cpp b/mlir/lib/Dialect/QCO/Utils/Matrix.cpp index 7f7075cd22..e9911fd3d1 100644 --- a/mlir/lib/Dialect/QCO/Utils/Matrix.cpp +++ b/mlir/lib/Dialect/QCO/Utils/Matrix.cpp @@ -49,24 +49,24 @@ namespace mlir::qco { /// Writes the conjugate transpose of @p in into @p out (square, row-major). static void adjointInto(ArrayRef in, MutableArrayRef out, - const std::size_t dim) { - for (std::size_t row = 0; row < dim; ++row) { - for (std::size_t col = 0; col < dim; ++col) { + const size_t dim) { + for (size_t row = 0; row < dim; ++row) { + for (size_t col = 0; col < dim; ++col) { out[(row * dim) + col] = std::conj(in[(col * dim) + row]); } } } -template -static void assignFixedImpl(std::int64_t& dim, SmallVector& data, +template +static void assignFixedImpl(int64_t& dim, SmallVector& data, const std::array& src) { - dim = static_cast(Dim); + dim = static_cast(Dim); data.assign(src.begin(), src.end()); } -template +template [[nodiscard]] static bool -isApproxFixedImpl(const std::int64_t dim, ArrayRef data, +isApproxFixedImpl(const int64_t dim, ArrayRef data, const std::array& other, const double tol) { if (std::cmp_not_equal(dim, Dim)) { return false; @@ -74,18 +74,18 @@ isApproxFixedImpl(const std::int64_t dim, ArrayRef data, return entriesAreApprox(data, other, tol); } -template +template [[nodiscard]] static bool assignFromDynamicImpl(const DynamicMatrix& src, std::array& dst) { - if (src.rows() != static_cast(Dim) || - src.cols() != static_cast(Dim)) { + if (src.rows() != static_cast(Dim) || + src.cols() != static_cast(Dim)) { return false; } - for (std::size_t row = 0; row < Dim; ++row) { - for (std::size_t col = 0; col < Dim; ++col) { + for (size_t row = 0; row < Dim; ++row) { + for (size_t col = 0; col < Dim; ++col) { dst[(row * Dim) + col] = - src(static_cast(row), static_cast(col)); + src(static_cast(row), static_cast(col)); } } return true; @@ -111,8 +111,8 @@ static void multiply4x4(const ArrayRef lhs, assert(lhs.size() == Matrix4x4::K_SIZE_AT_COMPILE_TIME && rhs.size() == Matrix4x4::K_SIZE_AT_COMPILE_TIME && out.size() == Matrix4x4::K_SIZE_AT_COMPILE_TIME); - for (std::size_t row = 0; row < Matrix4x4::K_ROWS; ++row) { - const std::size_t rowBase = row * Matrix4x4::K_COLS; + for (size_t row = 0; row < Matrix4x4::K_ROWS; ++row) { + const size_t rowBase = row * Matrix4x4::K_COLS; const Complex& a0 = lhs[rowBase + 0]; const Complex& a1 = lhs[rowBase + 1]; const Complex& a2 = lhs[rowBase + 2]; @@ -124,14 +124,39 @@ static void multiply4x4(const ArrayRef lhs, } } +/// Left-applies a 2x2 gate to a row pair (`out = gate * [a; b]`). +static void apply2x2LeftToRowPair(const ArrayRef gate, Complex& a, + Complex& b) { + assert(gate.size() == Matrix2x2::K_SIZE_AT_COMPILE_TIME); + const Complex newA = gate[0] * a + gate[1] * b; + const Complex newB = gate[2] * a + gate[3] * b; + a = newA; + b = newB; +} + +/// Left-applies a 4x4 gate to a column 4-vector (`[a; b; c; d] = gate * +/// [a; b; c; d]`). +static void apply4x4LeftToColumn(const ArrayRef gate, Complex& a, + Complex& b, Complex& c, Complex& d) { + assert(gate.size() == Matrix4x4::K_SIZE_AT_COMPILE_TIME); + const Complex newA = gate[0] * a + gate[1] * b + gate[2] * c + gate[3] * d; + const Complex newB = gate[4] * a + gate[5] * b + gate[6] * c + gate[7] * d; + const Complex newC = gate[8] * a + gate[9] * b + gate[10] * c + gate[11] * d; + const Complex newD = + gate[12] * a + gate[13] * b + gate[14] * c + gate[15] * d; + a = newA; + b = newB; + c = newC; + d = newD; +} + /// Returns true if @p data is approximately the @p dim x @p dim identity /// matrix. -[[nodiscard]] static bool isIdentityEntries(ArrayRef data, - const std::size_t dim, - const double tol) { +[[nodiscard]] static bool +isIdentityEntries(ArrayRef data, const size_t dim, const double tol) { assert(data.size() >= dim * dim); - for (std::size_t row = 0; row < dim; ++row) { - for (std::size_t col = 0; col < dim; ++col) { + for (size_t row = 0; row < dim; ++row) { + for (size_t col = 0; col < dim; ++col) { const Complex& entry = data[(row * dim) + col]; if (row == col) { if (!entryIsApprox(entry, Complex{1.0, 0.0}, tol)) { @@ -146,65 +171,61 @@ static void multiply4x4(const ArrayRef lhs, } /// Returns @p dim as `size_t` after asserting it is non-negative and squarable. -[[nodiscard]] static std::size_t checkedDim(const std::int64_t dim) { +[[nodiscard]] static size_t checkedDim(const int64_t dim) { assert(dim >= 0 && "DynamicMatrix dimension must be non-negative"); - const auto udim = static_cast(dim); - assert( - (udim == 0 || udim <= std::numeric_limits::max() / udim) && - "DynamicMatrix dimension is too large to allocate storage"); + const auto udim = static_cast(dim); + assert((udim == 0 || udim <= std::numeric_limits::max() / udim) && + "DynamicMatrix dimension is too large to allocate storage"); return udim; } /// Returns the flat row-major index for `(row, col)` after bounds checking. -[[nodiscard]] static std::size_t checkedFlatIndex(const std::size_t row, - const std::size_t col, - const std::size_t dim) { +[[nodiscard]] static size_t checkedFlatIndex(const size_t row, const size_t col, + const size_t dim) { assert(row < dim && col < dim && "matrix index out of bounds"); return (row * dim) + col; } -[[nodiscard]] static std::int64_t checkedFlatIndex(const std::int64_t row, - const std::int64_t col, - const std::int64_t dim) { +[[nodiscard]] static int64_t +checkedFlatIndex(const int64_t row, const int64_t col, const int64_t dim) { assert(row >= 0 && col >= 0 && row < dim && col < dim && "matrix index out of bounds"); return (row * dim) + col; } -[[nodiscard]] static std::size_t checkedStorageSize(const std::int64_t dim) { +[[nodiscard]] static size_t checkedStorageSize(const int64_t dim) { const auto udim = checkedDim(dim); return udim * udim; } /// Returns `2^numQubits` as `int64_t` after checking it fits. -[[nodiscard]] static std::int64_t -checkedHilbertDim(const std::size_t numQubits) { - assert(numQubits < std::numeric_limits::digits && +[[nodiscard]] static int64_t checkedHilbertDim(const size_t numQubits) { + assert(numQubits < std::numeric_limits::digits && "Hilbert-space dimension must fit in int64_t"); - return std::int64_t{static_cast(std::uint64_t{1} << numQubits)}; + return static_cast(uint64_t{1} << numQubits); } -static void validateCornerDims(const std::int64_t matrixDim, - const std::int64_t blockDim) { +static void validateCornerDims(const int64_t matrixDim, + const int64_t blockDim) { assert(matrixDim >= 0 && blockDim >= 0 && blockDim <= matrixDim && "block must fit in the bottom-right corner of the matrix"); std::ignore = checkedDim(matrixDim); } /// Copies @p blockData into the bottom-right @p blockDim x @p blockDim corner. -static void copyBottomRightCorner(const std::int64_t matrixDim, +static void copyBottomRightCorner(const int64_t matrixDim, MutableArrayRef matrixData, - const std::int64_t blockDim, + const int64_t blockDim, ArrayRef blockData) { validateCornerDims(matrixDim, blockDim); assert(matrixData.size() >= checkedStorageSize(matrixDim)); assert(blockData.size() >= checkedStorageSize(blockDim)); - const std::int64_t offset = matrixDim - blockDim; - for (std::int64_t row = 0; row < blockDim; ++row) { - for (std::int64_t col = 0; col < blockDim; ++col) { - matrixData[static_cast(((offset + row) * matrixDim) + - offset + col)] = - blockData[static_cast((row * blockDim) + col)]; + const int64_t offset = matrixDim - blockDim; + for (int64_t row = 0; row < blockDim; ++row) { + for (int64_t col = 0; col < blockDim; ++col) { + matrixData[static_cast(((offset + row) * matrixDim) + offset + + col)] = + blockData[static_cast((row * blockDim) + col)]; } } } @@ -215,9 +236,9 @@ static void copyBottomRightCorner(const std::int64_t matrixDim, * Qubit 0 is the MSB of @p stateIndex, matching @ref Matrix4x4::kron and * @ref Matrix2x2::embedInNqubit. */ -[[nodiscard]] static std::size_t qubitBitAt(const std::size_t stateIndex, - const std::size_t numQubits, - const std::size_t qubitIndex) { +[[nodiscard]] static size_t qubitBitAt(const size_t stateIndex, + const size_t numQubits, + const size_t qubitIndex) { return (stateIndex >> (numQubits - 1 - qubitIndex)) & 1U; } @@ -229,12 +250,10 @@ static void copyBottomRightCorner(const std::int64_t matrixDim, * is zero. For a single-qubit embed, pass @p skipB = @p numQubits so only @p * skipA is skipped. */ -[[nodiscard]] static bool otherQubitBitsMatch(const std::size_t row, - const std::size_t col, - const std::size_t numQubits, - const std::size_t skipA, - const std::size_t skipB) { - for (std::size_t q = 0; q < numQubits; ++q) { +[[nodiscard]] static bool +otherQubitBitsMatch(const size_t row, const size_t col, const size_t numQubits, + const size_t skipA, const size_t skipB) { + for (size_t q = 0; q < numQubits; ++q) { if (q == skipA || q == skipB) { continue; } @@ -245,13 +264,12 @@ static void copyBottomRightCorner(const std::int64_t matrixDim, return true; } -Complex& Matrix1x1::operator()(const std::size_t row, const std::size_t col) { +Complex& Matrix1x1::operator()(const size_t row, const size_t col) { assert(row == 0 && col == 0 && "matrix index out of bounds"); return value; } -Complex Matrix1x1::operator()(const std::size_t row, - const std::size_t col) const { +Complex Matrix1x1::operator()(const size_t row, const size_t col) const { assert(row == 0 && col == 0 && "matrix index out of bounds"); return value; } @@ -279,12 +297,11 @@ Matrix1x1& Matrix1x1::operator*=(const Complex& scalar) { Matrix1x1 Matrix1x1::adjoint() const { return fromElements(std::conj(value)); } -Complex& Matrix2x2::operator()(const std::size_t row, const std::size_t col) { +Complex& Matrix2x2::operator()(const size_t row, const size_t col) { return data[checkedFlatIndex(row, col, K_COLS)]; } -Complex Matrix2x2::operator()(const std::size_t row, - const std::size_t col) const { +Complex Matrix2x2::operator()(const size_t row, const size_t col) const { return data[checkedFlatIndex(row, col, K_COLS)]; } @@ -336,8 +353,8 @@ bool Matrix2x2::assignFrom(const DynamicMatrix& src) { return assignFromDynamicImpl(src, data); } -DynamicMatrix Matrix2x2::embedInNqubit(const std::size_t numQubits, - const std::size_t qubitIndex) const { +DynamicMatrix Matrix2x2::embedInNqubit(const size_t numQubits, + const size_t qubitIndex) const { assert(qubitIndex < numQubits && "Invalid qubit index for single-qubit embed"); if (numQubits == 2) { @@ -345,22 +362,22 @@ DynamicMatrix Matrix2x2::embedInNqubit(const std::size_t numQubits, } const auto dim = checkedHilbertDim(numQubits); DynamicMatrix out(dim); - const auto udim = static_cast(dim); - for (std::size_t row = 0; row < udim; ++row) { - for (std::size_t col = 0; col < udim; ++col) { + const auto udim = static_cast(dim); + for (size_t row = 0; row < udim; ++row) { + for (size_t col = 0; col < udim; ++col) { if (!otherQubitBitsMatch(row, col, numQubits, qubitIndex, numQubits)) { continue; } - const std::size_t rowBit = qubitBitAt(row, numQubits, qubitIndex); - const std::size_t colBit = qubitBitAt(col, numQubits, qubitIndex); - out(static_cast(row), static_cast(col)) = + const size_t rowBit = qubitBitAt(row, numQubits, qubitIndex); + const size_t colBit = qubitBitAt(col, numQubits, qubitIndex); + out(static_cast(row), static_cast(col)) = (*this)(rowBit, colBit); } } return out; } -Matrix4x4 Matrix2x2::embedInTwoQubit(const std::size_t qubitIndex) const { +Matrix4x4 Matrix2x2::embedInTwoQubit(const size_t qubitIndex) const { if (qubitIndex == 0) { return Matrix4x4::kron(*this, Matrix2x2::identity()); } @@ -370,12 +387,11 @@ Matrix4x4 Matrix2x2::embedInTwoQubit(const std::size_t qubitIndex) const { llvm::reportFatalInternalError("Invalid qubit index for single-qubit embed"); } -Complex& Matrix4x4::operator()(const std::size_t row, const std::size_t col) { +Complex& Matrix4x4::operator()(const size_t row, const size_t col) { return data[checkedFlatIndex(row, col, K_COLS)]; } -Complex Matrix4x4::operator()(const std::size_t row, - const std::size_t col) const { +Complex Matrix4x4::operator()(const size_t row, const size_t col) const { return data[checkedFlatIndex(row, col, K_COLS)]; } @@ -451,14 +467,13 @@ Matrix4x4 Matrix4x4::kron(const Matrix2x2& lhs, const Matrix2x2& rhs) { } std::array -Matrix4x4::column(const std::size_t col) const { +Matrix4x4::column(const size_t col) const { assert(col < K_COLS && "matrix index out of bounds"); return {data[col], data[K_COLS + col], data[(2 * K_COLS) + col], data[(3 * K_COLS) + col]}; } -void Matrix4x4::setColumn(const std::size_t col, - const ArrayRef values) { +void Matrix4x4::setColumn(const size_t col, const ArrayRef values) { assert(col < K_COLS && "matrix index out of bounds"); assert(values.size() == K_ROWS && "setColumn requires exactly K_ROWS entries"); @@ -468,15 +483,15 @@ void Matrix4x4::setColumn(const std::size_t col, data[(3 * K_COLS) + col] = values[3]; } -ArrayRef Matrix4x4::row(const std::size_t row) const { +ArrayRef Matrix4x4::row(const size_t row) const { assert(row < K_ROWS && "matrix index out of bounds"); return ArrayRef(data).slice(row * K_COLS, K_COLS); } -void Matrix4x4::setRow(const std::size_t row, const ArrayRef values) { +void Matrix4x4::setRow(const size_t row, const ArrayRef values) { assert(row < K_ROWS && "matrix index out of bounds"); assert(values.size() == K_COLS && "setRow requires exactly K_COLS entries"); - const std::size_t rowBase = row * K_COLS; + const size_t rowBase = row * K_COLS; data[rowBase + 0] = values[0]; data[rowBase + 1] = values[1]; data[rowBase + 2] = values[2]; @@ -507,9 +522,9 @@ bool Matrix4x4::assignFrom(const DynamicMatrix& src) { return assignFromDynamicImpl(src, data); } -DynamicMatrix Matrix4x4::embedInNqubit(const std::size_t numQubits, - const std::size_t q0Index, - const std::size_t q1Index) const { +DynamicMatrix Matrix4x4::embedInNqubit(const size_t numQubits, + const size_t q0Index, + const size_t q1Index) const { assert(q0Index < numQubits && q1Index < numQubits && q0Index != q1Index && "Invalid qubit indices for two-qubit embed"); if (numQubits == 2) { @@ -517,25 +532,25 @@ DynamicMatrix Matrix4x4::embedInNqubit(const std::size_t numQubits, } const auto dim = checkedHilbertDim(numQubits); DynamicMatrix out(dim); - const auto udim = static_cast(dim); - for (std::size_t row = 0; row < udim; ++row) { - for (std::size_t col = 0; col < udim; ++col) { + const auto udim = static_cast(dim); + for (size_t row = 0; row < udim; ++row) { + for (size_t col = 0; col < udim; ++col) { if (!otherQubitBitsMatch(row, col, numQubits, q0Index, q1Index)) { continue; } - const std::size_t rowPair = (qubitBitAt(row, numQubits, q0Index) << 1) | - qubitBitAt(row, numQubits, q1Index); - const std::size_t colPair = (qubitBitAt(col, numQubits, q0Index) << 1) | - qubitBitAt(col, numQubits, q1Index); - out(static_cast(row), static_cast(col)) = + const size_t rowPair = (qubitBitAt(row, numQubits, q0Index) << 1) | + qubitBitAt(row, numQubits, q1Index); + const size_t colPair = (qubitBitAt(col, numQubits, q0Index) << 1) | + qubitBitAt(col, numQubits, q1Index); + out(static_cast(row), static_cast(col)) = (*this)(rowPair, colPair); } } return out; } -Matrix4x4 Matrix4x4::reorderForQubits(const std::size_t q0Index, - const std::size_t q1Index) const { +Matrix4x4 Matrix4x4::reorderForQubits(const size_t q0Index, + const size_t q1Index) const { if (q0Index == 0 && q1Index == 1) { return *this; } @@ -560,31 +575,30 @@ Matrix4x4 Matrix4x4::reorderForQubits(const std::size_t q0Index, static void symmetricTred24(ArrayRef input, std::array& z, std::array& diag, std::array& subdiag) { - constexpr std::size_t n = 4; - const auto zAt = [&z](const std::size_t row, - const std::size_t col) -> double& { + constexpr size_t n = 4; + const auto zAt = [&z](const size_t row, const size_t col) -> double& { return z[row + (col * n)]; }; double h = 0.0; - for (std::size_t col = 0; col < n; ++col) { - for (std::size_t row = col; row < n; ++row) { + for (size_t col = 0; col < n; ++col) { + for (size_t row = col; row < n; ++row) { zAt(row, col) = input[(row * n) + col]; } diag[col] = input[((n - 1) * n) + col]; } for (int i = static_cast(n) - 1; i >= 1; --i) { - const auto ui = static_cast(i); - const std::size_t l = ui - 1; + const auto ui = static_cast(i); + const size_t l = ui - 1; h = 0.0; double scale = 0.0; - for (std::size_t k = 0; k <= l; ++k) { + for (size_t k = 0; k <= l; ++k) { scale += std::abs(diag[k]); } if (scale == 0.0) { subdiag[ui] = diag[l]; - for (std::size_t j = 0; j <= l; ++j) { + for (size_t j = 0; j <= l; ++j) { diag[j] = zAt(l, j); zAt(ui, j) = 0.0; zAt(j, ui) = 0.0; @@ -592,10 +606,10 @@ static void symmetricTred24(ArrayRef input, std::array& z, diag[ui] = 0.0; continue; } - for (std::size_t k = 0; k <= l; ++k) { + for (size_t k = 0; k <= l; ++k) { diag[k] /= scale; } - for (std::size_t k = 0; k <= l; ++k) { + for (size_t k = 0; k <= l; ++k) { h += diag[k] * diag[k]; } const double f = diag[l]; @@ -604,32 +618,32 @@ static void symmetricTred24(ArrayRef input, std::array& z, h -= f * g; diag[l] = f - g; - for (std::size_t k = 0; k <= l; ++k) { + for (size_t k = 0; k <= l; ++k) { subdiag[k] = 0.0; } - for (std::size_t j = 0; j <= l; ++j) { + for (size_t j = 0; j <= l; ++j) { const double fj = diag[j]; zAt(j, ui) = fj; double gj = subdiag[j] + (zAt(j, j) * fj); - for (std::size_t k = j + 1; k <= l; ++k) { + for (size_t k = j + 1; k <= l; ++k) { gj += zAt(k, j) * diag[k]; subdiag[k] += zAt(k, j) * fj; } subdiag[j] = gj; } double ff = 0.0; - for (std::size_t k = 0; k <= l; ++k) { + for (size_t k = 0; k <= l; ++k) { subdiag[k] /= h; ff += subdiag[k] * diag[k]; } const double hh = 0.5 * ff / h; - for (std::size_t k = 0; k <= l; ++k) { + for (size_t k = 0; k <= l; ++k) { subdiag[k] -= hh * diag[k]; } - for (std::size_t j = 0; j <= l; ++j) { + for (size_t j = 0; j <= l; ++j) { const double fj = diag[j]; const double gj = subdiag[j]; - for (std::size_t k = j; k <= l; ++k) { + for (size_t k = j; k <= l; ++k) { zAt(k, j) -= (fj * subdiag[k]) + (gj * diag[k]); } diag[j] = zAt(l, j); @@ -638,34 +652,34 @@ static void symmetricTred24(ArrayRef input, std::array& z, diag[ui] = h; } - for (std::size_t i = 1; i < n; ++i) { - const std::size_t l = i - 1; + for (size_t i = 1; i < n; ++i) { + const size_t l = i - 1; zAt(n - 1, l) = zAt(l, l); zAt(l, l) = 1.0; h = diag[i]; if (h != 0.0) { - for (std::size_t k = 0; k <= l; ++k) { + for (size_t k = 0; k <= l; ++k) { diag[k] = zAt(k, i) / h; } - for (std::size_t j = 0; j <= l; ++j) { + for (size_t j = 0; j <= l; ++j) { double g = 0.0; - for (std::size_t k = 0; k <= l; ++k) { + for (size_t k = 0; k <= l; ++k) { g += zAt(k, i) * zAt(k, j); } - for (std::size_t k = 0; k <= l; ++k) { + for (size_t k = 0; k <= l; ++k) { zAt(k, j) -= g * diag[k]; } } } - for (std::size_t k = 0; k <= l; ++k) { + for (size_t k = 0; k <= l; ++k) { zAt(k, i) = 0.0; } } - for (std::size_t j = 0; j < n; ++j) { + for (size_t j = 0; j < n; ++j) { diag[j] = zAt(n - 1, j); } - for (std::size_t j = 0; j < n - 1; ++j) { + for (size_t j = 0; j < n - 1; ++j) { zAt(n - 1, j) = 0.0; } zAt(n - 1, n - 1) = 1.0; @@ -676,25 +690,24 @@ static void symmetricTred24(ArrayRef input, std::array& z, static void symmetricTql24(std::array& diag, std::array& subdiag, std::array& z) { - constexpr std::size_t n = 4; - const auto zAt = [&z](const std::size_t row, - const std::size_t col) -> double& { + constexpr size_t n = 4; + const auto zAt = [&z](const size_t row, const size_t col) -> double& { return z[row + (col * n)]; }; - for (std::size_t i = 1; i < n; ++i) { + for (size_t i = 1; i < n; ++i) { subdiag[i - 1] = subdiag[i]; } double f = 0.0; double tst1 = 0.0; subdiag[n - 1] = 0.0; - for (std::size_t l = 0; l < n; ++l) { + for (size_t l = 0; l < n; ++l) { int j = 0; const double h = std::abs(diag[l]) + std::abs(subdiag[l]); tst1 = std::max(tst1, h); - std::size_t m = l; + size_t m = l; for (; m < n; ++m) { const double tst2 = tst1 + std::abs(subdiag[m]); if (tst2 == tst1) { @@ -709,8 +722,8 @@ static void symmetricTql24(std::array& diag, } ++j; - const std::size_t l1 = l + 1; - const std::size_t l2 = l1 + 1; + const size_t l1 = l + 1; + const size_t l2 = l1 + 1; const double g = diag[l]; const double p = (diag[l1] - g) / (2.0 * subdiag[l]); const double r = std::hypot(p, 1.0); @@ -718,7 +731,7 @@ static void symmetricTql24(std::array& diag, diag[l1] = subdiag[l] * (p + std::copysign(std::abs(r), p)); const double dl1 = diag[l1]; const double hh = g - diag[l]; - for (std::size_t i = l2; i < n; ++i) { + for (size_t i = l2; i < n; ++i) { diag[i] -= hh; } f += hh; @@ -730,12 +743,12 @@ static void symmetricTql24(std::array& diag, double s = 0.0; double c3 = 1.0; double s2 = 0.0; - const std::size_t mml = m - l; - for (std::size_t ii = 1; ii <= mml; ++ii) { + const size_t mml = m - l; + for (size_t ii = 1; ii <= mml; ++ii) { c3 = c2; c2 = c; s2 = s; - const std::size_t i = m - ii; + const size_t i = m - ii; const double gi = c * subdiag[i]; const double hi = c * pv; const double ri = std::hypot(pv, subdiag[i]); @@ -744,7 +757,7 @@ static void symmetricTql24(std::array& diag, c = pv / ri; pv = (c * diag[i]) - (s * gi); diag[i + 1] = hi + (s * ((c * gi) + (s * diag[i]))); - for (std::size_t k = 0; k < n; ++k) { + for (size_t k = 0; k < n; ++k) { const double zkI1 = zAt(k, i + 1); zAt(k, i + 1) = (s * zAt(k, i)) + (c * zkI1); zAt(k, i) = (c * zAt(k, i)) - (s * zkI1); @@ -763,11 +776,11 @@ static void symmetricTql24(std::array& diag, diag[l] += f; } - for (std::size_t ii = 1; ii < n; ++ii) { - const std::size_t i = ii - 1; - std::size_t k = i; + for (size_t ii = 1; ii < n; ++ii) { + const size_t i = ii - 1; + size_t k = i; double p = diag[i]; - for (std::size_t j = ii; j < n; ++j) { + for (size_t j = ii; j < n; ++j) { if (diag[j] < p) { k = j; p = diag[j]; @@ -778,7 +791,7 @@ static void symmetricTql24(std::array& diag, } diag[k] = diag[i]; diag[i] = p; - for (std::size_t j = 0; j < n; ++j) { + for (size_t j = 0; j < n; ++j) { const double tmp = zAt(j, i); zAt(j, i) = zAt(j, k); zAt(j, k) = tmp; @@ -810,7 +823,7 @@ symmetricEigenDecomposition4x4(const ArrayRef symmetric) { llvm::reportFatalInternalError( "symmetricEigenDecomposition4x4 expects 16 row-major entries"); } - constexpr std::size_t n = 4; + constexpr size_t n = 4; SymmetricEigenDecomposition4x4 result; std::array z{}; @@ -818,8 +831,8 @@ symmetricEigenDecomposition4x4(const ArrayRef symmetric) { symmetricTred24(symmetric, z, result.eigenvalues, subdiag); symmetricTql24(result.eigenvalues, subdiag, z); - for (std::size_t col = 0; col < n; ++col) { - for (std::size_t row = 0; row < n; ++row) { + for (size_t col = 0; col < n; ++col) { + for (size_t row = 0; row < n; ++row) { result.eigenvectors(row, col) = z[row + (col * n)]; } } @@ -865,10 +878,10 @@ class EispackMatrixView { EispackMatrixView(MutableArrayRef values, const int ld) : values_(values), ld_(ld) {} - [[nodiscard]] static std::size_t rowMajorIndex(const int row, const int col, - const int ld) { - return static_cast(row) + - (static_cast(col) * static_cast(ld)); + [[nodiscard]] static size_t rowMajorIndex(const int row, const int col, + const int ld) { + return static_cast(row) + + (static_cast(col) * static_cast(ld)); } [[nodiscard]] double& at(const int row, const int col) { @@ -965,11 +978,11 @@ static void eigenDecompositionReduceToHessenberg( EispackMatrixView matrixImag(matrixImagBuf, leadingDim); const auto householderRealAt = [&householderRealBuf](const int index) -> double& { - return householderRealBuf[static_cast(index)]; + return householderRealBuf[static_cast(index)]; }; const auto householderImagAt = [&householderImagBuf](const int index) -> double& { - return householderImagBuf[static_cast(index)]; + return householderImagBuf[static_cast(index)]; }; const int kp1 = rowLow + 1; @@ -1079,19 +1092,19 @@ eigenDecompositionQrSolve(const int leadingDim, const int order, EispackMatrixView eigenvectorImag(eigenvectorImagBuf, leadingDim); const auto householderRealAt = [&householderRealBuf](const int index) -> double& { - return householderRealBuf[static_cast(index)]; + return householderRealBuf[static_cast(index)]; }; const auto householderImagAt = [&householderImagBuf](const int index) -> double& { - return householderImagBuf[static_cast(index)]; + return householderImagBuf[static_cast(index)]; }; const auto eigenvalueRealAt = [&eigenvalueRealBuf](const int index) -> double& { - return eigenvalueRealBuf[static_cast(index)]; + return eigenvalueRealBuf[static_cast(index)]; }; const auto eigenvalueImagAt = [&eigenvalueImagBuf](const int index) -> double& { - return eigenvalueImagBuf[static_cast(index)]; + return eigenvalueImagBuf[static_cast(index)]; }; for (int j = 0; j < order; ++j) { @@ -1495,8 +1508,7 @@ eigenvectorColumnNorm(const int order, const int col, const int leadingDim, const ArrayRef eigenvectorImag) { double normSq = 0.0; for (int row = 0; row < order; ++row) { - const std::size_t idx = - EispackMatrixView::rowMajorIndex(row, col, leadingDim); + const size_t idx = EispackMatrixView::rowMajorIndex(row, col, leadingDim); normSq += (eigenvectorReal[idx] * eigenvectorReal[idx]) + (eigenvectorImag[idx] * eigenvectorImag[idx]); } @@ -1522,17 +1534,16 @@ eigenvectorColumnNorm(const int order, const int col, const int leadingDim, eigenvectorImag) { EigenDecomposition4x4 result; for (int col = 0; col < K_COMPLEX_EIGEN4_SIZE; ++col) { - result.eigenvalues[static_cast(col)] = - Complex(eigenvalueReal[static_cast(col)], - eigenvalueImag[static_cast(col)]); + result.eigenvalues[static_cast(col)] = + Complex(eigenvalueReal[static_cast(col)], + eigenvalueImag[static_cast(col)]); const double norm = eigenvectorColumnNorm(K_COMPLEX_EIGEN4_SIZE, col, K_COMPLEX_EIGEN4_SIZE, eigenvectorReal, eigenvectorImag); for (int row = 0; row < K_COMPLEX_EIGEN4_SIZE; ++row) { - const std::size_t idx = + const size_t idx = EispackMatrixView::rowMajorIndex(row, col, K_COMPLEX_EIGEN4_SIZE); - result.eigenvectors(static_cast(row), - static_cast(col)) = + result.eigenvectors(static_cast(row), static_cast(col)) = normalizedEigenvectorEntry(eigenvectorReal[idx], eigenvectorImag[idx], norm); } @@ -1544,10 +1555,10 @@ static void splitMatrix4x4ToRealImag( const Matrix4x4& matrix, std::array& matrixReal, std::array& matrixImag) { - for (std::size_t row = 0; row < Matrix4x4::K_ROWS; ++row) { - for (std::size_t col = 0; col < Matrix4x4::K_COLS; ++col) { + for (size_t row = 0; row < Matrix4x4::K_ROWS; ++row) { + for (size_t col = 0; col < Matrix4x4::K_COLS; ++col) { const Complex& value = matrix(row, col); - const std::size_t idx = row + (col * Matrix4x4::K_ROWS); + const size_t idx = row + (col * Matrix4x4::K_ROWS); matrixReal[idx] = std::real(value); matrixImag[idx] = std::imag(value); } @@ -1687,11 +1698,11 @@ eigenDecomposition2x2(const Matrix2x2& matrix) { */ [[nodiscard]] static std::optional eigenDecompositionDynamic(const DynamicMatrix& matrix) { - const std::int64_t dim = matrix.rows(); + const int64_t dim = matrix.rows(); if (dim != matrix.cols()) { return std::nullopt; } - if (dim > static_cast(std::numeric_limits::max())) { + if (dim > static_cast(std::numeric_limits::max())) { return std::nullopt; } assert(dim >= 3 && dim != 4); @@ -1700,25 +1711,24 @@ eigenDecompositionDynamic(const DynamicMatrix& matrix) { const int rowLow = 0; const int rowHigh = order - 1; - const std::size_t matrixStorage = - (static_cast(leadingDim) * static_cast(order)) + - static_cast(order) + 1U; + const size_t matrixStorage = + (static_cast(leadingDim) * static_cast(order)) + + static_cast(order) + 1U; SmallVector matrixReal(matrixStorage); SmallVector matrixImag(matrixStorage); for (int row = 0; row < order; ++row) { for (int col = 0; col < order; ++col) { const Complex value = matrix(row, col); - const std::size_t idx = - EispackMatrixView::rowMajorIndex(row, col, leadingDim); + const size_t idx = EispackMatrixView::rowMajorIndex(row, col, leadingDim); matrixReal[idx] = std::real(value); matrixImag[idx] = std::imag(value); } } - SmallVector householderReal(static_cast(order)); - SmallVector householderImag(static_cast(order)); - SmallVector eigenvalueReal(static_cast(order) + 1U); - SmallVector eigenvalueImag(static_cast(order) + 1U); + SmallVector householderReal(static_cast(order)); + SmallVector householderImag(static_cast(order)); + SmallVector eigenvalueReal(static_cast(order) + 1U); + SmallVector eigenvalueImag(static_cast(order) + 1U); SmallVector eigenvectorReal(matrixStorage); SmallVector eigenvectorImag(matrixStorage); @@ -1734,17 +1744,15 @@ eigenDecompositionDynamic(const DynamicMatrix& matrix) { } EigenDecomposition result; - result.eigenvalues.reserve(static_cast(order)); + result.eigenvalues.reserve(static_cast(order)); result.eigenvectors = DynamicMatrix(dim); for (int col = 0; col < order; ++col) { - result.eigenvalues.emplace_back( - eigenvalueReal[static_cast(col)], - eigenvalueImag[static_cast(col)]); + result.eigenvalues.emplace_back(eigenvalueReal[static_cast(col)], + eigenvalueImag[static_cast(col)]); const double norm = eigenvectorColumnNorm(order, col, leadingDim, eigenvectorReal, eigenvectorImag); for (int row = 0; row < order; ++row) { - const std::size_t idx = - EispackMatrixView::rowMajorIndex(row, col, leadingDim); + const size_t idx = EispackMatrixView::rowMajorIndex(row, col, leadingDim); result.eigenvectors(row, col) = normalizedEigenvectorEntry( eigenvectorReal[idx], eigenvectorImag[idx], norm); } @@ -1766,7 +1774,7 @@ Matrix4x4 Matrix4x4::fromRealRowMajor(const ArrayRef entries) { "Matrix4x4::fromRealRowMajor expects 16 row-major entries"); } Matrix4x4 result; - for (std::size_t i = 0; i < K_SIZE_AT_COMPILE_TIME; ++i) { + for (size_t i = 0; i < K_SIZE_AT_COMPILE_TIME; ++i) { result.data[i] = entries[i]; } return result; @@ -1781,13 +1789,13 @@ SymmetricEigenDecomposition4x4 Matrix4x4::symmetricEigenDecomposition() const { } struct DynamicMatrix::Impl { - std::int64_t dim = 0; + int64_t dim = 0; SmallVector data; }; DynamicMatrix::DynamicMatrix() : impl_(std::make_unique()) {} -DynamicMatrix::DynamicMatrix(const std::int64_t dim) +DynamicMatrix::DynamicMatrix(const int64_t dim) : impl_(std::make_unique()) { impl_->dim = dim; impl_->data.assign(checkedStorageSize(dim), Complex{}); @@ -1820,14 +1828,14 @@ DynamicMatrix::operator=(DynamicMatrix&& other) noexcept = default; DynamicMatrix::~DynamicMatrix() = default; -std::int64_t DynamicMatrix::rows() const { return impl_->dim; } +int64_t DynamicMatrix::rows() const { return impl_->dim; } -std::int64_t DynamicMatrix::cols() const { return impl_->dim; } +int64_t DynamicMatrix::cols() const { return impl_->dim; } -DynamicMatrix DynamicMatrix::identity(const std::int64_t dim) { +DynamicMatrix DynamicMatrix::identity(const int64_t dim) { DynamicMatrix matrix(dim); const auto udim = checkedDim(dim); - for (std::size_t i = 0; i < udim; ++i) { + for (size_t i = 0; i < udim; ++i) { matrix.impl_->data[(i * udim) + i] = 1.0; } return matrix; @@ -1837,28 +1845,24 @@ DynamicMatrix DynamicMatrix::fromAdjoint(const Matrix2x2& src) { return DynamicMatrix(src.adjoint()); } -Complex& DynamicMatrix::operator()(const std::int64_t row, - const std::int64_t col) { +Complex& DynamicMatrix::operator()(const int64_t row, const int64_t col) { return impl_ - ->data[static_cast(checkedFlatIndex(row, col, impl_->dim))]; + ->data[static_cast(checkedFlatIndex(row, col, impl_->dim))]; } -Complex DynamicMatrix::operator()(const std::int64_t row, - const std::int64_t col) const { +Complex DynamicMatrix::operator()(const int64_t row, const int64_t col) const { return impl_ - ->data[static_cast(checkedFlatIndex(row, col, impl_->dim))]; + ->data[static_cast(checkedFlatIndex(row, col, impl_->dim))]; } void DynamicMatrix::setBottomRightCorner(const Matrix2x2& block) { copyBottomRightCorner(impl_->dim, impl_->data, - static_cast(Matrix2x2::K_ROWS), - block.data); + static_cast(Matrix2x2::K_ROWS), block.data); } void DynamicMatrix::setBottomRightCorner(const Matrix4x4& block) { copyBottomRightCorner(impl_->dim, impl_->data, - static_cast(Matrix4x4::K_ROWS), - block.data); + static_cast(Matrix4x4::K_ROWS), block.data); } void DynamicMatrix::setBottomRightCorner(const DynamicMatrix& block) { @@ -1919,7 +1923,7 @@ Complex DynamicMatrix::trace() const { Complex sum{0.0, 0.0}; const auto udim = checkedDim(impl_->dim); const ArrayRef storage = impl_->data; - for (std::size_t i = 0; i < udim; ++i) { + for (size_t i = 0; i < udim; ++i) { sum += storage[(i * udim) + i]; } return sum; @@ -1939,10 +1943,10 @@ DynamicMatrix DynamicMatrix::operator*(const DynamicMatrix& rhs) const { } const auto udim = checkedDim(impl_->dim); - for (std::size_t row = 0; row < udim; ++row) { - for (std::size_t col = 0; col < udim; ++col) { + for (size_t row = 0; row < udim; ++row) { + for (size_t col = 0; col < udim; ++col) { Complex sum{0.0, 0.0}; - for (std::size_t k = 0; k < udim; ++k) { + for (size_t k = 0; k < udim; ++k) { sum += impl_->data[(row * udim) + k] * rhs.impl_->data[(k * udim) + col]; } @@ -1952,9 +1956,84 @@ DynamicMatrix DynamicMatrix::operator*(const DynamicMatrix& rhs) const { return out; } +void DynamicMatrix::premultiplyBy(const DynamicMatrix& lhs) { + *this = lhs * *this; +} + +void DynamicMatrix::premultiplyByEmbedded1Q(const Matrix2x2& gate, + const size_t numQubits, + const size_t qubitIndex) { + assert(qubitIndex < numQubits && + static_cast(impl_->dim) == (uint64_t{1} << numQubits) && + "Matrix dimension must match numQubits"); + if (std::cmp_equal(impl_->dim, Matrix2x2::K_ROWS)) { + assert(qubitIndex == 0); + std::array tmp{}; + multiply2x2(gate.data, impl_->data, tmp); + impl_->data.assign(tmp.begin(), tmp.end()); + return; + } + const auto udim = checkedDim(impl_->dim); + const size_t mask = size_t{1} << (numQubits - 1 - qubitIndex); + const size_t step = mask << 1; + auto& data = impl_->data; + for (size_t chunk = 0; chunk < udim; chunk += step) { + for (size_t inner = 0; inner < mask; ++inner) { + const size_t base = chunk | inner; + const size_t row1 = base | mask; + for (size_t col = 0; col < udim; ++col) { + const size_t idx0 = (base * udim) + col; + const size_t idx1 = (row1 * udim) + col; + apply2x2LeftToRowPair(gate.data, data[idx0], data[idx1]); + } + } + } +} + +void DynamicMatrix::premultiplyByEmbedded2Q(const Matrix4x4& gate, + const size_t numQubits, + const size_t q0Index, + const size_t q1Index) { + assert(q0Index < numQubits && q1Index < numQubits && q0Index != q1Index && + static_cast(impl_->dim) == (uint64_t{1} << numQubits) && + "Matrix dimension must match numQubits"); + if (std::cmp_equal(impl_->dim, Matrix4x4::K_ROWS)) { + assert(q0Index == 0 && q1Index == 1); + std::array tmp{}; + multiply4x4(gate.data, impl_->data, tmp); + impl_->data.assign(tmp.begin(), tmp.end()); + return; + } + const auto udim = checkedDim(impl_->dim); + const size_t mask0 = size_t{1} << (numQubits - 1 - q0Index); + const size_t mask1 = size_t{1} << (numQubits - 1 - q1Index); + auto& data = impl_->data; + for (size_t block = 0; block < (udim >> 2); ++block) { + size_t base = 0; + size_t rest = block; + for (size_t q = 0; q < numQubits; ++q) { + if (q == q0Index || q == q1Index) { + continue; + } + if ((rest & 1U) != 0) { + base |= size_t{1} << (numQubits - 1 - q); + } + rest >>= 1U; + } + const std::array rowIdx = { + base, base | mask1, base | mask0, base | mask0 | mask1}; + for (size_t col = 0; col < udim; ++col) { + apply4x4LeftToColumn(gate.data, data[(rowIdx[0] * udim) + col], + data[(rowIdx[1] * udim) + col], + data[(rowIdx[2] * udim) + col], + data[(rowIdx[3] * udim) + col]); + } + } +} + DynamicMatrix DynamicMatrix::operator*(const Complex& scalar) const { DynamicMatrix out(impl_->dim); - for (std::size_t i = 0; i < impl_->data.size(); ++i) { + for (size_t i = 0; i < impl_->data.size(); ++i) { out.impl_->data[i] = impl_->data[i] * scalar; } return out; @@ -2002,7 +2081,7 @@ EigenDecomposition::from(const EigenDecomposition4x4& eigen4) { } std::optional DynamicMatrix::eigenDecomposition() const { - const std::size_t dim = checkedDim(impl_->dim); + const size_t dim = checkedDim(impl_->dim); if (dim == 0) { return std::nullopt; } diff --git a/mlir/lib/Dialect/QCO/Utils/Qubits.cpp b/mlir/lib/Dialect/QCO/Utils/Qubits.cpp deleted file mode 100644 index c01187f625..0000000000 --- a/mlir/lib/Dialect/QCO/Utils/Qubits.cpp +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "mlir/Dialect/QCO/Utils/Qubits.h" - -#include "mlir/Dialect/QCO/IR/QCODialect.h" - -#include - -#include -#include -#include - -namespace mlir::qco { -void Qubits::add(TypedValue q) { - const auto index = programToValue_.size(); - programToValue_.try_emplace(index, q); - valueToIndex_.try_emplace(q, std::make_pair(QubitLocation::Program, index)); -} - -void Qubits::add(TypedValue q, std::size_t hw) { - hardwareToValue_.try_emplace(hw, q); - valueToIndex_.try_emplace(q, std::make_pair(QubitLocation::Hardware, hw)); -} - -void Qubits::remap(TypedValue prev, TypedValue next) { - assert(valueToIndex_.contains(prev)); - const auto& [location, index] = valueToIndex_.lookup(prev); - - valueToIndex_.erase(prev); - valueToIndex_.try_emplace(next, std::make_pair(location, index)); - - if (location == QubitLocation::Program) { - programToValue_[index] = next; - return; - } - - hardwareToValue_[index] = next; -} - -void Qubits::remove(TypedValue q) { - assert(valueToIndex_.contains(q)); - const auto& [location, index] = valueToIndex_.lookup(q); - - valueToIndex_.erase(q); - - if (location == QubitLocation::Program) { - programToValue_.erase(index); - return; - } - - hardwareToValue_.erase(index); -} - -TypedValue Qubits::getProgramQubit(std::size_t index) const { - assert(programToValue_.contains(index)); - return programToValue_.lookup(index); -} - -TypedValue Qubits::getHardwareQubit(std::size_t index) const { - assert(hardwareToValue_.contains(index)); - return hardwareToValue_.lookup(index); -} - -std::size_t Qubits::getIndex(TypedValue q) const { - assert(valueToIndex_.contains(q)); - const auto& res = valueToIndex_.lookup(q); - return res.second; -} -} // namespace mlir::qco diff --git a/mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp b/mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp index 40b89cde52..0004d0ebfb 100644 --- a/mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp +++ b/mlir/lib/Dialect/QIR/Builder/QIRProgramBuilder.cpp @@ -51,12 +51,14 @@ QIRProgramBuilder::QIRProgramBuilder(MLIRContext* context) getContext()->loadDialect(); } -void QIRProgramBuilder::initialize() { +void QIRProgramBuilder::initialize() { initialize(getI64Type()); } + +void QIRProgramBuilder::initialize(Type returnType) { // Set insertion point to the module body setInsertionPointToStart(cast(module).getBody()); // Create main function: () -> i64 - auto funcType = LLVM::LLVMFunctionType::get(getI64Type(), {}); + auto funcType = LLVM::LLVMFunctionType::get(returnType, {}); auto mainFuncOp = LLVM::LLVMFuncOp::create(*this, "main", funcType); mainFunc = mainFuncOp.getOperation(); @@ -87,7 +89,6 @@ void QIRProgramBuilder::initialize() { // Create exit code constant in entry block setInsertionPointToStart(entryBlock); - exitCode = intConstant(0); // Add initialize call auto initSig = LLVM::LLVMFunctionType::get(voidType, ptrType); @@ -99,14 +100,19 @@ void QIRProgramBuilder::initialize() { setInsertionPointToEnd(entryBlock); LLVM::BrOp::create(*this, bodyBlock); - // Return the exit code (success) in output block - setInsertionPointToEnd(outputBlock); - LLVM::ReturnOp::create(*this, exitCode); - // Set insertion point to body block for user operations setInsertionPointToStart(bodyBlock); } +void QIRProgramBuilder::retype(Type returnType) { + auto mainFn = dyn_cast(mainFunc); + if (!mainFn) { + llvm::reportFatalUsageError("Main function not found for retyping"); + } + auto funcType = LLVM::LLVMFunctionType::get(returnType, {}); + mainFn.setType(funcType); +} + Value QIRProgramBuilder::resolveIntVariant( const std::variant& variant) { if (std::holds_alternative(variant)) { @@ -349,7 +355,8 @@ QIRProgramBuilder::allocClassicalBitRegister(const int64_t size, return {.name = name, .size = size}; } -Value QIRProgramBuilder::measure(Value qubit, const int64_t resultIndex) { +Value QIRProgramBuilder::measure(Value qubit, const int64_t resultIndex, + bool record) { checkFinalized(); if (resultIndex < 0) { @@ -378,10 +385,14 @@ Value QIRProgramBuilder::measure(Value qubit, const int64_t resultIndex) { getOrCreateFunctionDeclaration(*this, module, QIR_MEASURE, mzSig); LLVM::CallOp::create(*this, mzDec, ValueRange{qubit, result}); + if (record) { + recordedIndices.insert(resultIndex); + } + return result; } -Value QIRProgramBuilder::measure(Value qubit, const Bit& bit) { +Value QIRProgramBuilder::measure(Value qubit, const Bit& bit, bool record) { checkFinalized(); const InsertionGuard guard(*this); @@ -400,6 +411,21 @@ Value QIRProgramBuilder::measure(Value qubit, const Bit& bit) { getOrCreateFunctionDeclaration(*this, module, QIR_MEASURE, fnSig); LLVM::CallOp::create(*this, fnDec, ValueRange{qubit, result}); + if (record) { + if (profile == Profile::Adaptive) { + recordedArrays.insert(stringSaver.save(bit.registerName)); + } else { + // In the base profile we don't have recorded arrays, so we need to + // find the index of the result and record that instead. + for (const auto& [index, ptr] : resultPtrs) { + if (ptr == result) { + recordedIndices.insert(index); + break; + } + } + } + } + return result; } @@ -951,6 +977,9 @@ void QIRProgramBuilder::generateOutputRecording() { getOrCreateFunctionDeclaration(*this, module, QIR_RECORD_OUTPUT, fnSig); // Create output recording for each result pointer for (const auto& [index, ptr] : resultPtrs) { + if (!recordedIndices.contains(index)) { + continue; + } auto label = createResultLabel(*this, module, "__unnamed__" + std::to_string(index)) .getResult(); @@ -965,6 +994,9 @@ void QIRProgramBuilder::generateOutputRecording() { QIR_ARRAY_RECORD_OUTPUT, fnSig); // Create output recording for each register for (const auto& [name, results] : resultArrays) { + if (!recordedArrays.contains(name)) { + continue; + } auto size = results.getDefiningOp().getArraySize(); auto label = createResultLabel(*this, module, name).getResult(); LLVM::CallOp::create(*this, fnDec, ValueRange{size, results, label}); @@ -975,10 +1007,21 @@ void QIRProgramBuilder::generateOutputRecording() { OwningOpRef QIRProgramBuilder::finalize() { checkFinalized(); + auto exitCode = intConstant(0); + return finalize(exitCode); +} + +OwningOpRef QIRProgramBuilder::finalize(Value returnValue) { + checkFinalized(); + // Save current insertion point const InsertionGuard guard(*this); const bool isAdaptive = (profile == Profile::Adaptive); + // Add return statement with the given return values to the main function + setInsertionPointToEnd(outputBlock); + LLVM::ReturnOp::create(*this, returnValue); + // Release resources in output block setInsertionPoint(outputBlock->getTerminator()); @@ -1034,12 +1077,13 @@ OwningOpRef QIRProgramBuilder::finalize() { OwningOpRef QIRProgramBuilder::build( MLIRContext* context, - const function_ref& buildFunc, Profile profile) { + const function_ref& buildFunc, Profile profile) { QIRProgramBuilder builder(context); builder.profile = profile; builder.initialize(); - buildFunc(builder); - return builder.finalize(); + auto result = buildFunc(builder); + builder.retype(result.getType()); + return builder.finalize(result); } } // namespace mlir::qir diff --git a/mlir/lib/Dialect/QTensor/Utils/TensorIterator.cpp b/mlir/lib/Dialect/QTensor/Utils/TensorIterator.cpp index 064cdbd624..6af8c5bff7 100644 --- a/mlir/lib/Dialect/QTensor/Utils/TensorIterator.cpp +++ b/mlir/lib/Dialect/QTensor/Utils/TensorIterator.cpp @@ -26,12 +26,8 @@ namespace mlir::qtensor { TypedValue TensorIterator::tensor() const { - if (op_ == nullptr) { - return tensor_; - } - // The following operations don't have an OpResult. - if (isa(op_)) { + if (op_ != nullptr && isa(op_)) { return nullptr; } diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index ddc3e4ce4d..b67f8c7f96 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -34,8 +34,10 @@ #include #include #include +#include #include #include +#include #include #include @@ -44,8 +46,13 @@ namespace mqt::test::compiler { -using QCProgramBuilderFn = NamedBuilder; -using QIRProgramBuilderFn = NamedBuilder; +using namespace mlir; +using namespace mlir::qc; +using namespace mlir::qco; +using namespace mlir::qir; + +using QCProgramBuilderFn = NamedMLIRBuilder; +using QIRProgramBuilderFn = NamedMLIRBuilder; using QuantumComputationBuilderFn = NamedBuilder<::qc::QuantumComputation>; namespace { @@ -82,46 +89,44 @@ std::ostream& operator<<(std::ostream& os, class CompilerPipelineTest : public testing::TestWithParam { protected: - std::unique_ptr context; + std::unique_ptr context; void SetUp() override { - mlir::DialectRegistry registry; - registry.insert(); - context = std::make_unique(); + DialectRegistry registry; + registry + .insert(); + context = std::make_unique(); context->appendDialectRegistry(registry); context->loadAllAvailableDialects(); } - [[nodiscard]] mlir::OwningOpRef + [[nodiscard]] OwningOpRef buildQCReference(const QCProgramBuilderFn builder) const { - auto module = mlir::qc::QCProgramBuilder::build(context.get(), builder.fn); + auto module = mqt::test::buildMLIRProgram(context.get(), builder); EXPECT_TRUE(runQCCleanupPipeline(module.get()).succeeded()); return module; } - [[nodiscard]] mlir::OwningOpRef + [[nodiscard]] OwningOpRef buildQIRReference(const QIRProgramBuilderFn builder) const { - auto module = mlir::qir::QIRProgramBuilder::build( - context.get(), builder.fn, - mlir::qir::QIRProgramBuilder::Profile::Adaptive); + auto module = mqt::test::buildMLIRProgram( + context.get(), builder, QIRProgramBuilder::Profile::Adaptive); EXPECT_TRUE(runQIRCleanupPipeline(module.get(), true).succeeded()); return module; } - [[nodiscard]] mlir::OwningOpRef + [[nodiscard]] OwningOpRef parseRecordedModule(const std::string& ir) const { - return mlir::parseSourceString(ir, context.get()); + return parseSourceString(ir, context.get()); } - static void runPipeline(const mlir::ModuleOp module, const bool convertToQIR, + static void runPipeline(const ModuleOp module, const bool convertToQIR, const bool disableMergeSingleQubitRotationGates, const bool enableHadamardLifting, - mlir::CompilationRecord& record) { - mlir::QuantumCompilerConfig config; + CompilationRecord& record) { + QuantumCompilerConfig config; config.convertToQIRAdaptive = convertToQIR; config.disableMergeSingleQubitRotationGates = disableMergeSingleQubitRotationGates; @@ -129,16 +134,16 @@ class CompilerPipelineTest config.recordIntermediates = true; config.printIRAfterAllStages = true; - mlir::QuantumCompilerPipeline pipeline(config); + QuantumCompilerPipeline pipeline(config); ASSERT_TRUE(pipeline.runPipeline(module, &record).succeeded()); } void expectEquivalent(const std::string& stage, const std::string& ir, - const mlir::ModuleOp expected) const { + const ModuleOp expected) const { auto actual = parseRecordedModule(ir); ASSERT_TRUE(actual) << stage << " failed to parse"; - EXPECT_TRUE(mlir::verify(*actual).succeeded()); - EXPECT_TRUE(mlir::verify(expected).succeeded()); + EXPECT_TRUE(verify(*actual).succeeded()); + EXPECT_TRUE(verify(expected).succeeded()); EXPECT_TRUE(areModulesEquivalentWithPermutations(actual.get(), expected)); } }; @@ -150,25 +155,25 @@ TEST_P(CompilerPipelineTest, EndToEndPipeline) { const auto name = " (" + testCase.name + ")"; DeferredPrinter printer; - mlir::OwningOpRef module; + OwningOpRef module; if (testCase.startFromQuantumComputation) { ASSERT_TRUE(testCase.quantumComputationBuilder); ::qc::QuantumComputation comp; testCase.quantumComputationBuilder.fn(comp); - module = mlir::translateQuantumComputationToQC(context.get(), comp); + module = translateQuantumComputationToQC(context.get(), comp); ASSERT_TRUE(module); printer.record(module.get(), "QC Import" + name); } else { ASSERT_TRUE(testCase.qcProgramBuilder); - module = mlir::qc::QCProgramBuilder::build(context.get(), - testCase.qcProgramBuilder.fn); + module = + mqt::test::buildMLIRProgram(context.get(), testCase.qcProgramBuilder); ASSERT_TRUE(module); printer.record(module.get(), "QC Input" + name); } - EXPECT_TRUE(mlir::verify(*module).succeeded()); + EXPECT_TRUE(verify(*module).succeeded()); - mlir::CompilationRecord record; + CompilationRecord record; runPipeline(module.get(), testCase.convertToQIR, false, false, record); ASSERT_TRUE(testCase.qcReferenceBuilder); @@ -207,15 +212,16 @@ TEST_P(CompilerPipelineTest, EndToEndPipeline) { * Correctness of the pass is tested in a dedicated test. */ TEST_F(CompilerPipelineTest, RotationGateMergingPass) { - auto module = mlir::qc::QCProgramBuilder::build( - context.get(), [&](mlir::qc::QCProgramBuilder& b) { + auto module = + QCProgramBuilder::build(context.get(), [&](QCProgramBuilder& b) { auto q = b.allocQubit(); b.rz(1.0, q); b.rx(1.0, q); + return b.measure(q); }); ASSERT_TRUE(module); - mlir::CompilationRecord record; + CompilationRecord record; runPipeline(module.get(), false, false, false, record); // The outputs must differ, proving the pass ran and transformed the IR @@ -230,15 +236,16 @@ TEST_F(CompilerPipelineTest, RotationGateMergingPass) { * Correctness of the pass is tested in a dedicated test. */ TEST_F(CompilerPipelineTest, HadamardLiftingPass) { - auto module = mlir::qc::QCProgramBuilder::build( - context.get(), [&](mlir::qc::QCProgramBuilder& b) { + auto module = + QCProgramBuilder::build(context.get(), [&](QCProgramBuilder& b) { auto q = b.allocQubit(); b.x(q); b.h(q); + return b.measure(q); }); ASSERT_TRUE(module); - mlir::CompilationRecord record; + CompilationRecord record; runPipeline(module.get(), false, true, true, record); // The outputs must differ, proving the pass ran and transformed the IR @@ -277,419 +284,431 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(mlir::qc::staticQubitsWithInv), MQT_NAMED_BUILDER(mlir::qc::staticQubitsWithInv), MQT_NAMED_BUILDER(mlir::qir::staticQubitsWithInv), false}, - CompilerPipelineTestCase{"AllocQubit", - MQT_NAMED_BUILDER(qc::allocQubit), nullptr, - MQT_NAMED_BUILDER(mlir::qc::emptyQC), - MQT_NAMED_BUILDER(mlir::qir::emptyQIR)}, - CompilerPipelineTestCase{"AllocQubitRegister", - MQT_NAMED_BUILDER(qc::allocQubitRegister), - nullptr, MQT_NAMED_BUILDER(mlir::qc::emptyQC), - MQT_NAMED_BUILDER(mlir::qir::emptyQIR)}, + CompilerPipelineTestCase{ + "AllocQubit", MQT_NAMED_BUILDER(::qc::allocQubit), nullptr, + MQT_NAMED_BUILDER(mlir::qc::alloc1QubitRegister), + MQT_NAMED_BUILDER(mlir::qir::alloc1QubitRegister)}, + CompilerPipelineTestCase{ + "AllocQubitRegister", MQT_NAMED_BUILDER(::qc::allocQubitRegister), + nullptr, MQT_NAMED_BUILDER(mlir::qc::allocQubitRegister), + MQT_NAMED_BUILDER(mlir::qir::allocQubitRegister)}, CompilerPipelineTestCase{ "AllocMultipleQubitRegisters", - MQT_NAMED_BUILDER(qc::allocMultipleQubitRegisters), nullptr, - MQT_NAMED_BUILDER(mlir::qc::emptyQC), - MQT_NAMED_BUILDER(mlir::qir::emptyQIR)}, - CompilerPipelineTestCase{"AllocLargeRegister", - MQT_NAMED_BUILDER(qc::allocLargeRegister), - nullptr, MQT_NAMED_BUILDER(mlir::qc::emptyQC), - MQT_NAMED_BUILDER(mlir::qir::emptyQIR)}, + MQT_NAMED_BUILDER(::qc::allocMultipleQubitRegisters), nullptr, + MQT_NAMED_BUILDER(mlir::qc::allocMultipleQubitRegisters), + MQT_NAMED_BUILDER(mlir::qir::allocMultipleQubitRegisters)}, + CompilerPipelineTestCase{ + "AllocLargeRegister", MQT_NAMED_BUILDER(::qc::allocLargeRegister), + nullptr, MQT_NAMED_BUILDER(mlir::qc::allocLargeRegister), + MQT_NAMED_BUILDER(mlir::qir::allocQubitRegister)}, CompilerPipelineTestCase{ "SingleMeasurementToSingleBit", - MQT_NAMED_BUILDER(qc::singleMeasurementToSingleBit), nullptr, + MQT_NAMED_BUILDER(::qc::singleMeasurementToSingleBit), nullptr, MQT_NAMED_BUILDER(mlir::qc::singleMeasurementToSingleBit), - MQT_NAMED_BUILDER(mlir::qir::singleMeasurementToSingleBit)}, + MQT_NAMED_BUILDER(mlir::qir::singleMeasurementToSingleBit)}, CompilerPipelineTestCase{ "RepeatedMeasurementToSameBit", - MQT_NAMED_BUILDER(qc::repeatedMeasurementToSameBit), nullptr, + MQT_NAMED_BUILDER(::qc::repeatedMeasurementToSameBit), nullptr, MQT_NAMED_BUILDER(mlir::qc::repeatedMeasurementToSameBit), - MQT_NAMED_BUILDER(mlir::qir::repeatedMeasurementToSameBit)}, + MQT_NAMED_BUILDER(mlir::qir::repeatedMeasurementToSameBit)}, CompilerPipelineTestCase{ "RepeatedMeasurementToDifferentBits", - MQT_NAMED_BUILDER(qc::repeatedMeasurementToDifferentBits), nullptr, + MQT_NAMED_BUILDER(::qc::repeatedMeasurementToDifferentBits), + nullptr, MQT_NAMED_BUILDER(mlir::qc::repeatedMeasurementToDifferentBits), - MQT_NAMED_BUILDER(mlir::qir::repeatedMeasurementToDifferentBits)}, + MQT_NAMED_BUILDER( + mlir::qir::repeatedMeasurementToDifferentBits)}, CompilerPipelineTestCase{ "MultipleClassicalRegistersAndMeasurements", - MQT_NAMED_BUILDER(qc::multipleClassicalRegistersAndMeasurements), + MQT_NAMED_BUILDER(::qc::multipleClassicalRegistersAndMeasurements), nullptr, MQT_NAMED_BUILDER( mlir::qc::multipleClassicalRegistersAndMeasurements), MQT_NAMED_BUILDER( - mlir::qir::multipleClassicalRegistersAndMeasurements)}, + mlir::qir::multipleClassicalRegistersAndMeasurements)}, CompilerPipelineTestCase{ "MeasurementWithoutRegisters", nullptr, MQT_NAMED_BUILDER(mlir::qc::measurementWithoutRegisters), MQT_NAMED_BUILDER(mlir::qc::measurementWithoutRegisters), - MQT_NAMED_BUILDER(mlir::qir::measurementWithoutRegisters), false}, + MQT_NAMED_BUILDER(mlir::qir::measurementWithoutRegisters), + false}, CompilerPipelineTestCase{ "ResetQubitAfterSingleOp", - MQT_NAMED_BUILDER(qc::resetQubitAfterSingleOp), nullptr, + MQT_NAMED_BUILDER(::qc::resetQubitAfterSingleOp), nullptr, MQT_NAMED_BUILDER(mlir::qc::resetQubitAfterSingleOp), - MQT_NAMED_BUILDER(mlir::qir::resetQubitAfterSingleOp)}, + MQT_NAMED_BUILDER(mlir::qir::resetQubitAfterSingleOp)}, CompilerPipelineTestCase{ "ResetMultipleQubitsAfterSingleOp", - MQT_NAMED_BUILDER(qc::resetMultipleQubitsAfterSingleOp), nullptr, + MQT_NAMED_BUILDER(::qc::resetMultipleQubitsAfterSingleOp), nullptr, MQT_NAMED_BUILDER(mlir::qc::resetMultipleQubitsAfterSingleOp), - MQT_NAMED_BUILDER(mlir::qir::resetMultipleQubitsAfterSingleOp)}, + MQT_NAMED_BUILDER( + mlir::qir::resetMultipleQubitsAfterSingleOp)}, CompilerPipelineTestCase{ "RepeatedResetAfterSingleOp", - MQT_NAMED_BUILDER(qc::repeatedResetAfterSingleOp), nullptr, + MQT_NAMED_BUILDER(::qc::repeatedResetAfterSingleOp), nullptr, MQT_NAMED_BUILDER(mlir::qc::resetQubitAfterSingleOp), - MQT_NAMED_BUILDER(mlir::qir::resetQubitAfterSingleOp)}, - CompilerPipelineTestCase{"GlobalPhase", - MQT_NAMED_BUILDER(qc::globalPhase), nullptr, - MQT_NAMED_BUILDER(mlir::qc::globalPhase), - MQT_NAMED_BUILDER(mlir::qir::globalPhase)}, - CompilerPipelineTestCase{"Identity", MQT_NAMED_BUILDER(qc::identity), - nullptr, MQT_NAMED_BUILDER(mlir::qc::emptyQC), - MQT_NAMED_BUILDER(mlir::qir::emptyQIR)}, + MQT_NAMED_BUILDER(mlir::qir::resetQubitAfterSingleOp)}, + CompilerPipelineTestCase{ + "GlobalPhase", MQT_NAMED_BUILDER(::qc::globalPhase), nullptr, + MQT_NAMED_BUILDER(mlir::qc::globalPhase), + MQT_NAMED_BUILDER(mlir::qir::globalPhase)}, + CompilerPipelineTestCase{ + "Identity", MQT_NAMED_BUILDER(::qc::identity), nullptr, + MQT_NAMED_BUILDER(mlir::qc::alloc1QubitRegister), + MQT_NAMED_BUILDER(mlir::qir::alloc1QubitRegister)}, CompilerPipelineTestCase{ "SingleControlledIdentity", - MQT_NAMED_BUILDER(qc::singleControlledIdentity), nullptr, - MQT_NAMED_BUILDER(mlir::qc::emptyQC), - MQT_NAMED_BUILDER(mlir::qir::emptyQIR)}, + MQT_NAMED_BUILDER(::qc::singleControlledIdentity), nullptr, + MQT_NAMED_BUILDER(mlir::qc::allocQubitRegister), + MQT_NAMED_BUILDER(mlir::qir::allocQubitRegister)}, CompilerPipelineTestCase{ "MultipleControlledIdentity", - MQT_NAMED_BUILDER(qc::multipleControlledIdentity), nullptr, - MQT_NAMED_BUILDER(mlir::qc::emptyQC), - MQT_NAMED_BUILDER(mlir::qir::emptyQIR)}, - CompilerPipelineTestCase{"X", MQT_NAMED_BUILDER(qc::x), nullptr, + MQT_NAMED_BUILDER(::qc::multipleControlledIdentity), nullptr, + MQT_NAMED_BUILDER(mlir::qc::alloc3QubitRegister), + MQT_NAMED_BUILDER(mlir::qir::alloc3QubitRegister)}, + CompilerPipelineTestCase{"X", MQT_NAMED_BUILDER(::qc::x), nullptr, MQT_NAMED_BUILDER(mlir::qc::x), - MQT_NAMED_BUILDER(mlir::qir::x)}, + MQT_NAMED_BUILDER(mlir::qir::x)}, CompilerPipelineTestCase{ - "SingleControlledX", MQT_NAMED_BUILDER(qc::singleControlledX), + "SingleControlledX", MQT_NAMED_BUILDER(::qc::singleControlledX), nullptr, MQT_NAMED_BUILDER(mlir::qc::singleControlledX), - MQT_NAMED_BUILDER(mlir::qir::singleControlledX)}, + MQT_NAMED_BUILDER(mlir::qir::singleControlledX)}, CompilerPipelineTestCase{ - "MultipleControlledX", MQT_NAMED_BUILDER(qc::multipleControlledX), + "MultipleControlledX", MQT_NAMED_BUILDER(::qc::multipleControlledX), nullptr, MQT_NAMED_BUILDER(mlir::qc::multipleControlledX), - MQT_NAMED_BUILDER(mlir::qir::multipleControlledX)}, - CompilerPipelineTestCase{"Y", MQT_NAMED_BUILDER(qc::y), nullptr, + MQT_NAMED_BUILDER(mlir::qir::multipleControlledX)}, + CompilerPipelineTestCase{"Y", MQT_NAMED_BUILDER(::qc::y), nullptr, MQT_NAMED_BUILDER(mlir::qc::y), - MQT_NAMED_BUILDER(mlir::qir::y)}, + MQT_NAMED_BUILDER(mlir::qir::y)}, CompilerPipelineTestCase{ - "SingleControlledY", MQT_NAMED_BUILDER(qc::singleControlledY), + "SingleControlledY", MQT_NAMED_BUILDER(::qc::singleControlledY), nullptr, MQT_NAMED_BUILDER(mlir::qc::singleControlledY), - MQT_NAMED_BUILDER(mlir::qir::singleControlledY)}, + MQT_NAMED_BUILDER(mlir::qir::singleControlledY)}, CompilerPipelineTestCase{ - "MultipleControlledY", MQT_NAMED_BUILDER(qc::multipleControlledY), + "MultipleControlledY", MQT_NAMED_BUILDER(::qc::multipleControlledY), nullptr, MQT_NAMED_BUILDER(mlir::qc::multipleControlledY), - MQT_NAMED_BUILDER(mlir::qir::multipleControlledY)}, - CompilerPipelineTestCase{"Z", MQT_NAMED_BUILDER(qc::z), nullptr, + MQT_NAMED_BUILDER(mlir::qir::multipleControlledY)}, + CompilerPipelineTestCase{"Z", MQT_NAMED_BUILDER(::qc::z), nullptr, MQT_NAMED_BUILDER(mlir::qc::z), - MQT_NAMED_BUILDER(mlir::qir::z)}, + MQT_NAMED_BUILDER(mlir::qir::z)}, CompilerPipelineTestCase{ - "SingleControlledZ", MQT_NAMED_BUILDER(qc::singleControlledZ), + "SingleControlledZ", MQT_NAMED_BUILDER(::qc::singleControlledZ), nullptr, MQT_NAMED_BUILDER(mlir::qc::singleControlledZ), - MQT_NAMED_BUILDER(mlir::qir::singleControlledZ)}, + MQT_NAMED_BUILDER(mlir::qir::singleControlledZ)}, CompilerPipelineTestCase{ - "MultipleControlledZ", MQT_NAMED_BUILDER(qc::multipleControlledZ), + "MultipleControlledZ", MQT_NAMED_BUILDER(::qc::multipleControlledZ), nullptr, MQT_NAMED_BUILDER(mlir::qc::multipleControlledZ), - MQT_NAMED_BUILDER(mlir::qir::multipleControlledZ)}, - CompilerPipelineTestCase{"H", MQT_NAMED_BUILDER(qc::h), nullptr, + MQT_NAMED_BUILDER(mlir::qir::multipleControlledZ)}, + CompilerPipelineTestCase{"H", MQT_NAMED_BUILDER(::qc::h), nullptr, MQT_NAMED_BUILDER(mlir::qc::h), - MQT_NAMED_BUILDER(mlir::qir::h)}, + MQT_NAMED_BUILDER(mlir::qir::h)}, CompilerPipelineTestCase{ - "SingleControlledH", MQT_NAMED_BUILDER(qc::singleControlledH), + "SingleControlledH", MQT_NAMED_BUILDER(::qc::singleControlledH), nullptr, MQT_NAMED_BUILDER(mlir::qc::singleControlledH), - MQT_NAMED_BUILDER(mlir::qir::singleControlledH)}, + MQT_NAMED_BUILDER(mlir::qir::singleControlledH)}, CompilerPipelineTestCase{ - "MultipleControlledH", MQT_NAMED_BUILDER(qc::multipleControlledH), + "MultipleControlledH", MQT_NAMED_BUILDER(::qc::multipleControlledH), nullptr, MQT_NAMED_BUILDER(mlir::qc::multipleControlledH), - MQT_NAMED_BUILDER(mlir::qir::multipleControlledH)}, - CompilerPipelineTestCase{"HWithoutRegister", nullptr, - MQT_NAMED_BUILDER(mlir::qc::hWithoutRegister), - MQT_NAMED_BUILDER(mlir::qc::hWithoutRegister), - MQT_NAMED_BUILDER(mlir::qir::hWithoutRegister), - false}, - CompilerPipelineTestCase{"S", MQT_NAMED_BUILDER(qc::s), nullptr, + MQT_NAMED_BUILDER(mlir::qir::multipleControlledH)}, + CompilerPipelineTestCase{ + "HWithoutRegister", nullptr, + MQT_NAMED_BUILDER(mlir::qc::hWithoutRegister), + MQT_NAMED_BUILDER(mlir::qc::hWithoutRegister), + MQT_NAMED_BUILDER(mlir::qir::hWithoutRegister), false}, + CompilerPipelineTestCase{"S", MQT_NAMED_BUILDER(::qc::s), nullptr, MQT_NAMED_BUILDER(mlir::qc::s), - MQT_NAMED_BUILDER(mlir::qir::s)}, + MQT_NAMED_BUILDER(mlir::qir::s)}, CompilerPipelineTestCase{ - "SingleControlledS", MQT_NAMED_BUILDER(qc::singleControlledS), + "SingleControlledS", MQT_NAMED_BUILDER(::qc::singleControlledS), nullptr, MQT_NAMED_BUILDER(mlir::qc::singleControlledS), - MQT_NAMED_BUILDER(mlir::qir::singleControlledS)}, + MQT_NAMED_BUILDER(mlir::qir::singleControlledS)}, CompilerPipelineTestCase{ - "MultipleControlledS", MQT_NAMED_BUILDER(qc::multipleControlledS), + "MultipleControlledS", MQT_NAMED_BUILDER(::qc::multipleControlledS), nullptr, MQT_NAMED_BUILDER(mlir::qc::multipleControlledS), - MQT_NAMED_BUILDER(mlir::qir::multipleControlledS)}, - CompilerPipelineTestCase{"Sdg", MQT_NAMED_BUILDER(qc::sdg), nullptr, + MQT_NAMED_BUILDER(mlir::qir::multipleControlledS)}, + CompilerPipelineTestCase{"Sdg", MQT_NAMED_BUILDER(::qc::sdg), nullptr, MQT_NAMED_BUILDER(mlir::qc::sdg), - MQT_NAMED_BUILDER(mlir::qir::sdg)}, + MQT_NAMED_BUILDER(mlir::qir::sdg)}, CompilerPipelineTestCase{ - "SingleControlledSdg", MQT_NAMED_BUILDER(qc::singleControlledSdg), + "SingleControlledSdg", MQT_NAMED_BUILDER(::qc::singleControlledSdg), nullptr, MQT_NAMED_BUILDER(mlir::qc::singleControlledSdg), - MQT_NAMED_BUILDER(mlir::qir::singleControlledSdg)}, + MQT_NAMED_BUILDER(mlir::qir::singleControlledSdg)}, CompilerPipelineTestCase{ "MultipleControlledSdg", - MQT_NAMED_BUILDER(qc::multipleControlledSdg), nullptr, + MQT_NAMED_BUILDER(::qc::multipleControlledSdg), nullptr, MQT_NAMED_BUILDER(mlir::qc::multipleControlledSdg), - MQT_NAMED_BUILDER(mlir::qir::multipleControlledSdg)}, - CompilerPipelineTestCase{"T", MQT_NAMED_BUILDER(qc::t_), nullptr, + MQT_NAMED_BUILDER(mlir::qir::multipleControlledSdg)}, + CompilerPipelineTestCase{"T", MQT_NAMED_BUILDER(::qc::t_), nullptr, MQT_NAMED_BUILDER(mlir::qc::t_), - MQT_NAMED_BUILDER(mlir::qir::t_)}, + MQT_NAMED_BUILDER(mlir::qir::t_)}, CompilerPipelineTestCase{ - "SingleControlledT", MQT_NAMED_BUILDER(qc::singleControlledT), + "SingleControlledT", MQT_NAMED_BUILDER(::qc::singleControlledT), nullptr, MQT_NAMED_BUILDER(mlir::qc::singleControlledT), - MQT_NAMED_BUILDER(mlir::qir::singleControlledT)}, + MQT_NAMED_BUILDER(mlir::qir::singleControlledT)}, CompilerPipelineTestCase{ - "MultipleControlledT", MQT_NAMED_BUILDER(qc::multipleControlledT), + "MultipleControlledT", MQT_NAMED_BUILDER(::qc::multipleControlledT), nullptr, MQT_NAMED_BUILDER(mlir::qc::multipleControlledT), - MQT_NAMED_BUILDER(mlir::qir::multipleControlledT)}, - CompilerPipelineTestCase{"Tdg", MQT_NAMED_BUILDER(qc::tdg), nullptr, + MQT_NAMED_BUILDER(mlir::qir::multipleControlledT)}, + CompilerPipelineTestCase{"Tdg", MQT_NAMED_BUILDER(::qc::tdg), nullptr, MQT_NAMED_BUILDER(mlir::qc::tdg), - MQT_NAMED_BUILDER(mlir::qir::tdg)}, + MQT_NAMED_BUILDER(mlir::qir::tdg)}, CompilerPipelineTestCase{ - "SingleControlledTdg", MQT_NAMED_BUILDER(qc::singleControlledTdg), + "SingleControlledTdg", MQT_NAMED_BUILDER(::qc::singleControlledTdg), nullptr, MQT_NAMED_BUILDER(mlir::qc::singleControlledTdg), - MQT_NAMED_BUILDER(mlir::qir::singleControlledTdg)}, + MQT_NAMED_BUILDER(mlir::qir::singleControlledTdg)}, CompilerPipelineTestCase{ "MultipleControlledTdg", - MQT_NAMED_BUILDER(qc::multipleControlledTdg), nullptr, + MQT_NAMED_BUILDER(::qc::multipleControlledTdg), nullptr, MQT_NAMED_BUILDER(mlir::qc::multipleControlledTdg), - MQT_NAMED_BUILDER(mlir::qir::multipleControlledTdg)}, - CompilerPipelineTestCase{"SX", MQT_NAMED_BUILDER(qc::sx), nullptr, + MQT_NAMED_BUILDER(mlir::qir::multipleControlledTdg)}, + CompilerPipelineTestCase{"SX", MQT_NAMED_BUILDER(::qc::sx), nullptr, MQT_NAMED_BUILDER(mlir::qc::sx), - MQT_NAMED_BUILDER(mlir::qir::sx)}, + MQT_NAMED_BUILDER(mlir::qir::sx)}, CompilerPipelineTestCase{ - "SingleControlledSX", MQT_NAMED_BUILDER(qc::singleControlledSx), + "SingleControlledSX", MQT_NAMED_BUILDER(::qc::singleControlledSx), nullptr, MQT_NAMED_BUILDER(mlir::qc::singleControlledSx), - MQT_NAMED_BUILDER(mlir::qir::singleControlledSx)}, + MQT_NAMED_BUILDER(mlir::qir::singleControlledSx)}, CompilerPipelineTestCase{ - "MultipleControlledSX", MQT_NAMED_BUILDER(qc::multipleControlledSx), - nullptr, MQT_NAMED_BUILDER(mlir::qc::multipleControlledSx), - MQT_NAMED_BUILDER(mlir::qir::multipleControlledSx)}, - CompilerPipelineTestCase{"SXdg", MQT_NAMED_BUILDER(qc::sxdg), nullptr, + "MultipleControlledSX", + MQT_NAMED_BUILDER(::qc::multipleControlledSx), nullptr, + MQT_NAMED_BUILDER(mlir::qc::multipleControlledSx), + MQT_NAMED_BUILDER(mlir::qir::multipleControlledSx)}, + CompilerPipelineTestCase{"SXdg", MQT_NAMED_BUILDER(::qc::sxdg), nullptr, MQT_NAMED_BUILDER(mlir::qc::sxdg), - MQT_NAMED_BUILDER(mlir::qir::sxdg)}, + MQT_NAMED_BUILDER(mlir::qir::sxdg)}, CompilerPipelineTestCase{ - "SingleControlledSXdg", MQT_NAMED_BUILDER(qc::singleControlledSxdg), - nullptr, MQT_NAMED_BUILDER(mlir::qc::singleControlledSxdg), - MQT_NAMED_BUILDER(mlir::qir::singleControlledSxdg)}, + "SingleControlledSXdg", + MQT_NAMED_BUILDER(::qc::singleControlledSxdg), nullptr, + MQT_NAMED_BUILDER(mlir::qc::singleControlledSxdg), + MQT_NAMED_BUILDER(mlir::qir::singleControlledSxdg)}, CompilerPipelineTestCase{ "MultipleControlledSXdg", - MQT_NAMED_BUILDER(qc::multipleControlledSxdg), nullptr, + MQT_NAMED_BUILDER(::qc::multipleControlledSxdg), nullptr, MQT_NAMED_BUILDER(mlir::qc::multipleControlledSxdg), - MQT_NAMED_BUILDER(mlir::qir::multipleControlledSxdg)}, - CompilerPipelineTestCase{"RX", MQT_NAMED_BUILDER(qc::rx), nullptr, + MQT_NAMED_BUILDER(mlir::qir::multipleControlledSxdg)}, + CompilerPipelineTestCase{"RX", MQT_NAMED_BUILDER(::qc::rx), nullptr, MQT_NAMED_BUILDER(mlir::qc::rx), - MQT_NAMED_BUILDER(mlir::qir::rx)}, + MQT_NAMED_BUILDER(mlir::qir::rx)}, CompilerPipelineTestCase{ - "SingleControlledRX", MQT_NAMED_BUILDER(qc::singleControlledRx), + "SingleControlledRX", MQT_NAMED_BUILDER(::qc::singleControlledRx), nullptr, MQT_NAMED_BUILDER(mlir::qc::singleControlledRx), - MQT_NAMED_BUILDER(mlir::qir::singleControlledRx)}, + MQT_NAMED_BUILDER(mlir::qir::singleControlledRx)}, CompilerPipelineTestCase{ - "MultipleControlledRX", MQT_NAMED_BUILDER(qc::multipleControlledRx), - nullptr, MQT_NAMED_BUILDER(mlir::qc::multipleControlledRx), - MQT_NAMED_BUILDER(mlir::qir::multipleControlledRx)}, - CompilerPipelineTestCase{"RY", MQT_NAMED_BUILDER(qc::ry), nullptr, + "MultipleControlledRX", + MQT_NAMED_BUILDER(::qc::multipleControlledRx), nullptr, + MQT_NAMED_BUILDER(mlir::qc::multipleControlledRx), + MQT_NAMED_BUILDER(mlir::qir::multipleControlledRx)}, + CompilerPipelineTestCase{"RY", MQT_NAMED_BUILDER(::qc::ry), nullptr, MQT_NAMED_BUILDER(mlir::qc::ry), - MQT_NAMED_BUILDER(mlir::qir::ry)}, + MQT_NAMED_BUILDER(mlir::qir::ry)}, CompilerPipelineTestCase{ - "SingleControlledRY", MQT_NAMED_BUILDER(qc::singleControlledRy), + "SingleControlledRY", MQT_NAMED_BUILDER(::qc::singleControlledRy), nullptr, MQT_NAMED_BUILDER(mlir::qc::singleControlledRy), - MQT_NAMED_BUILDER(mlir::qir::singleControlledRy)}, + MQT_NAMED_BUILDER(mlir::qir::singleControlledRy)}, CompilerPipelineTestCase{ - "MultipleControlledRY", MQT_NAMED_BUILDER(qc::multipleControlledRy), - nullptr, MQT_NAMED_BUILDER(mlir::qc::multipleControlledRy), - MQT_NAMED_BUILDER(mlir::qir::multipleControlledRy)}, - CompilerPipelineTestCase{"RZ", MQT_NAMED_BUILDER(qc::rz), nullptr, + "MultipleControlledRY", + MQT_NAMED_BUILDER(::qc::multipleControlledRy), nullptr, + MQT_NAMED_BUILDER(mlir::qc::multipleControlledRy), + MQT_NAMED_BUILDER(mlir::qir::multipleControlledRy)}, + CompilerPipelineTestCase{"RZ", MQT_NAMED_BUILDER(::qc::rz), nullptr, MQT_NAMED_BUILDER(mlir::qc::rz), - MQT_NAMED_BUILDER(mlir::qir::rz)}, + MQT_NAMED_BUILDER(mlir::qir::rz)}, CompilerPipelineTestCase{ - "SingleControlledRZ", MQT_NAMED_BUILDER(qc::singleControlledRz), + "SingleControlledRZ", MQT_NAMED_BUILDER(::qc::singleControlledRz), nullptr, MQT_NAMED_BUILDER(mlir::qc::singleControlledRz), - MQT_NAMED_BUILDER(mlir::qir::singleControlledRz)}, + MQT_NAMED_BUILDER(mlir::qir::singleControlledRz)}, CompilerPipelineTestCase{ - "MultipleControlledRZ", MQT_NAMED_BUILDER(qc::multipleControlledRz), - nullptr, MQT_NAMED_BUILDER(mlir::qc::multipleControlledRz), - MQT_NAMED_BUILDER(mlir::qir::multipleControlledRz)}, - CompilerPipelineTestCase{"P", MQT_NAMED_BUILDER(qc::p), nullptr, + "MultipleControlledRZ", + MQT_NAMED_BUILDER(::qc::multipleControlledRz), nullptr, + MQT_NAMED_BUILDER(mlir::qc::multipleControlledRz), + MQT_NAMED_BUILDER(mlir::qir::multipleControlledRz)}, + CompilerPipelineTestCase{"P", MQT_NAMED_BUILDER(::qc::p), nullptr, MQT_NAMED_BUILDER(mlir::qc::p), - MQT_NAMED_BUILDER(mlir::qir::p)}, + MQT_NAMED_BUILDER(mlir::qir::p)}, CompilerPipelineTestCase{ "SingleControlledP", - MQT_NAMED_BUILDER(qc::singleControlledP), nullptr, + MQT_NAMED_BUILDER(::qc::singleControlledP), nullptr, MQT_NAMED_BUILDER(mlir::qc::singleControlledP), - MQT_NAMED_BUILDER(mlir::qir::singleControlledP)}, + MQT_NAMED_BUILDER(mlir::qir::singleControlledP)}, CompilerPipelineTestCase{ - "MultipleControlledP", MQT_NAMED_BUILDER(qc::multipleControlledP), + "MultipleControlledP", MQT_NAMED_BUILDER(::qc::multipleControlledP), nullptr, MQT_NAMED_BUILDER(mlir::qc::multipleControlledP), - MQT_NAMED_BUILDER(mlir::qir::multipleControlledP)}, - CompilerPipelineTestCase{"R", MQT_NAMED_BUILDER(qc::r), nullptr, + MQT_NAMED_BUILDER(mlir::qir::multipleControlledP)}, + CompilerPipelineTestCase{"R", MQT_NAMED_BUILDER(::qc::r), nullptr, MQT_NAMED_BUILDER(mlir::qc::r), - MQT_NAMED_BUILDER(mlir::qir::r)}, + MQT_NAMED_BUILDER(mlir::qir::r)}, CompilerPipelineTestCase{ "SingleControlledR", - MQT_NAMED_BUILDER(qc::singleControlledR), nullptr, + MQT_NAMED_BUILDER(::qc::singleControlledR), nullptr, MQT_NAMED_BUILDER(mlir::qc::singleControlledR), - MQT_NAMED_BUILDER(mlir::qir::singleControlledR)}, + MQT_NAMED_BUILDER(mlir::qir::singleControlledR)}, CompilerPipelineTestCase{ - "MultipleControlledR", MQT_NAMED_BUILDER(qc::multipleControlledR), + "MultipleControlledR", MQT_NAMED_BUILDER(::qc::multipleControlledR), nullptr, MQT_NAMED_BUILDER(mlir::qc::multipleControlledR), - MQT_NAMED_BUILDER(mlir::qir::multipleControlledR)}, - CompilerPipelineTestCase{"U2", MQT_NAMED_BUILDER(qc::u2), nullptr, + MQT_NAMED_BUILDER(mlir::qir::multipleControlledR)}, + CompilerPipelineTestCase{"U2", MQT_NAMED_BUILDER(::qc::u2), nullptr, MQT_NAMED_BUILDER(mlir::qc::u2), - MQT_NAMED_BUILDER(mlir::qir::u2)}, + MQT_NAMED_BUILDER(mlir::qir::u2)}, CompilerPipelineTestCase{ - "SingleControlledU2", MQT_NAMED_BUILDER(qc::singleControlledU2), + "SingleControlledU2", MQT_NAMED_BUILDER(::qc::singleControlledU2), nullptr, MQT_NAMED_BUILDER(mlir::qc::singleControlledU2), - MQT_NAMED_BUILDER(mlir::qir::singleControlledU2)}, + MQT_NAMED_BUILDER(mlir::qir::singleControlledU2)}, CompilerPipelineTestCase{ - "MultipleControlledU2", MQT_NAMED_BUILDER(qc::multipleControlledU2), - nullptr, MQT_NAMED_BUILDER(mlir::qc::multipleControlledU2), - MQT_NAMED_BUILDER(mlir::qir::multipleControlledU2)}, - CompilerPipelineTestCase{"U", MQT_NAMED_BUILDER(qc::u), nullptr, + "MultipleControlledU2", + MQT_NAMED_BUILDER(::qc::multipleControlledU2), nullptr, + MQT_NAMED_BUILDER(mlir::qc::multipleControlledU2), + MQT_NAMED_BUILDER(mlir::qir::multipleControlledU2)}, + CompilerPipelineTestCase{"U", MQT_NAMED_BUILDER(::qc::u), nullptr, MQT_NAMED_BUILDER(mlir::qc::u), - MQT_NAMED_BUILDER(mlir::qir::u)}, + MQT_NAMED_BUILDER(mlir::qir::u)}, CompilerPipelineTestCase{ "SingleControlledU", - MQT_NAMED_BUILDER(qc::singleControlledU), nullptr, + MQT_NAMED_BUILDER(::qc::singleControlledU), nullptr, MQT_NAMED_BUILDER(mlir::qc::singleControlledU), - MQT_NAMED_BUILDER(mlir::qir::singleControlledU)}, + MQT_NAMED_BUILDER(mlir::qir::singleControlledU)}, CompilerPipelineTestCase{ - "MultipleControlledU", MQT_NAMED_BUILDER(qc::multipleControlledU), + "MultipleControlledU", MQT_NAMED_BUILDER(::qc::multipleControlledU), nullptr, MQT_NAMED_BUILDER(mlir::qc::multipleControlledU), - MQT_NAMED_BUILDER(mlir::qir::multipleControlledU)}, - CompilerPipelineTestCase{"SWAP", MQT_NAMED_BUILDER(qc::swap), nullptr, + MQT_NAMED_BUILDER(mlir::qir::multipleControlledU)}, + CompilerPipelineTestCase{"SWAP", MQT_NAMED_BUILDER(::qc::swap), nullptr, MQT_NAMED_BUILDER(mlir::qc::swap), - MQT_NAMED_BUILDER(mlir::qir::swap)}, + MQT_NAMED_BUILDER(mlir::qir::swap)}, CompilerPipelineTestCase{ - "SingleControlledSWAP", MQT_NAMED_BUILDER(qc::singleControlledSwap), - nullptr, MQT_NAMED_BUILDER(mlir::qc::singleControlledSwap), - MQT_NAMED_BUILDER(mlir::qir::singleControlledSwap)}, + "SingleControlledSWAP", + MQT_NAMED_BUILDER(::qc::singleControlledSwap), nullptr, + MQT_NAMED_BUILDER(mlir::qc::singleControlledSwap), + MQT_NAMED_BUILDER(mlir::qir::singleControlledSwap)}, CompilerPipelineTestCase{ "MultipleControlledSWAP", - MQT_NAMED_BUILDER(qc::multipleControlledSwap), nullptr, + MQT_NAMED_BUILDER(::qc::multipleControlledSwap), nullptr, MQT_NAMED_BUILDER(mlir::qc::multipleControlledSwap), - MQT_NAMED_BUILDER(mlir::qir::multipleControlledSwap)}, - CompilerPipelineTestCase{"iSWAP", MQT_NAMED_BUILDER(qc::iswap), nullptr, - MQT_NAMED_BUILDER(mlir::qc::iswap), - MQT_NAMED_BUILDER(mlir::qir::iswap)}, + MQT_NAMED_BUILDER(mlir::qir::multipleControlledSwap)}, + CompilerPipelineTestCase{"iSWAP", MQT_NAMED_BUILDER(::qc::iswap), + nullptr, MQT_NAMED_BUILDER(mlir::qc::iswap), + MQT_NAMED_BUILDER(mlir::qir::iswap)}, CompilerPipelineTestCase{ "SingleControllediSWAP", - MQT_NAMED_BUILDER(qc::singleControlledIswap), nullptr, + MQT_NAMED_BUILDER(::qc::singleControlledIswap), nullptr, MQT_NAMED_BUILDER(mlir::qc::singleControlledIswap), - MQT_NAMED_BUILDER(mlir::qir::singleControlledIswap)}, + MQT_NAMED_BUILDER(mlir::qir::singleControlledIswap)}, CompilerPipelineTestCase{ "MultipleControllediSWAP", - MQT_NAMED_BUILDER(qc::multipleControlledIswap), nullptr, + MQT_NAMED_BUILDER(::qc::multipleControlledIswap), nullptr, MQT_NAMED_BUILDER(mlir::qc::multipleControlledIswap), - MQT_NAMED_BUILDER(mlir::qir::multipleControlledIswap)}, + MQT_NAMED_BUILDER(mlir::qir::multipleControlledIswap)}, CompilerPipelineTestCase{ - "InverseISWAP", MQT_NAMED_BUILDER(qc::inverseIswap), nullptr, + "InverseISWAP", MQT_NAMED_BUILDER(::qc::inverseIswap), nullptr, MQT_NAMED_BUILDER(mlir::qc::inverseIswap), nullptr, true, false}, CompilerPipelineTestCase{ "InverseMultiControlledISWAP", - MQT_NAMED_BUILDER(qc::inverseMultipleControlledIswap), nullptr, + MQT_NAMED_BUILDER(::qc::inverseMultipleControlledIswap), nullptr, MQT_NAMED_BUILDER(mlir::qc::inverseMultipleControlledIswap), nullptr, true, false}, - CompilerPipelineTestCase{"DCX", MQT_NAMED_BUILDER(qc::dcx), nullptr, + CompilerPipelineTestCase{"DCX", MQT_NAMED_BUILDER(::qc::dcx), nullptr, MQT_NAMED_BUILDER(mlir::qc::dcx), - MQT_NAMED_BUILDER(mlir::qir::dcx)}, + MQT_NAMED_BUILDER(mlir::qir::dcx)}, CompilerPipelineTestCase{ - "SingleControlledDCX", MQT_NAMED_BUILDER(qc::singleControlledDcx), + "SingleControlledDCX", MQT_NAMED_BUILDER(::qc::singleControlledDcx), nullptr, MQT_NAMED_BUILDER(mlir::qc::singleControlledDcx), - MQT_NAMED_BUILDER(mlir::qir::singleControlledDcx)}, + MQT_NAMED_BUILDER(mlir::qir::singleControlledDcx)}, CompilerPipelineTestCase{ "MultipleControlledDCX", - MQT_NAMED_BUILDER(qc::multipleControlledDcx), nullptr, + MQT_NAMED_BUILDER(::qc::multipleControlledDcx), nullptr, MQT_NAMED_BUILDER(mlir::qc::multipleControlledDcx), - MQT_NAMED_BUILDER(mlir::qir::multipleControlledDcx)}, - CompilerPipelineTestCase{"ECR", MQT_NAMED_BUILDER(qc::ecr), nullptr, + MQT_NAMED_BUILDER(mlir::qir::multipleControlledDcx)}, + CompilerPipelineTestCase{"ECR", MQT_NAMED_BUILDER(::qc::ecr), nullptr, MQT_NAMED_BUILDER(mlir::qc::ecr), - MQT_NAMED_BUILDER(mlir::qir::ecr)}, + MQT_NAMED_BUILDER(mlir::qir::ecr)}, CompilerPipelineTestCase{ - "SingleControlledECR", MQT_NAMED_BUILDER(qc::singleControlledEcr), + "SingleControlledECR", MQT_NAMED_BUILDER(::qc::singleControlledEcr), nullptr, MQT_NAMED_BUILDER(mlir::qc::singleControlledEcr), - MQT_NAMED_BUILDER(mlir::qir::singleControlledEcr)}, + MQT_NAMED_BUILDER(mlir::qir::singleControlledEcr)}, CompilerPipelineTestCase{ "MultipleControlledECR", - MQT_NAMED_BUILDER(qc::multipleControlledEcr), nullptr, + MQT_NAMED_BUILDER(::qc::multipleControlledEcr), nullptr, MQT_NAMED_BUILDER(mlir::qc::multipleControlledEcr), - MQT_NAMED_BUILDER(mlir::qir::multipleControlledEcr)}, - CompilerPipelineTestCase{"RXX", MQT_NAMED_BUILDER(qc::rxx), nullptr, + MQT_NAMED_BUILDER(mlir::qir::multipleControlledEcr)}, + CompilerPipelineTestCase{"RXX", MQT_NAMED_BUILDER(::qc::rxx), nullptr, MQT_NAMED_BUILDER(mlir::qc::rxx), - MQT_NAMED_BUILDER(mlir::qir::rxx)}, + MQT_NAMED_BUILDER(mlir::qir::rxx)}, CompilerPipelineTestCase{ - "SingleControlledRXX", MQT_NAMED_BUILDER(qc::singleControlledRxx), + "SingleControlledRXX", MQT_NAMED_BUILDER(::qc::singleControlledRxx), nullptr, MQT_NAMED_BUILDER(mlir::qc::singleControlledRxx), - MQT_NAMED_BUILDER(mlir::qir::singleControlledRxx)}, + MQT_NAMED_BUILDER(mlir::qir::singleControlledRxx)}, CompilerPipelineTestCase{ "MultipleControlledRXX", - MQT_NAMED_BUILDER(qc::multipleControlledRxx), nullptr, + MQT_NAMED_BUILDER(::qc::multipleControlledRxx), nullptr, MQT_NAMED_BUILDER(mlir::qc::multipleControlledRxx), - MQT_NAMED_BUILDER(mlir::qir::multipleControlledRxx)}, + MQT_NAMED_BUILDER(mlir::qir::multipleControlledRxx)}, CompilerPipelineTestCase{ - "TripleControlledRXX", MQT_NAMED_BUILDER(qc::tripleControlledRxx), + "TripleControlledRXX", MQT_NAMED_BUILDER(::qc::tripleControlledRxx), nullptr, MQT_NAMED_BUILDER(mlir::qc::tripleControlledRxx), - MQT_NAMED_BUILDER(mlir::qir::tripleControlledRxx)}, - CompilerPipelineTestCase{"RYY", MQT_NAMED_BUILDER(qc::ryy), nullptr, + MQT_NAMED_BUILDER(mlir::qir::tripleControlledRxx)}, + CompilerPipelineTestCase{"RYY", MQT_NAMED_BUILDER(::qc::ryy), nullptr, MQT_NAMED_BUILDER(mlir::qc::ryy), - MQT_NAMED_BUILDER(mlir::qir::ryy)}, + MQT_NAMED_BUILDER(mlir::qir::ryy)}, CompilerPipelineTestCase{ - "SingleControlledRYY", MQT_NAMED_BUILDER(qc::singleControlledRyy), + "SingleControlledRYY", MQT_NAMED_BUILDER(::qc::singleControlledRyy), nullptr, MQT_NAMED_BUILDER(mlir::qc::singleControlledRyy), - MQT_NAMED_BUILDER(mlir::qir::singleControlledRyy)}, + MQT_NAMED_BUILDER(mlir::qir::singleControlledRyy)}, CompilerPipelineTestCase{ "MultipleControlledRYY", - MQT_NAMED_BUILDER(qc::multipleControlledRyy), nullptr, + MQT_NAMED_BUILDER(::qc::multipleControlledRyy), nullptr, MQT_NAMED_BUILDER(mlir::qc::multipleControlledRyy), - MQT_NAMED_BUILDER(mlir::qir::multipleControlledRyy)}, - CompilerPipelineTestCase{"RZX", MQT_NAMED_BUILDER(qc::rzx), nullptr, + MQT_NAMED_BUILDER(mlir::qir::multipleControlledRyy)}, + CompilerPipelineTestCase{"RZX", MQT_NAMED_BUILDER(::qc::rzx), nullptr, MQT_NAMED_BUILDER(mlir::qc::rzx), - MQT_NAMED_BUILDER(mlir::qir::rzx)}, + MQT_NAMED_BUILDER(mlir::qir::rzx)}, CompilerPipelineTestCase{ - "SingleControlledRZX", MQT_NAMED_BUILDER(qc::singleControlledRzx), + "SingleControlledRZX", MQT_NAMED_BUILDER(::qc::singleControlledRzx), nullptr, MQT_NAMED_BUILDER(mlir::qc::singleControlledRzx), - MQT_NAMED_BUILDER(mlir::qir::singleControlledRzx)}, + MQT_NAMED_BUILDER(mlir::qir::singleControlledRzx)}, CompilerPipelineTestCase{ "MultipleControlledRZX", - MQT_NAMED_BUILDER(qc::multipleControlledRzx), nullptr, + MQT_NAMED_BUILDER(::qc::multipleControlledRzx), nullptr, MQT_NAMED_BUILDER(mlir::qc::multipleControlledRzx), - MQT_NAMED_BUILDER(mlir::qir::multipleControlledRzx)}, - CompilerPipelineTestCase{"RZZ", MQT_NAMED_BUILDER(qc::rzz), nullptr, + MQT_NAMED_BUILDER(mlir::qir::multipleControlledRzx)}, + CompilerPipelineTestCase{"RZZ", MQT_NAMED_BUILDER(::qc::rzz), nullptr, MQT_NAMED_BUILDER(mlir::qc::rzz), - MQT_NAMED_BUILDER(mlir::qir::rzz)}, + MQT_NAMED_BUILDER(mlir::qir::rzz)}, CompilerPipelineTestCase{ - "SingleControlledRZZ", MQT_NAMED_BUILDER(qc::singleControlledRzz), + "SingleControlledRZZ", MQT_NAMED_BUILDER(::qc::singleControlledRzz), nullptr, MQT_NAMED_BUILDER(mlir::qc::singleControlledRzz), - MQT_NAMED_BUILDER(mlir::qir::singleControlledRzz)}, + MQT_NAMED_BUILDER(mlir::qir::singleControlledRzz)}, CompilerPipelineTestCase{ "MultipleControlledRZZ", - MQT_NAMED_BUILDER(qc::multipleControlledRzz), nullptr, + MQT_NAMED_BUILDER(::qc::multipleControlledRzz), nullptr, MQT_NAMED_BUILDER(mlir::qc::multipleControlledRzz), - MQT_NAMED_BUILDER(mlir::qir::multipleControlledRzz)}, - CompilerPipelineTestCase{"XXPlusYY", MQT_NAMED_BUILDER(qc::xxPlusYY), + MQT_NAMED_BUILDER(mlir::qir::multipleControlledRzz)}, + CompilerPipelineTestCase{"XXPlusYY", MQT_NAMED_BUILDER(::qc::xxPlusYY), nullptr, MQT_NAMED_BUILDER(mlir::qc::xxPlusYY), - MQT_NAMED_BUILDER(mlir::qir::xxPlusYY)}, + MQT_NAMED_BUILDER(mlir::qir::xxPlusYY)}, CompilerPipelineTestCase{ "SingleControlledXXPlusYY", - MQT_NAMED_BUILDER(qc::singleControlledXxPlusYY), nullptr, + MQT_NAMED_BUILDER(::qc::singleControlledXxPlusYY), nullptr, MQT_NAMED_BUILDER(mlir::qc::singleControlledXxPlusYY), - MQT_NAMED_BUILDER(mlir::qir::singleControlledXxPlusYY)}, + MQT_NAMED_BUILDER(mlir::qir::singleControlledXxPlusYY)}, CompilerPipelineTestCase{ "MultipleControlledXXPlusYY", - MQT_NAMED_BUILDER(qc::multipleControlledXxPlusYY), nullptr, + MQT_NAMED_BUILDER(::qc::multipleControlledXxPlusYY), nullptr, MQT_NAMED_BUILDER(mlir::qc::multipleControlledXxPlusYY), - MQT_NAMED_BUILDER(mlir::qir::multipleControlledXxPlusYY)}, - CompilerPipelineTestCase{"XXMinusYY", MQT_NAMED_BUILDER(qc::xxMinusYY), - nullptr, + MQT_NAMED_BUILDER(mlir::qir::multipleControlledXxPlusYY)}, + CompilerPipelineTestCase{"XXMinusYY", + MQT_NAMED_BUILDER(::qc::xxMinusYY), nullptr, MQT_NAMED_BUILDER(mlir::qc::xxMinusYY), - MQT_NAMED_BUILDER(mlir::qir::xxMinusYY)}, + MQT_NAMED_BUILDER(mlir::qir::xxMinusYY)}, CompilerPipelineTestCase{ "SingleControlledXXMinusYY", - MQT_NAMED_BUILDER(qc::singleControlledXxMinusYY), nullptr, + MQT_NAMED_BUILDER(::qc::singleControlledXxMinusYY), nullptr, MQT_NAMED_BUILDER(mlir::qc::singleControlledXxMinusYY), - MQT_NAMED_BUILDER(mlir::qir::singleControlledXxMinusYY)}, + MQT_NAMED_BUILDER(mlir::qir::singleControlledXxMinusYY)}, CompilerPipelineTestCase{ "MultipleControlledXXMinusYY", - MQT_NAMED_BUILDER(qc::multipleControlledXxMinusYY), nullptr, + MQT_NAMED_BUILDER(::qc::multipleControlledXxMinusYY), nullptr, MQT_NAMED_BUILDER(mlir::qc::multipleControlledXxMinusYY), - MQT_NAMED_BUILDER(mlir::qir::multipleControlledXxMinusYY)}, - CompilerPipelineTestCase{"CtrlTwo", MQT_NAMED_BUILDER(qc::ctrlTwo), + MQT_NAMED_BUILDER(mlir::qir::multipleControlledXxMinusYY)}, + CompilerPipelineTestCase{"CtrlTwo", MQT_NAMED_BUILDER(::qc::ctrlTwo), nullptr, MQT_NAMED_BUILDER(mlir::qc::ctrlTwo), - MQT_NAMED_BUILDER(mlir::qir::ctrlTwo)})); + MQT_NAMED_BUILDER(mlir::qir::ctrlTwo)})); } // namespace mqt::test::compiler diff --git a/mlir/unittests/Conversion/JeffRoundTrip/test_jeff_round_trip.cpp b/mlir/unittests/Conversion/JeffRoundTrip/test_jeff_round_trip.cpp index 0f09b2b274..cd4856ebac 100644 --- a/mlir/unittests/Conversion/JeffRoundTrip/test_jeff_round_trip.cpp +++ b/mlir/unittests/Conversion/JeffRoundTrip/test_jeff_round_trip.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include @@ -42,8 +43,8 @@ namespace { struct JeffRoundTripTestCase { std::string name; - mqt::test::NamedBuilder programBuilder; - mqt::test::NamedBuilder referenceBuilder; + mqt::test::NamedMLIRBuilder programBuilder; + mqt::test::NamedMLIRBuilder referenceBuilder; friend std::ostream& operator<<(std::ostream& os, const JeffRoundTripTestCase& info); @@ -91,8 +92,7 @@ TEST_P(JeffRoundTripTest, ProgramEquivalence) { const auto name = " (" + nameStr + ")"; mqt::test::DeferredPrinter printer; - auto program = - qco::QCOProgramBuilder::build(context.get(), programBuilder.fn); + auto program = mqt::test::buildMLIRProgram(context.get(), programBuilder); ASSERT_TRUE(program); printer.record(program.get(), "Original QCO IR" + name); EXPECT_TRUE(verify(*program).succeeded()); @@ -126,8 +126,7 @@ TEST_P(JeffRoundTripTest, ProgramEquivalence) { printer.record(program.get(), "Canonicalized Converted QCO IR" + name); EXPECT_TRUE(verify(*program).succeeded()); - auto reference = - qco::QCOProgramBuilder::build(context.get(), referenceBuilder.fn); + auto reference = mqt::test::buildMLIRProgram(context.get(), referenceBuilder); ASSERT_TRUE(reference); printer.record(reference.get(), "Reference QCO IR" + name); EXPECT_TRUE(verify(*reference).succeeded()); diff --git a/mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp b/mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp index f25ce73b6f..3cab63449f 100644 --- a/mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp +++ b/mlir/unittests/Conversion/QCOToQC/test_qco_to_qc.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include @@ -41,8 +42,8 @@ namespace { struct QCOToQCTestCase { std::string name; - mqt::test::NamedBuilder programBuilder; - mqt::test::NamedBuilder referenceBuilder; + mqt::test::NamedMLIRBuilder programBuilder; + mqt::test::NamedMLIRBuilder referenceBuilder; friend std::ostream& operator<<(std::ostream& os, const QCOToQCTestCase& info); @@ -85,8 +86,7 @@ TEST_P(QCOToQCTest, ProgramEquivalence) { const auto name = " (" + nameStr + ")"; mqt::test::DeferredPrinter printer; - auto program = - qco::QCOProgramBuilder::build(context.get(), programBuilder.fn); + auto program = mqt::test::buildMLIRProgram(context.get(), programBuilder); ASSERT_TRUE(program); printer.record(program.get(), "Original QCO IR" + name); EXPECT_TRUE(verify(*program).succeeded()); @@ -103,8 +103,7 @@ TEST_P(QCOToQCTest, ProgramEquivalence) { printer.record(program.get(), "Canonicalized Converted QC IR" + name); EXPECT_TRUE(verify(*program).succeeded()); - auto reference = - qc::QCProgramBuilder::build(context.get(), referenceBuilder.fn); + auto reference = mqt::test::buildMLIRProgram(context.get(), referenceBuilder); ASSERT_TRUE(reference); printer.record(reference.get(), "Reference QC IR" + name); EXPECT_TRUE(verify(*reference).succeeded()); diff --git a/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp b/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp index 9e66a0b655..0b78783834 100644 --- a/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp +++ b/mlir/unittests/Conversion/QCToQCO/test_qc_to_qco.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include @@ -41,8 +42,8 @@ namespace { struct QCToQCOTestCase { std::string name; - mqt::test::NamedBuilder programBuilder; - mqt::test::NamedBuilder referenceBuilder; + mqt::test::NamedMLIRBuilder programBuilder; + mqt::test::NamedMLIRBuilder referenceBuilder; friend std::ostream& operator<<(std::ostream& os, const QCToQCOTestCase& info); @@ -85,7 +86,7 @@ TEST_P(QCToQCOTest, ProgramEquivalence) { const auto name = " (" + GetParam().name + ")"; mqt::test::DeferredPrinter printer; - auto program = qc::QCProgramBuilder::build(context.get(), programBuilder.fn); + auto program = mqt::test::buildMLIRProgram(context.get(), programBuilder); ASSERT_TRUE(program); printer.record(program.get(), "Original QC IR" + name); EXPECT_TRUE(verify(*program).succeeded()); @@ -102,8 +103,7 @@ TEST_P(QCToQCOTest, ProgramEquivalence) { printer.record(program.get(), "Canonicalized Converted QCO IR" + name); EXPECT_TRUE(verify(*program).succeeded()); - auto reference = - qco::QCOProgramBuilder::build(context.get(), referenceBuilder.fn); + auto reference = mqt::test::buildMLIRProgram(context.get(), referenceBuilder); ASSERT_TRUE(reference); printer.record(reference.get(), "Reference QCO IR" + name); EXPECT_TRUE(verify(*reference).succeeded()); @@ -140,7 +140,7 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(qco::staticQubitsWithInv)}, QCToQCOTestCase{"AllocDeallocPair", MQT_NAMED_BUILDER(qc::allocDeallocPair), - MQT_NAMED_BUILDER(qco::allocSinkPair)})); + MQT_NAMED_BUILDER(qco::emptyQCO)})); /// @} /// \name QCToQCO/Modifiers/CtrlOp.cpp @@ -248,7 +248,7 @@ INSTANTIATE_TEST_SUITE_P( INSTANTIATE_TEST_SUITE_P(QCIDOpTest, QCToQCOTest, testing::Values(QCToQCOTestCase{ "Identity", MQT_NAMED_BUILDER(qc::identity), - MQT_NAMED_BUILDER(qco::emptyQCO)})); + MQT_NAMED_BUILDER(qco::alloc1QubitRegister)})); /// @} /// \name QCToQCO/Operations/StandardGates/IswapOp.cpp diff --git a/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp b/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp index 509f1ff321..e386a4e579 100644 --- a/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp +++ b/mlir/unittests/Conversion/QCToQIR/QCToQIRAdaptive/test_qc_to_qir_adaptive.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include @@ -42,8 +43,8 @@ namespace { struct QCToQIRAdaptiveTestCase { std::string name; - mqt::test::NamedBuilder programBuilder; - mqt::test::NamedBuilder referenceBuilder; + mqt::test::NamedMLIRBuilder programBuilder; + mqt::test::NamedMLIRBuilder referenceBuilder; friend std::ostream& operator<<(std::ostream& os, const QCToQIRAdaptiveTestCase& info); @@ -87,7 +88,7 @@ TEST_P(QCToQIRAdaptiveTest, ProgramEquivalence) { const auto name = " (" + GetParam().name + ")"; mqt::test::DeferredPrinter printer; - auto program = qc::QCProgramBuilder::build(context.get(), programBuilder.fn); + auto program = mqt::test::buildMLIRProgram(context.get(), programBuilder); ASSERT_TRUE(program); printer.record(program.get(), "Original QC IR" + name); EXPECT_TRUE(verify(*program).succeeded()); @@ -105,8 +106,8 @@ TEST_P(QCToQIRAdaptiveTest, ProgramEquivalence) { EXPECT_TRUE(verify(*program).succeeded()); auto reference = - qir::QIRProgramBuilder::build(context.get(), referenceBuilder.fn, - qir::QIRProgramBuilder::Profile::Adaptive); + mqt::test::buildMLIRProgram(context.get(), referenceBuilder, + qir::QIRProgramBuilder::Profile::Adaptive); ASSERT_TRUE(reference); printer.record(reference.get(), "Reference QIR IR" + name); EXPECT_TRUE(verify(*reference).succeeded()); @@ -125,16 +126,16 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRAdaptiveBarrierOpTest, QCToQIRAdaptiveTest, testing::Values( QCToQIRAdaptiveTestCase{"Barrier", MQT_NAMED_BUILDER(qc::barrier), - MQT_NAMED_BUILDER(qir::emptyQIR)}, + MQT_NAMED_BUILDER(qir::alloc1QubitRegister<>)}, QCToQIRAdaptiveTestCase{"BarrierTwoQubits", MQT_NAMED_BUILDER(qc::barrierTwoQubits), - MQT_NAMED_BUILDER(qir::emptyQIR)}, + MQT_NAMED_BUILDER(qir::allocQubitRegister<>)}, QCToQIRAdaptiveTestCase{"BarrierMultipleQubits", MQT_NAMED_BUILDER(qc::barrierMultipleQubits), - MQT_NAMED_BUILDER(qir::emptyQIR)}, + MQT_NAMED_BUILDER(qir::alloc3QubitRegister<>)}, QCToQIRAdaptiveTestCase{"SingleControlledBarrier", MQT_NAMED_BUILDER(qc::singleControlledBarrier), - MQT_NAMED_BUILDER(qir::emptyQIR)})); + MQT_NAMED_BUILDER(qir::allocQubitRegister<>)})); /// @} /// \name QCToQIRAdaptive/Operations/StandardGates/DcxOp.cpp @@ -142,15 +143,15 @@ INSTANTIATE_TEST_SUITE_P( INSTANTIATE_TEST_SUITE_P( QCToQIRAdaptiveDCXOpTest, QCToQIRAdaptiveTest, testing::Values(QCToQIRAdaptiveTestCase{"DCX", MQT_NAMED_BUILDER(qc::dcx), - MQT_NAMED_BUILDER(qir::dcx)}, + MQT_NAMED_BUILDER(qir::dcx<>)}, QCToQIRAdaptiveTestCase{ "SingleControlledDCX", MQT_NAMED_BUILDER(qc::singleControlledDcx), - MQT_NAMED_BUILDER(qir::singleControlledDcx)}, + MQT_NAMED_BUILDER(qir::singleControlledDcx<>)}, QCToQIRAdaptiveTestCase{ "MultipleControlledDCX", MQT_NAMED_BUILDER(qc::multipleControlledDcx), - MQT_NAMED_BUILDER(qir::multipleControlledDcx)})); + MQT_NAMED_BUILDER(qir::multipleControlledDcx<>)})); /// @} /// \name QCToQIRAdaptive/Operations/StandardGates/EcrOp.cpp @@ -158,15 +159,15 @@ INSTANTIATE_TEST_SUITE_P( INSTANTIATE_TEST_SUITE_P( QCToQIRAdaptiveECROpTest, QCToQIRAdaptiveTest, testing::Values(QCToQIRAdaptiveTestCase{"ECR", MQT_NAMED_BUILDER(qc::ecr), - MQT_NAMED_BUILDER(qir::ecr)}, + MQT_NAMED_BUILDER(qir::ecr<>)}, QCToQIRAdaptiveTestCase{ "SingleControlledECR", MQT_NAMED_BUILDER(qc::singleControlledEcr), - MQT_NAMED_BUILDER(qir::singleControlledEcr)}, + MQT_NAMED_BUILDER(qir::singleControlledEcr<>)}, QCToQIRAdaptiveTestCase{ "MultipleControlledECR", MQT_NAMED_BUILDER(qc::multipleControlledEcr), - MQT_NAMED_BUILDER(qir::multipleControlledEcr)})); + MQT_NAMED_BUILDER(qir::multipleControlledEcr<>)})); /// @} /// \name QCToQIRAdaptive/Operations/StandardGates/GphaseOp.cpp @@ -174,7 +175,7 @@ INSTANTIATE_TEST_SUITE_P( INSTANTIATE_TEST_SUITE_P(QCToQIRAdaptiveGPhaseOpTest, QCToQIRAdaptiveTest, testing::Values(QCToQIRAdaptiveTestCase{ "GlobalPhase", MQT_NAMED_BUILDER(qc::globalPhase), - MQT_NAMED_BUILDER(qir::globalPhase)})); + MQT_NAMED_BUILDER(qir::globalPhase<>)})); /// @} /// \name QCToQIRAdaptive/Operations/StandardGates/HOp.cpp @@ -183,16 +184,16 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRAdaptiveHOpTest, QCToQIRAdaptiveTest, testing::Values( QCToQIRAdaptiveTestCase{"H", MQT_NAMED_BUILDER(qc::h), - MQT_NAMED_BUILDER(qir::h)}, + MQT_NAMED_BUILDER(qir::h<>)}, QCToQIRAdaptiveTestCase{"SingleControlledH", MQT_NAMED_BUILDER(qc::singleControlledH), - MQT_NAMED_BUILDER(qir::singleControlledH)}, + MQT_NAMED_BUILDER(qir::singleControlledH<>)}, QCToQIRAdaptiveTestCase{"MultipleControlledH", MQT_NAMED_BUILDER(qc::multipleControlledH), - MQT_NAMED_BUILDER(qir::multipleControlledH)}, + MQT_NAMED_BUILDER(qir::multipleControlledH<>)}, QCToQIRAdaptiveTestCase{"HWithoutRegister", MQT_NAMED_BUILDER(qc::hWithoutRegister), - MQT_NAMED_BUILDER(qir::hWithoutRegister)})); + MQT_NAMED_BUILDER(qir::hWithoutRegister<>)})); /// @} /// \name QCToQIRAdaptive/Operations/StandardGates/IdOp.cpp @@ -201,30 +202,31 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRAdaptiveIDOpTest, QCToQIRAdaptiveTest, testing::Values( QCToQIRAdaptiveTestCase{"Identity", MQT_NAMED_BUILDER(qc::identity), - MQT_NAMED_BUILDER(qir::identity)}, + MQT_NAMED_BUILDER(qir::identity<>)}, QCToQIRAdaptiveTestCase{"SingleControlledIdentity", MQT_NAMED_BUILDER(qc::singleControlledIdentity), - MQT_NAMED_BUILDER(qir::identity)}, + MQT_NAMED_BUILDER(qir::twoQubitsOneIdentity<>)}, QCToQIRAdaptiveTestCase{ "MultipleControlledIdentity", MQT_NAMED_BUILDER(qc::multipleControlledIdentity), - MQT_NAMED_BUILDER(qir::identity)})); + MQT_NAMED_BUILDER(qir::threeQubitsOneIdentity<>)})); /// @} /// \name QCToQIRAdaptive/Operations/StandardGates/IswapOp.cpp /// @{ INSTANTIATE_TEST_SUITE_P( QCToQIRAdaptiveiSWAPOpTest, QCToQIRAdaptiveTest, - testing::Values( - QCToQIRAdaptiveTestCase{"iSWAP", MQT_NAMED_BUILDER(qc::iswap), - MQT_NAMED_BUILDER(qir::iswap)}, - QCToQIRAdaptiveTestCase{"SingleControllediSWAP", - MQT_NAMED_BUILDER(qc::singleControlledIswap), - MQT_NAMED_BUILDER(qir::singleControlledIswap)}, - QCToQIRAdaptiveTestCase{ - "MultipleControllediSWAP", - MQT_NAMED_BUILDER(qc::multipleControlledIswap), - MQT_NAMED_BUILDER(qir::multipleControlledIswap)})); + testing::Values(QCToQIRAdaptiveTestCase{"iSWAP", + MQT_NAMED_BUILDER(qc::iswap), + MQT_NAMED_BUILDER(qir::iswap<>)}, + QCToQIRAdaptiveTestCase{ + "SingleControllediSWAP", + MQT_NAMED_BUILDER(qc::singleControlledIswap), + MQT_NAMED_BUILDER(qir::singleControlledIswap<>)}, + QCToQIRAdaptiveTestCase{ + "MultipleControllediSWAP", + MQT_NAMED_BUILDER(qc::multipleControlledIswap), + MQT_NAMED_BUILDER(qir::multipleControlledIswap<>)})); /// @} /// \name QCToQIRAdaptive/Operations/StandardGates/POp.cpp @@ -233,13 +235,13 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRAdaptivePOpTest, QCToQIRAdaptiveTest, testing::Values( QCToQIRAdaptiveTestCase{"P", MQT_NAMED_BUILDER(qc::p), - MQT_NAMED_BUILDER(qir::p)}, + MQT_NAMED_BUILDER(qir::p<>)}, QCToQIRAdaptiveTestCase{"SingleControlledP", MQT_NAMED_BUILDER(qc::singleControlledP), - MQT_NAMED_BUILDER(qir::singleControlledP)}, - QCToQIRAdaptiveTestCase{"MultipleControlledP", - MQT_NAMED_BUILDER(qc::multipleControlledP), - MQT_NAMED_BUILDER(qir::multipleControlledP)})); + MQT_NAMED_BUILDER(qir::singleControlledP<>)}, + QCToQIRAdaptiveTestCase{ + "MultipleControlledP", MQT_NAMED_BUILDER(qc::multipleControlledP), + MQT_NAMED_BUILDER(qir::multipleControlledP<>)})); /// @} /// \name QCToQIRAdaptive/Operations/StandardGates/ROp.cpp @@ -248,13 +250,13 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRAdaptiveROpTest, QCToQIRAdaptiveTest, testing::Values( QCToQIRAdaptiveTestCase{"R", MQT_NAMED_BUILDER(qc::r), - MQT_NAMED_BUILDER(qir::r)}, + MQT_NAMED_BUILDER(qir::r<>)}, QCToQIRAdaptiveTestCase{"SingleControlledR", MQT_NAMED_BUILDER(qc::singleControlledR), - MQT_NAMED_BUILDER(qir::singleControlledR)}, - QCToQIRAdaptiveTestCase{"MultipleControlledR", - MQT_NAMED_BUILDER(qc::multipleControlledR), - MQT_NAMED_BUILDER(qir::multipleControlledR)})); + MQT_NAMED_BUILDER(qir::singleControlledR<>)}, + QCToQIRAdaptiveTestCase{ + "MultipleControlledR", MQT_NAMED_BUILDER(qc::multipleControlledR), + MQT_NAMED_BUILDER(qir::multipleControlledR<>)})); /// @} /// \name QCToQIRAdaptive/Operations/StandardGates/RxOp.cpp @@ -263,13 +265,13 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRAdaptiveRXOpTest, QCToQIRAdaptiveTest, testing::Values( QCToQIRAdaptiveTestCase{"RX", MQT_NAMED_BUILDER(qc::rx), - MQT_NAMED_BUILDER(qir::rx)}, + MQT_NAMED_BUILDER(qir::rx<>)}, QCToQIRAdaptiveTestCase{"SingleControlledRX", MQT_NAMED_BUILDER(qc::singleControlledRx), - MQT_NAMED_BUILDER(qir::singleControlledRx)}, - QCToQIRAdaptiveTestCase{"MultipleControlledRX", - MQT_NAMED_BUILDER(qc::multipleControlledRx), - MQT_NAMED_BUILDER(qir::multipleControlledRx)})); + MQT_NAMED_BUILDER(qir::singleControlledRx<>)}, + QCToQIRAdaptiveTestCase{ + "MultipleControlledRX", MQT_NAMED_BUILDER(qc::multipleControlledRx), + MQT_NAMED_BUILDER(qir::multipleControlledRx<>)})); /// @} /// \name QCToQIRAdaptive/Operations/StandardGates/RxxOp.cpp @@ -277,15 +279,15 @@ INSTANTIATE_TEST_SUITE_P( INSTANTIATE_TEST_SUITE_P( QCToQIRAdaptiveRXXOpTest, QCToQIRAdaptiveTest, testing::Values(QCToQIRAdaptiveTestCase{"RXX", MQT_NAMED_BUILDER(qc::rxx), - MQT_NAMED_BUILDER(qir::rxx)}, + MQT_NAMED_BUILDER(qir::rxx<>)}, QCToQIRAdaptiveTestCase{ "SingleControlledRXX", MQT_NAMED_BUILDER(qc::singleControlledRxx), - MQT_NAMED_BUILDER(qir::singleControlledRxx)}, + MQT_NAMED_BUILDER(qir::singleControlledRxx<>)}, QCToQIRAdaptiveTestCase{ "MultipleControlledRXX", MQT_NAMED_BUILDER(qc::multipleControlledRxx), - MQT_NAMED_BUILDER(qir::multipleControlledRxx)})); + MQT_NAMED_BUILDER(qir::multipleControlledRxx<>)})); /// @} /// \name QCToQIRAdaptive/Operations/StandardGates/RyOp.cpp @@ -294,13 +296,13 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRAdaptiveRYOpTest, QCToQIRAdaptiveTest, testing::Values( QCToQIRAdaptiveTestCase{"RY", MQT_NAMED_BUILDER(qc::ry), - MQT_NAMED_BUILDER(qir::ry)}, + MQT_NAMED_BUILDER(qir::ry<>)}, QCToQIRAdaptiveTestCase{"SingleControlledRY", MQT_NAMED_BUILDER(qc::singleControlledRy), - MQT_NAMED_BUILDER(qir::singleControlledRy)}, - QCToQIRAdaptiveTestCase{"MultipleControlledRY", - MQT_NAMED_BUILDER(qc::multipleControlledRy), - MQT_NAMED_BUILDER(qir::multipleControlledRy)})); + MQT_NAMED_BUILDER(qir::singleControlledRy<>)}, + QCToQIRAdaptiveTestCase{ + "MultipleControlledRY", MQT_NAMED_BUILDER(qc::multipleControlledRy), + MQT_NAMED_BUILDER(qir::multipleControlledRy<>)})); /// @} /// \name QCToQIRAdaptive/Operations/StandardGates/RyyOp.cpp @@ -308,15 +310,15 @@ INSTANTIATE_TEST_SUITE_P( INSTANTIATE_TEST_SUITE_P( QCToQIRAdaptiveRYYOpTest, QCToQIRAdaptiveTest, testing::Values(QCToQIRAdaptiveTestCase{"RYY", MQT_NAMED_BUILDER(qc::ryy), - MQT_NAMED_BUILDER(qir::ryy)}, + MQT_NAMED_BUILDER(qir::ryy<>)}, QCToQIRAdaptiveTestCase{ "SingleControlledRYY", MQT_NAMED_BUILDER(qc::singleControlledRyy), - MQT_NAMED_BUILDER(qir::singleControlledRyy)}, + MQT_NAMED_BUILDER(qir::singleControlledRyy<>)}, QCToQIRAdaptiveTestCase{ "MultipleControlledRYY", MQT_NAMED_BUILDER(qc::multipleControlledRyy), - MQT_NAMED_BUILDER(qir::multipleControlledRyy)})); + MQT_NAMED_BUILDER(qir::multipleControlledRyy<>)})); /// @} /// \name QCToQIRAdaptive/Operations/StandardGates/RzOp.cpp @@ -325,13 +327,13 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRAdaptiveRZOpTest, QCToQIRAdaptiveTest, testing::Values( QCToQIRAdaptiveTestCase{"RZ", MQT_NAMED_BUILDER(qc::rz), - MQT_NAMED_BUILDER(qir::rz)}, + MQT_NAMED_BUILDER(qir::rz<>)}, QCToQIRAdaptiveTestCase{"SingleControlledRZ", MQT_NAMED_BUILDER(qc::singleControlledRz), - MQT_NAMED_BUILDER(qir::singleControlledRz)}, - QCToQIRAdaptiveTestCase{"MultipleControlledRZ", - MQT_NAMED_BUILDER(qc::multipleControlledRz), - MQT_NAMED_BUILDER(qir::multipleControlledRz)})); + MQT_NAMED_BUILDER(qir::singleControlledRz<>)}, + QCToQIRAdaptiveTestCase{ + "MultipleControlledRZ", MQT_NAMED_BUILDER(qc::multipleControlledRz), + MQT_NAMED_BUILDER(qir::multipleControlledRz<>)})); /// @} /// \name QCToQIRAdaptive/Operations/StandardGates/RzxOp.cpp @@ -339,15 +341,15 @@ INSTANTIATE_TEST_SUITE_P( INSTANTIATE_TEST_SUITE_P( QCToQIRAdaptiveRZXOpTest, QCToQIRAdaptiveTest, testing::Values(QCToQIRAdaptiveTestCase{"RZX", MQT_NAMED_BUILDER(qc::rzx), - MQT_NAMED_BUILDER(qir::rzx)}, + MQT_NAMED_BUILDER(qir::rzx<>)}, QCToQIRAdaptiveTestCase{ "SingleControlledRZX", MQT_NAMED_BUILDER(qc::singleControlledRzx), - MQT_NAMED_BUILDER(qir::singleControlledRzx)}, + MQT_NAMED_BUILDER(qir::singleControlledRzx<>)}, QCToQIRAdaptiveTestCase{ "MultipleControlledRZX", MQT_NAMED_BUILDER(qc::multipleControlledRzx), - MQT_NAMED_BUILDER(qir::multipleControlledRzx)})); + MQT_NAMED_BUILDER(qir::multipleControlledRzx<>)})); /// @} /// \name QCToQIRAdaptive/Operations/StandardGates/RzzOp.cpp @@ -355,15 +357,15 @@ INSTANTIATE_TEST_SUITE_P( INSTANTIATE_TEST_SUITE_P( QCToQIRAdaptiveRZZOpTest, QCToQIRAdaptiveTest, testing::Values(QCToQIRAdaptiveTestCase{"RZZ", MQT_NAMED_BUILDER(qc::rzz), - MQT_NAMED_BUILDER(qir::rzz)}, + MQT_NAMED_BUILDER(qir::rzz<>)}, QCToQIRAdaptiveTestCase{ "SingleControlledRZZ", MQT_NAMED_BUILDER(qc::singleControlledRzz), - MQT_NAMED_BUILDER(qir::singleControlledRzz)}, + MQT_NAMED_BUILDER(qir::singleControlledRzz<>)}, QCToQIRAdaptiveTestCase{ "MultipleControlledRZZ", MQT_NAMED_BUILDER(qc::multipleControlledRzz), - MQT_NAMED_BUILDER(qir::multipleControlledRzz)})); + MQT_NAMED_BUILDER(qir::multipleControlledRzz<>)})); /// @} /// \name QCToQIRAdaptive/Operations/StandardGates/SOp.cpp @@ -372,13 +374,13 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRAdaptiveSOpTest, QCToQIRAdaptiveTest, testing::Values( QCToQIRAdaptiveTestCase{"S", MQT_NAMED_BUILDER(qc::s), - MQT_NAMED_BUILDER(qir::s)}, + MQT_NAMED_BUILDER(qir::s<>)}, QCToQIRAdaptiveTestCase{"SingleControlledS", MQT_NAMED_BUILDER(qc::singleControlledS), - MQT_NAMED_BUILDER(qir::singleControlledS)}, - QCToQIRAdaptiveTestCase{"MultipleControlledS", - MQT_NAMED_BUILDER(qc::multipleControlledS), - MQT_NAMED_BUILDER(qir::multipleControlledS)})); + MQT_NAMED_BUILDER(qir::singleControlledS<>)}, + QCToQIRAdaptiveTestCase{ + "MultipleControlledS", MQT_NAMED_BUILDER(qc::multipleControlledS), + MQT_NAMED_BUILDER(qir::multipleControlledS<>)})); /// @} /// \name QCToQIRAdaptive/Operations/StandardGates/SdgOp.cpp @@ -386,15 +388,15 @@ INSTANTIATE_TEST_SUITE_P( INSTANTIATE_TEST_SUITE_P( QCToQIRAdaptiveSdgOpTest, QCToQIRAdaptiveTest, testing::Values(QCToQIRAdaptiveTestCase{"Sdg", MQT_NAMED_BUILDER(qc::sdg), - MQT_NAMED_BUILDER(qir::sdg)}, + MQT_NAMED_BUILDER(qir::sdg<>)}, QCToQIRAdaptiveTestCase{ "SingleControlledSdg", MQT_NAMED_BUILDER(qc::singleControlledSdg), - MQT_NAMED_BUILDER(qir::singleControlledSdg)}, + MQT_NAMED_BUILDER(qir::singleControlledSdg<>)}, QCToQIRAdaptiveTestCase{ "MultipleControlledSdg", MQT_NAMED_BUILDER(qc::multipleControlledSdg), - MQT_NAMED_BUILDER(qir::multipleControlledSdg)})); + MQT_NAMED_BUILDER(qir::multipleControlledSdg<>)})); /// @} /// \name QCToQIRAdaptive/Operations/StandardGates/SwapOp.cpp @@ -402,15 +404,15 @@ INSTANTIATE_TEST_SUITE_P( INSTANTIATE_TEST_SUITE_P( QCToQIRAdaptiveSWAPOpTest, QCToQIRAdaptiveTest, testing::Values(QCToQIRAdaptiveTestCase{"SWAP", MQT_NAMED_BUILDER(qc::swap), - MQT_NAMED_BUILDER(qir::swap)}, + MQT_NAMED_BUILDER(qir::swap<>)}, QCToQIRAdaptiveTestCase{ "SingleControlledSWAP", MQT_NAMED_BUILDER(qc::singleControlledSwap), - MQT_NAMED_BUILDER(qir::singleControlledSwap)}, + MQT_NAMED_BUILDER(qir::singleControlledSwap<>)}, QCToQIRAdaptiveTestCase{ "MultipleControlledSWAP", MQT_NAMED_BUILDER(qc::multipleControlledSwap), - MQT_NAMED_BUILDER(qir::multipleControlledSwap)})); + MQT_NAMED_BUILDER(qir::multipleControlledSwap<>)})); /// @} /// \name QCToQIRAdaptive/Operations/StandardGates/SxOp.cpp @@ -419,13 +421,13 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRAdaptiveSXOpTest, QCToQIRAdaptiveTest, testing::Values( QCToQIRAdaptiveTestCase{"SX", MQT_NAMED_BUILDER(qc::sx), - MQT_NAMED_BUILDER(qir::sx)}, + MQT_NAMED_BUILDER(qir::sx<>)}, QCToQIRAdaptiveTestCase{"SingleControlledSX", MQT_NAMED_BUILDER(qc::singleControlledSx), - MQT_NAMED_BUILDER(qir::singleControlledSx)}, - QCToQIRAdaptiveTestCase{"MultipleControlledSX", - MQT_NAMED_BUILDER(qc::multipleControlledSx), - MQT_NAMED_BUILDER(qir::multipleControlledSx)})); + MQT_NAMED_BUILDER(qir::singleControlledSx<>)}, + QCToQIRAdaptiveTestCase{ + "MultipleControlledSX", MQT_NAMED_BUILDER(qc::multipleControlledSx), + MQT_NAMED_BUILDER(qir::multipleControlledSx<>)})); /// @} /// \name QCToQIRAdaptive/Operations/StandardGates/SxdgOp.cpp @@ -433,15 +435,15 @@ INSTANTIATE_TEST_SUITE_P( INSTANTIATE_TEST_SUITE_P( QCToQIRAdaptiveSXdgOpTest, QCToQIRAdaptiveTest, testing::Values(QCToQIRAdaptiveTestCase{"SXdg", MQT_NAMED_BUILDER(qc::sxdg), - MQT_NAMED_BUILDER(qir::sxdg)}, + MQT_NAMED_BUILDER(qir::sxdg<>)}, QCToQIRAdaptiveTestCase{ "SingleControlledSXdg", MQT_NAMED_BUILDER(qc::singleControlledSxdg), - MQT_NAMED_BUILDER(qir::singleControlledSxdg)}, + MQT_NAMED_BUILDER(qir::singleControlledSxdg<>)}, QCToQIRAdaptiveTestCase{ "MultipleControlledSXdg", MQT_NAMED_BUILDER(qc::multipleControlledSxdg), - MQT_NAMED_BUILDER(qir::multipleControlledSxdg)})); + MQT_NAMED_BUILDER(qir::multipleControlledSxdg<>)})); /// @} /// \name QCToQIRAdaptive/Operations/StandardGates/TOp.cpp @@ -450,13 +452,13 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRAdaptiveTOpTest, QCToQIRAdaptiveTest, testing::Values( QCToQIRAdaptiveTestCase{"T", MQT_NAMED_BUILDER(qc::t_), - MQT_NAMED_BUILDER(qir::t_)}, + MQT_NAMED_BUILDER(qir::t_<>)}, QCToQIRAdaptiveTestCase{"SingleControlledT", MQT_NAMED_BUILDER(qc::singleControlledT), - MQT_NAMED_BUILDER(qir::singleControlledT)}, - QCToQIRAdaptiveTestCase{"MultipleControlledT", - MQT_NAMED_BUILDER(qc::multipleControlledT), - MQT_NAMED_BUILDER(qir::multipleControlledT)})); + MQT_NAMED_BUILDER(qir::singleControlledT<>)}, + QCToQIRAdaptiveTestCase{ + "MultipleControlledT", MQT_NAMED_BUILDER(qc::multipleControlledT), + MQT_NAMED_BUILDER(qir::multipleControlledT<>)})); /// @} /// \name QCToQIRAdaptive/Operations/StandardGates/TdgOp.cpp @@ -464,15 +466,15 @@ INSTANTIATE_TEST_SUITE_P( INSTANTIATE_TEST_SUITE_P( QCToQIRAdaptiveTdgOpTest, QCToQIRAdaptiveTest, testing::Values(QCToQIRAdaptiveTestCase{"Tdg", MQT_NAMED_BUILDER(qc::tdg), - MQT_NAMED_BUILDER(qir::tdg)}, + MQT_NAMED_BUILDER(qir::tdg<>)}, QCToQIRAdaptiveTestCase{ "SingleControlledTdg", MQT_NAMED_BUILDER(qc::singleControlledTdg), - MQT_NAMED_BUILDER(qir::singleControlledTdg)}, + MQT_NAMED_BUILDER(qir::singleControlledTdg<>)}, QCToQIRAdaptiveTestCase{ "MultipleControlledTdg", MQT_NAMED_BUILDER(qc::multipleControlledTdg), - MQT_NAMED_BUILDER(qir::multipleControlledTdg)})); + MQT_NAMED_BUILDER(qir::multipleControlledTdg<>)})); /// @} /// \name QCToQIRAdaptive/Operations/StandardGates/U2Op.cpp @@ -481,13 +483,13 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRAdaptiveU2OpTest, QCToQIRAdaptiveTest, testing::Values( QCToQIRAdaptiveTestCase{"U2", MQT_NAMED_BUILDER(qc::u2), - MQT_NAMED_BUILDER(qir::u2)}, + MQT_NAMED_BUILDER(qir::u2<>)}, QCToQIRAdaptiveTestCase{"SingleControlledU2", MQT_NAMED_BUILDER(qc::singleControlledU2), - MQT_NAMED_BUILDER(qir::singleControlledU2)}, - QCToQIRAdaptiveTestCase{"MultipleControlledU2", - MQT_NAMED_BUILDER(qc::multipleControlledU2), - MQT_NAMED_BUILDER(qir::multipleControlledU2)})); + MQT_NAMED_BUILDER(qir::singleControlledU2<>)}, + QCToQIRAdaptiveTestCase{ + "MultipleControlledU2", MQT_NAMED_BUILDER(qc::multipleControlledU2), + MQT_NAMED_BUILDER(qir::multipleControlledU2<>)})); /// @} /// \name QCToQIRAdaptive/Operations/StandardGates/UOp.cpp @@ -496,13 +498,13 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRAdaptiveUOpTest, QCToQIRAdaptiveTest, testing::Values( QCToQIRAdaptiveTestCase{"U", MQT_NAMED_BUILDER(qc::u), - MQT_NAMED_BUILDER(qir::u)}, + MQT_NAMED_BUILDER(qir::u<>)}, QCToQIRAdaptiveTestCase{"SingleControlledU", MQT_NAMED_BUILDER(qc::singleControlledU), - MQT_NAMED_BUILDER(qir::singleControlledU)}, - QCToQIRAdaptiveTestCase{"MultipleControlledU", - MQT_NAMED_BUILDER(qc::multipleControlledU), - MQT_NAMED_BUILDER(qir::multipleControlledU)})); + MQT_NAMED_BUILDER(qir::singleControlledU<>)}, + QCToQIRAdaptiveTestCase{ + "MultipleControlledU", MQT_NAMED_BUILDER(qc::multipleControlledU), + MQT_NAMED_BUILDER(qir::multipleControlledU<>)})); /// @} /// \name QCToQIRAdaptive/Operations/StandardGates/XOp.cpp @@ -511,30 +513,30 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRAdaptiveXOpTest, QCToQIRAdaptiveTest, testing::Values( QCToQIRAdaptiveTestCase{"X", MQT_NAMED_BUILDER(qc::x), - MQT_NAMED_BUILDER(qir::x)}, + MQT_NAMED_BUILDER(qir::x<>)}, QCToQIRAdaptiveTestCase{"SingleControlledX", MQT_NAMED_BUILDER(qc::singleControlledX), - MQT_NAMED_BUILDER(qir::singleControlledX)}, - QCToQIRAdaptiveTestCase{"MultipleControlledX", - MQT_NAMED_BUILDER(qc::multipleControlledX), - MQT_NAMED_BUILDER(qir::multipleControlledX)})); + MQT_NAMED_BUILDER(qir::singleControlledX<>)}, + QCToQIRAdaptiveTestCase{ + "MultipleControlledX", MQT_NAMED_BUILDER(qc::multipleControlledX), + MQT_NAMED_BUILDER(qir::multipleControlledX<>)})); /// @} /// \name QCToQIRAdaptive/Operations/StandardGates/XxMinusYyOp.cpp /// @{ INSTANTIATE_TEST_SUITE_P( QCToQIRAdaptiveXXMinusYYOpTest, QCToQIRAdaptiveTest, - testing::Values(QCToQIRAdaptiveTestCase{"XXMinusYY", - MQT_NAMED_BUILDER(qc::xxMinusYY), - MQT_NAMED_BUILDER(qir::xxMinusYY)}, - QCToQIRAdaptiveTestCase{ - "SingleControlledXXMinusYY", - MQT_NAMED_BUILDER(qc::singleControlledXxMinusYY), - MQT_NAMED_BUILDER(qir::singleControlledXxMinusYY)}, - QCToQIRAdaptiveTestCase{ - "MultipleControlledXXMinusYY", - MQT_NAMED_BUILDER(qc::multipleControlledXxMinusYY), - MQT_NAMED_BUILDER(qir::multipleControlledXxMinusYY)})); + testing::Values( + QCToQIRAdaptiveTestCase{"XXMinusYY", MQT_NAMED_BUILDER(qc::xxMinusYY), + MQT_NAMED_BUILDER(qir::xxMinusYY<>)}, + QCToQIRAdaptiveTestCase{ + "SingleControlledXXMinusYY", + MQT_NAMED_BUILDER(qc::singleControlledXxMinusYY), + MQT_NAMED_BUILDER(qir::singleControlledXxMinusYY<>)}, + QCToQIRAdaptiveTestCase{ + "MultipleControlledXXMinusYY", + MQT_NAMED_BUILDER(qc::multipleControlledXxMinusYY), + MQT_NAMED_BUILDER(qir::multipleControlledXxMinusYY<>)})); /// @} /// \name QCToQIRAdaptive/Operations/StandardGates/XxPlusYyOp.cpp @@ -543,15 +545,15 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRAdaptiveXXPlusYYOpTest, QCToQIRAdaptiveTest, testing::Values(QCToQIRAdaptiveTestCase{"XXPlusYY", MQT_NAMED_BUILDER(qc::xxPlusYY), - MQT_NAMED_BUILDER(qir::xxPlusYY)}, + MQT_NAMED_BUILDER(qir::xxPlusYY<>)}, QCToQIRAdaptiveTestCase{ "SingleControlledXXPlusYY", MQT_NAMED_BUILDER(qc::singleControlledXxPlusYY), - MQT_NAMED_BUILDER(qir::singleControlledXxPlusYY)}, + MQT_NAMED_BUILDER(qir::singleControlledXxPlusYY<>)}, QCToQIRAdaptiveTestCase{ "MultipleControlledXXPlusYY", MQT_NAMED_BUILDER(qc::multipleControlledXxPlusYY), - MQT_NAMED_BUILDER(qir::multipleControlledXxPlusYY)})); + MQT_NAMED_BUILDER(qir::multipleControlledXxPlusYY<>)})); /// @} /// \name QCToQIRAdaptive/Operations/StandardGates/YOp.cpp @@ -560,13 +562,13 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRAdaptiveYOpTest, QCToQIRAdaptiveTest, testing::Values( QCToQIRAdaptiveTestCase{"Y", MQT_NAMED_BUILDER(qc::y), - MQT_NAMED_BUILDER(qir::y)}, + MQT_NAMED_BUILDER(qir::y<>)}, QCToQIRAdaptiveTestCase{"SingleControlledY", MQT_NAMED_BUILDER(qc::singleControlledY), - MQT_NAMED_BUILDER(qir::singleControlledY)}, - QCToQIRAdaptiveTestCase{"MultipleControlledY", - MQT_NAMED_BUILDER(qc::multipleControlledY), - MQT_NAMED_BUILDER(qir::multipleControlledY)})); + MQT_NAMED_BUILDER(qir::singleControlledY<>)}, + QCToQIRAdaptiveTestCase{ + "MultipleControlledY", MQT_NAMED_BUILDER(qc::multipleControlledY), + MQT_NAMED_BUILDER(qir::multipleControlledY<>)})); /// @} /// \name QCToQIRAdaptive/Operations/StandardGates/ZOp.cpp @@ -575,13 +577,13 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRAdaptiveZOpTest, QCToQIRAdaptiveTest, testing::Values( QCToQIRAdaptiveTestCase{"Z", MQT_NAMED_BUILDER(qc::z), - MQT_NAMED_BUILDER(qir::z)}, + MQT_NAMED_BUILDER(qir::z<>)}, QCToQIRAdaptiveTestCase{"SingleControlledZ", MQT_NAMED_BUILDER(qc::singleControlledZ), - MQT_NAMED_BUILDER(qir::singleControlledZ)}, - QCToQIRAdaptiveTestCase{"MultipleControlledZ", - MQT_NAMED_BUILDER(qc::multipleControlledZ), - MQT_NAMED_BUILDER(qir::multipleControlledZ)})); + MQT_NAMED_BUILDER(qir::singleControlledZ<>)}, + QCToQIRAdaptiveTestCase{ + "MultipleControlledZ", MQT_NAMED_BUILDER(qc::multipleControlledZ), + MQT_NAMED_BUILDER(qir::multipleControlledZ<>)})); /// @} /// \name QCToQIRAdaptive/Operations/MeasureOp.cpp @@ -592,23 +594,24 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRAdaptiveTestCase{ "SingleMeasurementToSingleBit", MQT_NAMED_BUILDER(qc::singleMeasurementToSingleBit), - MQT_NAMED_BUILDER(qir::singleMeasurementToSingleBit)}, + MQT_NAMED_BUILDER(qir::singleMeasurementToSingleBit<>)}, QCToQIRAdaptiveTestCase{ "RepeatedMeasurementToSameBit", MQT_NAMED_BUILDER(qc::repeatedMeasurementToSameBit), - MQT_NAMED_BUILDER(qir::repeatedMeasurementToSameBit)}, + MQT_NAMED_BUILDER(qir::repeatedMeasurementToSameBit<>)}, QCToQIRAdaptiveTestCase{ "RepeatedMeasurementToDifferentBits", MQT_NAMED_BUILDER(qc::repeatedMeasurementToDifferentBits), - MQT_NAMED_BUILDER(qir::repeatedMeasurementToDifferentBits)}, + MQT_NAMED_BUILDER(qir::repeatedMeasurementToDifferentBits<>)}, QCToQIRAdaptiveTestCase{ "MultipleClassicalRegistersAndMeasurements", MQT_NAMED_BUILDER(qc::multipleClassicalRegistersAndMeasurements), - MQT_NAMED_BUILDER(qir::multipleClassicalRegistersAndMeasurements)}, + MQT_NAMED_BUILDER( + qir::multipleClassicalRegistersAndMeasurements)}, QCToQIRAdaptiveTestCase{ "MeasurementWithoutRegisters", MQT_NAMED_BUILDER(qc::measurementWithoutRegisters), - MQT_NAMED_BUILDER(qir::measurementWithoutRegisters)})); + MQT_NAMED_BUILDER(qir::measurementWithoutRegisters<>)})); /// @} /// \name QCToQIRAdaptive/Operations/ResetOp.cpp @@ -618,26 +621,27 @@ INSTANTIATE_TEST_SUITE_P( testing::Values( QCToQIRAdaptiveTestCase{"ResetQubitWithoutOp", MQT_NAMED_BUILDER(qc::resetQubitWithoutOp), - MQT_NAMED_BUILDER(qir::resetQubitWithoutOp)}, + MQT_NAMED_BUILDER(qir::resetQubitWithoutOp<>)}, QCToQIRAdaptiveTestCase{ "ResetMultipleQubitsWithoutOp", MQT_NAMED_BUILDER(qc::resetMultipleQubitsWithoutOp), - MQT_NAMED_BUILDER(qir::resetMultipleQubitsWithoutOp)}, - QCToQIRAdaptiveTestCase{"RepeatedResetWithoutOp", - MQT_NAMED_BUILDER(qc::repeatedResetWithoutOp), - MQT_NAMED_BUILDER(qir::repeatedResetWithoutOp)}, + MQT_NAMED_BUILDER(qir::resetMultipleQubitsWithoutOp<>)}, + QCToQIRAdaptiveTestCase{ + "RepeatedResetWithoutOp", + MQT_NAMED_BUILDER(qc::repeatedResetWithoutOp), + MQT_NAMED_BUILDER(qir::repeatedResetWithoutOp<>)}, QCToQIRAdaptiveTestCase{ "ResetQubitAfterSingleOp", MQT_NAMED_BUILDER(qc::resetQubitAfterSingleOp), - MQT_NAMED_BUILDER(qir::resetQubitAfterSingleOp)}, + MQT_NAMED_BUILDER(qir::resetQubitAfterSingleOp<>)}, QCToQIRAdaptiveTestCase{ "ResetMultipleQubitsAfterSingleOp", MQT_NAMED_BUILDER(qc::resetMultipleQubitsAfterSingleOp), - MQT_NAMED_BUILDER(qir::resetMultipleQubitsAfterSingleOp)}, + MQT_NAMED_BUILDER(qir::resetMultipleQubitsAfterSingleOp<>)}, QCToQIRAdaptiveTestCase{ "RepeatedResetAfterSingleOp", MQT_NAMED_BUILDER(qc::repeatedResetAfterSingleOp), - MQT_NAMED_BUILDER(qir::repeatedResetAfterSingleOp)})); + MQT_NAMED_BUILDER(qir::repeatedResetAfterSingleOp<>)})); /// @} /// \name QCToQIRAdaptive/QubitManagement/QubitManagement.cpp @@ -646,24 +650,24 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRAdaptiveQubitManagementTest, QCToQIRAdaptiveTest, testing::Values( QCToQIRAdaptiveTestCase{"AllocQubit", MQT_NAMED_BUILDER(qc::allocQubit), - MQT_NAMED_BUILDER(qir::emptyQIR)}, + MQT_NAMED_BUILDER(qir::allocQubit<>)}, QCToQIRAdaptiveTestCase{"AllocQubitRegister", MQT_NAMED_BUILDER(qc::allocQubitRegister), - MQT_NAMED_BUILDER(qir::emptyQIR)}, + MQT_NAMED_BUILDER(qir::allocQubitRegister<>)}, QCToQIRAdaptiveTestCase{ "AllocMultipleQubitRegisters", MQT_NAMED_BUILDER(qc::allocMultipleQubitRegisters), - MQT_NAMED_BUILDER(qir::emptyQIR)}, + MQT_NAMED_BUILDER(qir::allocMultipleQubitRegisters<>)}, QCToQIRAdaptiveTestCase{ "AllocMultipleQubitRegistersWithOps", MQT_NAMED_BUILDER(qc::allocMultipleQubitRegistersWithOps), - MQT_NAMED_BUILDER(qir::allocMultipleQubitRegistersWithOps)}, + MQT_NAMED_BUILDER(qir::allocMultipleQubitRegistersWithOps<>)}, QCToQIRAdaptiveTestCase{"AllocLargeRegister", MQT_NAMED_BUILDER(qc::allocLargeRegister), - MQT_NAMED_BUILDER(qir::emptyQIR)}, + MQT_NAMED_BUILDER(qir::allocQubitRegister<>)}, QCToQIRAdaptiveTestCase{"StaticQubits", MQT_NAMED_BUILDER(qc::staticQubits), - MQT_NAMED_BUILDER(qir::emptyQIR)}, + MQT_NAMED_BUILDER(qir::staticQubits)}, QCToQIRAdaptiveTestCase{"StaticQubitsWithOps", MQT_NAMED_BUILDER(qc::staticQubitsWithOps), MQT_NAMED_BUILDER(qir::staticQubitsWithOps)}, @@ -683,7 +687,7 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(qir::staticQubitsWithInv)}, QCToQIRAdaptiveTestCase{"AllocDeallocPair", MQT_NAMED_BUILDER(qc::allocDeallocPair), - MQT_NAMED_BUILDER(qir::emptyQIR)})); + MQT_NAMED_BUILDER(qir::emptyQIR<>)})); /// @} /// \name QCToQIRAdaptive/Operations/IfOp.cpp @@ -692,15 +696,15 @@ INSTANTIATE_TEST_SUITE_P( SCFIfOpTest, QCToQIRAdaptiveTest, testing::Values( QCToQIRAdaptiveTestCase{"SimpleIfOp", MQT_NAMED_BUILDER(qc::simpleIf), - MQT_NAMED_BUILDER(qir::simpleIf)}, + MQT_NAMED_BUILDER(qir::simpleIf<>)}, QCToQIRAdaptiveTestCase{"IfTwoQubits", MQT_NAMED_BUILDER(qc::ifTwoQubits), - MQT_NAMED_BUILDER(qir::ifTwoQubits)}, + MQT_NAMED_BUILDER(qir::ifTwoQubits<>)}, QCToQIRAdaptiveTestCase{"IfElse", MQT_NAMED_BUILDER(qc::ifElse), - MQT_NAMED_BUILDER(qir::ifElse)}, + MQT_NAMED_BUILDER(qir::ifElse<>)}, QCToQIRAdaptiveTestCase{"NestedIfOpForLoop", MQT_NAMED_BUILDER(qc::nestedIfOpForLoop), - MQT_NAMED_BUILDER(qir::nestedIfOpForLoop)})); + MQT_NAMED_BUILDER(qir::nestedIfOpForLoop<>)})); /// @} /// \name QCToQIRAdaptive/Operations/WhileOp.cpp @@ -710,10 +714,10 @@ INSTANTIATE_TEST_SUITE_P( testing::Values( QCToQIRAdaptiveTestCase{"SimpleWhile", MQT_NAMED_BUILDER(qc::simpleWhileReset), - MQT_NAMED_BUILDER(qir::simpleWhileReset)}, + MQT_NAMED_BUILDER(qir::simpleWhileReset<>)}, QCToQIRAdaptiveTestCase{"SimpleDoWhile", MQT_NAMED_BUILDER(qc::simpleDoWhileReset), - MQT_NAMED_BUILDER(qir::simpleDoWhileReset)})); + MQT_NAMED_BUILDER(qir::simpleDoWhileReset<>)})); /// \name QCToQIRAdaptive/Operations/ForOp.cpp /// @{ @@ -722,26 +726,28 @@ INSTANTIATE_TEST_SUITE_P( testing::Values( QCToQIRAdaptiveTestCase{"SimpleForLoop", MQT_NAMED_BUILDER(qc::simpleForLoop), - MQT_NAMED_BUILDER(qir::simpleForLoop)}, + MQT_NAMED_BUILDER(qir::simpleForLoop<>)}, QCToQIRAdaptiveTestCase{"NestedForLoopIfOp", MQT_NAMED_BUILDER(qc::nestedForLoopIfOp), - MQT_NAMED_BUILDER(qir::nestedForLoopIfOp)}, + MQT_NAMED_BUILDER(qir::nestedForLoopIfOp<>)}, QCToQIRAdaptiveTestCase{"NestedForLoopWhileOp", MQT_NAMED_BUILDER(qc::nestedForLoopWhileOp), - MQT_NAMED_BUILDER(qir::nestedForLoopWhileOp)}, + MQT_NAMED_BUILDER(qir::nestedForLoopWhileOp<>)}, QCToQIRAdaptiveTestCase{ "nestedForLoopCtrlOpWithSeparateQubit", MQT_NAMED_BUILDER(qc::nestedForLoopCtrlOpWithSeparateQubit), - MQT_NAMED_BUILDER(qir::nestedForLoopCtrlOpWithSeparateQubit)}, + MQT_NAMED_BUILDER( + qir::nestedForLoopCtrlOpWithSeparateQubit)}, QCToQIRAdaptiveTestCase{ "nestedForLoopCtrlOpWithExtractedQubit", MQT_NAMED_BUILDER(qc::nestedForLoopCtrlOpWithExtractedQubit), - MQT_NAMED_BUILDER(qir::nestedForLoopCtrlOpWithExtractedQubit)})); + MQT_NAMED_BUILDER( + qir::nestedForLoopCtrlOpWithExtractedQubit)})); /// \name QCToQIRAdaptive/Modifiers/CtrlOp.cpp /// @{ INSTANTIATE_TEST_SUITE_P(QCToQIRCtrlOpTest, QCToQIRAdaptiveTest, testing::Values(QCToQIRAdaptiveTestCase{ "NestedCtrlTwo", MQT_NAMED_BUILDER(qc::ctrlTwo), - MQT_NAMED_BUILDER(qir::ctrlTwo)})); + MQT_NAMED_BUILDER(qir::ctrlTwo<>)})); /// @} diff --git a/mlir/unittests/Conversion/QCToQIR/QCToQIRBase/test_qc_to_qir_base.cpp b/mlir/unittests/Conversion/QCToQIR/QCToQIRBase/test_qc_to_qir_base.cpp index b637247132..4319c3b4d3 100644 --- a/mlir/unittests/Conversion/QCToQIR/QCToQIRBase/test_qc_to_qir_base.cpp +++ b/mlir/unittests/Conversion/QCToQIR/QCToQIRBase/test_qc_to_qir_base.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include @@ -42,8 +43,8 @@ namespace { struct QCToQIRBaseTestCase { std::string name; - mqt::test::NamedBuilder programBuilder; - mqt::test::NamedBuilder referenceBuilder; + mqt::test::NamedMLIRBuilder programBuilder; + mqt::test::NamedMLIRBuilder referenceBuilder; friend std::ostream& operator<<(std::ostream& os, const QCToQIRBaseTestCase& info); @@ -85,7 +86,7 @@ TEST_P(QCToQIRBaseTest, ProgramEquivalence) { const auto name = " (" + GetParam().name + ")"; mqt::test::DeferredPrinter printer; - auto program = qc::QCProgramBuilder::build(context.get(), programBuilder.fn); + auto program = mqt::test::buildMLIRProgram(context.get(), programBuilder); ASSERT_TRUE(program); printer.record(program.get(), "Original QC IR" + name); EXPECT_TRUE(verify(*program).succeeded()); @@ -102,9 +103,8 @@ TEST_P(QCToQIRBaseTest, ProgramEquivalence) { printer.record(program.get(), "Canonicalized Converted QIR IR" + name); EXPECT_TRUE(verify(*program).succeeded()); - auto reference = - qir::QIRProgramBuilder::build(context.get(), referenceBuilder.fn, - qir::QIRProgramBuilder::Profile::Base); + auto reference = mqt::test::buildMLIRProgram( + context.get(), referenceBuilder, qir::QIRProgramBuilder::Profile::Base); ASSERT_TRUE(reference); printer.record(reference.get(), "Reference QIR IR" + name); EXPECT_TRUE(verify(*reference).succeeded()); @@ -123,16 +123,16 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRBaseBarrierOpTest, QCToQIRBaseTest, testing::Values( QCToQIRBaseTestCase{"Barrier", MQT_NAMED_BUILDER(qc::barrier), - MQT_NAMED_BUILDER(qir::emptyQIR)}, + MQT_NAMED_BUILDER(qir::alloc1QubitRegister<>)}, QCToQIRBaseTestCase{"BarrierTwoQubits", MQT_NAMED_BUILDER(qc::barrierTwoQubits), - MQT_NAMED_BUILDER(qir::emptyQIR)}, + MQT_NAMED_BUILDER(qir::allocQubitRegister<>)}, QCToQIRBaseTestCase{"BarrierMultipleQubits", MQT_NAMED_BUILDER(qc::barrierMultipleQubits), - MQT_NAMED_BUILDER(qir::emptyQIR)}, + MQT_NAMED_BUILDER(qir::alloc3QubitRegister<>)}, QCToQIRBaseTestCase{"SingleControlledBarrier", MQT_NAMED_BUILDER(qc::singleControlledBarrier), - MQT_NAMED_BUILDER(qir::emptyQIR)})); + MQT_NAMED_BUILDER(qir::allocQubitRegister<>)})); /// @} /// \name QCToQIRBase/Operations/StandardGates/DcxOp.cpp @@ -141,13 +141,13 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRBaseDCXOpTest, QCToQIRBaseTest, testing::Values( QCToQIRBaseTestCase{"DCX", MQT_NAMED_BUILDER(qc::dcx), - MQT_NAMED_BUILDER(qir::dcx)}, + MQT_NAMED_BUILDER(qir::dcx<>)}, QCToQIRBaseTestCase{"SingleControlledDCX", MQT_NAMED_BUILDER(qc::singleControlledDcx), - MQT_NAMED_BUILDER(qir::singleControlledDcx)}, + MQT_NAMED_BUILDER(qir::singleControlledDcx<>)}, QCToQIRBaseTestCase{"MultipleControlledDCX", MQT_NAMED_BUILDER(qc::multipleControlledDcx), - MQT_NAMED_BUILDER(qir::multipleControlledDcx)})); + MQT_NAMED_BUILDER(qir::multipleControlledDcx<>)})); /// @} /// \name QCToQIRBase/Operations/StandardGates/EcrOp.cpp @@ -156,13 +156,13 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRBaseECROpTest, QCToQIRBaseTest, testing::Values( QCToQIRBaseTestCase{"ECR", MQT_NAMED_BUILDER(qc::ecr), - MQT_NAMED_BUILDER(qir::ecr)}, + MQT_NAMED_BUILDER(qir::ecr<>)}, QCToQIRBaseTestCase{"SingleControlledECR", MQT_NAMED_BUILDER(qc::singleControlledEcr), - MQT_NAMED_BUILDER(qir::singleControlledEcr)}, + MQT_NAMED_BUILDER(qir::singleControlledEcr<>)}, QCToQIRBaseTestCase{"MultipleControlledECR", MQT_NAMED_BUILDER(qc::multipleControlledEcr), - MQT_NAMED_BUILDER(qir::multipleControlledEcr)})); + MQT_NAMED_BUILDER(qir::multipleControlledEcr<>)})); /// @} /// \name QCToQIRBase/Operations/StandardGates/GphaseOp.cpp @@ -170,7 +170,7 @@ INSTANTIATE_TEST_SUITE_P( INSTANTIATE_TEST_SUITE_P(QCToQIRBaseGPhaseOpTest, QCToQIRBaseTest, testing::Values(QCToQIRBaseTestCase{ "GlobalPhase", MQT_NAMED_BUILDER(qc::globalPhase), - MQT_NAMED_BUILDER(qir::globalPhase)})); + MQT_NAMED_BUILDER(qir::globalPhase<>)})); /// @} /// \name QCToQIRBase/Operations/StandardGates/HOp.cpp @@ -179,16 +179,16 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRBaseHOpTest, QCToQIRBaseTest, testing::Values( QCToQIRBaseTestCase{"H", MQT_NAMED_BUILDER(qc::h), - MQT_NAMED_BUILDER(qir::h)}, + MQT_NAMED_BUILDER(qir::h<>)}, QCToQIRBaseTestCase{"SingleControlledH", MQT_NAMED_BUILDER(qc::singleControlledH), - MQT_NAMED_BUILDER(qir::singleControlledH)}, + MQT_NAMED_BUILDER(qir::singleControlledH<>)}, QCToQIRBaseTestCase{"MultipleControlledH", MQT_NAMED_BUILDER(qc::multipleControlledH), - MQT_NAMED_BUILDER(qir::multipleControlledH)}, + MQT_NAMED_BUILDER(qir::multipleControlledH<>)}, QCToQIRBaseTestCase{"HWithoutRegister", MQT_NAMED_BUILDER(qc::hWithoutRegister), - MQT_NAMED_BUILDER(qir::hWithoutRegister)})); + MQT_NAMED_BUILDER(qir::hWithoutRegister<>)})); /// @} /// \name QCToQIRBase/Operations/StandardGates/IdOp.cpp @@ -197,28 +197,29 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRBaseIDOpTest, QCToQIRBaseTest, testing::Values( QCToQIRBaseTestCase{"Identity", MQT_NAMED_BUILDER(qc::identity), - MQT_NAMED_BUILDER(qir::identity)}, + MQT_NAMED_BUILDER(qir::identity<>)}, QCToQIRBaseTestCase{"SingleControlledIdentity", MQT_NAMED_BUILDER(qc::singleControlledIdentity), - MQT_NAMED_BUILDER(qir::identity)}, + MQT_NAMED_BUILDER(qir::twoQubitsOneIdentity<>)}, QCToQIRBaseTestCase{"MultipleControlledIdentity", MQT_NAMED_BUILDER(qc::multipleControlledIdentity), - MQT_NAMED_BUILDER(qir::identity)})); + MQT_NAMED_BUILDER(qir::threeQubitsOneIdentity<>)})); /// @} /// \name QCToQIRBase/Operations/StandardGates/IswapOp.cpp /// @{ INSTANTIATE_TEST_SUITE_P( QCToQIRBaseiSWAPOpTest, QCToQIRBaseTest, - testing::Values( - QCToQIRBaseTestCase{"iSWAP", MQT_NAMED_BUILDER(qc::iswap), - MQT_NAMED_BUILDER(qir::iswap)}, - QCToQIRBaseTestCase{"SingleControllediSWAP", - MQT_NAMED_BUILDER(qc::singleControlledIswap), - MQT_NAMED_BUILDER(qir::singleControlledIswap)}, - QCToQIRBaseTestCase{"MultipleControllediSWAP", - MQT_NAMED_BUILDER(qc::multipleControlledIswap), - MQT_NAMED_BUILDER(qir::multipleControlledIswap)})); + testing::Values(QCToQIRBaseTestCase{"iSWAP", MQT_NAMED_BUILDER(qc::iswap), + MQT_NAMED_BUILDER(qir::iswap<>)}, + QCToQIRBaseTestCase{ + "SingleControllediSWAP", + MQT_NAMED_BUILDER(qc::singleControlledIswap), + MQT_NAMED_BUILDER(qir::singleControlledIswap<>)}, + QCToQIRBaseTestCase{ + "MultipleControllediSWAP", + MQT_NAMED_BUILDER(qc::multipleControlledIswap), + MQT_NAMED_BUILDER(qir::multipleControlledIswap<>)})); /// @} /// \name QCToQIRBase/Operations/StandardGates/POp.cpp @@ -227,13 +228,13 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRBasePOpTest, QCToQIRBaseTest, testing::Values( QCToQIRBaseTestCase{"P", MQT_NAMED_BUILDER(qc::p), - MQT_NAMED_BUILDER(qir::p)}, + MQT_NAMED_BUILDER(qir::p<>)}, QCToQIRBaseTestCase{"SingleControlledP", MQT_NAMED_BUILDER(qc::singleControlledP), - MQT_NAMED_BUILDER(qir::singleControlledP)}, + MQT_NAMED_BUILDER(qir::singleControlledP<>)}, QCToQIRBaseTestCase{"MultipleControlledP", MQT_NAMED_BUILDER(qc::multipleControlledP), - MQT_NAMED_BUILDER(qir::multipleControlledP)})); + MQT_NAMED_BUILDER(qir::multipleControlledP<>)})); /// @} /// \name QCToQIRBase/Operations/StandardGates/ROp.cpp @@ -242,13 +243,13 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRBaseROpTest, QCToQIRBaseTest, testing::Values( QCToQIRBaseTestCase{"R", MQT_NAMED_BUILDER(qc::r), - MQT_NAMED_BUILDER(qir::r)}, + MQT_NAMED_BUILDER(qir::r<>)}, QCToQIRBaseTestCase{"SingleControlledR", MQT_NAMED_BUILDER(qc::singleControlledR), - MQT_NAMED_BUILDER(qir::singleControlledR)}, + MQT_NAMED_BUILDER(qir::singleControlledR<>)}, QCToQIRBaseTestCase{"MultipleControlledR", MQT_NAMED_BUILDER(qc::multipleControlledR), - MQT_NAMED_BUILDER(qir::multipleControlledR)})); + MQT_NAMED_BUILDER(qir::multipleControlledR<>)})); /// @} /// \name QCToQIRBase/Operations/StandardGates/RxOp.cpp @@ -257,13 +258,13 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRBaseRXOpTest, QCToQIRBaseTest, testing::Values( QCToQIRBaseTestCase{"RX", MQT_NAMED_BUILDER(qc::rx), - MQT_NAMED_BUILDER(qir::rx)}, + MQT_NAMED_BUILDER(qir::rx<>)}, QCToQIRBaseTestCase{"SingleControlledRX", MQT_NAMED_BUILDER(qc::singleControlledRx), - MQT_NAMED_BUILDER(qir::singleControlledRx)}, + MQT_NAMED_BUILDER(qir::singleControlledRx<>)}, QCToQIRBaseTestCase{"MultipleControlledRX", MQT_NAMED_BUILDER(qc::multipleControlledRx), - MQT_NAMED_BUILDER(qir::multipleControlledRx)})); + MQT_NAMED_BUILDER(qir::multipleControlledRx<>)})); /// @} /// \name QCToQIRBase/Operations/StandardGates/RxxOp.cpp @@ -272,13 +273,13 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRBaseRXXOpTest, QCToQIRBaseTest, testing::Values( QCToQIRBaseTestCase{"RXX", MQT_NAMED_BUILDER(qc::rxx), - MQT_NAMED_BUILDER(qir::rxx)}, + MQT_NAMED_BUILDER(qir::rxx<>)}, QCToQIRBaseTestCase{"SingleControlledRXX", MQT_NAMED_BUILDER(qc::singleControlledRxx), - MQT_NAMED_BUILDER(qir::singleControlledRxx)}, + MQT_NAMED_BUILDER(qir::singleControlledRxx<>)}, QCToQIRBaseTestCase{"MultipleControlledRXX", MQT_NAMED_BUILDER(qc::multipleControlledRxx), - MQT_NAMED_BUILDER(qir::multipleControlledRxx)})); + MQT_NAMED_BUILDER(qir::multipleControlledRxx<>)})); /// @} /// \name QCToQIRBase/Operations/StandardGates/RyOp.cpp @@ -287,13 +288,13 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRBaseRYOpTest, QCToQIRBaseTest, testing::Values( QCToQIRBaseTestCase{"RY", MQT_NAMED_BUILDER(qc::ry), - MQT_NAMED_BUILDER(qir::ry)}, + MQT_NAMED_BUILDER(qir::ry<>)}, QCToQIRBaseTestCase{"SingleControlledRY", MQT_NAMED_BUILDER(qc::singleControlledRy), - MQT_NAMED_BUILDER(qir::singleControlledRy)}, + MQT_NAMED_BUILDER(qir::singleControlledRy<>)}, QCToQIRBaseTestCase{"MultipleControlledRY", MQT_NAMED_BUILDER(qc::multipleControlledRy), - MQT_NAMED_BUILDER(qir::multipleControlledRy)})); + MQT_NAMED_BUILDER(qir::multipleControlledRy<>)})); /// @} /// \name QCToQIRBase/Operations/StandardGates/RyyOp.cpp @@ -302,13 +303,13 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRBaseRYYOpTest, QCToQIRBaseTest, testing::Values( QCToQIRBaseTestCase{"RYY", MQT_NAMED_BUILDER(qc::ryy), - MQT_NAMED_BUILDER(qir::ryy)}, + MQT_NAMED_BUILDER(qir::ryy<>)}, QCToQIRBaseTestCase{"SingleControlledRYY", MQT_NAMED_BUILDER(qc::singleControlledRyy), - MQT_NAMED_BUILDER(qir::singleControlledRyy)}, + MQT_NAMED_BUILDER(qir::singleControlledRyy<>)}, QCToQIRBaseTestCase{"MultipleControlledRYY", MQT_NAMED_BUILDER(qc::multipleControlledRyy), - MQT_NAMED_BUILDER(qir::multipleControlledRyy)})); + MQT_NAMED_BUILDER(qir::multipleControlledRyy<>)})); /// @} /// \name QCToQIRBase/Operations/StandardGates/RzOp.cpp @@ -317,13 +318,13 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRBaseRZOpTest, QCToQIRBaseTest, testing::Values( QCToQIRBaseTestCase{"RZ", MQT_NAMED_BUILDER(qc::rz), - MQT_NAMED_BUILDER(qir::rz)}, + MQT_NAMED_BUILDER(qir::rz<>)}, QCToQIRBaseTestCase{"SingleControlledRZ", MQT_NAMED_BUILDER(qc::singleControlledRz), - MQT_NAMED_BUILDER(qir::singleControlledRz)}, + MQT_NAMED_BUILDER(qir::singleControlledRz<>)}, QCToQIRBaseTestCase{"MultipleControlledRZ", MQT_NAMED_BUILDER(qc::multipleControlledRz), - MQT_NAMED_BUILDER(qir::multipleControlledRz)})); + MQT_NAMED_BUILDER(qir::multipleControlledRz<>)})); /// @} /// \name QCToQIRBase/Operations/StandardGates/RzxOp.cpp @@ -332,13 +333,13 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRBaseRZXOpTest, QCToQIRBaseTest, testing::Values( QCToQIRBaseTestCase{"RZX", MQT_NAMED_BUILDER(qc::rzx), - MQT_NAMED_BUILDER(qir::rzx)}, + MQT_NAMED_BUILDER(qir::rzx<>)}, QCToQIRBaseTestCase{"SingleControlledRZX", MQT_NAMED_BUILDER(qc::singleControlledRzx), - MQT_NAMED_BUILDER(qir::singleControlledRzx)}, + MQT_NAMED_BUILDER(qir::singleControlledRzx<>)}, QCToQIRBaseTestCase{"MultipleControlledRZX", MQT_NAMED_BUILDER(qc::multipleControlledRzx), - MQT_NAMED_BUILDER(qir::multipleControlledRzx)})); + MQT_NAMED_BUILDER(qir::multipleControlledRzx<>)})); /// @} /// \name QCToQIRBase/Operations/StandardGates/RzzOp.cpp @@ -347,13 +348,13 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRBaseRZZOpTest, QCToQIRBaseTest, testing::Values( QCToQIRBaseTestCase{"RZZ", MQT_NAMED_BUILDER(qc::rzz), - MQT_NAMED_BUILDER(qir::rzz)}, + MQT_NAMED_BUILDER(qir::rzz<>)}, QCToQIRBaseTestCase{"SingleControlledRZZ", MQT_NAMED_BUILDER(qc::singleControlledRzz), - MQT_NAMED_BUILDER(qir::singleControlledRzz)}, + MQT_NAMED_BUILDER(qir::singleControlledRzz<>)}, QCToQIRBaseTestCase{"MultipleControlledRZZ", MQT_NAMED_BUILDER(qc::multipleControlledRzz), - MQT_NAMED_BUILDER(qir::multipleControlledRzz)})); + MQT_NAMED_BUILDER(qir::multipleControlledRzz<>)})); /// @} /// \name QCToQIRBase/Operations/StandardGates/SOp.cpp @@ -362,13 +363,13 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRBaseSOpTest, QCToQIRBaseTest, testing::Values( QCToQIRBaseTestCase{"S", MQT_NAMED_BUILDER(qc::s), - MQT_NAMED_BUILDER(qir::s)}, + MQT_NAMED_BUILDER(qir::s<>)}, QCToQIRBaseTestCase{"SingleControlledS", MQT_NAMED_BUILDER(qc::singleControlledS), - MQT_NAMED_BUILDER(qir::singleControlledS)}, + MQT_NAMED_BUILDER(qir::singleControlledS<>)}, QCToQIRBaseTestCase{"MultipleControlledS", MQT_NAMED_BUILDER(qc::multipleControlledS), - MQT_NAMED_BUILDER(qir::multipleControlledS)})); + MQT_NAMED_BUILDER(qir::multipleControlledS<>)})); /// @} /// \name QCToQIRBase/Operations/StandardGates/SdgOp.cpp @@ -377,13 +378,13 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRBaseSdgOpTest, QCToQIRBaseTest, testing::Values( QCToQIRBaseTestCase{"Sdg", MQT_NAMED_BUILDER(qc::sdg), - MQT_NAMED_BUILDER(qir::sdg)}, + MQT_NAMED_BUILDER(qir::sdg<>)}, QCToQIRBaseTestCase{"SingleControlledSdg", MQT_NAMED_BUILDER(qc::singleControlledSdg), - MQT_NAMED_BUILDER(qir::singleControlledSdg)}, + MQT_NAMED_BUILDER(qir::singleControlledSdg<>)}, QCToQIRBaseTestCase{"MultipleControlledSdg", MQT_NAMED_BUILDER(qc::multipleControlledSdg), - MQT_NAMED_BUILDER(qir::multipleControlledSdg)})); + MQT_NAMED_BUILDER(qir::multipleControlledSdg<>)})); /// @} /// \name QCToQIRBase/Operations/StandardGates/SwapOp.cpp @@ -392,13 +393,13 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRBaseSWAPOpTest, QCToQIRBaseTest, testing::Values( QCToQIRBaseTestCase{"SWAP", MQT_NAMED_BUILDER(qc::swap), - MQT_NAMED_BUILDER(qir::swap)}, + MQT_NAMED_BUILDER(qir::swap<>)}, QCToQIRBaseTestCase{"SingleControlledSWAP", MQT_NAMED_BUILDER(qc::singleControlledSwap), - MQT_NAMED_BUILDER(qir::singleControlledSwap)}, + MQT_NAMED_BUILDER(qir::singleControlledSwap<>)}, QCToQIRBaseTestCase{"MultipleControlledSWAP", MQT_NAMED_BUILDER(qc::multipleControlledSwap), - MQT_NAMED_BUILDER(qir::multipleControlledSwap)})); + MQT_NAMED_BUILDER(qir::multipleControlledSwap<>)})); /// @} /// \name QCToQIRBase/Operations/StandardGates/SxOp.cpp @@ -407,13 +408,13 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRBaseSXOpTest, QCToQIRBaseTest, testing::Values( QCToQIRBaseTestCase{"SX", MQT_NAMED_BUILDER(qc::sx), - MQT_NAMED_BUILDER(qir::sx)}, + MQT_NAMED_BUILDER(qir::sx<>)}, QCToQIRBaseTestCase{"SingleControlledSX", MQT_NAMED_BUILDER(qc::singleControlledSx), - MQT_NAMED_BUILDER(qir::singleControlledSx)}, + MQT_NAMED_BUILDER(qir::singleControlledSx<>)}, QCToQIRBaseTestCase{"MultipleControlledSX", MQT_NAMED_BUILDER(qc::multipleControlledSx), - MQT_NAMED_BUILDER(qir::multipleControlledSx)})); + MQT_NAMED_BUILDER(qir::multipleControlledSx<>)})); /// @} /// \name QCToQIRBase/Operations/StandardGates/SxdgOp.cpp @@ -422,13 +423,13 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRBaseSXdgOpTest, QCToQIRBaseTest, testing::Values( QCToQIRBaseTestCase{"SXdg", MQT_NAMED_BUILDER(qc::sxdg), - MQT_NAMED_BUILDER(qir::sxdg)}, + MQT_NAMED_BUILDER(qir::sxdg<>)}, QCToQIRBaseTestCase{"SingleControlledSXdg", MQT_NAMED_BUILDER(qc::singleControlledSxdg), - MQT_NAMED_BUILDER(qir::singleControlledSxdg)}, + MQT_NAMED_BUILDER(qir::singleControlledSxdg<>)}, QCToQIRBaseTestCase{"MultipleControlledSXdg", MQT_NAMED_BUILDER(qc::multipleControlledSxdg), - MQT_NAMED_BUILDER(qir::multipleControlledSxdg)})); + MQT_NAMED_BUILDER(qir::multipleControlledSxdg<>)})); /// @} /// \name QCToQIRBase/Operations/StandardGates/TOp.cpp @@ -437,13 +438,13 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRBaseTOpTest, QCToQIRBaseTest, testing::Values( QCToQIRBaseTestCase{"T", MQT_NAMED_BUILDER(qc::t_), - MQT_NAMED_BUILDER(qir::t_)}, + MQT_NAMED_BUILDER(qir::t_<>)}, QCToQIRBaseTestCase{"SingleControlledT", MQT_NAMED_BUILDER(qc::singleControlledT), - MQT_NAMED_BUILDER(qir::singleControlledT)}, + MQT_NAMED_BUILDER(qir::singleControlledT<>)}, QCToQIRBaseTestCase{"MultipleControlledT", MQT_NAMED_BUILDER(qc::multipleControlledT), - MQT_NAMED_BUILDER(qir::multipleControlledT)})); + MQT_NAMED_BUILDER(qir::multipleControlledT<>)})); /// @} /// \name QCToQIRBase/Operations/StandardGates/TdgOp.cpp @@ -452,13 +453,13 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRBaseTdgOpTest, QCToQIRBaseTest, testing::Values( QCToQIRBaseTestCase{"Tdg", MQT_NAMED_BUILDER(qc::tdg), - MQT_NAMED_BUILDER(qir::tdg)}, + MQT_NAMED_BUILDER(qir::tdg<>)}, QCToQIRBaseTestCase{"SingleControlledTdg", MQT_NAMED_BUILDER(qc::singleControlledTdg), - MQT_NAMED_BUILDER(qir::singleControlledTdg)}, + MQT_NAMED_BUILDER(qir::singleControlledTdg<>)}, QCToQIRBaseTestCase{"MultipleControlledTdg", MQT_NAMED_BUILDER(qc::multipleControlledTdg), - MQT_NAMED_BUILDER(qir::multipleControlledTdg)})); + MQT_NAMED_BUILDER(qir::multipleControlledTdg<>)})); /// @} /// \name QCToQIRBase/Operations/StandardGates/U2Op.cpp @@ -467,13 +468,13 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRBaseU2OpTest, QCToQIRBaseTest, testing::Values( QCToQIRBaseTestCase{"U2", MQT_NAMED_BUILDER(qc::u2), - MQT_NAMED_BUILDER(qir::u2)}, + MQT_NAMED_BUILDER(qir::u2<>)}, QCToQIRBaseTestCase{"SingleControlledU2", MQT_NAMED_BUILDER(qc::singleControlledU2), - MQT_NAMED_BUILDER(qir::singleControlledU2)}, + MQT_NAMED_BUILDER(qir::singleControlledU2<>)}, QCToQIRBaseTestCase{"MultipleControlledU2", MQT_NAMED_BUILDER(qc::multipleControlledU2), - MQT_NAMED_BUILDER(qir::multipleControlledU2)})); + MQT_NAMED_BUILDER(qir::multipleControlledU2<>)})); /// @} /// \name QCToQIRBase/Operations/StandardGates/UOp.cpp @@ -482,13 +483,13 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRBaseUOpTest, QCToQIRBaseTest, testing::Values( QCToQIRBaseTestCase{"U", MQT_NAMED_BUILDER(qc::u), - MQT_NAMED_BUILDER(qir::u)}, + MQT_NAMED_BUILDER(qir::u<>)}, QCToQIRBaseTestCase{"SingleControlledU", MQT_NAMED_BUILDER(qc::singleControlledU), - MQT_NAMED_BUILDER(qir::singleControlledU)}, + MQT_NAMED_BUILDER(qir::singleControlledU<>)}, QCToQIRBaseTestCase{"MultipleControlledU", MQT_NAMED_BUILDER(qc::multipleControlledU), - MQT_NAMED_BUILDER(qir::multipleControlledU)})); + MQT_NAMED_BUILDER(qir::multipleControlledU<>)})); /// @} /// \name QCToQIRBase/Operations/StandardGates/XOp.cpp @@ -497,13 +498,13 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRBaseXOpTest, QCToQIRBaseTest, testing::Values( QCToQIRBaseTestCase{"X", MQT_NAMED_BUILDER(qc::x), - MQT_NAMED_BUILDER(qir::x)}, + MQT_NAMED_BUILDER(qir::x<>)}, QCToQIRBaseTestCase{"SingleControlledX", MQT_NAMED_BUILDER(qc::singleControlledX), - MQT_NAMED_BUILDER(qir::singleControlledX)}, + MQT_NAMED_BUILDER(qir::singleControlledX<>)}, QCToQIRBaseTestCase{"MultipleControlledX", MQT_NAMED_BUILDER(qc::multipleControlledX), - MQT_NAMED_BUILDER(qir::multipleControlledX)})); + MQT_NAMED_BUILDER(qir::multipleControlledX<>)})); /// @} /// \name QCToQIRBase/Operations/StandardGates/XxMinusYyOp.cpp @@ -512,14 +513,15 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRBaseXXMinusYYOpTest, QCToQIRBaseTest, testing::Values( QCToQIRBaseTestCase{"XXMinusYY", MQT_NAMED_BUILDER(qc::xxMinusYY), - MQT_NAMED_BUILDER(qir::xxMinusYY)}, - QCToQIRBaseTestCase{"SingleControlledXXMinusYY", - MQT_NAMED_BUILDER(qc::singleControlledXxMinusYY), - MQT_NAMED_BUILDER(qir::singleControlledXxMinusYY)}, + MQT_NAMED_BUILDER(qir::xxMinusYY<>)}, + QCToQIRBaseTestCase{ + "SingleControlledXXMinusYY", + MQT_NAMED_BUILDER(qc::singleControlledXxMinusYY), + MQT_NAMED_BUILDER(qir::singleControlledXxMinusYY<>)}, QCToQIRBaseTestCase{ "MultipleControlledXXMinusYY", MQT_NAMED_BUILDER(qc::multipleControlledXxMinusYY), - MQT_NAMED_BUILDER(qir::multipleControlledXxMinusYY)})); + MQT_NAMED_BUILDER(qir::multipleControlledXxMinusYY<>)})); /// @} /// \name QCToQIRBase/Operations/StandardGates/XxPlusYyOp.cpp @@ -528,14 +530,14 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRBaseXXPlusYYOpTest, QCToQIRBaseTest, testing::Values( QCToQIRBaseTestCase{"XXPlusYY", MQT_NAMED_BUILDER(qc::xxPlusYY), - MQT_NAMED_BUILDER(qir::xxPlusYY)}, + MQT_NAMED_BUILDER(qir::xxPlusYY<>)}, QCToQIRBaseTestCase{"SingleControlledXXPlusYY", MQT_NAMED_BUILDER(qc::singleControlledXxPlusYY), - MQT_NAMED_BUILDER(qir::singleControlledXxPlusYY)}, + MQT_NAMED_BUILDER(qir::singleControlledXxPlusYY<>)}, QCToQIRBaseTestCase{ "MultipleControlledXXPlusYY", MQT_NAMED_BUILDER(qc::multipleControlledXxPlusYY), - MQT_NAMED_BUILDER(qir::multipleControlledXxPlusYY)})); + MQT_NAMED_BUILDER(qir::multipleControlledXxPlusYY<>)})); /// @} /// \name QCToQIRBase/Operations/StandardGates/YOp.cpp @@ -544,13 +546,13 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRBaseYOpTest, QCToQIRBaseTest, testing::Values( QCToQIRBaseTestCase{"Y", MQT_NAMED_BUILDER(qc::y), - MQT_NAMED_BUILDER(qir::y)}, + MQT_NAMED_BUILDER(qir::y<>)}, QCToQIRBaseTestCase{"SingleControlledY", MQT_NAMED_BUILDER(qc::singleControlledY), - MQT_NAMED_BUILDER(qir::singleControlledY)}, + MQT_NAMED_BUILDER(qir::singleControlledY<>)}, QCToQIRBaseTestCase{"MultipleControlledY", MQT_NAMED_BUILDER(qc::multipleControlledY), - MQT_NAMED_BUILDER(qir::multipleControlledY)})); + MQT_NAMED_BUILDER(qir::multipleControlledY<>)})); /// @} /// \name QCToQIRBase/Operations/StandardGates/ZOp.cpp @@ -559,13 +561,13 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRBaseZOpTest, QCToQIRBaseTest, testing::Values( QCToQIRBaseTestCase{"Z", MQT_NAMED_BUILDER(qc::z), - MQT_NAMED_BUILDER(qir::z)}, + MQT_NAMED_BUILDER(qir::z<>)}, QCToQIRBaseTestCase{"SingleControlledZ", MQT_NAMED_BUILDER(qc::singleControlledZ), - MQT_NAMED_BUILDER(qir::singleControlledZ)}, + MQT_NAMED_BUILDER(qir::singleControlledZ<>)}, QCToQIRBaseTestCase{"MultipleControlledZ", MQT_NAMED_BUILDER(qc::multipleControlledZ), - MQT_NAMED_BUILDER(qir::multipleControlledZ)})); + MQT_NAMED_BUILDER(qir::multipleControlledZ<>)})); /// @} /// \name QCToQIRBase/Operations/MeasureOp.cpp @@ -576,23 +578,24 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRBaseTestCase{ "SingleMeasurementToSingleBit", MQT_NAMED_BUILDER(qc::singleMeasurementToSingleBit), - MQT_NAMED_BUILDER(qir::singleMeasurementToSingleBit)}, + MQT_NAMED_BUILDER(qir::singleMeasurementToSingleBit<>)}, QCToQIRBaseTestCase{ "RepeatedMeasurementToSameBit", MQT_NAMED_BUILDER(qc::repeatedMeasurementToSameBit), - MQT_NAMED_BUILDER(qir::repeatedMeasurementToSameBit)}, + MQT_NAMED_BUILDER(qir::repeatedMeasurementToSameBit<>)}, QCToQIRBaseTestCase{ "RepeatedMeasurementToDifferentBits", MQT_NAMED_BUILDER(qc::repeatedMeasurementToDifferentBits), - MQT_NAMED_BUILDER(qir::repeatedMeasurementToDifferentBits)}, + MQT_NAMED_BUILDER(qir::repeatedMeasurementToDifferentBits<>)}, QCToQIRBaseTestCase{ "MultipleClassicalRegistersAndMeasurements", MQT_NAMED_BUILDER(qc::multipleClassicalRegistersAndMeasurements), - MQT_NAMED_BUILDER(qir::multipleClassicalRegistersAndMeasurements)}, + MQT_NAMED_BUILDER( + qir::multipleClassicalRegistersAndMeasurements)}, QCToQIRBaseTestCase{ "MeasurementWithoutRegisters", MQT_NAMED_BUILDER(qc::measurementWithoutRegisters), - MQT_NAMED_BUILDER(qir::measurementWithoutRegisters)})); + MQT_NAMED_BUILDER(qir::measurementWithoutRegisters<>)})); /// @} /// \name QCToQIRBase/QubitManagement/QubitManagement.cpp @@ -601,22 +604,23 @@ INSTANTIATE_TEST_SUITE_P( QCToQIRBaseQubitManagementTest, QCToQIRBaseTest, testing::Values( QCToQIRBaseTestCase{"AllocQubit", MQT_NAMED_BUILDER(qc::allocQubit), - MQT_NAMED_BUILDER(qir::emptyQIR)}, + MQT_NAMED_BUILDER(qir::allocQubit<>)}, QCToQIRBaseTestCase{"AllocQubitRegister", MQT_NAMED_BUILDER(qc::allocQubitRegister), - MQT_NAMED_BUILDER(qir::emptyQIR)}, - QCToQIRBaseTestCase{"AllocMultipleQubitRegisters", - MQT_NAMED_BUILDER(qc::allocMultipleQubitRegisters), - MQT_NAMED_BUILDER(qir::emptyQIR)}, + MQT_NAMED_BUILDER(qir::allocQubitRegister<>)}, + QCToQIRBaseTestCase{ + "AllocMultipleQubitRegisters", + MQT_NAMED_BUILDER(qc::allocMultipleQubitRegisters), + MQT_NAMED_BUILDER(qir::allocMultipleQubitRegisters<>)}, QCToQIRBaseTestCase{ "AllocMultipleQubitRegistersWithOps", MQT_NAMED_BUILDER(qc::allocMultipleQubitRegistersWithOps), - MQT_NAMED_BUILDER(qir::allocMultipleQubitRegistersWithOps)}, + MQT_NAMED_BUILDER(qir::allocMultipleQubitRegistersWithOps<>)}, QCToQIRBaseTestCase{"AllocLargeRegister", MQT_NAMED_BUILDER(qc::allocLargeRegister), - MQT_NAMED_BUILDER(qir::emptyQIR)}, + MQT_NAMED_BUILDER(qir::allocQubitRegister<>)}, QCToQIRBaseTestCase{"StaticQubits", MQT_NAMED_BUILDER(qc::staticQubits), - MQT_NAMED_BUILDER(qir::emptyQIR)}, + MQT_NAMED_BUILDER(qir::staticQubits)}, QCToQIRBaseTestCase{"StaticQubitsWithOps", MQT_NAMED_BUILDER(qc::staticQubitsWithOps), MQT_NAMED_BUILDER(qir::staticQubitsWithOps)}, @@ -636,5 +640,5 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(qir::staticQubitsWithInv)}, QCToQIRBaseTestCase{"AllocDeallocPair", MQT_NAMED_BUILDER(qc::allocDeallocPair), - MQT_NAMED_BUILDER(qir::emptyQIR)})); + MQT_NAMED_BUILDER(qir::emptyQIR<>)})); /// @} diff --git a/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp b/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp index 4d0f56912b..08d56935ed 100644 --- a/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp +++ b/mlir/unittests/Dialect/QC/IR/test_qc_ir.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -35,8 +36,8 @@ namespace { struct QCTestCase { std::string name; - mqt::test::NamedBuilder programBuilder; - mqt::test::NamedBuilder referenceBuilder; + mqt::test::NamedMLIRBuilder programBuilder; + mqt::test::NamedMLIRBuilder referenceBuilder; friend std::ostream& operator<<(std::ostream& os, const QCTestCase& info); }; @@ -73,7 +74,7 @@ TEST_P(QCTest, ProgramEquivalence) { const auto name = " (" + GetParam().name + ")"; mqt::test::DeferredPrinter printer; - auto program = QCProgramBuilder::build(context.get(), programBuilder.fn); + auto program = mqt::test::buildMLIRProgram(context.get(), programBuilder); ASSERT_TRUE(program); printer.record(program.get(), "Original QC IR" + name); EXPECT_TRUE(verify(*program).succeeded()); @@ -82,7 +83,7 @@ TEST_P(QCTest, ProgramEquivalence) { printer.record(program.get(), "Canonicalized QC IR" + name); EXPECT_TRUE(verify(*program).succeeded()); - auto reference = QCProgramBuilder::build(context.get(), referenceBuilder.fn); + auto reference = mqt::test::buildMLIRProgram(context.get(), referenceBuilder); ASSERT_TRUE(reference); printer.record(reference.get(), "Reference QC IR" + name); EXPECT_TRUE(verify(*reference).succeeded()); @@ -148,7 +149,9 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(rxx)}, QCTestCase{"InvControlSandwich", MQT_NAMED_BUILDER(invCtrlSandwich), - MQT_NAMED_BUILDER(singleControlledRxx)})); + MQT_NAMED_BUILDER(singleControlledRxx)}, + QCTestCase{"InverseT", MQT_NAMED_BUILDER(inverseT), + MQT_NAMED_BUILDER(tdg)})); /// @} /// \name QC/Operations/MeasureOp.cpp @@ -210,7 +213,7 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(barrierMultipleQubits)}, QCTestCase{"SingleControlledBarrier", MQT_NAMED_BUILDER(singleControlledBarrier), - MQT_NAMED_BUILDER(barrier)}, + MQT_NAMED_BUILDER(twoQubitsOneBarrier)}, QCTestCase{"InverseBarrier", MQT_NAMED_BUILDER(inverseBarrier), MQT_NAMED_BUILDER(barrier)})); @@ -284,7 +287,7 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(singleControlledP)}, QCTestCase{"TrivialControlledGlobalPhase", MQT_NAMED_BUILDER(trivialControlledGlobalPhase), - MQT_NAMED_BUILDER(globalPhase)}, + MQT_NAMED_BUILDER(globalPhaseAndMeasure)}, QCTestCase{"InverseGlobalPhase", MQT_NAMED_BUILDER(inverseGlobalPhase), MQT_NAMED_BUILDER(globalPhase)}, QCTestCase{"InverseMultipleControlledGlobalPhase", @@ -323,13 +326,13 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(identity)}, QCTestCase{"SingleControlledIdentity", MQT_NAMED_BUILDER(singleControlledIdentity), - MQT_NAMED_BUILDER(identity)}, + MQT_NAMED_BUILDER(twoQubitsOneIdentity)}, QCTestCase{"MultipleControlledIdentity", MQT_NAMED_BUILDER(multipleControlledIdentity), - MQT_NAMED_BUILDER(identity)}, + MQT_NAMED_BUILDER(threeQubitsOneIdentity)}, QCTestCase{"NestedControlledIdentity", MQT_NAMED_BUILDER(nestedControlledIdentity), - MQT_NAMED_BUILDER(identity)}, + MQT_NAMED_BUILDER(threeQubitsOneIdentity)}, QCTestCase{"TrivialControlledIdentity", MQT_NAMED_BUILDER(trivialControlledIdentity), MQT_NAMED_BUILDER(identity)}, @@ -337,7 +340,7 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(identity)}, QCTestCase{"InverseMultipleControlledIdentity", MQT_NAMED_BUILDER(inverseMultipleControlledIdentity), - MQT_NAMED_BUILDER(identity)})); + MQT_NAMED_BUILDER(threeQubitsOneIdentity)})); /// @} /// \name QC/Operations/StandardGates/IswapOp.cpp @@ -913,16 +916,11 @@ INSTANTIATE_TEST_SUITE_P( INSTANTIATE_TEST_SUITE_P( QCQubitManagementTest, QCTest, testing::Values( - QCTestCase{"AllocQubit", MQT_NAMED_BUILDER(allocQubit), - MQT_NAMED_BUILDER(emptyQC)}, - QCTestCase{"AllocQubitRegister", MQT_NAMED_BUILDER(allocQubitRegister), - MQT_NAMED_BUILDER(emptyQC)}, - QCTestCase{"AllocMultipleQubitRegisters", - MQT_NAMED_BUILDER(allocMultipleQubitRegisters), + QCTestCase{"AllocQubit", MQT_NAMED_BUILDER(allocQubitNoMeasure), MQT_NAMED_BUILDER(emptyQC)}, QCTestCase{"AllocLargeRegister", MQT_NAMED_BUILDER(allocLargeRegister), - MQT_NAMED_BUILDER(emptyQC)}, - QCTestCase{"StaticQubits", MQT_NAMED_BUILDER(staticQubits), + MQT_NAMED_BUILDER(allocQubitRegister)}, + QCTestCase{"StaticQubits", MQT_NAMED_BUILDER(staticQubitsNoMeasure), MQT_NAMED_BUILDER(emptyQC)}, QCTestCase{"StaticQubitsWithOps", MQT_NAMED_BUILDER(staticQubitsWithOps), diff --git a/mlir/unittests/Dialect/QC/Translation/test_qasm3_translation.cpp b/mlir/unittests/Dialect/QC/Translation/test_qasm3_translation.cpp index b18c858b2d..c99059aa77 100644 --- a/mlir/unittests/Dialect/QC/Translation/test_qasm3_translation.cpp +++ b/mlir/unittests/Dialect/QC/Translation/test_qasm3_translation.cpp @@ -24,7 +24,9 @@ #include #include #include +#include #include +#include #include #include @@ -37,7 +39,7 @@ namespace { struct QASM3TranslationTestCase { std::string name; std::string source; - mqt::test::NamedBuilder referenceBuilder; + mqt::test::NamedMLIRBuilder referenceBuilder; friend std::ostream& operator<<(std::ostream& os, const QASM3TranslationTestCase& test); @@ -67,32 +69,47 @@ class QASM3TranslationTest } // namespace -static void twoX(qc::QCProgramBuilder& b) { +static SmallVector twoX(qc::QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.x(q[0]); b.x(q[1]); + auto c0 = b.measure(q[0]); + auto c1 = b.measure(q[1]); + return {c0, c1}; } -static void singleNegControlledX(qc::QCProgramBuilder& b) { +static SmallVector singleNegControlledX(qc::QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.x(q[0]); b.cx(q[0], q[1]); b.x(q[0]); + auto c0 = b.measure(q[0]); + auto c1 = b.measure(q[1]); + return {c0, c1}; } -static void tripleControlledX(qc::QCProgramBuilder& b) { +static SmallVector tripleControlledX(qc::QCProgramBuilder& b) { auto q = b.allocQubitRegister(4); b.mcx({q[0], q[1], q[2]}, q[3]); + auto c0 = b.measure(q[0]); + auto c1 = b.measure(q[1]); + auto c2 = b.measure(q[2]); + auto c3 = b.measure(q[3]); + return {c0, c1, c2, c3}; } -static void mixedControlledX(qc::QCProgramBuilder& b) { +static SmallVector mixedControlledX(qc::QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.x(q[1]); b.mcx({q[0], q[1]}, q[2]); b.x(q[1]); + auto c0 = b.measure(q[0]); + auto c1 = b.measure(q[1]); + auto c2 = b.measure(q[2]); + return {c0, c1, c2}; } -static void twoMixedControlledX(qc::QCProgramBuilder& b) { +static SmallVector twoMixedControlledX(qc::QCProgramBuilder& b) { auto q1 = b.allocQubitRegister(2); auto q2 = b.allocQubitRegister(2); auto q3 = b.allocQubitRegister(2); @@ -102,15 +119,24 @@ static void twoMixedControlledX(qc::QCProgramBuilder& b) { b.x(q2[1]); b.mcx({q1[1], q2[1]}, q3[1]); b.x(q2[1]); + auto c0 = b.measure(q1[0]); + auto c1 = b.measure(q1[1]); + auto c2 = b.measure(q2[0]); + auto c3 = b.measure(q2[1]); + auto c4 = b.measure(q3[0]); + auto c5 = b.measure(q3[1]); + return {c0, c1, c2, c3, c4, c5}; } -static void ifNot(qc::QCProgramBuilder& b) { +static Value ifNot(qc::QCProgramBuilder& b) { auto trueValue = b.boolConstant(true); auto q = b.allocQubitRegister(1); b.h(q[0]); auto c = b.measure(q[0]); auto cond = arith::XOrIOp::create(b, c, trueValue).getResult(); b.scfIf(cond, [&] { b.x(q[0]); }); + auto out = b.measure(q[0]); + return out; } TEST_P(QASM3TranslationTest, ProgramEquivalence) { @@ -128,8 +154,7 @@ TEST_P(QASM3TranslationTest, ProgramEquivalence) { printer.record(translated.get(), "Canonicalized Translated QC IR" + name); EXPECT_TRUE(verify(*translated).succeeded()); - auto reference = - qc::QCProgramBuilder::build(context.get(), referenceBuilder.fn); + auto reference = mqt::test::buildMLIRProgram(context.get(), referenceBuilder); ASSERT_TRUE(reference); printer.record(reference.get(), "Reference QC IR" + name); EXPECT_TRUE(verify(*reference).succeeded()); @@ -147,7 +172,7 @@ INSTANTIATE_TEST_SUITE_P( testing::Values( QASM3TranslationTestCase{"AllocQubit", qasm::allocQubit, - MQT_NAMED_BUILDER(qc::allocQubit)}, + MQT_NAMED_BUILDER(qc::alloc1QubitRegister)}, QASM3TranslationTestCase{"AllocQubitRegister", qasm::allocQubitRegister, MQT_NAMED_BUILDER(qc::allocQubitRegister)}, QASM3TranslationTestCase{ @@ -185,12 +210,12 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(qc::inverseGlobalPhase)}, QASM3TranslationTestCase{"Identity", qasm::identity, MQT_NAMED_BUILDER(qc::identity)}, - QASM3TranslationTestCase{ - "SingleControlledIdentity", qasm::singleControlledIdentity, - MQT_NAMED_BUILDER(qc::singleControlledIdentity)}, - QASM3TranslationTestCase{ - "MultipleControlledIdentity", qasm::multipleControlledIdentity, - MQT_NAMED_BUILDER(qc::multipleControlledIdentity)}, + QASM3TranslationTestCase{"SingleControlledIdentity", + qasm::singleControlledIdentity, + MQT_NAMED_BUILDER(qc::twoQubitsOneIdentity)}, + QASM3TranslationTestCase{"MultipleControlledIdentity", + qasm::multipleControlledIdentity, + MQT_NAMED_BUILDER(qc::threeQubitsOneIdentity)}, QASM3TranslationTestCase{"X", qasm::x, MQT_NAMED_BUILDER(qc::x)}, QASM3TranslationTestCase{"TwoX", qasm::twoX, MQT_NAMED_BUILDER(twoX)}, QASM3TranslationTestCase{"SingleControlledX", qasm::singleControlledX, diff --git a/mlir/unittests/Dialect/QC/Translation/test_quantum_computation_translation.cpp b/mlir/unittests/Dialect/QC/Translation/test_quantum_computation_translation.cpp index b47c9f97a7..b94e3f082c 100644 --- a/mlir/unittests/Dialect/QC/Translation/test_quantum_computation_translation.cpp +++ b/mlir/unittests/Dialect/QC/Translation/test_quantum_computation_translation.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -36,7 +37,7 @@ namespace { struct QuantumComputationTranslationTestCase { std::string name; mqt::test::NamedBuilder<::qc::QuantumComputation> programBuilder; - mqt::test::NamedBuilder referenceBuilder; + mqt::test::NamedMLIRBuilder referenceBuilder; friend std::ostream& operator<<(std::ostream& os, @@ -87,8 +88,7 @@ TEST_P(QuantumComputationTranslationTest, ProgramEquivalence) { printer.record(translated.get(), "Canonicalized Translated QC IR" + name); EXPECT_TRUE(mlir::verify(*translated).succeeded()); - auto reference = - mlir::qc::QCProgramBuilder::build(context.get(), referenceBuilder.fn); + auto reference = mqt::test::buildMLIRProgram(context.get(), referenceBuilder); ASSERT_TRUE(reference); printer.record(reference.get(), "Reference QC IR" + name); EXPECT_TRUE(mlir::verify(*reference).succeeded()); @@ -107,7 +107,7 @@ INSTANTIATE_TEST_SUITE_P( testing::Values( QuantumComputationTranslationTestCase{ "AllocQubit", MQT_NAMED_BUILDER(qc::allocQubit), - MQT_NAMED_BUILDER(mlir::qc::allocQubit)}, + MQT_NAMED_BUILDER(mlir::qc::alloc1QubitRegister)}, QuantumComputationTranslationTestCase{ "AllocQubitRegister", MQT_NAMED_BUILDER(qc::allocQubitRegister), MQT_NAMED_BUILDER(mlir::qc::allocQubitRegister)}, @@ -160,7 +160,7 @@ INSTANTIATE_TEST_SUITE_P( QuantumComputationTranslationTestCase{ "MultipleControlledIdentity", MQT_NAMED_BUILDER(qc::multipleControlledIdentity), - MQT_NAMED_BUILDER(mlir::qc::multipleControlledIdentity)}, + MQT_NAMED_BUILDER(mlir::qc::threeQubitsOneIdentity)}, QuantumComputationTranslationTestCase{"X", MQT_NAMED_BUILDER(qc::x), MQT_NAMED_BUILDER(mlir::qc::x)}, QuantumComputationTranslationTestCase{ diff --git a/mlir/unittests/Dialect/QCO/IR/CMakeLists.txt b/mlir/unittests/Dialect/QCO/IR/CMakeLists.txt index eabfef0087..40f522e459 100644 --- a/mlir/unittests/Dialect/QCO/IR/CMakeLists.txt +++ b/mlir/unittests/Dialect/QCO/IR/CMakeLists.txt @@ -8,8 +8,15 @@ set(qco_ir_target mqt-core-mlir-unittest-qco-ir) add_executable(${qco_ir_target} test_qco_ir.cpp test_qco_ir_matrix.cpp) -target_link_libraries(${qco_ir_target} PRIVATE MLIRParser MLIRSupportMQT GTest::gtest_main - MLIRQCOProgramBuilder MLIRQCOPrograms MQT::CoreDD) +target_link_libraries( + ${qco_ir_target} + PRIVATE MLIRParser + MLIRSupportMQT + GTest::gtest_main + MLIRQCOProgramBuilder + MLIRQCOPrograms + MLIRQCOUtils + MQT::CoreDD) mqt_mlir_configure_unittest_target(${qco_ir_target}) gtest_discover_tests(${qco_ir_target} PROPERTIES LABELS mqt-mlir-unittests DISCOVERY_TIMEOUT 60) diff --git a/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp b/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp index 26fca1733f..5d829b6284 100644 --- a/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp +++ b/mlir/unittests/Dialect/QCO/IR/test_qco_ir.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -39,8 +40,8 @@ namespace { struct QCOTestCase { std::string name; - mqt::test::NamedBuilder programBuilder; - mqt::test::NamedBuilder referenceBuilder; + mqt::test::NamedMLIRBuilder programBuilder; + mqt::test::NamedMLIRBuilder referenceBuilder; friend std::ostream& operator<<(std::ostream& os, const QCOTestCase& info); }; @@ -74,7 +75,7 @@ TEST_P(QCOTest, ProgramEquivalence) { const auto name = " (" + GetParam().name + ")"; mqt::test::DeferredPrinter printer; - auto program = QCOProgramBuilder::build(context.get(), programBuilder.fn); + auto program = mqt::test::buildMLIRProgram(context.get(), programBuilder); ASSERT_TRUE(program); printer.record(program.get(), "Original QCO IR" + name); EXPECT_TRUE(verify(*program).succeeded()); @@ -83,7 +84,7 @@ TEST_P(QCOTest, ProgramEquivalence) { printer.record(program.get(), "Canonicalized QCO IR" + name); EXPECT_TRUE(verify(*program).succeeded()); - auto reference = QCOProgramBuilder::build(context.get(), referenceBuilder.fn); + auto reference = mqt::test::buildMLIRProgram(context.get(), referenceBuilder); ASSERT_TRUE(reference); printer.record(reference.get(), "Reference QCO IR" + name); EXPECT_TRUE(verify(*reference).succeeded()); @@ -117,7 +118,7 @@ TEST_F(QCOTest, BuilderRejectsMixedStaticAndDynamicQubitAllocationModes) { TEST_F(QCOTest, DirectIfBuilder) { // Test If construction directly QCOProgramBuilder builder(context.get()); - builder.initialize(); + builder.initialize({builder.getI1Type(), builder.getI1Type()}); auto c0 = arith::ConstantIndexOp::create(builder, 0); auto c1 = arith::ConstantIndexOp::create(builder, 1); auto r0 = qtensor::AllocOp::create(builder, c1); @@ -130,18 +131,20 @@ TEST_F(QCOTest, DirectIfBuilder) { auto innerQubit = XOp::create(builder, qubits[0]); return SmallVector{innerQubit}; }); - auto r2 = qtensor::InsertOp::create(builder, ifOp.getResult(0), + auto finalMeasureOp = MeasureOp::create(builder, ifOp.getResult(0)); + auto r2 = qtensor::InsertOp::create(builder, finalMeasureOp.getQubitOut(), extractOp.getOutTensor(), c0); qtensor::DeallocOp::create(builder, r2); - auto directBuilder = builder.finalize(); + auto directBuilder = + builder.finalize({measureOp.getResult(), finalMeasureOp.getResult()}); ASSERT_TRUE(directBuilder); EXPECT_TRUE(verify(*directBuilder).succeeded()); EXPECT_TRUE(runQCOCleanupPipeline(directBuilder.get()).succeeded()); EXPECT_TRUE(verify(*directBuilder).succeeded()); auto refBuilder = - QCOProgramBuilder::build(context.get(), MQT_NAMED_BUILDER(simpleIf).fn); + mqt::test::buildMLIRProgram(context.get(), MQT_NAMED_BUILDER(simpleIf)); ASSERT_TRUE(refBuilder); EXPECT_TRUE(verify(*refBuilder).succeeded()); EXPECT_TRUE(runQCOCleanupPipeline(refBuilder.get()).succeeded()); @@ -155,10 +158,9 @@ TEST_F(QCOTest, IfOpParser) { // Test IfOp parser const char* mlirCode = R"( module { - func.func @main() -> i64 attributes {passthrough = ["entry_point"]} { + func.func @main() -> i1 attributes {passthrough = ["entry_point"]} { %c0 = arith.constant 0 : index %c1 = arith.constant 1 : index - %c0_i64 = arith.constant 0 : i64 %q0_0 = qco.alloc : !qco.qubit %t0 = qtensor.alloc(%c1) : tensor<1x!qco.qubit> %q0_1 = qco.h %q0_0 : !qco.qubit -> !qco.qubit @@ -172,9 +174,10 @@ TEST_F(QCOTest, IfOpParser) { } else args(%arg0 = %q0_2, %arg1 = %t0) { qco.yield %arg0, %arg1 : !qco.qubit, tensor<1x!qco.qubit> } - qco.sink %q0_4 : !qco.qubit + %q0_5, %c = qco.measure %q0_4 : !qco.qubit + qco.sink %q0_5 : !qco.qubit qtensor.dealloc %t3 : tensor<1x!qco.qubit> - return %c0_i64 : i64 + return %c : i1 } })"; @@ -185,8 +188,8 @@ TEST_F(QCOTest, IfOpParser) { EXPECT_TRUE(runQCOCleanupPipeline(parsedSourceModule.get()).succeeded()); EXPECT_TRUE(verify(*parsedSourceModule).succeeded()); - auto refBuilder = QCOProgramBuilder::build( - context.get(), MQT_NAMED_BUILDER(ifOneQubitOneTensor).fn); + auto refBuilder = mqt::test::buildMLIRProgram( + context.get(), MQT_NAMED_BUILDER(ifOneQubitOneTensor)); ASSERT_TRUE(refBuilder); EXPECT_TRUE(verify(*refBuilder).succeeded()); EXPECT_TRUE(runQCOCleanupPipeline(refBuilder.get()).succeeded()); @@ -254,7 +257,9 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(invCtrlSandwich), MQT_NAMED_BUILDER(singleControlledRxx)}, QCOTestCase{"InvCtrlTwo", MQT_NAMED_BUILDER(invCtrlTwo), - MQT_NAMED_BUILDER(ctrlInvTwo)})); + MQT_NAMED_BUILDER(ctrlInvTwo)}, + QCOTestCase{"InverseT", MQT_NAMED_BUILDER(inverseT), + MQT_NAMED_BUILDER(tdg)})); /// @} /// \name QCO/Operations/StandardGates/BarrierOp.cpp @@ -306,7 +311,7 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(twoDcx)}, QCOTestCase{"TwoDCXSwappedTargets", MQT_NAMED_BUILDER(twoDcxSwappedTargets), - MQT_NAMED_BUILDER(emptyQCO)})); + MQT_NAMED_BUILDER(alloc2QubitRegister)})); /// @} /// \name QCO/Operations/StandardGates/EcrOp.cpp @@ -333,7 +338,7 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(inverseMultipleControlledEcr), MQT_NAMED_BUILDER(multipleControlledEcr)}, QCOTestCase{"TwoECR", MQT_NAMED_BUILDER(twoEcr), - MQT_NAMED_BUILDER(emptyQCO)})); + MQT_NAMED_BUILDER(alloc2QubitRegister)})); /// @} /// \name QCO/Operations/StandardGates/GphaseOp.cpp @@ -377,7 +382,7 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(inverseMultipleControlledH), MQT_NAMED_BUILDER(multipleControlledH)}, QCOTestCase{"TwoH", MQT_NAMED_BUILDER(twoH), - MQT_NAMED_BUILDER(emptyQCO)})); + MQT_NAMED_BUILDER(allocQubit)})); /// @} /// \name QCO/Operations/StandardGates/IdOp.cpp @@ -386,24 +391,24 @@ INSTANTIATE_TEST_SUITE_P( QCOIDOpTest, QCOTest, testing::Values( QCOTestCase{"Identity", MQT_NAMED_BUILDER(identity), - MQT_NAMED_BUILDER(emptyQCO)}, + MQT_NAMED_BUILDER(allocQubit)}, QCOTestCase{"SingleControlledIdentity", MQT_NAMED_BUILDER(singleControlledIdentity), - MQT_NAMED_BUILDER(emptyQCO)}, + MQT_NAMED_BUILDER(alloc2QubitRegister)}, QCOTestCase{"MultipleControlledIdentity", MQT_NAMED_BUILDER(multipleControlledIdentity), - MQT_NAMED_BUILDER(emptyQCO)}, + MQT_NAMED_BUILDER(alloc3QubitRegister)}, QCOTestCase{"NestedControlledIdentity", MQT_NAMED_BUILDER(nestedControlledIdentity), - MQT_NAMED_BUILDER(emptyQCO)}, + MQT_NAMED_BUILDER(alloc3QubitRegister)}, QCOTestCase{"TrivialControlledIdentity", MQT_NAMED_BUILDER(trivialControlledIdentity), - MQT_NAMED_BUILDER(emptyQCO)}, + MQT_NAMED_BUILDER(allocQubit)}, QCOTestCase{"InverseIdentity", MQT_NAMED_BUILDER(inverseIdentity), - MQT_NAMED_BUILDER(emptyQCO)}, + MQT_NAMED_BUILDER(alloc1QubitRegister)}, QCOTestCase{"InverseMultipleControlledIdentity", MQT_NAMED_BUILDER(inverseMultipleControlledIdentity), - MQT_NAMED_BUILDER(emptyQCO)})); + MQT_NAMED_BUILDER(alloc3QubitRegister)})); /// @} /// \name QCO/Operations/StandardGates/IswapOp.cpp @@ -453,7 +458,7 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(inverseMultipleControlledP), MQT_NAMED_BUILDER(multipleControlledP)}, QCOTestCase{"TwoPOppositePhase", MQT_NAMED_BUILDER(twoPOppositePhase), - MQT_NAMED_BUILDER(emptyQCO)})); + MQT_NAMED_BUILDER(allocQubit)})); /// @} /// \name QCO/Operations/StandardGates/ROp.cpp @@ -505,7 +510,7 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(inverseMultipleControlledRx), MQT_NAMED_BUILDER(multipleControlledRx)}, QCOTestCase{"TwoRXOppositePhase", MQT_NAMED_BUILDER(twoRxOppositePhase), - MQT_NAMED_BUILDER(emptyQCO)})); + MQT_NAMED_BUILDER(alloc1QubitRegister)})); /// @} /// \name QCO/Operations/StandardGates/RxxOp.cpp @@ -538,10 +543,10 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(rxx)}, QCOTestCase{"TwoRXXOppositePhase", MQT_NAMED_BUILDER(twoRxxOppositePhase), - MQT_NAMED_BUILDER(emptyQCO)}, + MQT_NAMED_BUILDER(alloc2QubitRegister)}, QCOTestCase{"TwoRXXOppositePhaseSwappedTargets", MQT_NAMED_BUILDER(twoRxxOppositePhaseSwappedTargets), - MQT_NAMED_BUILDER(emptyQCO)})); + MQT_NAMED_BUILDER(alloc2QubitRegister)})); /// @} /// \name QCO/Operations/StandardGates/RyOp.cpp @@ -566,7 +571,7 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(inverseMultipleControlledRy), MQT_NAMED_BUILDER(multipleControlledRy)}, QCOTestCase{"TwoRYOppositePhase", MQT_NAMED_BUILDER(twoRyOppositePhase), - MQT_NAMED_BUILDER(emptyQCO)})); + MQT_NAMED_BUILDER(alloc1QubitRegister)})); /// @} /// \name QCO/Operations/StandardGates/RyyOp.cpp @@ -599,10 +604,10 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(ryy)}, QCOTestCase{"TwoRYYOppositePhaseSwappedTargets", MQT_NAMED_BUILDER(twoRyyOppositePhaseSwappedTargets), - MQT_NAMED_BUILDER(emptyQCO)}, + MQT_NAMED_BUILDER(alloc2QubitRegister)}, QCOTestCase{"TwoRYYOppositePhase", MQT_NAMED_BUILDER(twoRyyOppositePhase), - MQT_NAMED_BUILDER(emptyQCO)})); + MQT_NAMED_BUILDER(alloc2QubitRegister)})); /// @} /// \name QCO/Operations/StandardGates/RzOp.cpp @@ -627,7 +632,7 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(inverseMultipleControlledRz), MQT_NAMED_BUILDER(multipleControlledRz)}, QCOTestCase{"TwoRZOppositePhase", MQT_NAMED_BUILDER(twoRzOppositePhase), - MQT_NAMED_BUILDER(emptyQCO)})); + MQT_NAMED_BUILDER(alloc1QubitRegister)})); /// @} /// \name QCO/Operations/StandardGates/RzxOp.cpp @@ -655,7 +660,7 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(multipleControlledRzx)}, QCOTestCase{"TwoRZXOppositePhase", MQT_NAMED_BUILDER(twoRzxOppositePhase), - MQT_NAMED_BUILDER(emptyQCO)})); + MQT_NAMED_BUILDER(alloc2QubitRegister)})); /// @} /// \name QCO/Operations/StandardGates/RzzOp.cpp @@ -688,10 +693,10 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(rzz)}, QCOTestCase{"TwoRZZOppositePhaseSwappedTargets", MQT_NAMED_BUILDER(twoRzzOppositePhaseSwappedTargets), - MQT_NAMED_BUILDER(emptyQCO)}, + MQT_NAMED_BUILDER(alloc2QubitRegister)}, QCOTestCase{"TwoRZZOppositePhase", MQT_NAMED_BUILDER(twoRzzOppositePhase), - MQT_NAMED_BUILDER(emptyQCO)})); + MQT_NAMED_BUILDER(alloc2QubitRegister)})); /// @} /// \name QCO/Operations/StandardGates/SOp.cpp @@ -715,7 +720,7 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(inverseMultipleControlledS), MQT_NAMED_BUILDER(multipleControlledSdg)}, QCOTestCase{"SThenSdg", MQT_NAMED_BUILDER(sThenSdg), - MQT_NAMED_BUILDER(emptyQCO)}, + MQT_NAMED_BUILDER(alloc1QubitRegister)}, QCOTestCase{"TwoS", MQT_NAMED_BUILDER(twoS), MQT_NAMED_BUILDER(z)})); /// @} @@ -743,7 +748,7 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(inverseMultipleControlledSdg), MQT_NAMED_BUILDER(multipleControlledS)}, QCOTestCase{"SdgThenS", MQT_NAMED_BUILDER(sdgThenS), - MQT_NAMED_BUILDER(emptyQCO)}, + MQT_NAMED_BUILDER(alloc1QubitRegister)}, QCOTestCase{"TwoSdg", MQT_NAMED_BUILDER(twoSdg), MQT_NAMED_BUILDER(z)})); /// @} @@ -772,10 +777,10 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(inverseMultipleControlledSwap), MQT_NAMED_BUILDER(multipleControlledSwap)}, QCOTestCase{"TwoSWAP", MQT_NAMED_BUILDER(twoSwap), - MQT_NAMED_BUILDER(emptyQCO)}, + MQT_NAMED_BUILDER(alloc2QubitRegister)}, QCOTestCase{"TwoSWAPSwappedTargets", MQT_NAMED_BUILDER(twoSwapSwappedTargets), - MQT_NAMED_BUILDER(emptyQCO)})); + MQT_NAMED_BUILDER(alloc2QubitRegister)})); /// @} /// \name QCO/Operations/StandardGates/SxOp.cpp @@ -800,7 +805,7 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(inverseMultipleControlledSx), MQT_NAMED_BUILDER(multipleControlledSxdg)}, QCOTestCase{"SXThenSXdg", MQT_NAMED_BUILDER(sxThenSxdg), - MQT_NAMED_BUILDER(emptyQCO)}, + MQT_NAMED_BUILDER(alloc1QubitRegister)}, QCOTestCase{"TwoSX", MQT_NAMED_BUILDER(twoSx), MQT_NAMED_BUILDER(x)})); /// @} @@ -828,7 +833,7 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(inverseMultipleControlledSxdg), MQT_NAMED_BUILDER(multipleControlledSx)}, QCOTestCase{"SXdgThenSX", MQT_NAMED_BUILDER(sxdgThenSx), - MQT_NAMED_BUILDER(emptyQCO)}, + MQT_NAMED_BUILDER(alloc1QubitRegister)}, QCOTestCase{"TwoSXdg", MQT_NAMED_BUILDER(twoSxdg), MQT_NAMED_BUILDER(x)})); /// @} @@ -854,7 +859,7 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(inverseMultipleControlledT), MQT_NAMED_BUILDER(multipleControlledTdg)}, QCOTestCase{"TThenTdg", MQT_NAMED_BUILDER(tThenTdg), - MQT_NAMED_BUILDER(emptyQCO)}, + MQT_NAMED_BUILDER(alloc1QubitRegister)}, QCOTestCase{"TwoT", MQT_NAMED_BUILDER(twoT), MQT_NAMED_BUILDER(s)})); /// @} @@ -882,7 +887,7 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(inverseMultipleControlledTdg), MQT_NAMED_BUILDER(multipleControlledT)}, QCOTestCase{"TdgThenS", MQT_NAMED_BUILDER(tdgThenT), - MQT_NAMED_BUILDER(emptyQCO)}, + MQT_NAMED_BUILDER(alloc1QubitRegister)}, QCOTestCase{"TwoTdg", MQT_NAMED_BUILDER(twoTdg), MQT_NAMED_BUILDER(sdg)})); /// @} @@ -967,11 +972,11 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(inverseMultipleControlledX), MQT_NAMED_BUILDER(multipleControlledX)}, QCOTestCase{"TwoX", MQT_NAMED_BUILDER(twoX), - MQT_NAMED_BUILDER(emptyQCO)}, + MQT_NAMED_BUILDER(alloc1QubitRegister)}, QCOTestCase{"ControlledTwoX", MQT_NAMED_BUILDER(controlledTwoX), - MQT_NAMED_BUILDER(emptyQCO)}, + MQT_NAMED_BUILDER(alloc2QubitRegister)}, QCOTestCase{"InverseTwoX", MQT_NAMED_BUILDER(inverseTwoX), - MQT_NAMED_BUILDER(emptyQCO)})); + MQT_NAMED_BUILDER(alloc1QubitRegister)})); /// @} /// \name QCO/Operations/StandardGates/XxMinusYyOp.cpp @@ -1000,7 +1005,7 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(multipleControlledXxMinusYY)}, QCOTestCase{"TwoXXMinusYYOppositePhase", MQT_NAMED_BUILDER(twoXxMinusYYOppositePhase), - MQT_NAMED_BUILDER(emptyQCO)}, + MQT_NAMED_BUILDER(alloc2QubitRegister)}, QCOTestCase{"TwoXXMinusYYSwappedTargets", MQT_NAMED_BUILDER(twoXxMinusYYSwappedTargets), MQT_NAMED_BUILDER(xxMinusYY)})); @@ -1032,7 +1037,7 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(multipleControlledXxPlusYY)}, QCOTestCase{"TwoXXPlusYYOppositePhase", MQT_NAMED_BUILDER(twoXxPlusYYOppositePhase), - MQT_NAMED_BUILDER(emptyQCO)}, + MQT_NAMED_BUILDER(alloc2QubitRegister)}, QCOTestCase{"TwoXXPlusYYSwappedTargets", MQT_NAMED_BUILDER(twoXxPlusYYSwappedTargets), MQT_NAMED_BUILDER(xxPlusYY)})); @@ -1059,7 +1064,7 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(inverseMultipleControlledY), MQT_NAMED_BUILDER(multipleControlledY)}, QCOTestCase{"TwoY", MQT_NAMED_BUILDER(twoY), - MQT_NAMED_BUILDER(emptyQCO)})); + MQT_NAMED_BUILDER(alloc1QubitRegister)})); /// @} /// \name QCO/Operations/StandardGates/ZOp.cpp @@ -1083,7 +1088,7 @@ INSTANTIATE_TEST_SUITE_P( MQT_NAMED_BUILDER(inverseMultipleControlledZ), MQT_NAMED_BUILDER(multipleControlledZ)}, QCOTestCase{"TwoZ", MQT_NAMED_BUILDER(twoZ), - MQT_NAMED_BUILDER(emptyQCO)})); + MQT_NAMED_BUILDER(alloc1QubitRegister)})); /// @} /// \name QCO/Operations/MeasureOp.cpp @@ -1112,13 +1117,13 @@ INSTANTIATE_TEST_SUITE_P( QCOResetOpTest, QCOTest, testing::Values(QCOTestCase{"ResetQubitWithoutOp", MQT_NAMED_BUILDER(resetQubitWithoutOp), - MQT_NAMED_BUILDER(emptyQCO)}, + MQT_NAMED_BUILDER(allocQubit)}, QCOTestCase{"ResetMultipleQubitsWithoutOp", MQT_NAMED_BUILDER(resetMultipleQubitsWithoutOp), - MQT_NAMED_BUILDER(emptyQCO)}, + MQT_NAMED_BUILDER(alloc2QubitRegister)}, QCOTestCase{"RepeatedResetWithoutOp", MQT_NAMED_BUILDER(repeatedResetWithoutOp), - MQT_NAMED_BUILDER(emptyQCO)}, + MQT_NAMED_BUILDER(allocQubit)}, QCOTestCase{"ResetQubitAfterSingleOp", MQT_NAMED_BUILDER(resetQubitAfterSingleOp), MQT_NAMED_BUILDER(resetQubitAfterSingleOp)}, @@ -1136,16 +1141,10 @@ INSTANTIATE_TEST_SUITE_P( INSTANTIATE_TEST_SUITE_P( QCOQubitManagementTest, QCOTest, testing::Values( - QCOTestCase{"AllocQubit", MQT_NAMED_BUILDER(allocQubit), - MQT_NAMED_BUILDER(emptyQCO)}, - QCOTestCase{"AllocQubitRegister", MQT_NAMED_BUILDER(allocQubitRegister), - MQT_NAMED_BUILDER(emptyQCO)}, - QCOTestCase{"AllocMultipleQubitRegisters", - MQT_NAMED_BUILDER(allocMultipleQubitRegisters), - MQT_NAMED_BUILDER(emptyQCO)}, - QCOTestCase{"AllocLargeRegister", MQT_NAMED_BUILDER(allocLargeRegister), + QCOTestCase{"AllocQubit", MQT_NAMED_BUILDER(allocQubitNoMeasure), MQT_NAMED_BUILDER(emptyQCO)}, - QCOTestCase{"StaticQubits", MQT_NAMED_BUILDER(staticQubits), + QCOTestCase{"StaticQubitsNoMeasure", + MQT_NAMED_BUILDER(staticQubitsNoMeasure), MQT_NAMED_BUILDER(emptyQCO)}, QCOTestCase{"StaticQubitsWithOps", MQT_NAMED_BUILDER(staticQubitsWithOps), @@ -1162,6 +1161,11 @@ INSTANTIATE_TEST_SUITE_P( QCOTestCase{"StaticQubitsWithInv", MQT_NAMED_BUILDER(staticQubitsWithInv), MQT_NAMED_BUILDER(staticQubitsWithInv)}, + QCOTestCase{"DeadGateElimination", MQT_NAMED_BUILDER(deadGatesProgram), + MQT_NAMED_BUILDER(alloc2Qubits)}, + QCOTestCase{"DeadGateEliminationIfOp", + MQT_NAMED_BUILDER(deadGatesWithIfOpProgram), + MQT_NAMED_BUILDER(deadGatesWithIfOpSimplified)}, QCOTestCase{"AllocSinkPair", MQT_NAMED_BUILDER(allocSinkPair), - MQT_NAMED_BUILDER(emptyQCO)})); + MQT_NAMED_BUILDER(allocQubitNoMeasure)})); /// @} diff --git a/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp b/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp index 7e0cd58d31..49233d5e4e 100644 --- a/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp +++ b/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp @@ -20,6 +20,7 @@ #include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/QCOUtils.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" #include "qco_programs.h" @@ -28,13 +29,17 @@ #include #include #include +#include #include #include +#include #include #include #include +#include #include +#include using namespace mlir; using namespace qco; @@ -55,8 +60,7 @@ matrix4FromDefinition(const Definition& definition) { template [[nodiscard]] static Matrix4x4 -expectedMatrixFromComputation(const Fn& build, - const std::size_t numQubits = 2) { +expectedMatrixFromComputation(const Fn& build, const size_t numQubits = 2) { qc::QuantumComputation comp; build(comp); const auto package = std::make_unique(numQubits); @@ -64,33 +68,45 @@ expectedMatrixFromComputation(const Fn& build, dd::buildFunctionality(comp, *package).getMatrix(numQubits)); } -static void controlledXH(QCOProgramBuilder& b) { - auto q = b.allocQubitRegister(2); - b.ctrl(q[0], q[1], [&](ValueRange targets) { - auto wire = b.x(targets[0]); - wire = b.h(wire); - return SmallVector{wire}; - }); +[[nodiscard]] static InvOp firstInvOp(ModuleOp module) { + auto funcOp = cast(module.getBody()->front()); + return *funcOp.getBody().getOps().begin(); } -static void controlledInverseHT(QCOProgramBuilder& b) { - auto q = b.allocQubitRegister(2); - b.ctrl(q[0], q[1], [&](ValueRange targets) { - auto wire = b.inv({targets[0]}, [&](ValueRange innerTargets) { - auto inner = b.h(innerTargets[0]); - inner = b.t(inner); - return SmallVector{inner}; - })[0]; - return SmallVector{wire}; - }); +[[nodiscard]] static CtrlOp firstCtrlOp(ModuleOp module) { + auto funcOp = cast(module.getBody()->front()); + return *funcOp.getBody().getOps().begin(); +} + +[[nodiscard]] static std::optional invMatrix(ModuleOp module) { + return firstInvOp(module).getUnitaryMatrix(); +} + +template +static void assertInvBodyAdjoint(MLIRContext* ctx, Builder&& build, + const DynamicMatrix& body) { + auto moduleOp = QCOProgramBuilder::build(ctx, std::forward(build)); + ASSERT_TRUE(moduleOp); + const auto matrix = invMatrix(*moduleOp); + ASSERT_TRUE(matrix); + ASSERT_TRUE(matrix->isApprox(body.adjoint())); +} + +template +static void expectComposeNTargetFails(MLIRContext* ctx, Builder&& build, + size_t numTargets) { + auto moduleOp = QCOProgramBuilder::build(ctx, std::forward(build)); + ASSERT_TRUE(moduleOp); + EXPECT_FALSE(composeBodyMatrix(*firstInvOp(*moduleOp).getBody(), numTargets) + .has_value()); } namespace { struct QCOMatrixTestCase { std::string name; - mqt::test::NamedBuilder programBuilder; - mqt::test::NamedBuilder referenceBuilder; + mqt::test::NamedMLIRBuilder programBuilder; + mqt::test::NamedMLIRBuilder referenceBuilder; }; class QCOMatrixTest : public testing::TestWithParam { @@ -114,10 +130,8 @@ TEST_F(QCOMatrixTest, CXOpMatrix) { auto moduleOp = QCOProgramBuilder::build(context.get(), singleControlledX); ASSERT_TRUE(moduleOp); - // Get the operation from the module - auto funcOp = *moduleOp->getBody()->getOps().begin(); - auto ctrlOp = *funcOp.getBody().getOps().begin(); - auto matrix = ctrlOp.getUnitaryMatrix(); + const auto matrix = firstCtrlOp(*moduleOp).getUnitaryMatrix(); + ASSERT_TRUE(matrix); const Matrix4x4 expected = expectedMatrixFromComputation([](qc::QuantumComputation& comp) { @@ -132,9 +146,7 @@ TEST_F(QCOMatrixTest, ControlledHOpMatrix) { auto moduleOp = QCOProgramBuilder::build(context.get(), singleControlledH); ASSERT_TRUE(moduleOp); - auto funcOp = *moduleOp->getBody()->getOps().begin(); - auto ctrlOp = *funcOp.getBody().getOps().begin(); - auto matrix = ctrlOp.getUnitaryMatrix(); + const auto matrix = firstCtrlOp(*moduleOp).getUnitaryMatrix(); ASSERT_TRUE(matrix); const Matrix4x4 expected = @@ -150,9 +162,7 @@ TEST_F(QCOMatrixTest, ControlledXHOpMatrix) { auto moduleOp = QCOProgramBuilder::build(context.get(), controlledXH); ASSERT_TRUE(moduleOp); - auto funcOp = *moduleOp->getBody()->getOps().begin(); - auto ctrlOp = *funcOp.getBody().getOps().begin(); - auto matrix = ctrlOp.getUnitaryMatrix(); + const auto matrix = firstCtrlOp(*moduleOp).getUnitaryMatrix(); ASSERT_TRUE(matrix); const Matrix4x4 expected = @@ -169,9 +179,7 @@ TEST_F(QCOMatrixTest, ControlledInverseHTOpMatrix) { auto moduleOp = QCOProgramBuilder::build(context.get(), controlledInverseHT); ASSERT_TRUE(moduleOp); - auto funcOp = *moduleOp->getBody()->getOps().begin(); - auto ctrlOp = *funcOp.getBody().getOps().begin(); - auto matrix = ctrlOp.getUnitaryMatrix(); + const auto matrix = firstCtrlOp(*moduleOp).getUnitaryMatrix(); ASSERT_TRUE(matrix); const Matrix4x4 expected = @@ -186,6 +194,118 @@ TEST_F(QCOMatrixTest, ControlledInverseHTOpMatrix) { ASSERT_TRUE(matrix->isApprox(expected)); } + +TEST_F(QCOMatrixTest, InverseTwoRxRyOpMatrix) { + auto moduleOp = QCOProgramBuilder::build(context.get(), inverseTwoRxRy); + ASSERT_TRUE(moduleOp); + + const auto matrix = invMatrix(*moduleOp); + ASSERT_TRUE(matrix); + + const DynamicMatrix body = RYOp::unitaryMatrix(0.3).embedInNqubit(2, 1) * + RXOp::unitaryMatrix(0.2).embedInNqubit(2, 0); + ASSERT_TRUE(matrix->isApprox(body.adjoint())); +} + +TEST_F(QCOMatrixTest, InverseCxThenRzOpMatrix) { + auto moduleOp = QCOProgramBuilder::build(context.get(), inverseCxThenRz); + ASSERT_TRUE(moduleOp); + + const auto matrix = invMatrix(*moduleOp); + ASSERT_TRUE(matrix); + + const Matrix4x4 cx = Matrix4x4::fromElements(1.0, 0.0, 0.0, 0.0, // + 0.0, 1.0, 0.0, 0.0, // + 0.0, 0.0, 0.0, 1.0, // + 0.0, 0.0, 1.0, 0.0); + const DynamicMatrix body = + RZOp::unitaryMatrix(0.4).embedInNqubit(2, 1) * cx.embedInNqubit(2, 0, 1); + ASSERT_TRUE(matrix->isApprox(body.adjoint())); +} + +TEST_F(QCOMatrixTest, InverseDcxThenRzOpMatrix) { + auto moduleOp = QCOProgramBuilder::build(context.get(), inverseDcxThenRz); + ASSERT_TRUE(moduleOp); + + const auto matrix = invMatrix(*moduleOp); + ASSERT_TRUE(matrix); + + const DynamicMatrix body = RZOp::unitaryMatrix(0.4).embedInNqubit(2, 1) * + DCXOp::getUnitaryMatrix().embedInNqubit(2, 0, 1); + ASSERT_TRUE(matrix->isApprox(body.adjoint())); +} + +TEST_F(QCOMatrixTest, InvCtrlTwoOpMatrix) { + auto moduleOp = QCOProgramBuilder::build(context.get(), invCtrlTwo); + ASSERT_TRUE(moduleOp); + + const auto matrix = invMatrix(*moduleOp); + ASSERT_TRUE(matrix); + + DynamicMatrix body = RXXOp::unitaryMatrix(0.123).embedInNqubit(2, 0, 1) * + XOp::getUnitaryMatrix().embedInNqubit(2, 0); + DynamicMatrix ctrl = DynamicMatrix::identity(8); + ctrl.setBottomRightCorner(body); + ASSERT_TRUE(matrix->isApprox(ctrl.adjoint())); +} + +TEST_F(QCOMatrixTest, InverseGphaseBarrierXOpMatrix) { + DynamicMatrix body; + body.assignFrom(XOp::getUnitaryMatrix()); + body *= std::exp(Complex{0.0, 0.25}); + assertInvBodyAdjoint(context.get(), inverseGphaseBarrierX, body); +} + +TEST_F(QCOMatrixTest, InverseModifierWiresOpMatrix) { + assertInvBodyAdjoint( + context.get(), inverseNestedInvHAndT, + DynamicMatrix(TOp::getUnitaryMatrix() * HOp::getUnitaryMatrix())); + assertInvBodyAdjoint(context.get(), inverseNestedInvHAndX, + XOp::getUnitaryMatrix().embedInNqubit(2, 1) * + HOp::getUnitaryMatrix().embedInNqubit(2, 0)); + assertInvBodyAdjoint(context.get(), inverseThreeWireRxRyRz, + RZOp::unitaryMatrix(0.4).embedInNqubit(3, 2) * + RYOp::unitaryMatrix(0.3).embedInNqubit(3, 1) * + RXOp::unitaryMatrix(0.2).embedInNqubit(3, 0)); + assertInvBodyAdjoint(context.get(), inverseThreeWireNestedTwoInv, + RZOp::unitaryMatrix(0.4).embedInNqubit(3, 2) * + (RYOp::unitaryMatrix(0.3).embedInNqubit(3, 1) * + RXOp::unitaryMatrix(0.2).embedInNqubit(3, 0)) + .adjoint()); +} + +TEST_F(QCOMatrixTest, ComposeNTargetRejectsExcessiveTargets) { + auto moduleOp = QCOProgramBuilder::build(context.get(), inverseTwoRxRy); + ASSERT_TRUE(moduleOp); + EXPECT_FALSE(composeBodyMatrix(*firstInvOp(*moduleOp).getBody(), + kMaxModifierTargetQubits + 1) + .has_value()); +} + +TEST_F(QCOMatrixTest, ComposeNTargetRejectsThreeQubitOp) { + expectComposeNTargetFails(context.get(), inverseWithThreeQubitOpInBody, 3); +} + +TEST_F(QCOMatrixTest, ComposeNTargetRejectsRuntimeGphase) { + constexpr auto mlirCode = R"( + module { + func.func @test(%theta: f64) -> !qco.qubit { + %q_in = qco.alloc : !qco.qubit + %q_out = qco.inv (%q = %q_in) { + qco.gphase(%theta) + %q_1 = qco.x %q : !qco.qubit -> !qco.qubit + qco.yield %q_1 : !qco.qubit + } : {!qco.qubit} -> {!qco.qubit} + return %q_out : !qco.qubit + } + } + )"; + + auto moduleOp = parseSourceString(mlirCode, context.get()); + ASSERT_TRUE(moduleOp); + EXPECT_FALSE( + composeBodyMatrix(*firstInvOp(*moduleOp).getBody(), 1).has_value()); +} /// @} /// \name QCO/Modifiers/InvOp.cpp @@ -194,10 +314,8 @@ TEST_F(QCOMatrixTest, InverseIswapOpMatrix) { auto moduleOp = QCOProgramBuilder::build(context.get(), inverseIswap); ASSERT_TRUE(moduleOp); - // Get the operation from the module - auto funcOp = *moduleOp->getBody()->getOps().begin(); - auto invOp = *funcOp.getBody().getOps().begin(); - auto matrix = invOp.getUnitaryMatrix(); + const auto matrix = invMatrix(*moduleOp); + ASSERT_TRUE(matrix); const Matrix4x4 expected = expectedMatrixFromComputation([](qc::QuantumComputation& comp) { @@ -212,9 +330,7 @@ TEST_F(QCOMatrixTest, InverseTwoXOpMatrix) { auto moduleOp = QCOProgramBuilder::build(context.get(), inverseTwoX); ASSERT_TRUE(moduleOp); - auto funcOp = *moduleOp->getBody()->getOps().begin(); - auto invOp = *funcOp.getBody().getOps().begin(); - const auto matrix = invOp.getUnitaryMatrix(); + const auto matrix = invMatrix(*moduleOp); ASSERT_TRUE(matrix); DynamicMatrix expected; @@ -226,9 +342,7 @@ TEST_F(QCOMatrixTest, InverseXOpMatrix) { auto moduleOp = QCOProgramBuilder::build(context.get(), inverseX); ASSERT_TRUE(moduleOp); - auto funcOp = *moduleOp->getBody()->getOps().begin(); - auto invOp = *funcOp.getBody().getOps().begin(); - const auto matrix = invOp.getUnitaryMatrix(); + const auto matrix = invMatrix(*moduleOp); ASSERT_TRUE(matrix); DynamicMatrix expected; @@ -240,9 +354,7 @@ TEST_F(QCOMatrixTest, InverseSxOpMatrix) { auto moduleOp = QCOProgramBuilder::build(context.get(), inverseSx); ASSERT_TRUE(moduleOp); - auto funcOp = *moduleOp->getBody()->getOps().begin(); - auto invOp = *funcOp.getBody().getOps().begin(); - const auto matrix = invOp.getUnitaryMatrix(); + const auto matrix = invMatrix(*moduleOp); ASSERT_TRUE(matrix); DynamicMatrix expected; @@ -254,9 +366,7 @@ TEST_F(QCOMatrixTest, InverseGphaseXOpMatrix) { auto moduleOp = QCOProgramBuilder::build(context.get(), inverseGphaseX); ASSERT_TRUE(moduleOp); - auto funcOp = *moduleOp->getBody()->getOps().begin(); - auto invOp = *funcOp.getBody().getOps().begin(); - const auto matrix = invOp.getUnitaryMatrix(); + const auto matrix = invMatrix(*moduleOp); ASSERT_TRUE(matrix); const auto composeGlobal = std::polar(1.0, -0.123); @@ -269,9 +379,7 @@ TEST_F(QCOMatrixTest, InverseGphaseBarrierOpMatrix) { auto moduleOp = QCOProgramBuilder::build(context.get(), inverseGphaseBarrier); ASSERT_TRUE(moduleOp); - auto funcOp = *moduleOp->getBody()->getOps().begin(); - auto invOp = *funcOp.getBody().getOps().begin(); - const auto matrix = invOp.getUnitaryMatrix(); + const auto matrix = invMatrix(*moduleOp); ASSERT_TRUE(matrix); const auto global = std::conj(std::polar(1.0, 0.123)); @@ -284,19 +392,20 @@ TEST_F(QCOMatrixTest, InverseTwoBarriersInInvOpMatrix) { auto moduleOp = QCOProgramBuilder::build(context.get(), inverseTwoBarriersInInv); ASSERT_TRUE(moduleOp); - - auto funcOp = *moduleOp->getBody()->getOps().begin(); - auto invOp = *funcOp.getBody().getOps().begin(); - EXPECT_FALSE(invOp.getUnitaryMatrix()); + EXPECT_FALSE(invMatrix(*moduleOp).has_value()); } TEST_F(QCOMatrixTest, InvTwoOpMatrix) { auto moduleOp = QCOProgramBuilder::build(context.get(), invTwo); ASSERT_TRUE(moduleOp); - auto funcOp = *moduleOp->getBody()->getOps().begin(); - auto invOp = *funcOp.getBody().getOps().begin(); - EXPECT_FALSE(invOp.getUnitaryMatrix()); + const auto matrix = invMatrix(*moduleOp); + ASSERT_TRUE(matrix); + + const DynamicMatrix body = + RXXOp::unitaryMatrix(0.123).embedInNqubit(2, 0, 1) * + XOp::getUnitaryMatrix().embedInNqubit(2, 0); + ASSERT_TRUE(matrix->isApprox(body.adjoint())); } TEST_F(QCOMatrixTest, InverseDynamicRzXOpMatrix) { @@ -316,10 +425,7 @@ TEST_F(QCOMatrixTest, InverseDynamicRzXOpMatrix) { auto moduleOp = parseSourceString(mlirCode, context.get()); ASSERT_TRUE(moduleOp); - - auto funcOp = *moduleOp->getBody()->getOps().begin(); - auto invOp = *funcOp.getBody().getOps().begin(); - EXPECT_FALSE(invOp.getUnitaryMatrix()); + EXPECT_FALSE(invMatrix(*moduleOp).has_value()); } /// @} diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp index 90041fbccd..f1413aed70 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -18,6 +18,8 @@ #include "mlir/Dialect/Utils/Utils.h" #include +#include +#include #include #include #include @@ -70,7 +72,7 @@ struct TestFixture { void setUp() { DialectRegistry registry; - registry.insert(); context = std::make_unique(); context->appendDialectRegistry(registry); @@ -101,6 +103,18 @@ class EulerSynthesisExactTest } // namespace +/** + * @brief Measures the given qubits and returns the measurement outcomes. + * @param b The `ProgramBuilder` used to perform the measurements. + * @param qubits The qubits to be measured. + * @return The result values. + */ +static SmallVector measureAndReturn(QCOProgramBuilder& b, + ValueRange qubits) { + return to_vector( + llvm::map_range(qubits, [&](Value q) { return b.measure(q).second; })); +} + //===----------------------------------------------------------------------===// // Euler synthesis support //===----------------------------------------------------------------------===// @@ -134,8 +148,8 @@ template } template static void forEachBasis(Fn fn) { - const std::array bases = {"zyz", "zxz", "xzx", "xyx", - "u", "zsxx", "r"}; + constexpr std::array bases = {"zyz", "zxz", "xzx", "xyx", + "u", "zsxx", "r"}; for (const char* basis : bases) { fn(StringRef{basis}); } @@ -361,7 +375,7 @@ INSTANTIATE_TEST_SUITE_P( ZSXXShortcutCase{"ZYZNearZeroTheta", [](MLIRContext*) -> Matrix2x2 { constexpr double tol = - 0.5 * mlir::utils::TOLERANCE; + 0.5 * utils::TOLERANCE; return RZOp::unitaryMatrix(0.4) * RYOp::unitaryMatrix(tol) * RZOp::unitaryMatrix(0.3); @@ -376,23 +390,21 @@ INSTANTIATE_TEST_SUITE_P( ZSXXShortcutCase{"RYNearHalfPi", [](MLIRContext* ctx) -> Matrix2x2 { return rotationMatrix( - ctx, - (std::numbers::pi / 2.0) + - (0.5 * mlir::utils::TOLERANCE)); + ctx, (std::numbers::pi / 2.0) + + (0.5 * utils::TOLERANCE)); }, 2, 1, 0}, ZSXXShortcutCase{"RYNearZero", [](MLIRContext* ctx) -> Matrix2x2 { return rotationMatrix( - ctx, 0.5 * mlir::utils::TOLERANCE); + ctx, 0.5 * utils::TOLERANCE); }, 0, 0, 0}, ZSXXShortcutCase{"RYNearPi", [](MLIRContext* ctx) -> Matrix2x2 { return rotationMatrix( - ctx, - std::numbers::pi - - (0.5 * mlir::utils::TOLERANCE)); + ctx, std::numbers::pi - + (0.5 * utils::TOLERANCE)); }, 1, 0, 1}), [](const testing::TestParamInfo& info) { @@ -456,7 +468,7 @@ TEST(EulerSynthesisTest, RandomReconstructionAllBases) { TEST(EulerAnglesCoverageTest, ParamsZYZUsesOffDiagonal01When10IsNearZero) { Matrix2x2 matrix = RXOp::unitaryMatrix(0.4); matrix(1, 0) = Complex{0.0, 0.0}; - ASSERT_GT(std::abs(matrix(0, 1)), mlir::utils::TOLERANCE); + ASSERT_GT(std::abs(matrix(0, 1)), utils::TOLERANCE); const EulerAngles angles = anglesFromUnitary(matrix, ZYZ); EXPECT_TRUE(std::isfinite(angles.theta)); EXPECT_TRUE(std::isfinite(angles.phi)); @@ -470,9 +482,9 @@ TEST(EulerAnglesCoverageTest, PhaseOnlyDecompositionSkipsRotationGates) { const Matrix2x2 matrix = Matrix2x2::fromElements(scale, 0, 0, scale); ASSERT_FALSE(matrix.isApprox(Matrix2x2::identity())); const EulerAngles angles = anglesFromUnitary(matrix, ZYZ); - EXPECT_LE(std::abs(angles.theta), mlir::utils::TOLERANCE); - EXPECT_LE(std::abs(angles.phi), mlir::utils::TOLERANCE); - EXPECT_LE(std::abs(angles.lambda), mlir::utils::TOLERANCE); + EXPECT_LE(std::abs(angles.theta), utils::TOLERANCE); + EXPECT_LE(std::abs(angles.phi), utils::TOLERANCE); + EXPECT_LE(std::abs(angles.lambda), utils::TOLERANCE); const auto circuit = synthesizeMatrix(fx.ctx(), matrix, ZYZ); ASSERT_TRUE(succeeded(verify(*circuit.mlirModule))); EXPECT_EQ(countZYZGates(circuit.func), 0U); @@ -494,7 +506,7 @@ TEST(EulerAnglesCoverageTest, UBasisNonzeroThetaEmitsSingleUGate) { fx.setUp(); const Matrix2x2 matrix = RYOp::unitaryMatrix(1.2); const EulerAngles angles = anglesFromUnitary(matrix, U); - ASSERT_GT(std::abs(angles.theta), mlir::utils::TOLERANCE); + ASSERT_GT(std::abs(angles.theta), utils::TOLERANCE); expectSynthesizedMatrix(fx.ctx(), matrix, U, [](func::FuncOp funcOp, const Matrix2x2& /*matrix*/) { EXPECT_EQ(countOps(funcOp), 1U); @@ -507,7 +519,7 @@ TEST(EulerAnglesCoverageTest, RBasisNonzeroThetaEmitsThreeRGates) { fx.setUp(); const Matrix2x2 matrix = HOp::getUnitaryMatrix(); const EulerAngles angles = anglesFromUnitary(matrix, R); - ASSERT_GT(std::abs(angles.theta), mlir::utils::TOLERANCE); + ASSERT_GT(std::abs(angles.theta), utils::TOLERANCE); expectSynthesizedMatrix(fx.ctx(), matrix, R, [](func::FuncOp funcOp, const Matrix2x2& /*matrix*/) { EXPECT_EQ(countOps(funcOp), 3U); @@ -517,7 +529,7 @@ TEST(EulerAnglesCoverageTest, RBasisNonzeroThetaEmitsThreeRGates) { TEST(EulerAnglesCoverageTest, Mod2PiMapsPiBoundaryThroughSynthesis) { TestFixture fx; fx.setUp(); - constexpr double eps = 0.5 * mlir::utils::TOLERANCE; + constexpr double eps = 0.5 * utils::TOLERANCE; const Complex global = std::polar(1.0, std::numbers::pi - eps); const Matrix2x2 matrix = Matrix2x2::fromElements(global, 0, 0, global); expectSynthesizedMatrix(fx.ctx(), matrix, U, @@ -532,7 +544,8 @@ TEST(EulerAnglesCoverageTest, Mod2PiPreservesNonFinitePhase) { fx.setUp(); const Matrix2x2 matrix = Matrix2x2::fromElements( Complex{std::numeric_limits::quiet_NaN(), 0}, 0, 0, 1); - EXPECT_NO_FATAL_FAILURE(synthesizeMatrix(fx.ctx(), matrix, ZYZ)); + EXPECT_NO_FATAL_FAILURE(std::ignore = + synthesizeMatrix(fx.ctx(), matrix, ZYZ)); } //===----------------------------------------------------------------------===// @@ -765,53 +778,58 @@ static void runFuseInParent(MLIRContext* ctx, ProgramT program, // --- Fuse program fixtures --- // -static void singleQubitRunWithSingleQubitGate(QCOProgramBuilder& b) { +static SmallVector +singleQubitRunWithSingleQubitGate(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.h(q[0]); q[0] = b.t(q[0]); q[0] = b.rz(0.123, q[0]); - q[0] = b.inv({q[0]}, [&b](ValueRange targets) -> SmallVector { - return {b.sx(targets[0])}; - })[0]; + q[0] = b.inv(q[0], [&b](Value qubit) { return b.sx(qubit); }); q[0] = b.ry(-0.456, q[0]); + return measureAndReturn(b, q.qubits); } -static void singleQubitRunsSplitByTwoQGate(QCOProgramBuilder& b) { +static SmallVector singleQubitRunsSplitByTwoQGate(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); q[0] = b.h(q[0]); q[0] = b.t(q[0]); std::tie(q[0], q[1]) = b.swap(q[0], q[1]); q[0] = b.rz(0.321, q[0]); q[0] = b.sx(q[0]); + return measureAndReturn(b, q.qubits); } -static void singleQubitRunsSplitByBarrier(QCOProgramBuilder& b) { +static SmallVector singleQubitRunsSplitByBarrier(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.h(q[0]); q[0] = b.t(q[0]); q[0] = b.barrier({q[0]})[0]; q[0] = b.rz(0.321, q[0]); q[0] = b.sx(q[0]); + return measureAndReturn(b, q.qubits); } -static void singleNonBasisGate(QCOProgramBuilder& b) { +static SmallVector singleNonBasisGate(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.h(q[0]); + return measureAndReturn(b, q.qubits); } -static void singlePauliX(QCOProgramBuilder& b) { +static SmallVector singlePauliX(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.x(q[0]); + return measureAndReturn(b, q.qubits); } -static void canonicalZYZRun(QCOProgramBuilder& b) { +static SmallVector canonicalZYZRun(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.rz(0.3, q[0]); q[0] = b.ry(0.5, q[0]); q[0] = b.rz(0.7, q[0]); + return measureAndReturn(b, q.qubits); } -static void overlongZYZRun(QCOProgramBuilder& b) { +static SmallVector overlongZYZRun(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.rz(0.3, q[0]); q[0] = b.ry(0.5, q[0]); @@ -819,38 +837,43 @@ static void overlongZYZRun(QCOProgramBuilder& b) { q[0] = b.ry(0.9, q[0]); q[0] = b.rz(1.1, q[0]); q[0] = b.ry(1.3, q[0]); + return measureAndReturn(b, q.qubits); } -static void overlongZSXXMixedPureZRun(QCOProgramBuilder& b) { +static SmallVector overlongZSXXMixedPureZRun(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.sx(q[0]); q[0] = b.rz(std::numbers::pi, q[0]); q[0] = b.sx(q[0]); + return measureAndReturn(b, q.qubits); } -static void singleQubitRunInScfFor(QCOProgramBuilder& b) { +static SmallVector singleQubitRunInScfFor(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.scfFor(0, 1, 1, ValueRange{q[0]}, [&b](Value, ValueRange iterArgs) { - Value wire = iterArgs[0]; - wire = b.h(wire); - wire = b.t(wire); - wire = b.rz(0.123, wire); - return SmallVector{wire}; - }); + auto res = + b.scfFor(0, 1, 1, ValueRange{q[0]}, [&b](Value, ValueRange iterArgs) { + Value wire = iterArgs[0]; + wire = b.h(wire); + wire = b.t(wire); + wire = b.rz(0.123, wire); + return SmallVector{wire}; + }); + return measureAndReturn(b, res); } -static void xInverseTwoX(QCOProgramBuilder& b) { +static SmallVector xInverseTwoX(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.x(q[0]); - q[0] = b.inv({q[0]}, [&b](ValueRange targets) { - Value wire = b.x(targets[0]); - wire = b.x(wire); - return SmallVector{wire}; - })[0]; + q[0] = b.inv(q[0], [&b](Value qubit) { + qubit = b.x(qubit); + return b.x(qubit); + }); q[0] = b.x(q[0]); + return measureAndReturn(b, q.qubits); } -static void inverseMultiQubitBodySingleQubitRun(QCOProgramBuilder& b) { +static SmallVector +inverseMultiQubitBodySingleQubitRun(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); auto outs = b.inv({q[0], q[1]}, [&b](ValueRange targets) -> SmallVector { @@ -860,36 +883,38 @@ static void inverseMultiQubitBodySingleQubitRun(QCOProgramBuilder& b) { }); q[0] = outs[0]; q[1] = outs[1]; + return measureAndReturn(b, q.qubits); } -static void controlledInverseHT(QCOProgramBuilder& b) { +static SmallVector controlledInverseHT(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.ctrl(q[0], q[1], [&b](ValueRange targets) { - auto wire = b.inv({targets[0]}, [&b](ValueRange innerTargets) { - auto inner = b.h(innerTargets[0]); - inner = b.t(inner); - return SmallVector{inner}; - })[0]; - return SmallVector{wire}; + auto res = b.ctrl(q[0], q[1], [&b](Value target) { + return b.inv(target, [&b](Value qubit) { + qubit = b.h(qubit); + return b.t(qubit); + }); }); + return measureAndReturn(b, {res.second}); } -static void controlledH(QCOProgramBuilder& b) { +static SmallVector controlledH(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.ctrl(q[0], q[1], - [&b](ValueRange targets) { return SmallVector{b.h(targets[0])}; }); + auto res = b.ctrl(q[0], q[1], [&b](Value target) { return b.h(target); }); + return measureAndReturn(b, {res.second}); } -static void singleQubitRunsSplitByScfFor(QCOProgramBuilder& b) { +static SmallVector singleQubitRunsSplitByScfFor(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.h(q[0]); q[0] = b.t(q[0]); - b.scfFor(0, 1, 1, ValueRange{q[0]}, [&b](Value, ValueRange iterArgs) { - Value wire = iterArgs[0]; - wire = b.rz(0.321, wire); - wire = b.sx(wire); - return SmallVector{wire}; - }); + auto res = + b.scfFor(0, 1, 1, ValueRange{q[0]}, [&b](Value, ValueRange iterArgs) { + Value wire = iterArgs[0]; + wire = b.rz(0.321, wire); + wire = b.sx(wire); + return SmallVector{wire}; + }); + return measureAndReturn(b, res); } //===----------------------------------------------------------------------===// @@ -910,7 +935,7 @@ TEST(FuseSingleQubitUnitaryRunsTest, FusesProgramsAllBases) { fx.setUp(); struct Case { - void (*program)(QCOProgramBuilder&); + SmallVector (*program)(QCOProgramBuilder&); void (*extra)(func::FuncOp, StringRef); }; const std::array cases = {{ @@ -990,7 +1015,7 @@ TEST(FuseSingleQubitUnitaryRunsTest, DoesNotFuseAcrossBoundariesAllBases) { fx.setUp(); struct Case { - void (*program)(QCOProgramBuilder&); + SmallVector (*program)(QCOProgramBuilder&); void (*check)(func::FuncOp, StringRef, MLIRContext*); }; const std::array cases = {{ diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index 37c175edc8..2b822c6cba 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -790,18 +790,15 @@ TEST_F(NativeGatesetMlirTest, AllowsOpMatchesGateset) { EXPECT_TRUE(spec->allowsOp( UOp::create(builder, loc, q0, 0.1, 0.2, 0.3).getOperation())); - auto cx = CtrlOp::create( - builder, loc, ValueRange{q0}, ValueRange{q1}, - [&builder, &loc](ValueRange targets) -> SmallVector { - return {XOp::create(builder, loc, targets[0]).getOutputQubit(0)}; - }); + auto cx = CtrlOp::create(builder, loc, q0, q1, [&](Value target) { + return XOp::create(builder, loc, target).getOutputQubit(0); + }); EXPECT_TRUE(spec->allowsOp(cx.getOperation())); - auto cxWithInterleavedH = CtrlOp::create( - builder, loc, ValueRange{q0}, ValueRange{q1}, - [&builder, &loc](ValueRange targets) -> SmallVector { - auto wire = XOp::create(builder, loc, targets[0]).getOutputQubit(0); - return {HOp::create(builder, loc, wire).getOutputQubit(0)}; + auto cxWithInterleavedH = + CtrlOp::create(builder, loc, q0, q1, [&](Value target) { + auto wire = XOp::create(builder, loc, target).getOutputQubit(0); + return HOp::create(builder, loc, wire).getOutputQubit(0); }); EXPECT_FALSE(spec->allowsOp(cxWithInterleavedH.getOperation())); @@ -816,11 +813,9 @@ TEST_F(NativeGatesetMlirTest, AllowsOpMatchesGateset) { EXPECT_FALSE( rzSpec->allowsOp(POp::create(builder, loc, q0, 0.3).getOperation())); - auto hCtrl = CtrlOp::create( - builder, loc, ValueRange{q0}, ValueRange{q1}, - [&builder, &loc](ValueRange targets) -> SmallVector { - return {HOp::create(builder, loc, targets[0]).getOutputQubit(0)}; - }); + auto hCtrl = CtrlOp::create(builder, loc, q0, q1, [&](Value target) { + return HOp::create(builder, loc, target).getOutputQubit(0); + }); EXPECT_FALSE(spec->allowsOp(hCtrl.getOperation())); const auto funcTy3 = builder.getFunctionType({qubitTy, qubitTy, qubitTy}, @@ -831,20 +826,17 @@ TEST_F(NativeGatesetMlirTest, AllowsOpMatchesGateset) { Value c0 = entry3->getArgument(0); Value c1 = entry3->getArgument(1); Value target = entry3->getArgument(2); - auto ccx = CtrlOp::create( - builder, loc, ValueRange{c0, c1}, ValueRange{target}, - [&builder, &loc](ValueRange targets) -> SmallVector { - return {XOp::create(builder, loc, targets[0]).getOutputQubit(0)}; + auto ccx = + CtrlOp::create(builder, loc, ValueRange{c0, c1}, target, [&](Value t) { + return XOp::create(builder, loc, t).getOutputQubit(0); }); EXPECT_FALSE(spec->allowsOp(ccx.getOperation())); const auto czSpec = NativeGateset::parse("u,cz"); ASSERT_TRUE(czSpec); - auto cz = CtrlOp::create( - builder, loc, ValueRange{q0}, ValueRange{q1}, - [&builder, &loc](ValueRange targets) -> SmallVector { - return {ZOp::create(builder, loc, targets[0]).getOutputQubit(0)}; - }); + auto cz = CtrlOp::create(builder, loc, q0, q1, [&](Value t) { + return ZOp::create(builder, loc, t).getOutputQubit(0); + }); EXPECT_TRUE(czSpec->allowsOp(cz.getOperation())); EXPECT_FALSE(czSpec->allowsOp(cx.getOperation())); } diff --git a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp index f209d23716..3153febf45 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp @@ -14,23 +14,27 @@ #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Transforms/Mapping/Mapping.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" -#include "mlir/Dialect/QCO/Utils/Algorithms.h" -#include "mlir/Dialect/QCO/Utils/Drivers.h" -#include "mlir/Dialect/QCO/Utils/Qubits.h" +#include "mlir/Dialect/Utils/Utils.h" #include +#include +#include +#include #include #include #include +#include #include #include #include #include +#include #include #include #include -#include +#include +#include #include #include #include @@ -40,67 +44,175 @@ using namespace mlir; using namespace mlir::qco; +using namespace mlir::utils; -using DeviceSpec = std::pair; - -/** - * @returns llvm::success() if all two-qubit gates inside @p region - * fulfill the given coupling constraints. llvm::failure(), otherwise. - */ -static LogicalResult isExecutable(Region& region, const Edges& coupling) { - return walkProgram(region, [&](Operation* curr, const Qubits& qubits) { - if (auto op = dyn_cast(curr)) { - if (isa(op)) { - return WalkResult::advance(); - } - - assert(op.getNumQubits() <= 2 && - "isExecutable: expected two-qubit gate decomposition"); - - if (op.getNumQubits() > 1) { - const auto q0 = cast>(op.getInputQubit(0)); - const auto q1 = cast>(op.getInputQubit(1)); - const auto i0 = qubits.getIndex(q0); - const auto i1 = qubits.getIndex(q1); - - if (!coupling.contains(std::make_pair(i0, i1))) { - return WalkResult::interrupt(); - } - } +namespace { +struct Device { + size_t nqubits{}; + DenseSet> couplingSet; +}; +} // namespace + +/// Return true, if the operations within a region fulfill the given coupling +/// constraints. +static bool +isExecutable(Region& body, DenseMap& m, + const DenseSet>& couplingSet) { + for (Operation& rop : body.getOps()) { + bool executable = true; + TypeSwitch(&rop) + .Case( + [&](StaticOp op) { m.try_emplace(op.getQubit(), op.getIndex()); }) + .Case([&](BarrierOp op) { + for (const auto [pred, succ] : + llvm::zip_equal(op.getInputQubits(), op.getOutputQubits())) { + const auto hw = m.at(pred); + m.try_emplace(succ, hw); + } + }) + .Case([&](UnitaryOpInterface& op) { + assert(op.getNumQubits() <= 2 && "expected two-qubit decomp."); + + if (op.getNumQubits() > 1) { + const auto hwA = m.at(op.getInputQubit(0)); + const auto hwB = m.at(op.getInputQubit(1)); + if (!couplingSet.contains(std::make_pair(hwA, hwB))) { + llvm::dbgs() << "(" << hwA << ", " << hwB << ") " + << "not executable: \n"; + op->dump(); + executable = false; + } + } + + for (const auto [pred, succ] : + llvm::zip_equal(op.getInputQubits(), op.getOutputQubits())) { + const auto hw = m.at(pred); + m.try_emplace(succ, hw); + } + }) + .Case([&](scf::ForOp forOp) { + DenseMap loopM; + for (const auto [init, arg] : + llvm::zip_equal(forOp.getInits(), forOp.getRegionIterArgs())) { + const auto hw = m.at(init); + loopM.try_emplace(arg, hw); + } + + for (OpOperand& operand : forOp.getInitsMutable()) { + const auto pred = operand.get(); + const auto succ = forOp.getTiedLoopResult(&operand); + const auto hw = m.at(pred); + m.try_emplace(succ, hw); + } + + if (!isExecutable(forOp.getRegion(), loopM, couplingSet)) { + executable = false; + return; + } + + for (const auto& [arg, yielded] : llvm::zip_equal( + forOp.getRegionIterArgs(), forOp.getYieldedValues())) { + if (loopM.at(arg) != loopM.at(yielded)) { + llvm::dbgs() << "scf::forOp: layout not restored!\n"; + executable = false; + return; + } + } + }) + .Case([&](qco::IfOp ifOp) { + std::array mappings{DenseMap{}, + DenseMap{}}; + + const std::array regions{&ifOp.getThenRegion(), + &ifOp.getElseRegion()}; + + for (size_t i = 0; i < 2; ++i) { + for (const auto [init, arg] : llvm::zip_equal( + ifOp.getQubits(), regions[i]->getArguments())) { + const auto hw = m.at(init); + mappings[i].try_emplace(arg, hw); + } + } + + for (OpOperand& operand : ifOp.getQubitsMutable()) { + const auto pred = operand.get(); + const auto succ = ifOp.getTiedResult(&operand); + const auto hw = m.at(pred); + m.try_emplace(succ, hw); + } + + std::array, 2> finalPermutation{}; + + for (size_t i = 0; i < 2; ++i) { + Region* body = regions[i]; + if (!isExecutable(*body, mappings[i], couplingSet)) { + executable = false; + return; + } + + Block& block = body->getBlocks().front(); + auto yield = cast(block.getTerminator()); + for (const auto v : yield.getTargets()) { + finalPermutation[i].emplace_back(mappings[i].at(v)); + } + } + + if (finalPermutation[0] != finalPermutation[1]) { + executable = false; + return; + } + }) + .Case([&](auto op) { + const auto pred = op.getQubitIn(); + const auto succ = op.getQubitOut(); + const auto hw = m.at(pred); + m.try_emplace(succ, hw); + }); + + if (!executable) { + return false; } + } - return WalkResult::advance(); - }); + return true; } -/** - * @returns a 9x9 square-grid device. - */ -static DeviceSpec getNineQubitSquareGrid() { - const static Edges COUPLING{{0, 3}, {3, 0}, {0, 1}, {1, 0}, {1, 4}, {4, 1}, - {1, 2}, {2, 1}, {2, 5}, {5, 2}, {3, 6}, {6, 3}, - {3, 4}, {4, 3}, {4, 7}, {7, 4}, {4, 5}, {5, 4}, - {5, 8}, {8, 5}, {6, 7}, {7, 6}, {7, 8}, {8, 7}}; - return std::make_pair(9, COUPLING); +/// Return true, if the entry point fulfills the given coupling constraints. +static bool +isExecutable(func::FuncOp entry, + const DenseSet>& couplingSet) { + DenseMap m; + return isExecutable(entry.getFunctionBody(), m, couplingSet); +} + +/// Return a 9x9 square-grid coupling set. +static Device getNineQubitSquareGrid() { + return {.nqubits = 9, + .couplingSet = {{0, 3}, {3, 0}, {0, 1}, {1, 0}, {1, 4}, {4, 1}, + {1, 2}, {2, 1}, {2, 5}, {5, 2}, {3, 6}, {6, 3}, + {3, 4}, {4, 3}, {4, 7}, {7, 4}, {4, 5}, {5, 4}, + {5, 8}, {8, 5}, {6, 7}, {7, 6}, {7, 8}, {8, 7}}}; } namespace { class MappingPassTest : public testing::Test, - public testing::WithParamInterface { + public testing::WithParamInterface { protected: void SetUp() override { DialectRegistry registry; - registry.insert(); + registry.insert(); context = std::make_unique(); context->appendDialectRegistry(registry); context->loadAllAvailableDialects(); } - static LogicalResult runPass(ModuleOp m, const DeviceSpec& device, - const MappingPassOptions& options) { + static LogicalResult + runPass(ModuleOp m, const DenseSet>& couplingSet, + const MappingPassOptions& options) { PassManager pm(m->getContext()); - pm.addPass(createMappingPass(device.first, device.second, options)); + pm.addPass(createMappingPass(couplingSet, options)); return pm.run(m); } @@ -113,9 +225,7 @@ TEST_P(MappingPassTest, NoEntryPoint) { const auto& device = GetParam(); OwningOpRef m = ModuleOp::create(UnknownLoc::get(context.get())); - - auto res = runPass(m.get(), device, MappingPassOptions{}); - + auto res = runPass(m.get(), device.couplingSet, MappingPassOptions{}); ASSERT_TRUE(res.failed()); } @@ -123,14 +233,17 @@ TEST_P(MappingPassTest, NoQubitAllocations) { const auto& device = GetParam(); QCOProgramBuilder builder(context.get()); - builder.initialize(); + builder.initialize({builder.getI1Type()}); - Value q0 = builder.allocQubit(); + Value q0; + Value c0; + q0 = builder.allocQubit(); q0 = builder.h(q0); + std::tie(q0, c0) = builder.measure(q0); builder.sink(q0); - auto m = builder.finalize(); - auto res = runPass(m.get(), device, MappingPassOptions{}); + auto m = builder.finalize(c0); + auto res = runPass(m.get(), device.couplingSet, MappingPassOptions{}); ASSERT_TRUE(res.failed()); } @@ -139,181 +252,424 @@ TEST_P(MappingPassTest, NoExtractAfterInsert) { const auto& device = GetParam(); QCOProgramBuilder builder(context.get()); - builder.initialize(); + builder.initialize({builder.getI1Type()}); Value tensor0 = builder.qtensorAlloc(1); Value q0; + Value c0; std::tie(tensor0, q0) = builder.qtensorExtract(tensor0, 0); q0 = builder.h(q0); tensor0 = builder.qtensorInsert(q0, tensor0, 0); std::tie(tensor0, q0) = builder.qtensorExtract(tensor0, 0); q0 = builder.x(q0); + std::tie(q0, c0) = builder.measure(q0); tensor0 = builder.qtensorInsert(q0, tensor0, 0); builder.qtensorDealloc(tensor0); - auto m = builder.finalize(); - auto res = runPass(m.get(), device, MappingPassOptions{}); + auto m = builder.finalize(c0); + auto res = runPass(m.get(), device.couplingSet, MappingPassOptions{}); ASSERT_TRUE(res.failed()); } TEST_P(MappingPassTest, TooManyQubitsForArch) { const auto& device = GetParam(); + const auto size = static_cast(device.nqubits) + 1; + + SmallVector bits(size); + SmallVector qubits(size); QCOProgramBuilder builder(context.get()); - builder.initialize(); - - int64_t nqubits = static_cast(device.first) + 1; - Value tensor = builder.qtensorAlloc(nqubits); - SmallVector qubits(nqubits); - for (int64_t i = 0; i < nqubits; ++i) { - Value qi; - std::tie(tensor, qi) = builder.qtensorExtract(tensor, i); - qi = builder.h(qi); - qubits[i] = qi; + builder.initialize(SmallVector(size, builder.getI1Type())); + + Value tensor = builder.qtensorAlloc(size); + + for (int64_t i = 0; i < size; ++i) { + std::tie(tensor, qubits[i]) = builder.qtensorExtract(tensor, i); + qubits[i] = builder.h(qubits[i]); + std::tie(qubits[i], bits[i]) = builder.measure(qubits[i]); } - for (int64_t i = 0; i < nqubits; ++i) { + for (int64_t i = 0; i < size; ++i) { tensor = builder.qtensorInsert(qubits[i], tensor, i); } builder.qtensorDealloc(tensor); - auto m = builder.finalize(); - auto res = runPass(m.get(), device, MappingPassOptions{}); + auto m = builder.finalize(bits); + auto res = runPass(m.get(), device.couplingSet, MappingPassOptions{}); ASSERT_TRUE(res.failed()); } TEST_P(MappingPassTest, GHZ) { const auto& device = GetParam(); + const int64_t size = 3; + + SmallVector qubits(size); + SmallVector bits(size); QCOProgramBuilder builder(context.get()); - builder.initialize(); + builder.initialize(SmallVector(3, builder.getI1Type())); Value tensor = builder.qtensorAlloc(3); + std::tie(tensor, qubits[0]) = builder.qtensorExtract(tensor, 0); + std::tie(tensor, qubits[1]) = builder.qtensorExtract(tensor, 1); + std::tie(tensor, qubits[2]) = builder.qtensorExtract(tensor, 2); + + qubits[0] = builder.h(qubits[0]); + std::tie(qubits[0], qubits[1]) = builder.cx(qubits[0], qubits[1]); + std::tie(qubits[0], qubits[2]) = builder.cx(qubits[0], qubits[2]); + + std::tie(qubits[0], bits[0]) = builder.measure(qubits[0]); + std::tie(qubits[1], bits[1]) = builder.measure(qubits[1]); + std::tie(qubits[2], bits[2]) = builder.measure(qubits[2]); + + tensor = builder.qtensorInsert(qubits[0], tensor, 0); + tensor = builder.qtensorInsert(qubits[1], tensor, 1); + tensor = builder.qtensorInsert(qubits[2], tensor, 2); + + builder.qtensorDealloc(tensor); + + auto m = builder.finalize(bits); + auto res = runPass(m.get(), device.couplingSet, MappingPassOptions{}); + auto entry = getEntryPoint(m.get()); + ASSERT_TRUE(res.succeeded()); + EXPECT_TRUE(isExecutable(entry, device.couplingSet)); +} + +TEST_P(MappingPassTest, GHZUnrolled) { + const auto& device = GetParam(); + const auto size = static_cast(device.nqubits); + + SmallVector bits(size); + + PassManager pm(context.get()); + pm.addNestedPass(createQuantumLoopUnroll()); + pm.addPass(createCSEPass()); + pm.addPass(createCanonicalizerPass()); + pm.addPass(createMappingPass(device.couplingSet, MappingPassOptions{})); + + QCOProgramBuilder builder(context.get()); + builder.initialize(SmallVector(size, builder.getI1Type())); + + Value tensor = builder.qtensorAlloc(size); Value q0; std::tie(tensor, q0) = builder.qtensorExtract(tensor, 0); + q0 = builder.h(q0); + tensor = builder.qtensorInsert(q0, tensor, 0); + tensor = builder.scfFor( + 1, size, 1, {tensor}, [&builder](Value iv, ValueRange iterArgs) { + Value loopTensor = iterArgs[0]; + Value ctrl; + Value targ; - Value q1; - std::tie(tensor, q1) = builder.qtensorExtract(tensor, 1); + std::tie(loopTensor, ctrl) = builder.qtensorExtract(loopTensor, 0); + std::tie(loopTensor, targ) = builder.qtensorExtract(loopTensor, iv); - Value q2; - std::tie(tensor, q2) = builder.qtensorExtract(tensor, 2); + std::tie(ctrl, targ) = builder.cx(ctrl, targ); - q0 = builder.h(q0); - std::tie(q0, q1) = builder.cx(q0, q1); - std::tie(q0, q2) = builder.cx(q0, q2); + loopTensor = builder.qtensorInsert(ctrl, loopTensor, 0); + loopTensor = builder.qtensorInsert(targ, loopTensor, iv); + + return SmallVector{loopTensor}; + })[0]; + + for (int64_t i = 0; i < size; ++i) { + Value q; + std::tie(tensor, q) = builder.qtensorExtract(tensor, i); + std::tie(q, bits[i]) = builder.measure(q); + tensor = builder.qtensorInsert(q, tensor, i); + } + + builder.qtensorDealloc(tensor); + + auto m = builder.finalize(bits); + auto res = pm.run(m.get()); + auto entry = getEntryPoint(m.get()); + + ASSERT_TRUE(res.succeeded()); + EXPECT_TRUE(isExecutable(entry, device.couplingSet)); +} + +TEST_P(MappingPassTest, GroverLike) { + const auto& device = GetParam(); + const int64_t size = 5; + + SmallVector qubits(size); + SmallVector bits(size); + + PassManager pm(context.get()); + pm.addPass(createMappingPass(device.couplingSet, MappingPassOptions{})); + + QCOProgramBuilder builder(context.get()); + builder.initialize(SmallVector(5, builder.getI1Type())); + + Value tensor = builder.qtensorAlloc(4); + Value flagTensor = builder.qtensorAlloc(1); + + std::tie(tensor, qubits[0]) = builder.qtensorExtract(tensor, 0); + std::tie(tensor, qubits[1]) = builder.qtensorExtract(tensor, 1); + std::tie(tensor, qubits[2]) = builder.qtensorExtract(tensor, 2); + std::tie(tensor, qubits[3]) = builder.qtensorExtract(tensor, 3); + std::tie(flagTensor, qubits[4]) = builder.qtensorExtract(flagTensor, 0); + + qubits[0] = builder.h(qubits[0]); + qubits[1] = builder.h(qubits[1]); + qubits[2] = builder.h(qubits[2]); + qubits[3] = builder.h(qubits[3]); + qubits[4] = builder.x(qubits[4]); + + qubits = + builder.scfFor(1, 3, 1, qubits, [&builder](Value, ValueRange iterArgs) { + Value iterQ0 = iterArgs[0]; + Value iterQ1 = iterArgs[1]; + Value iterQ2 = iterArgs[2]; + Value iterQ3 = iterArgs[3]; + Value iterFlag = iterArgs[4]; + + std::tie(iterQ0, iterQ2) = builder.cx(iterQ0, iterQ2); + std::tie(iterQ2, iterQ3) = builder.cx(iterQ2, iterQ3); + std::tie(iterQ3, iterQ0) = builder.cx(iterQ3, iterQ0); + std::tie(iterQ0, iterFlag) = builder.cx(iterQ0, iterFlag); + + return SmallVector{iterQ0, iterQ1, iterQ2, iterQ3, iterFlag}; + }); + qubits = builder.barrier(qubits); + + for (int64_t i = 0; i < size; ++i) { + std::tie(qubits[i], bits[i]) = builder.measure(qubits[i]); + } + + tensor = builder.qtensorInsert(qubits[0], tensor, 0); + tensor = builder.qtensorInsert(qubits[1], tensor, 1); + tensor = builder.qtensorInsert(qubits[2], tensor, 2); + tensor = builder.qtensorInsert(qubits[3], tensor, 3); + flagTensor = builder.qtensorInsert(qubits[4], flagTensor, 0); - tensor = builder.qtensorInsert(q0, tensor, 0); - tensor = builder.qtensorInsert(q1, tensor, 1); - tensor = builder.qtensorInsert(q2, tensor, 2); builder.qtensorDealloc(tensor); + builder.qtensorDealloc(flagTensor); - auto m = builder.finalize(); - auto res = runPass(m.get(), device, MappingPassOptions{}); + auto m = builder.finalize(bits); + auto res = pm.run(m.get()); auto entry = getEntryPoint(m.get()); ASSERT_TRUE(res.succeeded()); - EXPECT_TRUE(isExecutable(entry.getFunctionBody(), device.second).succeeded()); + EXPECT_TRUE(isExecutable(entry, device.couplingSet)); +} + +TEST_P(MappingPassTest, ParallelLoops) { + const auto& device = GetParam(); + constexpr int64_t size = 6; + + SmallVector qubits(size); + SmallVector bits(size); + + PassManager pm(context.get()); + pm.addPass(createMappingPass(device.couplingSet, MappingPassOptions{})); + + QCOProgramBuilder builder(context.get()); + builder.initialize(SmallVector(size, builder.getI1Type())); + + Value tensor = builder.qtensorAlloc(size); + for (int64_t i = 0; i < size; ++i) { + std::tie(tensor, qubits[i]) = builder.qtensorExtract(tensor, i); + qubits[i] = builder.h(qubits[i]); + } + + const auto upForResults = + builder.scfFor(1, 3, 1, {qubits[0], qubits[1], qubits[2]}, + [&builder](Value, ValueRange iterArgs) { + Value iterQ0 = iterArgs[0]; + Value iterQ1 = iterArgs[1]; + Value iterQ2 = iterArgs[2]; + + std::tie(iterQ0, iterQ1) = builder.cx(iterQ0, iterQ1); + iterQ0 = builder.h(iterQ0); + std::tie(iterQ0, iterQ1) = builder.cz(iterQ0, iterQ1); + std::tie(iterQ1, iterQ2) = builder.cz(iterQ1, iterQ2); + std::tie(iterQ0, iterQ2) = builder.cx(iterQ0, iterQ2); + + return SmallVector{iterQ0, iterQ1, iterQ2}; + }); + + qubits[0] = upForResults[0]; + qubits[1] = upForResults[1]; + qubits[2] = upForResults[2]; + + const auto downForResults = + builder.scfFor(1, 3, 1, {qubits[3], qubits[4], qubits[5]}, + [&builder](Value, ValueRange iterArgs) { + Value iterQ0 = iterArgs[0]; + Value iterQ1 = iterArgs[1]; + Value iterQ2 = iterArgs[2]; + + std::tie(iterQ0, iterQ1) = builder.cx(iterQ0, iterQ1); + iterQ0 = builder.h(iterQ0); + std::tie(iterQ1, iterQ2) = builder.cz(iterQ1, iterQ2); + std::tie(iterQ0, iterQ1) = builder.cz(iterQ0, iterQ1); + std::tie(iterQ0, iterQ2) = builder.cx(iterQ0, iterQ2); + + return SmallVector{iterQ0, iterQ1, iterQ2}; + }); + + qubits[3] = downForResults[0]; + qubits[4] = downForResults[1]; + qubits[5] = downForResults[2]; + + qubits = builder.barrier(qubits); + + for (int64_t i = 0; i < size; ++i) { + std::tie(qubits[i], bits[i]) = builder.measure(qubits[i]); + qubits[i] = builder.h(qubits[i]); + } + + for (int64_t i = 0; i < size; ++i) { + tensor = builder.qtensorInsert(qubits[i], tensor, i); + } + + builder.qtensorDealloc(tensor); + + auto m = builder.finalize(bits); + auto res = pm.run(m.get()); + auto entry = getEntryPoint(m.get()); + + ASSERT_TRUE(res.succeeded()); + EXPECT_TRUE(isExecutable(entry, device.couplingSet)); } TEST_P(MappingPassTest, Sabre) { const auto& device = GetParam(); + constexpr int64_t size = 6; + + SmallVector qubits(size); + SmallVector bits(size); QCOProgramBuilder builder(context.get()); - builder.initialize(); + builder.initialize(SmallVector(6, builder.getI1Type())); Value tensorUp = builder.qtensorAlloc(4); Value tensorDown = builder.qtensorAlloc(2); - Value q0; - std::tie(tensorUp, q0) = builder.qtensorExtract(tensorUp, 0); + std::tie(tensorUp, qubits[0]) = builder.qtensorExtract(tensorUp, 0); + std::tie(tensorUp, qubits[1]) = builder.qtensorExtract(tensorUp, 1); + std::tie(tensorUp, qubits[2]) = builder.qtensorExtract(tensorUp, 2); + std::tie(tensorUp, qubits[3]) = builder.qtensorExtract(tensorUp, 3); + std::tie(tensorDown, qubits[4]) = builder.qtensorExtract(tensorDown, 0); + std::tie(tensorDown, qubits[5]) = builder.qtensorExtract(tensorDown, 1); - Value q1; - std::tie(tensorUp, q1) = builder.qtensorExtract(tensorUp, 1); + qubits[0] = builder.h(qubits[0]); + qubits[1] = builder.h(qubits[1]); + qubits[4] = builder.h(qubits[4]); - Value q2; - std::tie(tensorUp, q2) = builder.qtensorExtract(tensorUp, 2); + qubits[0] = builder.z(qubits[0]); + std::tie(qubits[1], qubits[2]) = builder.cx(qubits[1], qubits[2]); + std::tie(qubits[4], qubits[5]) = builder.cx(qubits[4], qubits[5]); - Value q3; - std::tie(tensorUp, q3) = builder.qtensorExtract(tensorUp, 3); + std::tie(qubits[0], qubits[1]) = builder.cx(qubits[0], qubits[1]); - Value q4; - std::tie(tensorDown, q4) = builder.qtensorExtract(tensorDown, 0); + qubits[0] = builder.h(qubits[0]); + qubits[1] = builder.y(qubits[1]); + std::tie(qubits[0], qubits[1]) = builder.cx(qubits[0], qubits[1]); - Value q5; - std::tie(tensorDown, q5) = builder.qtensorExtract(tensorDown, 1); + std::tie(qubits[2], qubits[3]) = builder.cx(qubits[2], qubits[3]); - q0 = builder.h(q0); - q1 = builder.h(q1); - q4 = builder.h(q4); + qubits[2] = builder.h(qubits[2]); + qubits[3] = builder.h(qubits[3]); - q0 = builder.z(q0); - std::tie(q1, q2) = builder.cx(q1, q2); - std::tie(q4, q5) = builder.cx(q4, q5); + std::tie(qubits[1], qubits[2]) = builder.cx(qubits[1], qubits[2]); + std::tie(qubits[3], qubits[5]) = builder.cx(qubits[3], qubits[5]); - std::tie(q0, q1) = builder.cx(q0, q1); + qubits[3] = builder.z(qubits[3]); - q0 = builder.h(q0); - q1 = builder.y(q1); - std::tie(q0, q1) = builder.cx(q0, q1); + std::tie(qubits[3], qubits[4]) = builder.cx(qubits[3], qubits[4]); - std::tie(q2, q3) = builder.cx(q2, q3); + std::tie(qubits[3], qubits[0]) = builder.cx(qubits[3], qubits[0]); - q2 = builder.h(q2); - q3 = builder.h(q3); + qubits = builder.barrier(qubits); - std::tie(q1, q2) = builder.cx(q1, q2); - std::tie(q3, q5) = builder.cx(q3, q5); + for (int64_t i = 0; i < size; ++i) { + std::tie(qubits[i], bits[i]) = builder.measure(qubits[i]); + } - q3 = builder.z(q3); + tensorUp = builder.qtensorInsert(qubits[0], tensorUp, 0); + tensorUp = builder.qtensorInsert(qubits[1], tensorUp, 1); + tensorUp = builder.qtensorInsert(qubits[2], tensorUp, 2); + tensorUp = builder.qtensorInsert(qubits[3], tensorUp, 3); + tensorDown = builder.qtensorInsert(qubits[4], tensorDown, 0); + tensorDown = builder.qtensorInsert(qubits[5], tensorDown, 1); + builder.qtensorDealloc(tensorUp); + builder.qtensorDealloc(tensorDown); - std::tie(q3, q4) = builder.cx(q3, q4); + auto m = builder.finalize(bits); + auto res = runPass(m.get(), device.couplingSet, MappingPassOptions{}); + auto entry = getEntryPoint(m.get()); + + ASSERT_TRUE(res.succeeded()); + EXPECT_TRUE(isExecutable(entry, device.couplingSet)); +} - std::tie(q3, q0) = builder.cx(q3, q0); +TEST_P(MappingPassTest, RandomOrderGHZ) { + const auto& device = GetParam(); + constexpr int64_t size = 9; - ValueRange out = builder.barrier({q0, q1, q2, q3, q4, q5}); - q0 = out[0]; - q1 = out[1]; - q2 = out[2]; - q3 = out[3]; - q4 = out[4]; - q5 = out[5]; + SmallVector qubits(size); + SmallVector bits(size); - Value c0; - Value c1; - Value c2; - Value c3; - Value c4; - Value c5; + QCOProgramBuilder builder(context.get()); + builder.initialize(SmallVector(size, builder.getI1Type())); - std::tie(q0, c0) = builder.measure(q0); - std::tie(q1, c1) = builder.measure(q1); - std::tie(q2, c2) = builder.measure(q2); - std::tie(q3, c3) = builder.measure(q3); - std::tie(q4, c4) = builder.measure(q4); - std::tie(q5, c5) = builder.measure(q5); - - tensorUp = builder.qtensorInsert(q0, tensorUp, 0); - tensorUp = builder.qtensorInsert(q1, tensorUp, 1); - tensorUp = builder.qtensorInsert(q2, tensorUp, 2); - tensorUp = builder.qtensorInsert(q3, tensorUp, 3); - tensorDown = builder.qtensorInsert(q4, tensorDown, 0); - tensorDown = builder.qtensorInsert(q5, tensorDown, 1); - builder.qtensorDealloc(tensorUp); - builder.qtensorDealloc(tensorDown); + Value tensor = builder.qtensorAlloc(size); + for (int64_t i = 0; i < size; ++i) { + std::tie(tensor, qubits[i]) = builder.qtensorExtract(tensor, i); + } + + qubits[0] = builder.h(qubits[0]); + std::tie(qubits[0], bits[0]) = builder.measure(qubits[0]); + + qubits = builder.qcoIf( + bits[0], qubits, + [&](ValueRange args) { + SmallVector values(args); + values[0] = builder.h(values[0]); + for (size_t i = 1; i < size; ++i) { + std::tie(values[0], values[i]) = builder.cx(values[0], values[i]); + } + return values; + }, + [&](ValueRange args) { + SmallVector values(args); + values[size - 1] = builder.h(values[size - 1]); + for (size_t i = size - 1; i > 0; --i) { + std::tie(values[8], values[i - 1]) = + builder.cx(values[8], values[i - 1]); + } + return values; + }); + + qubits = builder.barrier(qubits); + + for (int64_t i = 0; i < size; ++i) { + std::tie(qubits[i], bits[i]) = builder.measure(qubits[i]); + } + + for (int64_t i = 0; i < size; ++i) { + tensor = builder.qtensorInsert(qubits[i], tensor, i); + } + + builder.qtensorDealloc(tensor); - auto m = builder.finalize(); - auto res = runPass(m.get(), device, MappingPassOptions{}); + auto m = builder.finalize(bits); + auto res = + runPass(m.get(), device.couplingSet, MappingPassOptions{.ntrials = 1}); auto entry = getEntryPoint(m.get()); ASSERT_TRUE(res.succeeded()); - EXPECT_TRUE(isExecutable(entry.getFunctionBody(), device.second).succeeded()); + EXPECT_TRUE(isExecutable(entry, device.couplingSet)); } INSTANTIATE_TEST_SUITE_P(NineQubitSquareGrid, MappingPassTest, diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp index 207363c98b..e96e6ccfd1 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -48,7 +49,13 @@ using namespace mlir; using namespace mlir::qco; -using ProgramFn = void (*)(mlir::qc::QCProgramBuilder&); +using ProgramFn = SmallVector (*)(mlir::qc::QCProgramBuilder&); + +static SmallVector measureAndReturn(mlir::qc::QCProgramBuilder& b, + ValueRange qubits) { + return llvm::to_vector( + llvm::map_range(qubits, [&](Value q) { return b.measure(q); })); +} // --- Native-gateset membership check ------------------------------------- // @@ -246,14 +253,15 @@ computeUnitaryFromQcoModule(const OwningOpRef& moduleOp) { // A handful of circuits, which are crossed with the gateset table below. /// A bare SWAP (three-entangler class), the canonical two-qubit decomposition. -static void swapTwoQ(mlir::qc::QCProgramBuilder& b) { +static SmallVector swapTwoQ(mlir::qc::QCProgramBuilder& b) { const auto q0 = b.allocQubit(); const auto q1 = b.allocQubit(); b.swap(q0, q1); + return measureAndReturn(b, {q0, q1}); } /// Rich single-qubit variety on both wires, followed by a two-qubit entangler. -static void broadOneQThenCz(mlir::qc::QCProgramBuilder& b) { +static SmallVector broadOneQThenCz(mlir::qc::QCProgramBuilder& b) { const auto q0 = b.allocQubit(); const auto q1 = b.allocQubit(); b.x(q0); @@ -264,10 +272,11 @@ static void broadOneQThenCz(mlir::qc::QCProgramBuilder& b) { b.ry(-0.47, q1); b.rz(0.29, q0); b.cz(q0, q1); + return measureAndReturn(b, {q0, q1}); } /// Long single-qubit run on one wire, then an entangler (Euler-run fusion). -static void hstycxTwoQ(mlir::qc::QCProgramBuilder& b) { +static SmallVector hstycxTwoQ(mlir::qc::QCProgramBuilder& b) { const auto q0 = b.allocQubit(); const auto q1 = b.allocQubit(); b.h(q0); @@ -275,10 +284,11 @@ static void hstycxTwoQ(mlir::qc::QCProgramBuilder& b) { b.t(q0); b.y(q0); b.cx(q0, q1); + return measureAndReturn(b, {q0, q1}); } /// Zero-angle rotations that must canonicalize away before the entangler. -static void zeroAngleThenCz(mlir::qc::QCProgramBuilder& b) { +static SmallVector zeroAngleThenCz(mlir::qc::QCProgramBuilder& b) { const auto q0 = b.allocQubit(); const auto q1 = b.allocQubit(); b.rx(0.0, q0); @@ -286,44 +296,49 @@ static void zeroAngleThenCz(mlir::qc::QCProgramBuilder& b) { b.rz(0.0, q0); b.p(0.0, q1); b.cz(q0, q1); + return measureAndReturn(b, {q0, q1}); } /// Single-qubit gates surrounding an entangler on both sides. -static void hCxSq1(mlir::qc::QCProgramBuilder& b) { +static SmallVector hCxSq1(mlir::qc::QCProgramBuilder& b) { const auto q0 = b.allocQubit(); const auto q1 = b.allocQubit(); b.h(q0); b.cx(q0, q1); b.s(q1); + return measureAndReturn(b, {q0, q1}); } /// Three-qubit program with chained entanglers on overlapping pairs. -static void threeQGhz(mlir::qc::QCProgramBuilder& b) { +static SmallVector threeQGhz(mlir::qc::QCProgramBuilder& b) { const auto q0 = b.allocQubit(); const auto q1 = b.allocQubit(); const auto q2 = b.allocQubit(); b.h(q0); b.cx(q0, q1); b.cx(q1, q2); + return measureAndReturn(b, {q0, q1, q2}); } /// Single-qubit gates wrapped in an inverse modifier (no entangler). -static void inverseTwoX(mlir::qc::QCProgramBuilder& b) { +static SmallVector inverseTwoX(mlir::qc::QCProgramBuilder& b) { const auto q0 = b.allocQubit(); - b.inv({q0}, [&](mlir::ValueRange qubits) { - b.x(qubits[0]); - b.x(qubits[0]); + b.inv(q0, [&](Value qubit) { + b.x(qubit); + b.x(qubit); }); + return measureAndReturn(b, {q0}); } /// A controlled two-gate body that must be synthesized as a two-qubit unitary. -static void controlledXH(mlir::qc::QCProgramBuilder& b) { +static SmallVector controlledXH(mlir::qc::QCProgramBuilder& b) { const auto q0 = b.allocQubit(); const auto q1 = b.allocQubit(); - b.ctrl(q0, {q1}, [&](ValueRange targets) { - b.x(targets[0]); - b.h(targets[0]); + b.ctrl(q0, q1, [&](Value target) { + b.x(target); + b.h(target); }); + return measureAndReturn(b, {q0, q1}); } // --- Fusion-window circuits ---------------------------------------------- // @@ -331,14 +346,16 @@ static void controlledXH(mlir::qc::QCProgramBuilder& b) { // These probe window geometry (where fusion starts/stops), so they run on a // single fixed gateset rather than the full table. -static void fusionCxCx(mlir::qc::QCProgramBuilder& b) { +static SmallVector fusionCxCx(mlir::qc::QCProgramBuilder& b) { const auto q0 = b.allocQubit(); const auto q1 = b.allocQubit(); b.cx(q0, q1); b.cx(q0, q1); + return measureAndReturn(b, {q0, q1}); } -static void fusionHCxInterleavedTCx(mlir::qc::QCProgramBuilder& b) { +static SmallVector +fusionHCxInterleavedTCx(mlir::qc::QCProgramBuilder& b) { const auto q0 = b.allocQubit(); const auto q1 = b.allocQubit(); b.h(q0); @@ -346,88 +363,103 @@ static void fusionHCxInterleavedTCx(mlir::qc::QCProgramBuilder& b) { b.t(q1); b.s(q0); b.cx(q0, q1); + return measureAndReturn(b, {q0, q1}); } -static void fusionThreeLineCx(mlir::qc::QCProgramBuilder& b) { +static SmallVector fusionThreeLineCx(mlir::qc::QCProgramBuilder& b) { const auto q0 = b.allocQubit(); const auto q1 = b.allocQubit(); const auto q2 = b.allocQubit(); b.cx(q0, q1); b.cx(q1, q2); b.cx(q0, q1); + return measureAndReturn(b, {q0, q1, q2}); } -static void fusionCxRSharedOtherPair(mlir::qc::QCProgramBuilder& b) { +static SmallVector +fusionCxRSharedOtherPair(mlir::qc::QCProgramBuilder& b) { const auto q0 = b.allocQubit(); const auto q1 = b.allocQubit(); const auto q2 = b.allocQubit(); b.cx(q0, q1); b.rz(0.17, q1); b.cx(q1, q2); + return measureAndReturn(b, {q0, q1, q2}); } -static void fusionCxBarrierCx(mlir::qc::QCProgramBuilder& b) { +static SmallVector fusionCxBarrierCx(mlir::qc::QCProgramBuilder& b) { const auto q0 = b.allocQubit(); const auto q1 = b.allocQubit(); b.cx(q0, q1); b.barrier({q0, q1}); b.cx(q0, q1); + return measureAndReturn(b, {q0, q1}); } /// A single-wire barrier between two entanglers: a non-walkable single-qubit /// shell terminates the run scan on wire A (and breaks the run-start chain of /// the second entangler), so neither entangler fuses. -static void fusionCxSingleWireBarrierCx(mlir::qc::QCProgramBuilder& b) { +static SmallVector +fusionCxSingleWireBarrierCx(mlir::qc::QCProgramBuilder& b) { const auto q0 = b.allocQubit(); const auto q1 = b.allocQubit(); b.cx(q0, q1); b.barrier({q0}); b.cx(q0, q1); + return measureAndReturn(b, {q0, q1}); } /// An entangler followed by a three-qubit gate sharing both run wires: the run /// scan must stop at the wider gate (it is neither a single- nor a two-qubit /// run member) and leave it untouched, since gates on more than two qubits are /// out of scope for this pass rather than a failure. -static void fusionCxThenMultiControlledX(mlir::qc::QCProgramBuilder& b) { +static SmallVector +fusionCxThenMultiControlledX(mlir::qc::QCProgramBuilder& b) { const auto q0 = b.allocQubit(); const auto q1 = b.allocQubit(); const auto q2 = b.allocQubit(); b.cx(q0, q1); b.mcx({q0, q1}, q2); + return measureAndReturn(b, {q0, q1, q2}); } -static void fusionSwapCxPattern(mlir::qc::QCProgramBuilder& b) { +static SmallVector fusionSwapCxPattern(mlir::qc::QCProgramBuilder& b) { const auto q0 = b.allocQubit(); const auto q1 = b.allocQubit(); b.cx(q0, q1); b.cx(q1, q0); b.cx(q0, q1); + return measureAndReturn(b, {q0, q1}); } -static void fusionOffMenuGateInWindow(mlir::qc::QCProgramBuilder& b) { +static SmallVector +fusionOffMenuGateInWindow(mlir::qc::QCProgramBuilder& b) { const auto q0 = b.allocQubit(); const auto q1 = b.allocQubit(); b.cx(q0, q1); b.h(q0); b.cx(q0, q1); + return measureAndReturn(b, {q0, q1}); } -static void fusionDualWireOneQBetweenCx(mlir::qc::QCProgramBuilder& b) { +static SmallVector +fusionDualWireOneQBetweenCx(mlir::qc::QCProgramBuilder& b) { const auto q0 = b.allocQubit(); const auto q1 = b.allocQubit(); b.cx(q0, q1); b.rz(0.11, q0); b.ry(0.22, q1); b.cx(q0, q1); + return measureAndReturn(b, {q0, q1}); } -static void determinismSwap(mlir::qc::QCProgramBuilder& b) { +static Value determinismSwap(mlir::qc::QCProgramBuilder& b) { const auto q0 = b.allocQubit(); const auto q1 = b.allocQubit(); b.swap(q0, q1); b.dealloc(q0); b.dealloc(q1); + return b.intConstant(0); } namespace { @@ -537,6 +569,17 @@ class FuseTwoQubitUnitaryRunsPassTest : public testing::Test { EXPECT_TRUE(failed(pm.run(*moduleOp))); } + void expectSynthesisFailure(Value (*program)(mlir::qc::QCProgramBuilder&), + StringRef nativeGates) { + auto moduleOp = mlir::qc::QCProgramBuilder::build(context.get(), program); + PassManager pm(moduleOp->getContext()); + pm.addPass(createQCToQCO()); + pm.addPass(createFuseTwoQubitUnitaryRuns(FuseTwoQubitUnitaryRunsOptions{ + .nativeGates = nativeGates.str(), + })); + EXPECT_TRUE(failed(pm.run(*moduleOp))); + } + void expectTwoQFusePreservesUnitary(ProgramFn program, StringRef nativeGates) { auto expected = mlir::qc::QCProgramBuilder::build(context.get(), program); diff --git a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_hadamard_lifting.cpp b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_hadamard_lifting.cpp index 616acd3a58..5653f4f36c 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_hadamard_lifting.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_hadamard_lifting.cpp @@ -311,11 +311,10 @@ TEST_F(QCOHadamardLiftingTest, liftHadamardOverCNOTGate) { TEST_F(QCOHadamardLiftingTest, liftHadamardOverMultipleControlledXGate) { auto q = programBuilder.allocQubitRegister(3); const auto b = programBuilder.allocClassicalBitRegister(1); - auto [q12, q0] = - programBuilder.ctrl({q[1], q[2]}, {q[0]}, [&](const ValueRange target) { - return SmallVector{programBuilder.x(target[0])}; - }); - q[1] = programBuilder.h(q0[0]); + auto [q12, q0] = programBuilder.ctrl({q[1], q[2]}, q[0], [&](Value target) { + return programBuilder.x(target); + }); + q[1] = programBuilder.h(q0); programBuilder.measure(q[1], b[0]); module = programBuilder.finalize(); @@ -323,11 +322,11 @@ TEST_F(QCOHadamardLiftingTest, liftHadamardOverMultipleControlledXGate) { const auto bRef = referenceBuilder.allocClassicalBitRegister(1); qRef[0] = referenceBuilder.h(qRef[0]); qRef[1] = referenceBuilder.h(qRef[1]); - auto [q02Ref, q1Ref] = referenceBuilder.ctrl( - {qRef[0], qRef[2]}, {qRef[1]}, [&](const ValueRange target) { - return SmallVector{referenceBuilder.x(target[0])}; + auto [q02Ref, q1Ref] = + referenceBuilder.ctrl({qRef[0], qRef[2]}, qRef[1], [&](Value target) { + return referenceBuilder.x(target); }); - referenceBuilder.h(q1Ref[0]); + referenceBuilder.h(q1Ref); referenceBuilder.measure(q02Ref[0], bRef[0]); reference = referenceBuilder.finalize(); @@ -388,11 +387,10 @@ TEST_F(QCOHadamardLiftingTest, q[0] = programBuilder.h(q0); programBuilder.measure(q[0], b[0]); programBuilder.measure(q1, b[1]); - auto [q34, q2] = - programBuilder.ctrl({q[3], q[4]}, {q[2]}, [&](const ValueRange target) { - return SmallVector{programBuilder.x(target[0])}; - }); - q[2] = programBuilder.h(q2[0]); + auto [q34, q2] = programBuilder.ctrl({q[3], q[4]}, q[2], [&](Value target) { + return programBuilder.x(target); + }); + q[2] = programBuilder.h(q2); programBuilder.measure(q[2], b[2]); programBuilder.measure(q34[0], b[3]); programBuilder.s(q34[1]); @@ -406,11 +404,11 @@ TEST_F(QCOHadamardLiftingTest, referenceBuilder.measure(qRef1, bRef[1]); qRef[2] = referenceBuilder.h(qRef[2]); qRef[4] = referenceBuilder.h(qRef[4]); - auto [qRef32, qRef4] = referenceBuilder.ctrl( - {qRef[3], qRef[2]}, {qRef[4]}, [&](const ValueRange target) { - return SmallVector{referenceBuilder.x(target[0])}; + auto [qRef32, qRef4] = + referenceBuilder.ctrl({qRef[3], qRef[2]}, qRef[4], [&](Value target) { + return referenceBuilder.x(target); }); - qRef[4] = referenceBuilder.h(qRef4[0]); + qRef[4] = referenceBuilder.h(qRef4); referenceBuilder.measure(qRef32[1], bRef[2]); referenceBuilder.measure(qRef32[0], bRef[3]); referenceBuilder.s(qRef[4]); diff --git a/mlir/unittests/Dialect/QCO/Utils/test_drivers.cpp b/mlir/unittests/Dialect/QCO/Utils/test_drivers.cpp index c9f84fefd0..7bbc2437dd 100644 --- a/mlir/unittests/Dialect/QCO/Utils/test_drivers.cpp +++ b/mlir/unittests/Dialect/QCO/Utils/test_drivers.cpp @@ -12,7 +12,6 @@ #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" #include "mlir/Dialect/QCO/Utils/Drivers.h" -#include "mlir/Dialect/QCO/Utils/Qubits.h" #include "mlir/Dialect/QCO/Utils/WireIterator.h" #include @@ -50,58 +49,6 @@ class DriversTest : public testing::Test { }; } // namespace -TEST_F(DriversTest, ProgramWalk) { - qco::QCOProgramBuilder builder(context.get()); - builder.initialize(); - const auto q00 = builder.allocQubit(); - const auto q10 = builder.allocQubit(); - const auto q20 = builder.allocQubit(); - const auto q30 = builder.allocQubit(); - - const auto q01 = builder.h(q00); - const auto [q02, q11] = builder.cx(q01, q10); - const auto [q21, q31] = builder.cx(q20, q30); - - const auto [q03, c0] = builder.measure(q02); - const auto [q12, c1] = builder.measure(q11); - const auto [q22, c2] = builder.measure(q21); - const auto [q32, c3] = builder.measure(q31); - - builder.sink(q03); - builder.sink(q12); - builder.sink(q22); - builder.sink(q32); - - auto mod = builder.finalize(); - auto func = *(mod->getOps().begin()); - - Value ex0 = nullptr; - Value ex1 = nullptr; - Value ex2 = nullptr; - Value ex3 = nullptr; - - // Walk until the first measurement operation is encountered and stop. - // Since WalkOrder::PreOrder is used here, the state of the qubits is not yet - // updated with the SSA values of the measurement op. - // Consequently, the program qubits point at the outputs of the controlled-Xs. - std::ignore = qco::walkProgram(func.getBody(), - [&](Operation* op, const qco::Qubits& qubits) { - if (op == q03.getDefiningOp()) { - ex0 = qubits.getProgramQubit(0); - ex1 = qubits.getProgramQubit(1); - ex2 = qubits.getProgramQubit(2); - ex3 = qubits.getProgramQubit(3); - return WalkResult::interrupt(); - } - return WalkResult::advance(); - }); - - ASSERT_EQ(ex0, q02); - ASSERT_EQ(ex1, q11); - ASSERT_EQ(ex2, q21); - ASSERT_EQ(ex3, q31); -} - TEST_F(DriversTest, ProgramGraphWalkTooFewWires) { qco::QCOProgramBuilder builder(context.get()); builder.initialize(); diff --git a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp index 618daff0db..4e5e1e4857 100644 --- a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp +++ b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp @@ -43,9 +43,9 @@ static_assert(!SupportedMatrix); 0, 0, 0, 1); // row 3 } -[[nodiscard]] static DynamicMatrix cyclicPermutation(const std::int64_t dim) { +[[nodiscard]] static DynamicMatrix cyclicPermutation(const int64_t dim) { DynamicMatrix matrix(dim); - for (std::int64_t row = 0; row < dim; ++row) { + for (int64_t row = 0; row < dim; ++row) { matrix(row, (row + 1) % dim) = 1.0; } return matrix; @@ -66,7 +66,7 @@ static void verifyMatrix2x2FixedMatchesDynamic() { ASSERT_TRUE(dynamic.has_value()); EXPECT_TRUE(DynamicMatrix(fixed->eigenvectors) .isApprox(dynamic->eigenvectors, MATRIX_TOLERANCE)); - for (std::size_t i = 0; i < 2; ++i) { + for (size_t i = 0; i < 2; ++i) { EXPECT_TRUE(complexesAreApprox(fixed->eigenvalues[i], dynamic->eigenvalues[i], MATRIX_TOLERANCE)); } @@ -83,7 +83,7 @@ static void verifyMatrix4x4FixedMatchesDynamic() { ASSERT_TRUE(dynamic.has_value()); EXPECT_TRUE(DynamicMatrix(fixed->eigenvectors) .isApprox(dynamic->eigenvectors, MATRIX_TOLERANCE)); - for (std::size_t i = 0; i < 4; ++i) { + for (size_t i = 0; i < 4; ++i) { EXPECT_TRUE(complexesAreApprox(fixed->eigenvalues[i], dynamic->eigenvalues[i], MATRIX_TOLERANCE)); } @@ -93,17 +93,16 @@ static void verifyMatrix4x4FixedMatchesDynamic() { eigenpairsSatisfy(const DynamicMatrix& matrix, const EigenDecomposition& decomposition, const double tol = MATRIX_TOLERANCE) { - const std::int64_t dim = matrix.rows(); - for (std::size_t colIdx = 0; colIdx < decomposition.eigenvalues.size(); - ++colIdx) { + const int64_t dim = matrix.rows(); + for (size_t colIdx = 0; colIdx < decomposition.eigenvalues.size(); ++colIdx) { DynamicMatrix eigenvector(dim); - for (std::int64_t row = 0; row < dim; ++row) { + for (int64_t row = 0; row < dim; ++row) { eigenvector(row, 0) = - decomposition.eigenvectors(row, static_cast(colIdx)); + decomposition.eigenvectors(row, static_cast(colIdx)); } const DynamicMatrix image = matrix * eigenvector; const Complex lambda = decomposition.eigenvalues[colIdx]; - for (std::int64_t row = 0; row < dim; ++row) { + for (int64_t row = 0; row < dim; ++row) { if (!complexesAreApprox(image(row, 0), lambda * eigenvector(row, 0), tol)) { return false; @@ -115,9 +114,9 @@ eigenpairsSatisfy(const DynamicMatrix& matrix, [[nodiscard]] static double matrixFrobeniusNorm(const DynamicMatrix& matrix) { double sumSq = 0.0; - const std::int64_t dim = matrix.rows(); - for (std::int64_t row = 0; row < dim; ++row) { - for (std::int64_t col = 0; col < dim; ++col) { + const int64_t dim = matrix.rows(); + for (int64_t row = 0; row < dim; ++row) { + for (int64_t col = 0; col < dim; ++col) { sumSq += std::norm(matrix(row, col)); } } @@ -127,18 +126,17 @@ eigenpairsSatisfy(const DynamicMatrix& matrix, [[nodiscard]] static double maxEigenpairResidual(const DynamicMatrix& matrix, const EigenDecomposition& decomposition) { - const std::int64_t dim = matrix.rows(); + const int64_t dim = matrix.rows(); double worst = 0.0; - for (std::size_t colIdx = 0; colIdx < decomposition.eigenvalues.size(); - ++colIdx) { + for (size_t colIdx = 0; colIdx < decomposition.eigenvalues.size(); ++colIdx) { DynamicMatrix eigenvector(dim); - for (std::int64_t row = 0; row < dim; ++row) { + for (int64_t row = 0; row < dim; ++row) { eigenvector(row, 0) = - decomposition.eigenvectors(row, static_cast(colIdx)); + decomposition.eigenvectors(row, static_cast(colIdx)); } const DynamicMatrix image = matrix * eigenvector; const Complex lambda = decomposition.eigenvalues[colIdx]; - for (std::int64_t row = 0; row < dim; ++row) { + for (int64_t row = 0; row < dim; ++row) { worst = std::max(worst, std::abs(image(row, 0) - lambda * eigenvector(row, 0))); } @@ -148,8 +146,7 @@ maxEigenpairResidual(const DynamicMatrix& matrix, [[nodiscard]] static double generalEigenpairTolerance(const DynamicMatrix& matrix) { - const auto dim = - static_cast(std::max(matrix.rows(), 1)); + const auto dim = static_cast(std::max(matrix.rows(), 1)); const double scale = std::max(1.0, matrixFrobeniusNorm(matrix)); // EISPACK backward error scales with dimension and ||A||_F. return std::max(1e-10, 1e-11 * dim * scale); @@ -162,7 +159,7 @@ static void expectEigenDecomposition(const DynamicMatrix& matrix) { << "eigenDecomposition failed for " << matrix.rows() << "x" << matrix.cols(); ASSERT_EQ(decomposition->eigenvalues.size(), - static_cast(matrix.rows())); + static_cast(matrix.rows())); ASSERT_EQ(decomposition->eigenvectors.rows(), matrix.rows()); ASSERT_EQ(decomposition->eigenvectors.cols(), matrix.cols()); for (const Complex& eigenvalue : decomposition->eigenvalues) { @@ -178,7 +175,7 @@ static void expectGeneralEigenDecomposition(const DynamicMatrix& matrix) { << "eigenDecomposition failed for " << matrix.rows() << "x" << matrix.cols(); ASSERT_EQ(decomposition->eigenvalues.size(), - static_cast(matrix.rows())); + static_cast(matrix.rows())); ASSERT_EQ(decomposition->eigenvectors.rows(), matrix.rows()); ASSERT_EQ(decomposition->eigenvectors.cols(), matrix.cols()); const double tol = generalEigenpairTolerance(matrix); @@ -193,21 +190,21 @@ static void expectGeneralEigenDecomposition(const DynamicMatrix& matrix) { EXPECT_TRUE(complexesAreApprox(matrix.trace(), eigenTrace, tol)); } -[[nodiscard]] static DynamicMatrix randomComplexMatrix(const std::int64_t dim, +[[nodiscard]] static DynamicMatrix randomComplexMatrix(const int64_t dim, std::mt19937& rng) { std::uniform_real_distribution dist(-3.0, 3.0); DynamicMatrix matrix(dim); - for (std::int64_t row = 0; row < dim; ++row) { - for (std::int64_t col = 0; col < dim; ++col) { + for (int64_t row = 0; row < dim; ++row) { + for (int64_t col = 0; col < dim; ++col) { matrix(row, col) = Complex{dist(rng), dist(rng)}; } } return matrix; } -[[nodiscard]] static DynamicMatrix diagonalUnitary(const std::int64_t dim) { +[[nodiscard]] static DynamicMatrix diagonalUnitary(const int64_t dim) { DynamicMatrix matrix(dim); - for (std::int64_t i = 0; i < dim; ++i) { + for (int64_t i = 0; i < dim; ++i) { matrix(i, i) = std::exp(1i * (0.1 * static_cast(i))); } return matrix; @@ -417,6 +414,49 @@ TEST(DynamicMatrix, ScalarMultiplyAssign) { std::exp(Complex{0.0, 0.5}))); } +TEST(DynamicMatrix, PremultiplyBy) { + const DynamicMatrix x(pauliX()); + const auto y = + DynamicMatrix(Matrix2x2::fromElements(1, 0, 0, std::exp(1i * 0.5))); + DynamicMatrix acc = DynamicMatrix::identity(2); + acc.premultiplyBy(x); + acc.premultiplyBy(y); + EXPECT_TRUE(acc.isApprox(y * x)); +} + +TEST(DynamicMatrix, PremultiplyByEmbeddedMatchesDense) { + const Matrix2x2 x = pauliX(); + const Matrix4x4 swap = swapMatrix(); + for (const size_t numQubits : {2U, 3U}) { + for (size_t wire = 0; wire < numQubits; ++wire) { + DynamicMatrix dense = + DynamicMatrix::identity(static_cast(1) << numQubits); + dense.premultiplyBy(x.embedInNqubit(numQubits, wire)); + DynamicMatrix structured = + DynamicMatrix::identity(static_cast(1) << numQubits); + structured.premultiplyByEmbedded1Q(x, numQubits, wire); + EXPECT_TRUE(dense.isApprox(structured)); + } + } + for (const std::array wires : + {std::array{0, 1}, std::array{0, 2}, + std::array{1, 2}}) { + constexpr size_t numQubits = 3; + DynamicMatrix dense = + DynamicMatrix::identity(static_cast(1) << numQubits); + dense.premultiplyBy(swap.embedInNqubit(numQubits, wires[0], wires[1])); + DynamicMatrix structured = + DynamicMatrix::identity(static_cast(1) << numQubits); + structured.premultiplyByEmbedded2Q(swap, numQubits, wires[0], wires[1]); + EXPECT_TRUE(dense.isApprox(structured)); + } + DynamicMatrix dense2 = DynamicMatrix::identity(4); + dense2.premultiplyBy(swap.embedInNqubit(2, 1, 0)); + DynamicMatrix structured2 = DynamicMatrix::identity(4); + structured2.premultiplyByEmbedded2Q(swap.reorderForQubits(1, 0), 2, 0, 1); + EXPECT_TRUE(dense2.isApprox(structured2)); +} + TEST(DynamicMatrix, AssignFrom) { DynamicMatrix dynamic; @@ -744,7 +784,7 @@ TEST(SymmetricEigensolver, Matrix4x4Overload) { Matrix4x4::fromRealRowMajor(a).symmetricEigenDecomposition(); const SymmetricEigenDecomposition4x4 fromMatrix = matrix.symmetricEigenDecomposition(); - for (std::size_t i = 0; i < 4; ++i) { + for (size_t i = 0; i < 4; ++i) { EXPECT_NEAR(fromMatrix.eigenvalues[i], fromArray.eigenvalues[i], MATRIX_TOLERANCE); } @@ -756,8 +796,8 @@ TEST(SymmetricEigensolver, ReconstructsRandomSymmetric) { std::uniform_real_distribution dist(-2.0, 2.0); for (int trial = 0; trial < 50; ++trial) { std::array a{}; - for (std::size_t i = 0; i < 4; ++i) { - for (std::size_t j = i; j < 4; ++j) { + for (size_t i = 0; i < 4; ++i) { + for (size_t j = i; j < 4; ++j) { const double value = dist(rng); a[(i * 4) + j] = value; a[(j * 4) + i] = value; @@ -767,7 +807,7 @@ TEST(SymmetricEigensolver, ReconstructsRandomSymmetric) { Matrix4x4::fromRealRowMajor(a).symmetricEigenDecomposition(); // Eigenvalues are ascending. - for (std::size_t i = 0; i + 1 < 4; ++i) { + for (size_t i = 0; i + 1 < 4; ++i) { EXPECT_LE(result.eigenvalues[i], result.eigenvalues[i + 1] + MATRIX_TOLERANCE); } @@ -794,7 +834,7 @@ TEST(SymmetricEigensolver, HandlesDegenerateSpectrum) { // A scalar multiple of the identity: every vector is an eigenvector, but the // returned basis must still be orthonormal. std::array a{}; - for (std::size_t i = 0; i < 4; ++i) { + for (size_t i = 0; i < 4; ++i) { a[(i * 4) + i] = 2.5; } const SymmetricEigenDecomposition4x4 result = @@ -951,7 +991,7 @@ TEST(SymmetricEigensolver, SparseCornerElement) { const SymmetricEigenDecomposition4x4 fromArray = Matrix4x4::fromRealRowMajor(sparse).symmetricEigenDecomposition(); EXPECT_NEAR(fromArray.eigenvalues[3], 7.0, MATRIX_TOLERANCE); - for (std::size_t i = 0; i + 1 < 4; ++i) { + for (size_t i = 0; i + 1 < 4; ++i) { EXPECT_NEAR(fromArray.eigenvalues[i], 0.0, MATRIX_TOLERANCE); } } diff --git a/mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp b/mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp index 765b249a41..da6b6e25d0 100644 --- a/mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp +++ b/mlir/unittests/Dialect/QIR/IR/test_qir_ir.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -32,8 +33,8 @@ namespace { struct QIRTestCase { std::string name; - mqt::test::NamedBuilder programBuilder; - mqt::test::NamedBuilder referenceBuilder; + mqt::test::NamedMLIRBuilder programBuilder; + mqt::test::NamedMLIRBuilder referenceBuilder; friend std::ostream& operator<<(std::ostream& os, const QIRTestCase& info); }; @@ -66,8 +67,8 @@ TEST_P(QIRTest, ProgramEquivalence) { const auto name = " (" + GetParam().name + ")"; mqt::test::DeferredPrinter printer; - auto program = QIRProgramBuilder::build(context.get(), programBuilder.fn, - QIRProgramBuilder::Profile::Adaptive); + auto program = mqt::test::buildMLIRProgram( + context.get(), programBuilder, QIRProgramBuilder::Profile::Adaptive); ASSERT_TRUE(program); printer.record(program.get(), "Original QIR IR" + name); EXPECT_TRUE(verify(*program).succeeded()); @@ -76,8 +77,8 @@ TEST_P(QIRTest, ProgramEquivalence) { printer.record(program.get(), "Canonicalized QIR IR" + name); EXPECT_TRUE(verify(*program).succeeded()); - auto reference = QIRProgramBuilder::build( - context.get(), referenceBuilder.fn, QIRProgramBuilder::Profile::Adaptive); + auto reference = mqt::test::buildMLIRProgram( + context.get(), referenceBuilder, QIRProgramBuilder::Profile::Adaptive); ASSERT_TRUE(reference); printer.record(reference.get(), "Reference QIR IR" + name); EXPECT_TRUE(verify(*reference).succeeded()); @@ -112,36 +113,36 @@ TEST_F(QIRTest, BuilderRejectsMixedStaticAndDynamicQubitAllocationModes) { /// @{ INSTANTIATE_TEST_SUITE_P( QIRDCXOpTest, QIRTest, - testing::Values(QIRTestCase{"DCX", MQT_NAMED_BUILDER(dcx), - MQT_NAMED_BUILDER(dcx)}, + testing::Values(QIRTestCase{"DCX", MQT_NAMED_BUILDER(dcx<>), + MQT_NAMED_BUILDER(dcx<>)}, QIRTestCase{"SingleControlledDCX", - MQT_NAMED_BUILDER(singleControlledDcx), - MQT_NAMED_BUILDER(singleControlledDcx)}, + MQT_NAMED_BUILDER(singleControlledDcx<>), + MQT_NAMED_BUILDER(singleControlledDcx<>)}, QIRTestCase{"MultipleControlledDCX", - MQT_NAMED_BUILDER(multipleControlledDcx), - MQT_NAMED_BUILDER(multipleControlledDcx)})); + MQT_NAMED_BUILDER(multipleControlledDcx<>), + MQT_NAMED_BUILDER(multipleControlledDcx<>)})); /// @} /// \name QIR/Operations/StandardGates/EcrOp.cpp /// @{ INSTANTIATE_TEST_SUITE_P( QIRECROpTest, QIRTest, - testing::Values(QIRTestCase{"ECR", MQT_NAMED_BUILDER(ecr), - MQT_NAMED_BUILDER(ecr)}, + testing::Values(QIRTestCase{"ECR", MQT_NAMED_BUILDER(ecr<>), + MQT_NAMED_BUILDER(ecr<>)}, QIRTestCase{"SingleControlledECR", - MQT_NAMED_BUILDER(singleControlledEcr), - MQT_NAMED_BUILDER(singleControlledEcr)}, + MQT_NAMED_BUILDER(singleControlledEcr<>), + MQT_NAMED_BUILDER(singleControlledEcr<>)}, QIRTestCase{"MultipleControlledECR", - MQT_NAMED_BUILDER(multipleControlledEcr), - MQT_NAMED_BUILDER(multipleControlledEcr)})); + MQT_NAMED_BUILDER(multipleControlledEcr<>), + MQT_NAMED_BUILDER(multipleControlledEcr<>)})); /// @} /// \name QIR/Operations/StandardGates/GphaseOp.cpp /// @{ INSTANTIATE_TEST_SUITE_P(QIRGPhaseOpTest, QIRTest, testing::Values(QIRTestCase{ - "GlobalPhase", MQT_NAMED_BUILDER(globalPhase), - MQT_NAMED_BUILDER(globalPhase)})); + "GlobalPhase", MQT_NAMED_BUILDER(globalPhase<>), + MQT_NAMED_BUILDER(globalPhase<>)})); /// @} /// \name QIR/Operations/StandardGates/HOp.cpp @@ -149,41 +150,41 @@ INSTANTIATE_TEST_SUITE_P(QIRGPhaseOpTest, QIRTest, INSTANTIATE_TEST_SUITE_P( QIRHOpTest, QIRTest, testing::Values( - QIRTestCase{"H", MQT_NAMED_BUILDER(h), MQT_NAMED_BUILDER(h)}, - QIRTestCase{"SingleControlledH", MQT_NAMED_BUILDER(singleControlledH), - MQT_NAMED_BUILDER(singleControlledH)}, + QIRTestCase{"H", MQT_NAMED_BUILDER(h<>), MQT_NAMED_BUILDER(h<>)}, + QIRTestCase{"SingleControlledH", MQT_NAMED_BUILDER(singleControlledH<>), + MQT_NAMED_BUILDER(singleControlledH<>)}, QIRTestCase{"MultipleControlledH", - MQT_NAMED_BUILDER(multipleControlledH), - MQT_NAMED_BUILDER(multipleControlledH)})); + MQT_NAMED_BUILDER(multipleControlledH<>), + MQT_NAMED_BUILDER(multipleControlledH<>)})); /// @} /// \name QIR/Operations/StandardGates/IdOp.cpp /// @{ INSTANTIATE_TEST_SUITE_P( QIRIDOpTest, QIRTest, - testing::Values(QIRTestCase{"Identity", MQT_NAMED_BUILDER(identity), - MQT_NAMED_BUILDER(identity)}, + testing::Values(QIRTestCase{"Identity", MQT_NAMED_BUILDER(identity<>), + MQT_NAMED_BUILDER(identity<>)}, QIRTestCase{"SingleControlledIdentity", - MQT_NAMED_BUILDER(singleControlledIdentity), - MQT_NAMED_BUILDER(singleControlledIdentity)}, + MQT_NAMED_BUILDER(singleControlledIdentity<>), + MQT_NAMED_BUILDER(singleControlledIdentity<>)}, QIRTestCase{ "MultipleControlledIdentity", - MQT_NAMED_BUILDER(multipleControlledIdentity), - MQT_NAMED_BUILDER(multipleControlledIdentity)})); + MQT_NAMED_BUILDER(multipleControlledIdentity<>), + MQT_NAMED_BUILDER(multipleControlledIdentity<>)})); /// @} /// \name QIR/Operations/StandardGates/IswapOp.cpp /// @{ INSTANTIATE_TEST_SUITE_P( QIRiSWAPOpTest, QIRTest, - testing::Values(QIRTestCase{"iSWAP", MQT_NAMED_BUILDER(iswap), - MQT_NAMED_BUILDER(iswap)}, + testing::Values(QIRTestCase{"iSWAP", MQT_NAMED_BUILDER(iswap<>), + MQT_NAMED_BUILDER(iswap<>)}, QIRTestCase{"SingleControllediSWAP", - MQT_NAMED_BUILDER(singleControlledIswap), - MQT_NAMED_BUILDER(singleControlledIswap)}, + MQT_NAMED_BUILDER(singleControlledIswap<>), + MQT_NAMED_BUILDER(singleControlledIswap<>)}, QIRTestCase{"MultipleControllediSWAP", - MQT_NAMED_BUILDER(multipleControlledIswap), - MQT_NAMED_BUILDER(multipleControlledIswap)})); + MQT_NAMED_BUILDER(multipleControlledIswap<>), + MQT_NAMED_BUILDER(multipleControlledIswap<>)})); /// @} /// \name QIR/Operations/StandardGates/POp.cpp @@ -191,12 +192,12 @@ INSTANTIATE_TEST_SUITE_P( INSTANTIATE_TEST_SUITE_P( QIRPOpTest, QIRTest, testing::Values( - QIRTestCase{"P", MQT_NAMED_BUILDER(p), MQT_NAMED_BUILDER(p)}, - QIRTestCase{"SingleControlledP", MQT_NAMED_BUILDER(singleControlledP), - MQT_NAMED_BUILDER(singleControlledP)}, + QIRTestCase{"P", MQT_NAMED_BUILDER(p<>), MQT_NAMED_BUILDER(p<>)}, + QIRTestCase{"SingleControlledP", MQT_NAMED_BUILDER(singleControlledP<>), + MQT_NAMED_BUILDER(singleControlledP<>)}, QIRTestCase{"MultipleControlledP", - MQT_NAMED_BUILDER(multipleControlledP), - MQT_NAMED_BUILDER(multipleControlledP)})); + MQT_NAMED_BUILDER(multipleControlledP<>), + MQT_NAMED_BUILDER(multipleControlledP<>)})); /// @} /// \name QIR/Operations/StandardGates/ROp.cpp @@ -204,107 +205,110 @@ INSTANTIATE_TEST_SUITE_P( INSTANTIATE_TEST_SUITE_P( QIRROpTest, QIRTest, testing::Values( - QIRTestCase{"R", MQT_NAMED_BUILDER(r), MQT_NAMED_BUILDER(r)}, - QIRTestCase{"SingleControlledR", MQT_NAMED_BUILDER(singleControlledR), - MQT_NAMED_BUILDER(singleControlledR)}, + QIRTestCase{"R", MQT_NAMED_BUILDER(r<>), MQT_NAMED_BUILDER(r<>)}, + QIRTestCase{"SingleControlledR", MQT_NAMED_BUILDER(singleControlledR<>), + MQT_NAMED_BUILDER(singleControlledR<>)}, QIRTestCase{"MultipleControlledR", - MQT_NAMED_BUILDER(multipleControlledR), - MQT_NAMED_BUILDER(multipleControlledR)})); + MQT_NAMED_BUILDER(multipleControlledR<>), + MQT_NAMED_BUILDER(multipleControlledR<>)})); /// @} /// \name QIR/Operations/StandardGates/RxOp.cpp /// @{ INSTANTIATE_TEST_SUITE_P( QIRRXOpTest, QIRTest, - testing::Values( - QIRTestCase{"RX", MQT_NAMED_BUILDER(rx), MQT_NAMED_BUILDER(rx)}, - QIRTestCase{"SingleControlledRX", MQT_NAMED_BUILDER(singleControlledRx), - MQT_NAMED_BUILDER(singleControlledRx)}, - QIRTestCase{"MultipleControlledRX", - MQT_NAMED_BUILDER(multipleControlledRx), - MQT_NAMED_BUILDER(multipleControlledRx)})); + testing::Values(QIRTestCase{"RX", MQT_NAMED_BUILDER(rx<>), + MQT_NAMED_BUILDER(rx<>)}, + QIRTestCase{"SingleControlledRX", + MQT_NAMED_BUILDER(singleControlledRx<>), + MQT_NAMED_BUILDER(singleControlledRx<>)}, + QIRTestCase{"MultipleControlledRX", + MQT_NAMED_BUILDER(multipleControlledRx<>), + MQT_NAMED_BUILDER(multipleControlledRx<>)})); /// @} /// \name QIR/Operations/StandardGates/RxxOp.cpp /// @{ INSTANTIATE_TEST_SUITE_P( QIRRXXOpTest, QIRTest, - testing::Values(QIRTestCase{"RXX", MQT_NAMED_BUILDER(rxx), - MQT_NAMED_BUILDER(rxx)}, + testing::Values(QIRTestCase{"RXX", MQT_NAMED_BUILDER(rxx<>), + MQT_NAMED_BUILDER(rxx<>)}, QIRTestCase{"SingleControlledRXX", - MQT_NAMED_BUILDER(singleControlledRxx), - MQT_NAMED_BUILDER(singleControlledRxx)}, + MQT_NAMED_BUILDER(singleControlledRxx<>), + MQT_NAMED_BUILDER(singleControlledRxx<>)}, QIRTestCase{"MultipleControlledRXX", - MQT_NAMED_BUILDER(multipleControlledRxx), - MQT_NAMED_BUILDER(multipleControlledRxx)})); + MQT_NAMED_BUILDER(multipleControlledRxx<>), + MQT_NAMED_BUILDER(multipleControlledRxx<>)})); /// @} /// \name QIR/Operations/StandardGates/RyOp.cpp /// @{ INSTANTIATE_TEST_SUITE_P( QIRRYOpTest, QIRTest, - testing::Values( - QIRTestCase{"RY", MQT_NAMED_BUILDER(ry), MQT_NAMED_BUILDER(ry)}, - QIRTestCase{"SingleControlledRY", MQT_NAMED_BUILDER(singleControlledRy), - MQT_NAMED_BUILDER(singleControlledRy)}, - QIRTestCase{"MultipleControlledRY", - MQT_NAMED_BUILDER(multipleControlledRy), - MQT_NAMED_BUILDER(multipleControlledRy)})); + testing::Values(QIRTestCase{"RY", MQT_NAMED_BUILDER(ry<>), + MQT_NAMED_BUILDER(ry<>)}, + QIRTestCase{"SingleControlledRY", + MQT_NAMED_BUILDER(singleControlledRy<>), + MQT_NAMED_BUILDER(singleControlledRy<>)}, + QIRTestCase{"MultipleControlledRY", + MQT_NAMED_BUILDER(multipleControlledRy<>), + MQT_NAMED_BUILDER(multipleControlledRy<>)})); /// @} /// \name QIR/Operations/StandardGates/RyyOp.cpp /// @{ INSTANTIATE_TEST_SUITE_P( QIRRYYOpTest, QIRTest, - testing::Values(QIRTestCase{"RYY", MQT_NAMED_BUILDER(ryy), - MQT_NAMED_BUILDER(ryy)}, + testing::Values(QIRTestCase{"RYY", MQT_NAMED_BUILDER(ryy<>), + MQT_NAMED_BUILDER(ryy<>)}, QIRTestCase{"SingleControlledRYY", - MQT_NAMED_BUILDER(singleControlledRyy), - MQT_NAMED_BUILDER(singleControlledRyy)}, + MQT_NAMED_BUILDER(singleControlledRyy<>), + MQT_NAMED_BUILDER(singleControlledRyy<>)}, QIRTestCase{"MultipleControlledRYY", - MQT_NAMED_BUILDER(multipleControlledRyy), - MQT_NAMED_BUILDER(multipleControlledRyy)})); + MQT_NAMED_BUILDER(multipleControlledRyy<>), + MQT_NAMED_BUILDER(multipleControlledRyy<>)})); /// @} /// \name QIR/Operations/StandardGates/RzOp.cpp /// @{ INSTANTIATE_TEST_SUITE_P( QIRRZOpTest, QIRTest, - testing::Values( - QIRTestCase{"RZ", MQT_NAMED_BUILDER(rz), MQT_NAMED_BUILDER(rz)}, - QIRTestCase{"SingleControlledRZ", MQT_NAMED_BUILDER(singleControlledRz), - MQT_NAMED_BUILDER(singleControlledRz)}, - QIRTestCase{"MultipleControlledRZ", - MQT_NAMED_BUILDER(multipleControlledRz), - MQT_NAMED_BUILDER(multipleControlledRz)})); + testing::Values(QIRTestCase{"RZ", MQT_NAMED_BUILDER(rz<>), + MQT_NAMED_BUILDER(rz<>)}, + QIRTestCase{"SingleControlledRZ", + MQT_NAMED_BUILDER(singleControlledRz<>), + MQT_NAMED_BUILDER(singleControlledRz<>)}, + QIRTestCase{"MultipleControlledRZ", + MQT_NAMED_BUILDER(multipleControlledRz<>), + MQT_NAMED_BUILDER(multipleControlledRz<>)})); /// @} /// \name QIR/Operations/StandardGates/RzxOp.cpp /// @{ INSTANTIATE_TEST_SUITE_P( QIRRZXOpTest, QIRTest, - testing::Values(QIRTestCase{"RZX", MQT_NAMED_BUILDER(rzx), - MQT_NAMED_BUILDER(rzx)}, + testing::Values(QIRTestCase{"RZX", MQT_NAMED_BUILDER(rzx<>), + MQT_NAMED_BUILDER(rzx<>)}, QIRTestCase{"SingleControlledRZX", - MQT_NAMED_BUILDER(singleControlledRzx), - MQT_NAMED_BUILDER(singleControlledRzx)}, + MQT_NAMED_BUILDER(singleControlledRzx<>), + MQT_NAMED_BUILDER(singleControlledRzx<>)}, QIRTestCase{"MultipleControlledRZX", - MQT_NAMED_BUILDER(multipleControlledRzx), - MQT_NAMED_BUILDER(multipleControlledRzx)})); + MQT_NAMED_BUILDER(multipleControlledRzx<>), + MQT_NAMED_BUILDER(multipleControlledRzx<>)})); /// @} /// \name QIR/Operations/StandardGates/RzzOp.cpp /// @{ INSTANTIATE_TEST_SUITE_P( QIRRZZOpTest, QIRTest, - testing::Values(QIRTestCase{"RZZ", MQT_NAMED_BUILDER(rzz), - MQT_NAMED_BUILDER(rzz)}, + testing::Values(QIRTestCase{"RZZ", MQT_NAMED_BUILDER(rzz<>), + MQT_NAMED_BUILDER(rzz<>)}, QIRTestCase{"SingleControlledRZZ", - MQT_NAMED_BUILDER(singleControlledRzz), - MQT_NAMED_BUILDER(singleControlledRzz)}, + MQT_NAMED_BUILDER(singleControlledRzz<>), + MQT_NAMED_BUILDER(singleControlledRzz<>)}, QIRTestCase{"MultipleControlledRZZ", - MQT_NAMED_BUILDER(multipleControlledRzz), - MQT_NAMED_BUILDER(multipleControlledRzz)})); + MQT_NAMED_BUILDER(multipleControlledRzz<>), + MQT_NAMED_BUILDER(multipleControlledRzz<>)})); /// @} /// \name QIR/Operations/StandardGates/SOp.cpp @@ -312,67 +316,68 @@ INSTANTIATE_TEST_SUITE_P( INSTANTIATE_TEST_SUITE_P( QIRSOpTest, QIRTest, testing::Values( - QIRTestCase{"S", MQT_NAMED_BUILDER(s), MQT_NAMED_BUILDER(s)}, - QIRTestCase{"SingleControlledS", MQT_NAMED_BUILDER(singleControlledS), - MQT_NAMED_BUILDER(singleControlledS)}, + QIRTestCase{"S", MQT_NAMED_BUILDER(s<>), MQT_NAMED_BUILDER(s<>)}, + QIRTestCase{"SingleControlledS", MQT_NAMED_BUILDER(singleControlledS<>), + MQT_NAMED_BUILDER(singleControlledS<>)}, QIRTestCase{"MultipleControlledS", - MQT_NAMED_BUILDER(multipleControlledS), - MQT_NAMED_BUILDER(multipleControlledS)})); + MQT_NAMED_BUILDER(multipleControlledS<>), + MQT_NAMED_BUILDER(multipleControlledS<>)})); /// @} /// \name QIR/Operations/StandardGates/SdgOp.cpp /// @{ INSTANTIATE_TEST_SUITE_P( QIRSdgOpTest, QIRTest, - testing::Values(QIRTestCase{"Sdg", MQT_NAMED_BUILDER(sdg), - MQT_NAMED_BUILDER(sdg)}, + testing::Values(QIRTestCase{"Sdg", MQT_NAMED_BUILDER(sdg<>), + MQT_NAMED_BUILDER(sdg<>)}, QIRTestCase{"SingleControlledSdg", - MQT_NAMED_BUILDER(singleControlledSdg), - MQT_NAMED_BUILDER(singleControlledSdg)}, + MQT_NAMED_BUILDER(singleControlledSdg<>), + MQT_NAMED_BUILDER(singleControlledSdg<>)}, QIRTestCase{"MultipleControlledSdg", - MQT_NAMED_BUILDER(multipleControlledSdg), - MQT_NAMED_BUILDER(multipleControlledSdg)})); + MQT_NAMED_BUILDER(multipleControlledSdg<>), + MQT_NAMED_BUILDER(multipleControlledSdg<>)})); /// @} /// \name QIR/Operations/StandardGates/SwapOp.cpp /// @{ INSTANTIATE_TEST_SUITE_P( QIRSWAPOpTest, QIRTest, - testing::Values(QIRTestCase{"SWAP", MQT_NAMED_BUILDER(swap), - MQT_NAMED_BUILDER(swap)}, + testing::Values(QIRTestCase{"SWAP", MQT_NAMED_BUILDER(swap<>), + MQT_NAMED_BUILDER(swap<>)}, QIRTestCase{"SingleControlledSWAP", - MQT_NAMED_BUILDER(singleControlledSwap), - MQT_NAMED_BUILDER(singleControlledSwap)}, + MQT_NAMED_BUILDER(singleControlledSwap<>), + MQT_NAMED_BUILDER(singleControlledSwap<>)}, QIRTestCase{"MultipleControlledSWAP", - MQT_NAMED_BUILDER(multipleControlledSwap), - MQT_NAMED_BUILDER(multipleControlledSwap)})); + MQT_NAMED_BUILDER(multipleControlledSwap<>), + MQT_NAMED_BUILDER(multipleControlledSwap<>)})); /// @} /// \name QIR/Operations/StandardGates/SxOp.cpp /// @{ INSTANTIATE_TEST_SUITE_P( QIRSXOpTest, QIRTest, - testing::Values( - QIRTestCase{"SX", MQT_NAMED_BUILDER(sx), MQT_NAMED_BUILDER(sx)}, - QIRTestCase{"SingleControlledSX", MQT_NAMED_BUILDER(singleControlledSx), - MQT_NAMED_BUILDER(singleControlledSx)}, - QIRTestCase{"MultipleControlledSX", - MQT_NAMED_BUILDER(multipleControlledSx), - MQT_NAMED_BUILDER(multipleControlledSx)})); + testing::Values(QIRTestCase{"SX", MQT_NAMED_BUILDER(sx<>), + MQT_NAMED_BUILDER(sx<>)}, + QIRTestCase{"SingleControlledSX", + MQT_NAMED_BUILDER(singleControlledSx<>), + MQT_NAMED_BUILDER(singleControlledSx<>)}, + QIRTestCase{"MultipleControlledSX", + MQT_NAMED_BUILDER(multipleControlledSx<>), + MQT_NAMED_BUILDER(multipleControlledSx<>)})); /// @} /// \name QIR/Operations/StandardGates/SxdgOp.cpp /// @{ INSTANTIATE_TEST_SUITE_P( QIRSXdgOpTest, QIRTest, - testing::Values(QIRTestCase{"SXdg", MQT_NAMED_BUILDER(sxdg), - MQT_NAMED_BUILDER(sxdg)}, + testing::Values(QIRTestCase{"SXdg", MQT_NAMED_BUILDER(sxdg<>), + MQT_NAMED_BUILDER(sxdg<>)}, QIRTestCase{"SingleControlledSXdg", - MQT_NAMED_BUILDER(singleControlledSxdg), - MQT_NAMED_BUILDER(singleControlledSxdg)}, + MQT_NAMED_BUILDER(singleControlledSxdg<>), + MQT_NAMED_BUILDER(singleControlledSxdg<>)}, QIRTestCase{"MultipleControlledSXdg", - MQT_NAMED_BUILDER(multipleControlledSxdg), - MQT_NAMED_BUILDER(multipleControlledSxdg)})); + MQT_NAMED_BUILDER(multipleControlledSxdg<>), + MQT_NAMED_BUILDER(multipleControlledSxdg<>)})); /// @} /// \name QIR/Operations/StandardGates/TOp.cpp @@ -380,39 +385,40 @@ INSTANTIATE_TEST_SUITE_P( INSTANTIATE_TEST_SUITE_P( QIRTOpTest, QIRTest, testing::Values( - QIRTestCase{"T", MQT_NAMED_BUILDER(t_), MQT_NAMED_BUILDER(t_)}, - QIRTestCase{"SingleControlledT", MQT_NAMED_BUILDER(singleControlledT), - MQT_NAMED_BUILDER(singleControlledT)}, + QIRTestCase{"T", MQT_NAMED_BUILDER(t_<>), MQT_NAMED_BUILDER(t_<>)}, + QIRTestCase{"SingleControlledT", MQT_NAMED_BUILDER(singleControlledT<>), + MQT_NAMED_BUILDER(singleControlledT<>)}, QIRTestCase{"MultipleControlledT", - MQT_NAMED_BUILDER(multipleControlledT), - MQT_NAMED_BUILDER(multipleControlledT)})); + MQT_NAMED_BUILDER(multipleControlledT<>), + MQT_NAMED_BUILDER(multipleControlledT<>)})); /// @} /// \name QIR/Operations/StandardGates/TdgOp.cpp /// @{ INSTANTIATE_TEST_SUITE_P( QIRTdgOpTest, QIRTest, - testing::Values(QIRTestCase{"Tdg", MQT_NAMED_BUILDER(tdg), - MQT_NAMED_BUILDER(tdg)}, + testing::Values(QIRTestCase{"Tdg", MQT_NAMED_BUILDER(tdg<>), + MQT_NAMED_BUILDER(tdg<>)}, QIRTestCase{"SingleControlledTdg", - MQT_NAMED_BUILDER(singleControlledTdg), - MQT_NAMED_BUILDER(singleControlledTdg)}, + MQT_NAMED_BUILDER(singleControlledTdg<>), + MQT_NAMED_BUILDER(singleControlledTdg<>)}, QIRTestCase{"MultipleControlledTdg", - MQT_NAMED_BUILDER(multipleControlledTdg), - MQT_NAMED_BUILDER(multipleControlledTdg)})); + MQT_NAMED_BUILDER(multipleControlledTdg<>), + MQT_NAMED_BUILDER(multipleControlledTdg<>)})); /// @} /// \name QIR/Operations/StandardGates/U2Op.cpp /// @{ INSTANTIATE_TEST_SUITE_P( QIRU2OpTest, QIRTest, - testing::Values( - QIRTestCase{"U2", MQT_NAMED_BUILDER(u2), MQT_NAMED_BUILDER(u2)}, - QIRTestCase{"SingleControlledU2", MQT_NAMED_BUILDER(singleControlledU2), - MQT_NAMED_BUILDER(singleControlledU2)}, - QIRTestCase{"MultipleControlledU2", - MQT_NAMED_BUILDER(multipleControlledU2), - MQT_NAMED_BUILDER(multipleControlledU2)})); + testing::Values(QIRTestCase{"U2", MQT_NAMED_BUILDER(u2<>), + MQT_NAMED_BUILDER(u2<>)}, + QIRTestCase{"SingleControlledU2", + MQT_NAMED_BUILDER(singleControlledU2<>), + MQT_NAMED_BUILDER(singleControlledU2<>)}, + QIRTestCase{"MultipleControlledU2", + MQT_NAMED_BUILDER(multipleControlledU2<>), + MQT_NAMED_BUILDER(multipleControlledU2<>)})); /// @} /// \name QIR/Operations/StandardGates/UOp.cpp @@ -420,12 +426,12 @@ INSTANTIATE_TEST_SUITE_P( INSTANTIATE_TEST_SUITE_P( QIRUOpTest, QIRTest, testing::Values( - QIRTestCase{"U", MQT_NAMED_BUILDER(u), MQT_NAMED_BUILDER(u)}, - QIRTestCase{"SingleControlledU", MQT_NAMED_BUILDER(singleControlledU), - MQT_NAMED_BUILDER(singleControlledU)}, + QIRTestCase{"U", MQT_NAMED_BUILDER(u<>), MQT_NAMED_BUILDER(u<>)}, + QIRTestCase{"SingleControlledU", MQT_NAMED_BUILDER(singleControlledU<>), + MQT_NAMED_BUILDER(singleControlledU<>)}, QIRTestCase{"MultipleControlledU", - MQT_NAMED_BUILDER(multipleControlledU), - MQT_NAMED_BUILDER(multipleControlledU)})); + MQT_NAMED_BUILDER(multipleControlledU<>), + MQT_NAMED_BUILDER(multipleControlledU<>)})); /// @} /// \name QIR/Operations/StandardGates/XOp.cpp @@ -433,42 +439,42 @@ INSTANTIATE_TEST_SUITE_P( INSTANTIATE_TEST_SUITE_P( QIRXOpTest, QIRTest, testing::Values( - QIRTestCase{"X", MQT_NAMED_BUILDER(x), MQT_NAMED_BUILDER(x)}, - QIRTestCase{"SingleControlledX", MQT_NAMED_BUILDER(singleControlledX), - MQT_NAMED_BUILDER(singleControlledX)}, + QIRTestCase{"X", MQT_NAMED_BUILDER(x<>), MQT_NAMED_BUILDER(x<>)}, + QIRTestCase{"SingleControlledX", MQT_NAMED_BUILDER(singleControlledX<>), + MQT_NAMED_BUILDER(singleControlledX<>)}, QIRTestCase{"MultipleControlledX", - MQT_NAMED_BUILDER(multipleControlledX), - MQT_NAMED_BUILDER(multipleControlledX)})); + MQT_NAMED_BUILDER(multipleControlledX<>), + MQT_NAMED_BUILDER(multipleControlledX<>)})); /// @} /// \name QIR/Operations/StandardGates/XxMinusYyOp.cpp /// @{ INSTANTIATE_TEST_SUITE_P( QIRXXMinusYYOpTest, QIRTest, - testing::Values(QIRTestCase{"XXMinusYY", MQT_NAMED_BUILDER(xxMinusYY), - MQT_NAMED_BUILDER(xxMinusYY)}, + testing::Values(QIRTestCase{"XXMinusYY", MQT_NAMED_BUILDER(xxMinusYY<>), + MQT_NAMED_BUILDER(xxMinusYY<>)}, QIRTestCase{"SingleControlledXXMinusYY", - MQT_NAMED_BUILDER(singleControlledXxMinusYY), - MQT_NAMED_BUILDER(singleControlledXxMinusYY)}, + MQT_NAMED_BUILDER(singleControlledXxMinusYY<>), + MQT_NAMED_BUILDER(singleControlledXxMinusYY<>)}, QIRTestCase{ "MultipleControlledXXMinusYY", - MQT_NAMED_BUILDER(multipleControlledXxMinusYY), - MQT_NAMED_BUILDER(multipleControlledXxMinusYY)})); + MQT_NAMED_BUILDER(multipleControlledXxMinusYY<>), + MQT_NAMED_BUILDER(multipleControlledXxMinusYY<>)})); /// @} /// \name QIR/Operations/StandardGates/XxPlusYyOp.cpp /// @{ INSTANTIATE_TEST_SUITE_P( QIRXXPlusYYOpTest, QIRTest, - testing::Values(QIRTestCase{"XXPlusYY", MQT_NAMED_BUILDER(xxPlusYY), - MQT_NAMED_BUILDER(xxPlusYY)}, + testing::Values(QIRTestCase{"XXPlusYY", MQT_NAMED_BUILDER(xxPlusYY<>), + MQT_NAMED_BUILDER(xxPlusYY<>)}, QIRTestCase{"SingleControlledXXPlusYY", - MQT_NAMED_BUILDER(singleControlledXxPlusYY), - MQT_NAMED_BUILDER(singleControlledXxPlusYY)}, + MQT_NAMED_BUILDER(singleControlledXxPlusYY<>), + MQT_NAMED_BUILDER(singleControlledXxPlusYY<>)}, QIRTestCase{ "MultipleControlledXXPlusYY", - MQT_NAMED_BUILDER(multipleControlledXxPlusYY), - MQT_NAMED_BUILDER(multipleControlledXxPlusYY)})); + MQT_NAMED_BUILDER(multipleControlledXxPlusYY<>), + MQT_NAMED_BUILDER(multipleControlledXxPlusYY<>)})); /// @} /// \name QIR/Operations/StandardGates/YOp.cpp @@ -476,12 +482,12 @@ INSTANTIATE_TEST_SUITE_P( INSTANTIATE_TEST_SUITE_P( QIRYOpTest, QIRTest, testing::Values( - QIRTestCase{"Y", MQT_NAMED_BUILDER(y), MQT_NAMED_BUILDER(y)}, - QIRTestCase{"SingleControlledY", MQT_NAMED_BUILDER(singleControlledY), - MQT_NAMED_BUILDER(singleControlledY)}, + QIRTestCase{"Y", MQT_NAMED_BUILDER(y<>), MQT_NAMED_BUILDER(y<>)}, + QIRTestCase{"SingleControlledY", MQT_NAMED_BUILDER(singleControlledY<>), + MQT_NAMED_BUILDER(singleControlledY<>)}, QIRTestCase{"MultipleControlledY", - MQT_NAMED_BUILDER(multipleControlledY), - MQT_NAMED_BUILDER(multipleControlledY)})); + MQT_NAMED_BUILDER(multipleControlledY<>), + MQT_NAMED_BUILDER(multipleControlledY<>)})); /// @} /// \name QIR/Operations/StandardGates/ZOp.cpp @@ -489,12 +495,12 @@ INSTANTIATE_TEST_SUITE_P( INSTANTIATE_TEST_SUITE_P( QIRZOpTest, QIRTest, testing::Values( - QIRTestCase{"Z", MQT_NAMED_BUILDER(z), MQT_NAMED_BUILDER(z)}, - QIRTestCase{"SingleControlledZ", MQT_NAMED_BUILDER(singleControlledZ), - MQT_NAMED_BUILDER(singleControlledZ)}, + QIRTestCase{"Z", MQT_NAMED_BUILDER(z<>), MQT_NAMED_BUILDER(z<>)}, + QIRTestCase{"SingleControlledZ", MQT_NAMED_BUILDER(singleControlledZ<>), + MQT_NAMED_BUILDER(singleControlledZ<>)}, QIRTestCase{"MultipleControlledZ", - MQT_NAMED_BUILDER(multipleControlledZ), - MQT_NAMED_BUILDER(multipleControlledZ)})); + MQT_NAMED_BUILDER(multipleControlledZ<>), + MQT_NAMED_BUILDER(multipleControlledZ<>)})); /// @} /// \name QIR/Operations/MeasureOp.cpp @@ -503,14 +509,14 @@ INSTANTIATE_TEST_SUITE_P( QIRMeasureOpTest, QIRTest, testing::Values( QIRTestCase{"SingleMeasurementToSingleBit", - MQT_NAMED_BUILDER(singleMeasurementToSingleBit), - MQT_NAMED_BUILDER(singleMeasurementToSingleBit)}, + MQT_NAMED_BUILDER(singleMeasurementToSingleBit<>), + MQT_NAMED_BUILDER(singleMeasurementToSingleBit<>)}, QIRTestCase{"RepeatedMeasurementToSameBit", - MQT_NAMED_BUILDER(repeatedMeasurementToSameBit), - MQT_NAMED_BUILDER(repeatedMeasurementToSameBit)}, + MQT_NAMED_BUILDER(repeatedMeasurementToSameBit<>), + MQT_NAMED_BUILDER(repeatedMeasurementToSameBit<>)}, QIRTestCase{"RepeatedMeasurementToDifferentBits", - MQT_NAMED_BUILDER(repeatedMeasurementToDifferentBits), - MQT_NAMED_BUILDER(repeatedMeasurementToDifferentBits)})); + MQT_NAMED_BUILDER(repeatedMeasurementToDifferentBits<>), + MQT_NAMED_BUILDER(repeatedMeasurementToDifferentBits<>)})); /// @} /// \name QIR/Operations/ResetOp.cpp @@ -519,23 +525,23 @@ INSTANTIATE_TEST_SUITE_P( QIRResetOpTest, QIRTest, testing::Values( QIRTestCase{"ResetQubitWithoutOp", - MQT_NAMED_BUILDER(resetQubitWithoutOp), - MQT_NAMED_BUILDER(resetQubitWithoutOp)}, + MQT_NAMED_BUILDER(resetQubitWithoutOp<>), + MQT_NAMED_BUILDER(resetQubitWithoutOp<>)}, QIRTestCase{"ResetMultipleQubitsWithoutOp", - MQT_NAMED_BUILDER(resetMultipleQubitsWithoutOp), - MQT_NAMED_BUILDER(resetMultipleQubitsWithoutOp)}, + MQT_NAMED_BUILDER(resetMultipleQubitsWithoutOp<>), + MQT_NAMED_BUILDER(resetMultipleQubitsWithoutOp<>)}, QIRTestCase{"RepeatedResetWithoutOp", - MQT_NAMED_BUILDER(repeatedResetWithoutOp), - MQT_NAMED_BUILDER(repeatedResetWithoutOp)}, + MQT_NAMED_BUILDER(repeatedResetWithoutOp<>), + MQT_NAMED_BUILDER(repeatedResetWithoutOp<>)}, QIRTestCase{"ResetQubitAfterSingleOp", - MQT_NAMED_BUILDER(resetQubitAfterSingleOp), - MQT_NAMED_BUILDER(resetQubitAfterSingleOp)}, + MQT_NAMED_BUILDER(resetQubitAfterSingleOp<>), + MQT_NAMED_BUILDER(resetQubitAfterSingleOp<>)}, QIRTestCase{"ResetMultipleQubitsAfterSingleOp", - MQT_NAMED_BUILDER(resetMultipleQubitsAfterSingleOp), - MQT_NAMED_BUILDER(resetMultipleQubitsAfterSingleOp)}, + MQT_NAMED_BUILDER(resetMultipleQubitsAfterSingleOp<>), + MQT_NAMED_BUILDER(resetMultipleQubitsAfterSingleOp<>)}, QIRTestCase{"RepeatedResetAfterSingleOp", - MQT_NAMED_BUILDER(repeatedResetAfterSingleOp), - MQT_NAMED_BUILDER(repeatedResetAfterSingleOp)})); + MQT_NAMED_BUILDER(repeatedResetAfterSingleOp<>), + MQT_NAMED_BUILDER(repeatedResetAfterSingleOp<>)})); /// @} /// \name QIR/QubitManagement/QubitManagement.cpp @@ -543,15 +549,17 @@ INSTANTIATE_TEST_SUITE_P( INSTANTIATE_TEST_SUITE_P( QIRQubitManagementTest, QIRTest, testing::Values( - QIRTestCase{"AllocQubit", MQT_NAMED_BUILDER(allocQubit), - MQT_NAMED_BUILDER(allocQubit)}, - QIRTestCase{"AllocQubitRegister", MQT_NAMED_BUILDER(allocQubitRegister), - MQT_NAMED_BUILDER(allocQubitRegister)}, + QIRTestCase{"AllocQubit", MQT_NAMED_BUILDER(allocQubit<>), + MQT_NAMED_BUILDER(allocQubit<>)}, + QIRTestCase{"AllocQubitRegister", + MQT_NAMED_BUILDER(allocQubitRegister<>), + MQT_NAMED_BUILDER(allocQubitRegister<>)}, QIRTestCase{"AllocMultipleQubitRegisters", - MQT_NAMED_BUILDER(allocMultipleQubitRegisters), - MQT_NAMED_BUILDER(allocMultipleQubitRegisters)}, - QIRTestCase{"AllocLargeRegister", MQT_NAMED_BUILDER(allocLargeRegister), - MQT_NAMED_BUILDER(allocLargeRegister)}, + MQT_NAMED_BUILDER(allocMultipleQubitRegisters<>), + MQT_NAMED_BUILDER(allocMultipleQubitRegisters<>)}, + QIRTestCase{"AllocLargeRegister", + MQT_NAMED_BUILDER(allocLargeRegister<>), + MQT_NAMED_BUILDER(allocLargeRegister<>)}, QIRTestCase{"StaticQubits", MQT_NAMED_BUILDER(staticQubits), MQT_NAMED_BUILDER(staticQubits)}, QIRTestCase{"StaticQubitsWithOps", diff --git a/mlir/unittests/Dialect/QTensor/IR/test_qtensor_ir.cpp b/mlir/unittests/Dialect/QTensor/IR/test_qtensor_ir.cpp index a76e20cc1a..a94ad944c0 100644 --- a/mlir/unittests/Dialect/QTensor/IR/test_qtensor_ir.cpp +++ b/mlir/unittests/Dialect/QTensor/IR/test_qtensor_ir.cpp @@ -33,7 +33,10 @@ #include #include #include +#include +#include #include +#include #include #include @@ -41,6 +44,7 @@ #include #include #include +#include using namespace mlir; using namespace mlir::qtensor; @@ -62,9 +66,11 @@ class QTensorTest : public ::testing::Test { } /// Build a module using the QCOProgramBuilder and run the cleanup pipeline. + template [[nodiscard]] OwningOpRef - buildAndCanonicalize(void (*buildFn)(QCOProgramBuilder&)) const { - auto module = QCOProgramBuilder::build(context.get(), buildFn); + buildAndCanonicalize(BuildFn&& buildFn) const { + auto module = + QCOProgramBuilder::build(context.get(), std::forward(buildFn)); if (!module) { return {}; } @@ -189,8 +195,10 @@ TEST_F(QTensorTest, AllocOpStaticTypeWithDynamicSizeOperandFailsVerification) { /// An alloc immediately followed by dealloc should be eliminated entirely. TEST_F(QTensorTest, DeallocOpAllocDeallocPairIsRemoved) { - auto canonicalized = - buildAndCanonicalize([](QCOProgramBuilder& b) { b.qtensorAlloc(3); }); + auto canonicalized = buildAndCanonicalize([](QCOProgramBuilder& b) { + b.qtensorAlloc(3); + return b.intConstant(0); + }); ASSERT_TRUE(canonicalized); EXPECT_TRUE(verify(*canonicalized).succeeded()); // Both AllocOp and DeallocOp should have been erased. @@ -416,8 +424,8 @@ TEST_F(QTensorTest, ResetAfterExtractThroughSameIndexInsertIsNotEliminated) { struct QTensorIntegrationTestCase { std::string name; - mqt::test::NamedBuilder programBuilder; - mqt::test::NamedBuilder referenceBuilder; + mqt::test::NamedMLIRBuilder programBuilder; + mqt::test::NamedMLIRBuilder referenceBuilder; friend std::ostream& operator<<(std::ostream& os, const QTensorIntegrationTestCase& info); @@ -449,7 +457,7 @@ TEST_P(QTensorIntegrationTest, ProgramEquivalence) { const auto name = " (" + GetParam().name + ")"; mqt::test::DeferredPrinter printer; - auto program = QCOProgramBuilder::build(context.get(), programBuilder.fn); + auto program = mqt::test::buildMLIRProgram(context.get(), programBuilder); ASSERT_TRUE(program); printer.record(program.get(), "Original QTensor IR" + name); EXPECT_TRUE(verify(*program).succeeded()); @@ -458,7 +466,7 @@ TEST_P(QTensorIntegrationTest, ProgramEquivalence) { printer.record(program.get(), "Canonicalized QTensor IR" + name); EXPECT_TRUE(verify(*program).succeeded()); - auto reference = QCOProgramBuilder::build(context.get(), referenceBuilder.fn); + auto reference = mqt::test::buildMLIRProgram(context.get(), referenceBuilder); ASSERT_TRUE(reference); printer.record(reference.get(), "Reference QTensor IR" + name); EXPECT_TRUE(verify(*reference).succeeded()); diff --git a/mlir/unittests/TestCaseUtils.h b/mlir/unittests/TestCaseUtils.h index 570c86c87f..2646b3207a 100644 --- a/mlir/unittests/TestCaseUtils.h +++ b/mlir/unittests/TestCaseUtils.h @@ -17,20 +17,25 @@ #include #include #include +#include +#include +#include +#include #include #include #include #include +#include #include namespace mqt::test { -template struct NamedBuilder { +template struct NamedBuilder { const char* name = nullptr; - void (*fn)(BuilderT&) = nullptr; + RetT (*fn)(BuilderT&) = nullptr; - constexpr NamedBuilder(const char* nameIn, void (*fnIn)(BuilderT&)) noexcept + constexpr NamedBuilder(const char* nameIn, RetT (*fnIn)(BuilderT&)) noexcept : name(nameIn), fn(fnIn) {} // NOLINTNEXTLINE(*-explicit-constructor) @@ -41,10 +46,62 @@ template struct NamedBuilder { } }; +template +[[nodiscard]] constexpr NamedBuilder +namedBuilder(const char* name, RetT (*fn)(BuilderT&)) noexcept { + return NamedBuilder{name, fn}; +} + +template struct NamedMLIRBuilder { + using SingleFn = mlir::Value (*)(BuilderT&); + using MultiFn = mlir::SmallVector (*)(BuilderT&); + + const char* name = nullptr; + std::variant fn; + + constexpr NamedMLIRBuilder(const char* nameIn, SingleFn fnIn) noexcept + : name(nameIn), fn(fnIn) {} + + constexpr NamedMLIRBuilder(const char* nameIn, MultiFn fnIn) noexcept + : name(nameIn), fn(fnIn) {} + + // NOLINTNEXTLINE(*-explicit-constructor) + constexpr NamedMLIRBuilder(std::nullptr_t) noexcept : fn(std::monostate{}) {} + + [[nodiscard]] constexpr explicit operator bool() const noexcept { + return !std::holds_alternative(fn); + } +}; + template -[[nodiscard]] constexpr NamedBuilder -namedBuilder(const char* name, void (*fn)(BuilderT&)) noexcept { - return NamedBuilder{name, fn}; +[[nodiscard]] constexpr NamedMLIRBuilder +namedBuilder(const char* name, mlir::Value (*fn)(BuilderT&)) noexcept { + return NamedMLIRBuilder{name, fn}; +} + +template +[[nodiscard]] constexpr NamedMLIRBuilder +namedBuilder(const char* name, + mlir::SmallVector (*fn)(BuilderT&)) noexcept { + return NamedMLIRBuilder{name, fn}; +} + +template +[[nodiscard]] mlir::OwningOpRef +buildMLIRProgram(mlir::MLIRContext* context, + const NamedMLIRBuilder& builder, Args&&... args) { + return std::visit( + [&](T fn) -> mlir::OwningOpRef { + if constexpr (requires { + BuilderT::build(context, fn, + std::forward(args)...); + }) { + return BuilderT::build(context, fn, std::forward(args)...); + } else { + return {}; + } + }, + builder.fn); } [[nodiscard]] constexpr const char* displayName(const char* name) noexcept { diff --git a/mlir/unittests/programs/qasm_programs.cpp b/mlir/unittests/programs/qasm_programs.cpp index 2386d7e8c7..97cf133e2a 100644 --- a/mlir/unittests/programs/qasm_programs.cpp +++ b/mlir/unittests/programs/qasm_programs.cpp @@ -18,29 +18,35 @@ namespace mlir::qasm { const std::string allocQubit = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit q; +bit c = measure q; )qasm"; const std::string allocQubitRegister = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[2] q; +bit[2] c = measure q; )qasm"; const std::string allocMultipleQubitRegisters = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[2] q0; qubit[3] q1; +bit[2] c0 = measure q0; +bit[3] c1 = measure q1; )qasm"; const std::string allocLargeRegister = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[100] q; +bit[2] c; +c[0] = measure q[0]; +c[1] = measure q[99]; )qasm"; const std::string singleMeasurementToSingleBit = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[1] q; -bit[1] c; -measure q[0] -> c[0]; +bit[1] c = measure q[0]; )qasm"; const std::string repeatedMeasurementToSameBit = R"qasm(OPENQASM 3.0; @@ -77,6 +83,7 @@ include "stdgates.inc"; qubit[1] q; h q[0]; reset q[0]; +bit[1] c = measure q; )qasm"; const std::string resetMultipleQubitsAfterSingleOp = R"qasm(OPENQASM 3.0; @@ -86,6 +93,7 @@ h q[0]; reset q[0]; h q[1]; reset q[1]; +bit[2] c = measure q; )qasm"; const std::string repeatedResetAfterSingleOp = R"qasm(OPENQASM 3.0; @@ -95,6 +103,7 @@ h q[0]; reset q[0]; reset q[0]; reset q[0]; +bit[1] c = measure q; )qasm"; const std::string globalPhase = R"qasm(OPENQASM 3.0; @@ -111,60 +120,70 @@ const std::string identity = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[1] q; id q[0]; +bit[1] c = measure q; )qasm"; const std::string singleControlledIdentity = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[2] q; ctrl @ id q[0], q[1]; +bit[2] c = measure q; )qasm"; const std::string multipleControlledIdentity = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[3] q; ctrl(2) @ id q[0], q[1], q[2]; +bit[3] c = measure q; )qasm"; const std::string x = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[1] q; x q[0]; +bit[1] c = measure q; )qasm"; const std::string twoX = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[2] q; x q; +bit[2] c = measure q; )qasm"; const std::string singleControlledX = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[2] q; ctrl @ x q[0], q[1]; +bit[2] c = measure q; )qasm"; const std::string singleNegControlledX = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[2] q; negctrl @ x q[0], q[1]; +bit[2] c = measure q; )qasm"; const std::string multipleControlledX = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[3] q; ctrl(2) @ x q[0], q[1], q[2]; +bit[3] c = measure q; )qasm"; const std::string tripleControlledXOpenQASM2 = R"qasm(OPENQASM 2.0; include "qelib1.inc"; qreg q[4]; cccx q[0], q[1], q[2], q[3]; +bit[4] c = measure q; )qasm"; const std::string mixedControlledX = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[3] q; ctrl @ negctrl @ x q[0], q[1], q[2]; +bit[3] c = measure q; )qasm"; const std::string twoMixedControlledX = R"qasm(OPENQASM 3.0; @@ -173,522 +192,611 @@ qubit[2] q1; qubit[2] q2; qubit[2] q3; ctrl @ negctrl @ x q1, q2, q3; +bit[2] c1 = measure q1; +bit[2] c2 = measure q2; +bit[2] c3 = measure q3; )qasm"; const std::string inverseX = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[1] q; inv @ x q[0]; +bit[1] c = measure q; )qasm"; const std::string inverseMultipleControlledX = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[3] q; inv @ ctrl(2) @ x q[0], q[1], q[2]; +bit[3] c = measure q; )qasm"; const std::string y = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[1] q; y q[0]; +bit[1] c = measure q; )qasm"; const std::string singleControlledY = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[2] q; ctrl @ y q[0], q[1]; +bit[2] c = measure q; )qasm"; const std::string multipleControlledY = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[3] q; ctrl(2) @ y q[0], q[1], q[2]; +bit[3] c = measure q; )qasm"; const std::string z = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[1] q; z q[0]; +bit[1] c = measure q; )qasm"; const std::string singleControlledZ = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[2] q; ctrl @ z q[0], q[1]; +bit[2] c = measure q; )qasm"; const std::string multipleControlledZ = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[3] q; ctrl(2) @ z q[0], q[1], q[2]; +bit[3] c = measure q; )qasm"; const std::string h = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[1] q; h q[0]; +bit[1] c = measure q; )qasm"; const std::string singleControlledH = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[2] q; ctrl @ h q[0], q[1]; +bit[2] c = measure q; )qasm"; const std::string multipleControlledH = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[3] q; ctrl(2) @ h q[0], q[1], q[2]; +bit[3] c = measure q; )qasm"; const std::string s = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[1] q; s q[0]; +bit[1] c = measure q; )qasm"; const std::string singleControlledS = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[2] q; ctrl @ s q[0], q[1]; +bit[2] c = measure q; )qasm"; const std::string multipleControlledS = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[3] q; ctrl(2) @ s q[0], q[1], q[2]; +bit[3] c = measure q; )qasm"; const std::string sdg = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[1] q; sdg q[0]; +bit[1] c = measure q; )qasm"; const std::string singleControlledSdg = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[2] q; ctrl @ sdg q[0], q[1]; +bit[2] c = measure q; )qasm"; const std::string multipleControlledSdg = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[3] q; ctrl(2) @ sdg q[0], q[1], q[2]; +bit[3] c = measure q; )qasm"; const std::string t_ = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[1] q; t q[0]; +bit[1] c = measure q; )qasm"; const std::string singleControlledT = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[2] q; ctrl @ t q[0], q[1]; +bit[2] c = measure q; )qasm"; const std::string multipleControlledT = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[3] q; ctrl(2) @ t q[0], q[1], q[2]; +bit[3] c = measure q; )qasm"; const std::string tdg = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[1] q; tdg q[0]; +bit[1] c = measure q; )qasm"; const std::string singleControlledTdg = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[2] q; ctrl @ tdg q[0], q[1]; +bit[2] c = measure q; )qasm"; const std::string multipleControlledTdg = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[3] q; ctrl(2) @ tdg q[0], q[1], q[2]; +bit[3] c = measure q; )qasm"; const std::string sx = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[1] q; sx q[0]; +bit[1] c = measure q; )qasm"; const std::string singleControlledSx = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[2] q; ctrl @ sx q[0], q[1]; +bit[2] c = measure q; )qasm"; const std::string multipleControlledSx = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[3] q; ctrl(2) @ sx q[0], q[1], q[2]; +bit[3] c = measure q; )qasm"; const std::string sxdg = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[1] q; sxdg q[0]; +bit[1] c = measure q; )qasm"; const std::string singleControlledSxdg = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[2] q; ctrl @ sxdg q[0], q[1]; +bit[2] c = measure q; )qasm"; const std::string multipleControlledSxdg = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[3] q; ctrl(2) @ sxdg q[0], q[1], q[2]; +bit[3] c = measure q; )qasm"; const std::string rx = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[1] q; rx(0.123) q[0]; +bit[1] c = measure q; )qasm"; const std::string singleControlledRx = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[2] q; ctrl @ rx(0.123) q[0], q[1]; +bit[2] c = measure q; )qasm"; const std::string multipleControlledRx = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[3] q; ctrl(2) @ rx(0.123) q[0], q[1], q[2]; +bit[3] c = measure q; )qasm"; const std::string ry = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[1] q; ry(0.456) q[0]; +bit[1] c = measure q; )qasm"; const std::string singleControlledRy = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[2] q; ctrl @ ry(0.456) q[0], q[1]; +bit[2] c = measure q; )qasm"; const std::string multipleControlledRy = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[3] q; ctrl(2) @ ry(0.456) q[0], q[1], q[2]; +bit[3] c = measure q; )qasm"; const std::string rz = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[1] q; rz(0.789) q[0]; +bit[1] c = measure q; )qasm"; const std::string singleControlledRz = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[2] q; ctrl @ rz(0.789) q[0], q[1]; +bit[2] c = measure q; )qasm"; const std::string multipleControlledRz = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[3] q; ctrl(2) @ rz(0.789) q[0], q[1], q[2]; +bit[3] c = measure q; )qasm"; const std::string p = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[1] q; p(0.123) q[0]; +bit[1] c = measure q; )qasm"; const std::string singleControlledP = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[2] q; ctrl @ p(0.123) q[0], q[1]; +bit[2] c = measure q; )qasm"; const std::string multipleControlledP = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[3] q; ctrl(2) @ p(0.123) q[0], q[1], q[2]; +bit[3] c = measure q; )qasm"; const std::string r = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[1] q; r(0.123, 0.456) q[0]; +bit[1] c = measure q; )qasm"; const std::string singleControlledR = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[2] q; ctrl @ r(0.123, 0.456) q[0], q[1]; +bit[2] c = measure q; )qasm"; const std::string multipleControlledR = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[3] q; ctrl(2) @ r(0.123, 0.456) q[0], q[1], q[2]; +bit[3] c = measure q; )qasm"; const std::string u2 = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[1] q; u2(0.234, 0.567) q[0]; +bit[1] c = measure q; )qasm"; const std::string singleControlledU2 = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[2] q; ctrl @ u2(0.234, 0.567) q[0], q[1]; +bit[2] c = measure q; )qasm"; const std::string multipleControlledU2 = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[3] q; ctrl(2) @ u2(0.234, 0.567) q[0], q[1], q[2]; +bit[3] c = measure q; )qasm"; const std::string u = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[1] q; u(0.1, 0.2, 0.3) q[0]; +bit[1] c = measure q; )qasm"; const std::string singleControlledU = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[2] q; ctrl @ u(0.1, 0.2, 0.3) q[0], q[1]; +bit[2] c = measure q; )qasm"; const std::string multipleControlledU = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[3] q; ctrl(2) @ u(0.1, 0.2, 0.3) q[0], q[1], q[2]; +bit[3] c = measure q; )qasm"; const std::string swap = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[2] q; swap q[0], q[1]; +bit[2] c = measure q; )qasm"; const std::string singleControlledSwap = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[3] q; ctrl @ swap q[0], q[1], q[2]; +bit[3] c = measure q; )qasm"; const std::string multipleControlledSwap = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[4] q; ctrl(2) @ swap q[0], q[1], q[2], q[3]; +bit[4] c = measure q; )qasm"; const std::string iswap = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[2] q; iswap q[0], q[1]; +bit[2] c = measure q; )qasm"; const std::string singleControlledIswap = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[3] q; ctrl @ iswap q[0], q[1], q[2]; +bit[3] c = measure q; )qasm"; const std::string multipleControlledIswap = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[4] q; ctrl(2) @ iswap q[0], q[1], q[2], q[3]; +bit[4] c = measure q; )qasm"; const std::string inverseIswap = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[2] q; inv @ iswap q[0], q[1]; +bit[2] c = measure q; )qasm"; const std::string inverseMultipleControlledIswap = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[4] q; inv @ ctrl(2) @ iswap q[0], q[1], q[2], q[3]; +bit[4] c = measure q; )qasm"; const std::string dcx = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[2] q; dcx q[0], q[1]; +bit[2] c = measure q; )qasm"; const std::string singleControlledDcx = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[3] q; ctrl @ dcx q[0], q[1], q[2]; +bit[3] c = measure q; )qasm"; const std::string multipleControlledDcx = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[4] q; ctrl(2) @ dcx q[0], q[1], q[2], q[3]; +bit[4] c = measure q; )qasm"; const std::string ecr = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[2] q; ecr q[0], q[1]; +bit[2] c = measure q; )qasm"; const std::string singleControlledEcr = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[3] q; ctrl @ ecr q[0], q[1], q[2]; +bit[3] c = measure q; )qasm"; const std::string multipleControlledEcr = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[4] q; ctrl(2) @ ecr q[0], q[1], q[2], q[3]; +bit[4] c = measure q; )qasm"; const std::string rxx = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[2] q; rxx(0.123) q[0], q[1]; +bit[2] c = measure q; )qasm"; const std::string singleControlledRxx = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[3] q; ctrl @ rxx(0.123) q[0], q[1], q[2]; +bit[3] c = measure q; )qasm"; const std::string multipleControlledRxx = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[4] q; ctrl(2) @ rxx(0.123) q[0], q[1], q[2], q[3]; +bit[4] c = measure q; )qasm"; const std::string tripleControlledRxx = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[5] q; ctrl(3) @ rxx(0.123) q[0], q[1], q[2], q[3], q[4]; +bit[5] c = measure q; )qasm"; const std::string ryy = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[2] q; ryy(0.123) q[0], q[1]; +bit[2] c = measure q; )qasm"; const std::string singleControlledRyy = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[3] q; ctrl @ ryy(0.123) q[0], q[1], q[2]; +bit[3] c = measure q; )qasm"; const std::string multipleControlledRyy = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[4] q; ctrl(2) @ ryy(0.123) q[0], q[1], q[2], q[3]; +bit[4] c = measure q; )qasm"; const std::string rzx = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[2] q; rzx(0.123) q[0], q[1]; +bit[2] c = measure q; )qasm"; const std::string singleControlledRzx = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[3] q; ctrl @ rzx(0.123) q[0], q[1], q[2]; +bit[3] c = measure q; )qasm"; const std::string multipleControlledRzx = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[4] q; ctrl(2) @ rzx(0.123) q[0], q[1], q[2], q[3]; +bit[4] c = measure q; )qasm"; const std::string rzz = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[2] q; rzz(0.123) q[0], q[1]; +bit[2] c = measure q; )qasm"; const std::string singleControlledRzz = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[3] q; ctrl @ rzz(0.123) q[0], q[1], q[2]; +bit[3] c = measure q; )qasm"; const std::string multipleControlledRzz = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[4] q; ctrl(2) @ rzz(0.123) q[0], q[1], q[2], q[3]; +bit[4] c = measure q; )qasm"; const std::string xxPlusYY = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[2] q; xx_plus_yy(0.123, 0.456) q[0], q[1]; +bit[2] c = measure q; )qasm"; const std::string singleControlledXxPlusYY = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[3] q; ctrl @ xx_plus_yy(0.123, 0.456) q[0], q[1], q[2]; +bit[3] c = measure q; )qasm"; const std::string multipleControlledXxPlusYY = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[4] q; ctrl(2) @ xx_plus_yy(0.123, 0.456) q[0], q[1], q[2], q[3]; +bit[4] c = measure q; )qasm"; const std::string xxMinusYY = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[2] q; xx_minus_yy(0.123, 0.456) q[0], q[1]; +bit[2] c = measure q; )qasm"; const std::string singleControlledXxMinusYY = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[3] q; ctrl @ xx_minus_yy(0.123, 0.456) q[0], q[1], q[2]; +bit[3] c = measure q; )qasm"; const std::string multipleControlledXxMinusYY = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[4] q; ctrl(2) @ xx_minus_yy(0.123, 0.456) q[0], q[1], q[2], q[3]; +bit[4] c = measure q; )qasm"; const std::string barrier = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[1] q; barrier q[0]; +bit[1] c = measure q; )qasm"; const std::string barrierTwoQubits = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[2] q; barrier q[0], q[1]; +bit[2] c = measure q; )qasm"; const std::string barrierMultipleQubits = R"qasm(OPENQASM 3.0; include "stdgates.inc"; qubit[3] q; barrier q[0], q[1], q[2]; +bit[3] c = measure q; )qasm"; const std::string ctrlTwo = R"qasm(OPENQASM 3.0; @@ -699,6 +807,7 @@ gate compound q0, q1 { rxx(0.123) q0, q1; } ctrl(2) @ compound q[0], q[1], q[2], q[3]; +bit[4] c = measure q; )qasm"; const std::string ctrlTwoMixed = R"qasm(OPENQASM 3.0; @@ -709,6 +818,7 @@ gate compound q0, q1 { rxx(0.123) q0, q1; } ctrl(2) @ compound q[0], q[1], q[2], q[3]; +bit[4] c = measure q; )qasm"; const std::string simpleIf = R"qasm(OPENQASM 3.0; @@ -719,6 +829,7 @@ bit c = measure q[0]; if (c) { x q[0]; } +bit[1] out = measure q; )qasm"; const std::string ifNot = R"qasm(OPENQASM 3.0; @@ -729,6 +840,7 @@ bit c = measure q[0]; if (!c) { x q[0]; } +output bit[1] out = measure q; )qasm"; const std::string ifTwoQubits = R"qasm(OPENQASM 3.0; @@ -740,6 +852,7 @@ if (c) { x q[0]; x q[1]; } +bit[2] out = measure q; )qasm"; const std::string ifEmptyThen = R"qasm(OPENQASM 3.0; @@ -751,6 +864,7 @@ if (c) { } else { x q[0]; } +output bit[1] out = measure q; )qasm"; const std::string ifElse = R"qasm(OPENQASM 3.0; @@ -763,6 +877,7 @@ if (c) { } else { z q[0]; } +bit[1] out = measure q; )qasm"; } // namespace mlir::qasm diff --git a/mlir/unittests/programs/qc_programs.cpp b/mlir/unittests/programs/qc_programs.cpp index 8410afd7a8..c54cc9c2d9 100644 --- a/mlir/unittests/programs/qc_programs.cpp +++ b/mlir/unittests/programs/qc_programs.cpp @@ -12,22 +12,40 @@ #include "mlir/Dialect/QC/Builder/QCProgramBuilder.h" +#include +#include +#include +#include + #include namespace mlir::qc { -void emptyQC([[maybe_unused]] QCProgramBuilder& builder) {} +/** + * @brief Measures the given qubits and returns the measurement outcomes. + * @param b The `ProgramBuilder` used to perform the measurements. + * @param qubits The qubits to be measured. + * @return The result values. + */ +static SmallVector measureAndReturn(QCProgramBuilder& b, + ValueRange qubits) { + return llvm::to_vector( + llvm::map_range(qubits, [&](Value q) { return b.measure(q); })); +} -void allocQubit(QCProgramBuilder& b) { b.allocQubit(); } +Value emptyQC(QCProgramBuilder& b) { return b.intConstant(0); } -void allocQubitRegister(QCProgramBuilder& b) { b.allocQubitRegister(2); } +Value allocQubit(QCProgramBuilder& b) { + auto q = b.allocQubit(); + return b.measure(q); +} -void allocMultipleQubitRegisters(QCProgramBuilder& b) { - b.allocQubitRegister(2); - b.allocQubitRegister(3); +Value allocQubitNoMeasure(QCProgramBuilder& b) { + b.allocQubit(); + return b.intConstant(0); } -void allocMultipleQubitRegistersWithOps(QCProgramBuilder& b) { +SmallVector allocMultipleQubitRegistersWithOps(QCProgramBuilder& b) { auto q0 = b.allocQubitRegister(2); auto q1 = b.allocQubitRegister(3); b.h(q0[0]); @@ -35,47 +53,84 @@ void allocMultipleQubitRegistersWithOps(QCProgramBuilder& b) { b.h(q1[0]); b.h(q1[1]); b.h(q1[2]); + return measureAndReturn(b, {q0[0], q0[1], q1[0], q1[1], q1[2]}); +} + +Value alloc1QubitRegister(QCProgramBuilder& b) { + auto q = b.allocQubitRegister(1); + return b.measure(q[0]); +} + +SmallVector allocQubitRegister(QCProgramBuilder& b) { + auto q = b.allocQubitRegister(2); + return measureAndReturn(b, q.qubits); +} + +SmallVector alloc3QubitRegister(QCProgramBuilder& b) { + auto q = b.allocQubitRegister(3); + return measureAndReturn(b, q.qubits); +} + +SmallVector allocMultipleQubitRegisters(QCProgramBuilder& b) { + auto q0 = b.allocQubitRegister(2); + auto q1 = b.allocQubitRegister(3); + return measureAndReturn(b, {q0[0], q0[1], q1[0], q1[1], q1[2]}); +} + +SmallVector allocLargeRegister(QCProgramBuilder& b) { + auto q = b.allocQubitRegister(100); + return measureAndReturn(b, {q[0], q[99]}); } -void allocLargeRegister(QCProgramBuilder& b) { b.allocQubitRegister(100); } +SmallVector staticQubits(QCProgramBuilder& b) { + auto q0 = b.staticQubit(0); + auto q1 = b.staticQubit(1); + return measureAndReturn(b, {q0, q1}); +} -void staticQubits(QCProgramBuilder& b) { +Value staticQubitsNoMeasure(QCProgramBuilder& b) { b.staticQubit(0); b.staticQubit(1); + return b.intConstant(0); } -void staticQubitsWithOps(QCProgramBuilder& b) { +SmallVector staticQubitsWithOps(QCProgramBuilder& b) { auto q0 = b.staticQubit(0); auto q1 = b.staticQubit(1); b.h(q0); b.h(q1); + return measureAndReturn(b, {q0, q1}); } -void staticQubitsWithParametricOps(QCProgramBuilder& b) { +SmallVector staticQubitsWithParametricOps(QCProgramBuilder& b) { auto q0 = b.staticQubit(0); auto q1 = b.staticQubit(1); b.rx(std::numbers::pi / 4., q0); b.p(std::numbers::pi / 2., q1); + return measureAndReturn(b, {q0, q1}); } -void staticQubitsWithTwoTargetOps(QCProgramBuilder& b) { +SmallVector staticQubitsWithTwoTargetOps(QCProgramBuilder& b) { auto q0 = b.staticQubit(0); auto q1 = b.staticQubit(1); b.rzz(0.123, q0, q1); + return measureAndReturn(b, {q0, q1}); } -void staticQubitsWithCtrl(QCProgramBuilder& b) { +SmallVector staticQubitsWithCtrl(QCProgramBuilder& b) { auto q0 = b.staticQubit(0); auto q1 = b.staticQubit(1); b.cx(q0, q1); + return measureAndReturn(b, {q0, q1}); } -void staticQubitsWithInv(QCProgramBuilder& b) { +Value staticQubitsWithInv(QCProgramBuilder& b) { auto q0 = b.staticQubit(0); - b.inv({q0}, [&](ValueRange qubits) { b.t(qubits[0]); }); + b.inv(q0, [&](Value qubit) { b.t(qubit); }); + return b.measure(q0); } -void staticQubitsWithDuplicates(QCProgramBuilder& b) { +SmallVector staticQubitsWithDuplicates(QCProgramBuilder& b) { const auto q0a = b.staticQubit(0); const auto q1a = b.staticQubit(1); const auto q0b = b.staticQubit(0); @@ -85,10 +140,11 @@ void staticQubitsWithDuplicates(QCProgramBuilder& b) { b.p(std::numbers::pi / 2., q1a); b.rzz(0.123, q0b, q1b); b.cx(q0b, q1b); - b.inv({q0a}, [&](ValueRange qubits) { b.t(qubits[0]); }); + b.inv(q0a, [&](Value qubit) { b.t(qubit); }); + return measureAndReturn(b, {q0b, q1b}); } -void staticQubitsCanonical(QCProgramBuilder& b) { +SmallVector staticQubitsCanonical(QCProgramBuilder& b) { const auto q0 = b.staticQubit(0); const auto q1 = b.staticQubit(1); @@ -96,1283 +152,1542 @@ void staticQubitsCanonical(QCProgramBuilder& b) { b.p(std::numbers::pi / 2., q1); b.rzz(0.123, q0, q1); b.cx(q0, q1); - b.inv({q0}, [&](ValueRange qubits) { b.t(qubits[0]); }); + b.inv(q0, [&](Value qubit) { b.t(qubit); }); + return measureAndReturn(b, {q0, q1}); } -void allocDeallocPair(QCProgramBuilder& b) { +Value allocDeallocPair(QCProgramBuilder& b) { auto q = b.allocQubit(); b.dealloc(q); + return b.intConstant(0); } -void mixedStaticThenDynamicQubit(QCProgramBuilder& b) { - b.staticQubit(0); - b.allocQubit(); +SmallVector mixedStaticThenDynamicQubit(QCProgramBuilder& b) { + auto q0 = b.staticQubit(0); + auto q1 = b.allocQubit(); + return measureAndReturn(b, {q0, q1}); } -void mixedDynamicRegisterThenStaticQubit(QCProgramBuilder& b) { - b.allocQubitRegister(2); - b.staticQubit(0); +SmallVector mixedDynamicRegisterThenStaticQubit(QCProgramBuilder& b) { + auto q0 = b.allocQubitRegister(2); + auto q1 = b.staticQubit(0); + return measureAndReturn(b, {q0[0], q0[1], q1}); } -void singleMeasurementToSingleBit(QCProgramBuilder& b) { +Value singleMeasurementToSingleBit(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); const auto& c = b.allocClassicalBitRegister(1); - b.measure(q[0], c[0]); + const auto outcome = b.measure(q[0], c[0]); + return outcome; } -void repeatedMeasurementToSameBit(QCProgramBuilder& b) { +Value repeatedMeasurementToSameBit(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); const auto& c = b.allocClassicalBitRegister(1); b.measure(q[0], c[0]); b.measure(q[0], c[0]); - b.measure(q[0], c[0]); + auto c3 = b.measure(q[0], c[0]); + return c3; } -void repeatedMeasurementToDifferentBits(QCProgramBuilder& b) { +SmallVector repeatedMeasurementToDifferentBits(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); const auto& c = b.allocClassicalBitRegister(3); - b.measure(q[0], c[0]); - b.measure(q[0], c[1]); - b.measure(q[0], c[2]); + auto c1 = b.measure(q[0], c[0]); + auto c2 = b.measure(q[0], c[1]); + auto c3 = b.measure(q[0], c[2]); + return {c1, c2, c3}; } -void multipleClassicalRegistersAndMeasurements(QCProgramBuilder& b) { +SmallVector +multipleClassicalRegistersAndMeasurements(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); const auto& c0 = b.allocClassicalBitRegister(1, "c0"); const auto& c1 = b.allocClassicalBitRegister(2, "c1"); - b.measure(q[0], c0[0]); - b.measure(q[1], c1[0]); - b.measure(q[2], c1[1]); + const auto b1 = b.measure(q[0], c0[0]); + const auto b2 = b.measure(q[1], c1[0]); + const auto b3 = b.measure(q[2], c1[1]); + return {b1, b2, b3}; } -void measurementWithoutRegisters(QCProgramBuilder& b) { +Value measurementWithoutRegisters(QCProgramBuilder& b) { auto q = b.allocQubit(); - b.measure(q); + auto c = b.measure(q); + return c; } -void resetQubitWithoutOp(QCProgramBuilder& b) { +Value resetQubitWithoutOp(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.reset(q[0]); + return b.measure(q[0]); } -void resetMultipleQubitsWithoutOp(QCProgramBuilder& b) { +SmallVector resetMultipleQubitsWithoutOp(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.reset(q[0]); b.reset(q[1]); + return measureAndReturn(b, q.qubits); } -void repeatedResetWithoutOp(QCProgramBuilder& b) { +Value repeatedResetWithoutOp(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.reset(q[0]); b.reset(q[0]); b.reset(q[0]); + return b.measure(q[0]); } -void resetQubitAfterSingleOp(QCProgramBuilder& b) { +Value resetQubitAfterSingleOp(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.h(q[0]); b.reset(q[0]); + return b.measure(q[0]); } -void resetMultipleQubitsAfterSingleOp(QCProgramBuilder& b) { +SmallVector resetMultipleQubitsAfterSingleOp(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.h(q[0]); b.reset(q[0]); b.h(q[1]); b.reset(q[1]); + return measureAndReturn(b, q.qubits); } -void repeatedResetAfterSingleOp(QCProgramBuilder& b) { +Value repeatedResetAfterSingleOp(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.h(q[0]); b.reset(q[0]); b.reset(q[0]); b.reset(q[0]); + return b.measure(q[0]); } -void globalPhase(QCProgramBuilder& b) { b.gphase(0.123); } +Value globalPhase(QCProgramBuilder& b) { + b.gphase(0.123); + return b.intConstant(0); +} + +Value globalPhaseAndMeasure(QCProgramBuilder& b) { + auto q = b.allocQubitRegister(1); + b.gphase(0.123); + return b.measure(q[0]); +} -void singleControlledGlobalPhase(QCProgramBuilder& b) { +Value singleControlledGlobalPhase(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.cgphase(0.123, q[0]); + return b.measure(q[0]); } -void multipleControlledGlobalPhase(QCProgramBuilder& b) { +SmallVector multipleControlledGlobalPhase(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.mcgphase(0.123, {q[0], q[1], q[2]}); + return measureAndReturn(b, q.qubits); } -void nestedControlledGlobalPhase(QCProgramBuilder& b) { - auto q = b.allocQubitRegister(3); - b.ctrl(q[0], {q[1]}, - [&](ValueRange targets) { b.cgphase(0.123, targets[0]); }); +SmallVector nestedControlledGlobalPhase(QCProgramBuilder& b) { + auto q = b.allocQubitRegister(2); + b.ctrl(q[0], q[1], [&](Value target) { b.cgphase(0.123, target); }); + return measureAndReturn(b, q.qubits); } -void trivialControlledGlobalPhase(QCProgramBuilder& b) { +Value trivialControlledGlobalPhase(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.mcgphase(0.123, {}); + return b.measure(q[0]); } -void inverseGlobalPhase(QCProgramBuilder& b) { - b.inv({}, [&](ValueRange /*qubits*/) { b.gphase(-0.123); }); +Value inverseGlobalPhase(QCProgramBuilder& b) { + b.inv(ValueRange{}, [&](ValueRange /*qubits*/) { b.gphase(-0.123); }); + return b.intConstant(0); } -void inverseMultipleControlledGlobalPhase(QCProgramBuilder& b) { +SmallVector inverseMultipleControlledGlobalPhase(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { b.mcgphase(-0.123, qubits); }); + return measureAndReturn(b, q.qubits); } -void identity(QCProgramBuilder& b) { +Value identity(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.id(q[0]); + return b.measure(q[0]); +} + +SmallVector singleControlledIdentity(QCProgramBuilder& b) { + auto q = b.allocQubitRegister(2); + b.cid(q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void singleControlledIdentity(QCProgramBuilder& b) { +SmallVector twoQubitsOneIdentity(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.cid(q[1], q[0]); + b.id(q[1]); + return measureAndReturn(b, q.qubits); +} + +SmallVector threeQubitsOneIdentity(QCProgramBuilder& b) { + auto q = b.allocQubitRegister(3); + b.id(q[2]); + return measureAndReturn(b, q.qubits); } -void multipleControlledIdentity(QCProgramBuilder& b) { +SmallVector multipleControlledIdentity(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.mcid({q[2], q[1]}, q[0]); + b.mcid({q[0], q[1]}, q[2]); + return measureAndReturn(b, q.qubits); +} + +SmallVector twoQubitsOneBarrier(QCProgramBuilder& b) { + auto q = b.allocQubitRegister(2); + b.barrier(q[0]); + return measureAndReturn(b, q.qubits); } -void nestedControlledIdentity(QCProgramBuilder& b) { +SmallVector nestedControlledIdentity(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.ctrl(q[2], {q[0], q[1]}, - [&](ValueRange targets) { b.cid(targets[1], targets[0]); }); + b.ctrl(q[0], {q[1], q[2]}, + [&](ValueRange targets) { b.cid(targets[0], targets[1]); }); + return measureAndReturn(b, q.qubits); } -void trivialControlledIdentity(QCProgramBuilder& b) { +Value trivialControlledIdentity(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.mcid({}, q[0]); + return b.measure(q[0]); } -void inverseIdentity(QCProgramBuilder& b) { +Value inverseIdentity(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv(q[0], [&](ValueRange qubits) { b.id(qubits[0]); }); + b.inv(q[0], [&](Value qubit) { b.id(qubit); }); + return b.measure(q[0]); } -void inverseMultipleControlledIdentity(QCProgramBuilder& b) { +SmallVector inverseMultipleControlledIdentity(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { b.mcid({qubits[0], qubits[1]}, qubits[2]); }); + return measureAndReturn(b, q.qubits); } -void x(QCProgramBuilder& b) { +Value x(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.x(q[0]); + return b.measure(q[0]); } -void singleControlledX(QCProgramBuilder& b) { +SmallVector singleControlledX(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.cx(q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void multipleControlledX(QCProgramBuilder& b) { +SmallVector multipleControlledX(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.mcx({q[0], q[1]}, q[2]); + return measureAndReturn(b, q.qubits); } -void nestedControlledX(QCProgramBuilder& b) { +SmallVector nestedControlledX(QCProgramBuilder& b) { auto reg = b.allocQubitRegister(3); b.ctrl(reg[0], {reg[1], reg[2]}, [&](ValueRange targets) { b.cx(targets[0], targets[1]); }); + return measureAndReturn(b, reg.qubits); } -void trivialControlledX(QCProgramBuilder& b) { +Value trivialControlledX(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.mcx({}, q[0]); + return b.measure(q[0]); } -void repeatedControlledX(QCProgramBuilder& b) { - auto q = b.allocQubitRegister(64); - b.h(q[0]); - for (auto i = 1; i < 64; i++) { - b.cx(q[0], q[i]); +SmallVector repeatedControlledX(QCProgramBuilder& b) { + auto control = b.allocQubit(); + b.h(control); + SmallVector qubits; + for (auto i = 0; i < 50; i++) { + auto qubit = b.allocQubit(); + b.cx(control, qubit); + qubits.push_back(qubit); } + qubits.push_back(control); + return measureAndReturn(b, qubits); } -void inverseX(QCProgramBuilder& b) { +Value inverseX(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv(q[0], [&](ValueRange qubits) { b.x(qubits[0]); }); + b.inv(q[0], [&](Value qubit) { b.x(qubit); }); + return b.measure(q[0]); } -void inverseMultipleControlledX(QCProgramBuilder& b) { +SmallVector inverseMultipleControlledX(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { b.mcx({qubits[0], qubits[1]}, qubits[2]); }); + return measureAndReturn(b, q.qubits); } -void y(QCProgramBuilder& b) { +Value y(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.y(q[0]); + return b.measure(q[0]); } -void singleControlledY(QCProgramBuilder& b) { +SmallVector singleControlledY(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.cy(q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void multipleControlledY(QCProgramBuilder& b) { +SmallVector multipleControlledY(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.mcy({q[0], q[1]}, q[2]); + return measureAndReturn(b, q.qubits); } -void nestedControlledY(QCProgramBuilder& b) { +SmallVector nestedControlledY(QCProgramBuilder& b) { auto reg = b.allocQubitRegister(3); b.ctrl(reg[0], {reg[1], reg[2]}, [&](ValueRange targets) { b.cy(targets[0], targets[1]); }); + return measureAndReturn(b, reg.qubits); } -void trivialControlledY(QCProgramBuilder& b) { +Value trivialControlledY(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.mcy({}, q[0]); + return b.measure(q[0]); } -void inverseY(QCProgramBuilder& b) { +Value inverseY(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv(q[0], [&](ValueRange qubits) { b.y(qubits[0]); }); + b.inv(q[0], [&](Value qubit) { b.y(qubit); }); + return b.measure(q[0]); } -void inverseMultipleControlledY(QCProgramBuilder& b) { +SmallVector inverseMultipleControlledY(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { b.mcy({qubits[0], qubits[1]}, qubits[2]); }); + return measureAndReturn(b, q.qubits); } -void z(QCProgramBuilder& b) { +Value z(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.z(q[0]); + return b.measure(q[0]); } -void singleControlledZ(QCProgramBuilder& b) { +SmallVector singleControlledZ(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.cz(q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void multipleControlledZ(QCProgramBuilder& b) { +SmallVector multipleControlledZ(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.mcz({q[0], q[1]}, q[2]); + return measureAndReturn(b, q.qubits); } -void nestedControlledZ(QCProgramBuilder& b) { +SmallVector nestedControlledZ(QCProgramBuilder& b) { auto reg = b.allocQubitRegister(3); b.ctrl(reg[0], {reg[1], reg[2]}, [&](ValueRange targets) { b.cz(targets[0], targets[1]); }); + return measureAndReturn(b, reg.qubits); } -void trivialControlledZ(QCProgramBuilder& b) { +Value trivialControlledZ(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.mcz({}, q[0]); + return b.measure(q[0]); } -void inverseZ(QCProgramBuilder& b) { +Value inverseZ(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv(q[0], [&](ValueRange qubits) { b.z(qubits[0]); }); + b.inv(q[0], [&](Value qubit) { b.z(qubit); }); + return b.measure(q[0]); } -void inverseMultipleControlledZ(QCProgramBuilder& b) { +SmallVector inverseMultipleControlledZ(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { b.mcz({qubits[0], qubits[1]}, qubits[2]); }); + return measureAndReturn(b, q.qubits); } -void h(QCProgramBuilder& b) { +Value h(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.h(q[0]); + return b.measure(q[0]); } -void singleControlledH(QCProgramBuilder& b) { +SmallVector singleControlledH(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.ch(q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void multipleControlledH(QCProgramBuilder& b) { +SmallVector multipleControlledH(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.mch({q[0], q[1]}, q[2]); + return measureAndReturn(b, q.qubits); } -void nestedControlledH(QCProgramBuilder& b) { +SmallVector nestedControlledH(QCProgramBuilder& b) { auto reg = b.allocQubitRegister(3); b.ctrl(reg[0], {reg[1], reg[2]}, [&](ValueRange targets) { b.ch(targets[0], targets[1]); }); + return measureAndReturn(b, reg.qubits); } -void trivialControlledH(QCProgramBuilder& b) { +Value trivialControlledH(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.mch({}, q[0]); + return b.measure(q[0]); } -void inverseH(QCProgramBuilder& b) { +Value inverseH(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv(q[0], [&](ValueRange qubits) { b.h(qubits[0]); }); + b.inv(q[0], [&](Value qubit) { b.h(qubit); }); + return b.measure(q[0]); } -void inverseMultipleControlledH(QCProgramBuilder& b) { +SmallVector inverseMultipleControlledH(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { b.mch({qubits[0], qubits[1]}, qubits[2]); }); + return measureAndReturn(b, q.qubits); } -void hWithoutRegister(QCProgramBuilder& b) { +Value hWithoutRegister(QCProgramBuilder& b) { auto q = b.allocQubit(); b.h(q); + return b.measure(q); } -void s(QCProgramBuilder& b) { +Value s(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.s(q[0]); + return b.measure(q[0]); } -void singleControlledS(QCProgramBuilder& b) { +SmallVector singleControlledS(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.cs(q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void multipleControlledS(QCProgramBuilder& b) { +SmallVector multipleControlledS(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.mcs({q[0], q[1]}, q[2]); + return measureAndReturn(b, q.qubits); } -void nestedControlledS(QCProgramBuilder& b) { +SmallVector nestedControlledS(QCProgramBuilder& b) { auto reg = b.allocQubitRegister(3); b.ctrl(reg[0], {reg[1], reg[2]}, [&](ValueRange targets) { b.cs(targets[0], targets[1]); }); + return measureAndReturn(b, reg.qubits); } -void trivialControlledS(QCProgramBuilder& b) { +Value trivialControlledS(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.mcs({}, q[0]); + return b.measure(q[0]); } -void inverseS(QCProgramBuilder& b) { +Value inverseS(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv(q[0], [&](ValueRange qubits) { b.s(qubits[0]); }); + b.inv(q[0], [&](Value qubit) { b.s(qubit); }); + return b.measure(q[0]); } -void inverseMultipleControlledS(QCProgramBuilder& b) { +SmallVector inverseMultipleControlledS(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { b.mcs({qubits[0], qubits[1]}, qubits[2]); }); + return measureAndReturn(b, q.qubits); } -void sdg(QCProgramBuilder& b) { +Value sdg(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.sdg(q[0]); + return b.measure(q[0]); } -void singleControlledSdg(QCProgramBuilder& b) { +SmallVector singleControlledSdg(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.csdg(q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void multipleControlledSdg(QCProgramBuilder& b) { +SmallVector multipleControlledSdg(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.mcsdg({q[0], q[1]}, q[2]); + return measureAndReturn(b, q.qubits); } -void nestedControlledSdg(QCProgramBuilder& b) { +SmallVector nestedControlledSdg(QCProgramBuilder& b) { auto reg = b.allocQubitRegister(3); b.ctrl(reg[0], {reg[1], reg[2]}, [&](ValueRange targets) { b.csdg(targets[0], targets[1]); }); + return measureAndReturn(b, reg.qubits); } -void trivialControlledSdg(QCProgramBuilder& b) { +Value trivialControlledSdg(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.mcsdg({}, q[0]); + return b.measure(q[0]); } -void inverseSdg(QCProgramBuilder& b) { +Value inverseSdg(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv(q[0], [&](ValueRange qubits) { b.sdg(qubits[0]); }); + b.inv(q[0], [&](Value qubit) { b.sdg(qubit); }); + return b.measure(q[0]); } -void inverseMultipleControlledSdg(QCProgramBuilder& b) { +SmallVector inverseMultipleControlledSdg(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { b.mcsdg({qubits[0], qubits[1]}, qubits[2]); }); + return measureAndReturn(b, q.qubits); } -void t_(QCProgramBuilder& b) { +Value t_(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.t(q[0]); + return b.measure(q[0]); } -void singleControlledT(QCProgramBuilder& b) { +SmallVector singleControlledT(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.ct(q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void multipleControlledT(QCProgramBuilder& b) { +SmallVector multipleControlledT(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.mct({q[0], q[1]}, q[2]); + return measureAndReturn(b, q.qubits); } -void nestedControlledT(QCProgramBuilder& b) { +SmallVector nestedControlledT(QCProgramBuilder& b) { auto reg = b.allocQubitRegister(3); b.ctrl(reg[0], {reg[1], reg[2]}, [&](ValueRange targets) { b.ct(targets[0], targets[1]); }); + return measureAndReturn(b, reg.qubits); } -void trivialControlledT(QCProgramBuilder& b) { +Value trivialControlledT(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.mct({}, q[0]); + return b.measure(q[0]); } -void inverseT(QCProgramBuilder& b) { +Value inverseT(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv(q[0], [&](ValueRange qubits) { b.t(qubits[0]); }); + b.inv(q[0], [&](Value qubit) { b.t(qubit); }); + return b.measure(q[0]); } -void inverseMultipleControlledT(QCProgramBuilder& b) { +SmallVector inverseMultipleControlledT(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { b.mct({qubits[0], qubits[1]}, qubits[2]); }); + return measureAndReturn(b, q.qubits); } -void tdg(QCProgramBuilder& b) { +Value tdg(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.tdg(q[0]); + return b.measure(q[0]); } -void singleControlledTdg(QCProgramBuilder& b) { +SmallVector singleControlledTdg(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.ctdg(q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void multipleControlledTdg(QCProgramBuilder& b) { +SmallVector multipleControlledTdg(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.mctdg({q[0], q[1]}, q[2]); + return measureAndReturn(b, q.qubits); } -void nestedControlledTdg(QCProgramBuilder& b) { +SmallVector nestedControlledTdg(QCProgramBuilder& b) { auto reg = b.allocQubitRegister(3); b.ctrl(reg[0], {reg[1], reg[2]}, [&](ValueRange targets) { b.ctdg(targets[0], targets[1]); }); + return measureAndReturn(b, reg.qubits); } -void trivialControlledTdg(QCProgramBuilder& b) { +Value trivialControlledTdg(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.mctdg({}, q[0]); + return b.measure(q[0]); } -void inverseTdg(QCProgramBuilder& b) { +Value inverseTdg(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv(q[0], [&](ValueRange qubits) { b.tdg(qubits[0]); }); + b.inv(q[0], [&](Value qubit) { b.tdg(qubit); }); + return b.measure(q[0]); } -void inverseMultipleControlledTdg(QCProgramBuilder& b) { +SmallVector inverseMultipleControlledTdg(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { b.mctdg({qubits[0], qubits[1]}, qubits[2]); }); + return measureAndReturn(b, q.qubits); } -void sx(QCProgramBuilder& b) { +Value sx(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.sx(q[0]); + return b.measure(q[0]); } -void singleControlledSx(QCProgramBuilder& b) { +SmallVector singleControlledSx(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.csx(q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void multipleControlledSx(QCProgramBuilder& b) { +SmallVector multipleControlledSx(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.mcsx({q[0], q[1]}, q[2]); + return measureAndReturn(b, q.qubits); } -void nestedControlledSx(QCProgramBuilder& b) { +SmallVector nestedControlledSx(QCProgramBuilder& b) { auto reg = b.allocQubitRegister(3); b.ctrl(reg[0], {reg[1], reg[2]}, [&](ValueRange targets) { b.csx(targets[0], targets[1]); }); + return measureAndReturn(b, reg.qubits); } -void trivialControlledSx(QCProgramBuilder& b) { +Value trivialControlledSx(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.mcsx({}, q[0]); + return b.measure(q[0]); } -void inverseSx(QCProgramBuilder& b) { +Value inverseSx(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv(q[0], [&](ValueRange qubits) { b.sx(qubits[0]); }); + b.inv(q[0], [&](Value qubit) { b.sx(qubit); }); + return b.measure(q[0]); } -void inverseMultipleControlledSx(QCProgramBuilder& b) { +SmallVector inverseMultipleControlledSx(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { b.mcsx({qubits[0], qubits[1]}, qubits[2]); }); + return measureAndReturn(b, q.qubits); } -void sxdg(QCProgramBuilder& b) { +Value sxdg(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.sxdg(q[0]); + return b.measure(q[0]); } -void singleControlledSxdg(QCProgramBuilder& b) { +SmallVector singleControlledSxdg(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.csxdg(q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void multipleControlledSxdg(QCProgramBuilder& b) { +SmallVector multipleControlledSxdg(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.mcsxdg({q[0], q[1]}, q[2]); + return measureAndReturn(b, q.qubits); } -void nestedControlledSxdg(QCProgramBuilder& b) { +SmallVector nestedControlledSxdg(QCProgramBuilder& b) { auto reg = b.allocQubitRegister(3); b.ctrl(reg[0], {reg[1], reg[2]}, [&](ValueRange targets) { b.csxdg(targets[0], targets[1]); }); + return measureAndReturn(b, reg.qubits); } -void trivialControlledSxdg(QCProgramBuilder& b) { +Value trivialControlledSxdg(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.mcsxdg({}, q[0]); + return b.measure(q[0]); } -void inverseSxdg(QCProgramBuilder& b) { +Value inverseSxdg(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv(q[0], [&](ValueRange qubits) { b.sxdg(qubits[0]); }); + b.inv(q[0], [&](Value qubit) { b.sxdg(qubit); }); + return b.measure(q[0]); } -void inverseMultipleControlledSxdg(QCProgramBuilder& b) { +SmallVector inverseMultipleControlledSxdg(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { b.mcsxdg({qubits[0], qubits[1]}, qubits[2]); }); + return measureAndReturn(b, q.qubits); } -void rx(QCProgramBuilder& b) { +Value rx(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.rx(0.123, q[0]); + return b.measure(q[0]); } -void singleControlledRx(QCProgramBuilder& b) { +SmallVector singleControlledRx(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.crx(0.123, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void multipleControlledRx(QCProgramBuilder& b) { +SmallVector multipleControlledRx(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.mcrx(0.123, {q[0], q[1]}, q[2]); + return measureAndReturn(b, q.qubits); } -void nestedControlledRx(QCProgramBuilder& b) { +SmallVector nestedControlledRx(QCProgramBuilder& b) { auto reg = b.allocQubitRegister(3); b.ctrl(reg[0], {reg[1], reg[2]}, [&](ValueRange targets) { b.crx(0.123, targets[0], targets[1]); }); + return measureAndReturn(b, reg.qubits); } -void trivialControlledRx(QCProgramBuilder& b) { +Value trivialControlledRx(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.mcrx(0.123, {}, q[0]); + return b.measure(q[0]); } -void inverseRx(QCProgramBuilder& b) { +Value inverseRx(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv(q[0], [&](ValueRange qubits) { b.rx(-0.123, qubits[0]); }); + b.inv(q[0], [&](Value qubit) { b.rx(-0.123, qubit); }); + return b.measure(q[0]); } -void inverseMultipleControlledRx(QCProgramBuilder& b) { +SmallVector inverseMultipleControlledRx(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { b.mcrx(-0.123, {qubits[0], qubits[1]}, qubits[2]); }); + return measureAndReturn(b, q.qubits); } -void ry(QCProgramBuilder& b) { +Value ry(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.ry(0.456, q[0]); + return b.measure(q[0]); } -void singleControlledRy(QCProgramBuilder& b) { +SmallVector singleControlledRy(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.cry(0.456, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void multipleControlledRy(QCProgramBuilder& b) { +SmallVector multipleControlledRy(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.mcry(0.456, {q[0], q[1]}, q[2]); + return measureAndReturn(b, q.qubits); } -void nestedControlledRy(QCProgramBuilder& b) { +SmallVector nestedControlledRy(QCProgramBuilder& b) { auto reg = b.allocQubitRegister(3); b.ctrl(reg[0], {reg[1], reg[2]}, [&](ValueRange targets) { b.cry(0.456, targets[0], targets[1]); }); + return measureAndReturn(b, reg.qubits); } -void trivialControlledRy(QCProgramBuilder& b) { +Value trivialControlledRy(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.mcry(0.456, {}, q[0]); + return b.measure(q[0]); } -void inverseRy(QCProgramBuilder& b) { +Value inverseRy(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv(q[0], [&](ValueRange qubits) { b.ry(-0.456, qubits[0]); }); + b.inv(q[0], [&](Value qubit) { b.ry(-0.456, qubit); }); + return b.measure(q[0]); } -void inverseMultipleControlledRy(QCProgramBuilder& b) { +SmallVector inverseMultipleControlledRy(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { b.mcry(-0.456, {qubits[0], qubits[1]}, qubits[2]); }); + return measureAndReturn(b, q.qubits); } -void rz(QCProgramBuilder& b) { +Value rz(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.rz(0.789, q[0]); + return b.measure(q[0]); } -void singleControlledRz(QCProgramBuilder& b) { +SmallVector singleControlledRz(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.crz(0.789, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void multipleControlledRz(QCProgramBuilder& b) { +SmallVector multipleControlledRz(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.mcrz(0.789, {q[0], q[1]}, q[2]); + return measureAndReturn(b, q.qubits); } -void nestedControlledRz(QCProgramBuilder& b) { +SmallVector nestedControlledRz(QCProgramBuilder& b) { auto reg = b.allocQubitRegister(3); b.ctrl(reg[0], {reg[1], reg[2]}, [&](ValueRange targets) { b.crz(0.789, targets[0], targets[1]); }); + return measureAndReturn(b, reg.qubits); } -void trivialControlledRz(QCProgramBuilder& b) { +Value trivialControlledRz(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.mcrz(0.789, {}, q[0]); + return b.measure(q[0]); } -void inverseRz(QCProgramBuilder& b) { +Value inverseRz(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv(q[0], [&](ValueRange qubits) { b.rz(-0.789, qubits[0]); }); + b.inv(q[0], [&](Value qubit) { b.rz(-0.789, qubit); }); + return b.measure(q[0]); } -void inverseMultipleControlledRz(QCProgramBuilder& b) { +SmallVector inverseMultipleControlledRz(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { b.mcrz(-0.789, {qubits[0], qubits[1]}, qubits[2]); }); + return measureAndReturn(b, q.qubits); } -void p(QCProgramBuilder& b) { +Value p(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.p(0.123, q[0]); + return b.measure(q[0]); } -void singleControlledP(QCProgramBuilder& b) { +SmallVector singleControlledP(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.cp(0.123, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void multipleControlledP(QCProgramBuilder& b) { +SmallVector multipleControlledP(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.mcp(0.123, {q[0], q[1]}, q[2]); + return measureAndReturn(b, q.qubits); } -void nestedControlledP(QCProgramBuilder& b) { +SmallVector nestedControlledP(QCProgramBuilder& b) { auto reg = b.allocQubitRegister(3); b.ctrl(reg[0], {reg[1], reg[2]}, [&](ValueRange targets) { b.cp(0.123, targets[0], targets[1]); }); + return measureAndReturn(b, reg.qubits); } -void trivialControlledP(QCProgramBuilder& b) { +Value trivialControlledP(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.mcp(0.123, {}, q[0]); + return b.measure(q[0]); } -void inverseP(QCProgramBuilder& b) { +Value inverseP(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv(q[0], [&](ValueRange qubits) { b.p(-0.123, qubits[0]); }); + b.inv(q[0], [&](Value qubit) { b.p(-0.123, qubit); }); + return b.measure(q[0]); } -void inverseMultipleControlledP(QCProgramBuilder& b) { +SmallVector inverseMultipleControlledP(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { b.mcp(-0.123, {qubits[0], qubits[1]}, qubits[2]); }); + return measureAndReturn(b, q.qubits); } -void r(QCProgramBuilder& b) { +Value r(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.r(0.123, 0.456, q[0]); + return b.measure(q[0]); } -void singleControlledR(QCProgramBuilder& b) { +SmallVector singleControlledR(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.cr(0.123, 0.456, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void multipleControlledR(QCProgramBuilder& b) { +SmallVector multipleControlledR(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.mcr(0.123, 0.456, {q[0], q[1]}, q[2]); + return measureAndReturn(b, q.qubits); } -void nestedControlledR(QCProgramBuilder& b) { +SmallVector nestedControlledR(QCProgramBuilder& b) { auto reg = b.allocQubitRegister(3); b.ctrl(reg[0], {reg[1], reg[2]}, [&](ValueRange targets) { b.cr(0.123, 0.456, targets[0], targets[1]); }); + return measureAndReturn(b, reg.qubits); } -void trivialControlledR(QCProgramBuilder& b) { +Value trivialControlledR(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.mcr(0.123, 0.456, {}, q[0]); + return b.measure(q[0]); } -void inverseR(QCProgramBuilder& b) { +Value inverseR(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv(q[0], [&](ValueRange qubits) { b.r(-0.123, 0.456, qubits[0]); }); + b.inv(q[0], [&](Value qubit) { b.r(-0.123, 0.456, qubit); }); + return b.measure(q[0]); } -void inverseMultipleControlledR(QCProgramBuilder& b) { +SmallVector inverseMultipleControlledR(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { b.mcr(-0.123, 0.456, {qubits[0], qubits[1]}, qubits[2]); }); + return measureAndReturn(b, q.qubits); } -void u2(QCProgramBuilder& b) { +Value u2(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.u2(0.234, 0.567, q[0]); + return b.measure(q[0]); } -void singleControlledU2(QCProgramBuilder& b) { +SmallVector singleControlledU2(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.cu2(0.234, 0.567, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void multipleControlledU2(QCProgramBuilder& b) { +SmallVector multipleControlledU2(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.mcu2(0.234, 0.567, {q[0], q[1]}, q[2]); + return measureAndReturn(b, q.qubits); } -void nestedControlledU2(QCProgramBuilder& b) { +SmallVector nestedControlledU2(QCProgramBuilder& b) { auto reg = b.allocQubitRegister(3); b.ctrl(reg[0], {reg[1], reg[2]}, [&](ValueRange targets) { b.cu2(0.234, 0.567, targets[0], targets[1]); }); + return measureAndReturn(b, reg.qubits); } -void trivialControlledU2(QCProgramBuilder& b) { +Value trivialControlledU2(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.mcu2(0.234, 0.567, {}, q[0]); + return b.measure(q[0]); } -void inverseU2(QCProgramBuilder& b) { +Value inverseU2(QCProgramBuilder& b) { constexpr double pi = std::numbers::pi; auto q = b.allocQubitRegister(1); - b.inv(q[0], - [&](ValueRange qubits) { b.u2(-0.567 + pi, -0.234 - pi, qubits[0]); }); + b.inv(q[0], [&](Value qubit) { b.u2(-0.567 + pi, -0.234 - pi, qubit); }); + return b.measure(q[0]); } -void inverseMultipleControlledU2(QCProgramBuilder& b) { +SmallVector inverseMultipleControlledU2(QCProgramBuilder& b) { constexpr double pi = std::numbers::pi; auto q = b.allocQubitRegister(3); b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { b.mcu2(-0.567 + pi, -0.234 - pi, {qubits[0], qubits[1]}, qubits[2]); }); + return measureAndReturn(b, q.qubits); } -void u(QCProgramBuilder& b) { +Value u(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.u(0.1, 0.2, 0.3, q[0]); + return b.measure(q[0]); } -void singleControlledU(QCProgramBuilder& b) { +SmallVector singleControlledU(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.cu(0.1, 0.2, 0.3, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void multipleControlledU(QCProgramBuilder& b) { +SmallVector multipleControlledU(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.mcu(0.1, 0.2, 0.3, {q[0], q[1]}, q[2]); + return measureAndReturn(b, q.qubits); } -void nestedControlledU(QCProgramBuilder& b) { +SmallVector nestedControlledU(QCProgramBuilder& b) { auto reg = b.allocQubitRegister(3); b.ctrl(reg[0], {reg[1], reg[2]}, [&](ValueRange targets) { b.cu(0.1, 0.2, 0.3, targets[0], targets[1]); }); + return measureAndReturn(b, reg.qubits); } -void trivialControlledU(QCProgramBuilder& b) { +Value trivialControlledU(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.mcu(0.1, 0.2, 0.3, {}, q[0]); + return b.measure(q[0]); } -void inverseU(QCProgramBuilder& b) { +Value inverseU(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv(q[0], [&](ValueRange qubits) { b.u(-0.1, -0.3, -0.2, qubits[0]); }); + b.inv(q[0], [&](Value qubit) { b.u(-0.1, -0.3, -0.2, qubit); }); + return b.measure(q[0]); } -void inverseMultipleControlledU(QCProgramBuilder& b) { +SmallVector inverseMultipleControlledU(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { b.mcu(-0.1, -0.3, -0.2, {qubits[0], qubits[1]}, qubits[2]); }); + return measureAndReturn(b, q.qubits); } -void swap(QCProgramBuilder& b) { +SmallVector swap(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.swap(q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void singleControlledSwap(QCProgramBuilder& b) { +SmallVector singleControlledSwap(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.cswap(q[0], q[1], q[2]); + return measureAndReturn(b, q.qubits); } -void multipleControlledSwap(QCProgramBuilder& b) { +SmallVector multipleControlledSwap(QCProgramBuilder& b) { auto q = b.allocQubitRegister(4); b.mcswap({q[0], q[1]}, q[2], q[3]); + return measureAndReturn(b, q.qubits); } -void nestedControlledSwap(QCProgramBuilder& b) { +SmallVector nestedControlledSwap(QCProgramBuilder& b) { auto reg = b.allocQubitRegister(4); b.ctrl(reg[0], {reg[1], reg[2], reg[3]}, [&](ValueRange targets) { b.cswap(targets[0], targets[1], targets[2]); }); + return measureAndReturn(b, reg.qubits); } -void trivialControlledSwap(QCProgramBuilder& b) { +SmallVector trivialControlledSwap(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.mcswap({}, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void inverseSwap(QCProgramBuilder& b) { +SmallVector inverseSwap(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.inv({q[0], q[1]}, [&](ValueRange qubits) { b.swap(qubits[0], qubits[1]); }); + return measureAndReturn(b, q.qubits); } -void inverseMultipleControlledSwap(QCProgramBuilder& b) { +SmallVector inverseMultipleControlledSwap(QCProgramBuilder& b) { auto q = b.allocQubitRegister(4); b.inv({q[0], q[1], q[2], q[3]}, [&](ValueRange qubits) { b.mcswap({qubits[0], qubits[1]}, qubits[2], qubits[3]); }); + return measureAndReturn(b, q.qubits); } -void iswap(QCProgramBuilder& b) { +SmallVector iswap(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.iswap(q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void singleControlledIswap(QCProgramBuilder& b) { +SmallVector singleControlledIswap(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.ciswap(q[0], q[1], q[2]); + return measureAndReturn(b, q.qubits); } -void multipleControlledIswap(QCProgramBuilder& b) { +SmallVector multipleControlledIswap(QCProgramBuilder& b) { auto q = b.allocQubitRegister(4); b.mciswap({q[0], q[1]}, q[2], q[3]); + return measureAndReturn(b, q.qubits); } -void nestedControlledIswap(QCProgramBuilder& b) { +SmallVector nestedControlledIswap(QCProgramBuilder& b) { auto reg = b.allocQubitRegister(4); b.ctrl(reg[0], {reg[1], reg[2], reg[3]}, [&](ValueRange targets) { b.ciswap(targets[0], targets[1], targets[2]); }); + return measureAndReturn(b, reg.qubits); } -void trivialControlledIswap(QCProgramBuilder& b) { +SmallVector trivialControlledIswap(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.mciswap({}, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void inverseIswap(QCProgramBuilder& b) { +SmallVector inverseIswap(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.inv({q[0], q[1]}, [&](ValueRange qubits) { b.iswap(qubits[0], qubits[1]); }); + return measureAndReturn(b, q.qubits); } -void inverseMultipleControlledIswap(QCProgramBuilder& b) { +SmallVector inverseMultipleControlledIswap(QCProgramBuilder& b) { auto q = b.allocQubitRegister(4); b.inv({q[0], q[1], q[2], q[3]}, [&](ValueRange qubits) { b.mciswap({qubits[0], qubits[1]}, qubits[2], qubits[3]); }); + return measureAndReturn(b, q.qubits); } -void dcx(QCProgramBuilder& b) { +SmallVector dcx(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.dcx(q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void singleControlledDcx(QCProgramBuilder& b) { +SmallVector singleControlledDcx(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.cdcx(q[0], q[1], q[2]); + return measureAndReturn(b, q.qubits); } -void multipleControlledDcx(QCProgramBuilder& b) { +SmallVector multipleControlledDcx(QCProgramBuilder& b) { auto q = b.allocQubitRegister(4); b.mcdcx({q[0], q[1]}, q[2], q[3]); + return measureAndReturn(b, q.qubits); } -void nestedControlledDcx(QCProgramBuilder& b) { +SmallVector nestedControlledDcx(QCProgramBuilder& b) { auto reg = b.allocQubitRegister(4); b.ctrl(reg[0], {reg[1], reg[2], reg[3]}, [&](ValueRange targets) { b.cdcx(targets[0], targets[1], targets[2]); }); + return measureAndReturn(b, reg.qubits); } -void trivialControlledDcx(QCProgramBuilder& b) { +SmallVector trivialControlledDcx(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.mcdcx({}, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void inverseDcx(QCProgramBuilder& b) { +SmallVector inverseDcx(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.inv({q[0], q[1]}, [&](ValueRange qubits) { b.dcx(qubits[1], qubits[0]); }); + return measureAndReturn(b, q.qubits); } -void inverseMultipleControlledDcx(QCProgramBuilder& b) { +SmallVector inverseMultipleControlledDcx(QCProgramBuilder& b) { auto q = b.allocQubitRegister(4); b.inv({q[0], q[1], q[3], q[2]}, [&](ValueRange qubits) { b.mcdcx({qubits[0], qubits[1]}, qubits[2], qubits[3]); }); + return measureAndReturn(b, q.qubits); } -void ecr(QCProgramBuilder& b) { +SmallVector ecr(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.ecr(q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void singleControlledEcr(QCProgramBuilder& b) { +SmallVector singleControlledEcr(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.cecr(q[0], q[1], q[2]); + return measureAndReturn(b, q.qubits); } -void multipleControlledEcr(QCProgramBuilder& b) { +SmallVector multipleControlledEcr(QCProgramBuilder& b) { auto q = b.allocQubitRegister(4); b.mcecr({q[0], q[1]}, q[2], q[3]); + return measureAndReturn(b, q.qubits); } -void nestedControlledEcr(QCProgramBuilder& b) { +SmallVector nestedControlledEcr(QCProgramBuilder& b) { auto reg = b.allocQubitRegister(4); b.ctrl(reg[0], {reg[1], reg[2], reg[3]}, [&](ValueRange targets) { b.cecr(targets[0], targets[1], targets[2]); }); + return measureAndReturn(b, reg.qubits); } -void trivialControlledEcr(QCProgramBuilder& b) { +SmallVector trivialControlledEcr(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.mcecr({}, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void inverseEcr(QCProgramBuilder& b) { +SmallVector inverseEcr(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.inv({q[0], q[1]}, [&](ValueRange qubits) { b.ecr(qubits[0], qubits[1]); }); + return measureAndReturn(b, q.qubits); } -void inverseMultipleControlledEcr(QCProgramBuilder& b) { +SmallVector inverseMultipleControlledEcr(QCProgramBuilder& b) { auto q = b.allocQubitRegister(4); b.inv({q[0], q[1], q[2], q[3]}, [&](ValueRange qubits) { b.mcecr({qubits[0], qubits[1]}, qubits[2], qubits[3]); }); + return measureAndReturn(b, q.qubits); } -void rxx(QCProgramBuilder& b) { +SmallVector rxx(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.rxx(0.123, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void singleControlledRxx(QCProgramBuilder& b) { +SmallVector singleControlledRxx(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.crxx(0.123, q[0], q[1], q[2]); + return measureAndReturn(b, q.qubits); } -void multipleControlledRxx(QCProgramBuilder& b) { +SmallVector multipleControlledRxx(QCProgramBuilder& b) { auto q = b.allocQubitRegister(4); b.mcrxx(0.123, {q[0], q[1]}, q[2], q[3]); + return measureAndReturn(b, q.qubits); } -void nestedControlledRxx(QCProgramBuilder& b) { +SmallVector nestedControlledRxx(QCProgramBuilder& b) { auto reg = b.allocQubitRegister(4); b.ctrl(reg[0], {reg[1], reg[2], reg[3]}, [&](ValueRange targets) { b.crxx(0.123, targets[0], targets[1], targets[2]); }); + return measureAndReturn(b, reg.qubits); } -void trivialControlledRxx(QCProgramBuilder& b) { +SmallVector trivialControlledRxx(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.mcrxx(0.123, {}, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void inverseRxx(QCProgramBuilder& b) { +SmallVector inverseRxx(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.inv({q[0], q[1]}, [&](ValueRange qubits) { b.rxx(-0.123, qubits[0], qubits[1]); }); + return measureAndReturn(b, q.qubits); } -void inverseMultipleControlledRxx(QCProgramBuilder& b) { +SmallVector inverseMultipleControlledRxx(QCProgramBuilder& b) { auto q = b.allocQubitRegister(4); b.inv({q[0], q[1], q[2], q[3]}, [&](ValueRange qubits) { b.mcrxx(-0.123, {qubits[0], qubits[1]}, qubits[2], qubits[3]); }); + return measureAndReturn(b, q.qubits); } -void tripleControlledRxx(QCProgramBuilder& b) { +SmallVector tripleControlledRxx(QCProgramBuilder& b) { auto q = b.allocQubitRegister(5); b.mcrxx(0.123, {q[0], q[1], q[2]}, q[3], q[4]); + return measureAndReturn(b, q.qubits); } -void fourControlledRxx(QCProgramBuilder& b) { +SmallVector fourControlledRxx(QCProgramBuilder& b) { auto q = b.allocQubitRegister(6); b.mcrxx(0.123, {q[0], q[1], q[2], q[3]}, q[4], q[5]); + return measureAndReturn(b, q.qubits); } -void ryy(QCProgramBuilder& b) { +SmallVector ryy(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.ryy(0.123, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void singleControlledRyy(QCProgramBuilder& b) { +SmallVector singleControlledRyy(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.cryy(0.123, q[0], q[1], q[2]); + return measureAndReturn(b, q.qubits); } -void multipleControlledRyy(QCProgramBuilder& b) { +SmallVector multipleControlledRyy(QCProgramBuilder& b) { auto q = b.allocQubitRegister(4); b.mcryy(0.123, {q[0], q[1]}, q[2], q[3]); + return measureAndReturn(b, q.qubits); } -void nestedControlledRyy(QCProgramBuilder& b) { +SmallVector nestedControlledRyy(QCProgramBuilder& b) { auto reg = b.allocQubitRegister(4); b.ctrl(reg[0], {reg[1], reg[2], reg[3]}, [&](ValueRange targets) { b.cryy(0.123, targets[0], targets[1], targets[2]); }); + return measureAndReturn(b, reg.qubits); } -void trivialControlledRyy(QCProgramBuilder& b) { +SmallVector trivialControlledRyy(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.mcryy(0.123, {}, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void inverseRyy(QCProgramBuilder& b) { +SmallVector inverseRyy(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.inv({q[0], q[1]}, [&](ValueRange qubits) { b.ryy(-0.123, qubits[0], qubits[1]); }); + return measureAndReturn(b, q.qubits); } -void inverseMultipleControlledRyy(QCProgramBuilder& b) { +SmallVector inverseMultipleControlledRyy(QCProgramBuilder& b) { auto q = b.allocQubitRegister(4); b.inv({q[0], q[1], q[2], q[3]}, [&](ValueRange qubits) { b.mcryy(-0.123, {qubits[0], qubits[1]}, qubits[2], qubits[3]); }); + return measureAndReturn(b, q.qubits); } -void rzx(QCProgramBuilder& b) { +SmallVector rzx(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.rzx(0.123, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void singleControlledRzx(QCProgramBuilder& b) { +SmallVector singleControlledRzx(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.crzx(0.123, q[0], q[1], q[2]); + return measureAndReturn(b, q.qubits); } -void multipleControlledRzx(QCProgramBuilder& b) { +SmallVector multipleControlledRzx(QCProgramBuilder& b) { auto q = b.allocQubitRegister(4); b.mcrzx(0.123, {q[0], q[1]}, q[2], q[3]); + return measureAndReturn(b, q.qubits); } -void nestedControlledRzx(QCProgramBuilder& b) { +SmallVector nestedControlledRzx(QCProgramBuilder& b) { auto reg = b.allocQubitRegister(4); b.ctrl(reg[0], {reg[1], reg[2], reg[3]}, [&](ValueRange targets) { b.crzx(0.123, targets[0], targets[1], targets[2]); }); + return measureAndReturn(b, reg.qubits); } -void trivialControlledRzx(QCProgramBuilder& b) { +SmallVector trivialControlledRzx(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.mcrzx(0.123, {}, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void inverseRzx(QCProgramBuilder& b) { +SmallVector inverseRzx(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.inv({q[0], q[1]}, [&](ValueRange qubits) { b.rzx(-0.123, qubits[0], qubits[1]); }); + return measureAndReturn(b, q.qubits); } -void inverseMultipleControlledRzx(QCProgramBuilder& b) { +SmallVector inverseMultipleControlledRzx(QCProgramBuilder& b) { auto q = b.allocQubitRegister(4); b.inv({q[0], q[1], q[2], q[3]}, [&](ValueRange qubits) { b.mcrzx(-0.123, {qubits[0], qubits[1]}, qubits[2], qubits[3]); }); + return measureAndReturn(b, q.qubits); } -void rzz(QCProgramBuilder& b) { +SmallVector rzz(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.rzz(0.123, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void singleControlledRzz(QCProgramBuilder& b) { +SmallVector singleControlledRzz(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.crzz(0.123, q[0], q[1], q[2]); + return measureAndReturn(b, q.qubits); } -void multipleControlledRzz(QCProgramBuilder& b) { +SmallVector multipleControlledRzz(QCProgramBuilder& b) { auto q = b.allocQubitRegister(4); b.mcrzz(0.123, {q[0], q[1]}, q[2], q[3]); + return measureAndReturn(b, q.qubits); } -void nestedControlledRzz(QCProgramBuilder& b) { +SmallVector nestedControlledRzz(QCProgramBuilder& b) { auto reg = b.allocQubitRegister(4); b.ctrl(reg[0], {reg[1], reg[2], reg[3]}, [&](ValueRange targets) { b.crzz(0.123, targets[0], targets[1], targets[2]); }); + return measureAndReturn(b, reg.qubits); } -void trivialControlledRzz(QCProgramBuilder& b) { +SmallVector trivialControlledRzz(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.mcrzz(0.123, {}, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void inverseRzz(QCProgramBuilder& b) { +SmallVector inverseRzz(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.inv({q[0], q[1]}, [&](ValueRange qubits) { b.rzz(-0.123, qubits[0], qubits[1]); }); + return measureAndReturn(b, q.qubits); } -void inverseMultipleControlledRzz(QCProgramBuilder& b) { +SmallVector inverseMultipleControlledRzz(QCProgramBuilder& b) { auto q = b.allocQubitRegister(4); b.inv({q[0], q[1], q[2], q[3]}, [&](ValueRange qubits) { b.mcrzz(-0.123, {qubits[0], qubits[1]}, qubits[2], qubits[3]); }); + return measureAndReturn(b, q.qubits); } -void xxPlusYY(QCProgramBuilder& b) { +SmallVector xxPlusYY(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.xx_plus_yy(0.123, 0.456, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void singleControlledXxPlusYY(QCProgramBuilder& b) { +SmallVector singleControlledXxPlusYY(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.cxx_plus_yy(0.123, 0.456, q[0], q[1], q[2]); + return measureAndReturn(b, q.qubits); } -void multipleControlledXxPlusYY(QCProgramBuilder& b) { +SmallVector multipleControlledXxPlusYY(QCProgramBuilder& b) { auto q = b.allocQubitRegister(4); b.mcxx_plus_yy(0.123, 0.456, {q[0], q[1]}, q[2], q[3]); + return measureAndReturn(b, q.qubits); } -void nestedControlledXxPlusYY(QCProgramBuilder& b) { +SmallVector nestedControlledXxPlusYY(QCProgramBuilder& b) { auto reg = b.allocQubitRegister(4); b.ctrl(reg[0], {reg[1], reg[2], reg[3]}, [&](ValueRange targets) { b.cxx_plus_yy(0.123, 0.456, targets[0], targets[1], targets[2]); }); + return measureAndReturn(b, reg.qubits); } -void trivialControlledXxPlusYY(QCProgramBuilder& b) { +SmallVector trivialControlledXxPlusYY(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.mcxx_plus_yy(0.123, 0.456, {}, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void inverseXxPlusYY(QCProgramBuilder& b) { +SmallVector inverseXxPlusYY(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.inv({q[0], q[1]}, [&](ValueRange qubits) { b.xx_plus_yy(-0.123, 0.456, qubits[0], qubits[1]); }); + return measureAndReturn(b, q.qubits); } -void inverseMultipleControlledXxPlusYY(QCProgramBuilder& b) { +SmallVector inverseMultipleControlledXxPlusYY(QCProgramBuilder& b) { auto q = b.allocQubitRegister(4); b.inv({q[0], q[1], q[2], q[3]}, [&](ValueRange qubits) { b.mcxx_plus_yy(-0.123, 0.456, {qubits[0], qubits[1]}, qubits[2], qubits[3]); }); + return measureAndReturn(b, q.qubits); } -void xxMinusYY(QCProgramBuilder& b) { +SmallVector xxMinusYY(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.xx_minus_yy(0.123, 0.456, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void singleControlledXxMinusYY(QCProgramBuilder& b) { +SmallVector singleControlledXxMinusYY(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.cxx_minus_yy(0.123, 0.456, q[0], q[1], q[2]); + return measureAndReturn(b, q.qubits); } -void multipleControlledXxMinusYY(QCProgramBuilder& b) { +SmallVector multipleControlledXxMinusYY(QCProgramBuilder& b) { auto q = b.allocQubitRegister(4); b.mcxx_minus_yy(0.123, 0.456, {q[0], q[1]}, q[2], q[3]); + return measureAndReturn(b, q.qubits); } -void nestedControlledXxMinusYY(QCProgramBuilder& b) { +SmallVector nestedControlledXxMinusYY(QCProgramBuilder& b) { auto reg = b.allocQubitRegister(4); b.ctrl(reg[0], {reg[1], reg[2], reg[3]}, [&](ValueRange targets) { b.cxx_minus_yy(0.123, 0.456, targets[0], targets[1], targets[2]); }); + return measureAndReturn(b, reg.qubits); } -void trivialControlledXxMinusYY(QCProgramBuilder& b) { +SmallVector trivialControlledXxMinusYY(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.mcxx_minus_yy(0.123, 0.456, {}, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void inverseXxMinusYY(QCProgramBuilder& b) { +SmallVector inverseXxMinusYY(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.inv({q[0], q[1]}, [&](ValueRange qubits) { b.xx_minus_yy(-0.123, 0.456, qubits[0], qubits[1]); }); + return measureAndReturn(b, q.qubits); } -void inverseMultipleControlledXxMinusYY(QCProgramBuilder& b) { +SmallVector inverseMultipleControlledXxMinusYY(QCProgramBuilder& b) { auto q = b.allocQubitRegister(4); b.inv({q[0], q[1], q[2], q[3]}, [&](ValueRange qubits) { b.mcxx_minus_yy(-0.123, 0.456, {qubits[0], qubits[1]}, qubits[2], qubits[3]); }); + return measureAndReturn(b, q.qubits); } -void barrier(QCProgramBuilder& b) { +Value barrier(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.barrier(q[0]); + return b.measure(q[0]); } -void barrierTwoQubits(QCProgramBuilder& b) { +SmallVector barrierTwoQubits(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.barrier({q[0], q[1]}); + return measureAndReturn(b, q.qubits); } -void barrierMultipleQubits(QCProgramBuilder& b) { +SmallVector barrierMultipleQubits(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.barrier({q[0], q[1], q[2]}); + return measureAndReturn(b, q.qubits); } -void singleControlledBarrier(QCProgramBuilder& b) { +SmallVector singleControlledBarrier(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.ctrl(q[1], q[0], [&](ValueRange targets) { b.barrier(targets[0]); }); + b.ctrl(q[1], q[0], [&](Value target) { b.barrier({target}); }); + return measureAndReturn(b, q.qubits); } -void inverseBarrier(QCProgramBuilder& b) { +Value inverseBarrier(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv(q[0], [&](ValueRange qubits) { b.barrier(qubits[0]); }); + b.inv(q[0], [&](Value qubit) { b.barrier(qubit); }); + return b.measure(q[0]); } -void trivialCtrl(QCProgramBuilder& b) { +SmallVector trivialCtrl(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.ctrl({}, {q[0], q[1]}, [&](ValueRange targets) { b.rxx(0.123, targets[0], targets[1]); }); + return measureAndReturn(b, q.qubits); } -void emptyCtrl(QCProgramBuilder& b) { +SmallVector emptyCtrl(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.rxx(0.123, q[0], q[1]); - b.ctrl({q[0]}, {q[1]}, [&](ValueRange /*targets*/) {}); + b.ctrl(q[0], q[1], [&](Value /*target*/) {}); + return measureAndReturn(b, q.qubits); } -void nestedCtrl(QCProgramBuilder& b) { +SmallVector nestedCtrl(QCProgramBuilder& b) { auto q = b.allocQubitRegister(4); b.ctrl(q[0], {q[1], q[2], q[3]}, [&](ValueRange targets) { b.ctrl(targets[0], {targets[1], targets[2]}, [&](ValueRange innerTargets) { b.rxx(0.123, innerTargets[0], innerTargets[1]); }); }); + return measureAndReturn(b, q.qubits); } -void tripleNestedCtrl(QCProgramBuilder& b) { +SmallVector tripleNestedCtrl(QCProgramBuilder& b) { auto q = b.allocQubitRegister(5); b.ctrl(q[0], {q[1], q[2], q[3], q[4]}, [&](ValueRange targets) { b.ctrl(targets[0], {targets[1], targets[2], targets[3]}, @@ -1383,9 +1698,10 @@ void tripleNestedCtrl(QCProgramBuilder& b) { }); }); }); + return measureAndReturn(b, q.qubits); } -void doubleNestedCtrlTwoQubits(QCProgramBuilder& b) { +SmallVector doubleNestedCtrlTwoQubits(QCProgramBuilder& b) { auto q = b.allocQubitRegister(6); b.ctrl({q[0], q[1]}, {q[2], q[3], q[4], q[5]}, [&](ValueRange targets) { b.ctrl({targets[0], targets[1]}, {targets[2], targets[3]}, @@ -1393,9 +1709,10 @@ void doubleNestedCtrlTwoQubits(QCProgramBuilder& b) { b.rxx(0.123, innerTargets[0], innerTargets[1]); }); }); + return measureAndReturn(b, q.qubits); } -void ctrlInvSandwich(QCProgramBuilder& b) { +SmallVector ctrlInvSandwich(QCProgramBuilder& b) { auto q = b.allocQubitRegister(4); b.ctrl(q[0], {q[1], q[2], q[3]}, [&](ValueRange targets) { b.inv(targets, [&](ValueRange qubits) { @@ -1404,25 +1721,28 @@ void ctrlInvSandwich(QCProgramBuilder& b) { }); }); }); + return measureAndReturn(b, q.qubits); } -void ctrlTwo(QCProgramBuilder& b) { +SmallVector ctrlTwo(QCProgramBuilder& b) { auto q = b.allocQubitRegister(4); b.ctrl({q[0], q[1]}, {q[2], q[3]}, [&](ValueRange targets) { b.x(targets[0]); b.rxx(0.123, targets[0], targets[1]); }); + return measureAndReturn(b, q.qubits); } -void ctrlTwoMixed(QCProgramBuilder& b) { +SmallVector ctrlTwoMixed(QCProgramBuilder& b) { auto q = b.allocQubitRegister(4); b.ctrl({q[0], q[1]}, {q[2], q[3]}, [&](ValueRange targets) { b.cx(targets[0], targets[1]); b.rxx(0.123, targets[0], targets[1]); }); + return measureAndReturn(b, q.qubits); } -void nestedCtrlTwo(QCProgramBuilder& b) { +SmallVector nestedCtrlTwo(QCProgramBuilder& b) { auto q = b.allocQubitRegister(4); b.ctrl(q[0], {q[1], q[2], q[3]}, [&](ValueRange targets) { b.ctrl(targets[0], {targets[1], targets[2]}, [&](ValueRange innerTargets) { @@ -1430,9 +1750,10 @@ void nestedCtrlTwo(QCProgramBuilder& b) { b.rxx(0.123, innerTargets[0], innerTargets[1]); }); }); + return measureAndReturn(b, q.qubits); } -void ctrlInvTwo(QCProgramBuilder& b) { +SmallVector ctrlInvTwo(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.ctrl(q[0], {q[1], q[2]}, [&](ValueRange targets) { b.inv(targets, [&](ValueRange qubits) { @@ -1440,24 +1761,27 @@ void ctrlInvTwo(QCProgramBuilder& b) { b.rxx(0.123, qubits[0], qubits[1]); }); }); + return measureAndReturn(b, q.qubits); } -void emptyInv(QCProgramBuilder& b) { +SmallVector emptyInv(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.rxx(0.123, q[0], q[1]); b.inv({q[0], q[1]}, [&](ValueRange /*targets*/) {}); + return measureAndReturn(b, q.qubits); } -void nestedInv(QCProgramBuilder& b) { +SmallVector nestedInv(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.inv({q[0], q[1]}, [&](ValueRange qubits) { b.inv(qubits, [&](ValueRange innerQubits) { b.rxx(0.123, innerQubits[0], innerQubits[1]); }); }); + return measureAndReturn(b, q.qubits); } -void tripleNestedInv(QCProgramBuilder& b) { +SmallVector tripleNestedInv(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.inv({q[0], q[1]}, [&](ValueRange qubits) { b.inv(qubits, [&](ValueRange innerQubits) { @@ -1466,9 +1790,10 @@ void tripleNestedInv(QCProgramBuilder& b) { }); }); }); + return measureAndReturn(b, q.qubits); } -void invCtrlSandwich(QCProgramBuilder& b) { +SmallVector invCtrlSandwich(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { b.ctrl(qubits[0], {qubits[1], qubits[2]}, [&](ValueRange targets) { @@ -1477,17 +1802,19 @@ void invCtrlSandwich(QCProgramBuilder& b) { }); }); }); + return measureAndReturn(b, q.qubits); } -void invTwo(QCProgramBuilder& b) { +SmallVector invTwo(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.inv({q[0], q[1]}, [&](ValueRange qubits) { b.x(qubits[0]); b.rxx(0.123, qubits[0], qubits[1]); }); + return measureAndReturn(b, q.qubits); } -void invCtrlTwo(QCProgramBuilder& b) { +SmallVector invCtrlTwo(QCProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { b.ctrl(qubits[0], {qubits[1], qubits[2]}, [&](ValueRange targets) { @@ -1495,16 +1822,28 @@ void invCtrlTwo(QCProgramBuilder& b) { b.rxx(0.123, targets[0], targets[1]); }); }); + return measureAndReturn(b, q.qubits); } -void simpleIf(QCProgramBuilder& b) { +SmallVector simpleIf(QCProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.h(q[0]); auto cond = b.measure(q[0]); b.scfIf(cond, [&] { b.x(q[0]); }); + auto res = b.measure(q[0]); + return {cond, res}; } -void ifTwoQubits(QCProgramBuilder& b) { +SmallVector ifElse(QCProgramBuilder& b) { + auto q = b.allocQubitRegister(1); + b.h(q[0]); + auto cond = b.measure(q[0]); + b.scfIf(cond, [&] { b.x(q[0]); }, [&] { b.z(q[0]); }); + auto bit = b.measure(q[0]); + return {cond, bit}; +} + +SmallVector ifTwoQubits(QCProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.h(q[0]); auto cond = b.measure(q[0]); @@ -1512,16 +1851,12 @@ void ifTwoQubits(QCProgramBuilder& b) { b.x(q[0]); b.x(q[1]); }); + auto c0 = b.measure(q[0]); + auto c1 = b.measure(q[1]); + return {cond, c0, c1}; } -void ifElse(QCProgramBuilder& b) { - auto q = b.allocQubitRegister(1); - b.h(q[0]); - auto cond = b.measure(q[0]); - b.scfIf(cond, [&] { b.x(q[0]); }, [&] { b.z(q[0]); }); -} - -void nestedIfOpForLoop(QCProgramBuilder& b) { +Value nestedIfOpForLoop(QCProgramBuilder& b) { auto reg = b.allocQubitRegister(3); auto q0 = b.allocQubit(); b.h(q0); @@ -1534,9 +1869,10 @@ void nestedIfOpForLoop(QCProgramBuilder& b) { b.h(q1); }); }); + return b.measure(q0); } -void simpleWhileReset(QCProgramBuilder& b) { +Value simpleWhileReset(QCProgramBuilder& b) { auto q = b.allocQubit(); b.h(q); b.scfWhile( @@ -1545,9 +1881,10 @@ void simpleWhileReset(QCProgramBuilder& b) { b.scfCondition(measureResult); }, [&] { b.h(q); }); + return b.measure(q); } -void simpleDoWhileReset(QCProgramBuilder& b) { +Value simpleDoWhileReset(QCProgramBuilder& b) { auto q = b.allocQubit(); b.scfWhile( [&] { @@ -1556,17 +1893,19 @@ void simpleDoWhileReset(QCProgramBuilder& b) { b.scfCondition(measureResult); }, [&] {}); + return b.measure(q); } -void simpleForLoop(QCProgramBuilder& b) { +SmallVector simpleForLoop(QCProgramBuilder& b) { auto reg = b.allocQubitRegister(2); b.scfFor(0, 2, 1, [&](Value iv) { auto q = b.memrefLoad(reg.value, iv); b.h(q); }); + return measureAndReturn(b, reg.qubits); }; -void nestedForLoopIfOp(QCProgramBuilder& b) { +Value nestedForLoopIfOp(QCProgramBuilder& b) { auto reg = b.allocQubitRegister(2); auto qCond = b.allocQubit(); b.scfFor(0, 2, 1, [&](Value iv) { @@ -1577,9 +1916,10 @@ void nestedForLoopIfOp(QCProgramBuilder& b) { b.h(q); }); }); + return b.measure(qCond); } -void nestedForLoopWhileOp(QCProgramBuilder& b) { +SmallVector nestedForLoopWhileOp(QCProgramBuilder& b) { auto reg = b.allocQubitRegister(2); b.scfFor(0, 2, 1, [&](Value iv) { auto q = b.memrefLoad(reg.value, iv); @@ -1594,9 +1934,10 @@ void nestedForLoopWhileOp(QCProgramBuilder& b) { }, [&] { b.h(q); }); }); + return measureAndReturn(b, reg.qubits); } -void nestedForLoopCtrlOpWithSeparateQubit(QCProgramBuilder& b) { +Value nestedForLoopCtrlOpWithSeparateQubit(QCProgramBuilder& b) { auto reg = b.allocQubitRegister(3); auto control = b.allocQubit(); b.h(control); @@ -1605,9 +1946,10 @@ void nestedForLoopCtrlOpWithSeparateQubit(QCProgramBuilder& b) { b.h(q0); b.cx(control, q0); }); + return b.measure(control); } -void nestedForLoopCtrlOpWithExtractedQubit(QCProgramBuilder& b) { +Value nestedForLoopCtrlOpWithExtractedQubit(QCProgramBuilder& b) { auto reg = b.allocQubitRegister(4); b.h(reg[0]); b.scfFor(1, 4, 1, [&](Value iv) { @@ -1615,6 +1957,7 @@ void nestedForLoopCtrlOpWithExtractedQubit(QCProgramBuilder& b) { b.h(q0); b.cx(reg[0], q0); }); + return b.measure(reg[0]); } } // namespace mlir::qc diff --git a/mlir/unittests/programs/qc_programs.h b/mlir/unittests/programs/qc_programs.h index 25d76718cc..4849d861c7 100644 --- a/mlir/unittests/programs/qc_programs.h +++ b/mlir/unittests/programs/qc_programs.h @@ -10,906 +10,934 @@ #pragma once +#include +#include + namespace mlir::qc { class QCProgramBuilder; /// Creates an empty QC Program. -void emptyQC(QCProgramBuilder& builder); +Value emptyQC(QCProgramBuilder& b); // --- Qubit Management ----------------------------------------------------- // /// Allocates a single qubit. -void allocQubit(QCProgramBuilder& b); +Value allocQubit(QCProgramBuilder& b); + +/// Allocates a single qubit without measuring it. +Value allocQubitNoMeasure(QCProgramBuilder& b); + +/// Allocates a qubit register of size `1`. +Value alloc1QubitRegister(QCProgramBuilder& b); /// Allocates a qubit register of size `2`. -void allocQubitRegister(QCProgramBuilder& b); +SmallVector allocQubitRegister(QCProgramBuilder& b); + +/// Allocates a qubit register of size `3`. +SmallVector alloc3QubitRegister(QCProgramBuilder& b); /// Allocates two qubit registers of size `2` and `3`. -void allocMultipleQubitRegisters(QCProgramBuilder& b); +SmallVector allocMultipleQubitRegisters(QCProgramBuilder& b); /// Allocates two qubit registers of size `2` and `3` and applies operations. -void allocMultipleQubitRegistersWithOps(QCProgramBuilder& b); +SmallVector allocMultipleQubitRegistersWithOps(QCProgramBuilder& b); /// Allocates a large qubit register. -void allocLargeRegister(QCProgramBuilder& b); +SmallVector allocLargeRegister(QCProgramBuilder& b); /// Allocates two inline qubits. -void staticQubits(QCProgramBuilder& b); +SmallVector staticQubits(QCProgramBuilder& b); + +/// Allocates two inline qubits without measuring them. +Value staticQubitsNoMeasure(QCProgramBuilder& b); /// Allocates two static qubits and applies operations. -void staticQubitsWithOps(QCProgramBuilder& b); +SmallVector staticQubitsWithOps(QCProgramBuilder& b); /// Allocates two static qubits and applies parametric gates. -void staticQubitsWithParametricOps(QCProgramBuilder& b); +SmallVector staticQubitsWithParametricOps(QCProgramBuilder& b); /// Allocates two static qubits and applies a two-target gate. -void staticQubitsWithTwoTargetOps(QCProgramBuilder& b); +SmallVector staticQubitsWithTwoTargetOps(QCProgramBuilder& b); /// Allocates two static qubits and applies a controlled gate. -void staticQubitsWithCtrl(QCProgramBuilder& b); +SmallVector staticQubitsWithCtrl(QCProgramBuilder& b); /// Allocates a static qubit and applies an inverse modifier. -void staticQubitsWithInv(QCProgramBuilder& b); +Value staticQubitsWithInv(QCProgramBuilder& b); /// Allocates duplicate static qubits and applies operations on both. -void staticQubitsWithDuplicates(QCProgramBuilder& b); +SmallVector staticQubitsWithDuplicates(QCProgramBuilder& b); /// Same as `staticQubitsWithDuplicates`, but with canonical static qubit /// retrievals. -void staticQubitsCanonical(QCProgramBuilder& b); +SmallVector staticQubitsCanonical(QCProgramBuilder& b); /// Allocates and explicitly deallocates a single qubit. -void allocDeallocPair(QCProgramBuilder& b); +Value allocDeallocPair(QCProgramBuilder& b); // --- Invalid / mixed addressing (unit tests) -------------------------------- /// @pre `builder.initialize()`. Fatal mixed addressing: static then dynamic /// alloc. -void mixedStaticThenDynamicQubit(QCProgramBuilder& b); +SmallVector mixedStaticThenDynamicQubit(QCProgramBuilder& b); /// @pre `builder.initialize()`. Fatal mixed addressing: dynamic register then /// static. -void mixedDynamicRegisterThenStaticQubit(QCProgramBuilder& b); +SmallVector mixedDynamicRegisterThenStaticQubit(QCProgramBuilder& b); // --- MeasureOp ------------------------------------------------------------ // /// Measures a single qubit into a single classical bit. -void singleMeasurementToSingleBit(QCProgramBuilder& b); +Value singleMeasurementToSingleBit(QCProgramBuilder& b); /// Repeatedly measures a single qubit into the same classical bit. -void repeatedMeasurementToSameBit(QCProgramBuilder& b); +Value repeatedMeasurementToSameBit(QCProgramBuilder& b); /// Repeatedly measures a single qubit into different classical bits. -void repeatedMeasurementToDifferentBits(QCProgramBuilder& b); +SmallVector repeatedMeasurementToDifferentBits(QCProgramBuilder& b); /// Measures multiple qubits into multiple classical bits. -void multipleClassicalRegistersAndMeasurements(QCProgramBuilder& b); +SmallVector +multipleClassicalRegistersAndMeasurements(QCProgramBuilder& b); /// Measures a single qubit into a single classical bit, without explicitly /// allocating a quantum or classical register. -void measurementWithoutRegisters(QCProgramBuilder& b); +Value measurementWithoutRegisters(QCProgramBuilder& b); // --- ResetOp -------------------------------------------------------------- // /// Resets a single qubit without any operations being applied. -void resetQubitWithoutOp(QCProgramBuilder& b); +Value resetQubitWithoutOp(QCProgramBuilder& b); /// Resets multiple qubits without any operations being applied. -void resetMultipleQubitsWithoutOp(QCProgramBuilder& b); +SmallVector resetMultipleQubitsWithoutOp(QCProgramBuilder& b); /// Repeatedly resets a single qubit without any operations being applied. -void repeatedResetWithoutOp(QCProgramBuilder& b); +Value repeatedResetWithoutOp(QCProgramBuilder& b); /// Resets a single qubit after a single operation. -void resetQubitAfterSingleOp(QCProgramBuilder& b); +Value resetQubitAfterSingleOp(QCProgramBuilder& b); /// Resets multiple qubits after a single operation. -void resetMultipleQubitsAfterSingleOp(QCProgramBuilder& b); +SmallVector resetMultipleQubitsAfterSingleOp(QCProgramBuilder& b); /// Repeatedly resets a single qubit after a single operation. -void repeatedResetAfterSingleOp(QCProgramBuilder& b); +Value repeatedResetAfterSingleOp(QCProgramBuilder& b); // --- GPhaseOp ------------------------------------------------------------- // /// Creates a circuit with just a global phase. -void globalPhase(QCProgramBuilder& b); +Value globalPhase(QCProgramBuilder& b); + +/// Creates a circuit with just a global phase and a single measured qubit. +Value globalPhaseAndMeasure(QCProgramBuilder& b); /// Creates a controlled global phase gate with a single control qubit. -void singleControlledGlobalPhase(QCProgramBuilder& b); +Value singleControlledGlobalPhase(QCProgramBuilder& b); /// Creates a multi-controlled global phase gate with multiple control qubits. -void multipleControlledGlobalPhase(QCProgramBuilder& b); +SmallVector multipleControlledGlobalPhase(QCProgramBuilder& b); /// Creates a circuit with a nested controlled global phase gate. -void nestedControlledGlobalPhase(QCProgramBuilder& b); +SmallVector nestedControlledGlobalPhase(QCProgramBuilder& b); /// Creates a circuit with a trivial controlled global phase gate. -void trivialControlledGlobalPhase(QCProgramBuilder& b); +Value trivialControlledGlobalPhase(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a global phase gate. -void inverseGlobalPhase(QCProgramBuilder& b); +Value inverseGlobalPhase(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled global /// phase gate. -void inverseMultipleControlledGlobalPhase(QCProgramBuilder& b); +SmallVector inverseMultipleControlledGlobalPhase(QCProgramBuilder& b); // --- IdOp ----------------------------------------------------------------- // /// Creates a circuit with just an identity gate. -void identity(QCProgramBuilder& b); +Value identity(QCProgramBuilder& b); /// Creates a controlled identity gate with a single control qubit. -void singleControlledIdentity(QCProgramBuilder& b); +SmallVector singleControlledIdentity(QCProgramBuilder& b); + +/// Creates an identity gate on a single qubit in a two-qubit register. +SmallVector twoQubitsOneIdentity(QCProgramBuilder& b); + +/// Creates an identity gate on a single qubit in a three-qubit register. +SmallVector threeQubitsOneIdentity(QCProgramBuilder& b); /// Creates a multi-controlled identity gate with multiple control qubits. -void multipleControlledIdentity(QCProgramBuilder& b); +SmallVector multipleControlledIdentity(QCProgramBuilder& b); + +/// Creates an barrier gate on a single qubit in a two-qubit register. +SmallVector twoQubitsOneBarrier(QCProgramBuilder& b); /// Creates a circuit with a nested controlled identity gate. -void nestedControlledIdentity(QCProgramBuilder& b); +SmallVector nestedControlledIdentity(QCProgramBuilder& b); /// Creates a circuit with a trivial controlled identity gate. -void trivialControlledIdentity(QCProgramBuilder& b); +Value trivialControlledIdentity(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to an identity gate. -void inverseIdentity(QCProgramBuilder& b); +Value inverseIdentity(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled identity /// gate. -void inverseMultipleControlledIdentity(QCProgramBuilder& b); +SmallVector inverseMultipleControlledIdentity(QCProgramBuilder& b); // --- XOp ------------------------------------------------------------------ // /// Creates a circuit with just an X gate. -void x(QCProgramBuilder& b); +Value x(QCProgramBuilder& b); /// Creates a circuit with a single controlled X gate. -void singleControlledX(QCProgramBuilder& b); +SmallVector singleControlledX(QCProgramBuilder& b); /// Creates a circuit with a multi-controlled X gate. -void multipleControlledX(QCProgramBuilder& b); +SmallVector multipleControlledX(QCProgramBuilder& b); /// Creates a circuit with a nested controlled X gate. -void nestedControlledX(QCProgramBuilder& b); +SmallVector nestedControlledX(QCProgramBuilder& b); /// Creates a circuit with a trivial controlled X gate. -void trivialControlledX(QCProgramBuilder& b); +Value trivialControlledX(QCProgramBuilder& b); /// Creates a circuit with repeated controlled X gates. -void repeatedControlledX(QCProgramBuilder& b); +SmallVector repeatedControlledX(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to an X gate. -void inverseX(QCProgramBuilder& b); +Value inverseX(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled X gate. -void inverseMultipleControlledX(QCProgramBuilder& b); +SmallVector inverseMultipleControlledX(QCProgramBuilder& b); // --- YOp ------------------------------------------------------------------ // /// Creates a circuit with just a Y gate. -void y(QCProgramBuilder& b); +Value y(QCProgramBuilder& b); /// Creates a circuit with a single controlled Y gate. -void singleControlledY(QCProgramBuilder& b); +SmallVector singleControlledY(QCProgramBuilder& b); /// Creates a circuit with a multi-controlled Y gate. -void multipleControlledY(QCProgramBuilder& b); +SmallVector multipleControlledY(QCProgramBuilder& b); /// Creates a circuit with a nested controlled Y gate. -void nestedControlledY(QCProgramBuilder& b); +SmallVector nestedControlledY(QCProgramBuilder& b); /// Creates a circuit with a trivial controlled Y gate. -void trivialControlledY(QCProgramBuilder& b); +Value trivialControlledY(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a Y gate. -void inverseY(QCProgramBuilder& b); +Value inverseY(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled Y gate. -void inverseMultipleControlledY(QCProgramBuilder& b); +SmallVector inverseMultipleControlledY(QCProgramBuilder& b); // --- ZOp ------------------------------------------------------------------ // /// Creates a circuit with just a Z gate. -void z(QCProgramBuilder& b); +Value z(QCProgramBuilder& b); /// Creates a circuit with a single controlled Z gate. -void singleControlledZ(QCProgramBuilder& b); +SmallVector singleControlledZ(QCProgramBuilder& b); /// Creates a circuit with a multi-controlled Z gate. -void multipleControlledZ(QCProgramBuilder& b); +SmallVector multipleControlledZ(QCProgramBuilder& b); /// Creates a circuit with a nested controlled Z gate. -void nestedControlledZ(QCProgramBuilder& b); +SmallVector nestedControlledZ(QCProgramBuilder& b); /// Creates a circuit with a trivial controlled Z gate. -void trivialControlledZ(QCProgramBuilder& b); +Value trivialControlledZ(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a Z gate. -void inverseZ(QCProgramBuilder& b); +Value inverseZ(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled Z gate. -void inverseMultipleControlledZ(QCProgramBuilder& b); +SmallVector inverseMultipleControlledZ(QCProgramBuilder& b); // --- HOp ------------------------------------------------------------------ // /// Creates a circuit with just an H gate. -void h(QCProgramBuilder& b); +Value h(QCProgramBuilder& b); /// Creates a circuit with a single controlled H gate. -void singleControlledH(QCProgramBuilder& b); +SmallVector singleControlledH(QCProgramBuilder& b); /// Creates a circuit with a multi-controlled H gate. -void multipleControlledH(QCProgramBuilder& b); +SmallVector multipleControlledH(QCProgramBuilder& b); /// Creates a circuit with a nested controlled H gate. -void nestedControlledH(QCProgramBuilder& b); +SmallVector nestedControlledH(QCProgramBuilder& b); /// Creates a circuit with a trivial controlled H gate. -void trivialControlledH(QCProgramBuilder& b); +Value trivialControlledH(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to an H gate. -void inverseH(QCProgramBuilder& b); +Value inverseH(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled H gate. -void inverseMultipleControlledH(QCProgramBuilder& b); +SmallVector inverseMultipleControlledH(QCProgramBuilder& b); /// Creates a circuit with just an H gate and no qubit register. -void hWithoutRegister(QCProgramBuilder& b); +Value hWithoutRegister(QCProgramBuilder& b); // --- SOp ------------------------------------------------------------------ // /// Creates a circuit with just an S gate. -void s(QCProgramBuilder& b); +Value s(QCProgramBuilder& b); /// Creates a circuit with a single controlled S gate. -void singleControlledS(QCProgramBuilder& b); +SmallVector singleControlledS(QCProgramBuilder& b); /// Creates a circuit with a multi-controlled S gate. -void multipleControlledS(QCProgramBuilder& b); +SmallVector multipleControlledS(QCProgramBuilder& b); /// Creates a circuit with a nested controlled S gate. -void nestedControlledS(QCProgramBuilder& b); +SmallVector nestedControlledS(QCProgramBuilder& b); /// Creates a circuit with a trivial controlled S gate. -void trivialControlledS(QCProgramBuilder& b); +Value trivialControlledS(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to an S gate. -void inverseS(QCProgramBuilder& b); +Value inverseS(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled S gate. -void inverseMultipleControlledS(QCProgramBuilder& b); +SmallVector inverseMultipleControlledS(QCProgramBuilder& b); // --- SdgOp ---------------------------------------------------------------- // /// Creates a circuit with just an Sdg gate. -void sdg(QCProgramBuilder& b); +Value sdg(QCProgramBuilder& b); /// Creates a circuit with a single controlled Sdg gate. -void singleControlledSdg(QCProgramBuilder& b); +SmallVector singleControlledSdg(QCProgramBuilder& b); /// Creates a circuit with a multi-controlled Sdg gate. -void multipleControlledSdg(QCProgramBuilder& b); +SmallVector multipleControlledSdg(QCProgramBuilder& b); /// Creates a circuit with a nested controlled Sdg gate. -void nestedControlledSdg(QCProgramBuilder& b); +SmallVector nestedControlledSdg(QCProgramBuilder& b); /// Creates a circuit with a trivial controlled Sdg gate. -void trivialControlledSdg(QCProgramBuilder& b); +Value trivialControlledSdg(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to an Sdg gate. -void inverseSdg(QCProgramBuilder& b); +Value inverseSdg(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled Sdg gate. -void inverseMultipleControlledSdg(QCProgramBuilder& b); +SmallVector inverseMultipleControlledSdg(QCProgramBuilder& b); // --- TOp ------------------------------------------------------------------ // /// Creates a circuit with just a T gate. -void t_(QCProgramBuilder& b); // NOLINT(*-identifier-naming) +Value t_(QCProgramBuilder& b); // NOLINT(*-identifier-naming) /// Creates a circuit with a single controlled T gate. -void singleControlledT(QCProgramBuilder& b); +SmallVector singleControlledT(QCProgramBuilder& b); /// Creates a circuit with a multi-controlled T gate. -void multipleControlledT(QCProgramBuilder& b); +SmallVector multipleControlledT(QCProgramBuilder& b); /// Creates a circuit with a nested controlled T gate. -void nestedControlledT(QCProgramBuilder& b); +SmallVector nestedControlledT(QCProgramBuilder& b); /// Creates a circuit with a trivial controlled T gate. -void trivialControlledT(QCProgramBuilder& b); +Value trivialControlledT(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a T gate. -void inverseT(QCProgramBuilder& b); +Value inverseT(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled T gate. -void inverseMultipleControlledT(QCProgramBuilder& b); +SmallVector inverseMultipleControlledT(QCProgramBuilder& b); // --- TdgOp ---------------------------------------------------------------- // /// Creates a circuit with just a Tdg gate. -void tdg(QCProgramBuilder& b); +Value tdg(QCProgramBuilder& b); /// Creates a circuit with a single controlled Tdg gate. -void singleControlledTdg(QCProgramBuilder& b); +SmallVector singleControlledTdg(QCProgramBuilder& b); /// Creates a circuit with a multi-controlled Tdg gate. -void multipleControlledTdg(QCProgramBuilder& b); +SmallVector multipleControlledTdg(QCProgramBuilder& b); /// Creates a circuit with a nested controlled Tdg gate. -void nestedControlledTdg(QCProgramBuilder& b); +SmallVector nestedControlledTdg(QCProgramBuilder& b); /// Creates a circuit with a trivial controlled Tdg gate. -void trivialControlledTdg(QCProgramBuilder& b); +Value trivialControlledTdg(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a Tdg gate. -void inverseTdg(QCProgramBuilder& b); +Value inverseTdg(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled Tdg gate. -void inverseMultipleControlledTdg(QCProgramBuilder& b); +SmallVector inverseMultipleControlledTdg(QCProgramBuilder& b); // --- SXOp ----------------------------------------------------------------- // /// Creates a circuit with just an SX gate. -void sx(QCProgramBuilder& b); +Value sx(QCProgramBuilder& b); /// Creates a circuit with a single controlled SX gate. -void singleControlledSx(QCProgramBuilder& b); +SmallVector singleControlledSx(QCProgramBuilder& b); /// Creates a circuit with a multi-controlled SX gate. -void multipleControlledSx(QCProgramBuilder& b); +SmallVector multipleControlledSx(QCProgramBuilder& b); /// Creates a circuit with a nested controlled SX gate. -void nestedControlledSx(QCProgramBuilder& b); +SmallVector nestedControlledSx(QCProgramBuilder& b); /// Creates a circuit with a trivial controlled SX gate. -void trivialControlledSx(QCProgramBuilder& b); +Value trivialControlledSx(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to an SX gate. -void inverseSx(QCProgramBuilder& b); +Value inverseSx(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled SX gate. -void inverseMultipleControlledSx(QCProgramBuilder& b); +SmallVector inverseMultipleControlledSx(QCProgramBuilder& b); // --- SXdgOp --------------------------------------------------------------- // /// Creates a circuit with just an SXdg gate. -void sxdg(QCProgramBuilder& b); +Value sxdg(QCProgramBuilder& b); /// Creates a circuit with a single controlled SXdg gate. -void singleControlledSxdg(QCProgramBuilder& b); +SmallVector singleControlledSxdg(QCProgramBuilder& b); /// Creates a circuit with a multi-controlled SXdg gate. -void multipleControlledSxdg(QCProgramBuilder& b); +SmallVector multipleControlledSxdg(QCProgramBuilder& b); /// Creates a circuit with a nested controlled SXdg gate. -void nestedControlledSxdg(QCProgramBuilder& b); +SmallVector nestedControlledSxdg(QCProgramBuilder& b); /// Creates a circuit with a trivial controlled SXdg gate. -void trivialControlledSxdg(QCProgramBuilder& b); +Value trivialControlledSxdg(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to an SXdg gate. -void inverseSxdg(QCProgramBuilder& b); +Value inverseSxdg(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled SXdg /// gate. -void inverseMultipleControlledSxdg(QCProgramBuilder& b); +SmallVector inverseMultipleControlledSxdg(QCProgramBuilder& b); // --- RXOp ----------------------------------------------------------------- // /// Creates a circuit with just an RX gate. -void rx(QCProgramBuilder& b); +Value rx(QCProgramBuilder& b); /// Creates a circuit with a single controlled RX gate. -void singleControlledRx(QCProgramBuilder& b); +SmallVector singleControlledRx(QCProgramBuilder& b); /// Creates a circuit with a multi-controlled RX gate. -void multipleControlledRx(QCProgramBuilder& b); +SmallVector multipleControlledRx(QCProgramBuilder& b); /// Creates a circuit with a nested controlled RX gate. -void nestedControlledRx(QCProgramBuilder& b); +SmallVector nestedControlledRx(QCProgramBuilder& b); /// Creates a circuit with a trivial controlled RX gate. -void trivialControlledRx(QCProgramBuilder& b); +Value trivialControlledRx(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to an RX gate. -void inverseRx(QCProgramBuilder& b); +Value inverseRx(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled RX gate. -void inverseMultipleControlledRx(QCProgramBuilder& b); +SmallVector inverseMultipleControlledRx(QCProgramBuilder& b); // --- RYOp ----------------------------------------------------------------- // /// Creates a circuit with just an RY gate. -void ry(QCProgramBuilder& b); +Value ry(QCProgramBuilder& b); /// Creates a circuit with a single controlled RY gate. -void singleControlledRy(QCProgramBuilder& b); +SmallVector singleControlledRy(QCProgramBuilder& b); /// Creates a circuit with a multi-controlled RY gate. -void multipleControlledRy(QCProgramBuilder& b); +SmallVector multipleControlledRy(QCProgramBuilder& b); /// Creates a circuit with a nested controlled RY gate. -void nestedControlledRy(QCProgramBuilder& b); +SmallVector nestedControlledRy(QCProgramBuilder& b); /// Creates a circuit with a trivial controlled RY gate. -void trivialControlledRy(QCProgramBuilder& b); +Value trivialControlledRy(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to an RY gate. -void inverseRy(QCProgramBuilder& b); +Value inverseRy(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled RY gate. -void inverseMultipleControlledRy(QCProgramBuilder& b); +SmallVector inverseMultipleControlledRy(QCProgramBuilder& b); // --- RZOp ----------------------------------------------------------------- // /// Creates a circuit with just an RZ gate. -void rz(QCProgramBuilder& b); +Value rz(QCProgramBuilder& b); /// Creates a circuit with a single controlled RZ gate. -void singleControlledRz(QCProgramBuilder& b); +SmallVector singleControlledRz(QCProgramBuilder& b); /// Creates a circuit with a multi-controlled RZ gate. -void multipleControlledRz(QCProgramBuilder& b); +SmallVector multipleControlledRz(QCProgramBuilder& b); /// Creates a circuit with a nested controlled RZ gate. -void nestedControlledRz(QCProgramBuilder& b); +SmallVector nestedControlledRz(QCProgramBuilder& b); /// Creates a circuit with a trivial controlled RZ gate. -void trivialControlledRz(QCProgramBuilder& b); +Value trivialControlledRz(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to an RZ gate. -void inverseRz(QCProgramBuilder& b); +Value inverseRz(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled RZ gate. -void inverseMultipleControlledRz(QCProgramBuilder& b); +SmallVector inverseMultipleControlledRz(QCProgramBuilder& b); // --- POp ------------------------------------------------------------------ // /// Creates a circuit with just a P gate. -void p(QCProgramBuilder& b); +Value p(QCProgramBuilder& b); /// Creates a circuit with a single controlled P gate. -void singleControlledP(QCProgramBuilder& b); +SmallVector singleControlledP(QCProgramBuilder& b); /// Creates a circuit with a multi-controlled P gate. -void multipleControlledP(QCProgramBuilder& b); +SmallVector multipleControlledP(QCProgramBuilder& b); /// Creates a circuit with a nested controlled P gate. -void nestedControlledP(QCProgramBuilder& b); +SmallVector nestedControlledP(QCProgramBuilder& b); /// Creates a circuit with a trivial controlled P gate. -void trivialControlledP(QCProgramBuilder& b); +Value trivialControlledP(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a P gate. -void inverseP(QCProgramBuilder& b); +Value inverseP(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled P gate. -void inverseMultipleControlledP(QCProgramBuilder& b); +SmallVector inverseMultipleControlledP(QCProgramBuilder& b); // --- ROp ------------------------------------------------------------------ // /// Creates a circuit with just an R gate. -void r(QCProgramBuilder& b); +Value r(QCProgramBuilder& b); /// Creates a circuit with a single controlled R gate. -void singleControlledR(QCProgramBuilder& b); +SmallVector singleControlledR(QCProgramBuilder& b); /// Creates a circuit with a multi-controlled R gate. -void multipleControlledR(QCProgramBuilder& b); +SmallVector multipleControlledR(QCProgramBuilder& b); /// Creates a circuit with a nested controlled R gate. -void nestedControlledR(QCProgramBuilder& b); +SmallVector nestedControlledR(QCProgramBuilder& b); /// Creates a circuit with a trivial controlled R gate. -void trivialControlledR(QCProgramBuilder& b); +Value trivialControlledR(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to an R gate. -void inverseR(QCProgramBuilder& b); +Value inverseR(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled R gate. -void inverseMultipleControlledR(QCProgramBuilder& b); +SmallVector inverseMultipleControlledR(QCProgramBuilder& b); // --- U2Op ----------------------------------------------------------------- // /// Creates a circuit with just a U2 gate. -void u2(QCProgramBuilder& b); +Value u2(QCProgramBuilder& b); /// Creates a circuit with a single controlled U2 gate. -void singleControlledU2(QCProgramBuilder& b); +SmallVector singleControlledU2(QCProgramBuilder& b); /// Creates a circuit with a multi-controlled U2 gate. -void multipleControlledU2(QCProgramBuilder& b); +SmallVector multipleControlledU2(QCProgramBuilder& b); /// Creates a circuit with a nested controlled U2 gate. -void nestedControlledU2(QCProgramBuilder& b); +SmallVector nestedControlledU2(QCProgramBuilder& b); /// Creates a circuit with a trivial controlled U2 gate. -void trivialControlledU2(QCProgramBuilder& b); +Value trivialControlledU2(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a U2 gate. -void inverseU2(QCProgramBuilder& b); +Value inverseU2(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled U2 gate. -void inverseMultipleControlledU2(QCProgramBuilder& b); +SmallVector inverseMultipleControlledU2(QCProgramBuilder& b); // --- UOp ------------------------------------------------------------------ // /// Creates a circuit with just a U gate. -void u(QCProgramBuilder& b); +Value u(QCProgramBuilder& b); /// Creates a circuit with a single controlled U gate. -void singleControlledU(QCProgramBuilder& b); +SmallVector singleControlledU(QCProgramBuilder& b); /// Creates a circuit with a multi-controlled U gate. -void multipleControlledU(QCProgramBuilder& b); +SmallVector multipleControlledU(QCProgramBuilder& b); /// Creates a circuit with a nested controlled U gate. -void nestedControlledU(QCProgramBuilder& b); +SmallVector nestedControlledU(QCProgramBuilder& b); /// Creates a circuit with a trivial controlled U gate. -void trivialControlledU(QCProgramBuilder& b); +Value trivialControlledU(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a U gate. -void inverseU(QCProgramBuilder& b); +Value inverseU(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled U gate. -void inverseMultipleControlledU(QCProgramBuilder& b); +SmallVector inverseMultipleControlledU(QCProgramBuilder& b); // --- SWAPOp --------------------------------------------------------------- // /// Creates a circuit with just a SWAP gate. -void swap(QCProgramBuilder& b); +SmallVector swap(QCProgramBuilder& b); /// Creates a circuit with a single controlled SWAP gate. -void singleControlledSwap(QCProgramBuilder& b); +SmallVector singleControlledSwap(QCProgramBuilder& b); /// Creates a circuit with a multi-controlled SWAP gate. -void multipleControlledSwap(QCProgramBuilder& b); +SmallVector multipleControlledSwap(QCProgramBuilder& b); /// Creates a circuit with a nested controlled SWAP gate. -void nestedControlledSwap(QCProgramBuilder& b); +SmallVector nestedControlledSwap(QCProgramBuilder& b); /// Creates a circuit with a trivial controlled SWAP gate. -void trivialControlledSwap(QCProgramBuilder& b); +SmallVector trivialControlledSwap(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a SWAP gate. -void inverseSwap(QCProgramBuilder& b); +SmallVector inverseSwap(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled SWAP /// gate. -void inverseMultipleControlledSwap(QCProgramBuilder& b); +SmallVector inverseMultipleControlledSwap(QCProgramBuilder& b); // --- iSWAPOp -------------------------------------------------------------- // /// Creates a circuit with just an iSWAP gate. -void iswap(QCProgramBuilder& b); +SmallVector iswap(QCProgramBuilder& b); /// Creates a circuit with a single controlled iSWAP gate. -void singleControlledIswap(QCProgramBuilder& b); +SmallVector singleControlledIswap(QCProgramBuilder& b); /// Creates a circuit with a multi-controlled iSWAP gate. -void multipleControlledIswap(QCProgramBuilder& b); +SmallVector multipleControlledIswap(QCProgramBuilder& b); /// Creates a circuit with a nested controlled iSWAP gate. -void nestedControlledIswap(QCProgramBuilder& b); +SmallVector nestedControlledIswap(QCProgramBuilder& b); /// Creates a circuit with a trivial controlled iSWAP gate. -void trivialControlledIswap(QCProgramBuilder& b); +SmallVector trivialControlledIswap(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to an iSWAP gate. -void inverseIswap(QCProgramBuilder& b); +SmallVector inverseIswap(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled iSWAP /// gate. -void inverseMultipleControlledIswap(QCProgramBuilder& b); +SmallVector inverseMultipleControlledIswap(QCProgramBuilder& b); // --- DCXOp ---------------------------------------------------------------- // /// Creates a circuit with just a DCX gate. -void dcx(QCProgramBuilder& b); +SmallVector dcx(QCProgramBuilder& b); /// Creates a circuit with a single controlled DCX gate. -void singleControlledDcx(QCProgramBuilder& b); +SmallVector singleControlledDcx(QCProgramBuilder& b); /// Creates a circuit with a multi-controlled DCX gate. -void multipleControlledDcx(QCProgramBuilder& b); +SmallVector multipleControlledDcx(QCProgramBuilder& b); /// Creates a circuit with a nested controlled DCX gate. -void nestedControlledDcx(QCProgramBuilder& b); +SmallVector nestedControlledDcx(QCProgramBuilder& b); /// Creates a circuit with a trivial controlled DCX gate. -void trivialControlledDcx(QCProgramBuilder& b); +SmallVector trivialControlledDcx(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a DCX gate. -void inverseDcx(QCProgramBuilder& b); +SmallVector inverseDcx(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled DCX gate. -void inverseMultipleControlledDcx(QCProgramBuilder& b); +SmallVector inverseMultipleControlledDcx(QCProgramBuilder& b); // --- ECROp ---------------------------------------------------------------- // /// Creates a circuit with just an ECR gate. -void ecr(QCProgramBuilder& b); +SmallVector ecr(QCProgramBuilder& b); /// Creates a circuit with a single controlled ECR gate. -void singleControlledEcr(QCProgramBuilder& b); +SmallVector singleControlledEcr(QCProgramBuilder& b); /// Creates a circuit with a multi-controlled ECR gate. -void multipleControlledEcr(QCProgramBuilder& b); +SmallVector multipleControlledEcr(QCProgramBuilder& b); /// Creates a circuit with a nested controlled ECR gate. -void nestedControlledEcr(QCProgramBuilder& b); +SmallVector nestedControlledEcr(QCProgramBuilder& b); /// Creates a circuit with a trivial controlled ECR gate. -void trivialControlledEcr(QCProgramBuilder& b); +SmallVector trivialControlledEcr(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to an ECR gate. -void inverseEcr(QCProgramBuilder& b); +SmallVector inverseEcr(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled ECR gate. -void inverseMultipleControlledEcr(QCProgramBuilder& b); +SmallVector inverseMultipleControlledEcr(QCProgramBuilder& b); // --- RXXOp ---------------------------------------------------------------- // /// Creates a circuit with just an RXX gate. -void rxx(QCProgramBuilder& b); +SmallVector rxx(QCProgramBuilder& b); /// Creates a circuit with a single controlled RXX gate. -void singleControlledRxx(QCProgramBuilder& b); +SmallVector singleControlledRxx(QCProgramBuilder& b); /// Creates a circuit with a multi-controlled RXX gate. -void multipleControlledRxx(QCProgramBuilder& b); +SmallVector multipleControlledRxx(QCProgramBuilder& b); /// Creates a circuit with a nested controlled RXX gate. -void nestedControlledRxx(QCProgramBuilder& b); +SmallVector nestedControlledRxx(QCProgramBuilder& b); /// Creates a circuit with a trivial controlled RXX gate. -void trivialControlledRxx(QCProgramBuilder& b); +SmallVector trivialControlledRxx(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to an RXX gate. -void inverseRxx(QCProgramBuilder& b); +SmallVector inverseRxx(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled RXX gate. -void inverseMultipleControlledRxx(QCProgramBuilder& b); +SmallVector inverseMultipleControlledRxx(QCProgramBuilder& b); /// Creates a circuit with a triple-controlled RXX gate. -void tripleControlledRxx(QCProgramBuilder& b); +SmallVector tripleControlledRxx(QCProgramBuilder& b); /// Creates a circuit with a four-controlled RXX gate. -void fourControlledRxx(QCProgramBuilder& b); +SmallVector fourControlledRxx(QCProgramBuilder& b); // --- RYYOp ---------------------------------------------------------------- // /// Creates a circuit with just an RYY gate. -void ryy(QCProgramBuilder& b); +SmallVector ryy(QCProgramBuilder& b); /// Creates a circuit with a single controlled RYY gate. -void singleControlledRyy(QCProgramBuilder& b); +SmallVector singleControlledRyy(QCProgramBuilder& b); /// Creates a circuit with a multi-controlled RYY gate. -void multipleControlledRyy(QCProgramBuilder& b); +SmallVector multipleControlledRyy(QCProgramBuilder& b); /// Creates a circuit with a nested controlled RYY gate. -void nestedControlledRyy(QCProgramBuilder& b); +SmallVector nestedControlledRyy(QCProgramBuilder& b); /// Creates a circuit with a trivial controlled RYY gate. -void trivialControlledRyy(QCProgramBuilder& b); +SmallVector trivialControlledRyy(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to an RYY gate. -void inverseRyy(QCProgramBuilder& b); +SmallVector inverseRyy(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled RYY gate. -void inverseMultipleControlledRyy(QCProgramBuilder& b); +SmallVector inverseMultipleControlledRyy(QCProgramBuilder& b); // --- RZXOp ---------------------------------------------------------------- // /// Creates a circuit with just an RZX gate. -void rzx(QCProgramBuilder& b); +SmallVector rzx(QCProgramBuilder& b); /// Creates a circuit with a single controlled RZX gate. -void singleControlledRzx(QCProgramBuilder& b); +SmallVector singleControlledRzx(QCProgramBuilder& b); /// Creates a circuit with a multi-controlled RZX gate. -void multipleControlledRzx(QCProgramBuilder& b); +SmallVector multipleControlledRzx(QCProgramBuilder& b); /// Creates a circuit with a nested controlled RZX gate. -void nestedControlledRzx(QCProgramBuilder& b); +SmallVector nestedControlledRzx(QCProgramBuilder& b); /// Creates a circuit with a trivial controlled RZX gate. -void trivialControlledRzx(QCProgramBuilder& b); +SmallVector trivialControlledRzx(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to an RZX gate. -void inverseRzx(QCProgramBuilder& b); +SmallVector inverseRzx(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled RZX gate. -void inverseMultipleControlledRzx(QCProgramBuilder& b); +SmallVector inverseMultipleControlledRzx(QCProgramBuilder& b); // --- RZZOp ---------------------------------------------------------------- // /// Creates a circuit with just an RZZ gate. -void rzz(QCProgramBuilder& b); +SmallVector rzz(QCProgramBuilder& b); /// Creates a circuit with a single controlled RZZ gate. -void singleControlledRzz(QCProgramBuilder& b); +SmallVector singleControlledRzz(QCProgramBuilder& b); /// Creates a circuit with a multi-controlled RZZ gate. -void multipleControlledRzz(QCProgramBuilder& b); +SmallVector multipleControlledRzz(QCProgramBuilder& b); /// Creates a circuit with a nested controlled RZZ gate. -void nestedControlledRzz(QCProgramBuilder& b); +SmallVector nestedControlledRzz(QCProgramBuilder& b); /// Creates a circuit with a trivial controlled RZZ gate. -void trivialControlledRzz(QCProgramBuilder& b); +SmallVector trivialControlledRzz(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to an RZZ gate. -void inverseRzz(QCProgramBuilder& b); +SmallVector inverseRzz(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled RZZ gate. -void inverseMultipleControlledRzz(QCProgramBuilder& b); +SmallVector inverseMultipleControlledRzz(QCProgramBuilder& b); // --- XXPlusYYOp ----------------------------------------------------------- // /// Creates a circuit with just an XXPlusYY gate. -void xxPlusYY(QCProgramBuilder& b); +SmallVector xxPlusYY(QCProgramBuilder& b); /// Creates a circuit with a single controlled XXPlusYY gate. -void singleControlledXxPlusYY(QCProgramBuilder& b); +SmallVector singleControlledXxPlusYY(QCProgramBuilder& b); /// Creates a circuit with a multi-controlled XXPlusYY gate. -void multipleControlledXxPlusYY(QCProgramBuilder& b); +SmallVector multipleControlledXxPlusYY(QCProgramBuilder& b); /// Creates a circuit with a nested controlled XXPlusYY gate. -void nestedControlledXxPlusYY(QCProgramBuilder& b); +SmallVector nestedControlledXxPlusYY(QCProgramBuilder& b); /// Creates a circuit with a trivial controlled XXPlusYY gate. -void trivialControlledXxPlusYY(QCProgramBuilder& b); +SmallVector trivialControlledXxPlusYY(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to an XXPlusYY gate. -void inverseXxPlusYY(QCProgramBuilder& b); +SmallVector inverseXxPlusYY(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled XXPlusYY /// gate. -void inverseMultipleControlledXxPlusYY(QCProgramBuilder& b); +SmallVector inverseMultipleControlledXxPlusYY(QCProgramBuilder& b); // --- XXMinusYYOp ---------------------------------------------------------- // /// Creates a circuit with just an XXMinusYY gate. -void xxMinusYY(QCProgramBuilder& b); +SmallVector xxMinusYY(QCProgramBuilder& b); /// Creates a circuit with a single controlled XXMinusYY gate. -void singleControlledXxMinusYY(QCProgramBuilder& b); +SmallVector singleControlledXxMinusYY(QCProgramBuilder& b); /// Creates a circuit with a multi-controlled XXMinusYY gate. -void multipleControlledXxMinusYY(QCProgramBuilder& b); +SmallVector multipleControlledXxMinusYY(QCProgramBuilder& b); /// Creates a circuit with a nested controlled XXMinusYY gate. -void nestedControlledXxMinusYY(QCProgramBuilder& b); +SmallVector nestedControlledXxMinusYY(QCProgramBuilder& b); /// Creates a circuit with a trivial controlled XXMinusYY gate. -void trivialControlledXxMinusYY(QCProgramBuilder& b); +SmallVector trivialControlledXxMinusYY(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to an XXMinusYY gate. -void inverseXxMinusYY(QCProgramBuilder& b); +SmallVector inverseXxMinusYY(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled XXMinusYY /// gate. -void inverseMultipleControlledXxMinusYY(QCProgramBuilder& b); +SmallVector inverseMultipleControlledXxMinusYY(QCProgramBuilder& b); // --- BarrierOp ------------------------------------------------------------ // /// Creates a circuit with a barrier. -void barrier(QCProgramBuilder& b); +Value barrier(QCProgramBuilder& b); /// Creates a circuit with a barrier on two qubits. -void barrierTwoQubits(QCProgramBuilder& b); +SmallVector barrierTwoQubits(QCProgramBuilder& b); /// Creates a circuit with a barrier on multiple qubits. -void barrierMultipleQubits(QCProgramBuilder& b); +SmallVector barrierMultipleQubits(QCProgramBuilder& b); /// Creates a circuit with a single controlled barrier. -void singleControlledBarrier(QCProgramBuilder& b); +SmallVector singleControlledBarrier(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a barrier. -void inverseBarrier(QCProgramBuilder& b); +Value inverseBarrier(QCProgramBuilder& b); // --- CtrlOp --------------------------------------------------------------- // /// Creates a circuit with a trivial ctrl modifier. -void trivialCtrl(QCProgramBuilder& b); +SmallVector trivialCtrl(QCProgramBuilder& b); /// Creates a circuit with an empty ctrl modifier. -void emptyCtrl(QCProgramBuilder& b); +SmallVector emptyCtrl(QCProgramBuilder& b); /// Creates a circuit with nested ctrl modifiers. -void nestedCtrl(QCProgramBuilder& b); +SmallVector nestedCtrl(QCProgramBuilder& b); /// Creates a circuit with triple nested ctrl modifiers. -void tripleNestedCtrl(QCProgramBuilder& b); +SmallVector tripleNestedCtrl(QCProgramBuilder& b); /// Creates a circuit with double nested ctrl modifiers with two qubits each. -void doubleNestedCtrlTwoQubits(QCProgramBuilder& b); +SmallVector doubleNestedCtrlTwoQubits(QCProgramBuilder& b); /// Creates a circuit with control modifiers interleaved by an inverse modifier. -void ctrlInvSandwich(QCProgramBuilder& b); +SmallVector ctrlInvSandwich(QCProgramBuilder& b); /// Creates a circuit with a control modifier applied to two gates. -void ctrlTwo(QCProgramBuilder& b); +SmallVector ctrlTwo(QCProgramBuilder& b); /// Creates a circuit with a control modifier applied to a controlled and a /// non-controlled gate. -void ctrlTwoMixed(QCProgramBuilder& b); +SmallVector ctrlTwoMixed(QCProgramBuilder& b); /// Creates a circuit with nested control modifiers applied to two gates. -void nestedCtrlTwo(QCProgramBuilder& b); +SmallVector nestedCtrlTwo(QCProgramBuilder& b); /// Creates a circuit with a control modifier applied to a inverse modifier /// applied to two gates. -void ctrlInvTwo(QCProgramBuilder& b); +SmallVector ctrlInvTwo(QCProgramBuilder& b); // --- InvOp ---------------------------------------------------------------- // /// Creates a circuit with an empty inverse modifier. -void emptyInv(QCProgramBuilder& b); +SmallVector emptyInv(QCProgramBuilder& b); /// Creates a circuit with nested inverse modifiers. -void nestedInv(QCProgramBuilder& b); +SmallVector nestedInv(QCProgramBuilder& b); /// Creates a circuit with triple nested inverse modifiers. -void tripleNestedInv(QCProgramBuilder& b); +SmallVector tripleNestedInv(QCProgramBuilder& b); /// Creates a circuit with inverse modifiers interleaved by a control modifier. -void invCtrlSandwich(QCProgramBuilder& b); +SmallVector invCtrlSandwich(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to two gates. -void invTwo(QCProgramBuilder& b); +SmallVector invTwo(QCProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a control modifier /// applied to two gates. -void invCtrlTwo(QCProgramBuilder& b); +SmallVector invCtrlTwo(QCProgramBuilder& b); // --- IfOp ----------------------------------------------------------------- // /// Creates a circuit with a simple if operation with one qubit. -void simpleIf(QCProgramBuilder& b); - -/// Creates a circuit with an if operation with two qubits. -void ifTwoQubits(QCProgramBuilder& b); +SmallVector simpleIf(QCProgramBuilder& b); /// Creates a circuit with an if operation with an else branch. -void ifElse(QCProgramBuilder& b); +SmallVector ifElse(QCProgramBuilder& b); + +/// Creates a circuit with an if operation with two qubits. +SmallVector ifTwoQubits(QCProgramBuilder& b); /// Creates a circuit with an if operation with a nested for operation with /// a register. -void nestedIfOpForLoop(QCProgramBuilder& b); +Value nestedIfOpForLoop(QCProgramBuilder& b); // --- WhileOp -------------------------------------------------------------- // /// Creates a circuit with a while operation using a while loop. -void simpleWhileReset(QCProgramBuilder& b); +Value simpleWhileReset(QCProgramBuilder& b); /// Creates a circuit with a while operation using a do-while loop. -void simpleDoWhileReset(QCProgramBuilder& b); +Value simpleDoWhileReset(QCProgramBuilder& b); // --- ForOp ---------------------------------------------------------------- // /// Creates a circuit with a simple for operation with a register. -void simpleForLoop(QCProgramBuilder& b); +SmallVector simpleForLoop(QCProgramBuilder& b); /// Creates a circuit with a for operation with a register and a qubit and a /// nested if operation. -void nestedForLoopIfOp(QCProgramBuilder& b); +Value nestedForLoopIfOp(QCProgramBuilder& b); /// Creates a circuit with a for operation with a register and a nested while /// operation. -void nestedForLoopWhileOp(QCProgramBuilder& b); +SmallVector nestedForLoopWhileOp(QCProgramBuilder& b); /// Creates a circuit with a for operation with a register and a qubit and a /// nested ctrl operation where the qubit is separately allocated from the /// register. -void nestedForLoopCtrlOpWithSeparateQubit(QCProgramBuilder& b); +Value nestedForLoopCtrlOpWithSeparateQubit(QCProgramBuilder& b); /// Creates a circuit with a for operation with a register and a qubit and a /// nested ctrl operation where the qubit is extracted from the register. -void nestedForLoopCtrlOpWithExtractedQubit(QCProgramBuilder& b); +Value nestedForLoopCtrlOpWithExtractedQubit(QCProgramBuilder& b); } // namespace mlir::qc diff --git a/mlir/unittests/programs/qco_programs.cpp b/mlir/unittests/programs/qco_programs.cpp index b384aa20a1..4cafe6572d 100644 --- a/mlir/unittests/programs/qco_programs.cpp +++ b/mlir/unittests/programs/qco_programs.cpp @@ -14,2007 +14,2804 @@ #include #include +#include #include +#include #include #include +#include namespace mlir::qco { -void emptyQCO([[maybe_unused]] QCOProgramBuilder& builder) {} +/** + * @brief Measures the given `qtensor` and returns the measurement outcomes. + * @param b The `ProgramBuilder` used to perform the measurements. + * @param qTensor The `qtensor` to be measured. + * @param size The number of qubits in the `qtensor`. + * @return The result values. + */ +static SmallVector measureAndReturnQTensor(QCOProgramBuilder& b, + Value qTensor, + const int64_t size) { + SmallVector bits; + for (auto i = 0; i < size; ++i) { + auto [qTensorOut, qubit] = b.qtensorExtract(qTensor, i); + auto [q2, bit] = b.measure(qubit); + bits.push_back(bit); + qTensor = b.qtensorInsert(q2, qTensorOut, i); + } + return bits; +} + +/** + * @brief Measures the given qubits and returns the measurement outcomes. + * @param b The `ProgramBuilder` used to perform the measurements. + * @param qubits The qubits to be measured. + * @return The result values. + */ +static SmallVector measureAndReturn(QCOProgramBuilder& b, + ValueRange qubits) { + return llvm::to_vector( + llvm::map_range(qubits, [&](Value q) { return b.measure(q).second; })); +} + +Value emptyQCO(QCOProgramBuilder& b) { return b.intConstant(0); } + +Value allocQubit(QCOProgramBuilder& b) { + auto q = b.allocQubit(); + return b.measure(q).second; +} + +SmallVector alloc2Qubits(QCOProgramBuilder& b) { + auto q0 = b.allocQubit(); + auto q1 = b.allocQubit(); + return measureAndReturn(b, {q0, q1}); +} -void allocQubit(QCOProgramBuilder& b) { b.allocQubit(); } +Value allocQubitNoMeasure(QCOProgramBuilder& b) { + (void)b.allocQubit(); + return b.intConstant(0); +} + +Value alloc1QubitRegister(QCOProgramBuilder& b) { + auto reg = b.allocQubitRegister(1); + return b.measure(reg[0]).second; +} + +SmallVector alloc2QubitRegister(QCOProgramBuilder& b) { + auto reg = b.allocQubitRegister(2); + return measureAndReturn(b, reg.qubits); +} + +SmallVector alloc3QubitRegister(QCOProgramBuilder& b) { + auto reg = b.allocQubitRegister(3); + return measureAndReturn(b, reg.qubits); +} -void allocQubitRegister(QCOProgramBuilder& b) { b.allocQubitRegister(2); } +SmallVector allocMultipleQubitRegisters(QCOProgramBuilder& b) { + auto r1 = b.allocQubitRegister(2); + auto r2 = b.allocQubitRegister(3); + return measureAndReturn(b, {r1[0], r1[1], r2[0], r2[1], r2[2]}); +} -void allocMultipleQubitRegisters(QCOProgramBuilder& b) { - b.allocQubitRegister(2); - b.allocQubitRegister(3); +Value allocLargeRegister(QCOProgramBuilder& b) { + auto r = b.allocQubitRegister(100); + return b.measure(r[0]).second; } -void allocLargeRegister(QCOProgramBuilder& b) { b.allocQubitRegister(100); } +Value staticQubitsNoMeasure(QCOProgramBuilder& b) { + (void)b.staticQubit(0); + (void)b.staticQubit(1); + return b.intConstant(0); +} -void staticQubits(QCOProgramBuilder& b) { - b.staticQubit(0); - b.staticQubit(1); +SmallVector staticQubits(QCOProgramBuilder& b) { + auto q1 = b.staticQubit(0); + auto q2 = b.staticQubit(1); + return measureAndReturn(b, {q1, q2}); } -void staticQubitsWithOps(QCOProgramBuilder& b) { +SmallVector staticQubitsWithOps(QCOProgramBuilder& b) { auto q0 = b.staticQubit(0); auto q1 = b.staticQubit(1); q0 = b.h(q0); q1 = b.h(q1); + return measureAndReturn(b, {q0, q1}); } -void staticQubitsWithParametricOps(QCOProgramBuilder& b) { +SmallVector staticQubitsWithParametricOps(QCOProgramBuilder& b) { auto q0 = b.staticQubit(0); auto q1 = b.staticQubit(1); q0 = b.rx(std::numbers::pi / 4., q0); q1 = b.p(std::numbers::pi / 2., q1); + return measureAndReturn(b, {q0, q1}); } -void staticQubitsWithTwoTargetOps(QCOProgramBuilder& b) { +SmallVector staticQubitsWithTwoTargetOps(QCOProgramBuilder& b) { auto q0 = b.staticQubit(0); auto q1 = b.staticQubit(1); std::tie(q0, q1) = b.rzz(0.123, q0, q1); + return measureAndReturn(b, {q0, q1}); } -void staticQubitsWithCtrl(QCOProgramBuilder& b) { +SmallVector staticQubitsWithCtrl(QCOProgramBuilder& b) { auto q0 = b.staticQubit(0); auto q1 = b.staticQubit(1); std::tie(q0, q1) = b.cx(q0, q1); + return measureAndReturn(b, {q0, q1}); } -void staticQubitsWithInv(QCOProgramBuilder& b) { +Value staticQubitsWithInv(QCOProgramBuilder& b) { auto q0 = b.staticQubit(0); - q0 = b.inv({q0}, [&](auto targets) -> SmallVector { - return {b.t(targets[0])}; - })[0]; + q0 = b.inv(q0, [&](Value qubit) { return b.t(qubit); }); + return b.measure(q0).second; } -void allocSinkPair(QCOProgramBuilder& b) { +Value allocSinkPair(QCOProgramBuilder& b) { auto q = b.allocQubit(); b.sink(q); + return b.intConstant(0); +} + +SmallVector deadGatesProgram(QCOProgramBuilder& b) { + auto q0 = b.allocQubit(); + auto q1 = b.allocQubit(); + + auto [q0M, m0] = b.measure(q0); + auto [q1M, m1] = b.measure(q1); + + q0 = b.h(q0M); + auto [res0, res1] = b.cx(q0, q1M); + auto [_, c1] = b.measure(res1); + q0 = b.reset(res0); + + return {m0, m1}; } -void mixedStaticThenDynamicQubit(QCOProgramBuilder& b) { - b.staticQubit(0); - b.allocQubit(); +Value deadGatesWithIfOpProgram(QCOProgramBuilder& b) { + auto q0 = b.allocQubit(); + auto q1 = b.allocQubit(); + q0 = b.h(q0); + auto [r0, c0] = b.measure(q0); + q0 = r0; + + // This is an `if` with memory effects - it can't be removed. + q1 = b.qcoIf( + c0, {q1}, + [&](ValueRange qubits) -> SmallVector { + auto q1Then = b.x(qubits[0]); + b.gphase(0.5); // This adds memory effects to the `IfOp`. + return {q1Then}; + }, + [&](ValueRange qubits) -> SmallVector { + auto q1Else = b.h(qubits[0]); + return {q1Else}; + })[0]; + + // This is an `if` without memory effects - it can be removed. + q1 = b.qcoIf( + c0, {q1}, + [&](ValueRange qubits) -> SmallVector { + auto q1Then = b.x(qubits[0]); + return {q1Then}; + }, + [&](ValueRange qubits) -> SmallVector { + auto q1Else = b.h(qubits[0]); + return {q1Else}; + })[0]; + + return c0; } -void mixedDynamicRegisterThenStaticQubit(QCOProgramBuilder& b) { +Value deadGatesWithIfOpSimplified(QCOProgramBuilder& b) { + auto q0 = b.allocQubit(); + auto q1 = b.allocQubit(); + q0 = b.h(q0); + auto [r0, c0] = b.measure(q0); + q0 = r0; + + // This is an `if` with memory effects - it can't be removed. + q1 = b.qcoIf( + c0, {q1}, + [&](ValueRange qubits) -> SmallVector { + auto q1Then = b.x(qubits[0]); + b.gphase(0.5); // Due to memory effect, the `IfOp` stays. + return {q1Then}; + }, + [&](ValueRange qubits) -> SmallVector { + auto q1Else = b.h(qubits[0]); + return {q1Else}; + })[0]; + + return c0; +} + +SmallVector mixedStaticThenDynamicQubit(QCOProgramBuilder& b) { + auto q0 = b.staticQubit(0); + auto q1 = b.allocQubit(); + return measureAndReturn(b, {q0, q1}); +} + +Value mixedDynamicRegisterThenStaticQubit(QCOProgramBuilder& b) { b.qtensorAlloc(2); - b.staticQubit(0); + auto q1 = b.staticQubit(0); + return b.measure(q1).second; } -void singleMeasurementToSingleBit(QCOProgramBuilder& b) { +Value singleMeasurementToSingleBit(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); const auto& c = b.allocClassicalBitRegister(1); - q[0] = b.measure(q[0], c[0]); + const auto [q1, bit] = b.measure(q[0], c[0]); + return bit; } -void repeatedMeasurementToSameBit(QCOProgramBuilder& b) { +Value repeatedMeasurementToSameBit(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); const auto& c = b.allocClassicalBitRegister(1); - q[0] = b.measure(q[0], c[0]); - q[0] = b.measure(q[0], c[0]); - q[0] = b.measure(q[0], c[0]); + auto [q1, _c1] = b.measure(q[0], c[0]); + auto [q2, _c2] = b.measure(q1, c[0]); + auto [q3, c3] = b.measure(q2, c[0]); + return c3; } -void repeatedMeasurementToDifferentBits(QCOProgramBuilder& b) { +SmallVector repeatedMeasurementToDifferentBits(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); const auto& c = b.allocClassicalBitRegister(3); - q[0] = b.measure(q[0], c[0]); - q[0] = b.measure(q[0], c[1]); - q[0] = b.measure(q[0], c[2]); + auto [q1, c1] = b.measure(q[0], c[0]); + auto [q2, c2] = b.measure(q1, c[1]); + auto [q3, c3] = b.measure(q2, c[2]); + return {c1, c2, c3}; } -void multipleClassicalRegistersAndMeasurements(QCOProgramBuilder& b) { +SmallVector +multipleClassicalRegistersAndMeasurements(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); const auto& c0 = b.allocClassicalBitRegister(1, "c0"); const auto& c1 = b.allocClassicalBitRegister(2, "c1"); - b.measure(q[0], c0[0]); - b.measure(q[1], c1[0]); - b.measure(q[2], c1[1]); + auto [q0, bit1] = b.measure(q[0], c0[0]); + auto [q1, bit2] = b.measure(q[1], c1[0]); + auto [q2, bit3] = b.measure(q[2], c1[1]); + return {bit1, bit2, bit3}; } -void measurementWithoutRegisters(QCOProgramBuilder& b) { +Value measurementWithoutRegisters(QCOProgramBuilder& b) { auto q = b.allocQubit(); - b.measure(q); + auto [q1, c] = b.measure(q); + return c; } -void resetQubitWithoutOp(QCOProgramBuilder& b) { +Value resetQubitWithoutOp(QCOProgramBuilder& b) { auto q = b.allocQubit(); q = b.reset(q); + return b.measure(q).second; } -void resetMultipleQubitsWithoutOp(QCOProgramBuilder& b) { +SmallVector resetMultipleQubitsWithoutOp(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); q[0] = b.reset(q[0]); q[1] = b.reset(q[1]); + return measureAndReturn(b, q.qubits); } -void repeatedResetWithoutOp(QCOProgramBuilder& b) { +Value repeatedResetWithoutOp(QCOProgramBuilder& b) { auto q = b.allocQubit(); q = b.reset(q); q = b.reset(q); q = b.reset(q); + return b.measure(q).second; } -void resetQubitAfterSingleOp(QCOProgramBuilder& b) { +Value resetQubitAfterSingleOp(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.h(q[0]); q[0] = b.reset(q[0]); + return b.measure(q[0]).second; } -void resetMultipleQubitsAfterSingleOp(QCOProgramBuilder& b) { +SmallVector resetMultipleQubitsAfterSingleOp(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); q[0] = b.h(q[0]); q[0] = b.reset(q[0]); q[1] = b.h(q[1]); q[1] = b.reset(q[1]); + return measureAndReturn(b, q.qubits); } -void repeatedResetAfterSingleOp(QCOProgramBuilder& b) { +Value repeatedResetAfterSingleOp(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.h(q[0]); q[0] = b.reset(q[0]); q[0] = b.reset(q[0]); q[0] = b.reset(q[0]); + return b.measure(q[0]).second; } -void globalPhase(QCOProgramBuilder& b) { b.gphase(0.123); } +Value globalPhase(QCOProgramBuilder& b) { + b.gphase(0.123); + return b.intConstant(0); +} -void singleControlledGlobalPhase(QCOProgramBuilder& b) { +Value singleControlledGlobalPhase(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.cgphase(0.123, q[0]); + q[0] = b.cgphase(0.123, q[0]); + return b.measure(q[0]).second; } -void multipleControlledGlobalPhase(QCOProgramBuilder& b) { +SmallVector multipleControlledGlobalPhase(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.mcgphase(0.123, {q[0], q[1], q[2]}); + auto qs = b.mcgphase(0.123, {q[0], q[1], q[2]}); + return measureAndReturn(b, qs); } -void inverseGlobalPhase(QCOProgramBuilder& b) { - b.inv({}, [&](ValueRange /*qubits*/) { +Value inverseGlobalPhase(QCOProgramBuilder& b) { + b.inv(ValueRange{}, [&](ValueRange /*qubits*/) { b.gphase(-0.123); return SmallVector{}; }); + return b.intConstant(0); } -void inverseMultipleControlledGlobalPhase(QCOProgramBuilder& b) { +SmallVector inverseMultipleControlledGlobalPhase(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { + auto qs = b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { SmallVector controls{qubits[0], qubits[1], qubits[2]}; auto controlsOut = b.mcgphase(-0.123, controls); return SmallVector(controlsOut.begin(), controlsOut.end()); }); + return measureAndReturn(b, qs); } -void identity(QCOProgramBuilder& b) { - auto q = b.allocQubitRegister(1); - b.id(q[0]); +Value identity(QCOProgramBuilder& b) { + auto q = b.allocQubit(); + q = b.id(q); + return b.measure(q).second; } -void singleControlledIdentity(QCOProgramBuilder& b) { +SmallVector singleControlledIdentity(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.cid(q[1], q[0]); + std::tie(q[1], q[0]) = b.cid(q[1], q[0]); + return measureAndReturn(b, q.qubits); } -void multipleControlledIdentity(QCOProgramBuilder& b) { +SmallVector multipleControlledIdentity(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.mcid({q[2], q[1]}, q[0]); + auto res = b.mcid({q[2], q[1]}, q[0]); + q[2] = res.first[0]; + q[1] = res.first[1]; + q[0] = res.second; + return measureAndReturn(b, q.qubits); } -void nestedControlledIdentity(QCOProgramBuilder& b) { +SmallVector nestedControlledIdentity(QCOProgramBuilder& b) { auto reg = b.allocQubitRegister(3); - b.ctrl({reg[0]}, {reg[1], reg[2]}, [&](ValueRange targets) { - const auto& [innerControlsOut, innerTargetsOut] = - b.ctrl({targets[0]}, {targets[1]}, [&](ValueRange innerTargets) { - return SmallVector{b.id(innerTargets[0])}; - }); - return llvm::to_vector( - llvm::concat(innerControlsOut, innerTargetsOut)); + auto res = b.ctrl({reg[0]}, {reg[1], reg[2]}, [&](ValueRange targets) { + const auto& [innerControlsOut, innerTargetsOut] = b.ctrl( + targets[0], targets[1], [&](Value target) { return b.id(target); }); + return SmallVector{innerControlsOut, innerTargetsOut}; }); + reg[0] = res.first[0]; + reg[1] = res.second[0]; + reg[2] = res.second[1]; + return measureAndReturn(b, reg.qubits); } -void trivialControlledIdentity(QCOProgramBuilder& b) { - auto q = b.allocQubitRegister(1); - b.mcid({}, q[0]); +Value trivialControlledIdentity(QCOProgramBuilder& b) { + auto q = b.allocQubit(); + auto res = b.mcid({}, q); + q = res.second; + return b.measure(q).second; } -void inverseIdentity(QCOProgramBuilder& b) { +Value inverseIdentity(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv({q[0]}, - [&](ValueRange qubits) { return SmallVector{b.id(qubits[0])}; }); + auto res = b.inv(q[0], [&](Value qubit) { return b.id(qubit); }); + return b.measure(res).second; } -void inverseMultipleControlledIdentity(QCOProgramBuilder& b) { +SmallVector inverseMultipleControlledIdentity(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { + auto res = b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { const auto& [controlsOut, targetOut] = b.mcid({qubits[0], qubits[1]}, qubits[2]); return llvm::to_vector( llvm::concat(controlsOut, ValueRange{targetOut})); }); + q[0] = res[0]; + q[1] = res[1]; + q[2] = res[2]; + return measureAndReturn(b, q.qubits); } -void x(QCOProgramBuilder& b) { +Value x(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.x(q[0]); + q[0] = b.x(q[0]); + return b.measure(q[0]).second; } -void singleControlledX(QCOProgramBuilder& b) { +SmallVector singleControlledX(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.cx(q[0], q[1]); + std::tie(q[0], q[1]) = b.cx(q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void multipleControlledX(QCOProgramBuilder& b) { +SmallVector multipleControlledX(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.mcx({q[0], q[1]}, q[2]); + auto res = b.mcx({q[0], q[1]}, q[2]); + q[0] = res.first[0]; + q[1] = res.first[1]; + q[2] = res.second; + return measureAndReturn(b, q.qubits); } -void nestedControlledX(QCOProgramBuilder& b) { +SmallVector nestedControlledX(QCOProgramBuilder& b) { auto reg = b.allocQubitRegister(3); - b.ctrl({reg[0]}, {reg[1], reg[2]}, [&](ValueRange targets) { - const auto& [innerControlsOut, innerTargetsOut] = - b.ctrl({targets[0]}, {targets[1]}, [&](ValueRange innerTargets) { - return SmallVector{b.x(innerTargets[0])}; - }); - return llvm::to_vector( - llvm::concat(innerControlsOut, innerTargetsOut)); + auto res = b.ctrl({reg[0]}, {reg[1], reg[2]}, [&](ValueRange targets) { + const auto& [innerControlsOut, innerTargetsOut] = b.ctrl( + targets[0], targets[1], [&](Value target) { return b.x(target); }); + return SmallVector{innerControlsOut, innerTargetsOut}; }); + reg[0] = res.first[0]; + reg[1] = res.second[0]; + reg[2] = res.second[1]; + return measureAndReturn(b, reg.qubits); } -void trivialControlledX(QCOProgramBuilder& b) { +Value trivialControlledX(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.mcx({}, q[0]); + auto res = b.mcx({}, q[0]); + q[0] = res.second; + return b.measure(q[0]).second; } -void repeatedControlledX(QCOProgramBuilder& b) { - auto tensor = b.qtensorAlloc(64); - - Value q0; - std::tie(tensor, q0) = b.qtensorExtract(tensor, 0); - - SmallVector values(63); - for (auto i = 1; i < 64; i++) { - Value qi; - std::tie(tensor, qi) = b.qtensorExtract(tensor, i); - values[i - 1] = qi; - } - - q0 = b.h(q0); - for (auto i = 1; i < 64; i++) { - std::tie(q0, values[i - 1]) = b.cx(q0, values[i - 1]); +SmallVector repeatedControlledX(QCOProgramBuilder& b) { + auto q0 = b.allocQubit(); + auto control = b.h(q0); + std::vector targets; + for (auto i = 0; i < 50; i++) { + auto qubit = b.allocQubit(); + auto res = b.cx(control, qubit); + control = res.first; + targets.push_back(res.second); } + targets.push_back(control); + return measureAndReturn(b, + SmallVector(targets.begin(), targets.end())); } -void inverseX(QCOProgramBuilder& b) { +Value inverseX(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv({q[0]}, [&](ValueRange qubits) { return SmallVector{b.x(qubits[0])}; }); + auto res = b.inv(q[0], [&](Value qubit) { return b.x(qubit); }); + return b.measure(res).second; } -void inverseMultipleControlledX(QCOProgramBuilder& b) { +SmallVector inverseMultipleControlledX(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { + auto res = b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { const auto& [controlsOut, targetOut] = b.mcx({qubits[0], qubits[1]}, qubits[2]); return llvm::to_vector( llvm::concat(controlsOut, ValueRange{targetOut})); }); + q[0] = res[0]; + q[1] = res[1]; + q[2] = res[2]; + return measureAndReturn(b, q.qubits); } -void twoX(QCOProgramBuilder& b) { +Value twoX(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.x(q[0]); q[0] = b.x(q[0]); + return b.measure(q[0]).second; } -void controlledTwoX(QCOProgramBuilder& b) { +SmallVector controlledTwoX(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.ctrl(q[0], q[1], [&](ValueRange targets) { - auto q = b.x(targets[0]); - q = b.x(q); - return SmallVector{q}; + auto res = b.ctrl(q[0], q[1], [&](Value target) { + target = b.x(target); + return b.x(target); }); + return measureAndReturn(b, {res.first, res.second}); } -void inverseTwoX(QCOProgramBuilder& b) { +Value inverseTwoX(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv(q[0], [&](ValueRange qubits) { - auto q = b.x(qubits[0]); - q = b.x(q); - return SmallVector{q}; + auto res = b.inv(q[0], [&](Value qubit) { + qubit = b.x(qubit); + qubit = b.x(qubit); + return qubit; }); + return b.measure(res).second; } -void inverseGphaseX(QCOProgramBuilder& b) { +Value inverseGphaseX(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv(q[0], [&](ValueRange qubits) { + auto res = b.inv(q[0], [&](Value qubit) { b.gphase(-0.123); - return SmallVector{b.x(qubits[0])}; + return b.x(qubit); }); + return b.measure(res).second; } -void inverseGphaseBarrier(QCOProgramBuilder& b) { +Value inverseGphaseBarrier(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv(q[0], [&](ValueRange qubits) -> SmallVector { + auto res = b.inv(q[0], [&](Value qubit) { b.gphase(0.123); - return {b.barrier({qubits[0]})[0]}; + return b.barrier({qubit})[0]; }); + return b.measure(res).second; } -void inverseTwoBarriersInInv(QCOProgramBuilder& b) { +Value inverseTwoBarriersInInv(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv(q[0], [&](ValueRange qubits) -> SmallVector { - auto q0 = b.barrier({qubits[0]})[0]; - return {b.barrier({q0})[0]}; + auto res = b.inv(q[0], [&](Value qubit) { + qubit = b.barrier({qubit})[0]; + return b.barrier({qubit})[0]; }); + return b.measure(res).second; } -void y(QCOProgramBuilder& b) { +Value y(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.y(q[0]); + q[0] = b.y(q[0]); + return b.measure(q[0]).second; } -void singleControlledY(QCOProgramBuilder& b) { +SmallVector singleControlledY(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.cy(q[0], q[1]); + std::tie(q[0], q[1]) = b.cy(q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void multipleControlledY(QCOProgramBuilder& b) { +SmallVector multipleControlledY(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.mcy({q[0], q[1]}, q[2]); + auto res = b.mcy({q[0], q[1]}, q[2]); + q[0] = res.first[0]; + q[1] = res.first[1]; + q[2] = res.second; + return measureAndReturn(b, q.qubits); } -void nestedControlledY(QCOProgramBuilder& b) { +SmallVector nestedControlledY(QCOProgramBuilder& b) { auto reg = b.allocQubitRegister(3); - b.ctrl({reg[0]}, {reg[1], reg[2]}, [&](ValueRange targets) { - const auto& [innerControlsOut, innerTargetsOut] = - b.ctrl({targets[0]}, {targets[1]}, [&](ValueRange innerTargets) { - return SmallVector{b.y(innerTargets[0])}; - }); - return llvm::to_vector( - llvm::concat(innerControlsOut, innerTargetsOut)); + auto res = b.ctrl({reg[0]}, {reg[1], reg[2]}, [&](ValueRange targets) { + const auto& [innerControlsOut, innerTargetsOut] = b.ctrl( + targets[0], targets[1], [&](Value target) { return b.y(target); }); + return SmallVector{innerControlsOut, innerTargetsOut}; }); + reg[0] = res.first[0]; + reg[1] = res.second[0]; + reg[2] = res.second[1]; + return measureAndReturn(b, reg.qubits); } -void trivialControlledY(QCOProgramBuilder& b) { +Value trivialControlledY(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.mcy({}, q[0]); + auto res = b.mcy({}, q[0]); + q[0] = res.second; + return b.measure(q[0]).second; } -void inverseY(QCOProgramBuilder& b) { +Value inverseY(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv({q[0]}, [&](ValueRange qubits) { return SmallVector{b.y(qubits[0])}; }); + auto res = b.inv(q[0], [&](Value qubit) { return b.y(qubit); }); + return b.measure(res).second; } -void inverseMultipleControlledY(QCOProgramBuilder& b) { +SmallVector inverseMultipleControlledY(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { + auto res = b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { const auto& [controlsOut, targetOut] = b.mcy({qubits[0], qubits[1]}, qubits[2]); return llvm::to_vector( llvm::concat(controlsOut, ValueRange{targetOut})); }); + q[0] = res[0]; + q[1] = res[1]; + q[2] = res[2]; + return measureAndReturn(b, q.qubits); } -void twoY(QCOProgramBuilder& b) { +Value twoY(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.y(q[0]); q[0] = b.y(q[0]); + return b.measure(q[0]).second; } -void z(QCOProgramBuilder& b) { +Value z(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.z(q[0]); + q[0] = b.z(q[0]); + return b.measure(q[0]).second; } -void singleControlledZ(QCOProgramBuilder& b) { +SmallVector singleControlledZ(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.cz(q[0], q[1]); + std::tie(q[0], q[1]) = b.cz(q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void multipleControlledZ(QCOProgramBuilder& b) { +SmallVector multipleControlledZ(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.mcz({q[0], q[1]}, q[2]); + auto res = b.mcz({q[0], q[1]}, q[2]); + q[0] = res.first[0]; + q[1] = res.first[1]; + q[2] = res.second; + return measureAndReturn(b, q.qubits); } -void nestedControlledZ(QCOProgramBuilder& b) { +SmallVector nestedControlledZ(QCOProgramBuilder& b) { auto reg = b.allocQubitRegister(3); - b.ctrl({reg[0]}, {reg[1], reg[2]}, [&](ValueRange targets) { - const auto& [innerControlsOut, innerTargetsOut] = - b.ctrl({targets[0]}, {targets[1]}, [&](ValueRange innerTargets) { - return SmallVector{b.z(innerTargets[0])}; - }); - return llvm::to_vector( - llvm::concat(innerControlsOut, innerTargetsOut)); + auto res = b.ctrl({reg[0]}, {reg[1], reg[2]}, [&](ValueRange targets) { + const auto& [innerControlsOut, innerTargetsOut] = b.ctrl( + targets[0], targets[1], [&](Value target) { return b.z(target); }); + return SmallVector{innerControlsOut, innerTargetsOut}; }); + reg[0] = res.first[0]; + reg[1] = res.second[0]; + reg[2] = res.second[1]; + return measureAndReturn(b, reg.qubits); } -void trivialControlledZ(QCOProgramBuilder& b) { +Value trivialControlledZ(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.mcz({}, q[0]); + auto res = b.mcz({}, q[0]); + q[0] = res.second; + return b.measure(q[0]).second; } -void inverseZ(QCOProgramBuilder& b) { +Value inverseZ(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv({q[0]}, [&](ValueRange qubits) { return SmallVector{b.z(qubits[0])}; }); + auto res = b.inv(q[0], [&](Value qubit) { return b.z(qubit); }); + return b.measure(res).second; } -void inverseMultipleControlledZ(QCOProgramBuilder& b) { +SmallVector inverseMultipleControlledZ(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { + auto res = b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { const auto& [controlsOut, targetOut] = b.mcz({qubits[0], qubits[1]}, qubits[2]); return llvm::to_vector( llvm::concat(controlsOut, ValueRange{targetOut})); }); + q[0] = res[0]; + q[1] = res[1]; + q[2] = res[2]; + return measureAndReturn(b, q.qubits); } -void twoZ(QCOProgramBuilder& b) { +Value twoZ(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.z(q[0]); q[0] = b.z(q[0]); + return b.measure(q[0]).second; } -void h(QCOProgramBuilder& b) { +Value h(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.h(q[0]); + q[0] = b.h(q[0]); + return b.measure(q[0]).second; } -void singleControlledH(QCOProgramBuilder& b) { +SmallVector singleControlledH(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.ch(q[0], q[1]); + std::tie(q[0], q[1]) = b.ch(q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void multipleControlledH(QCOProgramBuilder& b) { +SmallVector multipleControlledH(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.mch({q[0], q[1]}, q[2]); + auto res = b.mch({q[0], q[1]}, q[2]); + q[0] = res.first[0]; + q[1] = res.first[1]; + q[2] = res.second; + return measureAndReturn(b, q.qubits); } -void nestedControlledH(QCOProgramBuilder& b) { +SmallVector nestedControlledH(QCOProgramBuilder& b) { auto reg = b.allocQubitRegister(3); - b.ctrl({reg[0]}, {reg[1], reg[2]}, [&](ValueRange targets) { - const auto& [innerControlsOut, innerTargetsOut] = - b.ctrl({targets[0]}, {targets[1]}, [&](ValueRange innerTargets) { - return SmallVector{b.h(innerTargets[0])}; - }); - return llvm::to_vector( - llvm::concat(innerControlsOut, innerTargetsOut)); + auto res = b.ctrl({reg[0]}, {reg[1], reg[2]}, [&](ValueRange targets) { + const auto& [innerControlsOut, innerTargetsOut] = b.ctrl( + targets[0], targets[1], [&](Value target) { return b.h(target); }); + return SmallVector{innerControlsOut, innerTargetsOut}; }); + reg[0] = res.first[0]; + reg[1] = res.second[0]; + reg[2] = res.second[1]; + return measureAndReturn(b, reg.qubits); } -void trivialControlledH(QCOProgramBuilder& b) { +Value trivialControlledH(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.mch({}, q[0]); + auto res = b.mch({}, q[0]); + q[0] = res.second; + return b.measure(q[0]).second; } -void inverseH(QCOProgramBuilder& b) { +Value inverseH(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv({q[0]}, [&](ValueRange qubits) { return SmallVector{b.h(qubits[0])}; }); + auto res = b.inv(q[0], [&](Value qubit) { return b.h(qubit); }); + return b.measure(res).second; } -void inverseMultipleControlledH(QCOProgramBuilder& b) { +SmallVector inverseMultipleControlledH(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { + auto res = b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { const auto& [controlsOut, targetOut] = b.mch({qubits[0], qubits[1]}, qubits[2]); return llvm::to_vector( llvm::concat(controlsOut, ValueRange{targetOut})); }); + q[0] = res[0]; + q[1] = res[1]; + q[2] = res[2]; + return measureAndReturn(b, q.qubits); } -void twoH(QCOProgramBuilder& b) { - auto q = b.allocQubitRegister(1); - q[0] = b.h(q[0]); - q[0] = b.h(q[0]); +Value twoH(QCOProgramBuilder& b) { + auto q = b.allocQubit(); + q = b.h(q); + q = b.h(q); + return b.measure(q).second; } -void hWithoutRegister(QCOProgramBuilder& b) { +Value hWithoutRegister(QCOProgramBuilder& b) { auto q = b.allocQubit(); - b.h(q); + q = b.h(q); + return b.measure(q).second; } -void s(QCOProgramBuilder& b) { +Value s(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.s(q[0]); + q[0] = b.s(q[0]); + return b.measure(q[0]).second; } -void singleControlledS(QCOProgramBuilder& b) { +SmallVector singleControlledS(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.cs(q[0], q[1]); + auto res = b.cs(q[0], q[1]); + q[0] = res.first; + q[1] = res.second; + return measureAndReturn(b, q.qubits); } -void multipleControlledS(QCOProgramBuilder& b) { +SmallVector multipleControlledS(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.mcs({q[0], q[1]}, q[2]); + auto res = b.mcs({q[0], q[1]}, q[2]); + q[0] = res.first[0]; + q[1] = res.first[1]; + q[2] = res.second; + return measureAndReturn(b, q.qubits); } -void nestedControlledS(QCOProgramBuilder& b) { +SmallVector nestedControlledS(QCOProgramBuilder& b) { auto reg = b.allocQubitRegister(3); - b.ctrl({reg[0]}, {reg[1], reg[2]}, [&](ValueRange targets) { - const auto& [innerControlsOut, innerTargetsOut] = - b.ctrl({targets[0]}, {targets[1]}, [&](ValueRange innerTargets) { - return SmallVector{b.s(innerTargets[0])}; - }); - return llvm::to_vector( - llvm::concat(innerControlsOut, innerTargetsOut)); + auto res = b.ctrl({reg[0]}, {reg[1], reg[2]}, [&](ValueRange targets) { + const auto& [innerControlsOut, innerTargetsOut] = b.ctrl( + targets[0], targets[1], [&](Value target) { return b.s(target); }); + return SmallVector{innerControlsOut, innerTargetsOut}; }); + reg[0] = res.first[0]; + reg[1] = res.second[0]; + reg[2] = res.second[1]; + return measureAndReturn(b, reg.qubits); } -void trivialControlledS(QCOProgramBuilder& b) { +Value trivialControlledS(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.mcs({}, q[0]); + auto res = b.mcs({}, q[0]); + q[0] = res.second; + return b.measure(q[0]).second; } -void inverseS(QCOProgramBuilder& b) { +Value inverseS(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv({q[0]}, [&](ValueRange qubits) { return SmallVector{b.s(qubits[0])}; }); + auto res = b.inv(q[0], [&](Value qubit) { return b.s(qubit); }); + return b.measure(res).second; } -void inverseMultipleControlledS(QCOProgramBuilder& b) { +SmallVector inverseMultipleControlledS(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { + auto res = b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { const auto& [controlsOut, targetOut] = b.mcs({qubits[0], qubits[1]}, qubits[2]); return llvm::to_vector( llvm::concat(controlsOut, ValueRange{targetOut})); }); + q[0] = res[0]; + q[1] = res[1]; + q[2] = res[2]; + return measureAndReturn(b, q.qubits); } -void sThenSdg(QCOProgramBuilder& b) { +Value sThenSdg(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.s(q[0]); q[0] = b.sdg(q[0]); + return b.measure(q[0]).second; } -void twoS(QCOProgramBuilder& b) { +Value twoS(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.s(q[0]); q[0] = b.s(q[0]); + return b.measure(q[0]).second; } -void sdg(QCOProgramBuilder& b) { +Value sdg(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.sdg(q[0]); + q[0] = b.sdg(q[0]); + return b.measure(q[0]).second; } -void singleControlledSdg(QCOProgramBuilder& b) { +SmallVector singleControlledSdg(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.csdg(q[0], q[1]); + auto res = b.csdg(q[0], q[1]); + q[0] = res.first; + q[1] = res.second; + return measureAndReturn(b, q.qubits); } -void multipleControlledSdg(QCOProgramBuilder& b) { +SmallVector multipleControlledSdg(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.mcsdg({q[0], q[1]}, q[2]); + auto res = b.mcsdg({q[0], q[1]}, q[2]); + q[0] = res.first[0]; + q[1] = res.first[1]; + q[2] = res.second; + return measureAndReturn(b, q.qubits); } -void nestedControlledSdg(QCOProgramBuilder& b) { +SmallVector nestedControlledSdg(QCOProgramBuilder& b) { auto reg = b.allocQubitRegister(3); - b.ctrl({reg[0]}, {reg[1], reg[2]}, [&](ValueRange targets) { - const auto& [innerControlsOut, innerTargetsOut] = - b.ctrl({targets[0]}, {targets[1]}, [&](ValueRange innerTargets) { - return SmallVector{b.sdg(innerTargets[0])}; - }); - return llvm::to_vector( - llvm::concat(innerControlsOut, innerTargetsOut)); + auto res = b.ctrl({reg[0]}, {reg[1], reg[2]}, [&](ValueRange targets) { + const auto& [innerControlsOut, innerTargetsOut] = b.ctrl( + targets[0], targets[1], [&](Value target) { return b.sdg(target); }); + return SmallVector{innerControlsOut, innerTargetsOut}; }); + reg[0] = res.first[0]; + reg[1] = res.second[0]; + reg[2] = res.second[1]; + return measureAndReturn(b, reg.qubits); } -void trivialControlledSdg(QCOProgramBuilder& b) { +Value trivialControlledSdg(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.mcsdg({}, q[0]); + auto res = b.mcsdg({}, q[0]); + q[0] = res.second; + return b.measure(q[0]).second; } -void inverseSdg(QCOProgramBuilder& b) { +Value inverseSdg(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv({q[0]}, - [&](ValueRange qubits) { return SmallVector{b.sdg(qubits[0])}; }); + auto res = b.inv(q[0], [&](Value qubit) { return b.sdg(qubit); }); + return b.measure(res).second; } -void inverseMultipleControlledSdg(QCOProgramBuilder& b) { +SmallVector inverseMultipleControlledSdg(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { + auto res = b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { const auto& [controlsOut, targetOut] = b.mcsdg({qubits[0], qubits[1]}, qubits[2]); return llvm::to_vector( llvm::concat(controlsOut, ValueRange{targetOut})); }); + q[0] = res[0]; + q[1] = res[1]; + q[2] = res[2]; + return measureAndReturn(b, q.qubits); } -void sdgThenS(QCOProgramBuilder& b) { +Value sdgThenS(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.sdg(q[0]); q[0] = b.s(q[0]); + return b.measure(q[0]).second; } -void twoSdg(QCOProgramBuilder& b) { +Value twoSdg(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.sdg(q[0]); q[0] = b.sdg(q[0]); + return b.measure(q[0]).second; } -void t_(QCOProgramBuilder& b) { +Value t_(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.t(q[0]); + q[0] = b.t(q[0]); + return b.measure(q[0]).second; } -void singleControlledT(QCOProgramBuilder& b) { +SmallVector singleControlledT(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.ct(q[0], q[1]); + auto res = b.ct(q[0], q[1]); + q[0] = res.first; + q[1] = res.second; + return measureAndReturn(b, q.qubits); } -void multipleControlledT(QCOProgramBuilder& b) { +SmallVector multipleControlledT(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.mct({q[0], q[1]}, q[2]); + auto res = b.mct({q[0], q[1]}, q[2]); + q[0] = res.first[0]; + q[1] = res.first[1]; + q[2] = res.second; + return measureAndReturn(b, q.qubits); } -void nestedControlledT(QCOProgramBuilder& b) { +SmallVector nestedControlledT(QCOProgramBuilder& b) { auto reg = b.allocQubitRegister(3); - b.ctrl({reg[0]}, {reg[1], reg[2]}, [&](ValueRange targets) { - const auto& [innerControlsOut, innerTargetsOut] = - b.ctrl({targets[0]}, {targets[1]}, [&](ValueRange innerTargets) { - return SmallVector{b.t(innerTargets[0])}; - }); - return llvm::to_vector( - llvm::concat(innerControlsOut, innerTargetsOut)); + auto res = b.ctrl({reg[0]}, {reg[1], reg[2]}, [&](ValueRange targets) { + const auto& [innerControlsOut, innerTargetsOut] = b.ctrl( + targets[0], targets[1], [&](Value target) { return b.t(target); }); + return SmallVector{innerControlsOut, innerTargetsOut}; }); + reg[0] = res.first[0]; + reg[1] = res.second[0]; + reg[2] = res.second[1]; + return measureAndReturn(b, reg.qubits); } -void trivialControlledT(QCOProgramBuilder& b) { +Value trivialControlledT(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.mct({}, q[0]); + auto res = b.mct({}, q[0]); + q[0] = res.second; + return b.measure(q[0]).second; } -void inverseT(QCOProgramBuilder& b) { +Value inverseT(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv({q[0]}, [&](ValueRange qubits) { return SmallVector{b.t(qubits[0])}; }); + auto res = b.inv(q[0], [&](Value qubit) { return b.t(qubit); }); + return b.measure(res).second; } -void inverseMultipleControlledT(QCOProgramBuilder& b) { +SmallVector inverseMultipleControlledT(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { + auto res = b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { const auto& [controlsOut, targetOut] = b.mct({qubits[0], qubits[1]}, qubits[2]); return llvm::to_vector( llvm::concat(controlsOut, ValueRange{targetOut})); }); + q[0] = res[0]; + q[1] = res[1]; + q[2] = res[2]; + return measureAndReturn(b, q.qubits); } -void tThenTdg(QCOProgramBuilder& b) { +Value tThenTdg(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.t(q[0]); q[0] = b.tdg(q[0]); + return b.measure(q[0]).second; } -void twoT(QCOProgramBuilder& b) { +Value twoT(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.t(q[0]); q[0] = b.t(q[0]); + return b.measure(q[0]).second; } -void tdg(QCOProgramBuilder& b) { +Value tdg(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.tdg(q[0]); + q[0] = b.tdg(q[0]); + return b.measure(q[0]).second; } -void singleControlledTdg(QCOProgramBuilder& b) { +SmallVector singleControlledTdg(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.ctdg(q[0], q[1]); + auto res = b.ctdg(q[0], q[1]); + q[0] = res.first; + q[1] = res.second; + return measureAndReturn(b, q.qubits); } -void multipleControlledTdg(QCOProgramBuilder& b) { +SmallVector multipleControlledTdg(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.mctdg({q[0], q[1]}, q[2]); + auto res = b.mctdg({q[0], q[1]}, q[2]); + q[0] = res.first[0]; + q[1] = res.first[1]; + q[2] = res.second; + return measureAndReturn(b, q.qubits); } -void nestedControlledTdg(QCOProgramBuilder& b) { +SmallVector nestedControlledTdg(QCOProgramBuilder& b) { auto reg = b.allocQubitRegister(3); - b.ctrl({reg[0]}, {reg[1], reg[2]}, [&](ValueRange targets) { - const auto& [innerControlsOut, innerTargetsOut] = - b.ctrl({targets[0]}, {targets[1]}, [&](ValueRange innerTargets) { - return SmallVector{b.tdg(innerTargets[0])}; - }); - return llvm::to_vector( - llvm::concat(innerControlsOut, innerTargetsOut)); + auto res = b.ctrl({reg[0]}, {reg[1], reg[2]}, [&](ValueRange targets) { + const auto& [innerControlsOut, innerTargetsOut] = b.ctrl( + targets[0], targets[1], [&](Value target) { return b.tdg(target); }); + return SmallVector{innerControlsOut, innerTargetsOut}; }); + reg[0] = res.first[0]; + reg[1] = res.second[0]; + reg[2] = res.second[1]; + return measureAndReturn(b, reg.qubits); } -void trivialControlledTdg(QCOProgramBuilder& b) { +Value trivialControlledTdg(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.mctdg({}, q[0]); + auto res = b.mctdg({}, q[0]); + q[0] = res.second; + return b.measure(q[0]).second; } -void inverseTdg(QCOProgramBuilder& b) { +Value inverseTdg(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv({q[0]}, - [&](ValueRange qubits) { return SmallVector{b.tdg(qubits[0])}; }); + auto res = b.inv(q[0], [&](Value qubit) { return b.tdg(qubit); }); + return b.measure(res).second; } -void inverseMultipleControlledTdg(QCOProgramBuilder& b) { +SmallVector inverseMultipleControlledTdg(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { + auto res = b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { const auto& [controlsOut, targetOut] = b.mctdg({qubits[0], qubits[1]}, qubits[2]); return llvm::to_vector( llvm::concat(controlsOut, ValueRange{targetOut})); }); + q[0] = res[0]; + q[1] = res[1]; + q[2] = res[2]; + return measureAndReturn(b, q.qubits); } -void tdgThenT(QCOProgramBuilder& b) { +Value tdgThenT(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.tdg(q[0]); q[0] = b.t(q[0]); + return b.measure(q[0]).second; } -void twoTdg(QCOProgramBuilder& b) { +Value twoTdg(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.tdg(q[0]); q[0] = b.tdg(q[0]); + return b.measure(q[0]).second; } -void sx(QCOProgramBuilder& b) { +Value sx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.sx(q[0]); + q[0] = b.sx(q[0]); + return b.measure(q[0]).second; } -void singleControlledSx(QCOProgramBuilder& b) { +SmallVector singleControlledSx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.csx(q[0], q[1]); + auto res = b.csx(q[0], q[1]); + q[0] = res.first; + q[1] = res.second; + return measureAndReturn(b, q.qubits); } -void multipleControlledSx(QCOProgramBuilder& b) { +SmallVector multipleControlledSx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.mcsx({q[0], q[1]}, q[2]); + auto res = b.mcsx({q[0], q[1]}, q[2]); + q[0] = res.first[0]; + q[1] = res.first[1]; + q[2] = res.second; + return measureAndReturn(b, q.qubits); } -void nestedControlledSx(QCOProgramBuilder& b) { +SmallVector nestedControlledSx(QCOProgramBuilder& b) { auto reg = b.allocQubitRegister(3); - b.ctrl({reg[0]}, {reg[1], reg[2]}, [&](ValueRange targets) { - const auto& [innerControlsOut, innerTargetsOut] = - b.ctrl({targets[0]}, {targets[1]}, [&](ValueRange innerTargets) { - return SmallVector{b.sx(innerTargets[0])}; - }); - return llvm::to_vector( - llvm::concat(innerControlsOut, innerTargetsOut)); + auto res = b.ctrl({reg[0]}, {reg[1], reg[2]}, [&](ValueRange targets) { + const auto& [innerControlsOut, innerTargetsOut] = b.ctrl( + targets[0], targets[1], [&](Value target) { return b.sx(target); }); + return SmallVector{innerControlsOut, innerTargetsOut}; }); + reg[0] = res.first[0]; + reg[1] = res.second[0]; + reg[2] = res.second[1]; + return measureAndReturn(b, reg.qubits); } -void trivialControlledSx(QCOProgramBuilder& b) { +Value trivialControlledSx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.mcsx({}, q[0]); + auto res = b.mcsx({}, q[0]); + q[0] = res.second; + return b.measure(q[0]).second; } -void inverseSx(QCOProgramBuilder& b) { +Value inverseSx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv({q[0]}, - [&](ValueRange qubits) { return SmallVector{b.sx(qubits[0])}; }); + auto res = b.inv(q[0], [&](Value qubit) { return b.sx(qubit); }); + return b.measure(res).second; } -void inverseMultipleControlledSx(QCOProgramBuilder& b) { +SmallVector inverseMultipleControlledSx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { + auto res = b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { const auto& [controlsOut, targetOut] = b.mcsx({qubits[0], qubits[1]}, qubits[2]); return llvm::to_vector( llvm::concat(controlsOut, ValueRange{targetOut})); }); + q[0] = res[0]; + q[1] = res[1]; + q[2] = res[2]; + return measureAndReturn(b, q.qubits); } -void sxThenSxdg(QCOProgramBuilder& b) { +Value sxThenSxdg(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.sx(q[0]); q[0] = b.sxdg(q[0]); + return b.measure(q[0]).second; } -void twoSx(QCOProgramBuilder& b) { +Value twoSx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.sx(q[0]); q[0] = b.sx(q[0]); + return b.measure(q[0]).second; } -void sxdg(QCOProgramBuilder& b) { +Value sxdg(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.sxdg(q[0]); + q[0] = b.sxdg(q[0]); + return b.measure(q[0]).second; } -void singleControlledSxdg(QCOProgramBuilder& b) { +SmallVector singleControlledSxdg(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.csxdg(q[0], q[1]); + auto res = b.csxdg(q[0], q[1]); + q[0] = res.first; + q[1] = res.second; + return measureAndReturn(b, q.qubits); } -void multipleControlledSxdg(QCOProgramBuilder& b) { +SmallVector multipleControlledSxdg(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.mcsxdg({q[0], q[1]}, q[2]); + auto res = b.mcsxdg({q[0], q[1]}, q[2]); + q[0] = res.first[0]; + q[1] = res.first[1]; + q[2] = res.second; + return measureAndReturn(b, q.qubits); } -void nestedControlledSxdg(QCOProgramBuilder& b) { +SmallVector nestedControlledSxdg(QCOProgramBuilder& b) { auto reg = b.allocQubitRegister(3); - b.ctrl({reg[0]}, {reg[1], reg[2]}, [&](ValueRange targets) { - const auto& [innerControlsOut, innerTargetsOut] = - b.ctrl({targets[0]}, {targets[1]}, [&](ValueRange innerTargets) { - return SmallVector{b.sxdg(innerTargets[0])}; - }); - return llvm::to_vector( - llvm::concat(innerControlsOut, innerTargetsOut)); + auto res = b.ctrl({reg[0]}, {reg[1], reg[2]}, [&](ValueRange targets) { + const auto& [innerControlsOut, innerTargetsOut] = b.ctrl( + targets[0], targets[1], [&](Value target) { return b.sxdg(target); }); + return SmallVector{innerControlsOut, innerTargetsOut}; }); + reg[0] = res.first[0]; + reg[1] = res.second[0]; + reg[2] = res.second[1]; + return measureAndReturn(b, reg.qubits); } -void trivialControlledSxdg(QCOProgramBuilder& b) { +Value trivialControlledSxdg(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.mcsxdg({}, q[0]); + auto res = b.mcsxdg({}, q[0]); + q[0] = res.second; + return b.measure(q[0]).second; } -void inverseSxdg(QCOProgramBuilder& b) { +Value inverseSxdg(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv({q[0]}, - [&](ValueRange qubits) { return SmallVector{b.sxdg(qubits[0])}; }); + auto res = b.inv(q[0], [&](Value qubit) { return b.sxdg(qubit); }); + return b.measure(res).second; } -void inverseMultipleControlledSxdg(QCOProgramBuilder& b) { +SmallVector inverseMultipleControlledSxdg(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { + auto res = b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { const auto& [controlsOut, targetOut] = b.mcsxdg({qubits[0], qubits[1]}, qubits[2]); return llvm::to_vector( llvm::concat(controlsOut, ValueRange{targetOut})); }); + q[0] = res[0]; + q[1] = res[1]; + q[2] = res[2]; + return measureAndReturn(b, q.qubits); } -void sxdgThenSx(QCOProgramBuilder& b) { +Value sxdgThenSx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.sxdg(q[0]); q[0] = b.sx(q[0]); + return b.measure(q[0]).second; } -void twoSxdg(QCOProgramBuilder& b) { +Value twoSxdg(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.sxdg(q[0]); q[0] = b.sxdg(q[0]); + return b.measure(q[0]).second; } -void rx(QCOProgramBuilder& b) { +Value rx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.rx(0.123, q[0]); + q[0] = b.rx(0.123, q[0]); + return b.measure(q[0]).second; } -void singleControlledRx(QCOProgramBuilder& b) { +SmallVector singleControlledRx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.crx(0.123, q[0], q[1]); + auto res = b.crx(0.123, q[0], q[1]); + q[0] = res.first; + q[1] = res.second; + return measureAndReturn(b, q.qubits); } -void multipleControlledRx(QCOProgramBuilder& b) { +SmallVector multipleControlledRx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.mcrx(0.123, {q[0], q[1]}, q[2]); + auto res = b.mcrx(0.123, {q[0], q[1]}, q[2]); + q[0] = res.first[0]; + q[1] = res.first[1]; + q[2] = res.second; + return measureAndReturn(b, q.qubits); } -void nestedControlledRx(QCOProgramBuilder& b) { +SmallVector nestedControlledRx(QCOProgramBuilder& b) { auto reg = b.allocQubitRegister(3); - b.ctrl({reg[0]}, {reg[1], reg[2]}, [&](ValueRange targets) { + auto res = b.ctrl({reg[0]}, {reg[1], reg[2]}, [&](ValueRange targets) { const auto& [innerControlsOut, innerTargetsOut] = - b.ctrl({targets[0]}, {targets[1]}, [&](ValueRange innerTargets) { - return SmallVector{b.rx(0.123, innerTargets[0])}; - }); - return llvm::to_vector( - llvm::concat(innerControlsOut, innerTargetsOut)); + b.ctrl(targets[0], targets[1], + [&](Value target) { return b.rx(0.123, target); }); + return SmallVector{innerControlsOut, innerTargetsOut}; }); + reg[0] = res.first[0]; + reg[1] = res.second[0]; + reg[2] = res.second[1]; + return measureAndReturn(b, reg.qubits); } -void trivialControlledRx(QCOProgramBuilder& b) { +Value trivialControlledRx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.mcrx(0.123, {}, q[0]); + auto res = b.mcrx(0.123, {}, q[0]); + q[0] = res.second; + return b.measure(q[0]).second; } -void inverseRx(QCOProgramBuilder& b) { +Value inverseRx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv({q[0]}, [&](ValueRange qubits) { - return SmallVector{b.rx(-0.123, qubits[0])}; - }); + auto res = b.inv(q[0], [&](Value qubit) { return b.rx(-0.123, qubit); }); + return b.measure(res).second; } -void inverseMultipleControlledRx(QCOProgramBuilder& b) { +SmallVector inverseMultipleControlledRx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { + auto res = b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { const auto& [controlsOut, targetOut] = b.mcrx(-0.123, {qubits[0], qubits[1]}, qubits[2]); return llvm::to_vector( llvm::concat(controlsOut, ValueRange{targetOut})); }); + q[0] = res[0]; + q[1] = res[1]; + q[2] = res[2]; + return measureAndReturn(b, q.qubits); } -void twoRxOppositePhase(QCOProgramBuilder& b) { +Value twoRxOppositePhase(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.rx(0.123, q[0]); q[0] = b.rx(-0.123, q[0]); + return b.measure(q[0]).second; } -void rxPiOver2(QCOProgramBuilder& b) { +Value rxPiOver2(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.rx(std::numbers::pi / 2, q[0]); + q[0] = b.rx(std::numbers::pi / 2, q[0]); + return b.measure(q[0]).second; } -void ry(QCOProgramBuilder& b) { +Value ry(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.ry(0.456, q[0]); + q[0] = b.ry(0.456, q[0]); + return b.measure(q[0]).second; } -void singleControlledRy(QCOProgramBuilder& b) { +SmallVector singleControlledRy(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.cry(0.456, q[0], q[1]); + std::tie(q[0], q[1]) = b.cry(0.456, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void multipleControlledRy(QCOProgramBuilder& b) { +SmallVector multipleControlledRy(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.mcry(0.456, {q[0], q[1]}, q[2]); + auto res = b.mcry(0.456, {q[0], q[1]}, q[2]); + q[0] = res.first[0]; + q[1] = res.first[1]; + q[2] = res.second; + return measureAndReturn(b, q.qubits); } -void nestedControlledRy(QCOProgramBuilder& b) { +SmallVector nestedControlledRy(QCOProgramBuilder& b) { auto reg = b.allocQubitRegister(3); - b.ctrl({reg[0]}, {reg[1], reg[2]}, [&](ValueRange targets) { + auto res = b.ctrl({reg[0]}, {reg[1], reg[2]}, [&](ValueRange targets) { const auto& [innerControlsOut, innerTargetsOut] = - b.ctrl({targets[0]}, {targets[1]}, [&](ValueRange innerTargets) { - return SmallVector{b.ry(0.456, innerTargets[0])}; - }); - return llvm::to_vector( - llvm::concat(innerControlsOut, innerTargetsOut)); + b.ctrl(targets[0], targets[1], + [&](Value target) { return b.ry(0.456, target); }); + return SmallVector{innerControlsOut, innerTargetsOut}; }); + reg[0] = res.first[0]; + reg[1] = res.second[0]; + reg[2] = res.second[1]; + return measureAndReturn(b, reg.qubits); } -void trivialControlledRy(QCOProgramBuilder& b) { +Value trivialControlledRy(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.mcry(0.456, {}, q[0]); + auto res = b.mcry(0.456, {}, q[0]); + q[0] = res.second; + return b.measure(q[0]).second; } -void inverseRy(QCOProgramBuilder& b) { +Value inverseRy(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv({q[0]}, [&](ValueRange qubits) { - return SmallVector{b.ry(-0.456, qubits[0])}; - }); + auto res = b.inv(q[0], [&](Value qubit) { return b.ry(-0.456, qubit); }); + return b.measure(res).second; } -void inverseMultipleControlledRy(QCOProgramBuilder& b) { +SmallVector inverseMultipleControlledRy(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { + auto res = b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { const auto& [controlsOut, targetOut] = b.mcry(-0.456, {qubits[0], qubits[1]}, qubits[2]); return llvm::to_vector( llvm::concat(controlsOut, ValueRange{targetOut})); }); + q[0] = res[0]; + q[1] = res[1]; + q[2] = res[2]; + return measureAndReturn(b, q.qubits); } -void twoRyOppositePhase(QCOProgramBuilder& b) { + +Value twoRyOppositePhase(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.ry(0.456, q[0]); q[0] = b.ry(-0.456, q[0]); + return b.measure(q[0]).second; } -void ryPiOver2(QCOProgramBuilder& b) { +Value ryPiOver2(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.ry(std::numbers::pi / 2, q[0]); + q[0] = b.ry(std::numbers::pi / 2, q[0]); + return b.measure(q[0]).second; } -void rz(QCOProgramBuilder& b) { +Value rz(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.rz(0.789, q[0]); + q[0] = b.rz(0.789, q[0]); + return b.measure(q[0]).second; } -void singleControlledRz(QCOProgramBuilder& b) { +SmallVector singleControlledRz(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.crz(0.789, q[0], q[1]); + std::tie(q[0], q[1]) = b.crz(0.789, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void multipleControlledRz(QCOProgramBuilder& b) { +SmallVector multipleControlledRz(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.mcrz(0.789, {q[0], q[1]}, q[2]); + auto res = b.mcrz(0.789, {q[0], q[1]}, q[2]); + q[0] = res.first[0]; + q[1] = res.first[1]; + q[2] = res.second; + return measureAndReturn(b, q.qubits); } -void nestedControlledRz(QCOProgramBuilder& b) { +SmallVector nestedControlledRz(QCOProgramBuilder& b) { auto reg = b.allocQubitRegister(3); - b.ctrl({reg[0]}, {reg[1], reg[2]}, [&](ValueRange targets) { + auto res = b.ctrl({reg[0]}, {reg[1], reg[2]}, [&](ValueRange targets) { const auto& [innerControlsOut, innerTargetsOut] = - b.ctrl({targets[0]}, {targets[1]}, [&](ValueRange innerTargets) { - return SmallVector{b.rz(0.789, innerTargets[0])}; - }); - return llvm::to_vector( - llvm::concat(innerControlsOut, innerTargetsOut)); + b.ctrl(targets[0], targets[1], + [&](Value target) { return b.rz(0.789, target); }); + return SmallVector{innerControlsOut, innerTargetsOut}; }); + reg[0] = res.first[0]; + reg[1] = res.second[0]; + reg[2] = res.second[1]; + return measureAndReturn(b, reg.qubits); } -void trivialControlledRz(QCOProgramBuilder& b) { +Value trivialControlledRz(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.mcrz(0.789, {}, q[0]); + auto res = b.mcrz(0.789, {}, q[0]); + q[0] = res.second; + return b.measure(q[0]).second; } -void inverseRz(QCOProgramBuilder& b) { +Value inverseRz(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv({q[0]}, [&](ValueRange qubits) { - return SmallVector{b.rz(-0.789, qubits[0])}; - }); + auto res = b.inv(q[0], [&](Value qubit) { return b.rz(-0.789, qubit); }); + return b.measure(res).second; } -void inverseMultipleControlledRz(QCOProgramBuilder& b) { +SmallVector inverseMultipleControlledRz(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { + auto res = b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { const auto& [controlsOut, targetOut] = b.mcrz(-0.789, {qubits[0], qubits[1]}, qubits[2]); return llvm::to_vector( llvm::concat(controlsOut, ValueRange{targetOut})); }); + q[0] = res[0]; + q[1] = res[1]; + q[2] = res[2]; + return measureAndReturn(b, q.qubits); } -void twoRzOppositePhase(QCOProgramBuilder& b) { +Value twoRzOppositePhase(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.rz(0.789, q[0]); q[0] = b.rz(-0.789, q[0]); + return b.measure(q[0]).second; } -void p(QCOProgramBuilder& b) { +Value p(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.p(0.123, q[0]); + q[0] = b.p(0.123, q[0]); + return b.measure(q[0]).second; } -void singleControlledP(QCOProgramBuilder& b) { +SmallVector singleControlledP(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.cp(0.123, q[0], q[1]); + std::tie(q[0], q[1]) = b.cp(0.123, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void multipleControlledP(QCOProgramBuilder& b) { +SmallVector multipleControlledP(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.mcp(0.123, {q[0], q[1]}, q[2]); + auto res = b.mcp(0.123, {q[0], q[1]}, q[2]); + q[0] = res.first[0]; + q[1] = res.first[1]; + q[2] = res.second; + return measureAndReturn(b, q.qubits); } -void nestedControlledP(QCOProgramBuilder& b) { +SmallVector nestedControlledP(QCOProgramBuilder& b) { auto reg = b.allocQubitRegister(3); - b.ctrl({reg[0]}, {reg[1], reg[2]}, [&](ValueRange targets) { + auto res = b.ctrl({reg[0]}, {reg[1], reg[2]}, [&](ValueRange targets) { const auto& [innerControlsOut, innerTargetsOut] = - b.ctrl({targets[0]}, {targets[1]}, [&](ValueRange innerTargets) { - return SmallVector{b.p(0.123, innerTargets[0])}; - }); - return llvm::to_vector( - llvm::concat(innerControlsOut, innerTargetsOut)); + b.ctrl(targets[0], targets[1], + [&](Value target) { return b.p(0.123, target); }); + return SmallVector{innerControlsOut, innerTargetsOut}; }); + reg[0] = res.first[0]; + reg[1] = res.second[0]; + reg[2] = res.second[1]; + return measureAndReturn(b, reg.qubits); } -void trivialControlledP(QCOProgramBuilder& b) { +Value trivialControlledP(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.mcp(0.123, {}, q[0]); + auto res = b.mcp(0.123, {}, q[0]); + q[0] = res.second; + return b.measure(q[0]).second; } -void inverseP(QCOProgramBuilder& b) { +Value inverseP(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv({q[0]}, - [&](ValueRange qubits) { return SmallVector{b.p(-0.123, qubits[0])}; }); + auto res = b.inv(q[0], [&](Value qubit) { return b.p(-0.123, qubit); }); + return b.measure(res).second; } -void inverseMultipleControlledP(QCOProgramBuilder& b) { +SmallVector inverseMultipleControlledP(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { + auto res = b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { const auto& [controlsOut, targetOut] = b.mcp(-0.123, {qubits[0], qubits[1]}, qubits[2]); return llvm::to_vector( llvm::concat(controlsOut, ValueRange{targetOut})); }); + q[0] = res[0]; + q[1] = res[1]; + q[2] = res[2]; + return measureAndReturn(b, q.qubits); } -void twoPOppositePhase(QCOProgramBuilder& b) { - auto q = b.allocQubitRegister(1); - q[0] = b.p(0.123, q[0]); - q[0] = b.p(-0.123, q[0]); +Value twoPOppositePhase(QCOProgramBuilder& b) { + auto q = b.allocQubit(); + q = b.p(0.123, q); + q = b.p(-0.123, q); + return b.measure(q).second; } -void r(QCOProgramBuilder& b) { +Value r(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.r(0.123, 0.456, q[0]); + q[0] = b.r(0.123, 0.456, q[0]); + return b.measure(q[0]).second; } -void singleControlledR(QCOProgramBuilder& b) { +SmallVector singleControlledR(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.cr(0.123, 0.456, q[0], q[1]); + std::tie(q[0], q[1]) = b.cr(0.123, 0.456, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void multipleControlledR(QCOProgramBuilder& b) { +SmallVector multipleControlledR(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.mcr(0.123, 0.456, {q[0], q[1]}, q[2]); + auto res = b.mcr(0.123, 0.456, {q[0], q[1]}, q[2]); + q[0] = res.first[0]; + q[1] = res.first[1]; + q[2] = res.second; + return measureAndReturn(b, q.qubits); } -void nestedControlledR(QCOProgramBuilder& b) { +SmallVector nestedControlledR(QCOProgramBuilder& b) { auto reg = b.allocQubitRegister(3); - b.ctrl({reg[0]}, {reg[1], reg[2]}, [&](ValueRange targets) { + auto res = b.ctrl({reg[0]}, {reg[1], reg[2]}, [&](ValueRange targets) { const auto& [innerControlsOut, innerTargetsOut] = - b.ctrl({targets[0]}, {targets[1]}, [&](ValueRange innerTargets) { - return SmallVector{b.r(0.123, 0.456, innerTargets[0])}; - }); - return llvm::to_vector( - llvm::concat(innerControlsOut, innerTargetsOut)); + b.ctrl(targets[0], targets[1], + [&](Value target) { return b.r(0.123, 0.456, target); }); + return SmallVector{innerControlsOut, innerTargetsOut}; }); + reg[0] = res.first[0]; + reg[1] = res.second[0]; + reg[2] = res.second[1]; + return measureAndReturn(b, reg.qubits); } -void trivialControlledR(QCOProgramBuilder& b) { +Value trivialControlledR(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.mcr(0.123, 0.456, {}, q[0]); + auto res = b.mcr(0.123, 0.456, {}, q[0]); + q[0] = res.second; + return b.measure(q[0]).second; } -void inverseR(QCOProgramBuilder& b) { +Value inverseR(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv({q[0]}, [&](ValueRange qubits) { - return SmallVector{b.r(-0.123, 0.456, qubits[0])}; - }); + auto res = + b.inv(q[0], [&](Value qubit) { return b.r(-0.123, 0.456, qubit); }); + return b.measure(res).second; } -void inverseMultipleControlledR(QCOProgramBuilder& b) { +SmallVector inverseMultipleControlledR(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { + auto res = b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { const auto& [controlsOut, targetOut] = b.mcr(-0.123, 0.456, {qubits[0], qubits[1]}, qubits[2]); return llvm::to_vector( llvm::concat(controlsOut, ValueRange{targetOut})); }); + q[0] = res[0]; + q[1] = res[1]; + q[2] = res[2]; + return measureAndReturn(b, q.qubits); } -void canonicalizeRToRx(QCOProgramBuilder& b) { +Value canonicalizeRToRx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.r(0.123, 0., q[0]); + return b.measure(q[0]).second; } -void canonicalizeRToRy(QCOProgramBuilder& b) { +Value canonicalizeRToRy(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.r(0.456, std::numbers::pi / 2, q[0]); + return b.measure(q[0]).second; } -void twoR(QCOProgramBuilder& b) { +Value twoR(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.r(0.045, 0.456, q[0]); q[0] = b.r(0.078, 0.456, q[0]); + return b.measure(q[0]).second; } -void u2(QCOProgramBuilder& b) { +Value u2(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.u2(0.234, 0.567, q[0]); + q[0] = b.u2(0.234, 0.567, q[0]); + return b.measure(q[0]).second; } -void singleControlledU2(QCOProgramBuilder& b) { +SmallVector singleControlledU2(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.cu2(0.234, 0.567, q[0], q[1]); + std::tie(q[0], q[1]) = b.cu2(0.234, 0.567, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void multipleControlledU2(QCOProgramBuilder& b) { +SmallVector multipleControlledU2(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.mcu2(0.234, 0.567, {q[0], q[1]}, q[2]); + auto res = b.mcu2(0.234, 0.567, {q[0], q[1]}, q[2]); + q[0] = res.first[0]; + q[1] = res.first[1]; + q[2] = res.second; + return measureAndReturn(b, q.qubits); } -void nestedControlledU2(QCOProgramBuilder& b) { +SmallVector nestedControlledU2(QCOProgramBuilder& b) { auto reg = b.allocQubitRegister(3); - b.ctrl({reg[0]}, {reg[1], reg[2]}, [&](ValueRange targets) { + auto res = b.ctrl({reg[0]}, {reg[1], reg[2]}, [&](ValueRange targets) { const auto& [innerControlsOut, innerTargetsOut] = - b.ctrl({targets[0]}, {targets[1]}, [&](ValueRange innerTargets) { - return SmallVector{b.u2(0.234, 0.567, innerTargets[0])}; - }); - return llvm::to_vector( - llvm::concat(innerControlsOut, innerTargetsOut)); + b.ctrl(targets[0], targets[1], + [&](Value target) { return b.u2(0.234, 0.567, target); }); + return SmallVector{innerControlsOut, innerTargetsOut}; }); + reg[0] = res.first[0]; + reg[1] = res.second[0]; + reg[2] = res.second[1]; + return measureAndReturn(b, reg.qubits); } -void trivialControlledU2(QCOProgramBuilder& b) { +Value trivialControlledU2(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.mcu2(0.234, 0.567, {}, q[0]); + auto res = b.mcu2(0.234, 0.567, {}, q[0]); + q[0] = res.second; + return b.measure(q[0]).second; } -void inverseU2(QCOProgramBuilder& b) { +Value inverseU2(QCOProgramBuilder& b) { constexpr double pi = std::numbers::pi; auto q = b.allocQubitRegister(1); - b.inv({q[0]}, [&](ValueRange qubits) { - return SmallVector{b.u2(-0.567 + pi, -0.234 - pi, qubits[0])}; - }); + auto res = b.inv( + q[0], [&](Value qubit) { return b.u2(-0.567 + pi, -0.234 - pi, qubit); }); + return b.measure(res).second; } -void inverseMultipleControlledU2(QCOProgramBuilder& b) { +SmallVector inverseMultipleControlledU2(QCOProgramBuilder& b) { constexpr double pi = std::numbers::pi; auto q = b.allocQubitRegister(3); - b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { + auto res = b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { const auto& [controlsOut, targetOut] = b.mcu2(-0.567 + pi, -0.234 - pi, {qubits[0], qubits[1]}, qubits[2]); return llvm::to_vector( llvm::concat(controlsOut, ValueRange{targetOut})); }); + q[0] = res[0]; + q[1] = res[1]; + q[2] = res[2]; + return measureAndReturn(b, q.qubits); } -void canonicalizeU2ToH(QCOProgramBuilder& b) { + +Value canonicalizeU2ToH(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.u2(0., std::numbers::pi, q[0]); + q[0] = b.u2(0., std::numbers::pi, q[0]); + return b.measure(q[0]).second; } -void canonicalizeU2ToRx(QCOProgramBuilder& b) { +Value canonicalizeU2ToRx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.u2(-std::numbers::pi / 2, std::numbers::pi / 2, q[0]); + q[0] = b.u2(-std::numbers::pi / 2, std::numbers::pi / 2, q[0]); + return b.measure(q[0]).second; } -void canonicalizeU2ToRy(QCOProgramBuilder& b) { + +Value canonicalizeU2ToRy(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.u2(0., 0., q[0]); + q[0] = b.u2(0., 0., q[0]); + return b.measure(q[0]).second; } -void u(QCOProgramBuilder& b) { +Value u(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.u(0.1, 0.2, 0.3, q[0]); + q[0] = b.u(0.1, 0.2, 0.3, q[0]); + return b.measure(q[0]).second; } -void singleControlledU(QCOProgramBuilder& b) { +SmallVector singleControlledU(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.cu(0.1, 0.2, 0.3, q[0], q[1]); + std::tie(q[0], q[1]) = b.cu(0.1, 0.2, 0.3, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void multipleControlledU(QCOProgramBuilder& b) { +SmallVector multipleControlledU(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.mcu(0.1, 0.2, 0.3, {q[0], q[1]}, q[2]); + auto res = b.mcu(0.1, 0.2, 0.3, {q[0], q[1]}, q[2]); + q[0] = res.first[0]; + q[1] = res.first[1]; + q[2] = res.second; + return measureAndReturn(b, q.qubits); } -void nestedControlledU(QCOProgramBuilder& b) { +SmallVector nestedControlledU(QCOProgramBuilder& b) { auto reg = b.allocQubitRegister(3); - b.ctrl({reg[0]}, {reg[1], reg[2]}, [&](ValueRange targets) { + auto res = b.ctrl({reg[0]}, {reg[1], reg[2]}, [&](ValueRange targets) { const auto& [innerControlsOut, innerTargetsOut] = - b.ctrl({targets[0]}, {targets[1]}, [&](ValueRange innerTargets) { - return SmallVector{b.u(0.1, 0.2, 0.3, innerTargets[0])}; - }); - return llvm::to_vector( - llvm::concat(innerControlsOut, innerTargetsOut)); + b.ctrl(targets[0], targets[1], + [&](Value target) { return b.u(0.1, 0.2, 0.3, target); }); + return SmallVector{innerControlsOut, innerTargetsOut}; }); + reg[0] = res.first[0]; + reg[1] = res.second[0]; + reg[2] = res.second[1]; + return measureAndReturn(b, reg.qubits); } -void trivialControlledU(QCOProgramBuilder& b) { +Value trivialControlledU(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.mcu(0.1, 0.2, 0.3, {}, q[0]); + auto res = b.mcu(0.1, 0.2, 0.3, {}, q[0]); + q[0] = res.second; + return b.measure(q[0]).second; } -void inverseU(QCOProgramBuilder& b) { +Value inverseU(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv({q[0]}, [&](ValueRange qubits) { - return SmallVector{b.u(-0.1, -0.3, -0.2, qubits[0])}; - }); + auto res = + b.inv(q[0], [&](Value qubit) { return b.u(-0.1, -0.3, -0.2, qubit); }); + return b.measure(res).second; } -void inverseMultipleControlledU(QCOProgramBuilder& b) { +SmallVector inverseMultipleControlledU(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { + auto res = b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { const auto& [controlsOut, targetOut] = b.mcu(-0.1, -0.3, -0.2, {qubits[0], qubits[1]}, qubits[2]); return llvm::to_vector( llvm::concat(controlsOut, ValueRange{targetOut})); }); + q[0] = res[0]; + q[1] = res[1]; + q[2] = res[2]; + return measureAndReturn(b, q.qubits); } -void canonicalizeUToP(QCOProgramBuilder& b) { +Value canonicalizeUToP(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.u(0., 0., 0.123, q[0]); + q[0] = b.u(0., 0., 0.123, q[0]); + return b.measure(q[0]).second; } -void canonicalizeUToRx(QCOProgramBuilder& b) { +Value canonicalizeUToRx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.u(0.123, -std::numbers::pi / 2, std::numbers::pi / 2, q[0]); + q[0] = b.u(0.123, -std::numbers::pi / 2, std::numbers::pi / 2, q[0]); + return b.measure(q[0]).second; } -void canonicalizeUToRy(QCOProgramBuilder& b) { +Value canonicalizeUToRy(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.u(0.456, 0., 0., q[0]); + q[0] = b.u(0.456, 0., 0., q[0]); + return b.measure(q[0]).second; } -void canonicalizeUToU2(QCOProgramBuilder& b) { +Value canonicalizeUToU2(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.u(std::numbers::pi / 2, 0.234, 0.567, q[0]); + q[0] = b.u(std::numbers::pi / 2, 0.234, 0.567, q[0]); + return b.measure(q[0]).second; } -void swap(QCOProgramBuilder& b) { +SmallVector swap(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.swap(q[0], q[1]); + std::tie(q[0], q[1]) = b.swap(q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void singleControlledSwap(QCOProgramBuilder& b) { +SmallVector singleControlledSwap(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.cswap(q[0], q[1], q[2]); + auto res = b.cswap(q[0], q[1], q[2]); + q[0] = res.first; + q[1] = res.second.first; + q[2] = res.second.second; + return measureAndReturn(b, q.qubits); } -void multipleControlledSwap(QCOProgramBuilder& b) { +SmallVector multipleControlledSwap(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(4); - b.mcswap({q[0], q[1]}, q[2], q[3]); + auto [c, t] = b.mcswap({q[0], q[1]}, q[2], q[3]); + q[0] = c[0]; + q[1] = c[1]; + q[2] = t.first; + q[3] = t.second; + return measureAndReturn(b, q.qubits); } -void nestedControlledSwap(QCOProgramBuilder& b) { +SmallVector nestedControlledSwap(QCOProgramBuilder& b) { auto reg = b.allocQubitRegister(4); - b.ctrl({reg[0]}, {reg[1], reg[2], reg[3]}, [&](ValueRange targets) { - const auto& [innerControlsOut, innerTargetsOut] = b.ctrl( - {targets[0]}, {targets[1], targets[2]}, [&](ValueRange innerTargets) { - auto res = b.swap(innerTargets[0], innerTargets[1]); - return SmallVector{res.first, res.second}; - }); - return llvm::to_vector( - llvm::concat(innerControlsOut, innerTargetsOut)); - }); + auto [c, t] = + b.ctrl({reg[0]}, {reg[1], reg[2], reg[3]}, [&](ValueRange targets) { + const auto& [innerControlsOut, innerTargetsOut] = + b.ctrl({targets[0]}, {targets[1], targets[2]}, + [&](ValueRange innerTargets) { + auto [fst, snd] = b.swap(innerTargets[0], innerTargets[1]); + return SmallVector{fst, snd}; + }); + return llvm::to_vector( + llvm::concat(innerControlsOut, innerTargetsOut)); + }); + reg[0] = c[0]; + reg[1] = t[0]; + reg[2] = t[1]; + reg[3] = t[2]; + return measureAndReturn(b, reg.qubits); } -void trivialControlledSwap(QCOProgramBuilder& b) { +SmallVector trivialControlledSwap(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.mcswap({}, q[0], q[1]); + auto [c, t] = b.mcswap({}, q[0], q[1]); + q[0] = t.first; + q[1] = t.second; + return measureAndReturn(b, q.qubits); } -void inverseSwap(QCOProgramBuilder& b) { +SmallVector inverseSwap(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.inv({q[0], q[1]}, [&](ValueRange qubits) { - auto res = b.swap(qubits[0], qubits[1]); - return SmallVector{res.first, res.second}; + auto res = b.inv({q[0], q[1]}, [&](ValueRange qubits) { + auto [fst, snd] = b.swap(qubits[0], qubits[1]); + return SmallVector{fst, snd}; }); + q[0] = res[0]; + q[1] = res[1]; + return measureAndReturn(b, q.qubits); } -void inverseMultipleControlledSwap(QCOProgramBuilder& b) { +SmallVector inverseMultipleControlledSwap(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(4); - b.inv({q[0], q[1], q[2], q[3]}, [&](ValueRange qubits) { + auto res = b.inv({q[0], q[1], q[2], q[3]}, [&](ValueRange qubits) { const auto& [controlsOut, targetsOut] = b.mcswap({qubits[0], qubits[1]}, qubits[2], qubits[3]); SmallVector targets{targetsOut.first, targetsOut.second}; return llvm::to_vector(llvm::concat(controlsOut, targets)); }); + q[0] = res[0]; + q[1] = res[1]; + q[2] = res[2]; + q[3] = res[3]; + return measureAndReturn(b, q.qubits); } -void twoSwap(QCOProgramBuilder& b) { +SmallVector twoSwap(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); std::tie(q[0], q[1]) = b.swap(q[0], q[1]); std::tie(q[0], q[1]) = b.swap(q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void twoSwapSwappedTargets(QCOProgramBuilder& b) { +SmallVector twoSwapSwappedTargets(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); std::tie(q[0], q[1]) = b.swap(q[0], q[1]); std::tie(q[1], q[0]) = b.swap(q[1], q[0]); + return measureAndReturn(b, q.qubits); } -void iswap(QCOProgramBuilder& b) { +SmallVector iswap(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.iswap(q[0], q[1]); + std::tie(q[0], q[1]) = b.iswap(q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void singleControlledIswap(QCOProgramBuilder& b) { +SmallVector singleControlledIswap(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.ciswap(q[0], q[1], q[2]); + auto [c, t] = b.ciswap(q[0], q[1], q[2]); + q[0] = c; + q[1] = t.first; + q[2] = t.second; + return measureAndReturn(b, q.qubits); } -void multipleControlledIswap(QCOProgramBuilder& b) { +SmallVector multipleControlledIswap(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(4); - b.mciswap({q[0], q[1]}, q[2], q[3]); + auto [c, t] = b.mciswap({q[0], q[1]}, q[2], q[3]); + q[0] = c[0]; + q[1] = c[1]; + q[2] = t.first; + q[3] = t.second; + return measureAndReturn(b, q.qubits); } -void nestedControlledIswap(QCOProgramBuilder& b) { +SmallVector nestedControlledIswap(QCOProgramBuilder& b) { auto reg = b.allocQubitRegister(4); - b.ctrl({reg[0]}, {reg[1], reg[2], reg[3]}, [&](ValueRange targets) { - const auto& [innerControlsOut, innerTargetsOut] = b.ctrl( - {targets[0]}, {targets[1], targets[2]}, [&](ValueRange innerTargets) { - auto res = b.iswap(innerTargets[0], innerTargets[1]); - return SmallVector{res.first, res.second}; - }); - return llvm::to_vector( - llvm::concat(innerControlsOut, innerTargetsOut)); - }); + auto [c, t] = + b.ctrl({reg[0]}, {reg[1], reg[2], reg[3]}, [&](ValueRange targets) { + const auto& [innerControlsOut, innerTargetsOut] = + b.ctrl({targets[0]}, {targets[1], targets[2]}, + [&](ValueRange innerTargets) { + auto [fst, snd] = + b.iswap(innerTargets[0], innerTargets[1]); + return SmallVector{fst, snd}; + }); + return llvm::to_vector( + llvm::concat(innerControlsOut, innerTargetsOut)); + }); + reg[0] = c[0]; + reg[1] = t[0]; + reg[2] = t[1]; + reg[3] = t[2]; + return measureAndReturn(b, reg.qubits); } -void trivialControlledIswap(QCOProgramBuilder& b) { +SmallVector trivialControlledIswap(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.mciswap({}, q[0], q[1]); + auto [c, t] = b.mciswap({}, q[0], q[1]); + q[0] = t.first; + q[1] = t.second; + return measureAndReturn(b, q.qubits); } -void inverseIswap(QCOProgramBuilder& b) { +SmallVector inverseIswap(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.inv({q[0], q[1]}, [&](ValueRange qubits) { - auto res = b.iswap(qubits[0], qubits[1]); - return SmallVector{res.first, res.second}; + auto res = b.inv({q[0], q[1]}, [&](ValueRange qubits) { + auto [fst, snd] = b.iswap(qubits[0], qubits[1]); + return SmallVector{fst, snd}; }); + q[0] = res[0]; + q[1] = res[1]; + return measureAndReturn(b, q.qubits); } -void inverseMultipleControlledIswap(QCOProgramBuilder& b) { +SmallVector inverseMultipleControlledIswap(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(4); - b.inv({q[0], q[1], q[2], q[3]}, [&](ValueRange qubits) { + auto res = b.inv({q[0], q[1], q[2], q[3]}, [&](ValueRange qubits) { const auto& [controlsOut, targetsOut] = b.mciswap({qubits[0], qubits[1]}, qubits[2], qubits[3]); SmallVector targets{targetsOut.first, targetsOut.second}; return llvm::to_vector(llvm::concat(controlsOut, targets)); }); + q[0] = res[0]; + q[1] = res[1]; + q[2] = res[2]; + q[3] = res[3]; + return measureAndReturn(b, q.qubits); } -void dcx(QCOProgramBuilder& b) { +SmallVector dcx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.dcx(q[0], q[1]); + std::tie(q[0], q[1]) = b.dcx(q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void singleControlledDcx(QCOProgramBuilder& b) { +SmallVector singleControlledDcx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.cdcx(q[0], q[1], q[2]); + auto [c, t] = b.cdcx(q[0], q[1], q[2]); + q[0] = c; + q[1] = t.first; + q[2] = t.second; + return measureAndReturn(b, q.qubits); } -void multipleControlledDcx(QCOProgramBuilder& b) { +SmallVector multipleControlledDcx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(4); - b.mcdcx({q[0], q[1]}, q[2], q[3]); + auto [c, t] = b.mcdcx({q[0], q[1]}, q[2], q[3]); + q[0] = c[0]; + q[1] = c[1]; + q[2] = t.first; + q[3] = t.second; + return measureAndReturn(b, q.qubits); } -void nestedControlledDcx(QCOProgramBuilder& b) { +SmallVector nestedControlledDcx(QCOProgramBuilder& b) { auto reg = b.allocQubitRegister(4); - b.ctrl({reg[0]}, {reg[1], reg[2], reg[3]}, [&](ValueRange targets) { - const auto& [innerControlsOut, innerTargetsOut] = b.ctrl( - {targets[0]}, {targets[1], targets[2]}, [&](ValueRange innerTargets) { - auto res = b.dcx(innerTargets[0], innerTargets[1]); - return SmallVector{res.first, res.second}; - }); - return llvm::to_vector( - llvm::concat(innerControlsOut, innerTargetsOut)); - }); + auto [c, t] = + b.ctrl({reg[0]}, {reg[1], reg[2], reg[3]}, [&](ValueRange targets) { + const auto& [innerControlsOut, innerTargetsOut] = + b.ctrl({targets[0]}, {targets[1], targets[2]}, + [&](ValueRange innerTargets) { + auto [fst, snd] = b.dcx(innerTargets[0], innerTargets[1]); + return SmallVector{fst, snd}; + }); + return llvm::to_vector( + llvm::concat(innerControlsOut, innerTargetsOut)); + }); + reg[0] = c[0]; + reg[1] = t[0]; + reg[2] = t[1]; + reg[3] = t[2]; + return measureAndReturn(b, reg.qubits); } -void trivialControlledDcx(QCOProgramBuilder& b) { +SmallVector trivialControlledDcx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.mcdcx({}, q[0], q[1]); + auto [c, t] = b.mcdcx({}, q[0], q[1]); + q[0] = t.first; + q[1] = t.second; + return measureAndReturn(b, q.qubits); } -void inverseDcx(QCOProgramBuilder& b) { +SmallVector inverseDcx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.inv({q[1], q[0]}, [&](ValueRange qubits) { - auto res = b.dcx(qubits[0], qubits[1]); - return SmallVector{res.first, res.second}; + auto res = b.inv({q[1], q[0]}, [&](ValueRange qubits) { + auto [fst, snd] = b.dcx(qubits[0], qubits[1]); + return SmallVector{fst, snd}; }); + q[1] = res[0]; + q[0] = res[1]; + return measureAndReturn(b, q.qubits); } -void inverseMultipleControlledDcx(QCOProgramBuilder& b) { +SmallVector inverseMultipleControlledDcx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(4); - b.inv({q[0], q[1], q[3], q[2]}, [&](ValueRange qubits) { + auto res = b.inv({q[0], q[1], q[3], q[2]}, [&](ValueRange qubits) { const auto& [controlsOut, targetsOut] = b.mcdcx({qubits[0], qubits[1]}, qubits[2], qubits[3]); SmallVector targets{targetsOut.first, targetsOut.second}; return llvm::to_vector(llvm::concat(controlsOut, targets)); }); + q[0] = res[0]; + q[1] = res[1]; + q[3] = res[2]; + q[2] = res[3]; + return measureAndReturn(b, q.qubits); } -void twoDcx(QCOProgramBuilder& b) { +SmallVector twoDcx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); std::tie(q[0], q[1]) = b.dcx(q[0], q[1]); std::tie(q[0], q[1]) = b.dcx(q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void twoDcxSwappedTargets(QCOProgramBuilder& b) { +SmallVector twoDcxSwappedTargets(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); std::tie(q[0], q[1]) = b.dcx(q[0], q[1]); std::tie(q[1], q[0]) = b.dcx(q[1], q[0]); + return measureAndReturn(b, q.qubits); } -void ecr(QCOProgramBuilder& b) { +SmallVector ecr(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.ecr(q[0], q[1]); + std::tie(q[0], q[1]) = b.ecr(q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void singleControlledEcr(QCOProgramBuilder& b) { +SmallVector singleControlledEcr(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.cecr(q[0], q[1], q[2]); + auto [c, t] = b.cecr(q[0], q[1], q[2]); + q[0] = c; + q[1] = t.first; + q[2] = t.second; + return measureAndReturn(b, q.qubits); } -void multipleControlledEcr(QCOProgramBuilder& b) { +SmallVector multipleControlledEcr(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(4); - b.mcecr({q[0], q[1]}, q[2], q[3]); + auto [fst, snd] = b.mcecr({q[0], q[1]}, q[2], q[3]); + q[0] = fst[0]; + q[1] = fst[1]; + q[2] = snd.first; + q[3] = snd.second; + return measureAndReturn(b, q.qubits); } -void nestedControlledEcr(QCOProgramBuilder& b) { +SmallVector nestedControlledEcr(QCOProgramBuilder& b) { auto reg = b.allocQubitRegister(4); - b.ctrl({reg[0]}, {reg[1], reg[2], reg[3]}, [&](ValueRange targets) { - const auto& [innerControlsOut, innerTargetsOut] = b.ctrl( - {targets[0]}, {targets[1], targets[2]}, [&](ValueRange innerTargets) { - auto res = b.ecr(innerTargets[0], innerTargets[1]); - return SmallVector{res.first, res.second}; - }); - return llvm::to_vector( - llvm::concat(innerControlsOut, innerTargetsOut)); - }); + auto [c, t] = + b.ctrl({reg[0]}, {reg[1], reg[2], reg[3]}, [&](ValueRange targets) { + const auto& [innerControlsOut, innerTargetsOut] = + b.ctrl({targets[0]}, {targets[1], targets[2]}, + [&](ValueRange innerTargets) { + auto [fst, snd] = b.ecr(innerTargets[0], innerTargets[1]); + return SmallVector{fst, snd}; + }); + return llvm::to_vector( + llvm::concat(innerControlsOut, innerTargetsOut)); + }); + reg[0] = c[0]; + reg[1] = t[0]; + reg[2] = t[1]; + reg[3] = t[2]; + return measureAndReturn(b, reg.qubits); } -void trivialControlledEcr(QCOProgramBuilder& b) { +SmallVector trivialControlledEcr(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.mcecr({}, q[0], q[1]); + auto [c, t] = b.mcecr({}, q[0], q[1]); + q[0] = t.first; + q[1] = t.second; + return measureAndReturn(b, q.qubits); } -void inverseEcr(QCOProgramBuilder& b) { +SmallVector inverseEcr(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.inv({q[0], q[1]}, [&](ValueRange qubits) { - auto res = b.ecr(qubits[0], qubits[1]); - return SmallVector{res.first, res.second}; + auto res = b.inv({q[0], q[1]}, [&](ValueRange qubits) { + auto [fst, snd] = b.ecr(qubits[0], qubits[1]); + return SmallVector{fst, snd}; }); + q[0] = res[0]; + q[1] = res[1]; + return measureAndReturn(b, q.qubits); } -void inverseMultipleControlledEcr(QCOProgramBuilder& b) { +SmallVector inverseMultipleControlledEcr(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(4); - b.inv({q[0], q[1], q[2], q[3]}, [&](ValueRange qubits) { + auto res = b.inv({q[0], q[1], q[2], q[3]}, [&](ValueRange qubits) { const auto& [controlsOut, targetsOut] = b.mcecr({qubits[0], qubits[1]}, qubits[2], qubits[3]); SmallVector targets{targetsOut.first, targetsOut.second}; return llvm::to_vector(llvm::concat(controlsOut, targets)); }); + q[0] = res[0]; + q[1] = res[1]; + q[2] = res[2]; + q[3] = res[3]; + return measureAndReturn(b, q.qubits); } -void twoEcr(QCOProgramBuilder& b) { +SmallVector twoEcr(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); std::tie(q[0], q[1]) = b.ecr(q[0], q[1]); std::tie(q[0], q[1]) = b.ecr(q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void rxx(QCOProgramBuilder& b) { +SmallVector rxx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.rxx(0.123, q[0], q[1]); + std::tie(q[0], q[1]) = b.rxx(0.123, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void singleControlledRxx(QCOProgramBuilder& b) { +SmallVector singleControlledRxx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.crxx(0.123, q[0], q[1], q[2]); + auto [c, t] = b.crxx(0.123, q[0], q[1], q[2]); + q[0] = c; + q[1] = t.first; + q[2] = t.second; + return measureAndReturn(b, q.qubits); } -void multipleControlledRxx(QCOProgramBuilder& b) { +SmallVector multipleControlledRxx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(4); - b.mcrxx(0.123, {q[0], q[1]}, q[2], q[3]); + auto [c, t] = b.mcrxx(0.123, {q[0], q[1]}, q[2], q[3]); + q[0] = c[0]; + q[1] = c[1]; + q[2] = t.first; + q[3] = t.second; + return measureAndReturn(b, q.qubits); } -void nestedControlledRxx(QCOProgramBuilder& b) { +SmallVector nestedControlledRxx(QCOProgramBuilder& b) { auto reg = b.allocQubitRegister(4); - b.ctrl({reg[0]}, {reg[1], reg[2], reg[3]}, [&](ValueRange targets) { - const auto& [innerControlsOut, innerTargetsOut] = b.ctrl( - {targets[0]}, {targets[1], targets[2]}, [&](ValueRange innerTargets) { - auto res = b.rxx(0.123, innerTargets[0], innerTargets[1]); - return SmallVector{res.first, res.second}; - }); - return llvm::to_vector( - llvm::concat(innerControlsOut, innerTargetsOut)); - }); + auto [c, t] = + b.ctrl({reg[0]}, {reg[1], reg[2], reg[3]}, [&](ValueRange targets) { + const auto& [innerControlsOut, innerTargetsOut] = + b.ctrl({targets[0]}, {targets[1], targets[2]}, + [&](ValueRange innerTargets) { + auto [fst, snd] = + b.rxx(0.123, innerTargets[0], innerTargets[1]); + return SmallVector{fst, snd}; + }); + return llvm::to_vector( + llvm::concat(innerControlsOut, innerTargetsOut)); + }); + reg[0] = c[0]; + reg[1] = t[0]; + reg[2] = t[1]; + reg[3] = t[2]; + return measureAndReturn(b, reg.qubits); } -void trivialControlledRxx(QCOProgramBuilder& b) { +SmallVector trivialControlledRxx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.mcrxx(0.123, {}, q[0], q[1]); + auto [c, t] = b.mcrxx(0.123, {}, q[0], q[1]); + q[0] = t.first; + q[1] = t.second; + return measureAndReturn(b, q.qubits); } -void inverseRxx(QCOProgramBuilder& b) { +SmallVector inverseRxx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.inv({q[0], q[1]}, [&](ValueRange qubits) { - auto res = b.rxx(-0.123, qubits[0], qubits[1]); - return SmallVector{res.first, res.second}; + auto res = b.inv({q[0], q[1]}, [&](ValueRange qubits) { + auto [fst, snd] = b.rxx(-0.123, qubits[0], qubits[1]); + return SmallVector{fst, snd}; }); + q[0] = res[0]; + q[1] = res[1]; + return measureAndReturn(b, q.qubits); } -void inverseMultipleControlledRxx(QCOProgramBuilder& b) { +SmallVector inverseMultipleControlledRxx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(4); - b.inv({q[0], q[1], q[2], q[3]}, [&](ValueRange qubits) { + auto res = b.inv({q[0], q[1], q[2], q[3]}, [&](ValueRange qubits) { const auto& [controlsOut, targetsOut] = b.mcrxx(-0.123, {qubits[0], qubits[1]}, qubits[2], qubits[3]); SmallVector targets{targetsOut.first, targetsOut.second}; return llvm::to_vector(llvm::concat(controlsOut, targets)); }); + q[0] = res[0]; + q[1] = res[1]; + q[2] = res[2]; + q[3] = res[3]; + return measureAndReturn(b, q.qubits); } -void tripleControlledRxx(QCOProgramBuilder& b) { +SmallVector tripleControlledRxx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(5); - b.mcrxx(0.123, {q[0], q[1], q[2]}, q[3], q[4]); + auto [c, t] = b.mcrxx(0.123, {q[0], q[1], q[2]}, q[3], q[4]); + q[0] = c[0]; + q[1] = c[1]; + q[2] = c[2]; + q[3] = t.first; + q[4] = t.second; + return measureAndReturn(b, q.qubits); } -void fourControlledRxx(QCOProgramBuilder& b) { +SmallVector fourControlledRxx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(6); - b.mcrxx(0.123, {q[0], q[1], q[2], q[3]}, q[4], q[5]); + auto [c, t] = b.mcrxx(0.123, {q[0], q[1], q[2], q[3]}, q[4], q[5]); + q[0] = c[0]; + q[1] = c[1]; + q[2] = c[2]; + q[3] = c[3]; + q[4] = t.first; + q[5] = t.second; + return measureAndReturn(b, q.qubits); } -void twoRxx(QCOProgramBuilder& b) { +SmallVector twoRxx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); // 0.045 + 0.078 = 0.123 std::tie(q[0], q[1]) = b.rxx(0.045, q[0], q[1]); std::tie(q[0], q[1]) = b.rxx(0.078, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void twoRxxSwappedTargets(QCOProgramBuilder& b) { +SmallVector twoRxxSwappedTargets(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); // 0.045 + 0.078 = 0.123 std::tie(q[0], q[1]) = b.rxx(0.045, q[0], q[1]); std::tie(q[1], q[0]) = b.rxx(0.078, q[1], q[0]); + return measureAndReturn(b, q.qubits); } -void twoRxxOppositePhase(QCOProgramBuilder& b) { +SmallVector twoRxxOppositePhase(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); std::tie(q[0], q[1]) = b.rxx(0.123, q[0], q[1]); std::tie(q[0], q[1]) = b.rxx(-0.123, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void twoRxxOppositePhaseSwappedTargets(QCOProgramBuilder& b) { +SmallVector twoRxxOppositePhaseSwappedTargets(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); std::tie(q[0], q[1]) = b.rxx(0.123, q[0], q[1]); std::tie(q[1], q[0]) = b.rxx(-0.123, q[1], q[0]); + return measureAndReturn(b, q.qubits); } -void ryy(QCOProgramBuilder& b) { +SmallVector ryy(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.ryy(0.123, q[0], q[1]); + std::tie(q[0], q[1]) = b.ryy(0.123, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void singleControlledRyy(QCOProgramBuilder& b) { +SmallVector singleControlledRyy(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.cryy(0.123, q[0], q[1], q[2]); + auto [c, t] = b.cryy(0.123, q[0], q[1], q[2]); + q[0] = c; + q[1] = t.first; + q[2] = t.second; + return measureAndReturn(b, q.qubits); } -void multipleControlledRyy(QCOProgramBuilder& b) { +SmallVector multipleControlledRyy(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(4); - b.mcryy(0.123, {q[0], q[1]}, q[2], q[3]); + auto [c, t] = b.mcryy(0.123, {q[0], q[1]}, q[2], q[3]); + q[0] = c[0]; + q[1] = c[1]; + q[2] = t.first; + q[3] = t.second; + return measureAndReturn(b, q.qubits); } -void nestedControlledRyy(QCOProgramBuilder& b) { +SmallVector nestedControlledRyy(QCOProgramBuilder& b) { auto reg = b.allocQubitRegister(4); - b.ctrl({reg[0]}, {reg[1], reg[2], reg[3]}, [&](ValueRange targets) { - const auto& [innerControlsOut, innerTargetsOut] = b.ctrl( - {targets[0]}, {targets[1], targets[2]}, [&](ValueRange innerTargets) { - auto res = b.ryy(0.123, innerTargets[0], innerTargets[1]); - return SmallVector{res.first, res.second}; - }); - return llvm::to_vector( - llvm::concat(innerControlsOut, innerTargetsOut)); - }); + auto [c, t] = + b.ctrl({reg[0]}, {reg[1], reg[2], reg[3]}, [&](ValueRange targets) { + const auto& [innerControlsOut, innerTargetsOut] = + b.ctrl({targets[0]}, {targets[1], targets[2]}, + [&](ValueRange innerTargets) { + auto [fst, snd] = + b.ryy(0.123, innerTargets[0], innerTargets[1]); + return SmallVector{fst, snd}; + }); + return llvm::to_vector( + llvm::concat(innerControlsOut, innerTargetsOut)); + }); + reg[0] = c[0]; + reg[1] = t[0]; + reg[2] = t[1]; + reg[3] = t[2]; + return measureAndReturn(b, reg.qubits); } -void trivialControlledRyy(QCOProgramBuilder& b) { +SmallVector trivialControlledRyy(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.mcryy(0.123, {}, q[0], q[1]); + auto [c, t] = b.mcryy(0.123, {}, q[0], q[1]); + q[0] = t.first; + q[1] = t.second; + return measureAndReturn(b, q.qubits); } -void inverseRyy(QCOProgramBuilder& b) { +SmallVector inverseRyy(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.inv({q[0], q[1]}, [&](ValueRange qubits) { - auto res = b.ryy(-0.123, qubits[0], qubits[1]); - return SmallVector{res.first, res.second}; + auto res = b.inv({q[0], q[1]}, [&](ValueRange qubits) { + auto [fst, snd] = b.ryy(-0.123, qubits[0], qubits[1]); + return SmallVector{fst, snd}; }); + q[0] = res[0]; + q[1] = res[1]; + return measureAndReturn(b, q.qubits); } -void inverseMultipleControlledRyy(QCOProgramBuilder& b) { +SmallVector inverseMultipleControlledRyy(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(4); - b.inv({q[0], q[1], q[2], q[3]}, [&](ValueRange qubits) { + auto res = b.inv({q[0], q[1], q[2], q[3]}, [&](ValueRange qubits) { const auto& [controlsOut, targetsOut] = b.mcryy(-0.123, {qubits[0], qubits[1]}, qubits[2], qubits[3]); SmallVector targets{targetsOut.first, targetsOut.second}; return llvm::to_vector(llvm::concat(controlsOut, targets)); }); + q[0] = res[0]; + q[1] = res[1]; + q[2] = res[2]; + q[3] = res[3]; + return measureAndReturn(b, q.qubits); } -void twoRyy(QCOProgramBuilder& b) { +SmallVector twoRyy(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); // 0.045 + 0.078 = 0.123 std::tie(q[0], q[1]) = b.ryy(0.045, q[0], q[1]); std::tie(q[0], q[1]) = b.ryy(0.078, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void twoRyyOppositePhaseSwappedTargets(QCOProgramBuilder& b) { +SmallVector twoRyyOppositePhaseSwappedTargets(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); std::tie(q[0], q[1]) = b.ryy(0.123, q[0], q[1]); std::tie(q[1], q[0]) = b.ryy(-0.123, q[1], q[0]); + return measureAndReturn(b, q.qubits); } -void twoRyyOppositePhase(QCOProgramBuilder& b) { +SmallVector twoRyyOppositePhase(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); std::tie(q[0], q[1]) = b.ryy(0.123, q[0], q[1]); std::tie(q[0], q[1]) = b.ryy(-0.123, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void twoRyySwappedTargets(QCOProgramBuilder& b) { +SmallVector twoRyySwappedTargets(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); // 0.045 + 0.078 = 0.123 std::tie(q[0], q[1]) = b.ryy(0.045, q[0], q[1]); std::tie(q[1], q[0]) = b.ryy(0.078, q[1], q[0]); + return measureAndReturn(b, q.qubits); } -void rzx(QCOProgramBuilder& b) { +SmallVector rzx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.rzx(0.123, q[0], q[1]); + std::tie(q[0], q[1]) = b.rzx(0.123, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void singleControlledRzx(QCOProgramBuilder& b) { +SmallVector singleControlledRzx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.crzx(0.123, q[0], q[1], q[2]); + auto [c, t] = b.crzx(0.123, q[0], q[1], q[2]); + q[0] = c; + q[1] = t.first; + q[2] = t.second; + return measureAndReturn(b, q.qubits); } -void multipleControlledRzx(QCOProgramBuilder& b) { +SmallVector multipleControlledRzx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(4); - b.mcrzx(0.123, {q[0], q[1]}, q[2], q[3]); + auto [c, t] = b.mcrzx(0.123, {q[0], q[1]}, q[2], q[3]); + q[0] = c[0]; + q[1] = c[1]; + q[2] = t.first; + q[3] = t.second; + return measureAndReturn(b, q.qubits); } -void nestedControlledRzx(QCOProgramBuilder& b) { +SmallVector nestedControlledRzx(QCOProgramBuilder& b) { auto reg = b.allocQubitRegister(4); - b.ctrl({reg[0]}, {reg[1], reg[2], reg[3]}, [&](ValueRange targets) { - const auto& [innerControlsOut, innerTargetsOut] = b.ctrl( - {targets[0]}, {targets[1], targets[2]}, [&](ValueRange innerTargets) { - auto res = b.rzx(0.123, innerTargets[0], innerTargets[1]); - return SmallVector{res.first, res.second}; - }); - return llvm::to_vector( - llvm::concat(innerControlsOut, innerTargetsOut)); - }); + auto [c, t] = + b.ctrl({reg[0]}, {reg[1], reg[2], reg[3]}, [&](ValueRange targets) { + const auto& [innerControlsOut, innerTargetsOut] = + b.ctrl({targets[0]}, {targets[1], targets[2]}, + [&](ValueRange innerTargets) { + auto [fst, snd] = + b.rzx(0.123, innerTargets[0], innerTargets[1]); + return SmallVector{fst, snd}; + }); + return llvm::to_vector( + llvm::concat(innerControlsOut, innerTargetsOut)); + }); + reg[0] = c[0]; + reg[1] = t[0]; + reg[2] = t[1]; + reg[3] = t[2]; + return measureAndReturn(b, reg.qubits); } -void trivialControlledRzx(QCOProgramBuilder& b) { +SmallVector trivialControlledRzx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.mcrzx(0.123, {}, q[0], q[1]); + auto [c, t] = b.mcrzx(0.123, {}, q[0], q[1]); + q[0] = t.first; + q[1] = t.second; + return measureAndReturn(b, q.qubits); } -void inverseRzx(QCOProgramBuilder& b) { +SmallVector inverseRzx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.inv({q[0], q[1]}, [&](ValueRange qubits) { - auto res = b.rzx(-0.123, qubits[0], qubits[1]); - return SmallVector{res.first, res.second}; + auto res = b.inv({q[0], q[1]}, [&](ValueRange qubits) { + auto [fst, snd] = b.rzx(-0.123, qubits[0], qubits[1]); + return SmallVector{fst, snd}; }); + q[0] = res[0]; + q[1] = res[1]; + return measureAndReturn(b, q.qubits); } -void inverseMultipleControlledRzx(QCOProgramBuilder& b) { +SmallVector inverseMultipleControlledRzx(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(4); - b.inv({q[0], q[1], q[2], q[3]}, [&](ValueRange qubits) { + auto res = b.inv({q[0], q[1], q[2], q[3]}, [&](ValueRange qubits) { const auto& [controlsOut, targetsOut] = b.mcrzx(-0.123, {qubits[0], qubits[1]}, qubits[2], qubits[3]); SmallVector targets{targetsOut.first, targetsOut.second}; return llvm::to_vector(llvm::concat(controlsOut, targets)); }); + q[0] = res[0]; + q[1] = res[1]; + q[2] = res[2]; + q[3] = res[3]; + return measureAndReturn(b, q.qubits); } -void twoRzxOppositePhase(QCOProgramBuilder& b) { +SmallVector twoRzxOppositePhase(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); std::tie(q[0], q[1]) = b.rzx(0.123, q[0], q[1]); std::tie(q[0], q[1]) = b.rzx(-0.123, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void rzz(QCOProgramBuilder& b) { +SmallVector rzz(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.rzz(0.123, q[0], q[1]); + std::tie(q[0], q[1]) = b.rzz(0.123, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void singleControlledRzz(QCOProgramBuilder& b) { +SmallVector singleControlledRzz(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.crzz(0.123, q[0], q[1], q[2]); + auto [c, t] = b.crzz(0.123, q[0], q[1], q[2]); + q[0] = c; + q[1] = t.first; + q[2] = t.second; + return measureAndReturn(b, q.qubits); } -void multipleControlledRzz(QCOProgramBuilder& b) { +SmallVector multipleControlledRzz(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(4); - b.mcrzz(0.123, {q[0], q[1]}, q[2], q[3]); + auto [c, t] = b.mcrzz(0.123, {q[0], q[1]}, q[2], q[3]); + q[0] = c[0]; + q[1] = c[1]; + q[2] = t.first; + q[3] = t.second; + return measureAndReturn(b, q.qubits); } -void nestedControlledRzz(QCOProgramBuilder& b) { +SmallVector nestedControlledRzz(QCOProgramBuilder& b) { auto reg = b.allocQubitRegister(4); - b.ctrl({reg[0]}, {reg[1], reg[2], reg[3]}, [&](ValueRange targets) { - const auto& [innerControlsOut, innerTargetsOut] = b.ctrl( - {targets[0]}, {targets[1], targets[2]}, [&](ValueRange innerTargets) { - auto res = b.rzz(0.123, innerTargets[0], innerTargets[1]); - return SmallVector{res.first, res.second}; - }); - return llvm::to_vector( - llvm::concat(innerControlsOut, innerTargetsOut)); - }); + auto [c, t] = + b.ctrl({reg[0]}, {reg[1], reg[2], reg[3]}, [&](ValueRange targets) { + const auto& [innerControlsOut, innerTargetsOut] = + b.ctrl({targets[0]}, {targets[1], targets[2]}, + [&](ValueRange innerTargets) { + auto [fst, snd] = + b.rzz(0.123, innerTargets[0], innerTargets[1]); + return SmallVector{fst, snd}; + }); + return llvm::to_vector( + llvm::concat(innerControlsOut, innerTargetsOut)); + }); + reg[0] = c[0]; + reg[1] = t[0]; + reg[2] = t[1]; + reg[3] = t[2]; + return measureAndReturn(b, reg.qubits); } -void trivialControlledRzz(QCOProgramBuilder& b) { +SmallVector trivialControlledRzz(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.mcrzz(0.123, {}, q[0], q[1]); + auto [c, t] = b.mcrzz(0.123, {}, q[0], q[1]); + q[0] = t.first; + q[1] = t.second; + return measureAndReturn(b, q.qubits); } -void inverseRzz(QCOProgramBuilder& b) { +SmallVector inverseRzz(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.inv({q[0], q[1]}, [&](ValueRange qubits) { - auto res = b.rzz(-0.123, qubits[0], qubits[1]); - return SmallVector{res.first, res.second}; + auto res = b.inv({q[0], q[1]}, [&](ValueRange qubits) { + auto [fst, snd] = b.rzz(-0.123, qubits[0], qubits[1]); + return SmallVector{fst, snd}; }); + q[0] = res[0]; + q[1] = res[1]; + return measureAndReturn(b, q.qubits); } -void inverseMultipleControlledRzz(QCOProgramBuilder& b) { +SmallVector inverseMultipleControlledRzz(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(4); - b.inv({q[0], q[1], q[2], q[3]}, [&](ValueRange qubits) { + auto res = b.inv({q[0], q[1], q[2], q[3]}, [&](ValueRange qubits) { const auto& [controlsOut, targetsOut] = b.mcrzz(-0.123, {qubits[0], qubits[1]}, qubits[2], qubits[3]); SmallVector targets{targetsOut.first, targetsOut.second}; return llvm::to_vector(llvm::concat(controlsOut, targets)); }); + q[0] = res[0]; + q[1] = res[1]; + q[2] = res[2]; + q[3] = res[3]; + return measureAndReturn(b, q.qubits); } -void twoRzz(QCOProgramBuilder& b) { +SmallVector twoRzz(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); // 0.045 + 0.078 = 0.123 std::tie(q[0], q[1]) = b.rzz(0.045, q[0], q[1]); std::tie(q[0], q[1]) = b.rzz(0.078, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void twoRzzSwappedTargets(QCOProgramBuilder& b) { +SmallVector twoRzzSwappedTargets(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); // 0.045 + 0.078 = 0.123 std::tie(q[0], q[1]) = b.rzz(0.045, q[0], q[1]); std::tie(q[1], q[0]) = b.rzz(0.078, q[1], q[0]); + return measureAndReturn(b, q.qubits); } -void twoRzzOppositePhase(QCOProgramBuilder& b) { +SmallVector twoRzzOppositePhase(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); std::tie(q[0], q[1]) = b.rzz(0.123, q[0], q[1]); std::tie(q[0], q[1]) = b.rzz(-0.123, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void twoRzzOppositePhaseSwappedTargets(QCOProgramBuilder& b) { +SmallVector twoRzzOppositePhaseSwappedTargets(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); std::tie(q[0], q[1]) = b.rzz(0.123, q[0], q[1]); std::tie(q[1], q[0]) = b.rzz(-0.123, q[1], q[0]); + return measureAndReturn(b, q.qubits); } -void xxPlusYY(QCOProgramBuilder& b) { +SmallVector xxPlusYY(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.xx_plus_yy(0.123, 0.456, q[0], q[1]); + std::tie(q[0], q[1]) = b.xx_plus_yy(0.123, 0.456, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void singleControlledXxPlusYY(QCOProgramBuilder& b) { +SmallVector singleControlledXxPlusYY(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.cxx_plus_yy(0.123, 0.456, q[0], q[1], q[2]); + auto [c, t] = b.cxx_plus_yy(0.123, 0.456, q[0], q[1], q[2]); + q[0] = c; + q[1] = t.first; + q[2] = t.second; + return measureAndReturn(b, q.qubits); } -void multipleControlledXxPlusYY(QCOProgramBuilder& b) { +SmallVector multipleControlledXxPlusYY(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(4); - b.mcxx_plus_yy(0.123, 0.456, {q[0], q[1]}, q[2], q[3]); + auto [c, t] = b.mcxx_plus_yy(0.123, 0.456, {q[0], q[1]}, q[2], q[3]); + q[0] = c[0]; + q[1] = c[1]; + q[2] = t.first; + q[3] = t.second; + return measureAndReturn(b, q.qubits); } -void nestedControlledXxPlusYY(QCOProgramBuilder& b) { +SmallVector nestedControlledXxPlusYY(QCOProgramBuilder& b) { auto reg = b.allocQubitRegister(4); - b.ctrl({reg[0]}, {reg[1], reg[2], reg[3]}, [&](ValueRange targets) { - const auto& [innerControlsOut, innerTargetsOut] = b.ctrl( - {targets[0]}, {targets[1], targets[2]}, [&](ValueRange innerTargets) { - auto res = - b.xx_plus_yy(0.123, 0.456, innerTargets[0], innerTargets[1]); - return SmallVector{res.first, res.second}; - }); - return llvm::to_vector( - llvm::concat(innerControlsOut, innerTargetsOut)); - }); + auto [c, t] = + b.ctrl({reg[0]}, {reg[1], reg[2], reg[3]}, [&](ValueRange targets) { + const auto& [innerControlsOut, innerTargetsOut] = + b.ctrl({targets[0]}, {targets[1], targets[2]}, + [&](ValueRange innerTargets) { + auto [fst, snd] = b.xx_plus_yy( + 0.123, 0.456, innerTargets[0], innerTargets[1]); + return SmallVector{fst, snd}; + }); + return llvm::to_vector( + llvm::concat(innerControlsOut, innerTargetsOut)); + }); + reg[0] = c[0]; + reg[1] = t[0]; + reg[2] = t[1]; + reg[3] = t[2]; + return measureAndReturn(b, reg.qubits); } -void trivialControlledXxPlusYY(QCOProgramBuilder& b) { +SmallVector trivialControlledXxPlusYY(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.mcxx_plus_yy(0.123, 0.456, {}, q[0], q[1]); + auto [c, t] = b.mcxx_plus_yy(0.123, 0.456, {}, q[0], q[1]); + q[0] = t.first; + q[1] = t.second; + return measureAndReturn(b, q.qubits); } -void inverseXxPlusYY(QCOProgramBuilder& b) { +SmallVector inverseXxPlusYY(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.inv({q[0], q[1]}, [&](ValueRange qubits) { - auto res = b.xx_plus_yy(-0.123, 0.456, qubits[0], qubits[1]); - return SmallVector{res.first, res.second}; + auto res = b.inv({q[0], q[1]}, [&](ValueRange qubits) { + auto [fst, snd] = b.xx_plus_yy(-0.123, 0.456, qubits[0], qubits[1]); + return SmallVector{fst, snd}; }); + q[0] = res[0]; + q[1] = res[1]; + return measureAndReturn(b, q.qubits); } -void inverseMultipleControlledXxPlusYY(QCOProgramBuilder& b) { +SmallVector inverseMultipleControlledXxPlusYY(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(4); - b.inv({q[0], q[1], q[2], q[3]}, [&](ValueRange qubits) { + auto res = b.inv({q[0], q[1], q[2], q[3]}, [&](ValueRange qubits) { const auto& [controlsOut, targetsOut] = b.mcxx_plus_yy( -0.123, 0.456, {qubits[0], qubits[1]}, qubits[2], qubits[3]); SmallVector targets{targetsOut.first, targetsOut.second}; return llvm::to_vector(llvm::concat(controlsOut, targets)); }); + q[0] = res[0]; + q[1] = res[1]; + q[2] = res[2]; + q[3] = res[3]; + return measureAndReturn(b, q.qubits); } -void twoXxPlusYYOppositePhase(QCOProgramBuilder& b) { +SmallVector twoXxPlusYYOppositePhase(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); std::tie(q[0], q[1]) = b.xx_plus_yy(0.123, 0.456, q[0], q[1]); std::tie(q[0], q[1]) = b.xx_plus_yy(-0.123, 0.456, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void twoXxPlusYYSwappedTargets(QCOProgramBuilder& b) { +SmallVector twoXxPlusYYSwappedTargets(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); std::tie(q[0], q[1]) = b.xx_plus_yy(0.045, 0.456, q[0], q[1]); std::tie(q[1], q[0]) = b.xx_plus_yy(0.078, 0.456, q[1], q[0]); + return measureAndReturn(b, q.qubits); } -void xxMinusYY(QCOProgramBuilder& b) { +SmallVector xxMinusYY(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.xx_minus_yy(0.123, 0.456, q[0], q[1]); + std::tie(q[0], q[1]) = b.xx_minus_yy(0.123, 0.456, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void singleControlledXxMinusYY(QCOProgramBuilder& b) { +SmallVector singleControlledXxMinusYY(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.cxx_minus_yy(0.123, 0.456, q[0], q[1], q[2]); + auto [c, t] = b.cxx_minus_yy(0.123, 0.456, q[0], q[1], q[2]); + q[0] = c; + q[1] = t.first; + q[2] = t.second; + return measureAndReturn(b, q.qubits); } -void multipleControlledXxMinusYY(QCOProgramBuilder& b) { +SmallVector multipleControlledXxMinusYY(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(4); - b.mcxx_minus_yy(0.123, 0.456, {q[0], q[1]}, q[2], q[3]); + auto [c, t] = b.mcxx_minus_yy(0.123, 0.456, {q[0], q[1]}, q[2], q[3]); + q[0] = c[0]; + q[1] = c[1]; + q[2] = t.first; + q[3] = t.second; + return measureAndReturn(b, q.qubits); } -void nestedControlledXxMinusYY(QCOProgramBuilder& b) { +SmallVector nestedControlledXxMinusYY(QCOProgramBuilder& b) { auto reg = b.allocQubitRegister(4); - b.ctrl({reg[0]}, {reg[1], reg[2], reg[3]}, [&](ValueRange targets) { - const auto& [innerControlsOut, innerTargetsOut] = b.ctrl( - {targets[0]}, {targets[1], targets[2]}, [&](ValueRange innerTargets) { - auto res = - b.xx_minus_yy(0.123, 0.456, innerTargets[0], innerTargets[1]); - return SmallVector{res.first, res.second}; - }); - return llvm::to_vector( - llvm::concat(innerControlsOut, innerTargetsOut)); - }); + auto [c, t] = + b.ctrl({reg[0]}, {reg[1], reg[2], reg[3]}, [&](ValueRange targets) { + const auto& [innerControlsOut, innerTargetsOut] = + b.ctrl({targets[0]}, {targets[1], targets[2]}, + [&](ValueRange innerTargets) { + auto [fst, snd] = b.xx_minus_yy( + 0.123, 0.456, innerTargets[0], innerTargets[1]); + return SmallVector{fst, snd}; + }); + return llvm::to_vector( + llvm::concat(innerControlsOut, innerTargetsOut)); + }); + reg[0] = c[0]; + reg[1] = t[0]; + reg[2] = t[1]; + reg[3] = t[2]; + return measureAndReturn(b, reg.qubits); } -void trivialControlledXxMinusYY(QCOProgramBuilder& b) { +SmallVector trivialControlledXxMinusYY(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.mcxx_minus_yy(0.123, 0.456, {}, q[0], q[1]); + auto [c, t] = b.mcxx_minus_yy(0.123, 0.456, {}, q[0], q[1]); + q[0] = t.first; + q[1] = t.second; + return measureAndReturn(b, q.qubits); } -void inverseXxMinusYY(QCOProgramBuilder& b) { +SmallVector inverseXxMinusYY(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.inv({q[0], q[1]}, [&](ValueRange qubits) { - auto res = b.xx_minus_yy(-0.123, 0.456, qubits[0], qubits[1]); - return SmallVector{res.first, res.second}; + auto res = b.inv({q[0], q[1]}, [&](ValueRange qubits) { + auto [fst, snd] = b.xx_minus_yy(-0.123, 0.456, qubits[0], qubits[1]); + return SmallVector{fst, snd}; }); + q[0] = res[0]; + q[1] = res[1]; + return measureAndReturn(b, q.qubits); } -void inverseMultipleControlledXxMinusYY(QCOProgramBuilder& b) { +SmallVector inverseMultipleControlledXxMinusYY(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(4); - b.inv({q[0], q[1], q[2], q[3]}, [&](ValueRange qubits) { + auto res = b.inv({q[0], q[1], q[2], q[3]}, [&](ValueRange qubits) { const auto& [controlsOut, targetsOut] = b.mcxx_minus_yy( -0.123, 0.456, {qubits[0], qubits[1]}, qubits[2], qubits[3]); SmallVector targets{targetsOut.first, targetsOut.second}; return llvm::to_vector(llvm::concat(controlsOut, targets)); }); + q[0] = res[0]; + q[1] = res[1]; + q[2] = res[2]; + q[3] = res[3]; + return measureAndReturn(b, q.qubits); } -void twoXxMinusYYOppositePhase(QCOProgramBuilder& b) { +SmallVector twoXxMinusYYOppositePhase(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); std::tie(q[0], q[1]) = b.xx_minus_yy(0.123, 0.456, q[0], q[1]); std::tie(q[0], q[1]) = b.xx_minus_yy(-0.123, 0.456, q[0], q[1]); + return measureAndReturn(b, q.qubits); } -void twoXxMinusYYSwappedTargets(QCOProgramBuilder& b) { +SmallVector twoXxMinusYYSwappedTargets(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); std::tie(q[0], q[1]) = b.xx_minus_yy(0.045, 0.456, q[0], q[1]); std::tie(q[1], q[0]) = b.xx_minus_yy(0.078, 0.456, q[1], q[0]); + return measureAndReturn(b, q.qubits); } -void barrier(QCOProgramBuilder& b) { +Value barrier(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.barrier(q[0]); + q[0] = b.barrier(q[0])[0]; + return b.measure(q[0]).second; } -void barrierTwoQubits(QCOProgramBuilder& b) { +SmallVector barrierTwoQubits(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.barrier({q[0], q[1]}); + auto res = b.barrier({q[0], q[1]}); + q[0] = res[0]; + q[1] = res[1]; + return measureAndReturn(b, q.qubits); } -void barrierMultipleQubits(QCOProgramBuilder& b) { +SmallVector barrierMultipleQubits(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.barrier({q[0], q[1], q[2]}); + auto res = b.barrier({q[0], q[1], q[2]}); + q[0] = res[0]; + q[1] = res[1]; + q[2] = res[2]; + return measureAndReturn(b, q.qubits); } -void singleControlledBarrier(QCOProgramBuilder& b) { +Value singleControlledBarrier(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.ctrl({q[1]}, {q[0]}, [&](ValueRange targets) { - return SmallVector{b.barrier(targets[0])}; - }); + auto res = + b.ctrl(q[1], q[0], [&](Value target) { return b.barrier({target})[0]; }); + return b.measure(res.second).second; } -void inverseBarrier(QCOProgramBuilder& b) { +Value inverseBarrier(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.inv({q[0]}, [&](ValueRange qubits) { - return SmallVector{b.barrier(qubits[0])}; - }); + auto res = b.inv(q[0], [&](Value qubit) { return b.barrier({qubit})[0]; }); + return b.measure(res).second; } -void twoBarrier(QCOProgramBuilder& b) { +SmallVector twoBarrier(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); auto b1 = b.barrier({q[0], q[1]}); q[0] = b1[0]; q[1] = b1[1]; - b.barrier({q[0], q[1]}); + auto b2 = b.barrier({q[0], q[1]}); + q[0] = b2[0]; + q[1] = b2[1]; + return measureAndReturn(b, q.qubits); } -void trivialCtrl(QCOProgramBuilder& b) { +SmallVector trivialCtrl(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.ctrl({}, {q[0], q[1]}, [&](ValueRange targets) { + auto [_, q01] = b.ctrl({}, {q[0], q[1]}, [&](ValueRange targets) { auto [q0, q1] = b.rxx(0.123, targets[0], targets[1]); return SmallVector{q0, q1}; }); + return measureAndReturn(b, q01); } -void emptyCtrl(QCOProgramBuilder& b) { +SmallVector emptyCtrl(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); std::tie(q[0], q[1]) = b.rxx(0.123, q[0], q[1]); - b.ctrl(q[0], q[1], [&](ValueRange targets) { return targets; }); + auto [res0, res1] = b.ctrl(q[0], q[1], [&](Value target) { return target; }); + return measureAndReturn(b, {res0, res1}); } -void nestedCtrl(QCOProgramBuilder& b) { +SmallVector nestedCtrl(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(4); - b.ctrl({q[0]}, {q[1], q[2], q[3]}, [&](ValueRange targets) { + auto res = b.ctrl({q[0]}, {q[1], q[2], q[3]}, [&](ValueRange targets) { const auto& [innerControlsOut, innerTargetsOut] = b.ctrl( {targets[0]}, {targets[1], targets[2]}, [&](ValueRange innerTargets) { auto [q0, q1] = b.rxx(0.123, innerTargets[0], innerTargets[1]); @@ -2023,11 +2820,16 @@ void nestedCtrl(QCOProgramBuilder& b) { return llvm::to_vector( llvm::concat(innerControlsOut, innerTargetsOut)); }); + q[0] = res.first[0]; + q[1] = res.second[0]; + q[2] = res.second[1]; + q[3] = res.second[2]; + return measureAndReturn(b, q.qubits); } -void tripleNestedCtrl(QCOProgramBuilder& b) { +SmallVector tripleNestedCtrl(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(5); - b.ctrl({q[0]}, {q[1], q[2], q[3], q[4]}, [&](ValueRange targets) { + auto res = b.ctrl({q[0]}, {q[1], q[2], q[3], q[4]}, [&](ValueRange targets) { const auto& [innerControlsOut, innerTargetsOut] = b.ctrl( {targets[0]}, {targets[1], targets[2], targets[3]}, [&](ValueRange innerTargets) { @@ -2044,25 +2846,40 @@ void tripleNestedCtrl(QCOProgramBuilder& b) { return llvm::to_vector( llvm::concat(innerControlsOut, innerTargetsOut)); }); + q[0] = res.first[0]; + q[1] = res.second[0]; + q[2] = res.second[1]; + q[3] = res.second[2]; + q[4] = res.second[3]; + return measureAndReturn(b, q.qubits); } -void doubleNestedCtrlTwoQubits(QCOProgramBuilder& b) { +SmallVector doubleNestedCtrlTwoQubits(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(6); - b.ctrl({q[0], q[1]}, {q[2], q[3], q[4], q[5]}, [&](ValueRange targets) { - const auto& [innerControlsOut, innerTargetsOut] = - b.ctrl({targets[0], targets[1]}, {targets[2], targets[3]}, - [&](ValueRange innerTargets) { - auto [q0, q1] = b.rxx(0.123, innerTargets[0], innerTargets[1]); - return SmallVector{q0, q1}; - }); - return llvm::to_vector( - llvm::concat(innerControlsOut, innerTargetsOut)); - }); + auto res = + b.ctrl({q[0], q[1]}, {q[2], q[3], q[4], q[5]}, [&](ValueRange targets) { + const auto& [innerControlsOut, innerTargetsOut] = + b.ctrl({targets[0], targets[1]}, {targets[2], targets[3]}, + [&](ValueRange innerTargets) { + auto [q0, q1] = + b.rxx(0.123, innerTargets[0], innerTargets[1]); + return SmallVector{q0, q1}; + }); + return llvm::to_vector( + llvm::concat(innerControlsOut, innerTargetsOut)); + }); + q[0] = res.first[0]; + q[1] = res.first[1]; + q[2] = res.second[0]; + q[3] = res.second[1]; + q[4] = res.second[2]; + q[5] = res.second[3]; + return measureAndReturn(b, q.qubits); } -void ctrlInvSandwich(QCOProgramBuilder& b) { +SmallVector ctrlInvSandwich(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(4); - b.ctrl({q[0]}, {q[1], q[2], q[3]}, [&](ValueRange targets) { + auto res = b.ctrl({q[0]}, {q[1], q[2], q[3]}, [&](ValueRange targets) { auto inner = b.inv( {targets[0], targets[1], targets[2]}, [&](ValueRange innerTargets) { auto [innerControlsOut, innerTargetsOut] = @@ -2075,35 +2892,44 @@ void ctrlInvSandwich(QCOProgramBuilder& b) { return llvm::to_vector( llvm::concat(innerControlsOut, innerTargetsOut)); }); - return SmallVector{inner}; + return llvm::to_vector(inner); }); + q[0] = res.first[0]; + q[1] = res.second[0]; + q[2] = res.second[1]; + q[3] = res.second[2]; + return measureAndReturn(b, q.qubits); } -void ctrlTwo(QCOProgramBuilder& b) { +SmallVector ctrlTwo(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(4); - b.ctrl({q[0], q[1]}, {q[2], q[3]}, [&](ValueRange targets) { + auto res = b.ctrl({q[0], q[1]}, {q[2], q[3]}, [&](ValueRange targets) { auto i0 = targets[0]; auto i1 = targets[1]; i0 = b.x(i0); std::tie(i0, i1) = b.rxx(0.123, i0, i1); return SmallVector{i0, i1}; }); + return measureAndReturn( + b, {res.first[0], res.first[1], res.second[0], res.second[1]}); } -void ctrlTwoMixed(QCOProgramBuilder& b) { +SmallVector ctrlTwoMixed(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(4); - b.ctrl({q[0], q[1]}, {q[2], q[3]}, [&](ValueRange targets) { + auto res = b.ctrl({q[0], q[1]}, {q[2], q[3]}, [&](ValueRange targets) { auto i0 = targets[0]; auto i1 = targets[1]; std::tie(i0, i1) = b.cx(i0, i1); std::tie(i0, i1) = b.rxx(0.123, i0, i1); return SmallVector{i0, i1}; }); + return measureAndReturn( + b, {res.first[0], res.first[1], res.second[0], res.second[1]}); } -void nestedCtrlTwo(QCOProgramBuilder& b) { +SmallVector nestedCtrlTwo(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(4); - b.ctrl(q[0], {q[1], q[2], q[3]}, [&](ValueRange targets) { + auto res = b.ctrl(q[0], {q[1], q[2], q[3]}, [&](ValueRange targets) { const auto& [controlsOut, targetsOut] = b.ctrl( targets[0], {targets[1], targets[2]}, [&](ValueRange innerTargets) { auto i0 = innerTargets[0]; @@ -2114,11 +2940,13 @@ void nestedCtrlTwo(QCOProgramBuilder& b) { }); return llvm::to_vector(llvm::concat(controlsOut, targetsOut)); }); + return measureAndReturn( + b, {res.first[0], res.second[0], res.second[1], res.second[2]}); } -void ctrlInvTwo(QCOProgramBuilder& b) { +SmallVector ctrlInvTwo(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.ctrl(q[0], {q[1], q[2]}, [&](ValueRange targets) { + auto res = b.ctrl(q[0], {q[1], q[2]}, [&](ValueRange targets) { auto inner = b.inv(targets, [&](ValueRange qubits) { auto i0 = qubits[0]; auto i1 = qubits[1]; @@ -2128,28 +2956,33 @@ void ctrlInvTwo(QCOProgramBuilder& b) { }); return llvm::to_vector(inner); }); + return measureAndReturn(b, {res.first[0], res.second[0], res.second[1]}); } -void emptyInv(QCOProgramBuilder& b) { +SmallVector emptyInv(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); std::tie(q[0], q[1]) = b.rxx(0.123, q[0], q[1]); - b.inv({q[0], q[1]}, [&](ValueRange qubits) { return qubits; }); + auto res = b.inv({q[0], q[1]}, [&](ValueRange qubits) { return qubits; }); + return measureAndReturn(b, res); } -void nestedInv(QCOProgramBuilder& b) { +SmallVector nestedInv(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.inv({q[0], q[1]}, [&](ValueRange qubits) { + auto res = b.inv({q[0], q[1]}, [&](ValueRange qubits) { auto inner = b.inv({qubits[0], qubits[1]}, [&](ValueRange innerQubits) { auto [q0, q1] = b.rxx(0.123, innerQubits[0], innerQubits[1]); return SmallVector{q0, q1}; }); - return SmallVector{inner}; + return llvm::to_vector(inner); }); + q[0] = res[0]; + q[1] = res[1]; + return measureAndReturn(b, q.qubits); } -void tripleNestedInv(QCOProgramBuilder& b) { +SmallVector tripleNestedInv(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.inv({q[0], q[1]}, [&](ValueRange qubits) { + auto res = b.inv({q[0], q[1]}, [&](ValueRange qubits) { auto inner1 = b.inv({qubits[0], qubits[1]}, [&](ValueRange innerQubits) { auto inner2 = b.inv( {innerQubits[0], innerQubits[1]}, [&](ValueRange innerInnerQubits) { @@ -2157,15 +2990,18 @@ void tripleNestedInv(QCOProgramBuilder& b) { b.rxx(-0.123, innerInnerQubits[0], innerInnerQubits[1]); return SmallVector{q0, q1}; }); - return SmallVector{inner2}; + return llvm::to_vector(inner2); }); - return SmallVector{inner1}; + return llvm::to_vector(inner1); }); + q[0] = res[0]; + q[1] = res[1]; + return measureAndReturn(b, q.qubits); } -void invCtrlSandwich(QCOProgramBuilder& b) { +SmallVector invCtrlSandwich(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { + auto res = b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { const auto& [controlsOut, targetsOut] = b.ctrl({qubits[0]}, {qubits[1], qubits[2]}, [&](ValueRange targets) { auto inner = @@ -2173,26 +3009,31 @@ void invCtrlSandwich(QCOProgramBuilder& b) { auto [q0, q1] = b.rxx(0.123, innerQubits[0], innerQubits[1]); return SmallVector{q0, q1}; }); - return SmallVector{inner}; + return llvm::to_vector(inner); }); return llvm::to_vector(llvm::concat(controlsOut, targetsOut)); }); + q[0] = res[0]; + q[1] = res[1]; + q[2] = res[2]; + return measureAndReturn(b, q.qubits); } -void invTwo(QCOProgramBuilder& b) { +SmallVector invTwo(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.inv({q[0], q[1]}, [&](ValueRange qubits) { + auto res = b.inv({q[0], q[1]}, [&](ValueRange qubits) { auto i0 = qubits[0]; auto i1 = qubits[1]; i0 = b.x(i0); std::tie(i0, i1) = b.rxx(0.123, i0, i1); return SmallVector{i0, i1}; }); + return measureAndReturn(b, res); } -void invCtrlTwo(QCOProgramBuilder& b) { +SmallVector invCtrlTwo(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(3); - b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { + auto res = b.inv({q[0], q[1], q[2]}, [&](ValueRange qubits) { const auto& [controlsOut, targetsOut] = b.ctrl({qubits[0]}, {qubits[1], qubits[2]}, [&](ValueRange targets) { auto i0 = targets[0]; @@ -2203,45 +3044,56 @@ void invCtrlTwo(QCOProgramBuilder& b) { }); return llvm::to_vector(llvm::concat(controlsOut, targetsOut)); }); + return measureAndReturn(b, res); } -void simpleIf(QCOProgramBuilder& b) { +SmallVector simpleIf(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); auto q0 = b.h(q[0]); auto [measuredQubit, measureResult] = b.measure(q0); - b.qcoIf(measureResult, measuredQubit, [&](ValueRange args) { + auto res = b.qcoIf(measureResult, measuredQubit, [&](ValueRange args) { auto innerQubit = b.x(args[0]); return SmallVector{innerQubit}; }); + q[0] = res[0]; + auto [q1, bit] = b.measure(q[0]); + return {measureResult, bit}; } -void ifWithAngle(QCOProgramBuilder& b) { +Value ifWithAngle(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); auto theta = b.floatConstant(0.123); auto q0 = b.h(q[0]); auto [measuredQubit, measureResult] = b.measure(q0); - b.qcoIf(measureResult, measuredQubit, [&](ValueRange args) { + q0 = b.qcoIf(measureResult, measuredQubit, [&](ValueRange args) { auto innerQubit = b.rx(theta, args[0]); return SmallVector{innerQubit}; - }); + })[0]; + return b.measure(q0).second; } -void ifTwoQubits(QCOProgramBuilder& b) { +SmallVector ifTwoQubits(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); auto q0 = b.h(q[0]); auto [measuredQubit, measureResult] = b.measure(q0); - b.qcoIf(measureResult, {measuredQubit, q[1]}, [&](ValueRange args) { - auto innerQubit0 = b.x(args[0]); - auto innerQubit1 = b.x(args[1]); - return SmallVector{innerQubit0, innerQubit1}; - }); + auto res = + b.qcoIf(measureResult, {measuredQubit, q[1]}, [&](ValueRange args) { + auto innerQubit0 = b.x(args[0]); + auto innerQubit1 = b.x(args[1]); + return SmallVector{innerQubit0, innerQubit1}; + }); + q[0] = res[0]; + q[1] = res[1]; + auto [q0_, c0] = b.measure(q[0]); + auto [q1, c1] = b.measure(q[1]); + return {measureResult, c0, c1}; } -void ifElse(QCOProgramBuilder& b) { +SmallVector ifElse(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); auto q0 = b.h(q[0]); auto [measuredQubit, measureResult] = b.measure(q0); - b.qcoIf( + auto res = b.qcoIf( measureResult, {measuredQubit}, [&](ValueRange args) { auto innerQubit = b.x(args[0]); @@ -2251,25 +3103,30 @@ void ifElse(QCOProgramBuilder& b) { auto innerQubit = b.z(args[0]); return SmallVector{innerQubit}; }); + q[0] = res[0]; + auto [q0_, c0] = b.measure(q[0]); + return {measureResult, c0}; } -void ifOneQubitOneTensor(QCOProgramBuilder& b) { +Value ifOneQubitOneTensor(QCOProgramBuilder& b) { auto q0 = b.allocQubit(); auto t0 = b.allocQubitRegister(1); auto q1 = b.h(q0); auto [measuredQubit, measureResult] = b.measure(q1); - b.qcoIf(measureResult, {measuredQubit, t0.value}, [&](ValueRange args) { - auto innerQubit0 = b.x(args[0]); - auto [t1, innerQubit1] = b.qtensorExtract(args[1], 0); - auto innerQubit2 = b.x(innerQubit1); - auto t2 = b.qtensorInsert(innerQubit2, t1, 0); - return SmallVector{innerQubit0, t2}; - }); + auto ifRes = + b.qcoIf(measureResult, {measuredQubit, t0.value}, [&](ValueRange args) { + auto innerQubit0 = b.x(args[0]); + auto [t1, innerQubit1] = b.qtensorExtract(args[1], 0); + auto innerQubit2 = b.x(innerQubit1); + auto t2 = b.qtensorInsert(innerQubit2, t1, 0); + return SmallVector{innerQubit0, t2}; + }); + return b.measure(ifRes[0]).second; } -void constantTrueIf(QCOProgramBuilder& b) { +Value constantTrueIf(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.qcoIf( + auto ifRes = b.qcoIf( true, q.qubits, [&](ValueRange args) { auto innerQubit = b.x(args[0]); @@ -2279,11 +3136,12 @@ void constantTrueIf(QCOProgramBuilder& b) { auto innerQubit = b.z(args[0]); return SmallVector{innerQubit}; }); + return b.measure(ifRes[0]).second; } -void constantFalseIf(QCOProgramBuilder& b) { +Value constantFalseIf(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.qcoIf( + auto ifRes = b.qcoIf( false, q.qubits, [&](ValueRange args) { auto innerQubit = b.x(args[0]); @@ -2293,13 +3151,14 @@ void constantFalseIf(QCOProgramBuilder& b) { auto innerQubit = b.z(args[0]); return SmallVector{innerQubit}; }); + return b.measure(ifRes[0]).second; } -void nestedTrueIf(QCOProgramBuilder& b) { +SmallVector nestedTrueIf(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); auto q0 = b.h(q[0]); auto [measuredQubit, measureResult] = b.measure(q0); - b.qcoIf(measureResult, measuredQubit, [&](ValueRange outerArgs) { + auto ifRes = b.qcoIf(measureResult, measuredQubit, [&](ValueRange outerArgs) { auto innerResult = b.qcoIf(measureResult, outerArgs, [&](ValueRange innerArgs) { auto innerQubit = b.x(innerArgs[0]); @@ -2307,13 +3166,15 @@ void nestedTrueIf(QCOProgramBuilder& b) { }); return llvm::to_vector(innerResult); }); + auto [q1, c] = b.measure(ifRes[0]); + return {measureResult, c}; } -void nestedFalseIf(QCOProgramBuilder& b) { +SmallVector nestedFalseIf(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); auto q0 = b.h(q[0]); auto [measuredQubit, measureResult] = b.measure(q0); - b.qcoIf( + auto ifRes = b.qcoIf( measureResult, measuredQubit, [&](ValueRange args) { auto innerQubit = b.x(args[0]); @@ -2329,65 +3190,78 @@ void nestedFalseIf(QCOProgramBuilder& b) { }); return llvm::to_vector(innerResult); }); + auto [q1, c] = b.measure(ifRes[0]); + return {measureResult, c}; } -void qtensorAlloc(QCOProgramBuilder& b) { b.qtensorAlloc(3); } +SmallVector qtensorAlloc(QCOProgramBuilder& b) { + (void)b.qtensorAlloc(3); + return measureAndReturn(b, {}); +} -void qtensorDealloc(QCOProgramBuilder& b) { +SmallVector qtensorDealloc(QCOProgramBuilder& b) { auto qtensor = b.qtensorAlloc(3); b.qtensorDealloc(qtensor); + return measureAndReturn(b, {}); } -void qtensorFromElements(QCOProgramBuilder& b) { +SmallVector qtensorFromElements(QCOProgramBuilder& b) { auto q0 = b.allocQubit(); auto q1 = b.allocQubit(); auto q2 = b.allocQubit(); - b.qtensorFromElements({q0, q1, q2}); + (void)b.qtensorFromElements({q0, q1, q2}); + return measureAndReturn(b, {}); } -void qtensorExtract(QCOProgramBuilder& b) { +Value qtensorExtract(QCOProgramBuilder& b) { auto qtensor = b.qtensorAlloc(3); - b.qtensorExtract(qtensor, 0); + auto [t, q] = b.qtensorExtract(qtensor, 0); + return b.measure(q).second; } -void qtensorInsert(QCOProgramBuilder& b) { +SmallVector qtensorInsert(QCOProgramBuilder& b) { auto qtensor = b.qtensorAlloc(3); auto [extractOutTensor, q0] = b.qtensorExtract(qtensor, 0); auto q1 = b.h(q0); - b.qtensorInsert(q1, extractOutTensor, 0); + (void)b.qtensorInsert(q1, extractOutTensor, 0); + return measureAndReturn(b, {}); } -void qtensorExtractInsertIndexMismatch(QCOProgramBuilder& b) { +SmallVector qtensorExtractInsertIndexMismatch(QCOProgramBuilder& b) { auto qtensor = b.qtensorAlloc(3); auto [extractOutTensor, q0] = b.qtensorExtract(qtensor, 0); - b.qtensorInsert(q0, extractOutTensor, 1); + (void)b.qtensorInsert(q0, extractOutTensor, 1); + return measureAndReturn(b, {}); } -void qtensorExtractInsertSameIndex(QCOProgramBuilder& b) { +SmallVector qtensorExtractInsertSameIndex(QCOProgramBuilder& b) { auto qtensor = b.qtensorAlloc(3); auto [extractOutTensor, q0] = b.qtensorExtract(qtensor, 0); - b.qtensorInsert(q0, extractOutTensor, 0); + (void)b.qtensorInsert(q0, extractOutTensor, 0); + return measureAndReturn(b, {}); } -void qtensorInsertExtractIndexMismatch(QCOProgramBuilder& b) { +SmallVector qtensorInsertExtractIndexMismatch(QCOProgramBuilder& b) { auto qtensor = b.qtensorAlloc(3); auto [extractOutTensor, q0] = b.qtensorExtract(qtensor, 0); auto q1 = b.h(q0); auto insertOutTensor = b.qtensorInsert(q1, extractOutTensor, 0); auto [extractOutTensor1, q2] = b.qtensorExtract(insertOutTensor, 1); - b.qtensorInsert(q2, extractOutTensor1, 0); + (void)b.qtensorInsert(q2, extractOutTensor1, 0); + return measureAndReturn(b, {}); } -void qtensorInsertExtractSameIndex(QCOProgramBuilder& b) { +SmallVector qtensorInsertExtractSameIndex(QCOProgramBuilder& b) { auto qtensor = b.qtensorAlloc(3); auto [extractOutTensor, q0] = b.qtensorExtract(qtensor, 0); auto q1 = b.h(q0); auto insertOutTensor = b.qtensorInsert(q1, extractOutTensor, 0); auto [extractOutTensor1, q2] = b.qtensorExtract(insertOutTensor, 0); - b.qtensorInsert(q2, extractOutTensor1, 0); + (void)b.qtensorInsert(q2, extractOutTensor1, 0); + return measureAndReturn(b, {}); } -void qtensorChain(QCOProgramBuilder& b) { +SmallVector qtensorChain(QCOProgramBuilder& b) { Value q0; Value q1; Value q2; @@ -2403,9 +3277,11 @@ void qtensorChain(QCOProgramBuilder& b) { qtensor = b.qtensorInsert(q1, qtensor, 1); qtensor = b.qtensorInsert(q0, qtensor, 0); b.qtensorDealloc(qtensor); + + return measureAndReturn(b, {}); } -void qtensorAlternativeChain(QCOProgramBuilder& b) { +SmallVector qtensorAlternativeChain(QCOProgramBuilder& b) { Value q0; Value q1; Value q2; @@ -2421,12 +3297,14 @@ void qtensorAlternativeChain(QCOProgramBuilder& b) { qtensor = b.qtensorInsert(q1, qtensor, 1); qtensor = b.qtensorInsert(q2, qtensor, 2); b.qtensorDealloc(qtensor); + + return measureAndReturn(b, {}); } -void simpleWhileReset(QCOProgramBuilder& b) { +Value simpleWhileReset(QCOProgramBuilder& b) { auto q0 = b.allocQubit(); auto q1 = b.h(q0); - b.scfWhile( + auto scfWhile = b.scfWhile( ValueRange{q1}, [&](ValueRange iterArgs) { auto [q2, measureResult] = b.measure(iterArgs[0]); @@ -2437,11 +3315,12 @@ void simpleWhileReset(QCOProgramBuilder& b) { auto q3 = b.h(iterArgs[0]); return SmallVector{q3}; }); + return b.measure(scfWhile[0]).second; } -void simpleDoWhileReset(QCOProgramBuilder& b) { +Value simpleDoWhileReset(QCOProgramBuilder& b) { auto q0 = b.allocQubit(); - b.scfWhile( + auto scfWhile = b.scfWhile( ValueRange{q0}, [&](ValueRange iterArgs) { auto q1 = b.h(iterArgs[0]); @@ -2450,46 +3329,54 @@ void simpleDoWhileReset(QCOProgramBuilder& b) { return SmallVector{q2}; }, [&](ValueRange iterArgs) { return llvm::to_vector(iterArgs); }); + return b.measure(scfWhile[0]).second; } -void simpleForLoop(QCOProgramBuilder& b) { +SmallVector simpleForLoop(QCOProgramBuilder& b) { auto reg = b.allocQubitRegister(2); - b.scfFor(0, 2, 1, {reg.value}, [&](Value iv, ValueRange iterArgs) { - auto [t0, q0] = b.qtensorExtract(iterArgs[0], iv); - auto q1 = b.h(q0); - auto insert = b.qtensorInsert(q1, t0, iv); - return SmallVector{insert}; - }); + auto scfFor = + b.scfFor(0, 2, 1, {reg.value}, [&](Value iv, ValueRange iterArgs) { + auto [t0, q0] = b.qtensorExtract(iterArgs[0], iv); + auto q1 = b.h(q0); + auto insert = b.qtensorInsert(q1, t0, iv); + return SmallVector{insert}; + }); + return measureAndReturnQTensor(b, scfFor[0], 2); }; -void forLoopWithAngle(QCOProgramBuilder& b) { +Value forLoopWithAngle(QCOProgramBuilder& b) { auto reg = b.allocQubitRegister(2); auto theta = b.floatConstant(0.123); - b.scfFor(0, 2, 1, {reg.value}, [&](Value iv, ValueRange iterArgs) { - auto [t0, q0] = b.qtensorExtract(iterArgs[0], iv); - auto q1 = b.rx(theta, q0); - auto insert = b.qtensorInsert(q1, t0, iv); - return SmallVector{insert}; - }); -}; + auto scfFor = + b.scfFor(0, 2, 1, {reg.value}, [&](Value iv, ValueRange iterArgs) { + auto [t0, q0] = b.qtensorExtract(iterArgs[0], iv); + auto q1 = b.rx(theta, q0); + auto insert = b.qtensorInsert(q1, t0, iv); + return SmallVector{insert}; + }); + auto [newReg, q] = b.qtensorExtract(scfFor[0], 0); + return b.measure(q).second; +} -void nestedForLoopIfOp(QCOProgramBuilder& b) { +Value nestedForLoopIfOp(QCOProgramBuilder& b) { auto reg = b.allocQubitRegister(2); auto q0 = b.allocQubit(); - b.scfFor(0, 2, 1, {reg.value, q0}, [&](Value iv, ValueRange iterArgs) { - auto q1 = b.h(iterArgs[1]); - auto [q2, cond] = b.measure(q1); - auto ifOp = b.qcoIf(cond, iterArgs[0], [&](ValueRange args) { - auto [t0, q3] = b.qtensorExtract(args[0], iv); - auto q4 = b.h(q3); - auto insert = b.qtensorInsert(q4, t0, iv); - return SmallVector{insert}; - }); - return SmallVector{ifOp[0], q2}; - }); + auto scfFor = + b.scfFor(0, 2, 1, {reg.value, q0}, [&](Value iv, ValueRange iterArgs) { + auto q1 = b.h(iterArgs[1]); + auto [q2, cond] = b.measure(q1); + auto ifOp = b.qcoIf(cond, iterArgs[0], [&](ValueRange args) { + auto [t0, q3] = b.qtensorExtract(args[0], iv); + auto q4 = b.h(q3); + auto insert = b.qtensorInsert(q4, t0, iv); + return SmallVector{insert}; + }); + return SmallVector{ifOp[0], q2}; + }); + return b.measure(scfFor[1]).second; } -void nestedForLoopWhileOp(QCOProgramBuilder& b) { +SmallVector nestedForLoopWhileOp(QCOProgramBuilder& b) { auto reg = b.allocQubitRegister(2); auto loopResult = b.scfFor(0, 2, 1, {reg.value}, [&](Value iv, ValueRange iterArgs) { @@ -2498,61 +3385,63 @@ void nestedForLoopWhileOp(QCOProgramBuilder& b) { auto insert = b.qtensorInsert(q1, t0, iv); return SmallVector{insert}; }); - b.scfFor(0, 2, 1, loopResult, [&](Value iv, ValueRange iterArgs) { - auto [t0, q0] = b.qtensorExtract(iterArgs[0], iv); - auto whileResult = b.scfWhile( - q0, - [&](ValueRange iterArgs) { - auto [q1, measureResult] = b.measure(iterArgs[0]); - b.scfCondition(measureResult, q1); - return SmallVector{q1}; - }, - [&](ValueRange iterArgs) { - auto q2 = b.h(iterArgs[0]); - return SmallVector{q2}; - }); - auto insert = b.qtensorInsert(whileResult[0], t0, iv); - return SmallVector{insert}; - }); + auto scfFor = + b.scfFor(0, 2, 1, loopResult, [&](Value iv, ValueRange iterArgs) { + auto [t0, q0] = b.qtensorExtract(iterArgs[0], iv); + auto whileResult = b.scfWhile( + q0, + [&](ValueRange innerIterArgs) { + auto [q1, measureResult] = b.measure(innerIterArgs[0]); + b.scfCondition(measureResult, q1); + return SmallVector{q1}; + }, + [&](ValueRange innerIterArgs) { + auto q2 = b.h(innerIterArgs[0]); + return SmallVector{q2}; + }); + auto insert = b.qtensorInsert(whileResult[0], t0, iv); + return SmallVector{insert}; + }); + return measureAndReturnQTensor(b, scfFor[0], 2); } -void nestedForLoopCtrlOpWithSeparateQubit(QCOProgramBuilder& b) { +Value nestedForLoopCtrlOpWithSeparateQubit(QCOProgramBuilder& b) { auto reg = b.allocQubitRegister(3); auto control0 = b.allocQubit(); auto control1 = b.h(control0); - b.scfFor(0, 3, 1, {reg.value, control1}, [&](Value iv, ValueRange iterArgs) { - auto [t0, q0] = b.qtensorExtract(iterArgs[0], iv); - auto q1 = b.h(q0); - auto [controls, targets] = b.ctrl(iterArgs[1], q1, [&](ValueRange args) { - auto q2 = b.x(args[0]); - return SmallVector{q2}; - }); - auto insert = b.qtensorInsert(targets[0], t0, iv); - return SmallVector{insert, controls[0]}; - }); + auto scfFor = b.scfFor( + 0, 3, 1, {reg.value, control1}, [&](Value iv, ValueRange iterArgs) { + auto [t0, q0] = b.qtensorExtract(iterArgs[0], iv); + auto q1 = b.h(q0); + auto [controlOut, targetOut] = + b.ctrl(iterArgs[1], q1, [&](Value target) { return b.x(target); }); + auto insert = b.qtensorInsert(targetOut, t0, iv); + return SmallVector{insert, controlOut}; + }); + return b.measure(scfFor[1]).second; } -void nestedForLoopCtrlOpWithExtractedQubit(QCOProgramBuilder& b) { +Value nestedForLoopCtrlOpWithExtractedQubit(QCOProgramBuilder& b) { auto reg = b.allocQubitRegister(4); auto control = b.h(reg[0]); - b.scfFor(1, 4, 1, {reg.value, control}, [&](Value iv, ValueRange iterArgs) { - auto [t0, q0] = b.qtensorExtract(iterArgs[0], iv); - auto q1 = b.h(q0); - auto [controls, targets] = b.ctrl(iterArgs[1], q1, [&](ValueRange args) { - auto q2 = b.x(args[0]); - return SmallVector{q2}; - }); - auto insert = b.qtensorInsert(targets[0], t0, iv); - return SmallVector{insert, controls[0]}; - }); + auto scfFor = b.scfFor( + 1, 4, 1, {reg.value, control}, [&](Value iv, ValueRange iterArgs) { + auto [t0, q0] = b.qtensorExtract(iterArgs[0], iv); + auto q1 = b.h(q0); + auto [controlOut, targetOut] = + b.ctrl(iterArgs[1], q1, [&](Value target) { return b.x(target); }); + auto insert = b.qtensorInsert(targetOut, t0, iv); + return SmallVector{insert, controlOut}; + }); + return b.measure(scfFor[1]).second; } -void nestedIfOpForLoop(QCOProgramBuilder& b) { +Value nestedIfOpForLoop(QCOProgramBuilder& b) { auto reg = b.allocQubitRegister(3); auto q0 = b.allocQubit(); auto q1 = b.h(q0); auto [q2, cond] = b.measure(q1); - b.qcoIf( + auto ifRes = b.qcoIf( cond, {reg.value, q2}, [&](ValueRange args) { auto q3 = b.h(args[1]); @@ -2568,16 +3457,17 @@ void nestedIfOpForLoop(QCOProgramBuilder& b) { }); return SmallVector{scfFor[0], args[1]}; }); + return b.measure(ifRes[1]).second; } -void nestedIfOpForLoopWithAngle(QCOProgramBuilder& b) { +Value nestedIfOpForLoopWithAngle(QCOProgramBuilder& b) { auto reg = b.allocQubitRegister(3); auto q0 = b.allocQubit(); auto theta1 = b.floatConstant(0.123); auto theta2 = b.floatConstant(0.456); auto q1 = b.h(q0); auto [q2, cond] = b.measure(q1); - b.qcoIf( + auto res = b.qcoIf( cond, {reg.value, q2}, [&](ValueRange args) { auto q3 = b.rx(theta1, args[1]); @@ -2593,6 +3483,129 @@ void nestedIfOpForLoopWithAngle(QCOProgramBuilder& b) { }); return SmallVector{scfFor[0], args[1]}; }); + return b.measure(res[1]).second; +} + +SmallVector controlledXH(QCOProgramBuilder& b) { + auto q = b.allocQubitRegister(2); + auto [ctrl, targ] = b.ctrl(q[0], q[1], [&](Value target) { + target = b.x(target); + return b.h(target); + }); + return measureAndReturn(b, {ctrl, targ}); +} + +SmallVector controlledInverseHT(QCOProgramBuilder& b) { + auto q = b.allocQubitRegister(2); + auto [ctrl, targ] = b.ctrl(q[0], q[1], [&](ValueRange targets) { + auto wire = b.inv({targets[0]}, [&](ValueRange innerTargets) { + auto inner = b.h(innerTargets[0]); + inner = b.t(inner); + return SmallVector{inner}; + })[0]; + return SmallVector{wire}; + }); + return measureAndReturn(b, {ctrl[0], targ[0]}); +} + +SmallVector inverseTwoRxRy(QCOProgramBuilder& b) { + auto q = b.allocQubitRegister(2); + auto res = b.inv({q[0], q[1]}, [&](ValueRange targets) { + auto w0 = b.rx(0.2, targets[0]); + auto w1 = b.ry(0.3, targets[1]); + return SmallVector{w0, w1}; + }); + return measureAndReturn(b, res); +} + +SmallVector inverseCxThenRz(QCOProgramBuilder& b) { + auto q = b.allocQubitRegister(2); + auto res = b.inv({q[0], q[1]}, [&](ValueRange targets) { + auto w0 = targets[0]; + auto w1 = targets[1]; + std::tie(w0, w1) = b.cx(w0, w1); + w1 = b.rz(0.4, w1); + return SmallVector{w0, w1}; + }); + return measureAndReturn(b, res); +} + +SmallVector inverseDcxThenRz(QCOProgramBuilder& b) { + auto q = b.allocQubitRegister(2); + auto res = b.inv({q[0], q[1]}, [&](ValueRange targets) { + auto w0 = targets[0]; + auto w1 = targets[1]; + std::tie(w0, w1) = b.dcx(w0, w1); + w1 = b.rz(0.4, w1); + return SmallVector{w0, w1}; + }); + return measureAndReturn(b, res); +} + +Value inverseGphaseBarrierX(QCOProgramBuilder& b) { + auto q = b.allocQubitRegister(1); + auto res = b.inv(q[0], [&](Value target) { + b.gphase(0.25); + auto wire = b.barrier({target})[0]; + wire = b.x(wire); + return wire; + }); + return b.measure(res).second; +} + +Value inverseNestedInvHAndT(QCOProgramBuilder& b) { + auto q = b.allocQubitRegister(1); + auto res = b.inv(q[0], [&](Value target) { + auto wire = b.inv(target, [&](Value inner) { return b.h(inner); }); + return b.t(wire); + }); + return b.measure(res).second; +} + +SmallVector inverseNestedInvHAndX(QCOProgramBuilder& b) { + auto q = b.allocQubitRegister(2); + auto res = b.inv({q[0], q[1]}, [&](ValueRange targets) { + auto w0 = b.inv(targets[0], [&](Value inner) { return b.h(inner); }); + auto w1 = b.x(targets[1]); + return SmallVector{w0, w1}; + }); + return measureAndReturn(b, res); +} + +SmallVector inverseThreeWireRxRyRz(QCOProgramBuilder& b) { + auto q = b.allocQubitRegister(3); + auto res = b.inv({q[0], q[1], q[2]}, [&](ValueRange targets) { + auto w0 = b.rx(0.2, targets[0]); + auto w1 = b.ry(0.3, targets[1]); + auto w2 = b.rz(0.4, targets[2]); + return SmallVector{w0, w1, w2}; + }); + return measureAndReturn(b, res); +} + +SmallVector inverseThreeWireNestedTwoInv(QCOProgramBuilder& b) { + auto q = b.allocQubitRegister(3); + auto res = b.inv({q[0], q[1], q[2]}, [&](ValueRange targets) { + auto inner = b.inv({targets[0], targets[1]}, [&](ValueRange innerTargets) { + auto w0 = b.rx(0.2, innerTargets[0]); + auto w1 = b.ry(0.3, innerTargets[1]); + return SmallVector{w0, w1}; + }); + auto w2 = b.rz(0.4, targets[2]); + return SmallVector{inner[0], inner[1], w2}; + }); + return measureAndReturn(b, res); +} + +SmallVector inverseWithThreeQubitOpInBody(QCOProgramBuilder& b) { + auto q = b.allocQubitRegister(3); + auto res = b.inv({q[0], q[1], q[2]}, [&](ValueRange targets) { + auto [controls, innerTarget] = + b.ctrl({targets[0], targets[1]}, targets[2], + [&](Value inner) { return b.x(inner); }); + return SmallVector{controls[0], controls[1], innerTarget}; + }); + return measureAndReturn(b, res); } } // namespace mlir::qco diff --git a/mlir/unittests/programs/qco_programs.h b/mlir/unittests/programs/qco_programs.h index b568b4b02f..f764e7ab09 100644 --- a/mlir/unittests/programs/qco_programs.h +++ b/mlir/unittests/programs/qco_programs.h @@ -10,1146 +10,1197 @@ #pragma once +#include +#include + namespace mlir::qco { class QCOProgramBuilder; /// Creates an empty QCO program. -void emptyQCO(QCOProgramBuilder& builder); +Value emptyQCO(QCOProgramBuilder& builder); // --- Qubit Management ----------------------------------------------------- // /// Allocates a single qubit. -void allocQubit(QCOProgramBuilder& b); +Value allocQubit(QCOProgramBuilder& b); + +/// Allocates two individual qubits. +SmallVector alloc2Qubits(QCOProgramBuilder& b); + +/// Allocates a single qubit that is not measured. +Value allocQubitNoMeasure(QCOProgramBuilder& b); + +/// Allocates a qubit register of size `1`. +Value alloc1QubitRegister(QCOProgramBuilder& b); /// Allocates a qubit register of size `2`. -void allocQubitRegister(QCOProgramBuilder& b); +SmallVector alloc2QubitRegister(QCOProgramBuilder& b); + +/// Allocates a qubit register of size `3`. +SmallVector alloc3QubitRegister(QCOProgramBuilder& b); /// Allocates two qubit registers of size `2` and `3`. -void allocMultipleQubitRegisters(QCOProgramBuilder& b); +SmallVector allocMultipleQubitRegisters(QCOProgramBuilder& b); /// Allocates a large qubit register. -void allocLargeRegister(QCOProgramBuilder& b); +Value allocLargeRegister(QCOProgramBuilder& b); /// Allocates two inline qubits. -void staticQubits(QCOProgramBuilder& b); +SmallVector staticQubits(QCOProgramBuilder& b); + +/// Allocates two inline qubits without measuring them. +Value staticQubitsNoMeasure(QCOProgramBuilder& b); /// Allocates two static qubits and applies operations. -void staticQubitsWithOps(QCOProgramBuilder& b); +SmallVector staticQubitsWithOps(QCOProgramBuilder& b); /// Allocates two static qubits and applies parametric gates. -void staticQubitsWithParametricOps(QCOProgramBuilder& b); +SmallVector staticQubitsWithParametricOps(QCOProgramBuilder& b); /// Allocates two static qubits and applies a two-target gate. -void staticQubitsWithTwoTargetOps(QCOProgramBuilder& b); +SmallVector staticQubitsWithTwoTargetOps(QCOProgramBuilder& b); /// Allocates two static qubits and applies a controlled gate. -void staticQubitsWithCtrl(QCOProgramBuilder& b); +SmallVector staticQubitsWithCtrl(QCOProgramBuilder& b); /// Allocates a static qubit and applies an inverse modifier. -void staticQubitsWithInv(QCOProgramBuilder& b); +Value staticQubitsWithInv(QCOProgramBuilder& b); /// Allocates and explicitly sinks a single qubit. -void allocSinkPair(QCOProgramBuilder& b); +Value allocSinkPair(QCOProgramBuilder& b); + +/// Allocates two qubits and performs a set of dead gates on them. +SmallVector deadGatesProgram(QCOProgramBuilder& b); + +/// Allocates two qubits and performs a set of dead gates on them, including +/// `if` operations. +Value deadGatesWithIfOpProgram(QCOProgramBuilder& b); + +/// Allocates two qubits and performs only non-dead `if` operations. +Value deadGatesWithIfOpSimplified(QCOProgramBuilder& b); // --- Invalid / mixed addressing (unit tests) -------------------------------- /// @pre `builder.initialize()`. Fatal mixed addressing: static then dynamic /// alloc. -void mixedStaticThenDynamicQubit(QCOProgramBuilder& b); +SmallVector mixedStaticThenDynamicQubit(QCOProgramBuilder& b); /// @pre `builder.initialize()`. Fatal mixed addressing: `qtensor` alloc then /// static. -void mixedDynamicRegisterThenStaticQubit(QCOProgramBuilder& b); +Value mixedDynamicRegisterThenStaticQubit(QCOProgramBuilder& b); // --- MeasureOp ------------------------------------------------------------ // /// Measures a single qubit into a single classical bit. -void singleMeasurementToSingleBit(QCOProgramBuilder& b); +Value singleMeasurementToSingleBit(QCOProgramBuilder& b); /// Repeatedly measures a single qubit into the same classical bit. -void repeatedMeasurementToSameBit(QCOProgramBuilder& b); +Value repeatedMeasurementToSameBit(QCOProgramBuilder& b); /// Repeatedly measures a single qubit into different classical bits. -void repeatedMeasurementToDifferentBits(QCOProgramBuilder& b); +SmallVector repeatedMeasurementToDifferentBits(QCOProgramBuilder& b); /// Measures multiple qubits into multiple classical bits. -void multipleClassicalRegistersAndMeasurements(QCOProgramBuilder& b); +SmallVector +multipleClassicalRegistersAndMeasurements(QCOProgramBuilder& b); /// Measures a single qubit into a single classical bit, without explicitly /// allocating a quantum or classical register. -void measurementWithoutRegisters(QCOProgramBuilder& b); +Value measurementWithoutRegisters(QCOProgramBuilder& b); // --- ResetOp -------------------------------------------------------------- // /// Resets a single qubit without any operations being applied. -void resetQubitWithoutOp(QCOProgramBuilder& b); +Value resetQubitWithoutOp(QCOProgramBuilder& b); /// Resets multiple qubits without any operations being applied. -void resetMultipleQubitsWithoutOp(QCOProgramBuilder& b); +SmallVector resetMultipleQubitsWithoutOp(QCOProgramBuilder& b); /// Repeatedly resets a single qubit without any operations being applied. -void repeatedResetWithoutOp(QCOProgramBuilder& b); +Value repeatedResetWithoutOp(QCOProgramBuilder& b); /// Resets a single qubit after a single operation. -void resetQubitAfterSingleOp(QCOProgramBuilder& b); +Value resetQubitAfterSingleOp(QCOProgramBuilder& b); /// Resets multiple qubits after a single operation. -void resetMultipleQubitsAfterSingleOp(QCOProgramBuilder& b); +SmallVector resetMultipleQubitsAfterSingleOp(QCOProgramBuilder& b); /// Repeatedly resets a single qubit after a single operation. -void repeatedResetAfterSingleOp(QCOProgramBuilder& b); +Value repeatedResetAfterSingleOp(QCOProgramBuilder& b); // --- GPhaseOp ------------------------------------------------------------- // /// Creates a circuit with just a global phase. -void globalPhase(QCOProgramBuilder& b); +Value globalPhase(QCOProgramBuilder& b); /// Creates a controlled global phase gate with a single control qubit. -void singleControlledGlobalPhase(QCOProgramBuilder& b); +Value singleControlledGlobalPhase(QCOProgramBuilder& b); /// Creates a multi-controlled global phase gate with multiple control qubits. -void multipleControlledGlobalPhase(QCOProgramBuilder& b); +SmallVector multipleControlledGlobalPhase(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a global phase gate. -void inverseGlobalPhase(QCOProgramBuilder& b); +Value inverseGlobalPhase(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled global /// phase gate. -void inverseMultipleControlledGlobalPhase(QCOProgramBuilder& b); +SmallVector inverseMultipleControlledGlobalPhase(QCOProgramBuilder& b); // --- IdOp ----------------------------------------------------------------- // /// Creates a circuit with just an identity gate. -void identity(QCOProgramBuilder& b); +Value identity(QCOProgramBuilder& b); /// Creates a controlled identity gate with a single control qubit. -void singleControlledIdentity(QCOProgramBuilder& b); +SmallVector singleControlledIdentity(QCOProgramBuilder& b); /// Creates a multi-controlled identity gate with multiple control qubits. -void multipleControlledIdentity(QCOProgramBuilder& b); +SmallVector multipleControlledIdentity(QCOProgramBuilder& b); /// Creates a circuit with a nested controlled identity gate. -void nestedControlledIdentity(QCOProgramBuilder& b); +SmallVector nestedControlledIdentity(QCOProgramBuilder& b); /// Creates a circuit with a trivial controlled identity gate. -void trivialControlledIdentity(QCOProgramBuilder& b); +Value trivialControlledIdentity(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to an identity gate. -void inverseIdentity(QCOProgramBuilder& b); +Value inverseIdentity(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled identity /// gate. -void inverseMultipleControlledIdentity(QCOProgramBuilder& b); +SmallVector inverseMultipleControlledIdentity(QCOProgramBuilder& b); // --- XOp ------------------------------------------------------------------ // /// Creates a circuit with just an X gate. -void x(QCOProgramBuilder& b); +Value x(QCOProgramBuilder& b); /// Creates a circuit with a single controlled X gate. -void singleControlledX(QCOProgramBuilder& b); +SmallVector singleControlledX(QCOProgramBuilder& b); /// Creates a circuit with a multi-controlled X gate. -void multipleControlledX(QCOProgramBuilder& b); +SmallVector multipleControlledX(QCOProgramBuilder& b); /// Creates a circuit with a nested controlled X gate. -void nestedControlledX(QCOProgramBuilder& b); +SmallVector nestedControlledX(QCOProgramBuilder& b); /// Creates a circuit with a trivial controlled X gate. -void trivialControlledX(QCOProgramBuilder& b); +Value trivialControlledX(QCOProgramBuilder& b); /// Creates a circuit with repeated controlled X gates. -void repeatedControlledX(QCOProgramBuilder& b); +SmallVector repeatedControlledX(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to an X gate. -void inverseX(QCOProgramBuilder& b); +Value inverseX(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled X gate. -void inverseMultipleControlledX(QCOProgramBuilder& b); +SmallVector inverseMultipleControlledX(QCOProgramBuilder& b); /// Creates a circuit with two subsequent X gates. -void twoX(QCOProgramBuilder& b); +Value twoX(QCOProgramBuilder& b); /// Creates a circuit with a control modifier applied to two subsequent X gates. -void controlledTwoX(QCOProgramBuilder& b); +SmallVector controlledTwoX(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to two subsequent X /// gates. -void inverseTwoX(QCOProgramBuilder& b); +Value inverseTwoX(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a global phase and an /// X gate. -void inverseGphaseX(QCOProgramBuilder& b); +Value inverseGphaseX(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a global phase and a /// barrier. -void inverseGphaseBarrier(QCOProgramBuilder& b); +Value inverseGphaseBarrier(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to two consecutive /// barriers. -void inverseTwoBarriersInInv(QCOProgramBuilder& b); +Value inverseTwoBarriersInInv(QCOProgramBuilder& b); // --- YOp ------------------------------------------------------------------ // /// Creates a circuit with just a Y gate. -void y(QCOProgramBuilder& b); +Value y(QCOProgramBuilder& b); /// Creates a circuit with a single controlled Y gate. -void singleControlledY(QCOProgramBuilder& b); +SmallVector singleControlledY(QCOProgramBuilder& b); /// Creates a circuit with a multi-controlled Y gate. -void multipleControlledY(QCOProgramBuilder& b); +SmallVector multipleControlledY(QCOProgramBuilder& b); /// Creates a circuit with a nested controlled Y gate. -void nestedControlledY(QCOProgramBuilder& b); +SmallVector nestedControlledY(QCOProgramBuilder& b); /// Creates a circuit with a trivial controlled Y gate. -void trivialControlledY(QCOProgramBuilder& b); +Value trivialControlledY(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a Y gate. -void inverseY(QCOProgramBuilder& b); +Value inverseY(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled Y gate. -void inverseMultipleControlledY(QCOProgramBuilder& b); +SmallVector inverseMultipleControlledY(QCOProgramBuilder& b); /// Creates a circuit with two Y gates in a row. -void twoY(QCOProgramBuilder& b); +Value twoY(QCOProgramBuilder& b); // --- ZOp ------------------------------------------------------------------ // /// Creates a circuit with just a Z gate. -void z(QCOProgramBuilder& b); +Value z(QCOProgramBuilder& b); /// Creates a circuit with a single controlled Z gate. -void singleControlledZ(QCOProgramBuilder& b); +SmallVector singleControlledZ(QCOProgramBuilder& b); /// Creates a circuit with a multi-controlled Z gate. -void multipleControlledZ(QCOProgramBuilder& b); +SmallVector multipleControlledZ(QCOProgramBuilder& b); /// Creates a circuit with a nested controlled Z gate. -void nestedControlledZ(QCOProgramBuilder& b); +SmallVector nestedControlledZ(QCOProgramBuilder& b); /// Creates a circuit with a trivial controlled Z gate. -void trivialControlledZ(QCOProgramBuilder& b); +Value trivialControlledZ(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a Z gate. -void inverseZ(QCOProgramBuilder& b); +Value inverseZ(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled Z gate. -void inverseMultipleControlledZ(QCOProgramBuilder& b); +SmallVector inverseMultipleControlledZ(QCOProgramBuilder& b); /// Creates a circuit with two Z gates in a row. -void twoZ(QCOProgramBuilder& b); +Value twoZ(QCOProgramBuilder& b); // --- HOp ------------------------------------------------------------------ // /// Creates a circuit with just an H gate. -void h(QCOProgramBuilder& b); +Value h(QCOProgramBuilder& b); /// Creates a circuit with a single controlled H gate. -void singleControlledH(QCOProgramBuilder& b); +SmallVector singleControlledH(QCOProgramBuilder& b); /// Creates a circuit with a multi-controlled H gate. -void multipleControlledH(QCOProgramBuilder& b); +SmallVector multipleControlledH(QCOProgramBuilder& b); /// Creates a circuit with a nested controlled H gate. -void nestedControlledH(QCOProgramBuilder& b); +SmallVector nestedControlledH(QCOProgramBuilder& b); /// Creates a circuit with a trivial controlled H gate. -void trivialControlledH(QCOProgramBuilder& b); +Value trivialControlledH(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to an H gate. -void inverseH(QCOProgramBuilder& b); +Value inverseH(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled H gate. -void inverseMultipleControlledH(QCOProgramBuilder& b); +SmallVector inverseMultipleControlledH(QCOProgramBuilder& b); /// Creates a circuit with two H gates in a row. -void twoH(QCOProgramBuilder& b); +Value twoH(QCOProgramBuilder& b); /// Creates a circuit with just an H gate and no qubit register. -void hWithoutRegister(QCOProgramBuilder& b); +Value hWithoutRegister(QCOProgramBuilder& b); // --- SOp ------------------------------------------------------------------ // /// Creates a circuit with just an S gate. -void s(QCOProgramBuilder& b); +Value s(QCOProgramBuilder& b); /// Creates a circuit with a single controlled S gate. -void singleControlledS(QCOProgramBuilder& b); +SmallVector singleControlledS(QCOProgramBuilder& b); /// Creates a circuit with a multi-controlled S gate. -void multipleControlledS(QCOProgramBuilder& b); +SmallVector multipleControlledS(QCOProgramBuilder& b); /// Creates a circuit with a nested controlled S gate. -void nestedControlledS(QCOProgramBuilder& b); +SmallVector nestedControlledS(QCOProgramBuilder& b); /// Creates a circuit with a trivial controlled S gate. -void trivialControlledS(QCOProgramBuilder& b); +Value trivialControlledS(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to an S gate. -void inverseS(QCOProgramBuilder& b); +Value inverseS(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled S gate. -void inverseMultipleControlledS(QCOProgramBuilder& b); +SmallVector inverseMultipleControlledS(QCOProgramBuilder& b); /// Creates a circuit with an S gate followed by an Sdg gate. -void sThenSdg(QCOProgramBuilder& b); +Value sThenSdg(QCOProgramBuilder& b); /// Creates a circuit with two S gates in a row. -void twoS(QCOProgramBuilder& b); +Value twoS(QCOProgramBuilder& b); // --- SdgOp ---------------------------------------------------------------- // /// Creates a circuit with just an Sdg gate. -void sdg(QCOProgramBuilder& b); +Value sdg(QCOProgramBuilder& b); /// Creates a circuit with a single controlled Sdg gate. -void singleControlledSdg(QCOProgramBuilder& b); +SmallVector singleControlledSdg(QCOProgramBuilder& b); /// Creates a circuit with a multi-controlled Sdg gate. -void multipleControlledSdg(QCOProgramBuilder& b); +SmallVector multipleControlledSdg(QCOProgramBuilder& b); /// Creates a circuit with a nested controlled Sdg gate. -void nestedControlledSdg(QCOProgramBuilder& b); +SmallVector nestedControlledSdg(QCOProgramBuilder& b); /// Creates a circuit with a trivial controlled Sdg gate. -void trivialControlledSdg(QCOProgramBuilder& b); +Value trivialControlledSdg(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to an Sdg gate. -void inverseSdg(QCOProgramBuilder& b); +Value inverseSdg(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled Sdg gate. -void inverseMultipleControlledSdg(QCOProgramBuilder& b); +SmallVector inverseMultipleControlledSdg(QCOProgramBuilder& b); /// Creates a circuit with an Sdg gate followed an S gate. -void sdgThenS(QCOProgramBuilder& b); +Value sdgThenS(QCOProgramBuilder& b); /// Creates a circuit with two Sdg gates in a row. -void twoSdg(QCOProgramBuilder& b); +Value twoSdg(QCOProgramBuilder& b); // --- TOp ------------------------------------------------------------------ // /// Creates a circuit with just a T gate. -void t_(QCOProgramBuilder& b); // NOLINT(*-identifier-naming) +Value t_(QCOProgramBuilder& b); // NOLINT(*-identifier-naming) /// Creates a circuit with a single controlled T gate. -void singleControlledT(QCOProgramBuilder& b); +SmallVector singleControlledT(QCOProgramBuilder& b); /// Creates a circuit with a multi-controlled T gate. -void multipleControlledT(QCOProgramBuilder& b); +SmallVector multipleControlledT(QCOProgramBuilder& b); /// Creates a circuit with a nested controlled T gate. -void nestedControlledT(QCOProgramBuilder& b); +SmallVector nestedControlledT(QCOProgramBuilder& b); /// Creates a circuit with a trivial controlled T gate. -void trivialControlledT(QCOProgramBuilder& b); +Value trivialControlledT(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a T gate. -void inverseT(QCOProgramBuilder& b); +Value inverseT(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled T gate. -void inverseMultipleControlledT(QCOProgramBuilder& b); +SmallVector inverseMultipleControlledT(QCOProgramBuilder& b); /// Creates a circuit with a T gate followed by a Tdg gate. -void tThenTdg(QCOProgramBuilder& b); +Value tThenTdg(QCOProgramBuilder& b); /// Creates a circuit with two T gates in a row. -void twoT(QCOProgramBuilder& b); +Value twoT(QCOProgramBuilder& b); // --- TdgOp ---------------------------------------------------------------- // /// Creates a circuit with just a Tdg gate. -void tdg(QCOProgramBuilder& b); +Value tdg(QCOProgramBuilder& b); /// Creates a circuit with a single controlled Tdg gate. -void singleControlledTdg(QCOProgramBuilder& b); +SmallVector singleControlledTdg(QCOProgramBuilder& b); /// Creates a circuit with a multi-controlled Tdg gate. -void multipleControlledTdg(QCOProgramBuilder& b); +SmallVector multipleControlledTdg(QCOProgramBuilder& b); /// Creates a circuit with a nested controlled Tdg gate. -void nestedControlledTdg(QCOProgramBuilder& b); +SmallVector nestedControlledTdg(QCOProgramBuilder& b); /// Creates a circuit with a trivial controlled Tdg gate. -void trivialControlledTdg(QCOProgramBuilder& b); +Value trivialControlledTdg(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a Tdg gate. -void inverseTdg(QCOProgramBuilder& b); +Value inverseTdg(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled Tdg gate. -void inverseMultipleControlledTdg(QCOProgramBuilder& b); +SmallVector inverseMultipleControlledTdg(QCOProgramBuilder& b); /// Creates a circuit with a Tdg gate followed by a T gate. -void tdgThenT(QCOProgramBuilder& b); +Value tdgThenT(QCOProgramBuilder& b); /// Creates a circuit with two Tdg gates in a row. -void twoTdg(QCOProgramBuilder& b); +Value twoTdg(QCOProgramBuilder& b); // --- SXOp ----------------------------------------------------------------- // /// Creates a circuit with just an SX gate. -void sx(QCOProgramBuilder& b); +Value sx(QCOProgramBuilder& b); /// Creates a circuit with a single controlled SX gate. -void singleControlledSx(QCOProgramBuilder& b); +SmallVector singleControlledSx(QCOProgramBuilder& b); /// Creates a circuit with a multi-controlled SX gate. -void multipleControlledSx(QCOProgramBuilder& b); +SmallVector multipleControlledSx(QCOProgramBuilder& b); /// Creates a circuit with a nested controlled SX gate. -void nestedControlledSx(QCOProgramBuilder& b); +SmallVector nestedControlledSx(QCOProgramBuilder& b); /// Creates a circuit with a trivial controlled SX gate. -void trivialControlledSx(QCOProgramBuilder& b); +Value trivialControlledSx(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to an SX gate. -void inverseSx(QCOProgramBuilder& b); +Value inverseSx(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled SX gate. -void inverseMultipleControlledSx(QCOProgramBuilder& b); +SmallVector inverseMultipleControlledSx(QCOProgramBuilder& b); /// Creates a circuit with an SX gate followed by an SXdg gate. -void sxThenSxdg(QCOProgramBuilder& b); +Value sxThenSxdg(QCOProgramBuilder& b); /// Creates a circuit with two SX gates in a row. -void twoSx(QCOProgramBuilder& b); +Value twoSx(QCOProgramBuilder& b); // --- SXdgOp --------------------------------------------------------------- // /// Creates a circuit with just an SXdg gate. -void sxdg(QCOProgramBuilder& b); +Value sxdg(QCOProgramBuilder& b); /// Creates a circuit with a single controlled SXdg gate. -void singleControlledSxdg(QCOProgramBuilder& b); +SmallVector singleControlledSxdg(QCOProgramBuilder& b); /// Creates a circuit with a multi-controlled SXdg gate. -void multipleControlledSxdg(QCOProgramBuilder& b); +SmallVector multipleControlledSxdg(QCOProgramBuilder& b); /// Creates a circuit with a nested controlled SXdg gate. -void nestedControlledSxdg(QCOProgramBuilder& b); +SmallVector nestedControlledSxdg(QCOProgramBuilder& b); /// Creates a circuit with a trivial controlled SXdg gate. -void trivialControlledSxdg(QCOProgramBuilder& b); +Value trivialControlledSxdg(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to an SXdg gate. -void inverseSxdg(QCOProgramBuilder& b); +Value inverseSxdg(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled SXdg /// gate. -void inverseMultipleControlledSxdg(QCOProgramBuilder& b); +SmallVector inverseMultipleControlledSxdg(QCOProgramBuilder& b); /// Creates a circuit with an SXdg gate followed by an SX gate. -void sxdgThenSx(QCOProgramBuilder& b); +Value sxdgThenSx(QCOProgramBuilder& b); /// Creates a circuit with two SXdg gates in a row. -void twoSxdg(QCOProgramBuilder& b); +Value twoSxdg(QCOProgramBuilder& b); // --- RXOp ----------------------------------------------------------------- // /// Creates a circuit with just an RX gate. -void rx(QCOProgramBuilder& b); +Value rx(QCOProgramBuilder& b); /// Creates a circuit with a single controlled RX gate. -void singleControlledRx(QCOProgramBuilder& b); +SmallVector singleControlledRx(QCOProgramBuilder& b); /// Creates a circuit with a multi-controlled RX gate. -void multipleControlledRx(QCOProgramBuilder& b); +SmallVector multipleControlledRx(QCOProgramBuilder& b); /// Creates a circuit with a nested controlled RX gate. -void nestedControlledRx(QCOProgramBuilder& b); +SmallVector nestedControlledRx(QCOProgramBuilder& b); /// Creates a circuit with a trivial controlled RX gate. -void trivialControlledRx(QCOProgramBuilder& b); +Value trivialControlledRx(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to an RX gate. -void inverseRx(QCOProgramBuilder& b); +Value inverseRx(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled RX gate. -void inverseMultipleControlledRx(QCOProgramBuilder& b); +SmallVector inverseMultipleControlledRx(QCOProgramBuilder& b); /// Creates a circuit with two RX gates in a row with opposite phases. -void twoRxOppositePhase(QCOProgramBuilder& b); +Value twoRxOppositePhase(QCOProgramBuilder& b); /// Creates a circuit with an RX gate with an angle of pi/2. -void rxPiOver2(QCOProgramBuilder& b); +Value rxPiOver2(QCOProgramBuilder& b); // --- RYOp ----------------------------------------------------------------- // /// Creates a circuit with just an RY gate. -void ry(QCOProgramBuilder& b); +Value ry(QCOProgramBuilder& b); /// Creates a circuit with a single controlled RY gate. -void singleControlledRy(QCOProgramBuilder& b); +SmallVector singleControlledRy(QCOProgramBuilder& b); /// Creates a circuit with a multi-controlled RY gate. -void multipleControlledRy(QCOProgramBuilder& b); +SmallVector multipleControlledRy(QCOProgramBuilder& b); /// Creates a circuit with a nested controlled RY gate. -void nestedControlledRy(QCOProgramBuilder& b); +SmallVector nestedControlledRy(QCOProgramBuilder& b); /// Creates a circuit with a trivial controlled RY gate. -void trivialControlledRy(QCOProgramBuilder& b); +Value trivialControlledRy(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to an RY gate. -void inverseRy(QCOProgramBuilder& b); +Value inverseRy(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled RY gate. -void inverseMultipleControlledRy(QCOProgramBuilder& b); +SmallVector inverseMultipleControlledRy(QCOProgramBuilder& b); /// Creates a circuit with two RY gates in a row with opposite phases. -void twoRyOppositePhase(QCOProgramBuilder& b); +Value twoRyOppositePhase(QCOProgramBuilder& b); /// Creates a circuit with an RY gate with an angle of pi/2. -void ryPiOver2(QCOProgramBuilder& b); +Value ryPiOver2(QCOProgramBuilder& b); // --- RZOp ----------------------------------------------------------------- // /// Creates a circuit with just an RZ gate. -void rz(QCOProgramBuilder& b); +Value rz(QCOProgramBuilder& b); /// Creates a circuit with a single controlled RZ gate. -void singleControlledRz(QCOProgramBuilder& b); +SmallVector singleControlledRz(QCOProgramBuilder& b); /// Creates a circuit with a multi-controlled RZ gate. -void multipleControlledRz(QCOProgramBuilder& b); +SmallVector multipleControlledRz(QCOProgramBuilder& b); /// Creates a circuit with a nested controlled RZ gate. -void nestedControlledRz(QCOProgramBuilder& b); +SmallVector nestedControlledRz(QCOProgramBuilder& b); /// Creates a circuit with a trivial controlled RZ gate. -void trivialControlledRz(QCOProgramBuilder& b); +Value trivialControlledRz(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to an RZ gate. -void inverseRz(QCOProgramBuilder& b); +Value inverseRz(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled RZ gate. -void inverseMultipleControlledRz(QCOProgramBuilder& b); +SmallVector inverseMultipleControlledRz(QCOProgramBuilder& b); /// Creates a circuit with two RZ gates in a row with opposite phases. -void twoRzOppositePhase(QCOProgramBuilder& b); +Value twoRzOppositePhase(QCOProgramBuilder& b); // --- POp ------------------------------------------------------------------ // /// Creates a circuit with just a P gate. -void p(QCOProgramBuilder& b); +Value p(QCOProgramBuilder& b); /// Creates a circuit with a single controlled P gate. -void singleControlledP(QCOProgramBuilder& b); +SmallVector singleControlledP(QCOProgramBuilder& b); /// Creates a circuit with a multi-controlled P gate. -void multipleControlledP(QCOProgramBuilder& b); +SmallVector multipleControlledP(QCOProgramBuilder& b); /// Creates a circuit with a nested controlled P gate. -void nestedControlledP(QCOProgramBuilder& b); +SmallVector nestedControlledP(QCOProgramBuilder& b); /// Creates a circuit with a trivial controlled P gate. -void trivialControlledP(QCOProgramBuilder& b); +Value trivialControlledP(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a P gate. -void inverseP(QCOProgramBuilder& b); +Value inverseP(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled P gate. -void inverseMultipleControlledP(QCOProgramBuilder& b); +SmallVector inverseMultipleControlledP(QCOProgramBuilder& b); /// Creates a circuit with two P gates in a row with opposite phases. -void twoPOppositePhase(QCOProgramBuilder& b); +Value twoPOppositePhase(QCOProgramBuilder& b); // --- ROp ------------------------------------------------------------------ // /// Creates a circuit with just an R gate. -void r(QCOProgramBuilder& b); +Value r(QCOProgramBuilder& b); /// Creates a circuit with a single controlled R gate. -void singleControlledR(QCOProgramBuilder& b); +SmallVector singleControlledR(QCOProgramBuilder& b); /// Creates a circuit with a multi-controlled R gate. -void multipleControlledR(QCOProgramBuilder& b); +SmallVector multipleControlledR(QCOProgramBuilder& b); /// Creates a circuit with a nested controlled R gate. -void nestedControlledR(QCOProgramBuilder& b); +SmallVector nestedControlledR(QCOProgramBuilder& b); /// Creates a circuit with a trivial controlled R gate. -void trivialControlledR(QCOProgramBuilder& b); +Value trivialControlledR(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to an R gate. -void inverseR(QCOProgramBuilder& b); +Value inverseR(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled R gate. -void inverseMultipleControlledR(QCOProgramBuilder& b); +SmallVector inverseMultipleControlledR(QCOProgramBuilder& b); /// Creates a circuit with an R gate that can be canonicalized to an RX gate. -void canonicalizeRToRx(QCOProgramBuilder& b); +Value canonicalizeRToRx(QCOProgramBuilder& b); /// Creates a circuit with an R gate that can be canonicalized to an RY gate. -void canonicalizeRToRy(QCOProgramBuilder& b); +Value canonicalizeRToRy(QCOProgramBuilder& b); /// Creates a circuit with two R gates in a row with the same `phi`. -void twoR(QCOProgramBuilder& b); +Value twoR(QCOProgramBuilder& b); // --- U2Op ----------------------------------------------------------------- // /// Creates a circuit with just a U2 gate. -void u2(QCOProgramBuilder& b); +Value u2(QCOProgramBuilder& b); /// Creates a circuit with a single controlled U2 gate. -void singleControlledU2(QCOProgramBuilder& b); +SmallVector singleControlledU2(QCOProgramBuilder& b); /// Creates a circuit with a multi-controlled U2 gate. -void multipleControlledU2(QCOProgramBuilder& b); +SmallVector multipleControlledU2(QCOProgramBuilder& b); /// Creates a circuit with a nested controlled U2 gate. -void nestedControlledU2(QCOProgramBuilder& b); +SmallVector nestedControlledU2(QCOProgramBuilder& b); /// Creates a circuit with a trivial controlled U2 gate. -void trivialControlledU2(QCOProgramBuilder& b); +Value trivialControlledU2(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a U2 gate. -void inverseU2(QCOProgramBuilder& b); +Value inverseU2(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled U2 gate. -void inverseMultipleControlledU2(QCOProgramBuilder& b); +SmallVector inverseMultipleControlledU2(QCOProgramBuilder& b); /// Creates a circuit with a U2 gate that can be canonicalized to an H gate. -void canonicalizeU2ToH(QCOProgramBuilder& b); +Value canonicalizeU2ToH(QCOProgramBuilder& b); /// Creates a circuit with a U2 gate that can be canonicalized to an RX gate. -void canonicalizeU2ToRx(QCOProgramBuilder& b); +Value canonicalizeU2ToRx(QCOProgramBuilder& b); /// Creates a circuit with a U2 gate that can be canonicalized to an RY gate. -void canonicalizeU2ToRy(QCOProgramBuilder& b); +Value canonicalizeU2ToRy(QCOProgramBuilder& b); // --- UOp ------------------------------------------------------------------ // /// Creates a circuit with just a U gate. -void u(QCOProgramBuilder& b); +Value u(QCOProgramBuilder& b); /// Creates a circuit with a single controlled U gate. -void singleControlledU(QCOProgramBuilder& b); +SmallVector singleControlledU(QCOProgramBuilder& b); /// Creates a circuit with a multi-controlled U gate. -void multipleControlledU(QCOProgramBuilder& b); +SmallVector multipleControlledU(QCOProgramBuilder& b); /// Creates a circuit with a nested controlled U gate. -void nestedControlledU(QCOProgramBuilder& b); +SmallVector nestedControlledU(QCOProgramBuilder& b); /// Creates a circuit with a trivial controlled U gate. -void trivialControlledU(QCOProgramBuilder& b); +Value trivialControlledU(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a U gate. -void inverseU(QCOProgramBuilder& b); +Value inverseU(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled U gate. -void inverseMultipleControlledU(QCOProgramBuilder& b); +SmallVector inverseMultipleControlledU(QCOProgramBuilder& b); /// Creates a circuit with a U gate that can be canonicalized to a P gate. -void canonicalizeUToP(QCOProgramBuilder& b); +Value canonicalizeUToP(QCOProgramBuilder& b); /// Creates a circuit with a U gate that can be canonicalized to an RX gate. -void canonicalizeUToRx(QCOProgramBuilder& b); +Value canonicalizeUToRx(QCOProgramBuilder& b); /// Creates a circuit with a U gate that can be canonicalized to an RY gate. -void canonicalizeUToRy(QCOProgramBuilder& b); +Value canonicalizeUToRy(QCOProgramBuilder& b); /// Creates a circuit with a U gate that can be canonicalized to a U2 gate. -void canonicalizeUToU2(QCOProgramBuilder& b); +Value canonicalizeUToU2(QCOProgramBuilder& b); // --- SWAPOp --------------------------------------------------------------- // /// Creates a circuit with just a SWAP gate. -void swap(QCOProgramBuilder& b); +SmallVector swap(QCOProgramBuilder& b); /// Creates a circuit with a single controlled SWAP gate. -void singleControlledSwap(QCOProgramBuilder& b); +SmallVector singleControlledSwap(QCOProgramBuilder& b); /// Creates a circuit with a multi-controlled SWAP gate. -void multipleControlledSwap(QCOProgramBuilder& b); +SmallVector multipleControlledSwap(QCOProgramBuilder& b); /// Creates a circuit with a nested controlled SWAP gate. -void nestedControlledSwap(QCOProgramBuilder& b); +SmallVector nestedControlledSwap(QCOProgramBuilder& b); /// Creates a circuit with a trivial controlled SWAP gate. -void trivialControlledSwap(QCOProgramBuilder& b); +SmallVector trivialControlledSwap(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a SWAP gate. -void inverseSwap(QCOProgramBuilder& b); +SmallVector inverseSwap(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled SWAP /// gate. -void inverseMultipleControlledSwap(QCOProgramBuilder& b); +SmallVector inverseMultipleControlledSwap(QCOProgramBuilder& b); /// Creates a circuit with two SWAP gates in a row. -void twoSwap(QCOProgramBuilder& b); +SmallVector twoSwap(QCOProgramBuilder& b); /// Creates a circuit with two SWAP gates in a row with swapped targets. -void twoSwapSwappedTargets(QCOProgramBuilder& b); +SmallVector twoSwapSwappedTargets(QCOProgramBuilder& b); // --- iSWAPOp -------------------------------------------------------------- // /// Creates a circuit with just an iSWAP gate. -void iswap(QCOProgramBuilder& b); +SmallVector iswap(QCOProgramBuilder& b); /// Creates a circuit with a single controlled iSWAP gate. -void singleControlledIswap(QCOProgramBuilder& b); +SmallVector singleControlledIswap(QCOProgramBuilder& b); /// Creates a circuit with a multi-controlled iSWAP gate. -void multipleControlledIswap(QCOProgramBuilder& b); +SmallVector multipleControlledIswap(QCOProgramBuilder& b); /// Creates a circuit with a nested controlled iSWAP gate. -void nestedControlledIswap(QCOProgramBuilder& b); +SmallVector nestedControlledIswap(QCOProgramBuilder& b); /// Creates a circuit with a trivial controlled iSWAP gate. -void trivialControlledIswap(QCOProgramBuilder& b); +SmallVector trivialControlledIswap(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to an iSWAP gate. -void inverseIswap(QCOProgramBuilder& b); +SmallVector inverseIswap(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled iSWAP /// gate. -void inverseMultipleControlledIswap(QCOProgramBuilder& b); +SmallVector inverseMultipleControlledIswap(QCOProgramBuilder& b); // --- DCXOp ---------------------------------------------------------------- // /// Creates a circuit with just a DCX gate. -void dcx(QCOProgramBuilder& b); +SmallVector dcx(QCOProgramBuilder& b); /// Creates a circuit with a single controlled DCX gate. -void singleControlledDcx(QCOProgramBuilder& b); +SmallVector singleControlledDcx(QCOProgramBuilder& b); /// Creates a circuit with a multi-controlled DCX gate. -void multipleControlledDcx(QCOProgramBuilder& b); +SmallVector multipleControlledDcx(QCOProgramBuilder& b); /// Creates a circuit with a nested controlled DCX gate. -void nestedControlledDcx(QCOProgramBuilder& b); +SmallVector nestedControlledDcx(QCOProgramBuilder& b); /// Creates a circuit with a trivial controlled DCX gate. -void trivialControlledDcx(QCOProgramBuilder& b); +SmallVector trivialControlledDcx(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a DCX gate. -void inverseDcx(QCOProgramBuilder& b); +SmallVector inverseDcx(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled DCX gate. -void inverseMultipleControlledDcx(QCOProgramBuilder& b); +SmallVector inverseMultipleControlledDcx(QCOProgramBuilder& b); /// Creates a circuit with two DCX gates in a row with identical targets. -void twoDcx(QCOProgramBuilder& b); +SmallVector twoDcx(QCOProgramBuilder& b); /// Creates a circuit with two DCX gates in a row with swapped targets. -void twoDcxSwappedTargets(QCOProgramBuilder& b); +SmallVector twoDcxSwappedTargets(QCOProgramBuilder& b); // --- ECROp ---------------------------------------------------------------- // /// Creates a circuit with just an ECR gate. -void ecr(QCOProgramBuilder& b); +SmallVector ecr(QCOProgramBuilder& b); /// Creates a circuit with a single controlled ECR gate. -void singleControlledEcr(QCOProgramBuilder& b); +SmallVector singleControlledEcr(QCOProgramBuilder& b); /// Creates a circuit with a multi-controlled ECR gate. -void multipleControlledEcr(QCOProgramBuilder& b); +SmallVector multipleControlledEcr(QCOProgramBuilder& b); /// Creates a circuit with a nested controlled ECR gate. -void nestedControlledEcr(QCOProgramBuilder& b); +SmallVector nestedControlledEcr(QCOProgramBuilder& b); /// Creates a circuit with a trivial controlled ECR gate. -void trivialControlledEcr(QCOProgramBuilder& b); +SmallVector trivialControlledEcr(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to an ECR gate. -void inverseEcr(QCOProgramBuilder& b); +SmallVector inverseEcr(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled ECR gate. -void inverseMultipleControlledEcr(QCOProgramBuilder& b); +SmallVector inverseMultipleControlledEcr(QCOProgramBuilder& b); /// Creates a circuit with two ECR gates in a row. -void twoEcr(QCOProgramBuilder& b); +SmallVector twoEcr(QCOProgramBuilder& b); // --- RXXOp ---------------------------------------------------------------- // /// Creates a circuit with just an RXX gate. -void rxx(QCOProgramBuilder& b); +SmallVector rxx(QCOProgramBuilder& b); /// Creates a circuit with a single controlled RXX gate. -void singleControlledRxx(QCOProgramBuilder& b); +SmallVector singleControlledRxx(QCOProgramBuilder& b); /// Creates a circuit with a multi-controlled RXX gate. -void multipleControlledRxx(QCOProgramBuilder& b); +SmallVector multipleControlledRxx(QCOProgramBuilder& b); /// Creates a circuit with a nested controlled RXX gate. -void nestedControlledRxx(QCOProgramBuilder& b); +SmallVector nestedControlledRxx(QCOProgramBuilder& b); /// Creates a circuit with a trivial controlled RXX gate. -void trivialControlledRxx(QCOProgramBuilder& b); +SmallVector trivialControlledRxx(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to an RXX gate. -void inverseRxx(QCOProgramBuilder& b); +SmallVector inverseRxx(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled RXX gate. -void inverseMultipleControlledRxx(QCOProgramBuilder& b); +SmallVector inverseMultipleControlledRxx(QCOProgramBuilder& b); /// Creates a circuit with a triple-controlled RXX gate. -void tripleControlledRxx(QCOProgramBuilder& b); +SmallVector tripleControlledRxx(QCOProgramBuilder& b); /// Creates a circuit with a four-controlled RXX gate. -void fourControlledRxx(QCOProgramBuilder& b); +SmallVector fourControlledRxx(QCOProgramBuilder& b); /// Creates a circuit with two RXX gates in a row. -void twoRxx(QCOProgramBuilder& b); +SmallVector twoRxx(QCOProgramBuilder& b); /// Creates a circuit with two RXX gates in a row with swapped targets. -void twoRxxSwappedTargets(QCOProgramBuilder& b); +SmallVector twoRxxSwappedTargets(QCOProgramBuilder& b); /// Creates a circuit with two RXX gates in a row with opposite phases. -void twoRxxOppositePhase(QCOProgramBuilder& b); +SmallVector twoRxxOppositePhase(QCOProgramBuilder& b); /// Creates a circuit with two RXX gates in a row with opposite phases and /// swapped targets. -void twoRxxOppositePhaseSwappedTargets(QCOProgramBuilder& b); +SmallVector twoRxxOppositePhaseSwappedTargets(QCOProgramBuilder& b); // --- RYYOp ---------------------------------------------------------------- // /// Creates a circuit with just an RYY gate. -void ryy(QCOProgramBuilder& b); +SmallVector ryy(QCOProgramBuilder& b); /// Creates a circuit with a single controlled RYY gate. -void singleControlledRyy(QCOProgramBuilder& b); +SmallVector singleControlledRyy(QCOProgramBuilder& b); /// Creates a circuit with a multi-controlled RYY gate. -void multipleControlledRyy(QCOProgramBuilder& b); +SmallVector multipleControlledRyy(QCOProgramBuilder& b); /// Creates a circuit with a nested controlled RYY gate. -void nestedControlledRyy(QCOProgramBuilder& b); +SmallVector nestedControlledRyy(QCOProgramBuilder& b); /// Creates a circuit with a trivial controlled RYY gate. -void trivialControlledRyy(QCOProgramBuilder& b); +SmallVector trivialControlledRyy(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to an RYY gate. -void inverseRyy(QCOProgramBuilder& b); +SmallVector inverseRyy(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled RYY gate. -void inverseMultipleControlledRyy(QCOProgramBuilder& b); +SmallVector inverseMultipleControlledRyy(QCOProgramBuilder& b); /// Creates a circuit with two RYY gates in a row. -void twoRyy(QCOProgramBuilder& b); +SmallVector twoRyy(QCOProgramBuilder& b); /// Creates a circuit with two RYY gates in a row with swapped targets. -void twoRyySwappedTargets(QCOProgramBuilder& b); +SmallVector twoRyySwappedTargets(QCOProgramBuilder& b); /// Creates a circuit with two RYY gates in a row with opposite phases. -void twoRyyOppositePhase(QCOProgramBuilder& b); +SmallVector twoRyyOppositePhase(QCOProgramBuilder& b); /// Creates a circuit with two RYY gates in a row with opposite phases and /// swapped targets. -void twoRyyOppositePhaseSwappedTargets(QCOProgramBuilder& b); +SmallVector twoRyyOppositePhaseSwappedTargets(QCOProgramBuilder& b); // --- RZXOp ---------------------------------------------------------------- // /// Creates a circuit with just an RZX gate. -void rzx(QCOProgramBuilder& b); +SmallVector rzx(QCOProgramBuilder& b); /// Creates a circuit with a single controlled RZX gate. -void singleControlledRzx(QCOProgramBuilder& b); +SmallVector singleControlledRzx(QCOProgramBuilder& b); /// Creates a circuit with a multi-controlled RZX gate. -void multipleControlledRzx(QCOProgramBuilder& b); +SmallVector multipleControlledRzx(QCOProgramBuilder& b); /// Creates a circuit with a nested controlled RZX gate. -void nestedControlledRzx(QCOProgramBuilder& b); +SmallVector nestedControlledRzx(QCOProgramBuilder& b); /// Creates a circuit with a trivial controlled RZX gate. -void trivialControlledRzx(QCOProgramBuilder& b); +SmallVector trivialControlledRzx(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to an RZX gate. -void inverseRzx(QCOProgramBuilder& b); +SmallVector inverseRzx(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled RZX gate. -void inverseMultipleControlledRzx(QCOProgramBuilder& b); +SmallVector inverseMultipleControlledRzx(QCOProgramBuilder& b); /// Creates a circuit with two RZX gates in a row with opposite phases. -void twoRzxOppositePhase(QCOProgramBuilder& b); +SmallVector twoRzxOppositePhase(QCOProgramBuilder& b); // --- RZZOp ---------------------------------------------------------------- // /// Creates a circuit with just an RZZ gate. -void rzz(QCOProgramBuilder& b); +SmallVector rzz(QCOProgramBuilder& b); /// Creates a circuit with a single controlled RZZ gate. -void singleControlledRzz(QCOProgramBuilder& b); +SmallVector singleControlledRzz(QCOProgramBuilder& b); /// Creates a circuit with a multi-controlled RZZ gate. -void multipleControlledRzz(QCOProgramBuilder& b); +SmallVector multipleControlledRzz(QCOProgramBuilder& b); /// Creates a circuit with a nested controlled RZZ gate. -void nestedControlledRzz(QCOProgramBuilder& b); +SmallVector nestedControlledRzz(QCOProgramBuilder& b); /// Creates a circuit with a trivial controlled RZZ gate. -void trivialControlledRzz(QCOProgramBuilder& b); +SmallVector trivialControlledRzz(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to an RZZ gate. -void inverseRzz(QCOProgramBuilder& b); +SmallVector inverseRzz(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled RZZ gate. -void inverseMultipleControlledRzz(QCOProgramBuilder& b); +SmallVector inverseMultipleControlledRzz(QCOProgramBuilder& b); /// Creates a circuit with two RZZ gates in a row. -void twoRzz(QCOProgramBuilder& b); +SmallVector twoRzz(QCOProgramBuilder& b); /// Creates a circuit with two RZZ gates in a row with swapped targets. -void twoRzzSwappedTargets(QCOProgramBuilder& b); +SmallVector twoRzzSwappedTargets(QCOProgramBuilder& b); /// Creates a circuit with two RZZ gates in a row with opposite phases. -void twoRzzOppositePhase(QCOProgramBuilder& b); +SmallVector twoRzzOppositePhase(QCOProgramBuilder& b); /// Creates a circuit with two RZZ gates in a row with opposite phases and /// swapped targets. -void twoRzzOppositePhaseSwappedTargets(QCOProgramBuilder& b); +SmallVector twoRzzOppositePhaseSwappedTargets(QCOProgramBuilder& b); // --- XXPlusYYOp ----------------------------------------------------------- // /// Creates a circuit with just an XXPlusYY gate. -void xxPlusYY(QCOProgramBuilder& b); +SmallVector xxPlusYY(QCOProgramBuilder& b); /// Creates a circuit with a single controlled XXPlusYY gate. -void singleControlledXxPlusYY(QCOProgramBuilder& b); +SmallVector singleControlledXxPlusYY(QCOProgramBuilder& b); /// Creates a circuit with a multi-controlled XXPlusYY gate. -void multipleControlledXxPlusYY(QCOProgramBuilder& b); +SmallVector multipleControlledXxPlusYY(QCOProgramBuilder& b); /// Creates a circuit with a nested controlled XXPlusYY gate. -void nestedControlledXxPlusYY(QCOProgramBuilder& b); +SmallVector nestedControlledXxPlusYY(QCOProgramBuilder& b); /// Creates a circuit with a trivial controlled XXPlusYY gate. -void trivialControlledXxPlusYY(QCOProgramBuilder& b); +SmallVector trivialControlledXxPlusYY(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to an XXPlusYY gate. -void inverseXxPlusYY(QCOProgramBuilder& b); +SmallVector inverseXxPlusYY(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled XXPlusYY /// gate. -void inverseMultipleControlledXxPlusYY(QCOProgramBuilder& b); +SmallVector inverseMultipleControlledXxPlusYY(QCOProgramBuilder& b); /// Creates a circuit with two XXPlusYY gates in a row with opposite phases. -void twoXxPlusYYOppositePhase(QCOProgramBuilder& b); +SmallVector twoXxPlusYYOppositePhase(QCOProgramBuilder& b); /// Creates a circuit with two XXPlusYY gates in a row with swapped targets. -void twoXxPlusYYSwappedTargets(QCOProgramBuilder& b); +SmallVector twoXxPlusYYSwappedTargets(QCOProgramBuilder& b); // --- XXMinusYYOp ---------------------------------------------------------- // /// Creates a circuit with just an XXMinusYY gate. -void xxMinusYY(QCOProgramBuilder& b); +SmallVector xxMinusYY(QCOProgramBuilder& b); /// Creates a circuit with a single controlled XXMinusYY gate. -void singleControlledXxMinusYY(QCOProgramBuilder& b); +SmallVector singleControlledXxMinusYY(QCOProgramBuilder& b); /// Creates a circuit with a multi-controlled XXMinusYY gate. -void multipleControlledXxMinusYY(QCOProgramBuilder& b); +SmallVector multipleControlledXxMinusYY(QCOProgramBuilder& b); /// Creates a circuit with a nested controlled XXMinusYY gate. -void nestedControlledXxMinusYY(QCOProgramBuilder& b); +SmallVector nestedControlledXxMinusYY(QCOProgramBuilder& b); /// Creates a circuit with a trivial controlled XXMinusYY gate. -void trivialControlledXxMinusYY(QCOProgramBuilder& b); +SmallVector trivialControlledXxMinusYY(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to an XXMinusYY gate. -void inverseXxMinusYY(QCOProgramBuilder& b); +SmallVector inverseXxMinusYY(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a controlled XXMinusYY /// gate. -void inverseMultipleControlledXxMinusYY(QCOProgramBuilder& b); +SmallVector inverseMultipleControlledXxMinusYY(QCOProgramBuilder& b); /// Creates a circuit with two XXMinusYY gates in a row with opposite phases. -void twoXxMinusYYOppositePhase(QCOProgramBuilder& b); +SmallVector twoXxMinusYYOppositePhase(QCOProgramBuilder& b); /// Creates a circuit with two XXMinusYY gates in a row with swapped targets. -void twoXxMinusYYSwappedTargets(QCOProgramBuilder& b); +SmallVector twoXxMinusYYSwappedTargets(QCOProgramBuilder& b); // --- BarrierOp ------------------------------------------------------------ // /// Creates a circuit with a barrier. -void barrier(QCOProgramBuilder& b); +Value barrier(QCOProgramBuilder& b); /// Creates a circuit with a barrier on two qubits. -void barrierTwoQubits(QCOProgramBuilder& b); +SmallVector barrierTwoQubits(QCOProgramBuilder& b); /// Creates a circuit with a barrier on multiple qubits. -void barrierMultipleQubits(QCOProgramBuilder& b); +SmallVector barrierMultipleQubits(QCOProgramBuilder& b); /// Creates a circuit with a single controlled barrier. -void singleControlledBarrier(QCOProgramBuilder& b); +Value singleControlledBarrier(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a barrier. -void inverseBarrier(QCOProgramBuilder& b); +Value inverseBarrier(QCOProgramBuilder& b); /// Creates a circuit with two barriers in a row with overlapping qubits. -void twoBarrier(QCOProgramBuilder& b); +SmallVector twoBarrier(QCOProgramBuilder& b); // --- CtrlOp --------------------------------------------------------------- // /// Creates a circuit with a trivial ctrl modifier. -void trivialCtrl(QCOProgramBuilder& b); +SmallVector trivialCtrl(QCOProgramBuilder& b); /// Creates a circuit with an empty ctrl modifier. -void emptyCtrl(QCOProgramBuilder& b); +SmallVector emptyCtrl(QCOProgramBuilder& b); /// Creates a circuit with nested ctrl modifiers. -void nestedCtrl(QCOProgramBuilder& b); +SmallVector nestedCtrl(QCOProgramBuilder& b); /// Creates a circuit with triple nested ctrl modifiers. -void tripleNestedCtrl(QCOProgramBuilder& b); +SmallVector tripleNestedCtrl(QCOProgramBuilder& b); /// Creates a circuit with double nested ctrl modifiers with two qubits each. -void doubleNestedCtrlTwoQubits(QCOProgramBuilder& b); +SmallVector doubleNestedCtrlTwoQubits(QCOProgramBuilder& b); /// Creates a circuit with control modifiers interleaved by an inverse modifier. -void ctrlInvSandwich(QCOProgramBuilder& b); +SmallVector ctrlInvSandwich(QCOProgramBuilder& b); /// Creates a circuit with a control modifier applied to two gates. -void ctrlTwo(QCOProgramBuilder& b); +SmallVector ctrlTwo(QCOProgramBuilder& b); /// Creates a circuit with a control modifier applied to a controlled and a /// non-controlled gate. -void ctrlTwoMixed(QCOProgramBuilder& b); +SmallVector ctrlTwoMixed(QCOProgramBuilder& b); /// Creates a circuit with nested control modifiers applied to two gates. -void nestedCtrlTwo(QCOProgramBuilder& b); +SmallVector nestedCtrlTwo(QCOProgramBuilder& b); /// Creates a circuit with a control modifier applied to an inverse modifier /// applied to two gates. -void ctrlInvTwo(QCOProgramBuilder& b); +SmallVector ctrlInvTwo(QCOProgramBuilder& b); // --- InvOp ---------------------------------------------------------------- // /// Creates a circuit with an empty inverse modifier. -void emptyInv(QCOProgramBuilder& b); +SmallVector emptyInv(QCOProgramBuilder& b); /// Creates a circuit with nested inverse modifiers. -void nestedInv(QCOProgramBuilder& b); +SmallVector nestedInv(QCOProgramBuilder& b); /// Creates a circuit with triple nested inverse modifiers. -void tripleNestedInv(QCOProgramBuilder& b); +SmallVector tripleNestedInv(QCOProgramBuilder& b); /// Creates a circuit with inverse modifiers interleaved by a control modifier. -void invCtrlSandwich(QCOProgramBuilder& b); +SmallVector invCtrlSandwich(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to two gates. -void invTwo(QCOProgramBuilder& b); +SmallVector invTwo(QCOProgramBuilder& b); /// Creates a circuit with an inverse modifier applied to a control modifier /// applied to two gates. -void invCtrlTwo(QCOProgramBuilder& b); +SmallVector invCtrlTwo(QCOProgramBuilder& b); // --- IfOp ---------------------------------------------------------------- // /// Creates a circuit with a simple if operation with one qubit. -void simpleIf(QCOProgramBuilder& b); +SmallVector simpleIf(QCOProgramBuilder& b); /// Creates a circuit with an if operation with a parameterized gate. -void ifWithAngle(QCOProgramBuilder& b); +Value ifWithAngle(QCOProgramBuilder& b); /// Creates a circuit with an if operation with two qubits. -void ifTwoQubits(QCOProgramBuilder& b); +SmallVector ifTwoQubits(QCOProgramBuilder& b); /// Creates a circuit with an if operation with an else branch. -void ifElse(QCOProgramBuilder& b); +SmallVector ifElse(QCOProgramBuilder& b); /// Creates a circuit with an if operation with one qubit and one register. -void ifOneQubitOneTensor(QCOProgramBuilder& b); +Value ifOneQubitOneTensor(QCOProgramBuilder& b); /// Creates a circuit with an if operation that uses a constant true as /// condition. -void constantTrueIf(QCOProgramBuilder& b); +Value constantTrueIf(QCOProgramBuilder& b); /// Creates a circuit with an if operation that uses a constant false as /// condition. -void constantFalseIf(QCOProgramBuilder& b); +Value constantFalseIf(QCOProgramBuilder& b); /// Creates a circuit with a nested if operation in the then branch that uses /// the same condition. -void nestedTrueIf(QCOProgramBuilder& b); +SmallVector nestedTrueIf(QCOProgramBuilder& b); /// Creates a circuit with a nested if operation in the else branch that uses /// the same condition. -void nestedFalseIf(QCOProgramBuilder& b); +SmallVector nestedFalseIf(QCOProgramBuilder& b); /// Creates a circuit with an if operation with a nested for operation with /// a register. -void nestedIfOpForLoop(QCOProgramBuilder& b); +Value nestedIfOpForLoop(QCOProgramBuilder& b); /// Creates a circuit with an if operation with a nested for operation and /// parameterized gates. -void nestedIfOpForLoopWithAngle(QCOProgramBuilder& b); +Value nestedIfOpForLoopWithAngle(QCOProgramBuilder& b); // --- WhileOp -------------------------------------------------------------- // /// Creates a circuit with a while operation using a while loop. -void simpleWhileReset(QCOProgramBuilder& b); +Value simpleWhileReset(QCOProgramBuilder& b); /// Creates a circuit with a while operation using a do-while loop. -void simpleDoWhileReset(QCOProgramBuilder& b); +Value simpleDoWhileReset(QCOProgramBuilder& b); // --- ForOp ---------------------------------------------------------------- // /// Creates a circuit with a simple for operation with a register. -void simpleForLoop(QCOProgramBuilder& b); +SmallVector simpleForLoop(QCOProgramBuilder& b); /// Creates a circuit with a for operation with a parameterized gate. -void forLoopWithAngle(QCOProgramBuilder& b); +Value forLoopWithAngle(QCOProgramBuilder& b); /// Creates a circuit with a for operation with a register and a qubit and a /// nested if operation. -void nestedForLoopIfOp(QCOProgramBuilder& b); +Value nestedForLoopIfOp(QCOProgramBuilder& b); /// Creates a circuit with a for operation with a register and a nested while /// operation. -void nestedForLoopWhileOp(QCOProgramBuilder& b); +SmallVector nestedForLoopWhileOp(QCOProgramBuilder& b); /// Creates a circuit with a for operation with a register and a qubit and a /// nested ctrl operation where the qubit is separately allocated from the /// register. -void nestedForLoopCtrlOpWithSeparateQubit(QCOProgramBuilder& b); +Value nestedForLoopCtrlOpWithSeparateQubit(QCOProgramBuilder& b); /// Creates a circuit with a for operation with a register and a qubit and a /// nested ctrl operation where the qubit is extracted from the register. -void nestedForLoopCtrlOpWithExtractedQubit(QCOProgramBuilder& b); +Value nestedForLoopCtrlOpWithExtractedQubit(QCOProgramBuilder& b); // --- QTensor Operations -------------------------------------------------- // /// Allocates a tensor of size `3`. -void qtensorAlloc(QCOProgramBuilder& b); +SmallVector qtensorAlloc(QCOProgramBuilder& b); /// Allocates and explicitly deallocates a tensor. -void qtensorDealloc(QCOProgramBuilder& b); +SmallVector qtensorDealloc(QCOProgramBuilder& b); /// Constructs a tensor with from_elements. -void qtensorFromElements(QCOProgramBuilder& b); +SmallVector qtensorFromElements(QCOProgramBuilder& b); /// Extracts a qubit from a tensor. -void qtensorExtract(QCOProgramBuilder& b); +Value qtensorExtract(QCOProgramBuilder& b); /// Inserts a qubit into a tensor. -void qtensorInsert(QCOProgramBuilder& b); +SmallVector qtensorInsert(QCOProgramBuilder& b); /// Extracts a qubit from a tensor and inserts it immediately at a different /// index. -void qtensorExtractInsertIndexMismatch(QCOProgramBuilder& b); +SmallVector qtensorExtractInsertIndexMismatch(QCOProgramBuilder& b); /// Extracts a qubit from a tensor and inserts it immediately at the same index. -void qtensorExtractInsertSameIndex(QCOProgramBuilder& b); +SmallVector qtensorExtractInsertSameIndex(QCOProgramBuilder& b); /// Inserts a qubit into a tensor and extracts it immediately at a different /// index. -void qtensorInsertExtractIndexMismatch(QCOProgramBuilder& b); +SmallVector qtensorInsertExtractIndexMismatch(QCOProgramBuilder& b); /// Inserts a qubit into a tensor and extracts it immediately at the same index. -void qtensorInsertExtractSameIndex(QCOProgramBuilder& b); +SmallVector qtensorInsertExtractSameIndex(QCOProgramBuilder& b); /// Extracts three qubits with ascending index (0, 1, 2), performs a /// computation, and finally inserts the qubits in ascending order (0, 1, 2). -void qtensorChain(QCOProgramBuilder& b); +SmallVector qtensorChain(QCOProgramBuilder& b); /// Performs the same computation as the `qtensorChain` function, but uses /// qubits immediately after the extract and inserts the qubits in descending /// order (2, 1, 0). -void qtensorAlternativeChain(QCOProgramBuilder& b); +SmallVector qtensorAlternativeChain(QCOProgramBuilder& b); + +SmallVector controlledXH(QCOProgramBuilder& b); + +SmallVector controlledInverseHT(QCOProgramBuilder& b); + +SmallVector inverseTwoRxRy(QCOProgramBuilder& b); + +SmallVector inverseCxThenRz(QCOProgramBuilder& b); + +SmallVector inverseDcxThenRz(QCOProgramBuilder& b); + +Value inverseGphaseBarrierX(QCOProgramBuilder& b); + +Value inverseNestedInvHAndT(QCOProgramBuilder& b); + +SmallVector inverseNestedInvHAndX(QCOProgramBuilder& b); + +SmallVector inverseThreeWireRxRyRz(QCOProgramBuilder& b); + +SmallVector inverseThreeWireNestedTwoInv(QCOProgramBuilder& b); + +SmallVector inverseWithThreeQubitOpInBody(QCOProgramBuilder& b); } // namespace mlir::qco diff --git a/mlir/unittests/programs/qir_programs.cpp b/mlir/unittests/programs/qir_programs.cpp index 948a74b889..1eb9efae9e 100644 --- a/mlir/unittests/programs/qir_programs.cpp +++ b/mlir/unittests/programs/qir_programs.cpp @@ -12,22 +12,77 @@ #include "mlir/Dialect/QIR/Builder/QIRProgramBuilder.h" +#include + +#include #include namespace mlir::qir { -void emptyQIR([[maybe_unused]] QIRProgramBuilder& builder) {} +/** + * @brief Measures the given qubits, records the outcomes and returns + * a single `i64` exit code with the value 0. + * @param b The QIRProgramBuilder used to perform the measurements and create + * the struct. + * @param qubits The qubits to be measured. + * @param inRegister Whether to store the results in a classical result array or + * not. + * @param startIndex The starting index for measurement outcomes. + * @return The result value. + */ +static Value measureAndRecord(QIRProgramBuilder& b, ValueRange qubits, + bool inRegister, int64_t startIndex = 0) { + + if (qubits.empty()) { + return b.intConstant(0); + } + QIRProgramBuilder::ClassicalRegister resultArray; + if (inRegister) { + resultArray = b.allocClassicalBitRegister( + static_cast(qubits.size()), "meas"); + } -void allocQubit(QIRProgramBuilder& b) { b.allocQubitRegister(1); } + for (auto i = 0L; i < qubits.size(); ++i) { + inRegister ? b.measure(qubits[i], resultArray[i]) + : b.measure(qubits[i], startIndex + i); + } + + return b.intConstant(0); +} -void allocQubitRegister(QIRProgramBuilder& b) { b.allocQubitRegister(2); } +template Value emptyQIR(QIRProgramBuilder& b) { + return measureAndRecord(b, {}, IntoRegister); +} + +template Value allocQubit(QIRProgramBuilder& b) { + auto q = b.allocQubit(); + return measureAndRecord(b, {q}, IntoRegister); +} + +template Value alloc1QubitRegister(QIRProgramBuilder& b) { + auto q = b.allocQubitRegister(1); + return measureAndRecord(b, q.qubits, IntoRegister); +} + +template Value allocQubitRegister(QIRProgramBuilder& b) { + auto q = b.allocQubitRegister(2); + return measureAndRecord(b, q.qubits, IntoRegister); +} + +template Value alloc3QubitRegister(QIRProgramBuilder& b) { + auto q = b.allocQubitRegister(3); + return measureAndRecord(b, q.qubits, IntoRegister); +} -void allocMultipleQubitRegisters(QIRProgramBuilder& b) { - b.allocQubitRegister(2); - b.allocQubitRegister(3); +template +Value allocMultipleQubitRegisters(QIRProgramBuilder& b) { + auto q0 = b.allocQubitRegister(2); + auto q1 = b.allocQubitRegister(3); + return measureAndRecord(b, {q0[0], q0[1], q1[0], q1[1], q1[2]}, IntoRegister); } -void allocMultipleQubitRegistersWithOps(QIRProgramBuilder& b) { +template +Value allocMultipleQubitRegistersWithOps(QIRProgramBuilder& b) { auto q0 = b.allocQubitRegister(2); auto q1 = b.allocQubitRegister(3); b.h(q0[0]); @@ -35,47 +90,57 @@ void allocMultipleQubitRegistersWithOps(QIRProgramBuilder& b) { b.h(q1[0]); b.h(q1[1]); b.h(q1[2]); + return measureAndRecord(b, {q0[0], q0[1], q1[0], q1[1], q1[2]}, IntoRegister); } -void allocLargeRegister(QIRProgramBuilder& b) { b.allocQubitRegister(100); } +template Value allocLargeRegister(QIRProgramBuilder& b) { + auto q = b.allocQubitRegister(100); + return measureAndRecord(b, {q[0], q[99]}, IntoRegister); +} -void staticQubits(QIRProgramBuilder& b) { - b.staticQubit(0); - b.staticQubit(1); +Value staticQubits(QIRProgramBuilder& b) { + auto q0 = b.staticQubit(0); + auto q1 = b.staticQubit(1); + return measureAndRecord(b, {q0, q1}, false); } -void staticQubitsWithOps(QIRProgramBuilder& b) { +Value staticQubitsWithOps(QIRProgramBuilder& b) { auto q0 = b.staticQubit(0); auto q1 = b.staticQubit(1); b.h(q0); b.h(q1); + return measureAndRecord(b, {q0, q1}, false); } -void staticQubitsWithParametricOps(QIRProgramBuilder& b) { +Value staticQubitsWithParametricOps(QIRProgramBuilder& b) { auto q0 = b.staticQubit(0); auto q1 = b.staticQubit(1); b.rx(std::numbers::pi / 4., q0); b.p(std::numbers::pi / 2., q1); + return measureAndRecord(b, {q0, q1}, false); } -void staticQubitsWithTwoTargetOps(QIRProgramBuilder& b) { +Value staticQubitsWithTwoTargetOps(QIRProgramBuilder& b) { auto q0 = b.staticQubit(0); auto q1 = b.staticQubit(1); b.rzz(0.123, q0, q1); + return measureAndRecord(b, {q0, q1}, false); } -void staticQubitsWithCtrl(QIRProgramBuilder& b) { +Value staticQubitsWithCtrl(QIRProgramBuilder& b) { auto q0 = b.staticQubit(0); auto q1 = b.staticQubit(1); b.cx(q0, q1); + return measureAndRecord(b, {q0, q1}, false); } -void staticQubitsWithInv(QIRProgramBuilder& b) { +Value staticQubitsWithInv(QIRProgramBuilder& b) { auto q0 = b.staticQubit(0); b.tdg(q0); + return measureAndRecord(b, {q0}, false); } -void staticQubitsWithDuplicates(QIRProgramBuilder& b) { +Value staticQubitsWithDuplicates(QIRProgramBuilder& b) { auto q0a = b.staticQubit(0); auto q1a = b.staticQubit(1); auto q0b = b.staticQubit(0); @@ -85,9 +150,10 @@ void staticQubitsWithDuplicates(QIRProgramBuilder& b) { b.rzz(0.123, q0b, q1b); b.cx(q0b, q1b); b.tdg(q0a); + return measureAndRecord(b, {q0b, q1b}, false); } -void staticQubitsCanonical(QIRProgramBuilder& b) { +Value staticQubitsCanonical(QIRProgramBuilder& b) { auto q0 = b.staticQubit(0); auto q1 = b.staticQubit(1); b.rx(std::numbers::pi / 4., q0); @@ -95,541 +161,679 @@ void staticQubitsCanonical(QIRProgramBuilder& b) { b.rzz(0.123, q0, q1); b.cx(q0, q1); b.tdg(q0); + return measureAndRecord(b, {q0, q1}, false); } -void mixedStaticThenDynamicQubit(QIRProgramBuilder& b) { - b.staticQubit(0); - b.allocQubit(); +Value mixedStaticThenDynamicQubit(QIRProgramBuilder& b) { + auto q0 = b.staticQubit(0); + auto q1 = b.allocQubit(); + return measureAndRecord(b, {q0, q1}, false); } -void mixedDynamicRegisterThenStaticQubit(QIRProgramBuilder& b) { - b.allocQubitRegister(2); - b.staticQubit(0); +Value mixedDynamicRegisterThenStaticQubit(QIRProgramBuilder& b) { + auto q0 = b.allocQubitRegister(2); + auto q1 = b.staticQubit(0); + return measureAndRecord(b, {q0[0], q0[1], q1}, false); } -void singleMeasurementToSingleBit(QIRProgramBuilder& b) { +template +Value singleMeasurementToSingleBit(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(1); const auto c = b.allocClassicalBitRegister(1); b.measure(q[0], c[0]); + return b.intConstant(0); } -void repeatedMeasurementToSameBit(QIRProgramBuilder& b) { +template +Value repeatedMeasurementToSameBit(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(1); const auto c = b.allocClassicalBitRegister(1); b.measure(q[0], c[0]); b.measure(q[0], c[0]); b.measure(q[0], c[0]); + return b.intConstant(0); } -void repeatedMeasurementToDifferentBits(QIRProgramBuilder& b) { +template +Value repeatedMeasurementToDifferentBits(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(1); const auto c = b.allocClassicalBitRegister(3); b.measure(q[0], c[0]); b.measure(q[0], c[1]); b.measure(q[0], c[2]); + return b.intConstant(0); } -void multipleClassicalRegistersAndMeasurements(QIRProgramBuilder& b) { +template +Value multipleClassicalRegistersAndMeasurements(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(3); const auto& c0 = b.allocClassicalBitRegister(1, "c0"); const auto& c1 = b.allocClassicalBitRegister(2, "c1"); b.measure(q[0], c0[0]); b.measure(q[1], c1[0]); b.measure(q[2], c1[1]); + return b.intConstant(0); } -void measurementWithoutRegisters(QIRProgramBuilder& b) { +template +Value measurementWithoutRegisters(QIRProgramBuilder& b) { auto q = b.allocQubit(); - b.measure(q, 0); + auto bit = b.measure(q, 0); + return b.intConstant(0); } -void resetQubitWithoutOp(QIRProgramBuilder& b) { +template Value resetQubitWithoutOp(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.reset(q[0]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void resetMultipleQubitsWithoutOp(QIRProgramBuilder& b) { +template +Value resetMultipleQubitsWithoutOp(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.reset(q[0]); b.reset(q[1]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void repeatedResetWithoutOp(QIRProgramBuilder& b) { +template +Value repeatedResetWithoutOp(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.reset(q[0]); b.reset(q[0]); b.reset(q[0]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void resetQubitAfterSingleOp(QIRProgramBuilder& b) { +template +Value resetQubitAfterSingleOp(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.h(q[0]); b.reset(q[0]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void resetMultipleQubitsAfterSingleOp(QIRProgramBuilder& b) { +template +Value resetMultipleQubitsAfterSingleOp(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.h(q[0]); b.reset(q[0]); b.h(q[1]); b.reset(q[1]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void repeatedResetAfterSingleOp(QIRProgramBuilder& b) { +template +Value repeatedResetAfterSingleOp(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.h(q[0]); b.reset(q[0]); b.reset(q[0]); b.reset(q[0]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void globalPhase(QIRProgramBuilder& b) { b.gphase(0.123); } +template Value globalPhase(QIRProgramBuilder& b) { + b.gphase(0.123); + return measureAndRecord(b, {}, IntoRegister); +} -void identity(QIRProgramBuilder& b) { +template Value identity(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.id(q[0]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void singleControlledIdentity(QIRProgramBuilder& b) { +template +Value singleControlledIdentity(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.cid(q[0], q[1]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void multipleControlledIdentity(QIRProgramBuilder& b) { +template Value twoQubitsOneIdentity(QIRProgramBuilder& b) { + auto q = b.allocQubitRegister(2); + b.id(q[1]); + return measureAndRecord(b, q.qubits, IntoRegister); +} + +template +Value threeQubitsOneIdentity(QIRProgramBuilder& b) { + auto q = b.allocQubitRegister(3); + b.id(q[2]); + return measureAndRecord(b, q.qubits, IntoRegister); +} + +template +Value multipleControlledIdentity(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.mcid({q[0], q[1]}, q[2]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void x(QIRProgramBuilder& b) { +template Value x(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.x(q[0]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void singleControlledX(QIRProgramBuilder& b) { +template Value singleControlledX(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.cx(q[0], q[1]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void multipleControlledX(QIRProgramBuilder& b) { +template Value multipleControlledX(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.mcx({q[0], q[1]}, q[2]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void y(QIRProgramBuilder& b) { +template Value y(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.y(q[0]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void singleControlledY(QIRProgramBuilder& b) { +template Value singleControlledY(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.cy(q[0], q[1]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void multipleControlledY(QIRProgramBuilder& b) { +template Value multipleControlledY(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.mcy({q[0], q[1]}, q[2]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void z(QIRProgramBuilder& b) { +template Value z(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.z(q[0]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void singleControlledZ(QIRProgramBuilder& b) { +template Value singleControlledZ(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.cz(q[0], q[1]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void multipleControlledZ(QIRProgramBuilder& b) { +template Value multipleControlledZ(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.mcz({q[0], q[1]}, q[2]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void h(QIRProgramBuilder& b) { +template Value h(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.h(q[0]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void singleControlledH(QIRProgramBuilder& b) { +template Value singleControlledH(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.ch(q[0], q[1]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void multipleControlledH(QIRProgramBuilder& b) { +template Value multipleControlledH(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.mch({q[0], q[1]}, q[2]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void hWithoutRegister(QIRProgramBuilder& b) { +template Value hWithoutRegister(QIRProgramBuilder& b) { auto q = b.allocQubit(); b.h(q); + return measureAndRecord(b, {q}, false); } -void s(QIRProgramBuilder& b) { +template Value s(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.s(q[0]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void singleControlledS(QIRProgramBuilder& b) { +template Value singleControlledS(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.cs(q[0], q[1]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void multipleControlledS(QIRProgramBuilder& b) { +template Value multipleControlledS(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.mcs({q[0], q[1]}, q[2]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void sdg(QIRProgramBuilder& b) { +template Value sdg(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.sdg(q[0]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void singleControlledSdg(QIRProgramBuilder& b) { +template Value singleControlledSdg(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.csdg(q[0], q[1]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void multipleControlledSdg(QIRProgramBuilder& b) { +template Value multipleControlledSdg(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.mcsdg({q[0], q[1]}, q[2]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void t_(QIRProgramBuilder& b) { +template +Value t_(QIRProgramBuilder& b) { // NOLINT(*-identifier-naming) auto q = b.allocQubitRegister(1); b.t(q[0]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void singleControlledT(QIRProgramBuilder& b) { +template Value singleControlledT(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.ct(q[0], q[1]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void multipleControlledT(QIRProgramBuilder& b) { +template Value multipleControlledT(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.mct({q[0], q[1]}, q[2]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void tdg(QIRProgramBuilder& b) { +template Value tdg(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.tdg(q[0]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void singleControlledTdg(QIRProgramBuilder& b) { +template Value singleControlledTdg(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.ctdg(q[0], q[1]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void multipleControlledTdg(QIRProgramBuilder& b) { +template Value multipleControlledTdg(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.mctdg({q[0], q[1]}, q[2]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void sx(QIRProgramBuilder& b) { +template Value sx(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.sx(q[0]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void singleControlledSx(QIRProgramBuilder& b) { +template Value singleControlledSx(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.csx(q[0], q[1]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void multipleControlledSx(QIRProgramBuilder& b) { +template Value multipleControlledSx(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.mcsx({q[0], q[1]}, q[2]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void sxdg(QIRProgramBuilder& b) { +template Value sxdg(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.sxdg(q[0]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void singleControlledSxdg(QIRProgramBuilder& b) { +template Value singleControlledSxdg(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.csxdg(q[0], q[1]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void multipleControlledSxdg(QIRProgramBuilder& b) { +template +Value multipleControlledSxdg(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.mcsxdg({q[0], q[1]}, q[2]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void rx(QIRProgramBuilder& b) { +template Value rx(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.rx(0.123, q[0]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void singleControlledRx(QIRProgramBuilder& b) { +template Value singleControlledRx(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.crx(0.123, q[0], q[1]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void multipleControlledRx(QIRProgramBuilder& b) { +template Value multipleControlledRx(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.mcrx(0.123, {q[0], q[1]}, q[2]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void ry(QIRProgramBuilder& b) { +template Value ry(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.ry(0.456, q[0]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void singleControlledRy(QIRProgramBuilder& b) { +template Value singleControlledRy(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.cry(0.456, q[0], q[1]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void multipleControlledRy(QIRProgramBuilder& b) { +template Value multipleControlledRy(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.mcry(0.456, {q[0], q[1]}, q[2]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void rz(QIRProgramBuilder& b) { +template Value rz(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.rz(0.789, q[0]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void singleControlledRz(QIRProgramBuilder& b) { +template Value singleControlledRz(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.crz(0.789, q[0], q[1]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void multipleControlledRz(QIRProgramBuilder& b) { +template Value multipleControlledRz(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.mcrz(0.789, {q[0], q[1]}, q[2]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void p(QIRProgramBuilder& b) { +template Value p(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.p(0.123, q[0]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void singleControlledP(QIRProgramBuilder& b) { +template Value singleControlledP(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.cp(0.123, q[0], q[1]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void multipleControlledP(QIRProgramBuilder& b) { +template Value multipleControlledP(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.mcp(0.123, {q[0], q[1]}, q[2]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void r(QIRProgramBuilder& b) { +template Value r(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.r(0.123, 0.456, q[0]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void singleControlledR(QIRProgramBuilder& b) { +template Value singleControlledR(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.cr(0.123, 0.456, q[0], q[1]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void multipleControlledR(QIRProgramBuilder& b) { +template Value multipleControlledR(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.mcr(0.123, 0.456, {q[0], q[1]}, q[2]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void u2(QIRProgramBuilder& b) { +template Value u2(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.u2(0.234, 0.567, q[0]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void singleControlledU2(QIRProgramBuilder& b) { +template Value singleControlledU2(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.cu2(0.234, 0.567, q[0], q[1]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void multipleControlledU2(QIRProgramBuilder& b) { +template Value multipleControlledU2(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.mcu2(0.234, 0.567, {q[0], q[1]}, q[2]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void u(QIRProgramBuilder& b) { +template Value u(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.u(0.1, 0.2, 0.3, q[0]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void singleControlledU(QIRProgramBuilder& b) { +template Value singleControlledU(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.cu(0.1, 0.2, 0.3, q[0], q[1]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void multipleControlledU(QIRProgramBuilder& b) { +template Value multipleControlledU(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.mcu(0.1, 0.2, 0.3, {q[0], q[1]}, q[2]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void swap(QIRProgramBuilder& b) { +template Value swap(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.swap(q[0], q[1]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void singleControlledSwap(QIRProgramBuilder& b) { +template Value singleControlledSwap(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.cswap(q[0], q[1], q[2]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void multipleControlledSwap(QIRProgramBuilder& b) { +template +Value multipleControlledSwap(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(4); b.mcswap({q[0], q[1]}, q[2], q[3]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void iswap(QIRProgramBuilder& b) { +template Value iswap(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.iswap(q[0], q[1]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void singleControlledIswap(QIRProgramBuilder& b) { +template Value singleControlledIswap(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.ciswap(q[0], q[1], q[2]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void multipleControlledIswap(QIRProgramBuilder& b) { +template +Value multipleControlledIswap(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(4); b.mciswap({q[0], q[1]}, q[2], q[3]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void dcx(QIRProgramBuilder& b) { +template Value dcx(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.dcx(q[0], q[1]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void singleControlledDcx(QIRProgramBuilder& b) { +template Value singleControlledDcx(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.cdcx(q[0], q[1], q[2]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void multipleControlledDcx(QIRProgramBuilder& b) { +template Value multipleControlledDcx(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(4); b.mcdcx({q[0], q[1]}, q[2], q[3]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void ecr(QIRProgramBuilder& b) { +template Value ecr(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.ecr(q[0], q[1]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void singleControlledEcr(QIRProgramBuilder& b) { +template Value singleControlledEcr(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.cecr(q[0], q[1], q[2]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void multipleControlledEcr(QIRProgramBuilder& b) { +template Value multipleControlledEcr(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(4); b.mcecr({q[0], q[1]}, q[2], q[3]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void rxx(QIRProgramBuilder& b) { +template Value rxx(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.rxx(0.123, q[0], q[1]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void singleControlledRxx(QIRProgramBuilder& b) { +template Value singleControlledRxx(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.crxx(0.123, q[0], q[1], q[2]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void multipleControlledRxx(QIRProgramBuilder& b) { +template Value multipleControlledRxx(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(4); b.mcrxx(0.123, {q[0], q[1]}, q[2], q[3]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void tripleControlledRxx(QIRProgramBuilder& b) { +template Value tripleControlledRxx(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(5); b.mcrxx(0.123, {q[0], q[1], q[2]}, q[3], q[4]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void ryy(QIRProgramBuilder& b) { +template Value ryy(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.ryy(0.123, q[0], q[1]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void singleControlledRyy(QIRProgramBuilder& b) { +template Value singleControlledRyy(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.cryy(0.123, q[0], q[1], q[2]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void multipleControlledRyy(QIRProgramBuilder& b) { +template Value multipleControlledRyy(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(4); b.mcryy(0.123, {q[0], q[1]}, q[2], q[3]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void rzx(QIRProgramBuilder& b) { +template Value rzx(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.rzx(0.123, q[0], q[1]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void singleControlledRzx(QIRProgramBuilder& b) { +template Value singleControlledRzx(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.crzx(0.123, q[0], q[1], q[2]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void multipleControlledRzx(QIRProgramBuilder& b) { +template Value multipleControlledRzx(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(4); b.mcrzx(0.123, {q[0], q[1]}, q[2], q[3]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void rzz(QIRProgramBuilder& b) { +template Value rzz(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.rzz(0.123, q[0], q[1]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void singleControlledRzz(QIRProgramBuilder& b) { +template Value singleControlledRzz(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.crzz(0.123, q[0], q[1], q[2]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void multipleControlledRzz(QIRProgramBuilder& b) { +template Value multipleControlledRzz(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(4); b.mcrzz(0.123, {q[0], q[1]}, q[2], q[3]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void xxPlusYY(QIRProgramBuilder& b) { +template Value xxPlusYY(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.xx_plus_yy(0.123, 0.456, q[0], q[1]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void singleControlledXxPlusYY(QIRProgramBuilder& b) { +template +Value singleControlledXxPlusYY(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.cxx_plus_yy(0.123, 0.456, q[0], q[1], q[2]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void multipleControlledXxPlusYY(QIRProgramBuilder& b) { +template +Value multipleControlledXxPlusYY(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(4); b.mcxx_plus_yy(0.123, 0.456, {q[0], q[1]}, q[2], q[3]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void xxMinusYY(QIRProgramBuilder& b) { +template Value xxMinusYY(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.xx_minus_yy(0.123, 0.456, q[0], q[1]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void singleControlledXxMinusYY(QIRProgramBuilder& b) { +template +Value singleControlledXxMinusYY(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(3); b.cxx_minus_yy(0.123, 0.456, q[0], q[1], q[2]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void multipleControlledXxMinusYY(QIRProgramBuilder& b) { +template +Value multipleControlledXxMinusYY(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(4); b.mcxx_minus_yy(0.123, 0.456, {q[0], q[1]}, q[2], q[3]); + return measureAndRecord(b, q.qubits, IntoRegister); } -void simpleIf(QIRProgramBuilder& b) { +template Value simpleIf(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.h(q[0]); auto cond = b.measure(q[0], 0); b.scfIf(cond, [&] { b.x(q[0]); }); + return measureAndRecord(b, q.qubits, IntoRegister, 1); } -void ifElse(QIRProgramBuilder& b) { +template Value ifElse(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.h(q[0]); auto cond = b.measure(q[0], 0); b.scfIf(cond, [&] { b.x(q[0]); }, [&] { b.z(q[0]); }); + return measureAndRecord(b, q.qubits, IntoRegister, 1); } -void ifTwoQubits(QIRProgramBuilder& b) { +template Value ifTwoQubits(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.h(q[0]); auto cond = b.measure(q[0], 0); @@ -637,13 +841,14 @@ void ifTwoQubits(QIRProgramBuilder& b) { b.x(q[0]); b.x(q[1]); }); + return measureAndRecord(b, q.qubits, IntoRegister, 1); } -void nestedIfOpForLoop(QIRProgramBuilder& b) { +template Value nestedIfOpForLoop(QIRProgramBuilder& b) { auto reg = b.allocQubitRegister(3); auto q0 = b.allocQubit(); b.h(q0); - auto cond = b.measure(q0, 0); + auto cond = b.measure(q0, 0, false); b.scfIf( cond, [&] { b.h(q0); }, [&] { @@ -652,50 +857,55 @@ void nestedIfOpForLoop(QIRProgramBuilder& b) { b.h(q1); }); }); + return measureAndRecord(b, {q0}, IntoRegister, 1); } -void simpleWhileReset(QIRProgramBuilder& b) { +template Value simpleWhileReset(QIRProgramBuilder& b) { auto q = b.allocQubit(); b.h(q); b.scfWhile( [&] { - auto measureResult = b.measure(q, 0); + auto measureResult = b.measure(q, 0, false); return measureResult; }, [&] { b.h(q); }); + return measureAndRecord(b, {q}, IntoRegister, 1); } -void simpleDoWhileReset(QIRProgramBuilder& b) { +template Value simpleDoWhileReset(QIRProgramBuilder& b) { auto q = b.allocQubit(); b.scfWhile([&] { b.h(q); - auto measureResult = b.measure(q, 0); + auto measureResult = b.measure(q, 0, false); return measureResult; }); + return measureAndRecord(b, {q}, IntoRegister, 1); } -void simpleForLoop(QIRProgramBuilder& b) { +template Value simpleForLoop(QIRProgramBuilder& b) { auto reg = b.allocQubitRegister(2); b.scfFor(0, 2, 1, [&](Value iv) { auto q = b.load(reg.value, iv); b.h(q); }); + return measureAndRecord(b, reg.qubits, IntoRegister); }; -void nestedForLoopIfOp(QIRProgramBuilder& b) { +template Value nestedForLoopIfOp(QIRProgramBuilder& b) { auto reg = b.allocQubitRegister(2); auto qCond = b.allocQubit(); b.scfFor(0, 2, 1, [&](Value iv) { b.h(qCond); - auto cond = b.measure(qCond, 0); + auto cond = b.measure(qCond, 0, false); b.scfIf(cond, [&] { auto q = b.load(reg.value, iv); b.h(q); }); }); + return measureAndRecord(b, {qCond}, IntoRegister, 1); } -void nestedForLoopWhileOp(QIRProgramBuilder& b) { +template Value nestedForLoopWhileOp(QIRProgramBuilder& b) { auto reg = b.allocQubitRegister(2); b.scfFor(0, 2, 1, [&](Value iv) { auto q = b.load(reg.value, iv); @@ -705,14 +915,16 @@ void nestedForLoopWhileOp(QIRProgramBuilder& b) { auto q = b.load(reg.value, iv); b.scfWhile( [&] { - auto measureResult = b.measure(q, 0); + auto measureResult = b.measure(q, 0, false); return measureResult; }, [&] { b.h(q); }); }); + return measureAndRecord(b, reg.qubits, IntoRegister, 1); } -void nestedForLoopCtrlOpWithSeparateQubit(QIRProgramBuilder& b) { +template +Value nestedForLoopCtrlOpWithSeparateQubit(QIRProgramBuilder& b) { auto reg = b.allocQubitRegister(3); auto control = b.allocQubit(); b.h(control); @@ -721,9 +933,11 @@ void nestedForLoopCtrlOpWithSeparateQubit(QIRProgramBuilder& b) { b.h(q0); b.cx(control, q0); }); + return measureAndRecord(b, {control}, IntoRegister); } -void nestedForLoopCtrlOpWithExtractedQubit(QIRProgramBuilder& b) { +template +Value nestedForLoopCtrlOpWithExtractedQubit(QIRProgramBuilder& b) { auto reg = b.allocQubitRegister(4); b.h(reg[0]); b.scfFor(1, 4, 1, [&](Value iv) { @@ -731,12 +945,262 @@ void nestedForLoopCtrlOpWithExtractedQubit(QIRProgramBuilder& b) { b.h(q0); b.cx(reg[0], q0); }); + return measureAndRecord(b, {reg[0]}, IntoRegister); } -void ctrlTwo(QIRProgramBuilder& b) { +template Value ctrlTwo(QIRProgramBuilder& b) { auto q = b.allocQubitRegister(4); b.mcx({q[0], q[1]}, q[2]); b.mcrxx(0.123, {q[0], q[1]}, q[2], q[3]); -} - + return measureAndRecord(b, q.qubits, IntoRegister); +} + +// Instantiate the templates for IntoRegister = false +template Value emptyQIR(QIRProgramBuilder& builder); +template Value allocQubit(QIRProgramBuilder& b); +template Value alloc1QubitRegister(QIRProgramBuilder& b); +template Value allocQubitRegister(QIRProgramBuilder& b); +template Value alloc3QubitRegister(QIRProgramBuilder& b); +template Value allocMultipleQubitRegisters(QIRProgramBuilder& b); +template Value allocMultipleQubitRegistersWithOps(QIRProgramBuilder& b); +template Value allocLargeRegister(QIRProgramBuilder& b); +template Value singleMeasurementToSingleBit(QIRProgramBuilder& b); +template Value repeatedMeasurementToSameBit(QIRProgramBuilder& b); +template Value repeatedMeasurementToDifferentBits(QIRProgramBuilder& b); +template Value +multipleClassicalRegistersAndMeasurements(QIRProgramBuilder& b); +template Value measurementWithoutRegisters(QIRProgramBuilder& b); +template Value resetQubitWithoutOp(QIRProgramBuilder& b); +template Value resetMultipleQubitsWithoutOp(QIRProgramBuilder& b); +template Value repeatedResetWithoutOp(QIRProgramBuilder& b); +template Value resetQubitAfterSingleOp(QIRProgramBuilder& b); +template Value resetMultipleQubitsAfterSingleOp(QIRProgramBuilder& b); +template Value repeatedResetAfterSingleOp(QIRProgramBuilder& b); +template Value globalPhase(QIRProgramBuilder& b); +template Value identity(QIRProgramBuilder& b); +template Value singleControlledIdentity(QIRProgramBuilder& b); +template Value twoQubitsOneIdentity(QIRProgramBuilder& b); +template Value threeQubitsOneIdentity(QIRProgramBuilder& b); +template Value multipleControlledIdentity(QIRProgramBuilder& b); +template Value x(QIRProgramBuilder& b); +template Value singleControlledX(QIRProgramBuilder& b); +template Value multipleControlledX(QIRProgramBuilder& b); +template Value y(QIRProgramBuilder& b); +template Value singleControlledY(QIRProgramBuilder& b); +template Value multipleControlledY(QIRProgramBuilder& b); +template Value z(QIRProgramBuilder& b); +template Value singleControlledZ(QIRProgramBuilder& b); +template Value multipleControlledZ(QIRProgramBuilder& b); +template Value h(QIRProgramBuilder& b); +template Value singleControlledH(QIRProgramBuilder& b); +template Value multipleControlledH(QIRProgramBuilder& b); +template Value hWithoutRegister(QIRProgramBuilder& b); +template Value s(QIRProgramBuilder& b); +template Value singleControlledS(QIRProgramBuilder& b); +template Value multipleControlledS(QIRProgramBuilder& b); +template Value sdg(QIRProgramBuilder& b); +template Value singleControlledSdg(QIRProgramBuilder& b); +template Value multipleControlledSdg(QIRProgramBuilder& b); +template Value t_(QIRProgramBuilder& b); +template Value singleControlledT(QIRProgramBuilder& b); +template Value multipleControlledT(QIRProgramBuilder& b); +template Value tdg(QIRProgramBuilder& b); +template Value singleControlledTdg(QIRProgramBuilder& b); +template Value multipleControlledTdg(QIRProgramBuilder& b); +template Value sx(QIRProgramBuilder& b); +template Value singleControlledSx(QIRProgramBuilder& b); +template Value multipleControlledSx(QIRProgramBuilder& b); +template Value sxdg(QIRProgramBuilder& b); +template Value singleControlledSxdg(QIRProgramBuilder& b); +template Value multipleControlledSxdg(QIRProgramBuilder& b); +template Value rx(QIRProgramBuilder& b); +template Value singleControlledRx(QIRProgramBuilder& b); +template Value multipleControlledRx(QIRProgramBuilder& b); +template Value ry(QIRProgramBuilder& b); +template Value singleControlledRy(QIRProgramBuilder& b); +template Value multipleControlledRy(QIRProgramBuilder& b); +template Value rz(QIRProgramBuilder& b); +template Value singleControlledRz(QIRProgramBuilder& b); +template Value multipleControlledRz(QIRProgramBuilder& b); +template Value p(QIRProgramBuilder& b); +template Value singleControlledP(QIRProgramBuilder& b); +template Value multipleControlledP(QIRProgramBuilder& b); +template Value r(QIRProgramBuilder& b); +template Value singleControlledR(QIRProgramBuilder& b); +template Value multipleControlledR(QIRProgramBuilder& b); +template Value u2(QIRProgramBuilder& b); +template Value singleControlledU2(QIRProgramBuilder& b); +template Value multipleControlledU2(QIRProgramBuilder& b); +template Value u(QIRProgramBuilder& b); +template Value singleControlledU(QIRProgramBuilder& b); +template Value multipleControlledU(QIRProgramBuilder& b); +template Value swap(QIRProgramBuilder& b); +template Value singleControlledSwap(QIRProgramBuilder& b); +template Value multipleControlledSwap(QIRProgramBuilder& b); +template Value iswap(QIRProgramBuilder& b); +template Value singleControlledIswap(QIRProgramBuilder& b); +template Value multipleControlledIswap(QIRProgramBuilder& b); +template Value dcx(QIRProgramBuilder& b); +template Value singleControlledDcx(QIRProgramBuilder& b); +template Value multipleControlledDcx(QIRProgramBuilder& b); +template Value ecr(QIRProgramBuilder& b); +template Value singleControlledEcr(QIRProgramBuilder& b); +template Value multipleControlledEcr(QIRProgramBuilder& b); +template Value rxx(QIRProgramBuilder& b); +template Value singleControlledRxx(QIRProgramBuilder& b); +template Value multipleControlledRxx(QIRProgramBuilder& b); +template Value tripleControlledRxx(QIRProgramBuilder& b); +template Value ryy(QIRProgramBuilder& b); +template Value singleControlledRyy(QIRProgramBuilder& b); +template Value multipleControlledRyy(QIRProgramBuilder& b); +template Value rzx(QIRProgramBuilder& b); +template Value singleControlledRzx(QIRProgramBuilder& b); +template Value multipleControlledRzx(QIRProgramBuilder& b); +template Value rzz(QIRProgramBuilder& b); +template Value singleControlledRzz(QIRProgramBuilder& b); +template Value multipleControlledRzz(QIRProgramBuilder& b); +template Value xxPlusYY(QIRProgramBuilder& b); +template Value singleControlledXxPlusYY(QIRProgramBuilder& b); +template Value multipleControlledXxPlusYY(QIRProgramBuilder& b); +template Value xxMinusYY(QIRProgramBuilder& b); +template Value singleControlledXxMinusYY(QIRProgramBuilder& b); +template Value multipleControlledXxMinusYY(QIRProgramBuilder& b); +template Value simpleIf(QIRProgramBuilder& b); +template Value ifElse(QIRProgramBuilder& b); +template Value ifTwoQubits(QIRProgramBuilder& b); +template Value nestedIfOpForLoop(QIRProgramBuilder& b); +template Value simpleWhileReset(QIRProgramBuilder& b); +template Value simpleDoWhileReset(QIRProgramBuilder& b); +template Value simpleForLoop(QIRProgramBuilder& b); +template Value nestedForLoopIfOp(QIRProgramBuilder& b); +template Value nestedForLoopWhileOp(QIRProgramBuilder& b); +template Value +nestedForLoopCtrlOpWithSeparateQubit(QIRProgramBuilder& b); +template Value +nestedForLoopCtrlOpWithExtractedQubit(QIRProgramBuilder& b); +template Value ctrlTwo(QIRProgramBuilder& b); + +// Instantiate the templates for IntoRegister = true +template Value emptyQIR(QIRProgramBuilder& builder); +template Value allocQubit(QIRProgramBuilder& b); +template Value alloc1QubitRegister(QIRProgramBuilder& b); +template Value allocQubitRegister(QIRProgramBuilder& b); +template Value alloc3QubitRegister(QIRProgramBuilder& b); +template Value allocMultipleQubitRegisters(QIRProgramBuilder& b); +template Value allocMultipleQubitRegistersWithOps(QIRProgramBuilder& b); +template Value allocLargeRegister(QIRProgramBuilder& b); +template Value singleMeasurementToSingleBit(QIRProgramBuilder& b); +template Value repeatedMeasurementToSameBit(QIRProgramBuilder& b); +template Value repeatedMeasurementToDifferentBits(QIRProgramBuilder& b); +template Value +multipleClassicalRegistersAndMeasurements(QIRProgramBuilder& b); +template Value measurementWithoutRegisters(QIRProgramBuilder& b); +template Value resetQubitWithoutOp(QIRProgramBuilder& b); +template Value resetMultipleQubitsWithoutOp(QIRProgramBuilder& b); +template Value repeatedResetWithoutOp(QIRProgramBuilder& b); +template Value resetQubitAfterSingleOp(QIRProgramBuilder& b); +template Value resetMultipleQubitsAfterSingleOp(QIRProgramBuilder& b); +template Value repeatedResetAfterSingleOp(QIRProgramBuilder& b); +template Value globalPhase(QIRProgramBuilder& b); +template Value identity(QIRProgramBuilder& b); +template Value singleControlledIdentity(QIRProgramBuilder& b); +template Value twoQubitsOneIdentity(QIRProgramBuilder& b); +template Value threeQubitsOneIdentity(QIRProgramBuilder& b); +template Value multipleControlledIdentity(QIRProgramBuilder& b); +template Value x(QIRProgramBuilder& b); +template Value singleControlledX(QIRProgramBuilder& b); +template Value multipleControlledX(QIRProgramBuilder& b); +template Value y(QIRProgramBuilder& b); +template Value singleControlledY(QIRProgramBuilder& b); +template Value multipleControlledY(QIRProgramBuilder& b); +template Value z(QIRProgramBuilder& b); +template Value singleControlledZ(QIRProgramBuilder& b); +template Value multipleControlledZ(QIRProgramBuilder& b); +template Value h(QIRProgramBuilder& b); +template Value singleControlledH(QIRProgramBuilder& b); +template Value multipleControlledH(QIRProgramBuilder& b); +template Value hWithoutRegister(QIRProgramBuilder& b); +template Value s(QIRProgramBuilder& b); +template Value singleControlledS(QIRProgramBuilder& b); +template Value multipleControlledS(QIRProgramBuilder& b); +template Value sdg(QIRProgramBuilder& b); +template Value singleControlledSdg(QIRProgramBuilder& b); +template Value multipleControlledSdg(QIRProgramBuilder& b); +template Value t_(QIRProgramBuilder& b); +template Value singleControlledT(QIRProgramBuilder& b); +template Value multipleControlledT(QIRProgramBuilder& b); +template Value tdg(QIRProgramBuilder& b); +template Value singleControlledTdg(QIRProgramBuilder& b); +template Value multipleControlledTdg(QIRProgramBuilder& b); +template Value sx(QIRProgramBuilder& b); +template Value singleControlledSx(QIRProgramBuilder& b); +template Value multipleControlledSx(QIRProgramBuilder& b); +template Value sxdg(QIRProgramBuilder& b); +template Value singleControlledSxdg(QIRProgramBuilder& b); +template Value multipleControlledSxdg(QIRProgramBuilder& b); +template Value rx(QIRProgramBuilder& b); +template Value singleControlledRx(QIRProgramBuilder& b); +template Value multipleControlledRx(QIRProgramBuilder& b); +template Value ry(QIRProgramBuilder& b); +template Value singleControlledRy(QIRProgramBuilder& b); +template Value multipleControlledRy(QIRProgramBuilder& b); +template Value rz(QIRProgramBuilder& b); +template Value singleControlledRz(QIRProgramBuilder& b); +template Value multipleControlledRz(QIRProgramBuilder& b); +template Value p(QIRProgramBuilder& b); +template Value singleControlledP(QIRProgramBuilder& b); +template Value multipleControlledP(QIRProgramBuilder& b); +template Value r(QIRProgramBuilder& b); +template Value singleControlledR(QIRProgramBuilder& b); +template Value multipleControlledR(QIRProgramBuilder& b); +template Value u2(QIRProgramBuilder& b); +template Value singleControlledU2(QIRProgramBuilder& b); +template Value multipleControlledU2(QIRProgramBuilder& b); +template Value u(QIRProgramBuilder& b); +template Value singleControlledU(QIRProgramBuilder& b); +template Value multipleControlledU(QIRProgramBuilder& b); +template Value swap(QIRProgramBuilder& b); +template Value singleControlledSwap(QIRProgramBuilder& b); +template Value multipleControlledSwap(QIRProgramBuilder& b); +template Value iswap(QIRProgramBuilder& b); +template Value singleControlledIswap(QIRProgramBuilder& b); +template Value multipleControlledIswap(QIRProgramBuilder& b); +template Value dcx(QIRProgramBuilder& b); +template Value singleControlledDcx(QIRProgramBuilder& b); +template Value multipleControlledDcx(QIRProgramBuilder& b); +template Value ecr(QIRProgramBuilder& b); +template Value singleControlledEcr(QIRProgramBuilder& b); +template Value multipleControlledEcr(QIRProgramBuilder& b); +template Value rxx(QIRProgramBuilder& b); +template Value singleControlledRxx(QIRProgramBuilder& b); +template Value multipleControlledRxx(QIRProgramBuilder& b); +template Value tripleControlledRxx(QIRProgramBuilder& b); +template Value ryy(QIRProgramBuilder& b); +template Value singleControlledRyy(QIRProgramBuilder& b); +template Value multipleControlledRyy(QIRProgramBuilder& b); +template Value rzx(QIRProgramBuilder& b); +template Value singleControlledRzx(QIRProgramBuilder& b); +template Value multipleControlledRzx(QIRProgramBuilder& b); +template Value rzz(QIRProgramBuilder& b); +template Value singleControlledRzz(QIRProgramBuilder& b); +template Value multipleControlledRzz(QIRProgramBuilder& b); +template Value xxPlusYY(QIRProgramBuilder& b); +template Value singleControlledXxPlusYY(QIRProgramBuilder& b); +template Value multipleControlledXxPlusYY(QIRProgramBuilder& b); +template Value xxMinusYY(QIRProgramBuilder& b); +template Value singleControlledXxMinusYY(QIRProgramBuilder& b); +template Value multipleControlledXxMinusYY(QIRProgramBuilder& b); +template Value simpleIf(QIRProgramBuilder& b); +template Value ifElse(QIRProgramBuilder& b); +template Value ifTwoQubits(QIRProgramBuilder& b); +template Value nestedIfOpForLoop(QIRProgramBuilder& b); +template Value simpleWhileReset(QIRProgramBuilder& b); +template Value simpleDoWhileReset(QIRProgramBuilder& b); +template Value simpleForLoop(QIRProgramBuilder& b); +template Value nestedForLoopIfOp(QIRProgramBuilder& b); +template Value nestedForLoopWhileOp(QIRProgramBuilder& b); +template Value nestedForLoopCtrlOpWithSeparateQubit(QIRProgramBuilder& b); +template Value +nestedForLoopCtrlOpWithExtractedQubit(QIRProgramBuilder& b); +template Value ctrlTwo(QIRProgramBuilder& b); } // namespace mlir::qir diff --git a/mlir/unittests/programs/qir_programs.h b/mlir/unittests/programs/qir_programs.h index afa3daaef6..b58a3ff396 100644 --- a/mlir/unittests/programs/qir_programs.h +++ b/mlir/unittests/programs/qir_programs.h @@ -10,468 +10,567 @@ #pragma once +#include + namespace mlir::qir { class QIRProgramBuilder; /// Creates an empty QIR program. -void emptyQIR(QIRProgramBuilder& builder); +template Value emptyQIR(QIRProgramBuilder& builder); // --- Qubit Management ----------------------------------------------------- // /// Allocates a single qubit. -void allocQubit(QIRProgramBuilder& b); +template Value allocQubit(QIRProgramBuilder& b); + +/// Allocates a qubit register of size `1`. +template +Value alloc1QubitRegister(QIRProgramBuilder& b); /// Allocates a qubit register of size `2`. -void allocQubitRegister(QIRProgramBuilder& b); +template +Value allocQubitRegister(QIRProgramBuilder& b); + +/// Allocates a qubit register of size `3`. +template +Value alloc3QubitRegister(QIRProgramBuilder& b); /// Allocates two qubit registers of size `2` and `3`. -void allocMultipleQubitRegisters(QIRProgramBuilder& b); +template +Value allocMultipleQubitRegisters(QIRProgramBuilder& b); /// Allocates two qubit registers of size `2` and `3` and applies operations. -void allocMultipleQubitRegistersWithOps(QIRProgramBuilder& b); +template +Value allocMultipleQubitRegistersWithOps(QIRProgramBuilder& b); /// Allocates a large qubit register. -void allocLargeRegister(QIRProgramBuilder& b); +template +Value allocLargeRegister(QIRProgramBuilder& b); /// Allocates two inline qubits. -void staticQubits(QIRProgramBuilder& b); +Value staticQubits(QIRProgramBuilder& b); /// Allocates two static qubits and applies operations. -void staticQubitsWithOps(QIRProgramBuilder& b); +Value staticQubitsWithOps(QIRProgramBuilder& b); /// Allocates two static qubits and applies parametric gates. -void staticQubitsWithParametricOps(QIRProgramBuilder& b); +Value staticQubitsWithParametricOps(QIRProgramBuilder& b); /// Allocates two static qubits and applies a two-target gate. -void staticQubitsWithTwoTargetOps(QIRProgramBuilder& b); +Value staticQubitsWithTwoTargetOps(QIRProgramBuilder& b); /// Allocates two static qubits and applies a controlled gate. -void staticQubitsWithCtrl(QIRProgramBuilder& b); +Value staticQubitsWithCtrl(QIRProgramBuilder& b); /// Allocates a static qubit and applies the inverse of a T gate (Tdg). -void staticQubitsWithInv(QIRProgramBuilder& b); +Value staticQubitsWithInv(QIRProgramBuilder& b); /// Allocates duplicate static qubits and applies operations on both. -void staticQubitsWithDuplicates(QIRProgramBuilder& b); +Value staticQubitsWithDuplicates(QIRProgramBuilder& b); /// Same as `staticQubitsWithDuplicates`, but with canonical static qubit /// retrievals. -void staticQubitsCanonical(QIRProgramBuilder& b); +Value staticQubitsCanonical(QIRProgramBuilder& b); // --- Invalid / mixed addressing (unit tests) -------------------------------- /// @pre `builder.initialize()`. Fatal mixed addressing: static then dynamic /// alloc. -void mixedStaticThenDynamicQubit(QIRProgramBuilder& b); +Value mixedStaticThenDynamicQubit(QIRProgramBuilder& b); /// @pre `builder.initialize()`. Fatal mixed addressing: dynamic register then /// static. -void mixedDynamicRegisterThenStaticQubit(QIRProgramBuilder& b); +Value mixedDynamicRegisterThenStaticQubit(QIRProgramBuilder& b); // --- MeasureOp ------------------------------------------------------------ // /// Measures a single qubit into a single classical bit. -void singleMeasurementToSingleBit(QIRProgramBuilder& b); +template +Value singleMeasurementToSingleBit(QIRProgramBuilder& b); /// Repeatedly measures a single qubit into the same classical bit. -void repeatedMeasurementToSameBit(QIRProgramBuilder& b); +template +Value repeatedMeasurementToSameBit(QIRProgramBuilder& b); /// Repeatedly measures a single qubit into different classical bits. -void repeatedMeasurementToDifferentBits(QIRProgramBuilder& b); +template +Value repeatedMeasurementToDifferentBits(QIRProgramBuilder& b); /// Measures multiple qubits into multiple classical bits. -void multipleClassicalRegistersAndMeasurements(QIRProgramBuilder& b); +template +Value multipleClassicalRegistersAndMeasurements(QIRProgramBuilder& b); /// Measures a single qubit into a single classical bit, without explicitly /// allocating a quantum or classical register. -void measurementWithoutRegisters(QIRProgramBuilder& b); +template +Value measurementWithoutRegisters(QIRProgramBuilder& b); // --- ResetOp -------------------------------------------------------------- // /// Resets a single qubit without any operations being applied. -void resetQubitWithoutOp(QIRProgramBuilder& b); +template +Value resetQubitWithoutOp(QIRProgramBuilder& b); /// Resets multiple qubits without any operations being applied. -void resetMultipleQubitsWithoutOp(QIRProgramBuilder& b); +template +Value resetMultipleQubitsWithoutOp(QIRProgramBuilder& b); /// Repeatedly resets a single qubit without any operations being applied. -void repeatedResetWithoutOp(QIRProgramBuilder& b); +template +Value repeatedResetWithoutOp(QIRProgramBuilder& b); /// Resets a single qubit after a single operation. -void resetQubitAfterSingleOp(QIRProgramBuilder& b); +template +Value resetQubitAfterSingleOp(QIRProgramBuilder& b); /// Resets multiple qubits after a single operation. -void resetMultipleQubitsAfterSingleOp(QIRProgramBuilder& b); +template +Value resetMultipleQubitsAfterSingleOp(QIRProgramBuilder& b); /// Repeatedly resets a single qubit after a single operation. -void repeatedResetAfterSingleOp(QIRProgramBuilder& b); +template +Value repeatedResetAfterSingleOp(QIRProgramBuilder& b); // --- GPhaseOp ------------------------------------------------------------- // /// Creates a circuit with just a global phase. -void globalPhase(QIRProgramBuilder& b); +template Value globalPhase(QIRProgramBuilder& b); // --- IdOp ----------------------------------------------------------------- // /// Creates a circuit with just an identity gate. -void identity(QIRProgramBuilder& b); +template Value identity(QIRProgramBuilder& b); /// Creates a controlled identity gate with a single control qubit. -void singleControlledIdentity(QIRProgramBuilder& b); +template +Value singleControlledIdentity(QIRProgramBuilder& b); + +/// Creates an identity gate on a single qubit in a two-qubit register. +template +Value twoQubitsOneIdentity(QIRProgramBuilder& b); + +/// Creates an identity gate on a single qubit in a three-qubit register. +template +Value threeQubitsOneIdentity(QIRProgramBuilder& b); /// Creates a multi-controlled identity gate with multiple control qubits. -void multipleControlledIdentity(QIRProgramBuilder& b); +template +Value multipleControlledIdentity(QIRProgramBuilder& b); // --- XOp ------------------------------------------------------------------ // /// Creates a circuit with just an X gate. -void x(QIRProgramBuilder& b); +template Value x(QIRProgramBuilder& b); /// Creates a circuit with a single controlled X gate. -void singleControlledX(QIRProgramBuilder& b); +template +Value singleControlledX(QIRProgramBuilder& b); /// Creates a circuit with a multi-controlled X gate. -void multipleControlledX(QIRProgramBuilder& b); +template +Value multipleControlledX(QIRProgramBuilder& b); // --- YOp ------------------------------------------------------------------ // /// Creates a circuit with just a Y gate. -void y(QIRProgramBuilder& b); +template Value y(QIRProgramBuilder& b); /// Creates a circuit with a single controlled Y gate. -void singleControlledY(QIRProgramBuilder& b); +template +Value singleControlledY(QIRProgramBuilder& b); /// Creates a circuit with a multi-controlled Y gate. -void multipleControlledY(QIRProgramBuilder& b); +template +Value multipleControlledY(QIRProgramBuilder& b); // --- ZOp ------------------------------------------------------------------ // /// Creates a circuit with just a Z gate. -void z(QIRProgramBuilder& b); +template Value z(QIRProgramBuilder& b); /// Creates a circuit with a single controlled Z gate. -void singleControlledZ(QIRProgramBuilder& b); +template +Value singleControlledZ(QIRProgramBuilder& b); /// Creates a circuit with a multi-controlled Z gate. -void multipleControlledZ(QIRProgramBuilder& b); +template +Value multipleControlledZ(QIRProgramBuilder& b); // --- HOp ------------------------------------------------------------------ // /// Creates a circuit with just an H gate. -void h(QIRProgramBuilder& b); +template Value h(QIRProgramBuilder& b); /// Creates a circuit with a single controlled H gate. -void singleControlledH(QIRProgramBuilder& b); +template +Value singleControlledH(QIRProgramBuilder& b); /// Creates a circuit with a multi-controlled H gate. -void multipleControlledH(QIRProgramBuilder& b); +template +Value multipleControlledH(QIRProgramBuilder& b); /// Creates a circuit with just an H gate and no qubit register. -void hWithoutRegister(QIRProgramBuilder& b); +template +Value hWithoutRegister(QIRProgramBuilder& b); // --- SOp ------------------------------------------------------------------ // /// Creates a circuit with just an S gate. -void s(QIRProgramBuilder& b); +template Value s(QIRProgramBuilder& b); /// Creates a circuit with a single controlled S gate. -void singleControlledS(QIRProgramBuilder& b); +template +Value singleControlledS(QIRProgramBuilder& b); /// Creates a circuit with a multi-controlled S gate. -void multipleControlledS(QIRProgramBuilder& b); +template +Value multipleControlledS(QIRProgramBuilder& b); // --- SdgOp ---------------------------------------------------------------- // /// Creates a circuit with just an Sdg gate. -void sdg(QIRProgramBuilder& b); +template Value sdg(QIRProgramBuilder& b); /// Creates a circuit with a single controlled Sdg gate. -void singleControlledSdg(QIRProgramBuilder& b); +template +Value singleControlledSdg(QIRProgramBuilder& b); /// Creates a circuit with a multi-controlled Sdg gate. -void multipleControlledSdg(QIRProgramBuilder& b); +template +Value multipleControlledSdg(QIRProgramBuilder& b); // --- TOp ------------------------------------------------------------------ // /// Creates a circuit with just a T gate. -void t_(QIRProgramBuilder& b); // NOLINT(*-identifier-naming) +template +Value t_(QIRProgramBuilder& b); // NOLINT(*-identifier-naming) /// Creates a circuit with a single controlled T gate. -void singleControlledT(QIRProgramBuilder& b); +template +Value singleControlledT(QIRProgramBuilder& b); /// Creates a circuit with a multi-controlled T gate. -void multipleControlledT(QIRProgramBuilder& b); +template +Value multipleControlledT(QIRProgramBuilder& b); // --- TdgOp ---------------------------------------------------------------- // /// Creates a circuit with just a Tdg gate. -void tdg(QIRProgramBuilder& b); +template Value tdg(QIRProgramBuilder& b); /// Creates a circuit with a single controlled Tdg gate. -void singleControlledTdg(QIRProgramBuilder& b); +template +Value singleControlledTdg(QIRProgramBuilder& b); /// Creates a circuit with a multi-controlled Tdg gate. -void multipleControlledTdg(QIRProgramBuilder& b); +template +Value multipleControlledTdg(QIRProgramBuilder& b); // --- SXOp ----------------------------------------------------------------- // /// Creates a circuit with just an SX gate. -void sx(QIRProgramBuilder& b); +template Value sx(QIRProgramBuilder& b); /// Creates a circuit with a single controlled SX gate. -void singleControlledSx(QIRProgramBuilder& b); +template +Value singleControlledSx(QIRProgramBuilder& b); /// Creates a circuit with a multi-controlled SX gate. -void multipleControlledSx(QIRProgramBuilder& b); +template +Value multipleControlledSx(QIRProgramBuilder& b); // --- SXdgOp --------------------------------------------------------------- // /// Creates a circuit with just an SXdg gate. -void sxdg(QIRProgramBuilder& b); +template Value sxdg(QIRProgramBuilder& b); /// Creates a circuit with a single controlled SXdg gate. -void singleControlledSxdg(QIRProgramBuilder& b); +template +Value singleControlledSxdg(QIRProgramBuilder& b); /// Creates a circuit with a multi-controlled SXdg gate. -void multipleControlledSxdg(QIRProgramBuilder& b); +template +Value multipleControlledSxdg(QIRProgramBuilder& b); // --- RXOp ----------------------------------------------------------------- // /// Creates a circuit with just an RX gate. -void rx(QIRProgramBuilder& b); +template Value rx(QIRProgramBuilder& b); /// Creates a circuit with a single controlled RX gate. -void singleControlledRx(QIRProgramBuilder& b); +template +Value singleControlledRx(QIRProgramBuilder& b); /// Creates a circuit with a multi-controlled RX gate. -void multipleControlledRx(QIRProgramBuilder& b); +template +Value multipleControlledRx(QIRProgramBuilder& b); // --- RYOp ----------------------------------------------------------------- // /// Creates a circuit with just an RY gate. -void ry(QIRProgramBuilder& b); +template Value ry(QIRProgramBuilder& b); /// Creates a circuit with a single controlled RY gate. -void singleControlledRy(QIRProgramBuilder& b); +template +Value singleControlledRy(QIRProgramBuilder& b); /// Creates a circuit with a multi-controlled RY gate. -void multipleControlledRy(QIRProgramBuilder& b); +template +Value multipleControlledRy(QIRProgramBuilder& b); // --- RZOp ----------------------------------------------------------------- // /// Creates a circuit with just an RZ gate. -void rz(QIRProgramBuilder& b); +template Value rz(QIRProgramBuilder& b); /// Creates a circuit with a single controlled RZ gate. -void singleControlledRz(QIRProgramBuilder& b); +template +Value singleControlledRz(QIRProgramBuilder& b); /// Creates a circuit with a multi-controlled RZ gate. -void multipleControlledRz(QIRProgramBuilder& b); +template +Value multipleControlledRz(QIRProgramBuilder& b); // --- POp ------------------------------------------------------------------ // /// Creates a circuit with just a P gate. -void p(QIRProgramBuilder& b); +template Value p(QIRProgramBuilder& b); /// Creates a circuit with a single controlled P gate. -void singleControlledP(QIRProgramBuilder& b); +template +Value singleControlledP(QIRProgramBuilder& b); /// Creates a circuit with a multi-controlled P gate. -void multipleControlledP(QIRProgramBuilder& b); +template +Value multipleControlledP(QIRProgramBuilder& b); // --- ROp ------------------------------------------------------------------ // /// Creates a circuit with just an R gate. -void r(QIRProgramBuilder& b); +template Value r(QIRProgramBuilder& b); /// Creates a circuit with a single controlled R gate. -void singleControlledR(QIRProgramBuilder& b); +template +Value singleControlledR(QIRProgramBuilder& b); /// Creates a circuit with a multi-controlled R gate. -void multipleControlledR(QIRProgramBuilder& b); +template +Value multipleControlledR(QIRProgramBuilder& b); // --- U2Op ----------------------------------------------------------------- // /// Creates a circuit with just a U2 gate. -void u2(QIRProgramBuilder& b); +template Value u2(QIRProgramBuilder& b); /// Creates a circuit with a single controlled U2 gate. -void singleControlledU2(QIRProgramBuilder& b); +template +Value singleControlledU2(QIRProgramBuilder& b); /// Creates a circuit with a multi-controlled U2 gate. -void multipleControlledU2(QIRProgramBuilder& b); +template +Value multipleControlledU2(QIRProgramBuilder& b); // --- UOp ------------------------------------------------------------------ // /// Creates a circuit with just a U gate. -void u(QIRProgramBuilder& b); +template Value u(QIRProgramBuilder& b); /// Creates a circuit with a single controlled U gate. -void singleControlledU(QIRProgramBuilder& b); +template +Value singleControlledU(QIRProgramBuilder& b); /// Creates a circuit with a multi-controlled U gate. -void multipleControlledU(QIRProgramBuilder& b); +template +Value multipleControlledU(QIRProgramBuilder& b); // --- SWAPOp --------------------------------------------------------------- // /// Creates a circuit with just a SWAP gate. -void swap(QIRProgramBuilder& b); +template Value swap(QIRProgramBuilder& b); /// Creates a circuit with a single controlled SWAP gate. -void singleControlledSwap(QIRProgramBuilder& b); +template +Value singleControlledSwap(QIRProgramBuilder& b); /// Creates a circuit with a multi-controlled SWAP gate. -void multipleControlledSwap(QIRProgramBuilder& b); +template +Value multipleControlledSwap(QIRProgramBuilder& b); // --- iSWAPOp -------------------------------------------------------------- // /// Creates a circuit with just an iSWAP gate. -void iswap(QIRProgramBuilder& b); +template Value iswap(QIRProgramBuilder& b); /// Creates a circuit with a single controlled iSWAP gate. -void singleControlledIswap(QIRProgramBuilder& b); +template +Value singleControlledIswap(QIRProgramBuilder& b); /// Creates a circuit with a multi-controlled iSWAP gate. -void multipleControlledIswap(QIRProgramBuilder& b); +template +Value multipleControlledIswap(QIRProgramBuilder& b); // --- DCXOp ---------------------------------------------------------------- // /// Creates a circuit with just a DCX gate. -void dcx(QIRProgramBuilder& b); +template Value dcx(QIRProgramBuilder& b); /// Creates a circuit with a single controlled DCX gate. -void singleControlledDcx(QIRProgramBuilder& b); +template +Value singleControlledDcx(QIRProgramBuilder& b); /// Creates a circuit with a multi-controlled DCX gate. -void multipleControlledDcx(QIRProgramBuilder& b); +template +Value multipleControlledDcx(QIRProgramBuilder& b); // --- ECROp ---------------------------------------------------------------- // /// Creates a circuit with just an ECR gate. -void ecr(QIRProgramBuilder& b); +template Value ecr(QIRProgramBuilder& b); /// Creates a circuit with a single controlled ECR gate. -void singleControlledEcr(QIRProgramBuilder& b); +template +Value singleControlledEcr(QIRProgramBuilder& b); /// Creates a circuit with a multi-controlled ECR gate. -void multipleControlledEcr(QIRProgramBuilder& b); +template +Value multipleControlledEcr(QIRProgramBuilder& b); // --- RXXOp ---------------------------------------------------------------- // /// Creates a circuit with just an RXX gate. -void rxx(QIRProgramBuilder& b); +template Value rxx(QIRProgramBuilder& b); /// Creates a circuit with a single controlled RXX gate. -void singleControlledRxx(QIRProgramBuilder& b); +template +Value singleControlledRxx(QIRProgramBuilder& b); /// Creates a circuit with a multi-controlled RXX gate. -void multipleControlledRxx(QIRProgramBuilder& b); +template +Value multipleControlledRxx(QIRProgramBuilder& b); /// Creates a circuit with a triple-controlled RXX gate. -void tripleControlledRxx(QIRProgramBuilder& b); +template +Value tripleControlledRxx(QIRProgramBuilder& b); // --- RYYOp ---------------------------------------------------------------- // /// Creates a circuit with just an RYY gate. -void ryy(QIRProgramBuilder& b); +template Value ryy(QIRProgramBuilder& b); /// Creates a circuit with a single controlled RYY gate. -void singleControlledRyy(QIRProgramBuilder& b); +template +Value singleControlledRyy(QIRProgramBuilder& b); /// Creates a circuit with a multi-controlled RYY gate. -void multipleControlledRyy(QIRProgramBuilder& b); +template +Value multipleControlledRyy(QIRProgramBuilder& b); // --- RZXOp ---------------------------------------------------------------- // /// Creates a circuit with just an RZX gate. -void rzx(QIRProgramBuilder& b); +template Value rzx(QIRProgramBuilder& b); /// Creates a circuit with a single controlled RZX gate. -void singleControlledRzx(QIRProgramBuilder& b); +template +Value singleControlledRzx(QIRProgramBuilder& b); /// Creates a circuit with a multi-controlled RZX gate. -void multipleControlledRzx(QIRProgramBuilder& b); +template +Value multipleControlledRzx(QIRProgramBuilder& b); // --- RZZOp ---------------------------------------------------------------- // /// Creates a circuit with just an RZZ gate. -void rzz(QIRProgramBuilder& b); +template Value rzz(QIRProgramBuilder& b); /// Creates a circuit with a single controlled RZZ gate. -void singleControlledRzz(QIRProgramBuilder& b); +template +Value singleControlledRzz(QIRProgramBuilder& b); /// Creates a circuit with a multi-controlled RZZ gate. -void multipleControlledRzz(QIRProgramBuilder& b); +template +Value multipleControlledRzz(QIRProgramBuilder& b); // --- XXPlusYYOp ----------------------------------------------------------- // /// Creates a circuit with just an XXPlusYY gate. -void xxPlusYY(QIRProgramBuilder& b); +template Value xxPlusYY(QIRProgramBuilder& b); /// Creates a circuit with a single controlled XXPlusYY gate. -void singleControlledXxPlusYY(QIRProgramBuilder& b); +template +Value singleControlledXxPlusYY(QIRProgramBuilder& b); /// Creates a circuit with a multi-controlled XXPlusYY gate. -void multipleControlledXxPlusYY(QIRProgramBuilder& b); +template +Value multipleControlledXxPlusYY(QIRProgramBuilder& b); // --- XXMinusYYOp ---------------------------------------------------------- // /// Creates a circuit with just an XXMinusYY gate. -void xxMinusYY(QIRProgramBuilder& b); +template Value xxMinusYY(QIRProgramBuilder& b); /// Creates a circuit with a single controlled XXMinusYY gate. -void singleControlledXxMinusYY(QIRProgramBuilder& b); +template +Value singleControlledXxMinusYY(QIRProgramBuilder& b); /// Creates a circuit with a multi-controlled XXMinusYY gate. -void multipleControlledXxMinusYY(QIRProgramBuilder& b); +template +Value multipleControlledXxMinusYY(QIRProgramBuilder& b); // --- IfOp ----------------------------------------------------------------- // /// Creates a circuit with a simple if operation with one qubit. -void simpleIf(QIRProgramBuilder& b); +template Value simpleIf(QIRProgramBuilder& b); /// Creates a circuit with an if operation with an else branch. -void ifElse(QIRProgramBuilder& b); +template Value ifElse(QIRProgramBuilder& b); /// Creates a circuit with an if operation with two qubits. -void ifTwoQubits(QIRProgramBuilder& b); +template Value ifTwoQubits(QIRProgramBuilder& b); /// Creates a circuit with an if operation with a nested for operation with /// a register. -void nestedIfOpForLoop(QIRProgramBuilder& b); +template +Value nestedIfOpForLoop(QIRProgramBuilder& b); // --- WhileOp -------------------------------------------------------------- // /// Creates a circuit with a while operation using a while loop. -void simpleWhileReset(QIRProgramBuilder& b); +template +Value simpleWhileReset(QIRProgramBuilder& b); /// Creates a circuit with a while operation using a do-while loop. -void simpleDoWhileReset(QIRProgramBuilder& b); +template +Value simpleDoWhileReset(QIRProgramBuilder& b); // --- ForOp ---------------------------------------------------------------- // /// Creates a circuit with a simple for operation with a register. -void simpleForLoop(QIRProgramBuilder& b); +template Value simpleForLoop(QIRProgramBuilder& b); /// Creates a circuit with a for operation with a register and a qubit and a /// nested if operation. -void nestedForLoopIfOp(QIRProgramBuilder& b); +template +Value nestedForLoopIfOp(QIRProgramBuilder& b); /// Creates a circuit with a for operation with a register and a nested while /// operation. -void nestedForLoopWhileOp(QIRProgramBuilder& b); +template +Value nestedForLoopWhileOp(QIRProgramBuilder& b); /// Creates a circuit with a for operation with a register and a qubit and a /// nested ctrl operation where the qubit is separately allocated from the /// register. -void nestedForLoopCtrlOpWithSeparateQubit(QIRProgramBuilder& b); +template +Value nestedForLoopCtrlOpWithSeparateQubit(QIRProgramBuilder& b); /// Creates a circuit with a for operation with a register and a qubit and a /// nested ctrl operation where the qubit is extracted from the register. -void nestedForLoopCtrlOpWithExtractedQubit(QIRProgramBuilder& b); +template +Value nestedForLoopCtrlOpWithExtractedQubit(QIRProgramBuilder& b); // --- CtrlOp --------------------------------------------------------------- // /// Creates a circuit with a control modifier applied to two gates. -void ctrlTwo(QIRProgramBuilder& b); +template Value ctrlTwo(QIRProgramBuilder& b); } // namespace mlir::qir diff --git a/mlir/unittests/programs/quantum_computation_programs.cpp b/mlir/unittests/programs/quantum_computation_programs.cpp index f0b9b305cd..d473a61bb4 100644 --- a/mlir/unittests/programs/quantum_computation_programs.cpp +++ b/mlir/unittests/programs/quantum_computation_programs.cpp @@ -22,19 +22,27 @@ namespace qc { -void allocQubit(QuantumComputation& comp) { comp.addQubitRegister(1, "q"); } +void allocQubit(QuantumComputation& comp) { + auto qr = comp.addQubitRegister(1, "q"); + comp.measureAll(true, false); +} void allocQubitRegister(QuantumComputation& comp) { comp.addQubitRegister(2, "q"); + comp.measureAll(true, false); } void allocMultipleQubitRegisters(QuantumComputation& comp) { comp.addQubitRegister(2, "reg0"); comp.addQubitRegister(3, "reg1"); + comp.measureAll(true, false); } void allocLargeRegister(QuantumComputation& comp) { comp.addQubitRegister(100, "q"); + comp.addClassicalRegister(2, "meas"); + comp.measure(0, 0); + comp.measure(99, 1); } void singleMeasurementToSingleBit(QuantumComputation& comp) { @@ -72,6 +80,7 @@ void resetQubitAfterSingleOp(QuantumComputation& comp) { comp.addQubitRegister(1, "q"); comp.h(0); comp.reset(0); + comp.measureAll(true, false); } void resetMultipleQubitsAfterSingleOp(QuantumComputation& comp) { @@ -80,6 +89,7 @@ void resetMultipleQubitsAfterSingleOp(QuantumComputation& comp) { comp.reset(0); comp.h(1); comp.reset(1); + comp.measureAll(true, false); } void repeatedResetAfterSingleOp(QuantumComputation& comp) { @@ -88,6 +98,7 @@ void repeatedResetAfterSingleOp(QuantumComputation& comp) { comp.reset(0); comp.reset(0); comp.reset(0); + comp.measureAll(true, false); } void globalPhase(QuantumComputation& comp) { comp.gphase(0.123); } @@ -95,451 +106,541 @@ void globalPhase(QuantumComputation& comp) { comp.gphase(0.123); } void identity(QuantumComputation& comp) { comp.addQubitRegister(1, "q"); comp.i(0); + comp.measureAll(true, false); } void singleControlledIdentity(QuantumComputation& comp) { comp.addQubitRegister(2, "q"); - comp.ci(1, 0); + comp.ci(0, 1); + comp.measureAll(true, false); } void multipleControlledIdentity(QuantumComputation& comp) { comp.addQubitRegister(3, "q"); - comp.mci({2, 1}, 0); + comp.mci({0, 1}, 2); + comp.measureAll(true, false); } void x(QuantumComputation& comp) { comp.addQubitRegister(1, "q"); comp.x(0); + comp.measureAll(true, false); } void singleControlledX(QuantumComputation& comp) { comp.addQubitRegister(2, "q"); comp.cx(0, 1); + comp.measureAll(true, false); } void multipleControlledX(QuantumComputation& comp) { comp.addQubitRegister(3, "q"); comp.mcx({0, 1}, 2); + comp.measureAll(true, false); } void y(QuantumComputation& comp) { comp.addQubitRegister(1, "q"); comp.y(0); + comp.measureAll(true, false); } void singleControlledY(QuantumComputation& comp) { comp.addQubitRegister(2, "q"); comp.cy(0, 1); + comp.measureAll(true, false); } void multipleControlledY(QuantumComputation& comp) { comp.addQubitRegister(3, "q"); comp.mcy({0, 1}, 2); + comp.measureAll(true, false); } void z(QuantumComputation& comp) { comp.addQubitRegister(1, "q"); comp.z(0); + comp.measureAll(true, false); } void singleControlledZ(QuantumComputation& comp) { comp.addQubitRegister(2, "q"); comp.cz(0, 1); + comp.measureAll(true, false); } void multipleControlledZ(QuantumComputation& comp) { comp.addQubitRegister(3, "q"); comp.mcz({0, 1}, 2); + comp.measureAll(true, false); } void h(QuantumComputation& comp) { comp.addQubitRegister(1, "q"); comp.h(0); + comp.measureAll(true, false); } void singleControlledH(QuantumComputation& comp) { comp.addQubitRegister(2, "q"); comp.ch(0, 1); + comp.measureAll(true, false); } void multipleControlledH(QuantumComputation& comp) { comp.addQubitRegister(3, "q"); comp.mch({0, 1}, 2); + comp.measureAll(true, false); } void s(QuantumComputation& comp) { comp.addQubitRegister(1, "q"); comp.s(0); + comp.measureAll(true, false); } void singleControlledS(QuantumComputation& comp) { comp.addQubitRegister(2, "q"); comp.cs(0, 1); + comp.measureAll(true, false); } void multipleControlledS(QuantumComputation& comp) { comp.addQubitRegister(3, "q"); comp.mcs({0, 1}, 2); + comp.measureAll(true, false); } void sdg(QuantumComputation& comp) { comp.addQubitRegister(1, "q"); comp.sdg(0); + comp.measureAll(true, false); } void singleControlledSdg(QuantumComputation& comp) { comp.addQubitRegister(2, "q"); comp.csdg(0, 1); + comp.measureAll(true, false); } void multipleControlledSdg(QuantumComputation& comp) { comp.addQubitRegister(3, "q"); comp.mcsdg({0, 1}, 2); + comp.measureAll(true, false); } void t_(QuantumComputation& comp) { comp.addQubitRegister(1, "q"); comp.t(0); + comp.measureAll(true, false); } void singleControlledT(QuantumComputation& comp) { comp.addQubitRegister(2, "q"); comp.ct(0, 1); + comp.measureAll(true, false); } void multipleControlledT(QuantumComputation& comp) { comp.addQubitRegister(3, "q"); comp.mct({0, 1}, 2); + comp.measureAll(true, false); } void tdg(QuantumComputation& comp) { comp.addQubitRegister(1, "q"); comp.tdg(0); + comp.measureAll(true, false); } void singleControlledTdg(QuantumComputation& comp) { comp.addQubitRegister(2, "q"); comp.ctdg(0, 1); + comp.measureAll(true, false); } void multipleControlledTdg(QuantumComputation& comp) { comp.addQubitRegister(3, "q"); comp.mctdg({0, 1}, 2); + comp.measureAll(true, false); } void sx(QuantumComputation& comp) { comp.addQubitRegister(1, "q"); comp.sx(0); + comp.measureAll(true, false); } void singleControlledSx(QuantumComputation& comp) { comp.addQubitRegister(2, "q"); comp.csx(0, 1); + comp.measureAll(true, false); } void multipleControlledSx(QuantumComputation& comp) { comp.addQubitRegister(3, "q"); comp.mcsx({0, 1}, 2); + comp.measureAll(true, false); } void sxdg(QuantumComputation& comp) { comp.addQubitRegister(1, "q"); comp.sxdg(0); + comp.measureAll(true, false); } void singleControlledSxdg(QuantumComputation& comp) { comp.addQubitRegister(2, "q"); comp.csxdg(0, 1); + comp.measureAll(true, false); } void multipleControlledSxdg(QuantumComputation& comp) { comp.addQubitRegister(3, "q"); comp.mcsxdg({0, 1}, 2); + comp.measureAll(true, false); } void rx(QuantumComputation& comp) { comp.addQubitRegister(1, "q"); comp.rx(0.123, 0); + comp.measureAll(true, false); } void singleControlledRx(QuantumComputation& comp) { comp.addQubitRegister(2, "q"); comp.crx(0.123, 0, 1); + comp.measureAll(true, false); } void multipleControlledRx(QuantumComputation& comp) { comp.addQubitRegister(3, "q"); comp.mcrx(0.123, {0, 1}, 2); + comp.measureAll(true, false); } void ry(QuantumComputation& comp) { comp.addQubitRegister(1, "q"); comp.ry(0.456, 0); + comp.measureAll(true, false); } void singleControlledRy(QuantumComputation& comp) { comp.addQubitRegister(2, "q"); comp.cry(0.456, 0, 1); + comp.measureAll(true, false); } void multipleControlledRy(QuantumComputation& comp) { comp.addQubitRegister(3, "q"); comp.mcry(0.456, {0, 1}, 2); + comp.measureAll(true, false); } void rz(QuantumComputation& comp) { comp.addQubitRegister(1, "q"); comp.rz(0.789, 0); + comp.measureAll(true, false); } void singleControlledRz(QuantumComputation& comp) { comp.addQubitRegister(2, "q"); comp.crz(0.789, 0, 1); + comp.measureAll(true, false); } void multipleControlledRz(QuantumComputation& comp) { comp.addQubitRegister(3, "q"); comp.mcrz(0.789, {0, 1}, 2); + comp.measureAll(true, false); } void p(QuantumComputation& comp) { comp.addQubitRegister(1, "q"); comp.p(0.123, 0); + comp.measureAll(true, false); } void singleControlledP(QuantumComputation& comp) { comp.addQubitRegister(2, "q"); comp.cp(0.123, 0, 1); + comp.measureAll(true, false); } void multipleControlledP(QuantumComputation& comp) { comp.addQubitRegister(3, "q"); comp.mcp(0.123, {0, 1}, 2); + comp.measureAll(true, false); } void r(QuantumComputation& comp) { comp.addQubitRegister(1, "q"); comp.r(0.123, 0.456, 0); + comp.measureAll(true, false); } void singleControlledR(QuantumComputation& comp) { comp.addQubitRegister(2, "q"); comp.cr(0.123, 0.456, 0, 1); + comp.measureAll(true, false); } void multipleControlledR(QuantumComputation& comp) { comp.addQubitRegister(3, "q"); comp.mcr(0.123, 0.456, {0, 1}, 2); + comp.measureAll(true, false); } void u2(QuantumComputation& comp) { comp.addQubitRegister(1, "q"); comp.u2(0.234, 0.567, 0); + comp.measureAll(true, false); } void singleControlledU2(QuantumComputation& comp) { comp.addQubitRegister(2, "q"); comp.cu2(0.234, 0.567, 0, 1); + comp.measureAll(true, false); } void multipleControlledU2(QuantumComputation& comp) { comp.addQubitRegister(3, "q"); comp.mcu2(0.234, 0.567, {0, 1}, 2); + comp.measureAll(true, false); } void u(QuantumComputation& comp) { comp.addQubitRegister(1, "q"); comp.u(0.1, 0.2, 0.3, 0); + comp.measureAll(true, false); } void singleControlledU(QuantumComputation& comp) { comp.addQubitRegister(2, "q"); comp.cu(0.1, 0.2, 0.3, 0, 1); + comp.measureAll(true, false); } void multipleControlledU(QuantumComputation& comp) { comp.addQubitRegister(3, "q"); comp.mcu(0.1, 0.2, 0.3, {0, 1}, 2); + comp.measureAll(true, false); } void swap(QuantumComputation& comp) { comp.addQubitRegister(2, "q"); comp.swap(0, 1); + comp.measureAll(true, false); } void singleControlledSwap(QuantumComputation& comp) { comp.addQubitRegister(3, "q"); comp.cswap(0, 1, 2); + comp.measureAll(true, false); } void multipleControlledSwap(QuantumComputation& comp) { comp.addQubitRegister(4, "q"); comp.mcswap({0, 1}, 2, 3); + comp.measureAll(true, false); } void iswap(QuantumComputation& comp) { comp.addQubitRegister(2, "q"); comp.iswap(0, 1); + comp.measureAll(true, false); } void singleControlledIswap(QuantumComputation& comp) { comp.addQubitRegister(3, "q"); comp.ciswap(0, 1, 2); + comp.measureAll(true, false); } void multipleControlledIswap(QuantumComputation& comp) { comp.addQubitRegister(4, "q"); comp.mciswap({0, 1}, 2, 3); + comp.measureAll(true, false); } void inverseIswap(QuantumComputation& comp) { comp.addQubitRegister(2, "q"); comp.iswapdg(0, 1); + comp.measureAll(true, false); } void inverseMultipleControlledIswap(QuantumComputation& comp) { comp.addQubitRegister(4, "q"); comp.mciswapdg({0, 1}, 2, 3); + comp.measureAll(true, false); } void dcx(QuantumComputation& comp) { comp.addQubitRegister(2, "q"); comp.dcx(0, 1); + comp.measureAll(true, false); } void singleControlledDcx(QuantumComputation& comp) { comp.addQubitRegister(3, "q"); comp.cdcx(0, 1, 2); + comp.measureAll(true, false); } void multipleControlledDcx(QuantumComputation& comp) { comp.addQubitRegister(4, "q"); comp.mcdcx({0, 1}, 2, 3); + comp.measureAll(true, false); } void ecr(QuantumComputation& comp) { comp.addQubitRegister(2, "q"); comp.ecr(0, 1); + comp.measureAll(true, false); } void singleControlledEcr(QuantumComputation& comp) { comp.addQubitRegister(3, "q"); comp.cecr(0, 1, 2); + comp.measureAll(true, false); } void multipleControlledEcr(QuantumComputation& comp) { comp.addQubitRegister(4, "q"); comp.mcecr({0, 1}, 2, 3); + comp.measureAll(true, false); } void rxx(QuantumComputation& comp) { comp.addQubitRegister(2, "q"); comp.rxx(0.123, 0, 1); + comp.measureAll(true, false); } void singleControlledRxx(QuantumComputation& comp) { comp.addQubitRegister(3, "q"); comp.crxx(0.123, 0, 1, 2); + comp.measureAll(true, false); } void multipleControlledRxx(QuantumComputation& comp) { comp.addQubitRegister(4, "q"); comp.mcrxx(0.123, {0, 1}, 2, 3); + comp.measureAll(true, false); } void tripleControlledRxx(QuantumComputation& comp) { comp.addQubitRegister(5, "q"); comp.mcrxx(0.123, {0, 1, 2}, 3, 4); + comp.measureAll(true, false); } void ryy(QuantumComputation& comp) { comp.addQubitRegister(2, "q"); comp.ryy(0.123, 0, 1); + comp.measureAll(true, false); } void singleControlledRyy(QuantumComputation& comp) { comp.addQubitRegister(3, "q"); comp.cryy(0.123, 0, 1, 2); + comp.measureAll(true, false); } void multipleControlledRyy(QuantumComputation& comp) { comp.addQubitRegister(4, "q"); comp.mcryy(0.123, {0, 1}, 2, 3); + comp.measureAll(true, false); } void rzx(QuantumComputation& comp) { comp.addQubitRegister(2, "q"); comp.rzx(0.123, 0, 1); + comp.measureAll(true, false); } void singleControlledRzx(QuantumComputation& comp) { comp.addQubitRegister(3, "q"); comp.crzx(0.123, 0, 1, 2); + comp.measureAll(true, false); } void multipleControlledRzx(QuantumComputation& comp) { comp.addQubitRegister(4, "q"); comp.mcrzx(0.123, {0, 1}, 2, 3); + comp.measureAll(true, false); } void rzz(QuantumComputation& comp) { comp.addQubitRegister(2, "q"); comp.rzz(0.123, 0, 1); + comp.measureAll(true, false); } void singleControlledRzz(QuantumComputation& comp) { comp.addQubitRegister(3, "q"); comp.crzz(0.123, 0, 1, 2); + comp.measureAll(true, false); } void multipleControlledRzz(QuantumComputation& comp) { comp.addQubitRegister(4, "q"); comp.mcrzz(0.123, {0, 1}, 2, 3); + comp.measureAll(true, false); } void xxPlusYY(QuantumComputation& comp) { comp.addQubitRegister(2, "q"); comp.xx_plus_yy(0.123, 0.456, 0, 1); + comp.measureAll(true, false); } void singleControlledXxPlusYY(QuantumComputation& comp) { comp.addQubitRegister(3, "q"); comp.cxx_plus_yy(0.123, 0.456, 0, 1, 2); + comp.measureAll(true, false); } void multipleControlledXxPlusYY(QuantumComputation& comp) { comp.addQubitRegister(4, "q"); comp.mcxx_plus_yy(0.123, 0.456, {0, 1}, 2, 3); + comp.measureAll(true, false); } void xxMinusYY(QuantumComputation& comp) { comp.addQubitRegister(2, "q"); comp.xx_minus_yy(0.123, 0.456, 0, 1); + comp.measureAll(true, false); } void singleControlledXxMinusYY(QuantumComputation& comp) { comp.addQubitRegister(3, "q"); comp.cxx_minus_yy(0.123, 0.456, 0, 1, 2); + comp.measureAll(true, false); } void multipleControlledXxMinusYY(QuantumComputation& comp) { comp.addQubitRegister(4, "q"); comp.mcxx_minus_yy(0.123, 0.456, {0, 1}, 2, 3); + comp.measureAll(true, false); } void barrier(QuantumComputation& comp) { comp.addQubitRegister(1, "q"); comp.barrier(0); + comp.measureAll(true, false); } void barrierTwoQubits(QuantumComputation& comp) { comp.addQubitRegister(2, "q"); comp.barrier({0, 1}); + comp.measureAll(true, false); } void barrierMultipleQubits(QuantumComputation& comp) { comp.addQubitRegister(3, "q"); comp.barrier({0, 1, 2}); + comp.measureAll(true, false); } void ctrlTwo(QuantumComputation& comp) { @@ -551,6 +652,7 @@ void ctrlTwo(QuantumComputation& comp) { compound.addControl(0); compound.addControl(1); comp.emplace_back(std::move(compound)); + comp.measureAll(true, false); } void ctrlTwoMixed(QuantumComputation& comp) { @@ -562,6 +664,7 @@ void ctrlTwoMixed(QuantumComputation& comp) { compound.addControl(0); compound.addControl(1); comp.emplace_back(std::move(compound)); + comp.measureAll(true, false); } void simpleIf(QuantumComputation& comp) { @@ -570,6 +673,8 @@ void simpleIf(QuantumComputation& comp) { comp.h(q[0]); comp.measure(q[0], c[0]); comp.if_(X, q[0], c[0]); + const auto& c2 = comp.addClassicalRegister(1, "meas"); + comp.measure(q[0], c2[0]); } void ifTwoQubits(QuantumComputation& comp) { @@ -583,6 +688,7 @@ void ifTwoQubits(QuantumComputation& comp) { IfElseOperation ifElse( std::make_unique(std::move(compound)), nullptr, c[0]); comp.emplace_back(std::move(ifElse)); + comp.measureAll(true, false); } void ifElse(QuantumComputation& comp) { @@ -592,6 +698,7 @@ void ifElse(QuantumComputation& comp) { comp.measure(q[0], c[0]); comp.ifElse(std::make_unique(q[0], X), std::make_unique(q[0], Z), c[0]); + comp.measureAll(true, false); } } // namespace qc diff --git a/pyproject.toml b/pyproject.toml index 69158dc490..5524b3a2a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ [build-system] requires = [ "nanobind~=2.13.0", - "scikit-build-core>=0.12.2", + "scikit-build-core~=0.12", "setuptools-scm>=9.2.2", ] build-backend = "scikit_build_core.build" @@ -328,7 +328,7 @@ exclude = [ [dependency-groups] build = [ "nanobind~=2.13.0", - "scikit-build-core>=0.12.2", + "scikit-build-core~=0.12", "setuptools-scm>=9.2.2", ] docs = [ diff --git a/python/mqt/core/fomac.pyi b/python/mqt/core/fomac.pyi index 6dfef7e4db..9a1cd3a637 100644 --- a/python/mqt/core/fomac.pyi +++ b/python/mqt/core/fomac.pyi @@ -251,7 +251,18 @@ class Device: def supported_program_formats(self) -> list[ProgramFormat]: """Returns the list of program formats supported by the device.""" - def submit_job(self, program: str, program_format: ProgramFormat, num_shots: int) -> Job: + def submit_job( + self, + program: str, + program_format: ProgramFormat, + num_shots: int, + *, + custom1: str | bool | float | None = None, + custom2: str | bool | float | None = None, + custom3: str | bool | float | None = None, + custom4: str | bool | float | None = None, + custom5: str | bool | float | None = None, + ) -> Job: """Submits a job to the device.""" def __eq__(self, arg: object, /) -> bool: ... diff --git a/python/mqt/core/ir/__init__.pyi b/python/mqt/core/ir/__init__.pyi index 69ea76967a..b95847f98b 100644 --- a/python/mqt/core/ir/__init__.pyi +++ b/python/mqt/core/ir/__init__.pyi @@ -1850,15 +1850,17 @@ class QuantumComputation(MutableSequence[operations.Operation]): cbits: The classical bits to store the results """ - def measure_all(self, *, add_bits: bool = True) -> None: + def measure_all(self, *, add_bits: bool = True, add_barrier: bool = True) -> None: """Measure all qubits and store the results in classical bits. Details: If `add_bits` is `True`, a new classical register (named "`meas`") with the same size as the number of qubits will be added to the circuit and the results will be stored in it. If `add_bits` is `False`, the classical register must already exist and have a sufficient number of bits to store the results. + If `add_barrier` is `True`, a barrier is added before the measurements. Args: add_bits: Whether to explicitly add a classical register + add_barrier: Whether to add a barrier before the measurements """ @overload diff --git a/src/fomac/FoMaC.cpp b/src/fomac/FoMaC.cpp index 2f96b8c4ca..a64b98bdea 100644 --- a/src/fomac/FoMaC.cpp +++ b/src/fomac/FoMaC.cpp @@ -22,112 +22,111 @@ #include #include #include +#include #include #include #include #include #include +#include #include +#include #include namespace fomac { -auto Session::Device::Site::getIndex() const -> size_t { +size_t Site::getIndex() const { return queryProperty(QDMI_SITE_PROPERTY_INDEX); } -auto Session::Device::Site::getT1() const -> std::optional { +std::optional Site::getT1() const { return queryProperty>(QDMI_SITE_PROPERTY_T1); } -auto Session::Device::Site::getT2() const -> std::optional { +std::optional Site::getT2() const { return queryProperty>(QDMI_SITE_PROPERTY_T2); } -auto Session::Device::Site::getName() const -> std::optional { +std::optional Site::getName() const { return queryProperty>(QDMI_SITE_PROPERTY_NAME); } -auto Session::Device::Site::getXCoordinate() const -> std::optional { +std::optional Site::getXCoordinate() const { return queryProperty>(QDMI_SITE_PROPERTY_XCOORDINATE); } -auto Session::Device::Site::getYCoordinate() const -> std::optional { +std::optional Site::getYCoordinate() const { return queryProperty>(QDMI_SITE_PROPERTY_YCOORDINATE); } -auto Session::Device::Site::getZCoordinate() const -> std::optional { +std::optional Site::getZCoordinate() const { return queryProperty>(QDMI_SITE_PROPERTY_ZCOORDINATE); } -auto Session::Device::Site::isZone() const -> bool { +bool Site::isZone() const { return queryProperty>(QDMI_SITE_PROPERTY_ISZONE) .value_or(false); } -auto Session::Device::Site::getXExtent() const -> std::optional { +std::optional Site::getXExtent() const { return queryProperty>(QDMI_SITE_PROPERTY_XEXTENT); } -auto Session::Device::Site::getYExtent() const -> std::optional { +std::optional Site::getYExtent() const { return queryProperty>(QDMI_SITE_PROPERTY_YEXTENT); } -auto Session::Device::Site::getZExtent() const -> std::optional { +std::optional Site::getZExtent() const { return queryProperty>(QDMI_SITE_PROPERTY_ZEXTENT); } -auto Session::Device::Site::getModuleIndex() const -> std::optional { +std::optional Site::getModuleIndex() const { return queryProperty>(QDMI_SITE_PROPERTY_MODULEINDEX); } -auto Session::Device::Site::getSubmoduleIndex() const - -> std::optional { +std::optional Site::getSubmoduleIndex() const { return queryProperty>( QDMI_SITE_PROPERTY_SUBMODULEINDEX); } -auto Session::Device::Operation::getName( - const std::vector& sites, const std::vector& params) const - -> std::string { +std::string Operation::getName(const std::vector& sites, + const std::vector& params) const { return queryProperty(QDMI_OPERATION_PROPERTY_NAME, sites, params); } -auto Session::Device::Operation::getQubitsNum( - const std::vector& sites, const std::vector& params) const - -> std::optional { +std::optional +Operation::getQubitsNum(const std::vector& sites, + const std::vector& params) const { return queryProperty>(QDMI_OPERATION_PROPERTY_QUBITSNUM, sites, params); } -auto Session::Device::Operation::getParametersNum( - const std::vector& sites, const std::vector& params) const - -> size_t { +size_t Operation::getParametersNum(const std::vector& sites, + const std::vector& params) const { return queryProperty(QDMI_OPERATION_PROPERTY_PARAMETERSNUM, sites, params); } -auto Session::Device::Operation::getDuration( - const std::vector& sites, const std::vector& params) const - -> std::optional { +std::optional +Operation::getDuration(const std::vector& sites, + const std::vector& params) const { return queryProperty>( QDMI_OPERATION_PROPERTY_DURATION, sites, params); } -auto Session::Device::Operation::getFidelity( - const std::vector& sites, const std::vector& params) const - -> std::optional { +std::optional +Operation::getFidelity(const std::vector& sites, + const std::vector& params) const { return queryProperty>(QDMI_OPERATION_PROPERTY_FIDELITY, sites, params); } -auto Session::Device::Operation::getInteractionRadius( - const std::vector& sites, const std::vector& params) const - -> std::optional { +std::optional +Operation::getInteractionRadius(const std::vector& sites, + const std::vector& params) const { return queryProperty>( QDMI_OPERATION_PROPERTY_INTERACTIONRADIUS, sites, params); } -auto Session::Device::Operation::getBlockingRadius( - const std::vector& sites, const std::vector& params) const - -> std::optional { +std::optional +Operation::getBlockingRadius(const std::vector& sites, + const std::vector& params) const { return queryProperty>( QDMI_OPERATION_PROPERTY_BLOCKINGRADIUS, sites, params); } -auto Session::Device::Operation::getIdlingFidelity( - const std::vector& sites, const std::vector& params) const - -> std::optional { +std::optional +Operation::getIdlingFidelity(const std::vector& sites, + const std::vector& params) const { return queryProperty>( QDMI_OPERATION_PROPERTY_IDLINGFIDELITY, sites, params); } -auto Session::Device::Operation::isZoned() const -> bool { +bool Operation::isZoned() const { return queryProperty>(QDMI_OPERATION_PROPERTY_ISZONED, {}, {}) .value_or(false); } -auto Session::Device::Operation::getSites() const - -> std::optional> { +std::optional> Operation::getSites() const { const auto& qdmiSites = queryProperty>>( QDMI_OPERATION_PROPERTY_SITES, {}, {}); if (!qdmiSites.has_value()) { @@ -137,12 +136,12 @@ auto Session::Device::Operation::getSites() const returnedSites.reserve(qdmiSites->size()); std::ranges::transform(*qdmiSites, std::back_inserter(returnedSites), [device = device_](const QDMI_Site& site) -> Site { - return {Token{}, device, site}; + return {device, site}; }); return returnedSites; } -auto Session::Device::Operation::getSitePairs() const - -> std::optional>> { +std::optional>> +Operation::getSitePairs() const { if (const auto qubitsNum = getQubitsNum({}, {}); !qubitsNum.has_value() || *qubitsNum != 2 || isZoned()) { return std::nullopt; // Not a 2-qubit operation or operation is zoned @@ -167,152 +166,200 @@ auto Session::Device::Operation::getSitePairs() const return pairs; } -auto Session::Device::Operation::getMeanShuttlingSpeed( - const std::vector& sites, const std::vector& params) const - -> std::optional { +std::optional +Operation::getMeanShuttlingSpeed(const std::vector& sites, + const std::vector& params) const { return queryProperty>( QDMI_OPERATION_PROPERTY_MEANSHUTTLINGSPEED, sites, params); } -auto Session::Device::getName() const -> std::string { +std::string Device::getName() const { return queryProperty(QDMI_DEVICE_PROPERTY_NAME); } -auto Session::Device::getVersion() const -> std::string { + +std::string Device::getVersion() const { return queryProperty(QDMI_DEVICE_PROPERTY_VERSION); } -auto Session::Device::getStatus() const -> QDMI_Device_Status { + +QDMI_Device_Status Device::getStatus() const { return queryProperty(QDMI_DEVICE_PROPERTY_STATUS); } -auto Session::Device::getLibraryVersion() const -> std::string { + +std::string Device::getLibraryVersion() const { return queryProperty(QDMI_DEVICE_PROPERTY_LIBRARYVERSION); } -auto Session::Device::getQubitsNum() const -> size_t { + +size_t Device::getQubitsNum() const { return queryProperty(QDMI_DEVICE_PROPERTY_QUBITSNUM); } -auto Session::Device::getSites() const -> std::vector { + +std::vector Device::getSites() const { const auto& qdmiSites = queryProperty>(QDMI_DEVICE_PROPERTY_SITES); std::vector sites; sites.reserve(qdmiSites.size()); - std::ranges::transform(qdmiSites, std::back_inserter(sites), - [device = device_](const QDMI_Site& site) -> Site { - return {Token{}, device, site}; - }); + std::ranges::transform( + qdmiSites, std::back_inserter(sites), + [this](const QDMI_Site& site) -> Site { return {this, site}; }); return sites; } -auto Session::Device::getRegularSites() const -> std::vector { + +std::vector Device::getRegularSites() const { auto allSites = getSites(); const auto newEnd = std::ranges::remove_if( - allSites, [](const Site& s) { return s.isZone(); }); + allSites, [](const auto& s) { return s.isZone(); }); allSites.erase(newEnd.begin(), newEnd.end()); return allSites; } -auto Session::Device::getZones() const -> std::vector { + +std::vector Device::getZones() const { const auto& allSites = getSites(); std::vector zones; zones.reserve(3); // Reserve space for a typical max number of zones std::ranges::copy_if(allSites, std::back_inserter(zones), - [](const Site& s) { return s.isZone(); }); + [](const auto& s) { return s.isZone(); }); return zones; } -auto Session::Device::getOperations() const -> std::vector { + +std::vector Device::getOperations() const { const auto& qdmiOperations = queryProperty>( QDMI_DEVICE_PROPERTY_OPERATIONS); std::vector operations; operations.reserve(qdmiOperations.size()); std::ranges::transform( qdmiOperations, std::back_inserter(operations), - [device = device_](const QDMI_Operation& op) -> Operation { - return {Token{}, device, op}; - }); + [this](const QDMI_Operation& op) -> Operation { return {this, op}; }); return operations; } -auto Session::Device::getCouplingMap() const - -> std::optional>> { + +std::optional>> +Device::getCouplingMap() const { const auto& qdmiCouplingMap = queryProperty< std::optional>>>( QDMI_DEVICE_PROPERTY_COUPLINGMAP); if (!qdmiCouplingMap.has_value()) { return std::nullopt; } + std::vector> couplingMap; couplingMap.reserve(qdmiCouplingMap->size()); std::ranges::transform(*qdmiCouplingMap, std::back_inserter(couplingMap), [this](const std::pair& pair) -> std::pair { - return {Site{Token{}, device_, pair.first}, - Site{Token{}, device_, pair.second}}; + return {Site{this, pair.first}, + Site{this, pair.second}}; }); return couplingMap; } -auto Session::Device::getNeedsCalibration() const -> std::optional { + +std::optional Device::getNeedsCalibration() const { return queryProperty>( QDMI_DEVICE_PROPERTY_NEEDSCALIBRATION); } -auto Session::Device::getLengthUnit() const -> std::optional { + +std::optional Device::getLengthUnit() const { return queryProperty>( QDMI_DEVICE_PROPERTY_LENGTHUNIT); } -auto Session::Device::getLengthScaleFactor() const -> std::optional { + +std::optional Device::getLengthScaleFactor() const { return queryProperty>( QDMI_DEVICE_PROPERTY_LENGTHSCALEFACTOR); } -auto Session::Device::getDurationUnit() const -> std::optional { + +std::optional Device::getDurationUnit() const { return queryProperty>( QDMI_DEVICE_PROPERTY_DURATIONUNIT); } -auto Session::Device::getDurationScaleFactor() const -> std::optional { + +std::optional Device::getDurationScaleFactor() const { return queryProperty>( QDMI_DEVICE_PROPERTY_DURATIONSCALEFACTOR); } -auto Session::Device::getMinAtomDistance() const -> std::optional { + +std::optional Device::getMinAtomDistance() const { return queryProperty>( QDMI_DEVICE_PROPERTY_MINATOMDISTANCE); } -auto Session::Device::getSupportedProgramFormats() const - -> std::vector { +std::vector Device::getSupportedProgramFormats() const { return queryProperty>( QDMI_DEVICE_PROPERTY_SUPPORTEDPROGRAMFORMATS); } -auto Session::Device::submitJob(const std::string& program, - const QDMI_Program_Format format, - const size_t numShots) const -> Job { +Job Device::submitJob(const std::string& program, + const QDMI_Program_Format format, const size_t numShots, + const std::optional& custom1, + const std::optional& custom2, + const std::optional& custom3, + const std::optional& custom4, + const std::optional& custom5) const { QDMI_Job job = nullptr; qdmi::throwIfError(QDMI_device_create_job(device_, &job), "Creating job"); - Job jobWrapper{job}; // RAII wrapper to prevent leaks in case of exceptions + Job jobWrapper{job}; - // Set program format qdmi::throwIfError(QDMI_job_set_parameter(jobWrapper, QDMI_JOB_PARAMETER_PROGRAMFORMAT, sizeof(format), &format), "Setting program format"); - - // Set program qdmi::throwIfError( QDMI_job_set_parameter(jobWrapper, QDMI_JOB_PARAMETER_PROGRAM, program.size() + 1, program.c_str()), "Setting program"); - - // Set number of shots qdmi::throwIfError(QDMI_job_set_parameter(jobWrapper, QDMI_JOB_PARAMETER_SHOTSNUM, sizeof(numShots), &numShots), "Setting number of shots"); - // Submit the job + if (custom1.has_value()) { + setCustomJobParam(jobWrapper, QDMI_JOB_PARAMETER_CUSTOM1, *custom1); + } + if (custom2.has_value()) { + setCustomJobParam(jobWrapper, QDMI_JOB_PARAMETER_CUSTOM2, *custom2); + } + if (custom3.has_value()) { + setCustomJobParam(jobWrapper, QDMI_JOB_PARAMETER_CUSTOM3, *custom3); + } + if (custom4.has_value()) { + setCustomJobParam(jobWrapper, QDMI_JOB_PARAMETER_CUSTOM4, *custom4); + } + if (custom5.has_value()) { + setCustomJobParam(jobWrapper, QDMI_JOB_PARAMETER_CUSTOM5, *custom5); + } + qdmi::throwIfError(QDMI_job_submit(jobWrapper), "Submitting job"); return jobWrapper; } -auto Session::Job::check() const -> QDMI_Job_Status { +void Device::setCustomJobParam(QDMI_Job job, const QDMI_Job_Parameter param, + const CustomJobParameter& value) { + std::visit( + [&](const CustomValue& customValue) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + qdmi::throwIfError(QDMI_job_set_parameter(job, param, + customValue.size() + 1, + customValue.c_str()), + "Setting custom parameter"); + } else { + static_assert(std::is_trivially_copyable_v, + "Custom job parameters must be trivially copyable"); + qdmi::throwIfError( + QDMI_job_set_parameter(job, param, sizeof(T), &customValue), + "Setting custom parameter"); + } + }, + value); +} + +QDMI_Job_Status Job::check() const { QDMI_Job_Status status{}; - qdmi::throwIfError(QDMI_job_check(job_, &status), "Checking job status"); + qdmi::throwIfError(QDMI_job_check(job_.get(), &status), + "Checking job status"); return status; } -auto Session::Job::wait(const size_t timeout) const -> bool { - const auto ret = QDMI_job_wait(job_, timeout); +bool Job::wait(const size_t timeout) const { + const auto ret = QDMI_job_wait(job_.get(), timeout); if (ret == QDMI_SUCCESS) { return true; } @@ -323,65 +370,67 @@ auto Session::Job::wait(const size_t timeout) const -> bool { qdmi::unreachable(); } -auto Session::Job::cancel() const -> void { - qdmi::throwIfError(QDMI_job_cancel(job_), "Cancelling job"); +void Job::cancel() const { + qdmi::throwIfError(QDMI_job_cancel(job_.get()), "Cancelling job"); } -auto Session::Job::getId() const -> std::string { +std::string Job::getId() const { size_t size = 0; - qdmi::throwIfError( - QDMI_job_query_property(job_, QDMI_JOB_PROPERTY_ID, 0, nullptr, &size), - "Querying job ID size"); + qdmi::throwIfError(QDMI_job_query_property(job_.get(), QDMI_JOB_PROPERTY_ID, + 0, nullptr, &size), + "Querying job ID size"); std::string id(size - 1, '\0'); - qdmi::throwIfError(QDMI_job_query_property(job_, QDMI_JOB_PROPERTY_ID, size, - id.data(), nullptr), + qdmi::throwIfError(QDMI_job_query_property(job_.get(), QDMI_JOB_PROPERTY_ID, + size, id.data(), nullptr), "Querying job ID"); return id; } -auto Session::Job::getProgramFormat() const -> QDMI_Program_Format { +QDMI_Program_Format Job::getProgramFormat() const { QDMI_Program_Format format{}; - qdmi::throwIfError(QDMI_job_query_property(job_, + qdmi::throwIfError(QDMI_job_query_property(job_.get(), QDMI_JOB_PROPERTY_PROGRAMFORMAT, sizeof(format), &format, nullptr), "Querying program format"); return format; } -auto Session::Job::getProgram() const -> std::string { +std::string Job::getProgram() const { size_t size = 0; - qdmi::throwIfError(QDMI_job_query_property(job_, QDMI_JOB_PROPERTY_PROGRAM, 0, + qdmi::throwIfError(QDMI_job_query_property(job_.get(), + QDMI_JOB_PROPERTY_PROGRAM, 0, nullptr, &size), "Querying program size"); std::string program(size - 1, '\0'); - qdmi::throwIfError(QDMI_job_query_property(job_, QDMI_JOB_PROPERTY_PROGRAM, - size, program.data(), nullptr), + qdmi::throwIfError(QDMI_job_query_property(job_.get(), + QDMI_JOB_PROPERTY_PROGRAM, size, + program.data(), nullptr), "Querying program"); return program; } -auto Session::Job::getNumShots() const -> size_t { +size_t Job::getNumShots() const { size_t numShots = 0; - qdmi::throwIfError(QDMI_job_query_property(job_, QDMI_JOB_PROPERTY_SHOTSNUM, - sizeof(numShots), &numShots, - nullptr), - "Querying number of shots"); + qdmi::throwIfError( + QDMI_job_query_property(job_.get(), QDMI_JOB_PROPERTY_SHOTSNUM, + sizeof(numShots), &numShots, nullptr), + "Querying number of shots"); return numShots; } -auto Session::Job::getShots() const -> std::vector { +std::vector Job::getShots() const { size_t shotsSize = 0; - qdmi::throwIfError( - QDMI_job_get_results(job_, QDMI_JOB_RESULT_SHOTS, 0, nullptr, &shotsSize), - "Querying shots size"); + qdmi::throwIfError(QDMI_job_get_results(job_.get(), QDMI_JOB_RESULT_SHOTS, 0, + nullptr, &shotsSize), + "Querying shots size"); if (shotsSize == 0) { return {}; } std::string shots(shotsSize - 1, '\0'); - qdmi::throwIfError(QDMI_job_get_results(job_, QDMI_JOB_RESULT_SHOTS, + qdmi::throwIfError(QDMI_job_get_results(job_.get(), QDMI_JOB_RESULT_SHOTS, shotsSize, shots.data(), nullptr), "Querying shots"); @@ -401,11 +450,11 @@ auto Session::Job::getShots() const -> std::vector { return shotsVec; } -auto Session::Job::getCounts() const -> std::map { +std::map Job::getCounts() const { // Get the histogram keys size_t keysSize = 0; - qdmi::throwIfError(QDMI_job_get_results(job_, QDMI_JOB_RESULT_HIST_KEYS, 0, - nullptr, &keysSize), + qdmi::throwIfError(QDMI_job_get_results(job_.get(), QDMI_JOB_RESULT_HIST_KEYS, + 0, nullptr, &keysSize), "Querying histogram keys size"); if (keysSize == 0) { @@ -413,13 +462,14 @@ auto Session::Job::getCounts() const -> std::map { } std::string keys(keysSize - 1, '\0'); - qdmi::throwIfError(QDMI_job_get_results(job_, QDMI_JOB_RESULT_HIST_KEYS, + qdmi::throwIfError(QDMI_job_get_results(job_.get(), QDMI_JOB_RESULT_HIST_KEYS, keysSize, keys.data(), nullptr), "Querying histogram keys"); // Get the histogram values size_t valuesSize = 0; - qdmi::throwIfError(QDMI_job_get_results(job_, QDMI_JOB_RESULT_HIST_VALUES, 0, + qdmi::throwIfError(QDMI_job_get_results(job_.get(), + QDMI_JOB_RESULT_HIST_VALUES, 0, nullptr, &valuesSize), "Querying histogram values size"); @@ -429,7 +479,8 @@ auto Session::Job::getCounts() const -> std::map { } std::vector values(valuesSize / sizeof(size_t)); - qdmi::throwIfError(QDMI_job_get_results(job_, QDMI_JOB_RESULT_HIST_VALUES, + qdmi::throwIfError(QDMI_job_get_results(job_.get(), + QDMI_JOB_RESULT_HIST_VALUES, valuesSize, values.data(), nullptr), "Querying histogram values"); @@ -452,10 +503,9 @@ auto Session::Job::getCounts() const -> std::map { return counts; } -auto Session::Job::getDenseStateVector() const - -> std::vector> { +std::vector> Job::getDenseStateVector() const { size_t size = 0; - qdmi::throwIfError(QDMI_job_get_results(job_, + qdmi::throwIfError(QDMI_job_get_results(job_.get(), QDMI_JOB_RESULT_STATEVECTOR_DENSE, 0, nullptr, &size), "Querying dense state vector size"); @@ -467,16 +517,16 @@ auto Session::Job::getDenseStateVector() const std::vector> stateVector(size / sizeof(std::complex)); - qdmi::throwIfError(QDMI_job_get_results(job_, + qdmi::throwIfError(QDMI_job_get_results(job_.get(), QDMI_JOB_RESULT_STATEVECTOR_DENSE, size, stateVector.data(), nullptr), "Querying dense state vector"); return stateVector; } -auto Session::Job::getDenseProbabilities() const -> std::vector { +std::vector Job::getDenseProbabilities() const { size_t size = 0; - qdmi::throwIfError(QDMI_job_get_results(job_, + qdmi::throwIfError(QDMI_job_get_results(job_.get(), QDMI_JOB_RESULT_PROBABILITIES_DENSE, 0, nullptr, &size), "Querying dense probabilities size"); @@ -487,19 +537,18 @@ auto Session::Job::getDenseProbabilities() const -> std::vector { } std::vector probabilities(size / sizeof(double)); - qdmi::throwIfError(QDMI_job_get_results(job_, + qdmi::throwIfError(QDMI_job_get_results(job_.get(), QDMI_JOB_RESULT_PROBABILITIES_DENSE, size, probabilities.data(), nullptr), "Querying dense probabilities"); return probabilities; } -auto Session::Job::getSparseStateVector() const - -> std::map> { +std::map> Job::getSparseStateVector() const { size_t keysSize = 0; qdmi::throwIfError( - QDMI_job_get_results(job_, QDMI_JOB_RESULT_STATEVECTOR_SPARSE_KEYS, 0, - nullptr, &keysSize), + QDMI_job_get_results(job_.get(), QDMI_JOB_RESULT_STATEVECTOR_SPARSE_KEYS, + 0, nullptr, &keysSize), "Querying sparse state vector keys size"); if (keysSize == 0) { @@ -508,15 +557,15 @@ auto Session::Job::getSparseStateVector() const std::string keys(keysSize - 1, '\0'); qdmi::throwIfError( - QDMI_job_get_results(job_, QDMI_JOB_RESULT_STATEVECTOR_SPARSE_KEYS, + QDMI_job_get_results(job_.get(), QDMI_JOB_RESULT_STATEVECTOR_SPARSE_KEYS, keysSize, keys.data(), nullptr), "Querying sparse state vector keys"); size_t valuesSize = 0; - qdmi::throwIfError( - QDMI_job_get_results(job_, QDMI_JOB_RESULT_STATEVECTOR_SPARSE_VALUES, 0, - nullptr, &valuesSize), - "Querying sparse state vector values size"); + qdmi::throwIfError(QDMI_job_get_results( + job_.get(), QDMI_JOB_RESULT_STATEVECTOR_SPARSE_VALUES, + 0, nullptr, &valuesSize), + "Querying sparse state vector values size"); if (valuesSize % sizeof(std::complex) != 0) { throw std::runtime_error( @@ -526,10 +575,10 @@ auto Session::Job::getSparseStateVector() const std::vector> values(valuesSize / sizeof(std::complex)); - qdmi::throwIfError( - QDMI_job_get_results(job_, QDMI_JOB_RESULT_STATEVECTOR_SPARSE_VALUES, - valuesSize, values.data(), nullptr), - "Querying sparse state vector values"); + qdmi::throwIfError(QDMI_job_get_results( + job_.get(), QDMI_JOB_RESULT_STATEVECTOR_SPARSE_VALUES, + valuesSize, values.data(), nullptr), + "Querying sparse state vector values"); // Parse the keys (comma-separated) std::map> stateVector; @@ -550,27 +599,27 @@ auto Session::Job::getSparseStateVector() const return stateVector; } -auto Session::Job::getSparseProbabilities() const - -> std::map { +std::map Job::getSparseProbabilities() const { size_t keysSize = 0; - qdmi::throwIfError( - QDMI_job_get_results(job_, QDMI_JOB_RESULT_PROBABILITIES_SPARSE_KEYS, 0, - nullptr, &keysSize), - "Querying sparse probabilities keys size"); + qdmi::throwIfError(QDMI_job_get_results( + job_.get(), QDMI_JOB_RESULT_PROBABILITIES_SPARSE_KEYS, + 0, nullptr, &keysSize), + "Querying sparse probabilities keys size"); if (keysSize == 0) { return {}; // Empty probabilities } std::string keys(keysSize - 1, '\0'); - qdmi::throwIfError( - QDMI_job_get_results(job_, QDMI_JOB_RESULT_PROBABILITIES_SPARSE_KEYS, - keysSize, keys.data(), nullptr), - "Querying sparse probabilities keys"); + qdmi::throwIfError(QDMI_job_get_results( + job_.get(), QDMI_JOB_RESULT_PROBABILITIES_SPARSE_KEYS, + keysSize, keys.data(), nullptr), + "Querying sparse probabilities keys"); size_t valuesSize = 0; qdmi::throwIfError( - QDMI_job_get_results(job_, QDMI_JOB_RESULT_PROBABILITIES_SPARSE_VALUES, 0, + QDMI_job_get_results(job_.get(), + QDMI_JOB_RESULT_PROBABILITIES_SPARSE_VALUES, 0, nullptr, &valuesSize), "Querying sparse probabilities values size"); @@ -581,7 +630,8 @@ auto Session::Job::getSparseProbabilities() const std::vector values(valuesSize / sizeof(double)); qdmi::throwIfError( - QDMI_job_get_results(job_, QDMI_JOB_RESULT_PROBABILITIES_SPARSE_VALUES, + QDMI_job_get_results(job_.get(), + QDMI_JOB_RESULT_PROBABILITIES_SPARSE_VALUES, valuesSize, values.data(), nullptr), "Querying sparse probabilities values"); @@ -603,23 +653,25 @@ auto Session::Job::getSparseProbabilities() const return probabilities; } +Device Session::createSessionlessDevice(QDMI_Device device) { + return Device(device); +} + Session::Session(const SessionConfig& config) { - const auto result = QDMI_session_alloc(&session_); - qdmi::throwIfError(result, "Allocating QDMI session"); - - // Helper to ensure session is freed if an exception is thrown during setup - const auto cleanup = [this]() -> void { - if (session_ != nullptr) { - QDMI_session_free(session_); - session_ = nullptr; - } - }; + session_ = [] { + QDMI_Session session = nullptr; + const auto result = QDMI_session_alloc(&session); + qdmi::throwIfError(result, "Allocating QDMI session"); + return std::unique_ptr( + session, QDMI_session_free); + }(); + // Helper to set session parameters const auto setParameter = [this](const std::optional& value, QDMI_Session_Parameter param) -> void { if (value) { const auto status = static_cast(QDMI_session_set_parameter( - session_, param, value->size() + 1, value->c_str())); + session_.get(), param, value->size() + 1, value->c_str())); if (status == QDMI_ERROR_NOTSUPPORTED) { // Optional parameter not supported by session - skip it SPDLOG_INFO("Session parameter {} not supported (skipped)", @@ -636,97 +688,70 @@ Session::Session(const SessionConfig& config) { } }; - try { - // Validate file existence for authFile - if (config.authFile) { - if (!std::filesystem::exists(*config.authFile)) { - throw std::runtime_error("Authentication file does not exist: " + - *config.authFile); - } - } - // Validate URL format for authUrl - if (config.authUrl) { - // Breakdown of the regex pattern: - // 1. ^https?:// -> Start with http:// or https:// - // 2. (?: -> Start Host Group - // \[[a-fA-F0-9:]+\] -> Branch A: IPv6 (Must be in brackets like - // [::1]) - // -> Note: No \b used here because ']' is a - // non-word char - // | -> OR - // (?: -> Branch B: Alphanumeric Hosts (Group for - // \b check) - // (?:\d{1,3}\.){3}\d{1,3} -> IPv4 (e.g., 127.0.0.1) - // | -> OR - // localhost -> Localhost - // | -> OR - // (?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6} -> - // Domain - // )\b -> End Branch B + Word Boundary (Prevents - // "localhostX") - // ) -> End Host Group - // 3. (?::\d+)? -> Optional Port (e.g., :8080) - // 4. (?:...)*$ -> Optional Path/Query params + End of - // string - static const std::regex URL_PATTERN( - R"(^https?://(?:\[[a-fA-F0-9:]+\]|(?:(?:\d{1,3}\.){3}\d{1,3}|localhost|(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6})\b)(?::\d+)?(?:[-a-zA-Z0-9()@:%_\+.~#?&/=]*)$)", - std::regex::optimize); - if (!std::regex_match(*config.authUrl, URL_PATTERN)) { - throw std::runtime_error("Invalid URL format: " + *config.authUrl); - } + // Validate file existence for authFile + if (config.authFile) { + if (!std::filesystem::exists(*config.authFile)) { + throw std::runtime_error("Authentication file does not exist: " + + *config.authFile); } - - // Set session parameters - setParameter(config.token, QDMI_SESSION_PARAMETER_TOKEN); - setParameter(config.authFile, QDMI_SESSION_PARAMETER_AUTHFILE); - setParameter(config.authUrl, QDMI_SESSION_PARAMETER_AUTHURL); - setParameter(config.username, QDMI_SESSION_PARAMETER_USERNAME); - setParameter(config.password, QDMI_SESSION_PARAMETER_PASSWORD); - setParameter(config.projectId, QDMI_SESSION_PARAMETER_PROJECTID); - setParameter(config.custom1, QDMI_SESSION_PARAMETER_CUSTOM1); - setParameter(config.custom2, QDMI_SESSION_PARAMETER_CUSTOM2); - setParameter(config.custom3, QDMI_SESSION_PARAMETER_CUSTOM3); - setParameter(config.custom4, QDMI_SESSION_PARAMETER_CUSTOM4); - setParameter(config.custom5, QDMI_SESSION_PARAMETER_CUSTOM5); - - // Initialize the session - qdmi::throwIfError(QDMI_session_init(session_), "Initializing session"); - } catch (...) { - cleanup(); - throw; } -} - -Session::~Session() { - if (session_ != nullptr) { - QDMI_session_free(session_); + // Validate URL format for authUrl + if (config.authUrl) { + // Breakdown of the regex pattern: + // 1. ^https?:// -> Start with http:// or https:// + // 2. (?: -> Start Host Group + // \[[a-fA-F0-9:]+\] -> Branch A: IPv6 (Must be in brackets like + // [::1]) + // -> Note: No \b used here because ']' is a + // non-word char + // | -> OR + // (?: -> Branch B: Alphanumeric Hosts (Group for + // \b check) + // (?:\d{1,3}\.){3}\d{1,3} -> IPv4 (e.g., 127.0.0.1) + // | -> OR + // localhost -> Localhost + // | -> OR + // (?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6} -> + // Domain + // )\b -> End Branch B + Word Boundary (Prevents + // "localhostX") + // ) -> End Host Group + // 3. (?::\d+)? -> Optional Port (e.g., :8080) + // 4. (?:...)*$ -> Optional Path/Query params + End of + // string + static const std::regex URL_PATTERN( + R"(^https?://(?:\[[a-fA-F0-9:]+\]|(?:(?:\d{1,3}\.){3}\d{1,3}|localhost|(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6})\b)(?::\d+)?(?:[-a-zA-Z0-9()@:%_\+.~#?&/=]*)$)", + std::regex::optimize); + if (!std::regex_match(*config.authUrl, URL_PATTERN)) { + throw std::runtime_error("Invalid URL format: " + *config.authUrl); + } } -} -Session::Session(Session&& other) noexcept : session_(other.session_) { - other.session_ = nullptr; -} + // Set session parameters + setParameter(config.token, QDMI_SESSION_PARAMETER_TOKEN); + setParameter(config.authFile, QDMI_SESSION_PARAMETER_AUTHFILE); + setParameter(config.authUrl, QDMI_SESSION_PARAMETER_AUTHURL); + setParameter(config.username, QDMI_SESSION_PARAMETER_USERNAME); + setParameter(config.password, QDMI_SESSION_PARAMETER_PASSWORD); + setParameter(config.projectId, QDMI_SESSION_PARAMETER_PROJECTID); + setParameter(config.custom1, QDMI_SESSION_PARAMETER_CUSTOM1); + setParameter(config.custom2, QDMI_SESSION_PARAMETER_CUSTOM2); + setParameter(config.custom3, QDMI_SESSION_PARAMETER_CUSTOM3); + setParameter(config.custom4, QDMI_SESSION_PARAMETER_CUSTOM4); + setParameter(config.custom5, QDMI_SESSION_PARAMETER_CUSTOM5); -Session& Session::operator=(Session&& other) noexcept { - if (this != &other) { - if (session_ != nullptr) { - QDMI_session_free(session_); - } - session_ = other.session_; - other.session_ = nullptr; - } - return *this; + // Initialize the session + qdmi::throwIfError(QDMI_session_init(session_.get()), "Initializing session"); } -auto Session::getDevices() -> std::vector { - +std::vector Session::getDevices() { const auto& qdmiDevices = queryProperty>(QDMI_SESSION_PROPERTY_DEVICES); std::vector devices; devices.reserve(qdmiDevices.size()); std::ranges::transform( qdmiDevices, std::back_inserter(devices), - [](const QDMI_Device& dev) -> Device { return {Token{}, dev}; }); + [](const QDMI_Device& dev) -> Device { return Device(dev); }); return devices; } } // namespace fomac diff --git a/src/ir/QuantumComputation.cpp b/src/ir/QuantumComputation.cpp index e0b63f17b0..7b018582fd 100644 --- a/src/ir/QuantumComputation.cpp +++ b/src/ir/QuantumComputation.cpp @@ -1541,7 +1541,7 @@ void QuantumComputation::measure(const Targets& qubits, emplace_back(qubits, bits); } -void QuantumComputation::measureAll(const bool addBits) { +void QuantumComputation::measureAll(const bool addBits, const bool addBarrier) { if (addBits) { addClassicalRegister(getNqubits(), "meas"); } @@ -1553,7 +1553,9 @@ void QuantumComputation::measureAll(const bool addBits) { throw std::runtime_error(ss.str()); } - barrier(); + if (addBarrier) { + barrier(); + } Qubit start = 0U; if (addBits) { start = static_cast(classicalRegisters.at("meas").getStartIndex()); diff --git a/src/na/fomac/Device.cpp b/src/na/fomac/Device.cpp index be3dfd4f7a..474035413f 100644 --- a/src/na/fomac/Device.cpp +++ b/src/na/fomac/Device.cpp @@ -43,8 +43,8 @@ namespace { * @param sites is a vector of Session sites * @return the extent covering all given sites */ -auto calculateExtentFromSites( - const std::vector& sites) -> Device::Region { +auto calculateExtentFromSites(const std::vector& sites) + -> Device::Region { auto minX = std::numeric_limits::max(); auto maxX = std::numeric_limits::min(); auto minY = std::numeric_limits::max(); @@ -68,8 +68,7 @@ auto calculateExtentFromSites( * @return the extent covering all sites in the pairs */ auto calculateExtentFromSites( - const std::vector>& sitePairs) + const std::vector>& sitePairs) -> Device::Region { auto minX = std::numeric_limits::max(); auto maxX = std::numeric_limits::min(); @@ -125,7 +124,7 @@ auto Session::Device::initQubitsNumFromDevice() -> void { numQubits = getQubitsNum(); } auto Session::Device::initLengthUnitFromDevice() -> bool { - const auto& u = fomac::Session::Device::getLengthUnit(); + const auto& u = fomac::Device::getLengthUnit(); if (!u.has_value()) { SPDLOG_INFO("Length unit not set"); return false; @@ -135,7 +134,7 @@ auto Session::Device::initLengthUnitFromDevice() -> bool { return true; } auto Session::Device::initDurationUnitFromDevice() -> bool { - const auto& u = fomac::Session::Device::getDurationUnit(); + const auto& u = fomac::Device::getDurationUnit(); if (!u.has_value()) { SPDLOG_INFO("Duration unit not set"); return false; @@ -298,7 +297,7 @@ auto Session::Device::initTrapsfromDevice() -> bool { auto Session::Device::initOperationsFromDevice() -> bool { std::map>> shuttlingUnitsPerId; - for (const fomac::Session::Device::Operation& op : getOperations()) { + for (const fomac::Operation& op : getOperations()) { const auto zoned = op.isZoned(); const auto& nq = op.getQubitsNum(); const auto& opName = op.getName(); @@ -308,10 +307,9 @@ auto Session::Device::initOperationsFromDevice() -> bool { return false; } if (zoned) { - if (std::ranges::any_of( - *sitesOpt, [](const fomac::Session::Device::Site& site) -> bool { - return !site.isZone(); - })) { + if (std::ranges::any_of(*sitesOpt, [](const fomac::Site& site) -> bool { + return !site.isZone(); + })) { SPDLOG_INFO("Operation marked as zoned but has non-zone sites"); return false; } @@ -577,8 +575,7 @@ auto Session::Device::initOperationsFromDevice() -> bool { } return true; } -Session::Device::Device(const fomac::Session::Device& device) - : fomac::Session::Device(device) {} + auto Session::getDevices() -> std::vector { std::vector devices; fomac::Session session; diff --git a/src/qasm3/Parser.cpp b/src/qasm3/Parser.cpp index 04ae35473b..ce45d9f1f2 100644 --- a/src/qasm3/Parser.cpp +++ b/src/qasm3/Parser.cpp @@ -168,7 +168,11 @@ std::shared_ptr Parser::parseStatement() { if (current().kind == Token::Kind::Const) { scan(); - return parseDeclaration(true); + return parseDeclaration(true, false); + } + if (current().kind == Token::Kind::Output) { + scan(); + return parseDeclaration(false, true); } if (current().kind == Token::Kind::Int || @@ -181,7 +185,7 @@ std::shared_ptr Parser::parseStatement() { current().kind == Token::Kind::Duration || current().kind == Token::Kind::CReg || current().kind == Token::Kind::Qreg) { - return parseDeclaration(false); + return parseDeclaration(false, false); } if (current().kind == Token::Kind::InitialLayout) { @@ -558,7 +562,8 @@ std::shared_ptr Parser::parseGateOperand() { return std::make_shared(parseIndexedIdentifier()); } -std::shared_ptr Parser::parseDeclaration(bool isConst) { +std::shared_ptr Parser::parseDeclaration(bool isConst, + bool isOutput) { auto const tBegin = current(); auto [type, isOldStyleDeclaration] = parseType(); Token const identifier = expect(Token::Kind::Identifier); @@ -588,7 +593,7 @@ std::shared_ptr Parser::parseDeclaration(bool isConst) { auto const tEnd = expect(Token::Kind::Semicolon); auto statement = std::make_shared(DeclarationStatement{ - makeDebugInfo(tBegin, tEnd), isConst, type, name, expression}); + makeDebugInfo(tBegin, tEnd), isConst, isOutput, type, name, expression}); return statement; } diff --git a/src/qdmi/devices/dd/Device.cpp b/src/qdmi/devices/dd/Device.cpp index d30de4973e..785a8c5460 100644 --- a/src/qdmi/devices/dd/Device.cpp +++ b/src/qdmi/devices/dd/Device.cpp @@ -483,9 +483,13 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::submitQIRProgramSampling() }, program_); auto jitSession = qir::JitSession(irBytes, "QDMI job"); + runtime.outputProgramHeader(); for (size_t i = 0; i < numShots_; ++i) { runtime.reset(); - if (const auto rc = jitSession.run(); rc != 0) { + runtime.outputShotStart(); + const auto rc = jitSession.run(); + runtime.outputShotEnd(); + if (rc != 0) { throw std::runtime_error( llvm::formatv("QIR program failed with error: {}", rc)); } diff --git a/src/qdmi/driver/Driver.cpp b/src/qdmi/driver/Driver.cpp index c2c1dba036..18b85434c7 100644 --- a/src/qdmi/driver/Driver.cpp +++ b/src/qdmi/driver/Driver.cpp @@ -275,6 +275,16 @@ namespace { return QDMI_DEVICE_JOB_PARAMETER_PROGRAMFORMAT; case QDMI_JOB_PARAMETER_SHOTSNUM: return QDMI_DEVICE_JOB_PARAMETER_SHOTSNUM; + case QDMI_JOB_PARAMETER_CUSTOM1: + return QDMI_DEVICE_JOB_PARAMETER_CUSTOM1; + case QDMI_JOB_PARAMETER_CUSTOM2: + return QDMI_DEVICE_JOB_PARAMETER_CUSTOM2; + case QDMI_JOB_PARAMETER_CUSTOM3: + return QDMI_DEVICE_JOB_PARAMETER_CUSTOM3; + case QDMI_JOB_PARAMETER_CUSTOM4: + return QDMI_DEVICE_JOB_PARAMETER_CUSTOM4; + case QDMI_JOB_PARAMETER_CUSTOM5: + return QDMI_DEVICE_JOB_PARAMETER_CUSTOM5; default: return QDMI_DEVICE_JOB_PARAMETER_MAX; } @@ -286,7 +296,8 @@ QDMI_Job_impl_d::~QDMI_Job_impl_d() { } auto QDMI_Job_impl_d::setParameter(QDMI_Job_Parameter param, const size_t size, const void* value) const -> int { - if ((value != nullptr && size == 0) || param >= QDMI_JOB_PARAMETER_MAX) { + if ((value != nullptr && size == 0) || + IS_INVALID_ARGUMENT(param, QDMI_JOB_PARAMETER)) { return QDMI_ERROR_INVALIDARGUMENT; } return device_->getLibrary().device_job_set_parameter( diff --git a/src/qir/jit/Session.cpp b/src/qir/jit/Session.cpp index db04ffd48f..947f3758ea 100644 --- a/src/qir/jit/Session.cpp +++ b/src/qir/jit/Session.cpp @@ -12,6 +12,7 @@ #include "qir/jit/IRRewriter.hpp" #include "qir/runtime/QIR.h" +#include "qir/runtime/Runtime.hpp" #include #include @@ -44,6 +45,7 @@ #include #include +#include #include #include #include @@ -57,6 +59,28 @@ namespace qir { +/// Returns the function marked with the `entry_point` attribute, +/// or @c nullptr if no such function is defined. +static const llvm::Function* getEntryPointFunction(const llvm::Module& m) { + const auto it = std::ranges::find_if(m, [](const llvm::Function& fn) { + return fn.hasFnAttribute("entry_point"); + }); + return it != m.end() ? &*it : nullptr; +} + +/// Read the `output_labeling_schema` attribute from the program's entry point. +/// Returns @c Labeled when the attribute is absent or its value is not +/// exactly `ordered` (spec default). +static Runtime::OutputSchema readOutputSchema(const llvm::Module& m) { + if (const auto* fn = getEntryPointFunction(m); fn != nullptr) { + if (const auto attr = fn->getFnAttribute("output_labeling_schema"); + attr.isValid() && attr.getValueAsString() == "ordered") { + return Runtime::OutputSchema::Ordered; + } + } + return Runtime::OutputSchema::Labeled; +} + static void exitOnLazyCallThroughFailure() { exit(1); } static int mingwNoopMain() { @@ -239,6 +263,11 @@ void JitSession::initialize( } module_ = std::move(*llvmModule); + // Configure the runtime's labeling schema from the program's entry point. + module_.withModuleDo([](const llvm::Module& m) { + Runtime::getInstance().setOutputSchema(readOutputSchema(m)); + }); + // In StateExtraction mode, strip QIR measurement and result-management calls // so the runtime's quantum state remains intact after main returns. if (mode == Execution::StateExtraction) { diff --git a/src/qir/runner/CMakeLists.txt b/src/qir/runner/CMakeLists.txt index 4577bbfa0a..f52b85bbfb 100644 --- a/src/qir/runner/CMakeLists.txt +++ b/src/qir/runner/CMakeLists.txt @@ -12,7 +12,7 @@ if(NOT TARGET ${TARGET_NAME}) add_llvm_tool(${TARGET_NAME} Runner.cpp DEPENDS intrinsics_gen EXPORT_SYMBOLS) # Add link libraries - target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQIRJIT) + target_link_libraries(${TARGET_NAME} PRIVATE MQT::CoreQIRJIT MQT::CoreQIRRuntime) # Set versioning information set_target_properties(${TARGET_NAME} PROPERTIES VERSION ${PROJECT_VERSION} EXPORT_NAME diff --git a/src/qir/runner/Runner.cpp b/src/qir/runner/Runner.cpp index 4ef8fc200e..b6626cec41 100644 --- a/src/qir/runner/Runner.cpp +++ b/src/qir/runner/Runner.cpp @@ -9,6 +9,7 @@ */ #include "qir/jit/Session.hpp" +#include "qir/runtime/Runtime.hpp" #include #include @@ -42,7 +43,12 @@ auto main(int argc, char* argv[]) -> int { try { auto jitSession = qir::JitSession(llvm::StringRef(InputFile)); - return jitSession.run(InputArgv, InputFile); + auto& runtime = qir::Runtime::getInstance(); + runtime.outputProgramHeader(); + runtime.outputShotStart(); + const auto rc = jitSession.run(InputArgv, InputFile); + runtime.outputShotEnd(); + return rc; } catch (const std::exception& e) { ExitOnError(llvm::createStringError(e.what())); } diff --git a/src/qir/runtime/QIR.cpp b/src/qir/runtime/QIR.cpp index 6f225d4f5f..1d756a063d 100644 --- a/src/qir/runtime/QIR.cpp +++ b/src/qir/runtime/QIR.cpp @@ -15,8 +15,6 @@ #include #include -#include -#include #include extern "C" { @@ -384,37 +382,30 @@ bool __quantum__rt__read_result(Result* result) { void __quantum__rt__result_record_output(Result* result, const char* label) { const bool bit = __quantum__rt__read_result(result); auto& runtime = qir::Runtime::getInstance(); - runtime.outputValue(bit ? "1" : "0", label); + runtime.outputResult(bit, label); // Accumulate new measurement bit. runtime.appendMeasurementBit(bit); } void __quantum__rt__bool_record_output(bool value, const char* label) { - qir::Runtime::getInstance().outputValue(value ? "1" : "0", label); + qir::Runtime::getInstance().outputBool(value, label); } void __quantum__rt__int_record_output(int64_t value, const char* label) { - qir::Runtime::getInstance().outputValue(std::to_string(value), label); + qir::Runtime::getInstance().outputInt(value, label); } void __quantum__rt__float_record_output(double value, const char* label) { - // Use std::ostringstream rather than std::to_string. - // std::to_string formats with six digits after the decimal point and - // can print 0.000000 for very small numbers. - // std::ostringstream uses six significant digits by default and - // outputs very small numbers with scientific notation. - std::ostringstream oss; - oss << value; - qir::Runtime::getInstance().outputValue(oss.str(), label); + qir::Runtime::getInstance().outputFloat(value, label); } void __quantum__rt__tuple_record_output(int64_t elementCount, const char* label) { - qir::Runtime::getInstance().outputContainer(elementCount, label); + qir::Runtime::getInstance().outputTuple(elementCount, label); } void __quantum__rt__array_record_output(int64_t size, const char* label) { - qir::Runtime::getInstance().outputContainer(size, label); + qir::Runtime::getInstance().outputArray(size, label); } } // extern "C" diff --git a/src/qir/runtime/Runtime.cpp b/src/qir/runtime/Runtime.cpp index 6c998b75ac..8b2555b2a9 100644 --- a/src/qir/runtime/Runtime.cpp +++ b/src/qir/runtime/Runtime.cpp @@ -179,20 +179,76 @@ auto Runtime::takeState() -> QState { return ret; } -auto Runtime::outputContainer(int64_t /* elementCount */, - const char* label) const -> void { - *os << (label != nullptr ? label : "") << ":\n"; +auto Runtime::getOstream() const -> std::ostream& { return *os; } + +auto Runtime::setOstream(std::ostream& other) -> void { os = &other; } + +auto Runtime::resetOstream() -> void { os = &std::cout; } + +void Runtime::outputType(const char* type, std::string_view value, + const char* label) const { + *os << "OUTPUT\t" << type << "\t" << value; + if (label != nullptr && outputSchema == OutputSchema::Labeled) { + *os << "\t" << label; + } + *os << "\n"; +} + +auto Runtime::outputResult(bool value, const char* label) const -> void { + outputType("RESULT", value ? "1" : "0", label); +} + +auto Runtime::outputBool(bool value, const char* label) const -> void { + outputType("BOOL", value ? "true" : "false", label); +} + +auto Runtime::outputInt(int64_t value, const char* label) const -> void { + outputType("INT", std::to_string(value), label); +} + +auto Runtime::outputFloat(double value, const char* label) const -> void { + // Use std::ostringstream rather than std::to_string. + // std::to_string formats with six digits after the decimal point and + // can print 0.000000 for very small numbers. + // std::ostringstream uses six significant digits by default and + // outputs very small numbers with scientific notation. + std::ostringstream oss; + oss << value; + outputType("DOUBLE", oss.str(), label); } -auto Runtime::outputValue(std::string_view valueStr, const char* label) const +auto Runtime::outputTuple(int64_t elementCount, const char* label) const -> void { - *os << (label != nullptr ? label : "") << ": " << valueStr << "\n"; + outputType("TUPLE", std::to_string(elementCount), label); } -auto Runtime::getOstream() const -> std::ostream& { return *os; } +auto Runtime::outputArray(int64_t elementCount, const char* label) const + -> void { + outputType("ARRAY", std::to_string(elementCount), label); +} -auto Runtime::setOstream(std::ostream& other) -> void { os = &other; } +auto Runtime::outputProgramHeader() const -> void { + *os << "HEADER\tschema_id\t" << outputSchema << "\n"; + *os << "HEADER\tschema_version\t2.1\n"; +} -auto Runtime::resetOstream() -> void { os = &std::cout; } +auto Runtime::outputShotStart() const -> void { + *os << "START\n"; + *os << "METADATA\toutput_labeling_schema\t" << outputSchema << "\n"; +} + +auto Runtime::outputShotEnd() const -> void { *os << "END\t0\n"; } + +auto Runtime::getOutputSchema() const -> OutputSchema { return outputSchema; } + +auto Runtime::setOutputSchema(OutputSchema schema) -> void { + outputSchema = schema; +} + +auto operator<<(std::ostream& os, const Runtime::OutputSchema schema) + -> std::ostream& { + return os << (schema == Runtime::OutputSchema::Labeled ? "labeled" + : "ordered"); +} } // namespace qir diff --git a/test/fomac/test_fomac.cpp b/test/fomac/test_fomac.cpp index a3166e5f67..57a1229a78 100644 --- a/test/fomac/test_fomac.cpp +++ b/test/fomac/test_fomac.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -32,35 +33,35 @@ namespace fomac { namespace { -class DeviceTest : public testing::TestWithParam { +class DeviceTest : public testing::TestWithParam { protected: - Session::Device device; + Device device; DeviceTest() : device(GetParam()) {} }; class SiteTest : public DeviceTest { protected: - std::vector sites; + std::vector sites; void SetUp() override { sites = device.getSites(); } }; class OperationTest : public DeviceTest { protected: - std::vector operations; + std::vector operations; void SetUp() override { operations = device.getOperations(); } }; class DDSimulatorDeviceTest : public testing::Test { protected: - Session::Device device; + Device device; DDSimulatorDeviceTest() : device(getDDSimulatorDevice()) {} private: - static auto getDDSimulatorDevice() -> Session::Device { + static auto getDDSimulatorDevice() -> Device { Session session; for (const auto& dev : session.getDevices()) { if (dev.getName() == "MQT Core DDSIM QDMI Device") { @@ -73,11 +74,11 @@ class DDSimulatorDeviceTest : public testing::Test { class JobTest : public DDSimulatorDeviceTest { protected: - Session::Job job; + Job job; JobTest() : job(createTestJob()) {} - [[nodiscard]] Session::Job createTestJob() const { + [[nodiscard]] Job createTestJob() const { const std::string qasm3Program = R"( OPENQASM 3.0; qubit[1] q; @@ -91,11 +92,11 @@ c[0] = measure q[0]; class SimulatorJobTest : public DDSimulatorDeviceTest { protected: - Session::Job job; + Job job; SimulatorJobTest() : job(createTestJob()) {} - [[nodiscard]] Session::Job createTestJob() const { + [[nodiscard]] Job createTestJob() const { const std::string qasm3Program = R"( OPENQASM 3.0; qubit[2] q; @@ -556,6 +557,49 @@ c = measure q;)"; EXPECT_EQ(job.check(), QDMI_JOB_STATUS_DONE); } +TEST_F(DDSimulatorDeviceTest, SubmitJobCustomSupportedTypes) { + constexpr auto qasm3Program = "OPENQASM 3.0;"; + + auto submitWithCustoms = [&](auto custom, const size_t which) { + try { + switch (which) { + case 1: + device.submitJob(qasm3Program, QDMI_PROGRAM_FORMAT_QASM3, 10, custom); + break; + case 2: + device.submitJob(qasm3Program, QDMI_PROGRAM_FORMAT_QASM3, 10, + std::nullopt, custom); + break; + case 3: + device.submitJob(qasm3Program, QDMI_PROGRAM_FORMAT_QASM3, 10, + std::nullopt, std::nullopt, custom); + break; + case 4: + device.submitJob(qasm3Program, QDMI_PROGRAM_FORMAT_QASM3, 10, + std::nullopt, std::nullopt, std::nullopt, custom); + break; + case 5: + device.submitJob(qasm3Program, QDMI_PROGRAM_FORMAT_QASM3, 10, + std::nullopt, std::nullopt, std::nullopt, std::nullopt, + custom); + break; + default: + throw std::invalid_argument("Invalid 'which' value"); + } + } catch (const std::runtime_error& e) { + const std::string errorMsg(e.what()); + EXPECT_TRUE(errorMsg.find("Setting custom parameter") != + std::string::npos); + } + }; + for (size_t i = 1; i <= 5; ++i) { + submitWithCustoms(std::string("custom"), i); + submitWithCustoms(42, i); + submitWithCustoms(3.14, i); + submitWithCustoms(true, i); + } +} + TEST_F(DDSimulatorDeviceTest, SubmitJobPreservesNumShots) { const std::string qasm3Program = R"( OPENQASM 3.0; @@ -993,7 +1037,7 @@ TEST(AuthenticationTest, SessionMultipleInstances) { namespace { // Helper function to get all devices for parameterized tests -auto getDevices() -> std::vector { +auto getDevices() -> std::vector { Session session; return session.getDevices(); } @@ -1006,7 +1050,7 @@ INSTANTIATE_TEST_SUITE_P( DeviceTest, // Parameters to test with testing::ValuesIn(getDevices()), - [](const testing::TestParamInfo& paramInfo) { + [](const testing::TestParamInfo& paramInfo) { auto name = paramInfo.param.getName(); // Replace spaces with underscores for valid test names std::ranges::replace(name, ' ', '_'); @@ -1020,7 +1064,7 @@ INSTANTIATE_TEST_SUITE_P( SiteTest, // Parameters to test with testing::ValuesIn(getDevices()), - [](const testing::TestParamInfo& paramInfo) { + [](const testing::TestParamInfo& paramInfo) { auto name = paramInfo.param.getName(); // Replace spaces with underscores for valid test names std::ranges::replace(name, ' ', '_'); @@ -1034,7 +1078,7 @@ INSTANTIATE_TEST_SUITE_P( OperationTest, // Parameters to test with testing::ValuesIn(getDevices()), - [](const testing::TestParamInfo& paramInfo) { + [](const testing::TestParamInfo& paramInfo) { auto name = paramInfo.param.getName(); // Replace spaces with underscores for valid test names std::ranges::replace(name, ' ', '_'); diff --git a/test/python/fomac/test_fomac.py b/test/python/fomac/test_fomac.py index e53a04799b..42c1264ea6 100644 --- a/test/python/fomac/test_fomac.py +++ b/test/python/fomac/test_fomac.py @@ -421,6 +421,20 @@ def test_device_submit_job_returns_valid_job(ddsim_device: Device) -> None: assert job.num_shots == 100 +def test_device_submit_job_handles_custom_parameters(ddsim_device: Device) -> None: + """Test that submit_job forwards custom job parameters to DDSIM.""" + with pytest.raises(RuntimeError, match=r"Setting custom parameter: Not supported\."): + ddsim_device.submit_job("OPENQASM 3.0;", ProgramFormat.QASM3, 1, custom1="value") + with pytest.raises(RuntimeError, match=r"Setting custom parameter: Not supported\."): + ddsim_device.submit_job("OPENQASM 3.0;", ProgramFormat.QASM3, 1, custom2="value") + with pytest.raises(RuntimeError, match=r"Setting custom parameter: Not supported\."): + ddsim_device.submit_job("OPENQASM 3.0;", ProgramFormat.QASM3, 1, custom3="value") + with pytest.raises(RuntimeError, match=r"Setting custom parameter: Not supported\."): + ddsim_device.submit_job("OPENQASM 3.0;", ProgramFormat.QASM3, 1, custom4="value") + with pytest.raises(RuntimeError, match=r"Setting custom parameter: Not supported\."): + ddsim_device.submit_job("OPENQASM 3.0;", ProgramFormat.QASM3, 1, custom5="value") + + def test_device_submit_job_preserves_num_shots(ddsim_device: Device) -> None: """Test that different shot counts are correctly preserved.""" qasm3_program = """ diff --git a/test/qdmi/driver/test_driver.cpp b/test/qdmi/driver/test_driver.cpp index 42b876e5c1..79b2d89a84 100644 --- a/test/qdmi/driver/test_driver.cpp +++ b/test/qdmi/driver/test_driver.cpp @@ -168,6 +168,14 @@ TEST_P(DriverJobTest, JobSetParameter) { EXPECT_THAT(QDMI_job_set_parameter(job, QDMI_JOB_PARAMETER_SHOTSNUM, sizeof(size_t), &numShots), testing::AnyOf(QDMI_SUCCESS, QDMI_ERROR_NOTSUPPORTED)); + constexpr std::array customParams{ + QDMI_JOB_PARAMETER_CUSTOM1, QDMI_JOB_PARAMETER_CUSTOM2, + QDMI_JOB_PARAMETER_CUSTOM3, QDMI_JOB_PARAMETER_CUSTOM4, + QDMI_JOB_PARAMETER_CUSTOM5}; + for (const auto param : customParams) { + EXPECT_THAT(QDMI_job_set_parameter(job, param, 0, nullptr), + testing::AnyOf(QDMI_SUCCESS, QDMI_ERROR_NOTSUPPORTED)); + } EXPECT_EQ(QDMI_job_set_parameter(job, QDMI_JOB_PARAMETER_MAX, 0, nullptr), QDMI_ERROR_INVALIDARGUMENT); } diff --git a/test/qir/jit/test_jit_session.cpp b/test/qir/jit/test_jit_session.cpp index 3c614dd18f..8a7a362dda 100644 --- a/test/qir/jit/test_jit_session.cpp +++ b/test/qir/jit/test_jit_session.cpp @@ -30,7 +30,11 @@ class JitSessionTest : public testing::Test { runtime.reset(); runtime.setOstream(sink); } - void TearDown() override { qir::Runtime::getInstance().resetOstream(); } + void TearDown() override { + auto& runtime = qir::Runtime::getInstance(); + runtime.resetOstream(); + runtime.setOutputSchema(qir::Runtime::OutputSchema::Labeled); + } }; TEST_F(JitSessionTest, LoadModuleFromMemory) { @@ -55,6 +59,39 @@ TEST_F(JitSessionTest, StateExtractionLeavesNoRecordedOutputs) { EXPECT_TRUE(qir::Runtime::getInstance().getMeasurements().empty()); } +TEST_F(JitSessionTest, OutputSchemaDefaultsToLabeledWhenAttributeAbsent) { + constexpr std::string_view ir = R"( +define i32 @main() #0 { ret i32 0 } +attributes #0 = { "entry_point" } +)"; + // Preset @c Ordered so we can tell the default kicked in. + qir::Runtime::getInstance().setOutputSchema( + qir::Runtime::OutputSchema::Ordered); + const qir::JitSession session(ir, "NoOutputSchema.ll"); + EXPECT_EQ(qir::Runtime::getInstance().getOutputSchema(), + qir::Runtime::OutputSchema::Labeled); +} + +TEST_F(JitSessionTest, OutputSchemaFromLabeledAttribute) { + constexpr std::string_view ir = R"( +define i32 @main() #0 { ret i32 0 } +attributes #0 = { "entry_point" "output_labeling_schema"="labeled" } +)"; + const qir::JitSession session(ir, "LabeledOutputSchema.ll"); + EXPECT_EQ(qir::Runtime::getInstance().getOutputSchema(), + qir::Runtime::OutputSchema::Labeled); +} + +TEST_F(JitSessionTest, OutputSchemaFromOrderedAttribute) { + constexpr std::string_view ir = R"( +define i32 @main() #0 { ret i32 0 } +attributes #0 = { "entry_point" "output_labeling_schema"="ordered" } +)"; + const qir::JitSession session(ir, "OrderedOutputSchema.ll"); + EXPECT_EQ(qir::Runtime::getInstance().getOutputSchema(), + qir::Runtime::OutputSchema::Ordered); +} + TEST(JitSessionErrors, MalformedIRThrows) { constexpr std::string_view ir = R"(define i32 @main() {})"; EXPECT_THROW(qir::JitSession(ir, "MalformedIR.ll"), std::runtime_error); diff --git a/test/qir/runtime/test_qir_runtime.cpp b/test/qir/runtime/test_qir_runtime.cpp index 4a08b0030b..2987e0870e 100644 --- a/test/qir/runtime/test_qir_runtime.cpp +++ b/test/qir/runtime/test_qir_runtime.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #ifdef _WIN32 @@ -36,11 +37,89 @@ class QIRRuntimeTest : public testing::Test { protected: std::ostringstream sink; void SetUp() override { Runtime::getInstance().setOstream(sink); } - void TearDown() override { Runtime::getInstance().resetOstream(); } + void TearDown() override { + Runtime::getInstance().resetOstream(); + Runtime::getInstance().setOutputSchema(Runtime::OutputSchema::Labeled); + } }; } // namespace +// Any test that emits output relies on the runtime producing the spec-mandated +// HEADER/START/METADATA/END records around the per-shot OUTPUT block. +// The runtime picks @c Labeled as default output schema, which is why the +// the framing emits `labeled` in both HEADER and METADATA here. +TEST_F(QIRRuntimeTest, OutputFraming) { + auto& runtime = Runtime::getInstance(); + runtime.outputProgramHeader(); + runtime.outputShotStart(); + runtime.outputShotEnd(); + std::ostringstream expected; + expected << "HEADER\tschema_id\tlabeled\n" + << "HEADER\tschema_version\t2.1\n" + << "START\n" + << "METADATA\toutput_labeling_schema\tlabeled\n" + << "END\t0\n"; + EXPECT_EQ(sink.str(), expected.str()); +} + +// In Labeled mode: +// - the HEADER announces `labeled`, +// - the per-shot METADATA line matches the output schema, and +// - OUTPUT records carry the label column. +TEST_F(QIRRuntimeTest, OutputFramingLabeled) { + auto& runtime = Runtime::getInstance(); + runtime.outputProgramHeader(); + runtime.outputShotStart(); + runtime.outputBool(true, "bool_label"); + runtime.outputInt(42, "int_label"); + runtime.outputFloat(3.14, "double_label"); + runtime.outputTuple(2, "tuple_label"); + runtime.outputArray(3, "array_label"); + runtime.outputShotEnd(); + std::ostringstream expected; + expected << "HEADER\tschema_id\tlabeled\n" + << "HEADER\tschema_version\t2.1\n" + << "START\n" + << "METADATA\toutput_labeling_schema\tlabeled\n" + << "OUTPUT\tBOOL\ttrue\tbool_label\n" + << "OUTPUT\tINT\t42\tint_label\n" + << "OUTPUT\tDOUBLE\t3.14\tdouble_label\n" + << "OUTPUT\tTUPLE\t2\ttuple_label\n" + << "OUTPUT\tARRAY\t3\tarray_label\n" + << "END\t0\n"; + EXPECT_EQ(sink.str(), expected.str()); +} + +// In Ordered mode: +// - the HEADER announces `ordered`, +// - the per-shot METADATA line matches the output schema, and +// - OUTPUT records drop the label column. +TEST_F(QIRRuntimeTest, OutputFramingOrdered) { + auto& runtime = Runtime::getInstance(); + runtime.setOutputSchema(Runtime::OutputSchema::Ordered); + runtime.outputProgramHeader(); + runtime.outputShotStart(); + runtime.outputBool(true, "bool_label"); + runtime.outputInt(42, "int_label"); + runtime.outputFloat(3.14, "double_label"); + runtime.outputTuple(2, "tuple_label"); + runtime.outputArray(3, "array_label"); + runtime.outputShotEnd(); + std::ostringstream expected; + expected << "HEADER\tschema_id\tordered\n" + << "HEADER\tschema_version\t2.1\n" + << "START\n" + << "METADATA\toutput_labeling_schema\tordered\n" + << "OUTPUT\tBOOL\ttrue\n" + << "OUTPUT\tINT\t42\n" + << "OUTPUT\tDOUBLE\t3.14\n" + << "OUTPUT\tTUPLE\t2\n" + << "OUTPUT\tARRAY\t3\n" + << "END\t0\n"; + EXPECT_EQ(sink.str(), expected.str()); +} + TEST_F(QIRRuntimeTest, XGate) { auto* q0 = reinterpret_cast(0UL); __quantum__rt__initialize(nullptr); @@ -256,13 +335,19 @@ TEST_F(QIRRuntimeTest, SwapGate) { auto* r0 = reinterpret_cast(0UL); auto* r1 = reinterpret_cast(1UL); __quantum__rt__initialize(nullptr); + Runtime::getInstance().outputProgramHeader(); + Runtime::getInstance().outputShotStart(); __quantum__qis__x__body(q0); __quantum__qis__swap__body(q0, q1); __quantum__qis__mz__body(q0, r0); __quantum__qis__mz__body(q1, r1); __quantum__rt__result_record_output(r0, "r0"); __quantum__rt__result_record_output(r1, "r1"); - EXPECT_EQ(sink.str(), "r0: 0\nr1: 1\n"); + Runtime::getInstance().outputShotEnd(); + std::ostringstream expected; + expected << "OUTPUT\tRESULT\t0\tr0\n" + << "OUTPUT\tRESULT\t1\tr1\n"; + EXPECT_THAT(sink.str(), ::testing::HasSubstr(expected.str())); } TEST_F(QIRRuntimeTest, CSwapGate) { @@ -356,6 +441,8 @@ TEST_F(QIRRuntimeTest, BellPairStatic) { auto* r0 = reinterpret_cast(0UL); auto* r1 = reinterpret_cast(1UL); __quantum__rt__initialize(nullptr); + Runtime::getInstance().outputProgramHeader(); + Runtime::getInstance().outputShotStart(); __quantum__qis__h__body(q0); __quantum__qis__cx__body(q0, q1); __quantum__qis__mz__body(q0, r0); @@ -365,11 +452,17 @@ TEST_F(QIRRuntimeTest, BellPairStatic) { EXPECT_EQ(m1, m2); __quantum__rt__result_record_output(r0, "r0"); __quantum__rt__result_record_output(r1, "r1"); - EXPECT_THAT(sink.str(), testing::AnyOf("r0: 0\nr1: 0\n", "r0: 1\nr1: 1\n")); + Runtime::getInstance().outputShotEnd(); + std::ostringstream expected; + expected << "OUTPUT\tRESULT\t" << m1 << "\tr0\n" + << "OUTPUT\tRESULT\t" << m2 << "\tr1\n"; + EXPECT_THAT(sink.str(), ::testing::HasSubstr(expected.str())); } TEST_F(QIRRuntimeTest, BellPairDynamic) { __quantum__rt__initialize(nullptr); + Runtime::getInstance().outputProgramHeader(); + Runtime::getInstance().outputShotStart(); auto* q0 = __quantum__rt__qubit_allocate(); auto* q1 = __quantum__rt__qubit_allocate(); __quantum__qis__h__body(q0); @@ -383,7 +476,11 @@ TEST_F(QIRRuntimeTest, BellPairDynamic) { EXPECT_EQ(m1, m2); __quantum__rt__result_record_output(r0, "r0"); __quantum__rt__result_record_output(r1, "r1"); - EXPECT_THAT(sink.str(), testing::AnyOf("r0: 0\nr1: 0\n", "r0: 1\nr1: 1\n")); + Runtime::getInstance().outputShotEnd(); + std::ostringstream expected; + expected << "OUTPUT\tRESULT\t" << m1 << "\tr0\n" + << "OUTPUT\tRESULT\t" << m2 << "\tr1\n"; + EXPECT_THAT(sink.str(), ::testing::HasSubstr(expected.str())); __quantum__rt__result_update_reference_count(r0, -1); __quantum__rt__result_update_reference_count(r1, -1); } @@ -394,6 +491,8 @@ TEST_F(QIRRuntimeTest, BellPairStaticReverse) { auto* r0 = reinterpret_cast(0UL); auto* r1 = reinterpret_cast(1UL); __quantum__rt__initialize(nullptr); + Runtime::getInstance().outputProgramHeader(); + Runtime::getInstance().outputShotStart(); __quantum__qis__h__body(q1); __quantum__qis__cx__body(q1, q0); __quantum__qis__mz__body(q0, r0); @@ -403,11 +502,17 @@ TEST_F(QIRRuntimeTest, BellPairStaticReverse) { EXPECT_EQ(m1, m2); __quantum__rt__result_record_output(r0, "r0"); __quantum__rt__result_record_output(r1, "r1"); - EXPECT_THAT(sink.str(), testing::AnyOf("r0: 0\nr1: 0\n", "r0: 1\nr1: 1\n")); + Runtime::getInstance().outputShotEnd(); + std::ostringstream expected; + expected << "OUTPUT\tRESULT\t" << m1 << "\tr0\n" + << "OUTPUT\tRESULT\t" << m2 << "\tr1\n"; + EXPECT_THAT(sink.str(), ::testing::HasSubstr(expected.str())); } TEST_F(QIRRuntimeTest, BellPairDynamicReverse) { __quantum__rt__initialize(nullptr); + Runtime::getInstance().outputProgramHeader(); + Runtime::getInstance().outputShotStart(); auto* q0 = __quantum__rt__qubit_allocate(); auto* q1 = __quantum__rt__qubit_allocate(); __quantum__qis__h__body(q1); @@ -421,7 +526,11 @@ TEST_F(QIRRuntimeTest, BellPairDynamicReverse) { EXPECT_EQ(m1, m2); __quantum__rt__result_record_output(r0, "r0"); __quantum__rt__result_record_output(r1, "r1"); - EXPECT_THAT(sink.str(), testing::AnyOf("r0: 0\nr1: 0\n", "r0: 1\nr1: 1\n")); + Runtime::getInstance().outputShotEnd(); + std::ostringstream expected; + expected << "OUTPUT\tRESULT\t" << m1 << "\tr0\n" + << "OUTPUT\tRESULT\t" << m2 << "\tr1\n"; + EXPECT_THAT(sink.str(), ::testing::HasSubstr(expected.str())); __quantum__rt__result_update_reference_count(r0, -1); __quantum__rt__result_update_reference_count(r1, -1); } @@ -530,6 +639,8 @@ TEST_F(QIRRuntimeTest, TakeStateReturnsStateAndResetsRuntime) { TEST_F(QIRRuntimeTest, AdaptiveRecordOutputs) { __quantum__rt__initialize(nullptr); + Runtime::getInstance().outputProgramHeader(); + Runtime::getInstance().outputShotStart(); auto* q0 = __quantum__rt__qubit_allocate(); auto* q1 = __quantum__rt__qubit_allocate(); auto* q2 = __quantum__rt__qubit_allocate(); @@ -553,22 +664,24 @@ TEST_F(QIRRuntimeTest, AdaptiveRecordOutputs) { // Output: tuple of 3 elements (array of 3 bools, int weight, float mean). __quantum__rt__tuple_record_output(3, "outputs"); - __quantum__rt__array_record_output(3, " measurements"); - __quantum__rt__bool_record_output(b0, " m0"); - __quantum__rt__bool_record_output(b1, " m1"); - __quantum__rt__bool_record_output(b2, " m2"); - __quantum__rt__int_record_output(weight, " hamming_weight"); - __quantum__rt__float_record_output(mean, " mean"); + __quantum__rt__array_record_output(3, "measurements"); + __quantum__rt__bool_record_output(b0, "m0"); + __quantum__rt__bool_record_output(b1, "m1"); + __quantum__rt__bool_record_output(b2, "m2"); + __quantum__rt__int_record_output(weight, "hamming_weight"); + __quantum__rt__float_record_output(mean, "mean"); + Runtime::getInstance().outputShotEnd(); std::ostringstream expected; - expected << "outputs:\n" - << " measurements:\n" - << " m0: " << b0 << "\n" - << " m1: " << b1 << "\n" - << " m2: " << b2 << "\n" - << " hamming_weight: " << weight << "\n" - << " mean: " << mean << "\n"; - EXPECT_EQ(sink.str(), expected.str()); + expected.setf(std::ios::boolalpha); + expected << "OUTPUT\tTUPLE\t3\toutputs\n" + << "OUTPUT\tARRAY\t3\tmeasurements\n" + << "OUTPUT\tBOOL\t" << b0 << "\tm0\n" + << "OUTPUT\tBOOL\t" << b1 << "\tm1\n" + << "OUTPUT\tBOOL\t" << b2 << "\tm2\n" + << "OUTPUT\tINT\t" << weight << "\thamming_weight\n" + << "OUTPUT\tDOUBLE\t" << mean << "\tmean\n"; + EXPECT_THAT(sink.str(), ::testing::HasSubstr(expected.str())); __quantum__rt__result_update_reference_count(r0, -1); __quantum__rt__result_update_reference_count(r1, -1); diff --git a/uv.lock b/uv.lock index 1107582cdf..0dcee73873 100644 --- a/uv.lock +++ b/uv.lock @@ -79,11 +79,11 @@ wheels = [ [[package]] name = "asttokens" -version = "3.0.1" +version = "3.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } +sdist = { url = "https://files.pythonhosted.org/packages/25/1e/faf0f247f6f881b98fc4d6d07e14085cb89d13665084e6d6ac1dc2c03d0b/asttokens-3.0.2.tar.gz", hash = "sha256:3ecdbd8f2cc195f53ccada3a613538bb5f9ef6f6869129f13e03c30a677b8fe2", size = 63136, upload-time = "2026-07-12T03:31:49.084Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, + { url = "https://files.pythonhosted.org/packages/d4/2b/04b8a15f3a1c77bc79ddf5c73875327f34b4fa75982df2b76e45e402d364/asttokens-3.0.2-py3-none-any.whl", hash = "sha256:9da13157f5b28becde0bd374fc677dcd3c290614264eff096f167c469cd9f933", size = 28702, upload-time = "2026-07-12T03:31:47.542Z" }, ] [[package]] @@ -142,189 +142,199 @@ wheels = [ [[package]] name = "cffi" -version = "2.0.0" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, - { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, - { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, - { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, - { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, - { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, - { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, - { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, - { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, - { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, - { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, - { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, - { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, - { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, - { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, - { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, - { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, - { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, - { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, - { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, - { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, - { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, - { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, - { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, - { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, - { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, - { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, - { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, - { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, - { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, - { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, - { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, - { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, - { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, - { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, - { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, - { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, - { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, - { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, - { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, - { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, - { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, - { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, - { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, - { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, - { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, - { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, - { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, - { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, - { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, - { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, - { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, - { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, - { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, - { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, - { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, - { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/e9/6d7724983b3d5a0908dbf74f64038ade77c18646ff6636ec7894fd392ce1/cffi-2.1.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:b65f590ef2a44640f9a05dbb548a429b4ade77913ce683ac8b1480777658a6c0", size = 183837, upload-time = "2026-07-06T21:32:09.655Z" }, + { url = "https://files.pythonhosted.org/packages/69/aa/24580a278de21fd7322635556334d9b535f1cbc00b0a3919447cdf464c65/cffi-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164bff1657b2a74f0b6d54e11c9b375bc97b931f2ca9c43fcf875838da1570dd", size = 184226, upload-time = "2026-07-06T21:32:11.196Z" }, + { url = "https://files.pythonhosted.org/packages/88/a9/02cae418ec4beb282ace11958d9d4737793439d561fadc7e6d56f2e2b354/cffi-2.1.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:c941bb58d5a6e1c3892d86e42927ed6c180302f07e6d395d08c416e594b98b46", size = 211107, upload-time = "2026-07-06T21:32:12.328Z" }, + { url = "https://files.pythonhosted.org/packages/3b/30/c806937ed5e4c2c7ac30d9d6b76b5dc57ff8b75d83800d9bb11a8253cf2a/cffi-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a016194dbe13d14ee9556e734b772d8d67b947092b268d757fd4290e3ba2dfc2", size = 218733, upload-time = "2026-07-06T21:32:13.67Z" }, + { url = "https://files.pythonhosted.org/packages/f9/cf/398272b8bbfd58aa314fda5a7f1cdbb26d1d78ae324a11211521315dd1f0/cffi-2.1.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:03e9810d18c646077e501f661b682fbf5dee4676048527ca3cffe66faa9960dd", size = 205543, upload-time = "2026-07-06T21:32:15.148Z" }, + { url = "https://files.pythonhosted.org/packages/45/ca/f91641185cdd90c36d317a9dc7f85e88ef8682d8b300977baff5e23c35d8/cffi-2.1.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:19c54ac121cad98450b4896fa9a43ee0180d57bc4bc911a33db6cab1efab6cd3", size = 205460, upload-time = "2026-07-06T21:32:16.479Z" }, + { url = "https://files.pythonhosted.org/packages/38/66/04781a77b411f0bb5b234d62c1814754ab75ebe455ccff1b08e8d7aae98f/cffi-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d433a51f1870e43a13b6732f92aaf540ff77c2015097c78556f75a2d6c030e0", size = 218760, upload-time = "2026-07-06T21:32:17.98Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9a/bb1d5ed9c3fcae158e9f6391bf309c95d98c2ac37ed56573228471d0af5e/cffi-2.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d7f118b5adbfdfead90c25822690b02bc8074fba949bb7858bec4ebd55adb43", size = 221230, upload-time = "2026-07-06T21:32:19.407Z" }, + { url = "https://files.pythonhosted.org/packages/41/aa/3c1409cdd26094efacd1c36c66e0a6eb9d4296e4fd4f9901b8b2042f4323/cffi-2.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c5f5df567f6eb216de69be06ce55c8b714090fae02b18a3b40da8163b8c5fa9c", size = 213524, upload-time = "2026-07-06T21:32:20.828Z" }, + { url = "https://files.pythonhosted.org/packages/fa/75/74dfb7c3fc6ebbd408038476bd4c1d7e925c62614e7b9c534ecc34218288/cffi-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:11b3fb55f4f8ad92274ed26705f65d8f91457de71f5380061eb6d125a768fecd", size = 220341, upload-time = "2026-07-06T21:32:21.9Z" }, + { url = "https://files.pythonhosted.org/packages/70/b6/9003c33a3e7d2c1306f5962e646457dcfe5a8cd8fce6bbe02d7af25db783/cffi-2.1.0-cp310-cp310-win32.whl", hash = "sha256:9d72af0cf10a76a600a9690078fe31c63b9588c8e86bf9fd353f713c84b5db0f", size = 174578, upload-time = "2026-07-06T21:32:23.073Z" }, + { url = "https://files.pythonhosted.org/packages/8a/26/710688310447531c7a22f857c7f79d9855ec18b03e04494ced723fb37e2f/cffi-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb62edb5bb52cca65fab91a63afa7561607120d26090a7e8fda6fb9f064726da", size = 185071, upload-time = "2026-07-06T21:32:24.671Z" }, + { url = "https://files.pythonhosted.org/packages/d3/67/85c89a59ba36a671e79638f44d466749f08179266a57e4f2ffdf92174072/cffi-2.1.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc", size = 183845, upload-time = "2026-07-06T21:32:26.32Z" }, + { url = "https://files.pythonhosted.org/packages/ea/dd/e3b0baa2d3d6a857ac72b7efbf18e32e487c9cdafcc13049ad765495b15e/cffi-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7", size = 184186, upload-time = "2026-07-06T21:32:28.025Z" }, + { url = "https://files.pythonhosted.org/packages/65/68/9f3ef890cf3c6ab97bd531c5677f67613d302165d16f8142b2811782a614/cffi-2.1.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93", size = 211892, upload-time = "2026-07-06T21:32:29.565Z" }, + { url = "https://files.pythonhosted.org/packages/22/d7/1a74539db16d8bfd839ff1515948948efbb162e574650fd3d846896eea95/cffi-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2", size = 218793, upload-time = "2026-07-06T21:32:30.951Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d1/9a5b7169499e8e8d8e636de70b97ac7c9447104d2ff1a2cd94790cea5162/cffi-2.1.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c", size = 205737, upload-time = "2026-07-06T21:32:32.216Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b0/e131a9c41f10607926278453d9596163594fe1c4ebc46efe3b5e5b34eb84/cffi-2.1.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f", size = 204909, upload-time = "2026-07-06T21:32:33.655Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d2/4398416cd699b35167947c6e22aca52c47e69ad5695073c9f1f2c52e04aa/cffi-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565", size = 217883, upload-time = "2026-07-06T21:32:35.173Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a5/d4fe77b589e5e82d43ebc809bf2e6474afe8e48e32ea050b9357645b6471/cffi-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c", size = 221251, upload-time = "2026-07-06T21:32:36.527Z" }, + { url = "https://files.pythonhosted.org/packages/22/f0/a2fc43084c0433caf7f461bccc013e28f848d04ee1c5ed7fce71423cf4d9/cffi-2.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02", size = 214250, upload-time = "2026-07-06T21:32:37.852Z" }, + { url = "https://files.pythonhosted.org/packages/04/8c/b925975448cf20634a9fbd5efceb807219db452653648d2897c0989cab2d/cffi-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e", size = 219441, upload-time = "2026-07-06T21:32:39.146Z" }, + { url = "https://files.pythonhosted.org/packages/eb/da/5c4918a2d61d86fa927d716cb3d8e4626ef8dc8f605a599d32f33897f59a/cffi-2.1.0-cp311-cp311-win32.whl", hash = "sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479", size = 174496, upload-time = "2026-07-06T21:32:40.467Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c8/6c2de1d55cf35ef8b92885d5ef280790f0fb9634d87ea1cc315176aecd61/cffi-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458", size = 185113, upload-time = "2026-07-06T21:32:41.761Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4e/e8d7cb5783f1841a3c8fb3a7735838d7484d08ec08c9f984b14cac1ac0e9/cffi-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d", size = 179927, upload-time = "2026-07-06T21:32:42.961Z" }, + { url = "https://files.pythonhosted.org/packages/1e/85/990925db5df586ec90beb97529c853497e7f85ba0234830447faf41c3057/cffi-2.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f", size = 184829, upload-time = "2026-07-06T21:32:44.324Z" }, + { url = "https://files.pythonhosted.org/packages/4b/92/e7bb136ad6b5352603732cf907ef862ca103f20f2031c1735a46300c20c9/cffi-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde", size = 184728, upload-time = "2026-07-06T21:32:45.683Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c0/d1ec30ffb370f748f2fb54425972bfef9871e0132e82fb589c46b6676049/cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d", size = 214815, upload-time = "2026-07-06T21:32:48.557Z" }, + { url = "https://files.pythonhosted.org/packages/1b/dc/5620cf930688be01f2d673804291de757a934c90b946dbdc3d84130c2ea4/cffi-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7", size = 222429, upload-time = "2026-07-06T21:32:49.848Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a4/77b53abbf7a1e0beb9637edbef2a94d15f9c822f591e85d439ffd91519a6/cffi-2.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b", size = 210315, upload-time = "2026-07-06T21:32:51.221Z" }, + { url = "https://files.pythonhosted.org/packages/58/0c/f528df19cc94b675087324d4760d9e6d5bfae97d6217aa4fac43de4f5fcc/cffi-2.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7", size = 208859, upload-time = "2026-07-06T21:32:52.512Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/c9522a81c32132799a1972c39f5c5f8b4c8b9f00488a23feaa6c06f07741/cffi-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66", size = 221844, upload-time = "2026-07-06T21:32:53.704Z" }, + { url = "https://files.pythonhosted.org/packages/6e/28/bd53988b9833e8f8ad539d26f4c07a6b3f6bcb1e9e02e7ca038250b3428d/cffi-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe", size = 225287, upload-time = "2026-07-06T21:32:54.907Z" }, + { url = "https://files.pythonhosted.org/packages/79/99/0d0fd37f055224085f42bbb2c022d002e17dde4a97972822327b07d84101/cffi-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b", size = 223681, upload-time = "2026-07-06T21:32:56.329Z" }, + { url = "https://files.pythonhosted.org/packages/b0/80/c138990aa2a70b1a269f6e06348729836d733d6f970867943f61d367f8cc/cffi-2.1.0-cp312-cp312-win32.whl", hash = "sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a", size = 175269, upload-time = "2026-07-06T21:32:57.777Z" }, + { url = "https://files.pythonhosted.org/packages/a8/eb/f636456ff21a83fc13c032b58cc5dde061691546ac79efa284b2989b7982/cffi-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384", size = 185881, upload-time = "2026-07-06T21:32:59.253Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/400ea43e721727dca8a65c4521390e9196757caba4a45643acb2b63271b8/cffi-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6", size = 180088, upload-time = "2026-07-06T21:33:02.278Z" }, + { url = "https://files.pythonhosted.org/packages/96/88/a996879e2eeccb815f6e3a5967b12a308257412acec882039d386bd2aa7b/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda", size = 194331, upload-time = "2026-07-06T21:33:03.697Z" }, + { url = "https://files.pythonhosted.org/packages/58/85/7ae00d5c8dd6266f4e944c3db630f3c5c9a98b61d469c714d848b1d8138a/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b", size = 196966, upload-time = "2026-07-06T21:33:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e9/45c3a76ad8d43ad9261f4c95436da61128d3ca545d72b9612c0ab5be0b1c/cffi-2.1.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a", size = 184795, upload-time = "2026-07-06T21:33:06.699Z" }, + { url = "https://files.pythonhosted.org/packages/84/4c/82f132cb4418ee6d953d982b19191e87e2a6372c8a4ce36e50b69d6ade4a/cffi-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea", size = 184746, upload-time = "2026-07-06T21:33:08.071Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1c/4ed5a0e5bdca6cbc275556de3328dd1b76fd0c11cc13c88fe66d1d8715f2/cffi-2.1.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db", size = 214747, upload-time = "2026-07-06T21:33:09.671Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a6/e879bb68cc23a2bc9ba8f4b7d8019f0c2694bad2ab6c4a3701d429439f58/cffi-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f", size = 222392, upload-time = "2026-07-06T21:33:10.896Z" }, + { url = "https://files.pythonhosted.org/packages/88/f6/01890cfd63c08f8eb96a8319b0443690197d240a8bd6346048cf7bde9190/cffi-2.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d", size = 210285, upload-time = "2026-07-06T21:33:12.251Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cf/2b684132056f438567b61e19d690dd31cd0921ace051e0a458be6074369e/cffi-2.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0", size = 208801, upload-time = "2026-07-06T21:33:13.617Z" }, + { url = "https://files.pythonhosted.org/packages/6f/08/f2e7d62c460faae0926f2d6e423694aa409ced3bc1fe2927a0a6e5f05416/cffi-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224", size = 221808, upload-time = "2026-07-06T21:33:15.466Z" }, + { url = "https://files.pythonhosted.org/packages/38/37/04f54b8e63a02f3d908332c9effbf8c366167c6f733ed8a3d4f79b7e2a1e/cffi-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c", size = 225241, upload-time = "2026-07-06T21:33:16.869Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d6/c72eecca433cd3e681c65ed313ab4835d9d4a379704d0f628a6a05f51c2e/cffi-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a", size = 223588, upload-time = "2026-07-06T21:33:18.239Z" }, + { url = "https://files.pythonhosted.org/packages/c6/4b/e706f67279140f92939da3475ad610df18bfd52d50f14953a8e5fede71d5/cffi-2.1.0-cp313-cp313-win32.whl", hash = "sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2", size = 175248, upload-time = "2026-07-06T21:33:19.799Z" }, + { url = "https://files.pythonhosted.org/packages/5a/47/59eb7975cb0e4ef0afa764ea945b29a5bb4537a9f771cb7d6c8a5dd74c95/cffi-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512", size = 185717, upload-time = "2026-07-06T21:33:21.47Z" }, + { url = "https://files.pythonhosted.org/packages/5a/af/34fee85c48f8d94efc8597bc09470c9dd274c145f1c12e0fbc6ab6d38d74/cffi-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f", size = 180114, upload-time = "2026-07-06T21:33:22.515Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f0/81478e482afa03f6d18dc8f2afb5edc45b3080853b634b5ed91961be0998/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a", size = 194142, upload-time = "2026-07-06T21:33:23.657Z" }, + { url = "https://files.pythonhosted.org/packages/7d/95/8de304305cd9204974b0ca051b86d307cafca13aa575a0ef1b44d92c0d8c/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3", size = 196819, upload-time = "2026-07-06T21:33:25.007Z" }, + { url = "https://files.pythonhosted.org/packages/20/71/7c8372d30e42415602ed9f268f7cfd66f1b855fed881ecd168bcb45dbc0b/cffi-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d", size = 184965, upload-time = "2026-07-06T21:33:26.605Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5c/584e626835f0375c928176c04137c96927165cb8733cdb3150ec04e5ee5e/cffi-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac", size = 184952, upload-time = "2026-07-06T21:33:27.823Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d2/065fcae1c73979fac8e054462478d0ff8a29c40cdc2ed7ea5676a061df53/cffi-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6", size = 222353, upload-time = "2026-07-06T21:33:29.178Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a5/e8bbb1ce5b3ac2f53ad6a10bde44318a5a8d99d4f4a000d44a6e39aeb3e4/cffi-2.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913", size = 210051, upload-time = "2026-07-06T21:33:30.534Z" }, + { url = "https://files.pythonhosted.org/packages/28/ed/c127d3ac36e899c965e3361357c3befacd6578c03f40125183e41c3b219e/cffi-2.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d", size = 208630, upload-time = "2026-07-06T21:33:31.753Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d7/97d3136f81db489ec8d1d67748c110d6c994268fd7528014aa9f2b085e4e/cffi-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5", size = 221593, upload-time = "2026-07-06T21:33:33.044Z" }, + { url = "https://files.pythonhosted.org/packages/d3/27/93195977168ee63aed233a1a0993a2178798654d1f4bddcdd321d6fd3b21/cffi-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce", size = 225146, upload-time = "2026-07-06T21:33:34.224Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c1/6dbd291ee2ae5a50a034aa057207081f545923bbf15dad4511e985aafff5/cffi-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326", size = 223240, upload-time = "2026-07-06T21:33:35.57Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6f/ade5ce9863a57992a6ea3d0d10d7e29b8749fc127204b3d493d667b2815f/cffi-2.1.0-cp314-cp314-win32.whl", hash = "sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd", size = 177723, upload-time = "2026-07-06T21:33:51.626Z" }, + { url = "https://files.pythonhosted.org/packages/41/de/92b9eeed4ae4a21d6fd9b2a2c8505cbed573299902ea73981cc13f7ff62c/cffi-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb", size = 187937, upload-time = "2026-07-06T21:33:53.403Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1a/cc6ae6c2913a03aab8898eee57963cf1035b8df5872ed8b9115fcc7e2be8/cffi-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804", size = 183001, upload-time = "2026-07-06T21:33:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/14/f0/134c00ce0779ec86dea2aa1aac69339c2741a8045072676763512363a2ea/cffi-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714", size = 188538, upload-time = "2026-07-06T21:33:36.792Z" }, + { url = "https://files.pythonhosted.org/packages/50/d8/3b86aba791cb610d24e8a3e1b2cd529e71fa15096b04e4d4e360049d4a4c/cffi-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376", size = 188230, upload-time = "2026-07-06T21:33:38.011Z" }, + { url = "https://files.pythonhosted.org/packages/14/d0/117dcd9209255ad8571fbc8c92ef32593a1d294dcec91ddc4e4db50606f2/cffi-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98", size = 223899, upload-time = "2026-07-06T21:33:39.514Z" }, + { url = "https://files.pythonhosted.org/packages/b6/3d/f20f8b886b254e3ad10e15cd4186d3aed49f3e6a35ab37aab9f8f25f7c03/cffi-2.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13", size = 211652, upload-time = "2026-07-06T21:33:40.851Z" }, + { url = "https://files.pythonhosted.org/packages/28/3b/fad54de07260b93ddeef4b96d0131d57ea900675df1d410ae1deee52d7a6/cffi-2.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d", size = 210755, upload-time = "2026-07-06T21:33:42.183Z" }, + { url = "https://files.pythonhosted.org/packages/cc/82/3d5c705acb7abbba9bbd7d79b8e62e0f25b6120eb7ae6ac49f1b721722fe/cffi-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056", size = 223933, upload-time = "2026-07-06T21:33:43.603Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d0/47e338384ab6b1004241002fa616301020cea4fc95f283506565d252f276/cffi-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4", size = 226749, upload-time = "2026-07-06T21:33:45.046Z" }, + { url = "https://files.pythonhosted.org/packages/70/25/65bd5b58ea4bfdfc15cde02cb5365f89ef8ab8b2adfb8fe5c4bd4233382f/cffi-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94", size = 225703, upload-time = "2026-07-06T21:33:46.374Z" }, + { url = "https://files.pythonhosted.org/packages/dc/78/aa01ac599a8a4322533d45a1f9bc93b338276d2d59dabbe7c6d92a775c81/cffi-2.1.0-cp314-cp314t-win32.whl", hash = "sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76", size = 182857, upload-time = "2026-07-06T21:33:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/b9/26/d00496b22de4d4228f32dde94ad996f350c8aad676d63bcca0743c8dea4d/cffi-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5", size = 194065, upload-time = "2026-07-06T21:33:48.953Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dd/0c7dbf815a579ff005008a2d815a55d6bb047c349eef536d9dc53d3f0a8d/cffi-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8", size = 186404, upload-time = "2026-07-06T21:33:50.309Z" }, + { url = "https://files.pythonhosted.org/packages/55/c7/8c8c50cb11c6750051daf12164098a9a6f027ac4356967fd4d800a07f242/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c", size = 194121, upload-time = "2026-07-06T21:33:56.109Z" }, + { url = "https://files.pythonhosted.org/packages/99/e2/67680bf19a6b60d2bb7ff83baefa2a4c3d2d7dc0f3277034b802e1fc504c/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001", size = 196820, upload-time = "2026-07-06T21:33:57.288Z" }, + { url = "https://files.pythonhosted.org/packages/ed/da/4bbe583a3b3a5c8c60892124fe17f3fa3656523faf0d3484eae90f091853/cffi-2.1.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3", size = 184936, upload-time = "2026-07-06T21:33:58.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/4b/1f4c36ab273980d7aa75bb126ea4f8971f24a96108acad3a0a084028c57b/cffi-2.1.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc", size = 185045, upload-time = "2026-07-06T21:34:00.085Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c3/ad299dc38f3583f8d916b299f028af418a9ec98bc695fcbebeae7420691c/cffi-2.1.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699", size = 222342, upload-time = "2026-07-06T21:34:01.814Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d8/df4543cc087245044ed02ef3ad8e0a26619d0075ac7a77a12dc81177851b/cffi-2.1.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022", size = 210073, upload-time = "2026-07-06T21:34:03.255Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/fac738d73728c6cea2a88a2883dca54892496cbba88a1dc1f2909cb8a6f5/cffi-2.1.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0", size = 208551, upload-time = "2026-07-06T21:34:04.433Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3f/0b04a700dd64f465c93020253a793a82c9b4dff9961f48facd0df945d9b8/cffi-2.1.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1", size = 221649, upload-time = "2026-07-06T21:34:06.157Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/b7379a5704c79eda57ce075869ba70a0368d1c850f803b3c0d078d39dcaf/cffi-2.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28", size = 225203, upload-time = "2026-07-06T21:34:07.489Z" }, + { url = "https://files.pythonhosted.org/packages/5a/02/d5e6c43ea85c41bda2a184a3418f195fe7cf602967a8d2b94e085b83deef/cffi-2.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629", size = 223263, upload-time = "2026-07-06T21:34:08.712Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d8/772b8259bf75749adffb1c546828978381fb516f60cf701f6c83daf60c85/cffi-2.1.0-cp315-cp315-win32.whl", hash = "sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6", size = 177696, upload-time = "2026-07-06T21:34:26.355Z" }, + { url = "https://files.pythonhosted.org/packages/2f/dd/afa2191fc6d57fedd26e5844a2fe2fcc0bbfa00961bbaa5a41e4921e7cca/cffi-2.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853", size = 187914, upload-time = "2026-07-06T21:34:27.58Z" }, + { url = "https://files.pythonhosted.org/packages/05/ef/6cd4f8c671517162379dc79cfae5aea9106bc38abb89628d5c16adf6a838/cffi-2.1.0-cp315-cp315-win_arm64.whl", hash = "sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda", size = 183004, upload-time = "2026-07-06T21:34:28.905Z" }, + { url = "https://files.pythonhosted.org/packages/11/b6/12fc55092817a5faa26fb8c40c7f9d662e11a46ee248c137aafc42517d92/cffi-2.1.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc", size = 188378, upload-time = "2026-07-06T21:34:09.926Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2e/cdac88979f295fde5daa69622c7d2111e56e7ceb94f211357fbe452339e4/cffi-2.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca", size = 188319, upload-time = "2026-07-06T21:34:11.101Z" }, + { url = "https://files.pythonhosted.org/packages/e0/27/1d0b408497e41a74795af122d7b603c418c5fed0171450f899afd04e594f/cffi-2.1.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d", size = 223904, upload-time = "2026-07-06T21:34:12.606Z" }, + { url = "https://files.pythonhosted.org/packages/8b/31/e115c985105dd7ffb32444505f18ceb874bb42d992af05d5dced7ecf1980/cffi-2.1.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8", size = 211554, upload-time = "2026-07-06T21:34:13.987Z" }, + { url = "https://files.pythonhosted.org/packages/5a/67/9e6e09409336d9e515c58367e7cfcf4f89df06ad25252675595a58eb59d5/cffi-2.1.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd", size = 210795, upload-time = "2026-07-06T21:34:15.972Z" }, + { url = "https://files.pythonhosted.org/packages/19/e5/d3cc82a4a0be7902af279c04181ad038449c096734464a5ae1de3e1401bd/cffi-2.1.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f", size = 223843, upload-time = "2026-07-06T21:34:17.509Z" }, + { url = "https://files.pythonhosted.org/packages/b9/65/b434abc97ce7cecc2c640fde160507c0ecc7e21544b483ba3325d2e2ea17/cffi-2.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc", size = 226773, upload-time = "2026-07-06T21:34:19.05Z" }, + { url = "https://files.pythonhosted.org/packages/b5/9f/d4dc66ca651eb1145a133314cda721abf13cfac3d28c4a0402263ae6ad75/cffi-2.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9", size = 225719, upload-time = "2026-07-06T21:34:20.576Z" }, + { url = "https://files.pythonhosted.org/packages/68/5a/e536c528bc8057496c360c0978559a2dc45653f89dd6151078aa7d8fca1a/cffi-2.1.0-cp315-cp315t-win32.whl", hash = "sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b", size = 182760, upload-time = "2026-07-06T21:34:22.059Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0b/0ffe8b82d3875bced5fa1e7986a7a46b748262a40ab7f60b475eb9fb1bb3/cffi-2.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5", size = 193769, upload-time = "2026-07-06T21:34:23.589Z" }, + { url = "https://files.pythonhosted.org/packages/a0/17/1073b53b68c9b5ca6914adf5f8bf55aacc2d3be102418c90700160ea8605/cffi-2.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210", size = 186405, upload-time = "2026-07-06T21:34:24.857Z" }, ] [[package]] name = "charset-normalizer" -version = "3.4.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" }, - { url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" }, - { url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" }, - { url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" }, - { url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" }, - { url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" }, - { url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" }, - { url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" }, - { url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" }, - { url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" }, - { url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" }, - { url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" }, - { url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" }, - { url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" }, - { url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" }, - { url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" }, - { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, - { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, - { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, - { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, - { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, - { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, - { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, - { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, - { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, - { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, - { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, - { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, - { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, - { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, - { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, - { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, - { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, - { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, - { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, - { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, - { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, - { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, - { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, - { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, - { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, - { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, - { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, - { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, - { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, - { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, - { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, - { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, - { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, - { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, - { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, - { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, - { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, - { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, - { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, - { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, - { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, - { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, - { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, - { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, - { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, - { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, - { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, - { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, - { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, - { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, - { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, - { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, - { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, - { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, - { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, - { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, - { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, - { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, - { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, - { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, - { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, - { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, - { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, - { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, - { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, - { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, - { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, - { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, - { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, - { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, - { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, - { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, - { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, - { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, - { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, - { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +version = "3.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/81/8e983840c6e5b93b33c2ba81aa3d52c2e42f0e9a690ce7607a2e61da4a5c/charset_normalizer-3.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a", size = 322240, upload-time = "2026-07-07T14:32:36.236Z" }, + { url = "https://files.pythonhosted.org/packages/de/d1/b4319dc3229d8272fba305e206fc0a148e2de8d4087917ce62ae6382f359/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616", size = 216475, upload-time = "2026-07-07T14:32:38.142Z" }, + { url = "https://files.pythonhosted.org/packages/80/33/6c99c1b3e6b8bf730e1bc809b9a2608f224145069114c479a2e9e1494346/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209", size = 238670, upload-time = "2026-07-07T14:32:39.658Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f4/ffbb83546e1f198ecc70ecd372b65cf2b50f9068b380abd67640f17a8e18/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99", size = 233476, upload-time = "2026-07-07T14:32:41.155Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5f/b98b8da398637b551e427e7be922bdec19177dc54d6811dcdaa503f23aac/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8", size = 223817, upload-time = "2026-07-07T14:32:42.592Z" }, + { url = "https://files.pythonhosted.org/packages/36/31/a276bb2e66243072a3fd06fdcab9cbb61a305b02143d70d2bda21d888fa8/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b", size = 207974, upload-time = "2026-07-07T14:32:44.258Z" }, + { url = "https://files.pythonhosted.org/packages/5e/be/7ee4453d7e88dfbc4104ccd34900b9f2c7c17dac22881865fe0e82424a25/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2", size = 221655, upload-time = "2026-07-07T14:32:45.64Z" }, + { url = "https://files.pythonhosted.org/packages/1d/85/181c652953eb5276d198f375b1dd641047392050098100a3a02d6534f657/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9", size = 219229, upload-time = "2026-07-07T14:32:47.376Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e7/aaf6da33fc9f4691cda8f7efbc9f69179d3d39ec8a4799baf273ee1d8db0/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15", size = 209704, upload-time = "2026-07-07T14:32:48.855Z" }, + { url = "https://files.pythonhosted.org/packages/63/01/f2fb3bd3a73be48b173ee0c6aa8d2497af97d5663a8c4c4b491de4c62f7a/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d", size = 226243, upload-time = "2026-07-07T14:32:50.239Z" }, + { url = "https://files.pythonhosted.org/packages/c4/02/c57a22739fe05246b0b5783b3bfb6afaac4eebb46f3ececdfb2f048f780e/charset_normalizer-3.4.9-cp310-cp310-win32.whl", hash = "sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381", size = 150935, upload-time = "2026-07-07T14:32:51.676Z" }, + { url = "https://files.pythonhosted.org/packages/37/8d/ca39a7559a4797505530d084fd3a49a2c959efbbbff146302fb7be4e3b35/charset_normalizer-3.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee", size = 162314, upload-time = "2026-07-07T14:32:53.193Z" }, + { url = "https://files.pythonhosted.org/packages/01/da/a44bd7a13d426e69e4894557106cd58669097bfad4a8681123b618fbfc5d/charset_normalizer-3.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419", size = 153075, upload-time = "2026-07-07T14:32:54.554Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5", size = 317075, upload-time = "2026-07-07T14:32:56.021Z" }, + { url = "https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", size = 213837, upload-time = "2026-07-07T14:32:57.78Z" }, + { url = "https://files.pythonhosted.org/packages/b3/46/03ddc7da576d814fe0a36dd1f0fd3258e95404b4b2e3c026b7923d7e133f/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", size = 235503, upload-time = "2026-07-07T14:32:59.205Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6e/de0229a7ef40f6f9d28a837eebf4ec47bdca5dab4e900c84f22919af636a/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", size = 229944, upload-time = "2026-07-07T14:33:00.803Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", size = 221276, upload-time = "2026-07-07T14:33:02.199Z" }, + { url = "https://files.pythonhosted.org/packages/44/95/80282cce0fae9c3061203d723ee87da996aed79679e65d8935050ee7ca1f/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", size = 205260, upload-time = "2026-07-07T14:33:03.698Z" }, + { url = "https://files.pythonhosted.org/packages/0c/74/2f62c8821b969ea3bd67cc2e6976834f48ca5d12664d2559ebcd9bcfbed7/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", size = 217786, upload-time = "2026-07-07T14:33:05.12Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8d/feabb82cb49fcad14515b1d7d1ca4787b0da7fc723a212bf89bc9e0fac52/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", size = 216798, upload-time = "2026-07-07T14:33:06.629Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ff/c946d63bc3786d5b84d960b0f7ab7e25b828486a946b5aa997625bcaf6a6/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", size = 206429, upload-time = "2026-07-07T14:33:08.006Z" }, + { url = "https://files.pythonhosted.org/packages/af/ba/5e5007c370702f85d2ef75791fac7943ed41e080364a673b20142e430e3e/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", size = 223066, upload-time = "2026-07-07T14:33:09.783Z" }, + { url = "https://files.pythonhosted.org/packages/83/d5/9096aa3cf532dfad237861544eb47a0f20d5adbf1039760fed8eaae935d9/charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", size = 150456, upload-time = "2026-07-07T14:33:11.217Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", size = 161410, upload-time = "2026-07-07T14:33:12.743Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8d/1569f4d0032d6ba2a4fe4591c35bf87868c600c41a71eb5c2e1ffa8464c2/charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", size = 152649, upload-time = "2026-07-07T14:33:14.173Z" }, + { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" }, + { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" }, + { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" }, + { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" }, + { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" }, + { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" }, + { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" }, + { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" }, + { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" }, + { url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" }, + { url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" }, + { url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" }, + { url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" }, + { url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" }, + { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" }, + { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" }, + { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" }, + { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" }, + { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" }, + { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" }, + { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" }, + { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" }, + { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, ] [[package]] @@ -538,100 +548,100 @@ wheels = [ [[package]] name = "coverage" -version = "7.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/8b/adeb62ea8951f13c4c7fef2e7a85e1a06b499c8d8237ea589d496029e53f/coverage-7.15.0.tar.gz", hash = "sha256:9ac3fe7a1435986463eaa8ee253ae2f2a268709ba4ae5c7dd1f52a05391ad78f", size = 925362, upload-time = "2026-07-02T13:10:50.535Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/97/c52dc440c390b6cfa87be9432b141a956e2d56d9b9f5fc8bd71c5f471722/coverage-7.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:50913d4bf5ddafa6ca3693da5e4dd833dd1b772e0283c99ca7f7d287db67331a", size = 220539, upload-time = "2026-07-02T13:08:19.252Z" }, - { url = "https://files.pythonhosted.org/packages/3f/26/602de8c2aec7e2e3e99ebfb8e04ba65598f746275396eea5f6794ff4673f/coverage-7.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:359e141ccd33893ce3f1ad5525f8b96083003677c82182e5907d62d4ea5799fc", size = 221058, upload-time = "2026-07-02T13:08:21.013Z" }, - { url = "https://files.pythonhosted.org/packages/fc/13/ebab0743138891c1d646d61e247ec29639afcbb6c4e1905e6a0f0c75291a/coverage-7.15.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3200b6204935f928c64b2ca1f923ab8c1acb7c9de45ec61569711b34d25cccaf", size = 247797, upload-time = "2026-07-02T13:08:22.474Z" }, - { url = "https://files.pythonhosted.org/packages/d3/b7/b6ffb9e042aa48dc4144a8a65529affaec8dca0685309353614a2a7386ad/coverage-7.15.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:be616bf61346883b2cfdc5178669647e03531d81ab761a7e378558b7e8bcb628", size = 249626, upload-time = "2026-07-02T13:08:23.803Z" }, - { url = "https://files.pythonhosted.org/packages/9c/06/243ff05b652333d8e3d060c11223efc2723b19cacf6605e433fa686ab5d4/coverage-7.15.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc7bafc3fe1059463a8fdd97ca79972d6e2bf819d775c7d54991b5b1971201d6", size = 251493, upload-time = "2026-07-02T13:08:25.397Z" }, - { url = "https://files.pythonhosted.org/packages/d3/2b/867faa17030a806114dae388b32a3fa929d8cd4bf39226fbc11f6e6bb705/coverage-7.15.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b713aa7fcf325a01d4184d848acb46fd84f78fdb0978470c636b23a06a753d91", size = 253406, upload-time = "2026-07-02T13:08:26.842Z" }, - { url = "https://files.pythonhosted.org/packages/94/c0/d789ce18f6605afc4895db75723424be2ef494282f77f61d8e5832923183/coverage-7.15.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e38e6fba2d56652fdfaf0231f8f78aeb805234a912de25dc291ee5cce5b8faa4", size = 248512, upload-time = "2026-07-02T13:08:28.398Z" }, - { url = "https://files.pythonhosted.org/packages/c9/b6/b2673c30739f4a2e06649a0a38ad8b093c4d865462dc7bab0e9524a2c3b1/coverage-7.15.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:884499f42e382675be80770391983b90e0c0c774d87dbeeebf5f991cf6612b20", size = 249532, upload-time = "2026-07-02T13:08:29.731Z" }, - { url = "https://files.pythonhosted.org/packages/3c/2e/acd79e9a41beabee92b623afe4f30b549916f48566271475f2907e752828/coverage-7.15.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:840481b12e083dbcbafab14794a8781a958edf327c8d3d70b4eee42f9b8253aa", size = 247537, upload-time = "2026-07-02T13:08:31.173Z" }, - { url = "https://files.pythonhosted.org/packages/12/d4/2d301c4d1b3238d7c88b70ab9d13fd53ed9505662a7ff1b46ba1e2e4e3c3/coverage-7.15.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:276646e9481703d09f854f3b2f018f24e19fd7049ae670a92570043eb97203b1", size = 251348, upload-time = "2026-07-02T13:08:32.63Z" }, - { url = "https://files.pythonhosted.org/packages/35/bb/c67708b2bc00f32e12805ec23d5fa677a0a51652f449341a89f9d6b1b715/coverage-7.15.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4de4b4d3f5545aa6c60dc4efd9c63b5b5dcc3bf00fe83146b2bdfffb8f6613bd", size = 247806, upload-time = "2026-07-02T13:08:33.931Z" }, - { url = "https://files.pythonhosted.org/packages/eb/6c/57c4f653c47a6e917748f8938e389e72fbcae44e3643cd906664f0477a13/coverage-7.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5c504097b2a89b1e85bc6070d920df77daec701337e3aeef2c17775a5dd0ca90", size = 248410, upload-time = "2026-07-02T13:08:35.189Z" }, - { url = "https://files.pythonhosted.org/packages/6c/94/bb083041aef828903668f134273f319f2bd49224962875359c52faa5497f/coverage-7.15.0-cp310-cp310-win32.whl", hash = "sha256:f6e80ed91f98316e86b9c137206b04b2bcfbffccbdff49bd2eb09dddb1cf14e0", size = 222588, upload-time = "2026-07-02T13:08:36.486Z" }, - { url = "https://files.pythonhosted.org/packages/ef/94/a09d8ee618956f626741b0734854bac4425a00e10c0565f5abca64e7e751/coverage-7.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:b3b3e22030f3f6f5e01a5ce69936552a5c0f6992b7698777377b99041961031f", size = 223214, upload-time = "2026-07-02T13:08:37.885Z" }, - { url = "https://files.pythonhosted.org/packages/ae/23/82e910835ef4b8391047025e1d53aa48d66029f444eb8b25373c849bf503/coverage-7.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:003fff99412ea848c0aaebcc78ed2b6ce7d8a1227ed17e68470672770b78a02a", size = 220662, upload-time = "2026-07-02T13:08:39.205Z" }, - { url = "https://files.pythonhosted.org/packages/6d/0d/c7b213dde2f1579de5231062b386d8413f79c11667eb58c39319b25991da/coverage-7.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5cbd804bf2784ce7b45114516050f346ecd50f960c4bb630a7ee9e1d78fa2118", size = 221168, upload-time = "2026-07-02T13:08:40.471Z" }, - { url = "https://files.pythonhosted.org/packages/33/77/d000aeedfac085088337b3c7becdad328474b1f8a9e4c9368a0c99605d68/coverage-7.15.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8773e15c23305b58882a4611fb9b2755977eae0dc2e515366a1b6c98866cc4c2", size = 251587, upload-time = "2026-07-02T13:08:42.033Z" }, - { url = "https://files.pythonhosted.org/packages/cc/e0/86787c56b9df17afd370d5e293515dd4d9a107a561d13054873eefad8ecc/coverage-7.15.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f50e40081494c1dc4239ebb202014cbcc3306ea96fb6302a34c8cc0967fc5ae8", size = 253497, upload-time = "2026-07-02T13:08:43.387Z" }, - { url = "https://files.pythonhosted.org/packages/3f/02/181bc917359299c07dead6270f94e411151c8b60cec905c33499da69afe6/coverage-7.15.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daf96f37f5fc3a7b6c6da862eb4aee61c426bd63da236ed4a73ef0e503b4bca5", size = 255607, upload-time = "2026-07-02T13:08:44.897Z" }, - { url = "https://files.pythonhosted.org/packages/b9/35/ca5e7427699913da6788c4f910e73ab16c5f4b59ec5d3a999dce2a45112f/coverage-7.15.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:51aa20f6ae2788fd197747766edf4cd8234fd9423309b934257fa6b21a592723", size = 257563, upload-time = "2026-07-02T13:08:46.334Z" }, - { url = "https://files.pythonhosted.org/packages/0b/4d/b8220bacc2fc3c4e9078e27c32e99fb411479a4718a72bdd00036a9891c8/coverage-7.15.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03d1f922757662eb7af586e77834792274cff776bc7b1d1a0b66a49ea9d84735", size = 251726, upload-time = "2026-07-02T13:08:47.941Z" }, - { url = "https://files.pythonhosted.org/packages/c4/e4/2e145da1991d72189b9c3cf7eca05c716ee7080d099aaea6757cfc7df008/coverage-7.15.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a6d6acc9a7666245e6133dd15144ca038a85a9cd5026bb06d6bbae9e77440dc9", size = 253301, upload-time = "2026-07-02T13:08:49.5Z" }, - { url = "https://files.pythonhosted.org/packages/72/28/d2c841d698bf762e481f08bd4839d370246b6d9b61dab085a7b20b201a08/coverage-7.15.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1ac2c4c27c7df851dc9a017c2d7de00b69147e84ba3d96f37a530b0b6fb51035", size = 251361, upload-time = "2026-07-02T13:08:51.304Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ed/55d9ffde994fba3897c0c783f77a7d053b0c18787f6892ed5b0aed73f469/coverage-7.15.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b761a1d504fd4bd1f20f418753964dca9f5862a511fc854dac58296b3b223671", size = 255129, upload-time = "2026-07-02T13:08:52.661Z" }, - { url = "https://files.pythonhosted.org/packages/1d/c0/ecbf33b8c460ea2718aeb813e2df8140d0370e5f67261c31524ceb0a2a8d/coverage-7.15.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:e43b045e11c16e897895758ae90e4a90cf99e93d58549e2f90c0e2272e155695", size = 251081, upload-time = "2026-07-02T13:08:54.188Z" }, - { url = "https://files.pythonhosted.org/packages/a9/de/fb87b4261f54448dd2b9504ef19a58be42cef0d9520595fbfe1219b15234/coverage-7.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:589b54513e901739f4b4582c705ce96b80c96f57641b1464607e2367a270e540", size = 251988, upload-time = "2026-07-02T13:08:55.726Z" }, - { url = "https://files.pythonhosted.org/packages/df/27/3494d5f291b9a4cb868f73c11221a8bd2d5bd761a8f9acea61ff57128dd1/coverage-7.15.0-cp311-cp311-win32.whl", hash = "sha256:106781b8482749162d0b47056937ba0933508e5d9447f65a5e7d5c422f0d6bb4", size = 222754, upload-time = "2026-07-02T13:08:57.091Z" }, - { url = "https://files.pythonhosted.org/packages/2a/ee/cd4847ebc9be6a9c0123d763645a6f1f3be6b8c58c962706368b79cbac07/coverage-7.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:821e92b3631d762a339695824cadbbc73020354eba2a23a551a99ad34938fbe6", size = 223225, upload-time = "2026-07-02T13:08:58.594Z" }, - { url = "https://files.pythonhosted.org/packages/57/37/5011581aa7f2be498b97dcc7c9902192442a42f4f9a748aeadb3d6506b42/coverage-7.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:309990eb5fb8014b9f67cb211f7fd41876ec8a88a88d3ae76de0ed1d611e3640", size = 222774, upload-time = "2026-07-02T13:09:00.074Z" }, - { url = "https://files.pythonhosted.org/packages/2a/74/fd4c0901137c4f8d81a76ada99e43c65163b4c94a02ece107a4ec0c6b615/coverage-7.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b75ee5e8cb7575636ac598719b4307ac529ec8fcd79608a35c3cd4d4dada812d", size = 220838, upload-time = "2026-07-02T13:09:02.084Z" }, - { url = "https://files.pythonhosted.org/packages/0f/2e/2347583467bd7f0402635101a916961915cc68fce652cd0db5f173ea04fc/coverage-7.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffb31267816b93b075302248cc1737506081b4f163df4401e9df1a6424aafabe", size = 221197, upload-time = "2026-07-02T13:09:03.617Z" }, - { url = "https://files.pythonhosted.org/packages/f0/17/99fa688541ae1d6e84543a0e544f83de0c944815b63e9e7b1ed411d15036/coverage-7.15.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e4d0bb73455bf97ab243a8f12c37c686ccf1c13bb614b7b85f1d062f06f42b2c", size = 252705, upload-time = "2026-07-02T13:09:05.059Z" }, - { url = "https://files.pythonhosted.org/packages/fb/02/6a95a5cd83b74839017ef9cf48d2d8c9ae60af919e17a3f336e6f9f1b7bd/coverage-7.15.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:20d9ccc4ebd0edc434d86dfd2a1dd2a8efa6b6b3073d0485a394fee86459ebb4", size = 255441, upload-time = "2026-07-02T13:09:06.559Z" }, - { url = "https://files.pythonhosted.org/packages/67/f2/406f6c57d600f68185942422c4c00f1a3255d60aee6e5fd961425cd9987e/coverage-7.15.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20c8a976c365c8cb12f0cbd099508772ea41fb5fa80657a8506df0e11bd278c5", size = 256556, upload-time = "2026-07-02T13:09:08.197Z" }, - { url = "https://files.pythonhosted.org/packages/74/8e/d3fa48489c15ecdec1ba48fd61f68798555dddd2f6716f9ad42adeb1a2a9/coverage-7.15.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f948fd5ba1b9cbca91f0ae08b4c1ce2b139509149a435e2585d056d57d70bf01", size = 258815, upload-time = "2026-07-02T13:09:09.691Z" }, - { url = "https://files.pythonhosted.org/packages/47/2e/2d40ddd110462c6a2769677cf7f1c119a52b45f568978fc6c98e4cc0dd0f/coverage-7.15.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f58185f06edf6ad68ec9fb155d63ef650c82f3fbd7e1770e2867751fb13158f4", size = 253117, upload-time = "2026-07-02T13:09:11.212Z" }, - { url = "https://files.pythonhosted.org/packages/51/c0/310782f0d7c3cb2b5ac05ba8d205fe91f24a36f6bf3256098f1782181c38/coverage-7.15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02adc79a920c73c647c5d117f55747df7f2de94571884758ce8bc58e04f0a796", size = 254475, upload-time = "2026-07-02T13:09:13.029Z" }, - { url = "https://files.pythonhosted.org/packages/86/f7/702da6c275f8ae6ade423d2877243122932c9b27f5403003b9ef8c927d12/coverage-7.15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6eb7c300fbed667fd6e3588eba71c1904cdb06110ca6fdf908c26bdd88b8e382", size = 252619, upload-time = "2026-07-02T13:09:14.699Z" }, - { url = "https://files.pythonhosted.org/packages/fb/84/c5b15a7e5ecba4e56218d772d99fe80a63e63f8d11f12783723a6005ab45/coverage-7.15.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b5fb23fa2de9dce1f5c36c09066d8fcda16cd96e8e26686caa2d7cb9b567d65c", size = 256689, upload-time = "2026-07-02T13:09:16.103Z" }, - { url = "https://files.pythonhosted.org/packages/95/2f/c8b07559b57701230c61b23a953858c052890c12ef568d81780c6c46e92e/coverage-7.15.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cec79341dbe6281484024979976d0c7f22beae08b4a254655decd25d42cbe766", size = 252189, upload-time = "2026-07-02T13:09:17.828Z" }, - { url = "https://files.pythonhosted.org/packages/6b/80/6d2f049dd3fd3dbfd60b62ba6b2162a04009e2c002ce70b24cf3878dec7a/coverage-7.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c664c5444b1d970b1b2a450e21fb19ee5c9cfdf151ded2dda37260031cca0da", size = 254059, upload-time = "2026-07-02T13:09:19.304Z" }, - { url = "https://files.pythonhosted.org/packages/ce/92/b0287a2c42031d25c628f815f89a3cd9f8268ee78bb1252c9356cda1c689/coverage-7.15.0-cp312-cp312-win32.whl", hash = "sha256:5f764a3fa339bde6b3aa97657f5a6a3a9451e4a5b4ea98a2892c773a43525f77", size = 222893, upload-time = "2026-07-02T13:09:20.812Z" }, - { url = "https://files.pythonhosted.org/packages/a9/69/e34c481915fecb499b3146975061dac528752e37706edc1804f32c822469/coverage-7.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:52f9a4d2c4c56c8848bc2f524916698354b0211488b38c49ad9ae54f6cafbff6", size = 223429, upload-time = "2026-07-02T13:09:22.315Z" }, - { url = "https://files.pythonhosted.org/packages/fe/98/6e878f0b571d32684ef3f38d7c03db241ca5b82a5da8a5391596a8f209c4/coverage-7.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:31e5c3e70c85307ea35a12964e2e40f56ca2ee4b1c8c721ccf4609d17071080b", size = 222810, upload-time = "2026-07-02T13:09:23.812Z" }, - { url = "https://files.pythonhosted.org/packages/76/04/145a3748098bcc86b631a85408d2c3dc5c104e0bd86d605468239b25b6c4/coverage-7.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5be4caf3b28836f078abe700f8944dac4a65d78f16d6c600c89cb624e5535782", size = 220863, upload-time = "2026-07-02T13:09:25.371Z" }, - { url = "https://files.pythonhosted.org/packages/a4/5c/4ed55708fed2c64b63c9bc5715daef670872202101938869b7fe5d5fbb8f/coverage-7.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dd58ad1404704303ca8d4f4b8a1095e7cbc7040ef17a66df1e6619aa10176430", size = 221230, upload-time = "2026-07-02T13:09:26.897Z" }, - { url = "https://files.pythonhosted.org/packages/7b/19/3a80b97d3b2a5c77a01ae359c6bed20c13738fe3d9380f08616d4fec0281/coverage-7.15.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bbcbb317c2e5ded5b21104af81c29f391be2af98d065693ffbe8d23949b948e5", size = 252227, upload-time = "2026-07-02T13:09:28.543Z" }, - { url = "https://files.pythonhosted.org/packages/a1/fa/b70062750686bd7da454da27927622f48bbac6990ac7a4c4a4653e7b0036/coverage-7.15.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:27f31ecb458da3f859aab3f15ada871eb7a7768807d88df4a9f186bb17737970", size = 254823, upload-time = "2026-07-02T13:09:30.177Z" }, - { url = "https://files.pythonhosted.org/packages/a9/09/dad6a75a2e561b9dc5086a8c5257a7591d584246f67e23e70d2995b89ab6/coverage-7.15.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fb759be317fdc62e0f56bffdf61cfcb45c7761ad6b71e3e583e71a67ae753c", size = 256059, upload-time = "2026-07-02T13:09:31.979Z" }, - { url = "https://files.pythonhosted.org/packages/e6/e7/b5d2941fa9564573d44b693a871ff3156f0c42cbefe977a09fa7fdc59971/coverage-7.15.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5cf007add5ab4bb8fa9f4c77e3732127c9e6cad501d7db43355fbfafca0be84", size = 258190, upload-time = "2026-07-02T13:09:34.035Z" }, - { url = "https://files.pythonhosted.org/packages/7c/1d/8e895bcde3c57ccd46d896dda5f2b3d5df761a1b0c6c9d450d175dedc632/coverage-7.15.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc78d9843bd576fbe2118248258d485e968dc535f95ed504a7b0867ba9b51389", size = 252456, upload-time = "2026-07-02T13:09:35.765Z" }, - { url = "https://files.pythonhosted.org/packages/14/4c/f6997da343ddeb959be82c3b05322793f92c071ad45f7cb8a96336e2dd5f/coverage-7.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a263060f1de0b4b74b4e089c2a70b8003b3781c733329a9c8fd54995328f9950", size = 254192, upload-time = "2026-07-02T13:09:37.445Z" }, - { url = "https://files.pythonhosted.org/packages/17/27/a0bc09d032267b9da89d95a2d874cfbef2a5aebbf0e87cf7aba221d79a99/coverage-7.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c48decf16e0dfd5b049c7d5e82200c23c08126719142998d4f172444e3d0529e", size = 252153, upload-time = "2026-07-02T13:09:39.422Z" }, - { url = "https://files.pythonhosted.org/packages/54/c0/77fc233d9fba07b244c40948c53fe27308b8f21732fb3417f87fbd6fd992/coverage-7.15.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:08fb028000ed0aaa0a4cbdfbb98be7cb42f370db973fbbb469733505ab20e13e", size = 256310, upload-time = "2026-07-02T13:09:41.006Z" }, - { url = "https://files.pythonhosted.org/packages/d5/24/601cecfb5825becacb8d45219a018a3b55b9dbaec624efdb0ea249d08be2/coverage-7.15.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb7dc0c3b7d8a1077abea0b8546ebc5e26d6ef6ecefc2f0f5ad2b8a53bdad837", size = 251974, upload-time = "2026-07-02T13:09:42.733Z" }, - { url = "https://files.pythonhosted.org/packages/47/1e/6f45e5a5b3d5484318d368702af6716b5ab8913b0428bec981a562fcf296/coverage-7.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cb3602054ccbe9f0d8c2dc04bbeba90d5719236e2cd06e042ddd6d3fc7b6e37", size = 253745, upload-time = "2026-07-02T13:09:44.376Z" }, - { url = "https://files.pythonhosted.org/packages/8e/db/4df027a77bd11d0e527f44c53557c76e54ad027413d0304252ea3a78d67e/coverage-7.15.0-cp313-cp313-win32.whl", hash = "sha256:0bf781da64326b677be344df505171435b6f58716108606621d5d27d964fff8b", size = 222902, upload-time = "2026-07-02T13:09:46.122Z" }, - { url = "https://files.pythonhosted.org/packages/a0/10/0355894d34e231f2c5449e71287e81a50793a325df2e2b027b7bcd9dfd19/coverage-7.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:2c57a275078ee3fa185f83e400f765bc764a549de66d99b47881645cbd4ea629", size = 223444, upload-time = "2026-07-02T13:09:47.687Z" }, - { url = "https://files.pythonhosted.org/packages/06/ef/bb725f263befaaff851203ab338e68af15e195d7f7b5f323162532d9b6a8/coverage-7.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:3812c61afc6685c7999b39320779ab8f43b7a3081fdb0def39976e56fbdb9a21", size = 222839, upload-time = "2026-07-02T13:09:49.717Z" }, - { url = "https://files.pythonhosted.org/packages/4f/9c/1e3ca54f72a3185ece06c58d871099898c48f0ed6430d17b6ab75f0d180a/coverage-7.15.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:41cb79af843222e11da87127ad0ecbfa878abadd0f770a4a99391a27d3887324", size = 220906, upload-time = "2026-07-02T13:09:51.339Z" }, - { url = "https://files.pythonhosted.org/packages/09/37/f718613d83b274880382f6b67e78f3802549ae39b0b3e65ae5b5974df56e/coverage-7.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7d2008989ef8fe54188d3f3bfa2e3099b025af11e90a6a1b9e7dc433d04263d8", size = 221239, upload-time = "2026-07-02T13:09:53.138Z" }, - { url = "https://files.pythonhosted.org/packages/a7/ce/22bae91e0b75445f68d365c7643ed0aa4880bbf77450ee74ca65bdae53a7/coverage-7.15.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:769e8ece11a596315ebf5aa7ec383aeeed016c091d2bf6363ffb996d41529092", size = 252286, upload-time = "2026-07-02T13:09:54.996Z" }, - { url = "https://files.pythonhosted.org/packages/dd/1e/bec5e32aa508615d9d7a2790effb25fb4dc28606e995816afe400b25ece3/coverage-7.15.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:65a6b6164ee5c39e2f3803f314292d6c61a607ba7fee253d1e03c42dc3903502", size = 254789, upload-time = "2026-07-02T13:09:56.678Z" }, - { url = "https://files.pythonhosted.org/packages/17/29/0e865435b4354e4a7c03b1b7920046d31d0a273d55decefea27e011cb9bf/coverage-7.15.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75128817f95a5c45bb01d65fd2d8b9cb54bbe03d81608fb70e3e14b437ad56c2", size = 256135, upload-time = "2026-07-02T13:09:58.343Z" }, - { url = "https://files.pythonhosted.org/packages/84/ff/33a870b58a13325d62fc0a6c8f01fa0ff667cef60c7498e2382a147dfa18/coverage-7.15.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9887bb428fe2d4cd4bee89bac1a6c9932f484afd5b36fbd4ff6ea5f825bb1f5e", size = 258449, upload-time = "2026-07-02T13:10:00.057Z" }, - { url = "https://files.pythonhosted.org/packages/18/7b/6fffe596bf3ddba8462758d02c5dad730fd91055a6634aa2e4226229181a/coverage-7.15.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0bfc0be1f702042207a93a00523b1065ee1fe951e96edf311581c0bbc2e34888", size = 252313, upload-time = "2026-07-02T13:10:01.946Z" }, - { url = "https://files.pythonhosted.org/packages/58/1b/11468dd6c1676ab831a70cb9a8d4e198e8607fa0b7220ab918b73fe9bfbd/coverage-7.15.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f64627d55def5a43282d70e08396672692f77e4da610a5bb8bb4060b432b6859", size = 254142, upload-time = "2026-07-02T13:10:04.065Z" }, - { url = "https://files.pythonhosted.org/packages/79/41/29328e21d16b1b95092c30dd700e08cf915bd3734f836df8f3bdb0e8fa9f/coverage-7.15.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:2c6f0fa473003905c6d5bac328ee4eba9fbea654f15bc24b8a3274b23363fa99", size = 252108, upload-time = "2026-07-02T13:10:06.11Z" }, - { url = "https://files.pythonhosted.org/packages/9b/de/05ccfb990439655b35afbfd8e0d13fe66677565a7d4eb38c3f5ef2635e1c/coverage-7.15.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2bcf9afaf064172c6ec3c58a325a9957ad1178c05dd934e25f253321776e0676", size = 256385, upload-time = "2026-07-02T13:10:08.141Z" }, - { url = "https://files.pythonhosted.org/packages/51/0e/486828a3d2695ea7a2609f17ff572f6b01905e608379440a11da4b8dffbe/coverage-7.15.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:baf06bc987115d6fb938d403f7eab684a057766c490367999a2b71a6883110c6", size = 251923, upload-time = "2026-07-02T13:10:10.179Z" }, - { url = "https://files.pythonhosted.org/packages/18/c7/03582b6715f078e5e558354c87616d945b9894cda2dace8e4009b17035e4/coverage-7.15.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f0405f2ff97b1c4c0e782cb32e02f32369bcf2e6b618b591d67e1ea754575dfe", size = 253580, upload-time = "2026-07-02T13:10:12.052Z" }, - { url = "https://files.pythonhosted.org/packages/db/dc/9e578bbaf2ecb4959a81b7e7601ad8cca772cba2892e8d144cb749b4a71a/coverage-7.15.0-cp314-cp314-win32.whl", hash = "sha256:ab282853ed5fbd64bbb162f19cb8fcb7087187508a6374b4f9c34ec1577c4e8f", size = 223107, upload-time = "2026-07-02T13:10:13.994Z" }, - { url = "https://files.pythonhosted.org/packages/ae/3e/c8c3b75d8dbe0e35f7b0cc3ff5e949fc59500f70b21d0398813f66740664/coverage-7.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:3bb3040e9f4bbe26fcb0cd7cc85ac63e630d3f3a9c74f027abf4caa27e706663", size = 223597, upload-time = "2026-07-02T13:10:15.906Z" }, - { url = "https://files.pythonhosted.org/packages/cd/bc/3cbc9fb036eb388519bccd521f783499c39b64256013fbc362782f196fe1/coverage-7.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:346771144d34f7fa84ec28386f78e0f31653f33cf35e19d253d5b35f9e8201da", size = 223020, upload-time = "2026-07-02T13:10:17.844Z" }, - { url = "https://files.pythonhosted.org/packages/28/00/199c4a8d656dff63102577a056c0fce2ff6a79e40adac092fc986c49cbf1/coverage-7.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d34a010905fb6401324ba016b5da03d574967f7b21ce48ea41e66f0f1f95f641", size = 221638, upload-time = "2026-07-02T13:10:19.703Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8e/9d0092c96a3d3a26951ecc7020826aa57bcb1b119ca81acbba996884ab13/coverage-7.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bb25d825d885ca8036795dacfc3924d33091fc76d71ebc99420c6b79e77d96fa", size = 221903, upload-time = "2026-07-02T13:10:21.514Z" }, - { url = "https://files.pythonhosted.org/packages/6d/b4/c0ca3028f42c9a08e51feb4561ef1192e5de99797cd1db5b04590c215bda/coverage-7.15.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:94c9686bfe8a9a6810297aecbd99beaa3445f9e8dc2f80b1382cca0d86b64461", size = 263267, upload-time = "2026-07-02T13:10:23.261Z" }, - { url = "https://files.pythonhosted.org/packages/5f/aa/a375e3846e5d3c013dc600b2a3231089055c73d77f5393dd2192a8d64da6/coverage-7.15.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9bd671c25f9d85f09d7ec481d0e43d5139f486c06a37139847a7ce569788af72", size = 265390, upload-time = "2026-07-02T13:10:25.152Z" }, - { url = "https://files.pythonhosted.org/packages/92/e1/5783cdabb797305e1c9e4809fea496d31834c51fa772514f73dc148bcfc9/coverage-7.15.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:110cbdf8d2e216577312cf06ccf85539c0e5a5420ef747e4a4719b5e483c88cd", size = 267811, upload-time = "2026-07-02T13:10:27.249Z" }, - { url = "https://files.pythonhosted.org/packages/85/31/96d8bbf58b8e9193bc8389574a91a0db48355ee98feb66aa6bf8d1b32eea/coverage-7.15.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2c5d4619214f1d9993e7b00a8600d14614b7e9d84e89507460b126aa5e6559e5", size = 268928, upload-time = "2026-07-02T13:10:29.242Z" }, - { url = "https://files.pythonhosted.org/packages/5e/7a/5294567e811a1cb7eda93140c628fa050d66189da28da320f93d1d815c73/coverage-7.15.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:781a704516e2d8346fbbd5be6c6f3412dd824785146528b3a01816f26c081007", size = 262378, upload-time = "2026-07-02T13:10:31.107Z" }, - { url = "https://files.pythonhosted.org/packages/69/3f/3f48538421f899f28946f90a3d272136a4686e1abf461cc9249a783ee0f3/coverage-7.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd4a1b44bcb65ee29e947ac92bbee04956df3a6bfc6143641bb6cae7ede00fc9", size = 265263, upload-time = "2026-07-02T13:10:32.942Z" }, - { url = "https://files.pythonhosted.org/packages/ce/d3/092df15efcab8a9c1467ee960eb8019bbad3f9300d115d89ea6195f369ff/coverage-7.15.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0e4950c9d6d3e39c64c991814ff315e2d0b9cb8152363594212c9e55208c0a8f", size = 262866, upload-time = "2026-07-02T13:10:35.104Z" }, - { url = "https://files.pythonhosted.org/packages/e5/ab/0254d2b88665efb2c57ad368cc77ab5de3435bd8d5add4729c1b0e79431e/coverage-7.15.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:fe9c87ff42e5472d80d21704972e1f96e104a0a599d77c5e35db5a3c562e2571", size = 266599, upload-time = "2026-07-02T13:10:37.05Z" }, - { url = "https://files.pythonhosted.org/packages/a8/79/1cfa4023e489ce6fbc7be4a5d442dbc375edb4f4fda39a352cedb53263c2/coverage-7.15.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f00d5ae1dd2fe13fb8186e3e7d37bcbd8b25c0d764ff7d1b32cef9be058510a8", size = 261714, upload-time = "2026-07-02T13:10:38.966Z" }, - { url = "https://files.pythonhosted.org/packages/b7/eb/fee5c8665656be63f497418d410484637c438172568688e8ac92e06574e7/coverage-7.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:363ab38cc78b615f11c9cac3cf1d7eef950c18b9fdedfb9066f59461dcf84d68", size = 264025, upload-time = "2026-07-02T13:10:40.789Z" }, - { url = "https://files.pythonhosted.org/packages/ab/99/63005db722f91edc81abc16302f9cc2f6228c1679e46e15be9ae144b14d0/coverage-7.15.0-cp314-cp314t-win32.whl", hash = "sha256:54fd9c53a5fafff509195f1b6a3f9be615d8e8362a3629ff1de23d270c03c86b", size = 223413, upload-time = "2026-07-02T13:10:42.597Z" }, - { url = "https://files.pythonhosted.org/packages/c1/e8/2bc6181c4fb06f1a6b981eb85330cc57bfad7e3f710fc9c9d350013ba228/coverage-7.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:87b47553097ba185ed964866078e7e63adea9f5f51b5f39691c34f30afd21080", size = 224245, upload-time = "2026-07-02T13:10:44.47Z" }, - { url = "https://files.pythonhosted.org/packages/79/b8/4d959bf9cc45d0cfed2f4d35cafcab978cdb6ea02eb5100009cd740632a3/coverage-7.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aeefb2dd178fe7eee79f0ad25d75855cb35ee9ed472db2c5ea06f5b4fd00cec5", size = 223558, upload-time = "2026-07-02T13:10:46.368Z" }, - { url = "https://files.pythonhosted.org/packages/52/30/21b2ad45959cd50e909e02ebac1e30b4ceb7162e91c11d4c570223a458b7/coverage-7.15.0-py3-none-any.whl", hash = "sha256:56da6a4cbe8f7e9e80bd072ca9cefe67d7106a440a7ec06519ec6507ac94ad19", size = 212632, upload-time = "2026-07-02T13:10:48.641Z" }, +version = "7.15.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/83/6051c2a2feab48ae5bd27c84ef047191d2d4a3172f689e38eaa48ed17db1/coverage-7.15.1.tar.gz", hash = "sha256:165e9949eaf222ef1f018635d0d7f368a23bfe0212af558534c40d8c04686d67", size = 927640, upload-time = "2026-07-12T20:58:19.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/76/eef242d6bb95fb737fefd9bbf3a318897e4e82cf543b53a61c2a6e8588eb/coverage-7.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:05d87c2a43373ad6b976d0a99ad58c48633633bcdeb896dc645a006472cc4a71", size = 221195, upload-time = "2026-07-12T20:55:55.82Z" }, + { url = "https://files.pythonhosted.org/packages/89/73/41cd50da59d513a30fd3a34b5516b962ac17514defdb6705948446a5c0b1/coverage-7.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2afce82f2cf8f4c9002746a42755e1dc61baff33d9f7ab5569b4c9101f8f4d1d", size = 221714, upload-time = "2026-07-12T20:55:58.104Z" }, + { url = "https://files.pythonhosted.org/packages/22/9d/6df6ac6677719a087c839208186525b7b1f3653c50196c43246482ada65e/coverage-7.15.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0a545ef5384d787d0fcac6c349afc2c5f99dcc39e13ed3c191b2c06305f64c04", size = 248454, upload-time = "2026-07-12T20:55:59.336Z" }, + { url = "https://files.pythonhosted.org/packages/9f/f5/86f92c429fbf942986d01c3bb7b0b6c3ad06b09f7fa745023877413dde83/coverage-7.15.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:afaa144b8f5b3bc69fe0ce50d401c46b01ab264782553bfd05a3f98804524ecb", size = 250282, upload-time = "2026-07-12T20:56:00.589Z" }, + { url = "https://files.pythonhosted.org/packages/47/c6/99d0f1c1dd1583aaf8c1deca447e44426be54a76027a8c55e29970f22b15/coverage-7.15.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1b6099490e5f88569c46b18605f556c3b30acc9a0a219cf7ef8fab8f7161ec4", size = 252149, upload-time = "2026-07-12T20:56:01.853Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c3/5d30740ec96f5fd8a659e46b6ee9224197f87aa66ce4a20e93dcf46221ac/coverage-7.15.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bb5f6c2cf1ffd0bf2bd925c7cdcae9b4f208e9696d453ed51eb1f5fa0cc5b45b", size = 254061, upload-time = "2026-07-12T20:56:03.277Z" }, + { url = "https://files.pythonhosted.org/packages/71/e5/3afb2950673ca6cd22bc9ad6a9d6058c853bef8fe27a6d1df3adc274fd88/coverage-7.15.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:81572011fc1fc271317da35da593944daef7bfd507085e35751abbe702b74f69", size = 249152, upload-time = "2026-07-12T20:56:04.618Z" }, + { url = "https://files.pythonhosted.org/packages/ca/53/25ab3c0a7cf517ed4d30cd4dd82331e005d4b57fec8c19c0ffd94fb50baa/coverage-7.15.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8f765d13c08497687d0780cca66115c6aa4ba6703ad43b61e94fab9db689e3a3", size = 250187, upload-time = "2026-07-12T20:56:06Z" }, + { url = "https://files.pythonhosted.org/packages/ad/2a/8afb5f994e26e919e5dca5164f1c7b6b7ce6090aab1aac32b1ae1782f047/coverage-7.15.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:771caba880fee96493d18dfc465c318e08ab74e3bc2a3e4089e52514be6a6e54", size = 248193, upload-time = "2026-07-12T20:56:07.376Z" }, + { url = "https://files.pythonhosted.org/packages/d4/62/b6616e15288c9a7b628f7745e832fd8af2c03ec1f7dc11da25947f5ab16f/coverage-7.15.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:433a73200848e80f27712fc113b6ff5311f29b479a7d3bd4b1106138a77f9674", size = 252004, upload-time = "2026-07-12T20:56:08.787Z" }, + { url = "https://files.pythonhosted.org/packages/9e/21/5d80d65e4df3018dedb23a44f276f20ce26e429ffd76df03de03bcf9a767/coverage-7.15.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:5d6fa45079db9fbeba0a69e3d91189f05301d6ac918162a53179d32fc9ed4910", size = 248463, upload-time = "2026-07-12T20:56:10.245Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/04a22708691561c44d77e1d5a60857b351310fdfbf253533780bb31c8b6f/coverage-7.15.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:00b6703c6640075cdce5124e9335dfdf9167272475301828acfdd09c0e5ee731", size = 249064, upload-time = "2026-07-12T20:56:11.614Z" }, + { url = "https://files.pythonhosted.org/packages/82/c9/a5c1e8954e657559de478acc1ef73e9393d3fc9b4a2ab4427501d765aca2/coverage-7.15.1-cp310-cp310-win32.whl", hash = "sha256:3ad9a0eac4728327fd870d52f74d2e631d176c5f178eaea2d9983ab5b9755a55", size = 223248, upload-time = "2026-07-12T20:56:13.068Z" }, + { url = "https://files.pythonhosted.org/packages/54/49/5f6566e14611a58e93a926f3a8a7b22e4e0702ebb4c467ac38f452c7f4cf/coverage-7.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:eb5fa75dc3d30e3a1b75da97973479b20ffa9b0641ff56d6e94b5f3e210daa54", size = 223874, upload-time = "2026-07-12T20:56:14.396Z" }, + { url = "https://files.pythonhosted.org/packages/67/ee/5095e07a0986930174fc153fd0bb115f7f2724f5344cc672e016d4f23a4e/coverage-7.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6506330b4a8dcf53b95bd84d8d0e817107cdb3fc1438e835029cdf0bc6612eb0", size = 221320, upload-time = "2026-07-12T20:56:16.036Z" }, + { url = "https://files.pythonhosted.org/packages/43/7a/7e2598ce98433a214020a61c0755d5961f9f26df0c630dc73e06b86d3416/coverage-7.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8a51a8ec382c39d939cba0ab07ae949077ae4e842343bd4eed22d432358cea9", size = 221823, upload-time = "2026-07-12T20:56:17.54Z" }, + { url = "https://files.pythonhosted.org/packages/8b/4f/67dac6a69139b490a239d91b6d5ef127982ffb89159769241656ab879ee5/coverage-7.15.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:18ea20e3922d7f8ca9e0ef1084408d08c4ad62d5e531cb9c1f6896a99297ebea", size = 252242, upload-time = "2026-07-12T20:56:18.868Z" }, + { url = "https://files.pythonhosted.org/packages/05/37/5e439725a96de592309725550c8df639c359c209dcb4b152ed46032d4c0d/coverage-7.15.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:aab9902a64b8390e3b56e539fddae1d79a267807fe5cb0c18d7d2f544ce867e2", size = 254153, upload-time = "2026-07-12T20:56:20.118Z" }, + { url = "https://files.pythonhosted.org/packages/0f/1c/671ba9e15f9651a99962fb588a1fe4fb267cae4bb85f9bb4ac4413c6f7ef/coverage-7.15.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cc316264317b07a9e90d7f2b4188a15e36e9b54e651081b791b0515fa612a29", size = 256261, upload-time = "2026-07-12T20:56:21.682Z" }, + { url = "https://files.pythonhosted.org/packages/d6/3c/88305322b4d015b4536b7174b8f9b87f850f01af9d89008cdbf984b65ff2/coverage-7.15.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d4e47e7eea81a8ccf060a07627654151d929da62c7b715738387c200905cec89", size = 258222, upload-time = "2026-07-12T20:56:22.962Z" }, + { url = "https://files.pythonhosted.org/packages/8d/20/d02e42333a57f266ded61ed4fb3821da1f2dc143734aaa3dcc9c117204c5/coverage-7.15.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c6829d9a3b55ad2b73ef5fda8302e5be03683789e88b1a079dcf4a773229c21d", size = 252368, upload-time = "2026-07-12T20:56:24.475Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d0/94ebc010926a191d83e83b1f1236f8bd0d14d0eb98b5c324aa4be2589a8e/coverage-7.15.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:764e045811f9c8cda436641f3f088283351d331a519b5807f19041cd0a68da1c", size = 253953, upload-time = "2026-07-12T20:56:26.036Z" }, + { url = "https://files.pythonhosted.org/packages/2e/a6/2c8553191da0c2da2c43ff294fb9bab57a5b0fae03560304a82a8b5addc3/coverage-7.15.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:09b3b088aa24489c4082bcc35fcc8224281ab94a653dfb6d3f0c8165b0d628ab", size = 252016, upload-time = "2026-07-12T20:56:27.374Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/20f47986d8360fa1a31d9858dd2ba0be009d44ecfa8d02120b1eb93c028b/coverage-7.15.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7911b02f57053adf8164ae63edb1c26574d24dfccabadc5268cf69310a69a358", size = 255783, upload-time = "2026-07-12T20:56:28.921Z" }, + { url = "https://files.pythonhosted.org/packages/17/36/fb61983b5259f78fa5e62ee74e91fe4de4c556fcbc9a3b9c9021c2f8b57b/coverage-7.15.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:664e279ed40599b8ed16f4db18d92a7e212c73129672bec8f5d96d4da48d2404", size = 251735, upload-time = "2026-07-12T20:56:30.465Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2e/d109d8d2d6c726ba2e78125297073918a2a91464f0ea777e5398fa3cf75c/coverage-7.15.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:34fe7cf79d5f1f87f2e8ce7dc1c32950841f50e10d0120f263856acfad66de34", size = 252645, upload-time = "2026-07-12T20:56:32.076Z" }, + { url = "https://files.pythonhosted.org/packages/bc/a0/5b3219cbb46135e14673427f2bf5890c4da4e6f655243692f3448aa3f42c/coverage-7.15.1-cp311-cp311-win32.whl", hash = "sha256:5e2d2536d2f57a354aa382ed303ac0e2e5c9522a508c05b998d26181b94163a7", size = 223414, upload-time = "2026-07-12T20:56:33.432Z" }, + { url = "https://files.pythonhosted.org/packages/5b/80/24e13ade0b6df8090e2492f9c55044bf1423f7ef28c76fc1af8c827a61cb/coverage-7.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:c337da8fca7ea93ab43f3868cfcde6cf6dad32c3906b273cfbad5d7390bc423b", size = 223888, upload-time = "2026-07-12T20:56:34.784Z" }, + { url = "https://files.pythonhosted.org/packages/2d/9e/34659930ae380491af48e2f5f5f9a2e99cd9fb7de3d30f27be5ac5425137/coverage-7.15.1-cp311-cp311-win_arm64.whl", hash = "sha256:db3403fdb7a94d5eb73e099befad8104d2a7d110a0f0d99df0de61c5d1fa756c", size = 223435, upload-time = "2026-07-12T20:56:36.302Z" }, + { url = "https://files.pythonhosted.org/packages/d9/76/32c1826309beaf4604c54accef108fdd611e5e5e93f2f5192f050cd5f6bd/coverage-7.15.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d9476292594309db922cc841dd13b303b3c388f4c25d279884f7e2341c681f80", size = 221497, upload-time = "2026-07-12T20:56:37.628Z" }, + { url = "https://files.pythonhosted.org/packages/db/5c/b88ce0d68fa550c7f3b58617fbf363bce64df5bf8295a01b627e4696e022/coverage-7.15.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0c579056b0de461b3a62318b63d0b6ce90aed7f8158d3f00da094df82f29d189", size = 221854, upload-time = "2026-07-12T20:56:39.033Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fe/8509fd2a66fc4e0a829f76a0f0b1dc3cc163368352435b5f243168658077/coverage-7.15.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:23214bdbe226f2b0e9c66a7d6a1d59d4a88045dcf86e702cf0fe0d0935e3d615", size = 253359, upload-time = "2026-07-12T20:56:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/e5/81/c7009ed7ea9765adb2b9d095054d748266fae5f07ac6c5f925f33715fcde/coverage-7.15.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:df164be93b46b4825cc39339440a05edc54c4d1d865ba4a60fd43d151a2a1cd3", size = 256096, upload-time = "2026-07-12T20:56:42.115Z" }, + { url = "https://files.pythonhosted.org/packages/21/52/dc8ee03968a5ba86e2da5aa48ddc9e3747bd65d63825fdce2d96acb9c5ff/coverage-7.15.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a524fca1a6f08927d9dc2d4c873cfb7bd7202c247f08b14bdc02424071b8b304", size = 257211, upload-time = "2026-07-12T20:56:43.513Z" }, + { url = "https://files.pythonhosted.org/packages/b8/27/95d7623908da8937deb53d48efcdbf423907a47540e63c62fa21372c652b/coverage-7.15.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d70f3542cd38de85a9e257dcb1ac4c1ab4b6d7d2c2a645809207556628755d1c", size = 259473, upload-time = "2026-07-12T20:56:44.974Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3b/730d761928de97d585465680b568ae69622fb40716babadeabffe75cb51b/coverage-7.15.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d78aa537237212c4313aabe5e964b66acc86350ed19ebc56a3e202df33b6077b", size = 253759, upload-time = "2026-07-12T20:56:46.615Z" }, + { url = "https://files.pythonhosted.org/packages/f1/fc/6b9277acff1f9484b6c12857af5774689d1a6a95e13265f7405329d2f5da/coverage-7.15.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a318112bb4f79d9d04766196d5a3388caa825908a6a9b052aa87de3d9aea7c61", size = 255131, upload-time = "2026-07-12T20:56:48.073Z" }, + { url = "https://files.pythonhosted.org/packages/3d/f2/c704f86129594ba34e25a64695d2068c71d51c2b98907184d716c94f4aec/coverage-7.15.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e55d24cada901963eed5bc89fa562aa033f0d84b9d3de4ecf363737c13aed11e", size = 253275, upload-time = "2026-07-12T20:56:49.538Z" }, + { url = "https://files.pythonhosted.org/packages/f6/29/80fee8af47de4a6dce71ccf2938491f444687a756af258a56d8469b8f1b0/coverage-7.15.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3c78f0cea7275342cf2adc2ad5fdd0aafa106ad91e66d573568f2fcf62c41df5", size = 257345, upload-time = "2026-07-12T20:56:51.038Z" }, + { url = "https://files.pythonhosted.org/packages/20/21/a1e7d7ed1b48a8adf8fd5154d9e83fcc5ad8e6ff20ae00e44865057dce8d/coverage-7.15.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:86bd37eabe39977216f630a7fc1b698e7f5e81a191c7186013245c6c3d313f9d", size = 252844, upload-time = "2026-07-12T20:56:52.535Z" }, + { url = "https://files.pythonhosted.org/packages/a7/8c/a4bc26e6ee207d412f3678f04d74be1550e83140563ca0e4997510579712/coverage-7.15.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c6db15c217693bdc3ca0b84de1ba9afafe1c14c26a8a29d77f4ed0de2b6132e2", size = 254716, upload-time = "2026-07-12T20:56:53.968Z" }, + { url = "https://files.pythonhosted.org/packages/11/9d/8ad0266ecfada6353cf6627a1a02294cf55a907521b6ee0bd7b770cfd659/coverage-7.15.1-cp312-cp312-win32.whl", hash = "sha256:359f3fbe09a51500c51966596ee4ee4070b356552c70b3b2420eb200d68e0f76", size = 223554, upload-time = "2026-07-12T20:56:55.583Z" }, + { url = "https://files.pythonhosted.org/packages/81/6d/24224929e06c6e05a93f738bc5f9e8e6ab658f8f1d9b823e7b85430e28b8/coverage-7.15.1-cp312-cp312-win_amd64.whl", hash = "sha256:fa75dc099c126e941a9c0baa8ebd2cbc78bd778687534fe410baf754f6d9e374", size = 224087, upload-time = "2026-07-12T20:56:57.041Z" }, + { url = "https://files.pythonhosted.org/packages/35/23/f81441dd01de88e53c97842e706907b307d9078918c3f4998b11e9ac7250/coverage-7.15.1-cp312-cp312-win_arm64.whl", hash = "sha256:26f89cf6d0634375f454fa71057945ad18edb0f1607a90fecf22c57dc3dc289a", size = 223472, upload-time = "2026-07-12T20:56:58.594Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1e/6fa289d7993a2a39f1b283ddb58c4bfec80f7800be654b8ba8a9f6a07c63/coverage-7.15.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:71ac4ca1658ca99160fd58cc6967110e989c34b04627f24ed6ec9f70fb24571a", size = 221519, upload-time = "2026-07-12T20:57:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e1/0db4902a0588234a70ab0218073c0b20fbc5c740aa35f91d360160a2ebc9/coverage-7.15.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:26a40cbf2b13bd94af53ee02a424cb3bb96a9edfac0d00834bd068512a62714b", size = 221895, upload-time = "2026-07-12T20:57:01.867Z" }, + { url = "https://files.pythonhosted.org/packages/b4/cb/3719783865092dac5e08df842730305ee9ab1973ae7ddb6fbdf27d401f30/coverage-7.15.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f4c5a5eff4ad4f9f7088fd3fc7a66d98d06566ee294b3b053309fb0a3b45be1e", size = 252882, upload-time = "2026-07-12T20:57:03.459Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5e/caf3abbdbb22629626160ffc9c017eb995b7cb11c0be46b974834cef1792/coverage-7.15.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:962aa56c1c9b016d681265880eb6acc9966029d2c4c559319cc43a1abbb9b59a", size = 255479, upload-time = "2026-07-12T20:57:04.984Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f1/d60f375bfe095fef944f0f19427aefdbf9bdd5a9571c41a4bf6e2f5fdb81/coverage-7.15.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1678eb2dc57a8ce67601b029582ef6d41e9e6ca22692aaeccd4107e40f27386c", size = 256715, upload-time = "2026-07-12T20:57:06.446Z" }, + { url = "https://files.pythonhosted.org/packages/d7/17/8b0cbc90d02dc5adad4d9034c1824ec3fa567771b4c39d9c1e3f9b1431b8/coverage-7.15.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1174900a43f6f8c425fee10d7dbddc308adefcdc78aaced32357f5ab750a0e90", size = 258845, upload-time = "2026-07-12T20:57:08.092Z" }, + { url = "https://files.pythonhosted.org/packages/92/29/c5e69f5fb75c322e9a3e4ef64d02eebfc3d66efceccc8514ff80a3c13a56/coverage-7.15.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:98847557a6859cadf693792ce89f440cb89692993f60dc6d3a7e35f3d340216f", size = 253098, upload-time = "2026-07-12T20:57:09.636Z" }, + { url = "https://files.pythonhosted.org/packages/64/57/21144252fdd0c01d707d48fbcea13a80b0b7c42ced3f299f885ab8978c3a/coverage-7.15.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8697b2edb57143546a24389efc11e1b000cd5800fc20d84f04edb601e4a7cfb8", size = 254844, upload-time = "2026-07-12T20:57:11.141Z" }, + { url = "https://files.pythonhosted.org/packages/59/2a/499a28a322b0ce6768328e6c5bb2e2ad00ac068a7c7adb2ecd8533c8c5d9/coverage-7.15.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6827ac0519be3fe91bf96b4060eb00d1d24f82649b29862cd75a3cfca248b02a", size = 252807, upload-time = "2026-07-12T20:57:12.678Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/928a95da5da8b60f2b00e1482c7787b3316188e6d2d227fb8e124ada43a1/coverage-7.15.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2de8ecbbc77c7e4d22572779920ed8979c69168675e96be3a548c996568c6c31", size = 256965, upload-time = "2026-07-12T20:57:14.326Z" }, + { url = "https://files.pythonhosted.org/packages/16/10/889adbc1b8c9f866ed51e18a98bcafc0259fb9d29b81f50a719407c64ea8/coverage-7.15.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2b25f0f0fa5260df9d7bb55d47c8bdc23fa3382c1a18f7c9cae122e6c320b1ad", size = 252628, upload-time = "2026-07-12T20:57:15.892Z" }, + { url = "https://files.pythonhosted.org/packages/1a/30/a5e1871e5d93416511f8e359d1ccebfe0cbb050a1bbf7dd20228533ec0cf/coverage-7.15.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a2effcbd93ae340a58db718fe4181d967f84d352c4cefeaab4ff82ce813901a", size = 254399, upload-time = "2026-07-12T20:57:17.703Z" }, + { url = "https://files.pythonhosted.org/packages/2d/26/c36fbffd549dadbdd1a75827528fb00a4c46aa3187b007b750b1e2cebbf2/coverage-7.15.1-cp313-cp313-win32.whl", hash = "sha256:895e65c96aef0cecea250f6e35e9a32f11375514e1a0cb5210e0fda128c04e8e", size = 223564, upload-time = "2026-07-12T20:57:19.253Z" }, + { url = "https://files.pythonhosted.org/packages/16/fc/becbb9d2c4206d242b9b1e1e8e24a42f7926c0200dd3c788b9fab4bb96d5/coverage-7.15.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6d0a28b63a0d75f9ed5118105d1154fc3aa40a8605a30d5d87e3d043ad90fe7", size = 224106, upload-time = "2026-07-12T20:57:21.108Z" }, + { url = "https://files.pythonhosted.org/packages/d3/30/1cfc641461369b6858799fca61c0a8b5edc490c519bf7c636ffa6bbf556f/coverage-7.15.1-cp313-cp313-win_arm64.whl", hash = "sha256:b4ee9818e8bae3544379ad2c09b851c4fb886aaa8860d57a1c1316ddcb16db49", size = 223497, upload-time = "2026-07-12T20:57:22.734Z" }, + { url = "https://files.pythonhosted.org/packages/f0/46/81961952e7aebfb38ad0ae4264e8954cc607a7af9e7ac111f9fa986595cc/coverage-7.15.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a886af95f59edf67d5770fd3564d53f4a8af93f25f8c1d60d27e00d7f5674ee8", size = 221560, upload-time = "2026-07-12T20:57:24.282Z" }, + { url = "https://files.pythonhosted.org/packages/13/d2/ee14d715889f216baf47301d9f469e08fff6995552aaf67e897b282865f6/coverage-7.15.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:985657ebd707941de90d488d1cbb5efac20bdf81f7b91eba771624ccda4d36f4", size = 221894, upload-time = "2026-07-12T20:57:25.87Z" }, + { url = "https://files.pythonhosted.org/packages/f3/38/f830bc6e6c2c5f23f43847125e6c650d378872f7eeba8d49f1d42193e8a9/coverage-7.15.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5bbe2a06e0a5e1404d9ffbdb49b819bbd6a3bb198ebea4c8dfe7ad9f1e1c2e81", size = 252938, upload-time = "2026-07-12T20:57:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/e1/53/0d3dd963631259d794c898735d5436e68d6a8d40749c419a07ff7c171469/coverage-7.15.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bde0fe24083d0b7b3dbafa7a09f0796410af1afa2523f28f5f208d8340a4aaca", size = 255445, upload-time = "2026-07-12T20:57:29.234Z" }, + { url = "https://files.pythonhosted.org/packages/b1/fd/aabed228557565c958259251b89bab8c5669b31291fa63b3e2154ebb017a/coverage-7.15.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f89f7453d6d46db14cf233e2cd8edcd78de2b9c49d4f1dc109590b4e5dbfbb74", size = 256790, upload-time = "2026-07-12T20:57:30.826Z" }, + { url = "https://files.pythonhosted.org/packages/bc/aa/1cc888e5d3623e603c4e5399653cb25728bb2b40d7519188a3e293d24620/coverage-7.15.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc3656c9ecc27b36bd0907455b77f83c0069ca9ad4a66dec892b76c696eb6047", size = 259104, upload-time = "2026-07-12T20:57:32.63Z" }, + { url = "https://files.pythonhosted.org/packages/5f/61/fc16d5f5e53098dae41efa21e8ccc611a9b4fe922750dd03dc56db552182/coverage-7.15.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:24d8e85a2a45e44883b488c2659f51fa761dad5353fdb319b672a93facbd2ca9", size = 252956, upload-time = "2026-07-12T20:57:34.316Z" }, + { url = "https://files.pythonhosted.org/packages/a1/f3/52384668c3de4519ca770bf1975a89e4d6eb5aa2faf0da0577a14008cba4/coverage-7.15.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68931b5fe746ed4fdaa8892989cab9e6c35781eeb3b0ab2ded893d561e1b3652", size = 254797, upload-time = "2026-07-12T20:57:35.947Z" }, + { url = "https://files.pythonhosted.org/packages/ce/68/54b807e7c1868178e902fd8360b5d4e559394462f97285c50edf1c4608db/coverage-7.15.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1ce6947e2a95534ecaa5a15e73c21e550514c980d80eda204d064d789a95f6a4", size = 252762, upload-time = "2026-07-12T20:57:37.856Z" }, + { url = "https://files.pythonhosted.org/packages/7b/48/dde8adf0338e3ace738757dccf1ce817e5fdcadfae77e1b48a77e5a3b265/coverage-7.15.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:841befdbc89b9c82435fc25b0f4f41858b6238693e45af758bec4cfc1968171c", size = 257037, upload-time = "2026-07-12T20:57:39.488Z" }, + { url = "https://files.pythonhosted.org/packages/07/f2/179dd88cf60a0aeeee16a970ffe250dccea8b80ed4beab4c5d3f6c41ad4b/coverage-7.15.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5d3de58b837375e7f4c0e1a088ccab5f655efb2fd7427b729df02c862a559633", size = 252577, upload-time = "2026-07-12T20:57:41.363Z" }, + { url = "https://files.pythonhosted.org/packages/2c/37/8a593d69ab521beb6a105a2017cac4ba94425ee0a8349e29c3c0b522d24f/coverage-7.15.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b1801963f9f44ae0c0f6d737bc7aeb2bbcde7d1fe7e3b43cddc1961af42d3b41", size = 254235, upload-time = "2026-07-12T20:57:43.025Z" }, + { url = "https://files.pythonhosted.org/packages/6d/34/bc9b3bced66f2cdad4bf5e57ae51c54ea226e8aaaebfc9370a9a11877bf3/coverage-7.15.1-cp314-cp314-win32.whl", hash = "sha256:8c7953c4128ef53b6ffb5f90d87c87d4ce26731df294760bb2314eb0e069e44b", size = 223771, upload-time = "2026-07-12T20:57:44.662Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f3/4d20337bed61915d14349e62b88d5e4144d5a9872b64adbe90e9906db6db/coverage-7.15.1-cp314-cp314-win_amd64.whl", hash = "sha256:6f0bab60a582d415f0fb535ccff13ba334a47a1538f98913330a525d23bd535a", size = 224257, upload-time = "2026-07-12T20:57:46.412Z" }, + { url = "https://files.pythonhosted.org/packages/7b/df/bbfeae4948f3ded516f92b32f2d57952427fc5ecfc0924487bb6ee6a5f38/coverage-7.15.1-cp314-cp314-win_arm64.whl", hash = "sha256:0f410ee8f0ac4ec7db71bc0b7632a8b9994e1cad2755bd1566c17e6a162caa74", size = 223683, upload-time = "2026-07-12T20:57:48.106Z" }, + { url = "https://files.pythonhosted.org/packages/35/65/0b431856064e387d1f5cf474625e4a0465e907024d42f35de6af19ced0be/coverage-7.15.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:fc868bab88e049d41fcd41766810d790a8b960053be2a45e060f5ce0d31d258b", size = 222298, upload-time = "2026-07-12T20:57:49.882Z" }, + { url = "https://files.pythonhosted.org/packages/a6/96/50eac9bd49df8a3df5f3d38746d1bf332299dffb554486c94ebd55c9dc49/coverage-7.15.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:206d4ec6028f2773b40932d09f074539d6bcdd8f6b318d40cb04bdbd68ed0b49", size = 222561, upload-time = "2026-07-12T20:57:51.688Z" }, + { url = "https://files.pythonhosted.org/packages/e0/5b/6ba1c4a27e10b8816fd2622b98162c83d3bdf1185097360373611bf96364/coverage-7.15.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:620482ef1c9f4e61f962e159325fe77dea59d16e39d9c9470d069053b244d864", size = 263923, upload-time = "2026-07-12T20:57:53.392Z" }, + { url = "https://files.pythonhosted.org/packages/e4/59/fe03ade97a3ca2d890e98c572cf48a99fda9adba85757c34b823f41efe1e/coverage-7.15.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d385fc9b054e309ad3cecdc77b586d2af0c98aeec2fdb3773544586f366e817c", size = 266043, upload-time = "2026-07-12T20:57:55.095Z" }, + { url = "https://files.pythonhosted.org/packages/16/e0/55c4b1217a572a43e13b39e1eb78d0da29fb23679003bd0cdf22c50b1978/coverage-7.15.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1198bca9c0dd7c188aae1f185b0c0b5fc4f0a2b6909000858c29550320bdb07", size = 268465, upload-time = "2026-07-12T20:57:57.017Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e2/ee47944f76afc03909119b036fe9e0da8cbd274a5141287de79791a0fb6d/coverage-7.15.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d0297e6a070eadb49df7cddd0ab6f420b8b689dd8904c7dd815a323168fa57e", size = 269584, upload-time = "2026-07-12T20:57:58.958Z" }, + { url = "https://files.pythonhosted.org/packages/cb/8a/6b4d9779c7b2e21c3d12c3425e3261aa7411399319e27aa402dfec4db5d0/coverage-7.15.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916fcf2214f56960e409561b37fc32a160a42b6e85483d0652d7b70fa55d707e", size = 263019, upload-time = "2026-07-12T20:58:00.979Z" }, + { url = "https://files.pythonhosted.org/packages/c4/1e/db5c7fa0c8ba5ece390a1e1a3f30db71d440240a80589df28e66a7503c40/coverage-7.15.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f837bae572c7869ffaa502e604c87e182543012831cf87aae4586ad090ac6dcf", size = 265916, upload-time = "2026-07-12T20:58:03.005Z" }, + { url = "https://files.pythonhosted.org/packages/83/53/fe5176682b00709b13fab36addd26883139d0dea430816fea412e69255e2/coverage-7.15.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3ea65e3ee6c7c32349fd00559927a9e577bdd72386087eeed1c42b62dfce9b82", size = 263520, upload-time = "2026-07-12T20:58:04.994Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e7/16f15127be93fbc70c667df5ec5dce934fc76c9b0888d84969a5d5341e2c/coverage-7.15.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:345034976f46a1c54bd17f4e43eb30bb92cb7082fcddff03250cff136cc4eb82", size = 267254, upload-time = "2026-07-12T20:58:06.824Z" }, + { url = "https://files.pythonhosted.org/packages/cd/73/e5119111f6f065376395a525f7ce6e9174d83f3db6d217ea0211a61cca4d/coverage-7.15.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4f051a64eb8f8addb4661c2b41d6eea5b7ebc68ad4b2baea8d9bc54e1956e5f7", size = 262366, upload-time = "2026-07-12T20:58:08.555Z" }, + { url = "https://files.pythonhosted.org/packages/d7/9c/6d0a81182df18a73b081e7a8630f0e2a52b12dfd7898c6ab839551a454d2/coverage-7.15.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a7625770f7720b49bb30d194ad2f8d50fab3c5177874af3d2399676f95f9c594", size = 264680, upload-time = "2026-07-12T20:58:10.359Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a2/bac0cbd4450638f1be2041a464b1766c8cc94abf705a2df6f1c8d4be870d/coverage-7.15.1-cp314-cp314t-win32.whl", hash = "sha256:81e503d130a472ad1bd38199ecd35116b40d92bcd31e27a2cacde035381f2070", size = 224077, upload-time = "2026-07-12T20:58:12.065Z" }, + { url = "https://files.pythonhosted.org/packages/f0/b2/d83c5403155172a43ba47c08641bad3f89822d8405102423a41339d2c857/coverage-7.15.1-cp314-cp314t-win_amd64.whl", hash = "sha256:724e878b213b302ad46e9f2fc872d386613f20ebfc492a211482d917ea76c14f", size = 224908, upload-time = "2026-07-12T20:58:13.956Z" }, + { url = "https://files.pythonhosted.org/packages/cc/41/442b74cad832cc77712080585455482e7cc4f4a9a13192f65731dcd18231/coverage-7.15.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ce2f05c14d077f406fefc4fa5e4f093ad0e0787549f6582535d6e28766f0361b", size = 224219, upload-time = "2026-07-12T20:58:16.315Z" }, + { url = "https://files.pythonhosted.org/packages/34/98/07a67cf1a26e795d617ed5c540c042b0ac87b72f810c30c07f076cf334f3/coverage-7.15.1-py3-none-any.whl", hash = "sha256:717d01e6e00bed56ad13306f19e0dd2f4f645ee8159d2c72c72301d6cfc7090c", size = 213284, upload-time = "2026-07-12T20:58:18.079Z" }, ] [package.optional-dependencies] @@ -793,11 +803,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.29.5" +version = "3.29.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/ee/29c668c50888588c432a702f7c2e8ee8a0c9e5286028d91f170308d6b2e9/filelock-3.29.5.tar.gz", hash = "sha256:6e6034c57a00a020e767f2614a5539863f056de7e7991d6d1473aef7ff73f156", size = 68927, upload-time = "2026-07-03T03:50:31.818Z" } +sdist = { url = "https://files.pythonhosted.org/packages/35/94/00f2059e4835eace3ae8fde680b932c496f8ec7bdc99168dfa53fb2e6b79/filelock-3.29.7.tar.gz", hash = "sha256:5b481979797ae69e72f0b389d89a80bdd585c260c5b3f1fb9c0a5ba9bb3f195d", size = 71521, upload-time = "2026-07-08T05:46:58.716Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/e3/f1fae3647d170919c2cf2a898e77e7d1a4e5c7cae0aed7bb4bd3f5ebff6f/filelock-3.29.5-py3-none-any.whl", hash = "sha256:8af830889ba3a0ffcefbd6c7d2af8a54012058103771f2e10848222f476a1693", size = 45073, upload-time = "2026-07-03T03:50:30.445Z" }, + { url = "https://files.pythonhosted.org/packages/60/02/be4a57b60c7149b55b9e3b3c13f609cd8eb5307c751f22bd8fb8d262e75b/filelock-3.29.7-py3-none-any.whl", hash = "sha256:987db6f789a3a2a59f55081801b2b3697cb97e2a736b5f1a9e99b559285fbc51", size = 46036, upload-time = "2026-07-08T05:46:57.53Z" }, ] [[package]] @@ -1746,7 +1756,7 @@ provides-extras = ["qiskit"] [package.metadata.requires-dev] build = [ { name = "nanobind", specifier = "~=2.13.0" }, - { name = "scikit-build-core", specifier = ">=0.12.2" }, + { name = "scikit-build-core", specifier = "~=0.12" }, { name = "setuptools-scm", specifier = ">=9.2.2" }, ] dev = [ @@ -1761,7 +1771,7 @@ dev = [ { name = "pytest-sugar", specifier = ">=1.1.1" }, { name = "pytest-xdist", specifier = ">=3.8.0" }, { name = "qiskit", extras = ["qasm3-import"], specifier = ">=1.1.0" }, - { name = "scikit-build-core", specifier = ">=0.12.2" }, + { name = "scikit-build-core", specifier = "~=0.12" }, { name = "setuptools-scm", specifier = ">=9.2.2" }, ] docs = [ @@ -1920,7 +1930,7 @@ wheels = [ [[package]] name = "nox" -version = "2026.4.10" +version = "2026.7.11" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "argcomplete" }, @@ -1932,9 +1942,9 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "virtualenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e7/6b/e672c862a43cfca704d32359221fa3780226daa1e5db5dfc401bcc8be9c9/nox-2026.4.10.tar.gz", hash = "sha256:2d0af5374f3f37a295428c927d1b04a8182aa01762897d172446dda2f1ce9692", size = 4034839, upload-time = "2026-04-10T17:42:42.209Z" } +sdist = { url = "https://files.pythonhosted.org/packages/75/bf/aafe066019cb1bcd3e1c22957412d6eb560bd6553c1afd28502168a51418/nox-2026.7.11.tar.gz", hash = "sha256:dec9bd2c854540a2d5c0b841eaaf1d23a7c26cd90af36d9f1f1668b34524bfd9", size = 4042267, upload-time = "2026-07-12T00:32:19.859Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/95/4df134a100b5a9a12378d5301b934366686ef6fbdaffcd21211d5654970e/nox-2026.4.10-py3-none-any.whl", hash = "sha256:082c117627590d9b90aa21f86df89b310b07c5842539524203bcb3c719f116c1", size = 75536, upload-time = "2026-04-10T17:42:40.664Z" }, + { url = "https://files.pythonhosted.org/packages/a0/70/4b459b66bbd7c9be3c9b15f2cf1605bbe10f58c6395fd99a59a21f3c0777/nox-2026.7.11-py3-none-any.whl", hash = "sha256:f5e811693ee8374d269396204eb39990d2084da67ed968239f94301805c9a169", size = 77265, upload-time = "2026-07-12T00:32:18.07Z" }, ] [[package]] @@ -2678,15 +2688,15 @@ wheels = [ [[package]] name = "python-discovery" -version = "1.4.3" +version = "1.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/26/8b004cc36f430345136f6f00fa1aa9ed596c8ed1e8504625fa79522ff39c/python_discovery-1.4.3.tar.gz", hash = "sha256:ad57d7045a862460d4a235986c33f13ed707d3aeb9153fa47eb7dfd0d4673289", size = 70438, upload-time = "2026-07-03T13:21:51.621Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/81/58c70036dffeccb7fe7d79d6260c69f7a28272bbd3909c29a01ea9422744/python_discovery-1.4.4.tar.gz", hash = "sha256:5cad33982d412c1f3ffb8f9ca4ea292c9680bca3942451d30b69c37fce53a4a3", size = 72212, upload-time = "2026-07-08T23:06:50.691Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/78/9b77ecb4644d1bbea94d29abf78f21c47eca6eb79e9745b702ec0bed2e19/python_discovery-1.4.3-py3-none-any.whl", hash = "sha256:b6e1e4a7d9e3f6948c39746ffe8218225162d738ba39d05ab1d2f6c1cac4878c", size = 33885, upload-time = "2026-07-03T13:21:50.174Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ae/84bc0d2440c95772272bb6f4b3d09ccf08b2898fce89b3d4f969a9fc74e9/python_discovery-1.4.4-py3-none-any.whl", hash = "sha256:abebe9120b43453b68c908acfb1e72a19d1a959ed2cb620ad38fc57d08056dbe", size = 34181, upload-time = "2026-07-08T23:06:49.402Z" }, ] [[package]] @@ -4018,11 +4028,11 @@ wheels = [ [[package]] name = "tzdata" -version = "2026.2" +version = "2026.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } +sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, + { url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" }, ] [[package]] @@ -4050,7 +4060,7 @@ wheels = [ [[package]] name = "virtualenv" -version = "21.5.1" +version = "21.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, @@ -4059,9 +4069,9 @@ dependencies = [ { name = "python-discovery" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f1/a5/81f987504738e6defeed61ec1c47e2aefab3c35d8eeb87e1b3f38cf28254/virtualenv-21.5.1.tar.gz", hash = "sha256:dca3bf98275a59c652b69d68e73433e597d977c2da9198882479d1a7188009c8", size = 4578798, upload-time = "2026-06-16T16:23:58.603Z" } +sdist = { url = "https://files.pythonhosted.org/packages/34/d9/b477fddb68840b570af8b22afe9b035cbc277b5fb7b33dea390617a8b10f/virtualenv-21.6.1.tar.gz", hash = "sha256:15f978b7cd329f24855ff4a0c4b4899cc7678589f49adbdcbbb4d3232e641128", size = 5526620, upload-time = "2026-07-10T19:33:53.312Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl", hash = "sha256:55aa670b67bbfb991b03fda39bd3276d92c419d702376e98c5df1c9989a26783", size = 4558820, upload-time = "2026-06-16T16:23:56.963Z" }, + { url = "https://files.pythonhosted.org/packages/c1/7c/4e7225d46d634a0d8d534dd8a6ce0c319d09b4d0cf0337eb314ca4789d8c/virtualenv-21.6.1-py3-none-any.whl", hash = "sha256:afe991df855715a2b2f60edfcc0107ef95a79fdfd8cb4cdaa71603d1c12e463b", size = 5506392, upload-time = "2026-07-10T19:33:51.629Z" }, ] [[package]] From bc6741399462f381653a294fe28aaa67e43507fc Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 13 Jul 2026 12:03:24 +0200 Subject: [PATCH 120/122] =?UTF-8?q?=F0=9F=93=9D=20Reorder=20CHANGELOG.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75357fe3fc..be7c8c0c40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,13 +12,13 @@ releases may include breaking changes. ### Added +- ✨ Add a `fuse-two-qubit-unitary-runs` pass for fusing compile-time two-qubit + unitary windows via Weyl/KAK resynthesis ([#1865]) ([**@simon1hofmann**], + [**@burgholzer**]) - ✨ Add support for custom job parameters to C++ and Python FoMaC library ([#1887]) ([**@flowerthrower**], [**@burgholzer**]) - ✨ Add QIR Output Schemas support to the QIR runtime ([#1877]) ([**@rturrado**]) -- ✨ Add a `fuse-two-qubit-unitary-runs` pass for fusing compile-time two-qubit - unitary windows via Weyl/KAK resynthesis ([#1865]) ([**@simon1hofmann**], - [**@burgholzer**]) - ✨ Add support for IQM's `move` gate in the QDMI Qiskit backend converter ([#1844], [#1848]) ([**@burgholzer**], [**@marcelwa**]) - 🚸 Add `const` version of the `CompoundOperation`'s `getOps()` function From 2236c0c235db454e48c84d05a5682e9576de9bac Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 13 Jul 2026 12:08:30 +0200 Subject: [PATCH 121/122] =?UTF-8?q?=E2=9C=A8=20Add=20missing=20empty=20lin?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/Decomposition/test_weyl_decomposition.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp index 153edee416..2b822c6cba 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_weyl_decomposition.cpp @@ -789,6 +789,7 @@ TEST_F(NativeGatesetMlirTest, AllowsOpMatchesGateset) { spec->allowsOp(GPhaseOp::create(builder, loc, 0.1).getOperation())); EXPECT_TRUE(spec->allowsOp( UOp::create(builder, loc, q0, 0.1, 0.2, 0.3).getOperation())); + auto cx = CtrlOp::create(builder, loc, q0, q1, [&](Value target) { return XOp::create(builder, loc, target).getOutputQubit(0); }); From 170eb491a8560e07fc85e6e2abe823068189d8ce Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 13 Jul 2026 12:45:37 +0200 Subject: [PATCH 122/122] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20linter=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp index e96e6ccfd1..14c76e7e71 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/NativeSynthesis/test_fuse_two_qubit_unitary_runs.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include