From 38b54987eb041e253c178b43f95f6937836bef7f Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 22 Apr 2026 13:40:25 +0200 Subject: [PATCH 01/47] =?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 02/47] =?UTF-8?q?=E2=9C=A8=20Implement=20quantum=20gate=20?= =?UTF-8?q?decomposition=20utilities,=20including=20two-qubit=20basis=20de?= =?UTF-8?q?composer,=20Euler=20decomposition,=20and=20associated=20helper?= =?UTF-8?q?=20functions.=20This=20update=20introduces=20new=20headers=20an?= =?UTF-8?q?d=20source=20files=20for=20managing=20gate=20sequences,=20unita?= =?UTF-8?q?ry=20matrices,=20and=20decomposition=20strategies,=20enhancing?= =?UTF-8?q?=20the=20framework's=20capabilities=20for=20quantum=20circuit?= =?UTF-8?q?=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 03/47] =?UTF-8?q?=E2=9C=A8=20Introduce=20native=20gate=20s?= =?UTF-8?q?ynthesis=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 04/47] =?UTF-8?q?=E2=9C=A8=20Enhance=20quantum=20gate=20de?= =?UTF-8?q?composition=20tests=20with=20new=20utility=20functions=20and=20?= =?UTF-8?q?additional=20test=20cases=20for=20Euler=20and=20Weyl=20decompos?= =?UTF-8?q?itions.?= 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 05/47] =?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 06/47] =?UTF-8?q?=E2=9C=A8=20Update=20documentation=20for?= =?UTF-8?q?=20native=20gate=20synthesis=20pass=20with=20enhanced=20example?= =?UTF-8?q?s=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 07/47] =?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 08/47] =?UTF-8?q?=E2=9C=85=20Refactor=20native=20synthesis?= =?UTF-8?q?=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 09/47] =?UTF-8?q?=E2=9C=A8=20Enhance=20two-qubit=20gate=20?= =?UTF-8?q?sequence=20emission=20by=20adding=20support=20for=20residual=20?= =?UTF-8?q?global=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 10/47] =?UTF-8?q?=E2=9C=A8=20Euler=20sequence=20support=20?= =?UTF-8?q?for=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 11/47] =?UTF-8?q?=E2=9C=A8=20Refactor=20parameter=20orderi?= =?UTF-8?q?ng=20for=20U=20and=20U2=20gates=20in=20decomposition=20and=20sy?= =?UTF-8?q?nthesis=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 12/47] =?UTF-8?q?=F0=9F=93=9D=20Clean=20up=20documentation?= =?UTF-8?q?=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 13/47] =?UTF-8?q?=E2=9C=A8=20Support=20arbitrary=20single?= =?UTF-8?q?=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 14/47] =?UTF-8?q?=F0=9F=93=9D=20Update=20documentation=20f?= =?UTF-8?q?or=20native=20gate=20menu=20in=20QuantumCompilerConfig,=20clari?= =?UTF-8?q?fying=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 15/47] =?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 16/47] =?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 17/47] =?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 18/47] =?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 19/47] =?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 20/47] =?UTF-8?q?=E2=9C=85=20Introduce=20helper=20function?= =?UTF-8?q?s=20for=20retrieving=20unitary=20qubit=20operands=20and=20resul?= =?UTF-8?q?ts=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 21/47] =?UTF-8?q?=F0=9F=94=A7=20Update=20qubit=20compariso?= =?UTF-8?q?n=20logic=20in=20mergeTwoTargetOneParameter=20to=20ensure=20bot?= =?UTF-8?q?h=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 22/47] =?UTF-8?q?=F0=9F=94=A7=20Refactor=20single-qubit=20?= =?UTF-8?q?matrix=20extraction=20logic=20to=20use=20raw=20operation=20poin?= =?UTF-8?q?ters=20and=20ensure=20operand=20count=20validation=20for=20RZ,?= =?UTF-8?q?=20RX,=20RY,=20U,=20P,=20and=20R=20operations=20in=20native=20s?= =?UTF-8?q?ynthesis=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 23/47] =?UTF-8?q?=F0=9F=94=A7=20Enhance=20qubit=20comparis?= =?UTF-8?q?on=20logic=20in=20QCO=20operations=20to=20validate=20both=20qub?= =?UTF-8?q?it=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 24/47] =?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 25/47] =?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 26/47] =?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 27/47] =?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 28/47] =?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 29/47] =?UTF-8?q?=F0=9F=90=87=20Address=20Rabbit's=20Comme?= =?UTF-8?q?nts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/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 30/47] =?UTF-8?q?=F0=9F=90=87=20Address=20Rabbit's=20Comme?= =?UTF-8?q?nts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../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 31/47] =?UTF-8?q?=F0=9F=90=9B=20Skip=20stale=20windows=20i?= =?UTF-8?q?n=20TwoQubitWindowConsolidator=20to=20avoid=20erasing=20capture?= =?UTF-8?q?d=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 32/47] =?UTF-8?q?=F0=9F=90=87=20Address=20Rabbit's=20Comme?= =?UTF-8?q?nts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../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 33/47] =?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 34/47] =?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 35/47] =?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 36/47] =?UTF-8?q?=F0=9F=90=87=20Address=20Rabbit's=20Comme?= =?UTF-8?q?nts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../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 37/47] =?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 38/47] =?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 39/47] =?UTF-8?q?=F0=9F=8E=A8=20Revert=20unitary=20matrix?= =?UTF-8?q?=20calculations=20in=20QCO=20standard=20gates=20to=20use=20std:?= =?UTF-8?q?:polar=20for=20complex=20exponentiation,=20improving=20numerica?= =?UTF-8?q?l=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 40/47] =?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 41/47] =?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 42/47] =?UTF-8?q?=F0=9F=94=A5=20Remove=20`Eigen`=20and=20o?= =?UTF-8?q?ld=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 43/47] =?UTF-8?q?=F0=9F=94=A5=20Remove=20`fuseRzAcrossCtrl?= =?UTF-8?q?Controls`=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 44/47] =?UTF-8?q?=F0=9F=94=A5=20Remove=20`Gate.h`=20and=20?= =?UTF-8?q?`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 45/47] =?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 46/47] =?UTF-8?q?=E2=9C=A8=20Add=20`fuse-two-qubit-unitary?= =?UTF-8?q?-runs`=20pass=20for=20fusing=20compile-time=20two-qubit=20unita?= =?UTF-8?q?ry=20windows=20via=20Weyl/KAK=20resynthesis.=20Update=20changel?= =?UTF-8?q?og=20and=20remove=20obsolete=20files=20related=20to=20two-qubit?= =?UTF-8?q?=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 47/47] =?UTF-8?q?=F0=9F=94=A5=20Remove=20and=20merge=20tes?= =?UTF-8?q?ts?= 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