From e5280c5363f21024d11e1325a07f2bb863a4345c Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 28 Apr 2026 21:36:46 +0200 Subject: [PATCH 01/68] =?UTF-8?q?=E2=9C=A8=20Add=20Euler=20decomposition?= =?UTF-8?q?=20and=20supporting=20decomposition=20primitives?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tamino Bauknecht --- .../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 | 61 ++++ .../Decomposition/UnitaryMatrices.h | 55 +++ .../Transforms/Decomposition/EulerBasis.cpp | 52 +++ .../Decomposition/EulerDecomposition.cpp | 317 ++++++++++++++++++ .../Transforms/Decomposition/GateSequence.cpp | 55 +++ .../QCO/Transforms/Decomposition/Helpers.cpp | 65 ++++ .../Decomposition/UnitaryMatrices.cpp | 223 ++++++++++++ .../Dialect/QCO/Transforms/CMakeLists.txt | 1 + .../Transforms/Decomposition/CMakeLists.txt | 16 + .../Decomposition/decomposition_test_utils.h | 52 +++ .../test_decomposition_helpers.cpp | 59 ++++ .../test_euler_decomposition.cpp | 253 ++++++++++++++ 17 files changed, 1559 insertions(+) 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/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/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_decomposition_helpers.cpp create mode 100644 mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp 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..0bef568fb8 --- /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`. +/// `QubitGateSequence::getUnitaryMatrix()` still returns an `Eigen::Matrix4cd` +/// in the shared two-qubit workspace convention, even for one-qubit sequences. +using OneQubitGateSequence = 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..1d2f1fae02 --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" + +#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 { + +/// 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) { + if (matrix.rows() != matrix.cols()) { + return false; + } + return (matrix.adjoint() * 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); + +/** + * 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..5eebb3885e --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.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 "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 theta, double phi, double lambda); +/// `U2(phi, lambda) == U(pi/2, phi, lambda)`. +[[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); +[[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::Matrix2cd H_GATE{{FRAC1_SQRT2, FRAC1_SQRT2}, + {FRAC1_SQRT2, -FRAC1_SQRT2}}; + +/// Kronecker-embed a 2x2 on wire ``qubitId`` (identity on the other wire). +[[nodiscard]] Eigen::Matrix4cd +expandToTwoQubits(const Eigen::Matrix2cd& singleQubitMatrix, QubitId qubitId); + +/// 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/lib/Dialect/QCO/Transforms/Decomposition/EulerBasis.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerBasis.cpp new file mode 100644 index 0000000000..a12771bdd9 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerBasis.cpp @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h" + +#include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.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..d781990cda --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.cpp @@ -0,0 +1,317 @@ +/* + * 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(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 = {theta, phi, lambda}}}, + .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 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 new file mode 100644 index 0000000000..29db6a303e --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/GateSequence.cpp @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h" + +#include "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 new file mode 100644 index 0000000000..bec566df84 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp @@ -0,0 +1,65 @@ +/* + * 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/Transforms/Decomposition/GateKind.h" + +#include + +#include +#include +#include +#include + +namespace mlir::qco::helpers { + +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; +} + +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; +} + +std::size_t getComplexity(decomposition::GateKind type, + std::size_t numOfQubits) { + if (type == decomposition::GateKind::GPhase) { + return 0; + } + if (numOfQubits > 1) { + // Multi-qubit operations dominate the heuristic cost model. + constexpr std::size_t multiQubitFactor = 10; + return (numOfQubits - 1) * multiQubitFactor; + } + 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..817c380fce --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp @@ -0,0 +1,223 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" + +#include "mlir/Dialect/QCO/Transforms/Decomposition/Gate.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.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.)}}}}; +} + +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}}}; +} + +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::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); + 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); + const double phi = gate.parameter[0]; + const double lambda = gate.parameter[1]; + return u2Matrix(phi, lambda); + } + if (gate.type == GateKind::H) { + return H_GATE; + } + 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) { + 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) { + 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 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}}; + } + 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/unittests/Dialect/QCO/Transforms/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/CMakeLists.txt index 9f9b03449d..d59780f461 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/CMakeLists.txt +++ b/mlir/unittests/Dialect/QCO/Transforms/CMakeLists.txt @@ -6,5 +6,6 @@ # # Licensed under the MIT License +add_subdirectory(Decomposition) add_subdirectory(Mapping) add_subdirectory(Optimizations) 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..83fe424bab --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt @@ -0,0 +1,16 @@ +# 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_decomposition_helpers.cpp test_euler_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..c30c8dc6f6 --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/decomposition_test_utils.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/Decomposition/Helpers.h" + +#include +#include + +#include +#include +#include + +namespace mlir::qco::decomposition_test { + +template +[[nodiscard]] MatrixType randomUnitaryMatrix(std::mt19937& rng) { + 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(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}; + } + const MatrixType unitaryMatrix = qMatrix * dMatrix; + assert(helpers::isUnitaryMatrix(unitaryMatrix)); + return unitaryMatrix; +} + +} // namespace mlir::qco::decomposition_test 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..ac63477f76 --- /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, GetComplexitySingleQubitAndGphase) { + EXPECT_EQ(getComplexity(GateKind::X, 1), 1U); + EXPECT_EQ(getComplexity(GateKind::GPhase, 1), 0U); + EXPECT_EQ(getComplexity(GateKind::GPhase, 2), 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)); +} + +TEST(DecompositionHelpersTest, IsUnitaryMatrixAcceptsUnitary) { + const Eigen::Matrix2cd m = Eigen::Matrix2cd::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 new file mode 100644 index 0000000000..e41eac6d43 --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -0,0 +1,253 @@ +/* + * 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) { + const Eigen::Matrix4cd expanded = expandToTwoQubits(u, 0); + return expanded.isApprox(seq.getUnitaryMatrix()); +} + +// 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; + 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'; + } +} + +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); + OneQubitGateSequence zyzSeq{ + .gates = + { + {.type = GateKind::RZ, .parameter = {angles[2]}}, + {.type = GateKind::RY, .parameter = {angles[0]}}, + {.type = GateKind::RZ, .parameter = {angles[1]}}, + }, + .globalPhase = angles[3], + }; + const Eigen::Matrix2cd reconstructed = + EulerDecompositionTest::restore(zyzSeq); + + EXPECT_TRUE(reconstructed.isApprox(hadamard)); +} + +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)) * + decomposition::uMatrix(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(expanded.isApprox(u3Seq.getUnitaryMatrix())); + EXPECT_TRUE(expanded.isApprox(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)); +} + +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); + const auto angles = EulerDecomposition::anglesFromUnitary(u, EulerBasis::XZX); + OneQubitGateSequence seq{ + .gates = + { + {.type = GateKind::RX, .parameter = {angles[2]}}, + {.type = GateKind::RZ, .parameter = {angles[0]}}, + {.type = GateKind::RX, .parameter = {angles[1]}}, + }, + .globalPhase = angles[3], + }; + 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, + 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; }))); From 01ff59157ccd28942ade0e4f945aa7e2da436343 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 28 Apr 2026 22:00:20 +0200 Subject: [PATCH 02/68] =?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/Decomposition/decomposition_test_utils.h | 2 ++ .../QCO/Transforms/Decomposition/test_euler_decomposition.cpp | 4 ++++ 2 files changed, 6 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 c30c8dc6f6..724634523c 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/decomposition_test_utils.h +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/decomposition_test_utils.h @@ -26,6 +26,8 @@ template static_assert(MatrixType::RowsAtCompileTime != Eigen::Dynamic && MatrixType::ColsAtCompileTime != Eigen::Dynamic, "randomUnitaryMatrix requires fixed-size matrices"); + static_assert(MatrixType::RowsAtCompileTime == MatrixType::ColsAtCompileTime, + "randomUnitaryMatrix requires square matrices"); std::normal_distribution normalDist(0.0, 1.0); MatrixType randomMatrix; for (auto& x : randomMatrix.reshaped()) { 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 e41eac6d43..88e0b214e0 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -175,8 +175,12 @@ TEST(EulerDecompositionTest, ZsxxPauliXUsesSingleXGate) { pauliX << 0.0, 1.0, 1.0, 0.0; const auto seq = EulerDecomposition::generateCircuit(EulerBasis::ZSXX, pauliX, true, std::nullopt); + EXPECT_EQ(seq.gates.size(), 1U); EXPECT_EQ(countGatesOfType(seq, GateKind::X), 1U); + EXPECT_EQ(countGatesOfType(seq, GateKind::RZ), 0U); EXPECT_EQ(countGatesOfType(seq, GateKind::SX), 0U); + EXPECT_EQ(countGatesOfType(seq, GateKind::H), 0U); + EXPECT_EQ(countGatesOfType(seq, GateKind::U), 0U); EXPECT_TRUE(sequenceMatchesSingleQubitMatrix(pauliX, seq)); } From 2c749814b3376e56f2795a55271549c979ebf014 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 28 Apr 2026 22:14:27 +0200 Subject: [PATCH 03/68] =?UTF-8?q?=F0=9F=93=9D=20Update=20factorization=20d?= =?UTF-8?q?ocumentation=20in=20EulerDecomposition?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/Decomposition/EulerDecomposition.h | 8 ++++---- .../QCO/Transforms/Decomposition/EulerDecomposition.cpp | 9 +++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.h index 0a34812af3..285f0a4f3f 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.h @@ -53,19 +53,19 @@ class EulerDecomposition { anglesFromUnitary(const Eigen::Matrix2cd& matrix, EulerBasis basis); private: - /// Extract parameters for a `RZ(phi) RY(theta) RZ(lambda)` factorization. + /// 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. + /// 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. + /// 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. + /// Extract parameters for a `RX(phi) . RZ(theta) . RX(lambda)` factorization. [[nodiscard]] static std::array paramsXzx(const Eigen::Matrix2cd& matrix); diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.cpp index d781990cda..f82c4a36bf 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.cpp @@ -115,9 +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 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). + // 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}; @@ -195,7 +195,8 @@ EulerDecomposition::decomposeKAK(double theta, double phi, double lambda, .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). + // 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) { From b7307a3f032583b05f703af18afaab5159e5370e Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 29 Apr 2026 12:32:02 +0200 Subject: [PATCH 04/68] =?UTF-8?q?=F0=9F=93=9D=20Update=20docs=20and=20comm?= =?UTF-8?q?ents?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/Decomposition/EulerBasis.h | 2 +- .../Decomposition/EulerDecomposition.h | 10 +++---- .../Transforms/Decomposition/EulerBasis.cpp | 2 +- .../Decomposition/EulerDecomposition.cpp | 30 +++++++++---------- .../Decomposition/UnitaryMatrices.cpp | 16 ++++++---- 5 files changed, 32 insertions(+), 28 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h index 1834802039..c23c9111e2 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h @@ -39,7 +39,7 @@ enum class EulerBasis : std::uint8_t { 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`. + ZSXX = 7, ///< `rz · sx` chain, with `sx · rz(+/- pi) · sx` collapsed to `x`. ZSX = 8, ///< Like `ZSXX` but without the `x` shortcut. }; diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.h index 285f0a4f3f..8b9eb6a4f2 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.h @@ -53,19 +53,19 @@ class EulerDecomposition { anglesFromUnitary(const Eigen::Matrix2cd& matrix, EulerBasis basis); private: - /// Extract parameters for a `RZ(phi) . RY(theta) . RZ(lambda)` factorization. + /// 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. + /// 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. + /// 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. + /// Extract parameters for a `rx(phi) · rz(theta) · rx(lambda)` factorization. [[nodiscard]] static std::array paramsXzx(const Eigen::Matrix2cd& matrix); @@ -124,7 +124,7 @@ class EulerDecomposition { * `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` + * 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. diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerBasis.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerBasis.cpp index a12771bdd9..6595a8e44f 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerBasis.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerBasis.cpp @@ -41,7 +41,7 @@ getGateTypesForEulerBasis(EulerBasis eulerBasis) { // `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 + // `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}; } diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.cpp index f82c4a36bf..27ea5bf699 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.cpp @@ -115,9 +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 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). + // 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}; @@ -195,8 +195,8 @@ EulerDecomposition::decomposeKAK(double theta, double phi, double lambda, .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). + // `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) { @@ -251,7 +251,7 @@ EulerDecomposition::decomposePsxGen(double theta, double phi, double lambda, .globalPhase = phase, }; - // Append `RZ(angle)` and add `angle / 2` to `globalPhase` so the combined + // 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) { @@ -263,16 +263,16 @@ EulerDecomposition::decomposePsxGen(double theta, double phi, double lambda, } }; - // Zero-`sx` decomposition: RZ(phi) . I . RZ(lambda) collapses to a single - // phase gate RZ(lambda + phi) (plus the matching phase correction). + // 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} + // `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}); @@ -294,17 +294,17 @@ EulerDecomposition::decomposePsxGen(double theta, double phi, double lambda, 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). + // `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). + // `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}); diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp index 817c380fce..c4086f037a 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp @@ -176,11 +176,13 @@ Eigen::Matrix4cd getTwoQubitMatrix(const Gate& gate) { "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. + // Controlled-X (`cx`) is directional: swapping `{control, target}` + // changes the operator. We therefore handle both orderings explicitly. + // + // The two matrices below represent `cx` for the two possible + // `Gate::qubitId` orderings. Qubit 0 is the MSB of the 4x4 computational + // basis (matching `UnitaryOpInterface::getUnitaryMatrix4x4`), so + // `{0,1}` and `{1,0}` lead to different basis-layout matrices. if (validPair01) { // control = wire 0 (MSB), target = wire 1. return Eigen::Matrix4cd{ @@ -194,7 +196,9 @@ Eigen::Matrix4cd getTwoQubitMatrix(const Gate& gate) { llvm::reportFatalInternalError("Invalid qubit IDs for CX gate"); } if (gate.type == GateKind::Z) { - // controlled Z (CZ) + // Controlled-Z (`cz`) is symmetric in its two qubits; swapping `{0,1}` + // and + // `{1,0}` yields the same operator, so one matrix is sufficient. return Eigen::Matrix4cd{ {1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}, {0, 0, 0, -1}}; } From e978f1695481cc414007a077b89855a180c14311 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 29 Apr 2026 12:46:43 +0200 Subject: [PATCH 05/68] =?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/Decomposition/UnitaryMatrices.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp index c4086f037a..a9919fb986 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp @@ -161,7 +161,11 @@ Eigen::Matrix2cd getSingleQubitMatrix(const Gate& gate) { // Used by sequence verification and `QubitGateSequence::getUnitaryMatrix()`. Eigen::Matrix4cd getTwoQubitMatrix(const Gate& gate) { if (gate.qubitId.empty()) { - return Eigen::Matrix4cd::Identity(); + if (gate.type == GateKind::I) { + return Eigen::Matrix4cd::Identity(); + } + llvm::reportFatalInternalError( + "Invalid gate: empty qubit IDs are only allowed for identity"); } if (gate.qubitId.size() == 1) { return expandToTwoQubits(getSingleQubitMatrix(gate), gate.qubitId[0]); From 1d1ba722f050d812aee13a0c75f319e9f9c91a3e Mon Sep 17 00:00:00 2001 From: Daniel Haag <121057143+denialhaag@users.noreply.github.com> Date: Wed, 29 Apr 2026 16:14:23 +0200 Subject: [PATCH 06/68] Adapt to new include style Co-authored-by: Copilot --- .../Dialect/QCO/Transforms/Decomposition/EulerBasis.h | 3 ++- .../mlir/Dialect/QCO/Transforms/Decomposition/Gate.h | 5 +++-- .../Dialect/QCO/Transforms/Decomposition/GateSequence.h | 6 ++---- .../QCO/Transforms/Decomposition/UnitaryMatrices.h | 1 - .../Dialect/QCO/Transforms/Decomposition/EulerBasis.cpp | 4 ++-- .../QCO/Transforms/Decomposition/UnitaryMatrices.cpp | 8 +++----- 6 files changed, 12 insertions(+), 15 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h index c23c9111e2..def869612f 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h @@ -13,6 +13,7 @@ #include "GateKind.h" #include +#include #include @@ -49,7 +50,7 @@ enum class EulerBasis : std::uint8_t { * The result describes the basis alphabet, not the exact gate count. Some * decompositions emit fewer than three gates after simplification. */ -[[nodiscard]] llvm::SmallVector +[[nodiscard]] SmallVector getGateTypesForEulerBasis(EulerBasis eulerBasis); } // 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 index 429f10482f..efada70fcd 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Gate.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Gate.h @@ -13,6 +13,7 @@ #include "GateKind.h" #include +#include #include @@ -32,10 +33,10 @@ struct Gate { GateKind type{GateKind::I}; /// Gate parameters in operation-specific order. - llvm::SmallVector parameter; + SmallVector parameter; /// Logical qubit ids used by the gate, in operand order. - llvm::SmallVector qubitId = {0}; + SmallVector qubitId = {0}; }; } // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h index 0bef568fb8..ae68ca40bc 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h @@ -14,6 +14,7 @@ #include #include +#include #include @@ -29,11 +30,8 @@ namespace mlir::qco::decomposition { * `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; + SmallVector gates; /// Residual global phase in radians, not represented by explicit gates. double globalPhase{}; diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h index 5eebb3885e..7b8e91c34f 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h @@ -13,7 +13,6 @@ #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 diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerBasis.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerBasis.cpp index 6595a8e44f..5b6db92610 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerBasis.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerBasis.cpp @@ -12,12 +12,12 @@ #include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" -#include #include +#include namespace mlir::qco::decomposition { -[[nodiscard]] llvm::SmallVector +[[nodiscard]] SmallVector getGateTypesForEulerBasis(EulerBasis eulerBasis) { switch (eulerBasis) { case EulerBasis::ZYZ: diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp index a9919fb986..e8293bbbd7 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp @@ -14,8 +14,8 @@ #include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" #include -#include #include +#include #include #include @@ -171,10 +171,8 @@ 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}; + const bool validPair01 = gate.qubitId == SmallVector{0, 1}; + const bool validPair10 = gate.qubitId == SmallVector{1, 0}; if (!validPair01 && !validPair10) { llvm::reportFatalInternalError( "Invalid two-qubit gate qubit IDs: expected {0,1} or {1,0}"); From ab35f91acb5561a30756490e79008064864b0929 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 19 May 2026 16:36:48 +0200 Subject: [PATCH 07/68] =?UTF-8?q?=E2=9C=A8=20Add=20QCO=20UnitaryMatrixOpIn?= =?UTF-8?q?terface=20for=20compile-time=20unitary=20matrices?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split matrix extraction from UnitaryOpInterface so gates can expose constant unitary matrices only when parameters are known at compile time. --- .../mlir/Dialect/QCO/IR/CMakeLists.txt | 3 + .../mlir/Dialect/QCO/IR/QCOInterfaces.h | 1 - .../mlir/Dialect/QCO/IR/QCOInterfaces.td | 111 +------------- mlir/include/mlir/Dialect/QCO/IR/QCOOps.h | 1 + mlir/include/mlir/Dialect/QCO/IR/QCOOps.td | 102 ++++++++----- .../QCO/IR/QCOUnitaryMatrixInterfaces.h | 26 ++++ .../QCO/IR/QCOUnitaryMatrixInterfaces.td | 139 ++++++++++++++++++ mlir/lib/Dialect/QCO/IR/CMakeLists.txt | 2 + .../QCO/IR/QCOUnitaryMatrixInterfaces.cpp | 13 ++ 9 files changed, 249 insertions(+), 149 deletions(-) create mode 100644 mlir/include/mlir/Dialect/QCO/IR/QCOUnitaryMatrixInterfaces.h create mode 100644 mlir/include/mlir/Dialect/QCO/IR/QCOUnitaryMatrixInterfaces.td create mode 100644 mlir/lib/Dialect/QCO/IR/QCOUnitaryMatrixInterfaces.cpp diff --git a/mlir/include/mlir/Dialect/QCO/IR/CMakeLists.txt b/mlir/include/mlir/Dialect/QCO/IR/CMakeLists.txt index f5368621d1..f5a9e74218 100644 --- a/mlir/include/mlir/Dialect/QCO/IR/CMakeLists.txt +++ b/mlir/include/mlir/Dialect/QCO/IR/CMakeLists.txt @@ -8,6 +8,9 @@ add_mlir_dialect(QCOOps qco) add_mlir_interface(QCOInterfaces) +add_mlir_interface(QCOUnitaryMatrixInterfaces) add_mlir_doc(QCOOps QCODialect Dialects/ -gen-dialect-doc) add_mlir_doc(QCOInterfaces QCOInterfaces Dialects/ -gen-op-interface-docs -dialect=qco) +add_mlir_doc(QCOUnitaryMatrixInterfaces QCOUnitaryMatrixInterfaces Dialects/ -gen-op-interface-docs + -dialect=qco) diff --git a/mlir/include/mlir/Dialect/QCO/IR/QCOInterfaces.h b/mlir/include/mlir/Dialect/QCO/IR/QCOInterfaces.h index 035b0b5268..6296cd0325 100644 --- a/mlir/include/mlir/Dialect/QCO/IR/QCOInterfaces.h +++ b/mlir/include/mlir/Dialect/QCO/IR/QCOInterfaces.h @@ -10,7 +10,6 @@ #pragma once -#include #include #include #include diff --git a/mlir/include/mlir/Dialect/QCO/IR/QCOInterfaces.td b/mlir/include/mlir/Dialect/QCO/IR/QCOInterfaces.td index 3de8255b8e..4a507ea984 100644 --- a/mlir/include/mlir/Dialect/QCO/IR/QCOInterfaces.td +++ b/mlir/include/mlir/Dialect/QCO/IR/QCOInterfaces.td @@ -28,62 +28,6 @@ def UnitaryOpInterface : OpInterface<"UnitaryOpInterface"> { let cppNamespace = "::mlir::qco"; - // Generic implementation body for getUnitaryMatrix methods - defvar unitaryMatrixMethodBody = [{ - auto process = [&](MatrixType&& m) -> bool { - using TargetT = std::remove_cvref_t; - using SourceT = std::remove_cvref_t; - - constexpr bool isTargetDynamic = - (TargetT::SizeAtCompileTime == Eigen::Dynamic); - constexpr bool isSourceDynamic = - (SourceT::SizeAtCompileTime == Eigen::Dynamic); - - // Case 1: Target is Dynamic. Always accepts source. - if constexpr (isTargetDynamic) { - out = std::forward(m); - return true; - } - // Case 2: Target is Fixed. - else { - // Case 2a: Source is Dynamic. Runtime dimension check required. - if constexpr (isSourceDynamic) { - if (m.rows() == static_cast(TargetT::RowsAtCompileTime) && - m.cols() == static_cast(TargetT::ColsAtCompileTime)) - [[likely]] { - out = std::forward(m); - return true; - } - } - // Case 2b: Source is Fixed. Compile-time check. - else if constexpr (static_cast( - SourceT::RowsAtCompileTime) == - static_cast( - TargetT::RowsAtCompileTime) && - static_cast( - SourceT::ColsAtCompileTime) == - static_cast( - TargetT::ColsAtCompileTime)) { - out = std::forward(m); - return true; - } - } - return false; - }; - - - if constexpr (requires { $_op.getUnitaryMatrix().has_value(); }) { - if (auto&& matrix = $_op.getUnitaryMatrix()) { - return process(std::move(*matrix)); - } - return false; - } else if constexpr (requires { $_op.getUnitaryMatrix(); }) { - return process($_op.getUnitaryMatrix()); - } else { - llvm::reportFatalUsageError("Operation '" + $_op.getBaseSymbol() + "' has no unitary matrix definition!"); - } - }]; - let methods = [ // Qubit accessors InterfaceMethod< @@ -152,60 +96,7 @@ def UnitaryOpInterface : OpInterface<"UnitaryOpInterface"> { // Identification InterfaceMethod<"Returns the base symbol/mnemonic of the operation.", - "StringRef", "getBaseSymbol", (ins)>, - - // Unitary matrix helpers - InterfaceMethod<"Populates the given 1x1 unitary matrix if possible.", - "bool", "getUnitaryMatrix1x1", - (ins "Eigen::Matrix, 1, 1>&":$out), - unitaryMatrixMethodBody>, - InterfaceMethod<"Populates the given 2x2 unitary matrix if possible.", - "bool", "getUnitaryMatrix2x2", - (ins "Eigen::Matrix2cd&":$out), unitaryMatrixMethodBody>, - InterfaceMethod<"Populates the given 4x4 unitary matrix if possible.", - "bool", "getUnitaryMatrix4x4", - (ins "Eigen::Matrix4cd&":$out), unitaryMatrixMethodBody>, - InterfaceMethod<"Populates the given dynamic unitary matrix.", "bool", - "getUnitaryMatrixDynamic", (ins "Eigen::MatrixXcd&":$out), - unitaryMatrixMethodBody>]; - - let extraClassDeclaration = [{ - template - std::optional getUnitaryMatrix() { - MatrixType out; - bool result = false; - - // Dispatch to the appropriate fixed-size or dynamic method based on the - // matrix type. - if constexpr (MatrixType::RowsAtCompileTime == 1 && - MatrixType::ColsAtCompileTime == 1) { - result = this->getUnitaryMatrix1x1(out); - } else if constexpr (MatrixType::RowsAtCompileTime == 2 && - MatrixType::ColsAtCompileTime == 2) { - result = this->getUnitaryMatrix2x2(out); - } else if constexpr (MatrixType::RowsAtCompileTime == 4 && - MatrixType::ColsAtCompileTime == 4) { - result = this->getUnitaryMatrix4x4(out); - } else if constexpr (MatrixType::SizeAtCompileTime == Eigen::Dynamic) { - result = this->getUnitaryMatrixDynamic(out); - } else { - // Fallback: Try obtaining dynamic matrix and see if size matches - Eigen::MatrixXcd dynamicOut; - if (this->getUnitaryMatrixDynamic(dynamicOut)) { - if (dynamicOut.rows() == MatrixType::RowsAtCompileTime && - dynamicOut.cols() == MatrixType::ColsAtCompileTime) { - out = dynamicOut; - result = true; - } - } - } - - if (result) { - return out; - } - return std::nullopt; - } - }]; + "StringRef", "getBaseSymbol", (ins)>]; } #endif // MLIR_DIALECT_QCO_IR_QCOINTERFACES_TD diff --git a/mlir/include/mlir/Dialect/QCO/IR/QCOOps.h b/mlir/include/mlir/Dialect/QCO/IR/QCOOps.h index 99d5bb6fc0..3a80a7e483 100644 --- a/mlir/include/mlir/Dialect/QCO/IR/QCOOps.h +++ b/mlir/include/mlir/Dialect/QCO/IR/QCOOps.h @@ -23,6 +23,7 @@ #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" +#include "mlir/Dialect/QCO/IR/QCOUnitaryMatrixInterfaces.h" #include #include diff --git a/mlir/include/mlir/Dialect/QCO/IR/QCOOps.td b/mlir/include/mlir/Dialect/QCO/IR/QCOOps.td index a5bbfb7f51..d632c33216 100644 --- a/mlir/include/mlir/Dialect/QCO/IR/QCOOps.td +++ b/mlir/include/mlir/Dialect/QCO/IR/QCOOps.td @@ -11,6 +11,7 @@ include "mlir/Dialect/QCO/IR/QCODialect.td" include "mlir/Dialect/QCO/IR/QCOInterfaces.td" +include "mlir/Dialect/QCO/IR/QCOUnitaryMatrixInterfaces.td" include "mlir/Dialect/QCO/IR/QCOTypes.td" include "mlir/IR/EnumAttr.td" @@ -183,8 +184,9 @@ def TwoTargetTwoParameter : TargetAndParameterArityTrait<2, 2>; //===----------------------------------------------------------------------===// def GPhaseOp - : QCOOp<"gphase", traits = [UnitaryOpInterface, ZeroTargetOneParameter, - MemoryEffects<[MemWrite]>]> { + : QCOOp<"gphase", + traits = [UnitaryOpInterface, UnitaryMatrixOpInterface, + ZeroTargetOneParameter, MemoryEffects<[MemWrite]>]> { let summary = "Apply a global phase to the state"; let description = [{ Applies a global phase to the state. @@ -208,7 +210,8 @@ def GPhaseOp let hasCanonicalizer = 1; } -def IdOp : QCOOp<"id", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { +def IdOp : QCOOp<"id", traits = [UnitaryOpInterface, UnitaryMatrixOpInterface, + OneTargetZeroParameter]> { let summary = "Apply an Id gate to a qubit"; let description = [{ Applies an Id gate to a qubit and returns the transformed qubit. @@ -232,7 +235,8 @@ def IdOp : QCOOp<"id", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { let hasCanonicalizer = 1; } -def XOp : QCOOp<"x", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { +def XOp : QCOOp<"x", traits = [UnitaryOpInterface, UnitaryMatrixOpInterface, + OneTargetZeroParameter]> { let summary = "Apply an X gate to a qubit"; let description = [{ Applies an X gate to a qubit and returns the transformed qubit. @@ -256,7 +260,8 @@ def XOp : QCOOp<"x", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { let hasCanonicalizer = 1; } -def YOp : QCOOp<"y", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { +def YOp : QCOOp<"y", traits = [UnitaryOpInterface, UnitaryMatrixOpInterface, + OneTargetZeroParameter]> { let summary = "Apply a Y gate to a qubit"; let description = [{ Applies a Y gate to a qubit and returns the transformed qubit. @@ -280,7 +285,8 @@ def YOp : QCOOp<"y", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { let hasCanonicalizer = 1; } -def ZOp : QCOOp<"z", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { +def ZOp : QCOOp<"z", traits = [UnitaryOpInterface, UnitaryMatrixOpInterface, + OneTargetZeroParameter]> { let summary = "Apply a Z gate to a qubit"; let description = [{ Applies a Z gate to a qubit and returns the transformed qubit. @@ -304,7 +310,8 @@ def ZOp : QCOOp<"z", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { let hasCanonicalizer = 1; } -def HOp : QCOOp<"h", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { +def HOp : QCOOp<"h", traits = [UnitaryOpInterface, UnitaryMatrixOpInterface, + OneTargetZeroParameter]> { let summary = "Apply a H gate to a qubit"; let description = [{ Applies a H gate to a qubit and returns the transformed qubit. @@ -328,7 +335,8 @@ def HOp : QCOOp<"h", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { let hasCanonicalizer = 1; } -def SOp : QCOOp<"s", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { +def SOp : QCOOp<"s", traits = [UnitaryOpInterface, UnitaryMatrixOpInterface, + OneTargetZeroParameter]> { let summary = "Apply an S gate to a qubit"; let description = [{ Applies an S gate to a qubit and returns the transformed qubit. @@ -352,8 +360,8 @@ def SOp : QCOOp<"s", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { let hasCanonicalizer = 1; } -def SdgOp - : QCOOp<"sdg", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { +def SdgOp : QCOOp<"sdg", traits = [UnitaryOpInterface, UnitaryMatrixOpInterface, + OneTargetZeroParameter]> { let summary = "Apply an Sdg gate to a qubit"; let description = [{ Applies an Sdg gate to a qubit and returns the transformed qubit. @@ -377,7 +385,8 @@ def SdgOp let hasCanonicalizer = 1; } -def TOp : QCOOp<"t", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { +def TOp : QCOOp<"t", traits = [UnitaryOpInterface, UnitaryMatrixOpInterface, + OneTargetZeroParameter]> { let summary = "Apply a T gate to a qubit"; let description = [{ Applies a T gate to a qubit and returns the transformed qubit. @@ -401,8 +410,8 @@ def TOp : QCOOp<"t", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { let hasCanonicalizer = 1; } -def TdgOp - : QCOOp<"tdg", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { +def TdgOp : QCOOp<"tdg", traits = [UnitaryOpInterface, UnitaryMatrixOpInterface, + OneTargetZeroParameter]> { let summary = "Apply a Tdg gate to a qubit"; let description = [{ Applies a Tdg gate to a qubit and returns the transformed qubit. @@ -426,7 +435,8 @@ def TdgOp let hasCanonicalizer = 1; } -def SXOp : QCOOp<"sx", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { +def SXOp : QCOOp<"sx", traits = [UnitaryOpInterface, UnitaryMatrixOpInterface, + OneTargetZeroParameter]> { let summary = "Apply an SX gate to a qubit"; let description = [{ Applies an SX gate to a qubit and returns the transformed qubit. @@ -451,7 +461,8 @@ def SXOp : QCOOp<"sx", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { } def SXdgOp - : QCOOp<"sxdg", traits = [UnitaryOpInterface, OneTargetZeroParameter]> { + : QCOOp<"sxdg", traits = [UnitaryOpInterface, UnitaryMatrixOpInterface, + OneTargetZeroParameter]> { let summary = "Apply an SXdg gate to a qubit"; let description = [{ Applies an SXdg gate to a qubit and returns the transformed qubit. @@ -475,7 +486,8 @@ def SXdgOp let hasCanonicalizer = 1; } -def RXOp : QCOOp<"rx", traits = [UnitaryOpInterface, OneTargetOneParameter]> { +def RXOp : QCOOp<"rx", traits = [UnitaryOpInterface, UnitaryMatrixOpInterface, + OneTargetOneParameter]> { let summary = "Apply an RX gate to a qubit"; let description = [{ Applies an RX gate to a qubit and returns the transformed qubit. @@ -504,7 +516,8 @@ def RXOp : QCOOp<"rx", traits = [UnitaryOpInterface, OneTargetOneParameter]> { let hasCanonicalizer = 1; } -def RYOp : QCOOp<"ry", traits = [UnitaryOpInterface, OneTargetOneParameter]> { +def RYOp : QCOOp<"ry", traits = [UnitaryOpInterface, UnitaryMatrixOpInterface, + OneTargetOneParameter]> { let summary = "Apply an RY gate to a qubit"; let description = [{ Applies an RY gate to a qubit and returns the transformed qubit. @@ -533,7 +546,8 @@ def RYOp : QCOOp<"ry", traits = [UnitaryOpInterface, OneTargetOneParameter]> { let hasCanonicalizer = 1; } -def RZOp : QCOOp<"rz", traits = [UnitaryOpInterface, OneTargetOneParameter]> { +def RZOp : QCOOp<"rz", traits = [UnitaryOpInterface, UnitaryMatrixOpInterface, + OneTargetOneParameter]> { let summary = "Apply an RZ gate to a qubit"; let description = [{ Applies an RZ gate to a qubit and returns the transformed qubit. @@ -562,7 +576,8 @@ def RZOp : QCOOp<"rz", traits = [UnitaryOpInterface, OneTargetOneParameter]> { let hasCanonicalizer = 1; } -def POp : QCOOp<"p", traits = [UnitaryOpInterface, OneTargetOneParameter]> { +def POp : QCOOp<"p", traits = [UnitaryOpInterface, UnitaryMatrixOpInterface, + OneTargetOneParameter]> { let summary = "Apply a P gate to a qubit"; let description = [{ Applies a P gate to a qubit and returns the transformed qubit. @@ -591,7 +606,8 @@ def POp : QCOOp<"p", traits = [UnitaryOpInterface, OneTargetOneParameter]> { let hasCanonicalizer = 1; } -def ROp : QCOOp<"r", traits = [UnitaryOpInterface, OneTargetTwoParameter]> { +def ROp : QCOOp<"r", traits = [UnitaryOpInterface, UnitaryMatrixOpInterface, + OneTargetTwoParameter]> { let summary = "Apply an R gate to a qubit"; let description = [{ Applies an R gate to a qubit and returns the transformed qubit. @@ -621,7 +637,8 @@ def ROp : QCOOp<"r", traits = [UnitaryOpInterface, OneTargetTwoParameter]> { let hasCanonicalizer = 1; } -def U2Op : QCOOp<"u2", traits = [UnitaryOpInterface, OneTargetTwoParameter]> { +def U2Op : QCOOp<"u2", traits = [UnitaryOpInterface, UnitaryMatrixOpInterface, + OneTargetTwoParameter]> { let summary = "Apply a U2 gate to a qubit"; let description = [{ Applies a U2 gate to a qubit and returns the transformed qubit. @@ -651,7 +668,8 @@ def U2Op : QCOOp<"u2", traits = [UnitaryOpInterface, OneTargetTwoParameter]> { let hasCanonicalizer = 1; } -def UOp : QCOOp<"u", traits = [UnitaryOpInterface, OneTargetThreeParameter]> { +def UOp : QCOOp<"u", traits = [UnitaryOpInterface, UnitaryMatrixOpInterface, + OneTargetThreeParameter]> { let summary = "Apply a U gate to a qubit"; let description = [{ Applies a U gate to a qubit and returns the transformed qubit. @@ -684,7 +702,8 @@ def UOp : QCOOp<"u", traits = [UnitaryOpInterface, OneTargetThreeParameter]> { } def SWAPOp - : QCOOp<"swap", traits = [UnitaryOpInterface, TwoTargetZeroParameter]> { + : QCOOp<"swap", traits = [UnitaryOpInterface, UnitaryMatrixOpInterface, + TwoTargetZeroParameter]> { let summary = "Apply a SWAP gate to two qubits"; let description = [{ Applies a SWAP gate to two qubits and returns the transformed qubits. @@ -712,7 +731,8 @@ def SWAPOp } def iSWAPOp - : QCOOp<"iswap", traits = [UnitaryOpInterface, TwoTargetZeroParameter]> { + : QCOOp<"iswap", traits = [UnitaryOpInterface, UnitaryMatrixOpInterface, + TwoTargetZeroParameter]> { let summary = "Apply a iSWAP gate to two qubits"; let description = [{ Applies a iSWAP gate to two qubits and returns the transformed qubits. @@ -737,8 +757,8 @@ def iSWAPOp }]; } -def DCXOp - : QCOOp<"dcx", traits = [UnitaryOpInterface, TwoTargetZeroParameter]> { +def DCXOp : QCOOp<"dcx", traits = [UnitaryOpInterface, UnitaryMatrixOpInterface, + TwoTargetZeroParameter]> { let summary = "Apply a DCX gate to two qubits"; let description = [{ Applies a DCX gate to two qubits and returns the transformed qubits. @@ -765,8 +785,8 @@ def DCXOp let hasCanonicalizer = 1; } -def ECROp - : QCOOp<"ecr", traits = [UnitaryOpInterface, TwoTargetZeroParameter]> { +def ECROp : QCOOp<"ecr", traits = [UnitaryOpInterface, UnitaryMatrixOpInterface, + TwoTargetZeroParameter]> { let summary = "Apply an ECR gate to two qubits"; let description = [{ Applies an ECR gate to two qubits and returns the transformed qubits. @@ -793,7 +813,8 @@ def ECROp let hasCanonicalizer = 1; } -def RXXOp : QCOOp<"rxx", traits = [UnitaryOpInterface, TwoTargetOneParameter]> { +def RXXOp : QCOOp<"rxx", traits = [UnitaryOpInterface, UnitaryMatrixOpInterface, + TwoTargetOneParameter]> { let summary = "Apply an RXX gate to two qubits"; let description = [{ Applies an RXX gate to two qubits and returns the transformed qubits. @@ -825,7 +846,8 @@ def RXXOp : QCOOp<"rxx", traits = [UnitaryOpInterface, TwoTargetOneParameter]> { let hasCanonicalizer = 1; } -def RYYOp : QCOOp<"ryy", traits = [UnitaryOpInterface, TwoTargetOneParameter]> { +def RYYOp : QCOOp<"ryy", traits = [UnitaryOpInterface, UnitaryMatrixOpInterface, + TwoTargetOneParameter]> { let summary = "Apply an RYY gate to two qubits"; let description = [{ Applies an RYY gate to two qubits and returns the transformed qubits. @@ -857,7 +879,8 @@ def RYYOp : QCOOp<"ryy", traits = [UnitaryOpInterface, TwoTargetOneParameter]> { let hasCanonicalizer = 1; } -def RZXOp : QCOOp<"rzx", traits = [UnitaryOpInterface, TwoTargetOneParameter]> { +def RZXOp : QCOOp<"rzx", traits = [UnitaryOpInterface, UnitaryMatrixOpInterface, + TwoTargetOneParameter]> { let summary = "Apply an RZX gate to two qubits"; let description = [{ Applies an RZX gate to two qubits and returns the transformed qubits. @@ -889,7 +912,8 @@ def RZXOp : QCOOp<"rzx", traits = [UnitaryOpInterface, TwoTargetOneParameter]> { let hasCanonicalizer = 1; } -def RZZOp : QCOOp<"rzz", traits = [UnitaryOpInterface, TwoTargetOneParameter]> { +def RZZOp : QCOOp<"rzz", traits = [UnitaryOpInterface, UnitaryMatrixOpInterface, + TwoTargetOneParameter]> { let summary = "Apply an RZZ gate to two qubits"; let description = [{ Applies an RZZ gate to two qubits and returns the transformed qubits. @@ -922,7 +946,8 @@ def RZZOp : QCOOp<"rzz", traits = [UnitaryOpInterface, TwoTargetOneParameter]> { } def XXPlusYYOp : QCOOp<"xx_plus_yy", - traits = [UnitaryOpInterface, TwoTargetTwoParameter]> { + traits = [UnitaryOpInterface, UnitaryMatrixOpInterface, + TwoTargetTwoParameter]> { let summary = "Apply an XX+YY gate to two qubits"; let description = [{ Applies an XX+YY gate to two qubits and returns the transformed qubits. @@ -957,7 +982,8 @@ def XXPlusYYOp : QCOOp<"xx_plus_yy", } def XXMinusYYOp : QCOOp<"xx_minus_yy", - traits = [UnitaryOpInterface, TwoTargetTwoParameter]> { + traits = [UnitaryOpInterface, UnitaryMatrixOpInterface, + TwoTargetTwoParameter]> { let summary = "Apply an XX-YY gate to two qubits"; let description = [{ Applies an XX-YY gate to two qubits and returns the transformed qubits. @@ -1062,9 +1088,9 @@ def YieldOp : QCOOp<"yield", traits = [Terminator, ReturnLike]> { def CtrlOp : QCOOp<"ctrl", - traits = [UnitaryOpInterface, AttrSizedOperandSegments, - AttrSizedResultSegments, SameOperandsAndResultType, - SameOperandsAndResultShape, + traits = [UnitaryOpInterface, UnitaryMatrixOpInterface, + AttrSizedOperandSegments, AttrSizedResultSegments, + SameOperandsAndResultType, SameOperandsAndResultShape, SingleBlockImplicitTerminator<"::mlir::qco::YieldOp">, RecursiveMemoryEffects]> { let summary = "Add control qubits to a unitary operation"; @@ -1141,7 +1167,7 @@ def CtrlOp def InvOp : QCOOp<"inv", - traits = [UnitaryOpInterface, + traits = [UnitaryOpInterface, UnitaryMatrixOpInterface, SingleBlockImplicitTerminator<"::mlir::qco::YieldOp">, RecursiveMemoryEffects]> { let summary = "Invert a unitary operation"; diff --git a/mlir/include/mlir/Dialect/QCO/IR/QCOUnitaryMatrixInterfaces.h b/mlir/include/mlir/Dialect/QCO/IR/QCOUnitaryMatrixInterfaces.h new file mode 100644 index 0000000000..c926d171ed --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/IR/QCOUnitaryMatrixInterfaces.h @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +// clang-format:off +#include "mlir/Dialect/QCO/IR/QCOUnitaryMatrixInterfaces.h.inc" // IWYU pragma: export +// clang-format:on diff --git a/mlir/include/mlir/Dialect/QCO/IR/QCOUnitaryMatrixInterfaces.td b/mlir/include/mlir/Dialect/QCO/IR/QCOUnitaryMatrixInterfaces.td new file mode 100644 index 0000000000..eff36c0338 --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/IR/QCOUnitaryMatrixInterfaces.td @@ -0,0 +1,139 @@ +// Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +// Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +// All rights reserved. +// +// SPDX-License-Identifier: MIT +// +// Licensed under the MIT License + +#ifndef MLIR_DIALECT_QCO_IR_QCOUNIMARYMATRIXINTERFACES_TD +#define MLIR_DIALECT_QCO_IR_QCOUNIMARYMATRIXINTERFACES_TD + +include "mlir/IR/OpBase.td" + +//===----------------------------------------------------------------------===// +// UnitaryMatrixOpInterface +//===----------------------------------------------------------------------===// + +def UnitaryMatrixOpInterface : OpInterface<"UnitaryMatrixOpInterface"> { + let description = [{ + This interface provides a unified API for all operations in the QCO + dialect that can expose their unitary matrix representation. + + This interface is intentionally separate from `UnitaryOpInterface` to + avoid propagating the Eigen dependency to all unitary ops. + }]; + + let cppNamespace = "::mlir::qco"; + + // Generic implementation body for getUnitaryMatrix methods + defvar unitaryMatrixMethodBody = [{ + auto process = [&](MatrixType&& m) -> bool { + using TargetT = std::remove_cvref_t; + using SourceT = std::remove_cvref_t; + + constexpr bool isTargetDynamic = + (TargetT::SizeAtCompileTime == Eigen::Dynamic); + constexpr bool isSourceDynamic = + (SourceT::SizeAtCompileTime == Eigen::Dynamic); + + // Case 1: Target is Dynamic. Always accepts source. + if constexpr (isTargetDynamic) { + out = std::forward(m); + return true; + } + // Case 2: Target is Fixed. + else { + // Case 2a: Source is Dynamic. Runtime dimension check required. + if constexpr (isSourceDynamic) { + if (m.rows() == static_cast(TargetT::RowsAtCompileTime) && + m.cols() == static_cast(TargetT::ColsAtCompileTime)) + [[likely]] { + out = std::forward(m); + return true; + } + } + // Case 2b: Source is Fixed. Compile-time check. + else if constexpr (static_cast( + SourceT::RowsAtCompileTime) == + static_cast( + TargetT::RowsAtCompileTime) && + static_cast( + SourceT::ColsAtCompileTime) == + static_cast( + TargetT::ColsAtCompileTime)) { + out = std::forward(m); + return true; + } + } + return false; + }; + + if constexpr (requires { $_op.getUnitaryMatrix().has_value(); }) { + if (auto&& matrix = $_op.getUnitaryMatrix()) { + return process(std::move(*matrix)); + } + return false; + } else if constexpr (requires { $_op.getUnitaryMatrix(); }) { + return process($_op.getUnitaryMatrix()); + } else { + llvm::reportFatalUsageError("Operation '" + $_op.getBaseSymbol() + "' has no unitary matrix definition!"); + } + }]; + + let methods = + [InterfaceMethod<"Populates the given 1x1 unitary matrix if possible.", + "bool", "getUnitaryMatrix1x1", + (ins "Eigen::Matrix, 1, 1>&":$out), + unitaryMatrixMethodBody>, + InterfaceMethod<"Populates the given 2x2 unitary matrix if possible.", + "bool", "getUnitaryMatrix2x2", + (ins "Eigen::Matrix2cd&":$out), unitaryMatrixMethodBody>, + InterfaceMethod<"Populates the given 4x4 unitary matrix if possible.", + "bool", "getUnitaryMatrix4x4", + (ins "Eigen::Matrix4cd&":$out), unitaryMatrixMethodBody>, + InterfaceMethod<"Populates the given dynamic unitary matrix.", "bool", + "getUnitaryMatrixDynamic", + (ins "Eigen::MatrixXcd&":$out), + unitaryMatrixMethodBody>]; + + let extraClassDeclaration = [{ + template + std::optional getUnitaryMatrix() { + MatrixType out; + bool result = false; + + // Dispatch to the appropriate fixed-size or dynamic method based on the + // matrix type. + if constexpr (MatrixType::RowsAtCompileTime == 1 && + MatrixType::ColsAtCompileTime == 1) { + result = this->getUnitaryMatrix1x1(out); + } else if constexpr (MatrixType::RowsAtCompileTime == 2 && + MatrixType::ColsAtCompileTime == 2) { + result = this->getUnitaryMatrix2x2(out); + } else if constexpr (MatrixType::RowsAtCompileTime == 4 && + MatrixType::ColsAtCompileTime == 4) { + result = this->getUnitaryMatrix4x4(out); + } else if constexpr (MatrixType::SizeAtCompileTime == Eigen::Dynamic) { + result = this->getUnitaryMatrixDynamic(out); + } else { + // Fallback: Try obtaining dynamic matrix and see if size matches + Eigen::MatrixXcd dynamicOut; + if (this->getUnitaryMatrixDynamic(dynamicOut)) { + if (dynamicOut.rows() == MatrixType::RowsAtCompileTime && + dynamicOut.cols() == MatrixType::ColsAtCompileTime) { + out = dynamicOut; + result = true; + } + } + } + + if (result) { + return out; + } + return std::nullopt; + } + }]; +} + +#endif // MLIR_DIALECT_QCO_IR_QCOUNIMARYMATRIXINTERFACES_TD diff --git a/mlir/lib/Dialect/QCO/IR/CMakeLists.txt b/mlir/lib/Dialect/QCO/IR/CMakeLists.txt index f33ece7000..6f7b298d10 100644 --- a/mlir/lib/Dialect/QCO/IR/CMakeLists.txt +++ b/mlir/lib/Dialect/QCO/IR/CMakeLists.txt @@ -14,6 +14,7 @@ file(GLOB_RECURSE SCF "${CMAKE_CURRENT_SOURCE_DIR}/SCF/*.cpp") add_mlir_dialect_library( MLIRQCODialect QCOOps.cpp + QCOUnitaryMatrixInterfaces.cpp ${MODIFIERS} ${OPERATIONS} ${QUBIT_MANAGEMENT} @@ -23,6 +24,7 @@ add_mlir_dialect_library( DEPENDS MLIRQCOOpsIncGen MLIRQCOInterfacesIncGen + MLIRQCOUnitaryMatrixInterfacesIncGen LINK_LIBS PRIVATE MLIRIR diff --git a/mlir/lib/Dialect/QCO/IR/QCOUnitaryMatrixInterfaces.cpp b/mlir/lib/Dialect/QCO/IR/QCOUnitaryMatrixInterfaces.cpp new file mode 100644 index 0000000000..d07d931e7c --- /dev/null +++ b/mlir/lib/Dialect/QCO/IR/QCOUnitaryMatrixInterfaces.cpp @@ -0,0 +1,13 @@ +/* + * 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/QCOUnitaryMatrixInterfaces.h" + +#include "mlir/Dialect/QCO/IR/QCOUnitaryMatrixInterfaces.cpp.inc" From 6cb4af265b1c55e87c9cda5863555f7bbc62c1db Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 19 May 2026 16:37:07 +0200 Subject: [PATCH 08/68] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Use=20UnitaryMatrixO?= =?UTF-8?q?pInterface=20in=20CtrlOp=20and=20InvOp=20matrix=20expansion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp | 5 ++++- mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp b/mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp index 25fc88d084..7bbbeabeba 100644 --- a/mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp @@ -10,6 +10,7 @@ #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/IR/QCOUnitaryMatrixInterfaces.h" #include #include @@ -345,7 +346,9 @@ std::optional CtrlOp::getUnitaryMatrix() { if (!bodyUnitary) { return std::nullopt; } - auto&& targetMatrix = bodyUnitary.getUnitaryMatrix(); + auto&& targetMatrix = + cast(bodyUnitary.getOperation()) + .getUnitaryMatrix(); if (!targetMatrix) { return std::nullopt; } diff --git a/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp b/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp index d82a64f819..66f12e72a0 100644 --- a/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp @@ -10,6 +10,7 @@ #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/IR/QCOUnitaryMatrixInterfaces.h" #include #include @@ -406,7 +407,9 @@ std::optional InvOp::getUnitaryMatrix() { if (!bodyUnitary) { return std::nullopt; } - auto&& targetMatrix = bodyUnitary.getUnitaryMatrix(); + auto&& targetMatrix = + cast(bodyUnitary.getOperation()) + .getUnitaryMatrix(); if (!targetMatrix) { return std::nullopt; } From abd87fcfea3ca94d8dab761d63e159cff951e8fc Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 19 May 2026 16:37:25 +0200 Subject: [PATCH 09/68] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Consolidate=20QCO=20?= =?UTF-8?q?Euler=20decomposition=20into=20Euler.h/cpp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the old GateSequence/Helpers/UnitaryMatrices split with a single Euler API (angle extraction + IR synthesis) and unify MLIR numeric tolerance. --- .../QCO/Transforms/Decomposition/Euler.h | 184 ++++++++ .../QCO/Transforms/Decomposition/EulerBasis.h | 56 --- .../Decomposition/EulerDecomposition.h | 147 ------- .../QCO/Transforms/Decomposition/Gate.h | 42 -- .../QCO/Transforms/Decomposition/GateKind.h | 44 -- .../Transforms/Decomposition/GateSequence.h | 61 --- .../QCO/Transforms/Decomposition/Helpers.h | 61 --- .../Decomposition/UnitaryMatrices.h | 54 --- mlir/include/mlir/Dialect/Utils/Utils.h | 4 +- .../QCO/Transforms/Decomposition/Euler.cpp | 402 ++++++++++++++++++ .../Transforms/Decomposition/EulerBasis.cpp | 52 --- .../Decomposition/EulerDecomposition.cpp | 318 -------------- .../Transforms/Decomposition/GateSequence.cpp | 55 --- .../QCO/Transforms/Decomposition/Helpers.cpp | 65 --- .../Decomposition/UnitaryMatrices.cpp | 229 ---------- 15 files changed, 589 insertions(+), 1185 deletions(-) create mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h 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/Gate.h delete mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h delete mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h delete mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h delete mode 100644 mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h create mode 100644 mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp 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/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp delete mode 100644 mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h new file mode 100644 index 0000000000..aed91b529b --- /dev/null +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace mlir::qco::decomposition { +/** + * Target gate set for single-qubit Euler synthesis. + * + * Selects which QCO operations `synthesizeUnitary1QEuler` emits when lowering a + * 2×2 unitary. Angle extraction (`EulerDecomposition::anglesFromUnitary`) and + * IR emission are separate; several enumerators share the same + * `(theta, phi, lambda, phase)` computation and differ only in the emitted + * sequence. + * + * - **KAK bases** (`ZYZ`, `ZXZ`, `XZX`, `XYX`): Euler decompositions named by + * the middle and outer rotation axes. + * - **`U`**: single `u(theta, phi, lambda)` gate (angles from the Z-Y-Z form). + * - **`ZSX` / `ZSXX`**: `rz`/`sx` templates; `ZSXX` may emit `x` instead of + * `sx · rz · sx` when simplification is on. + */ +enum class EulerBasis : std::uint8_t { + ZYZ = 0, ///< `rz · ry · rz`. + ZXZ = 1, ///< `rz · rx · rz`. + XZX = 2, ///< `rx · rz · rx`. + XYX = 3, ///< `rx · ry · rx`. + U = 4, ///< `u(theta, phi, lambda)`. + ZSXX = 5, ///< `rz · sx · rz · sx · rz`. + ZSX = 6, ///< `rz · sx · rz · sx · rz`. +}; + +} // namespace mlir::qco::decomposition + +// NOTE: We keep the small numeric helpers in this header because Euler +// decomposition/synthesis is the only current user and we want to avoid +// scattering tiny utilities across multiple files. +namespace mlir::qco::helpers { + +/// Check whether `matrix` is unitary within `tolerance` (i.e. `M^H M` is +/// approximately `I`, using Eigen's `isIdentity`). +template +[[nodiscard]] inline bool +isUnitaryMatrix(const Eigen::Matrix& matrix, + double tolerance = mlir::utils::TOLERANCE) { + if (matrix.rows() != matrix.cols()) { + return false; + } + return (matrix.adjoint() * matrix).isIdentity(tolerance); +} + +/** + * Wrap angle into interval [-pi, pi). If within atol of the endpoint, clamp to + * -pi. + */ +[[nodiscard]] inline double mod2pi(double angle, + double atol = mlir::utils::TOLERANCE) { + // Wrap angle into the half-open interval [-pi, pi). + // For non-finite values, keep the original (caller error / upstream issue). + if (!std::isfinite(angle)) { + return angle; + } + + constexpr double pi = std::numbers::pi; + constexpr double twoPi = 2.0 * std::numbers::pi; + + // Euclidean remainder of (angle + pi) modulo 2pi, then shift back by pi. + // This ensures correct wrapping for negative angles as well. + double r = std::fmod(angle + pi, twoPi); + if (r < 0.0) { + r += twoPi; + } + double wrapped = r - pi; + + // Canonicalize the upper endpoint back to -pi so callers always receive a + // half-open interval [-pi, pi). We use an epsilon guard since rounding can + // produce wrapped ~= +pi. + if (wrapped >= pi - atol) { + wrapped = -pi; + } + + return wrapped; +} + +} // namespace mlir::qco::helpers + +namespace mlir::qco::decomposition { + +/** + * Extract Euler parameters from single-qubit unitary matrices. + * + * Angle extraction is separate from IR emission (`synthesizeUnitary1QEuler`), + * which tracks global phase via `qco.gphase` so the synthesized circuit matches + * the target matrix exactly (not only up to global phase). + */ +class EulerDecomposition { +public: + /** + * 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 Euler angles for `ZSX` / `ZSXX` (`rz` / `sx`) synthesis. + * + * Reuses `(theta, phi, lambda)` from `paramsZYZ` and sets the scalar phase to + * `phase - 0.5 * (theta + phi + lambda)` so `emitPSXGen` reproduces `matrix` + * exactly, including the global phase induced by the `rz`/`sx` + * parameterization. + * + * @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 + paramsPSX(const Eigen::Matrix2cd& matrix); +}; + +/// Parse a user-facing basis string (e.g. "zyz", "zsxx") into an Euler basis. +[[nodiscard]] std::optional parseEulerBasis(StringRef basis); + +/// Emit an Euler-basis gate sequence implementing `targetMatrix` on `qubit`. +/// Returns the output qubit value. +/// +/// The emitted circuit includes a `qco.gphase` correction when needed so the +/// overall unitary matches `targetMatrix` exactly (not only up to global +/// phase). +/// +/// When `simplify` is false, near-zero rotations and basis-specific shortcuts +/// are not applied. +[[nodiscard]] Value +synthesizeUnitary1QEuler(OpBuilder& builder, Location loc, Value qubit, + const Eigen::Matrix2cd& targetMatrix, EulerBasis basis, + bool simplify = true); + +} // 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 deleted file mode 100644 index def869612f..0000000000 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/EulerBasis.h +++ /dev/null @@ -1,56 +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 - -#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(+/- pi) · 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]] 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 deleted file mode 100644 index 8b9eb6a4f2..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(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 deleted file mode 100644 index efada70fcd..0000000000 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Gate.h +++ /dev/null @@ -1,42 +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 - -#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. - SmallVector parameter; - - /// Logical qubit ids used by the gate, in operand order. - 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/GateSequence.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h deleted file mode 100644 index ae68ca40bc..0000000000 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/GateSequence.h +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#pragma once - -#include "Gate.h" - -#include -#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 { - /// Gates in execution order (see struct comment). - 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; - -} // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h deleted file mode 100644 index 1d2f1fae02..0000000000 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#pragma once - -#include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.h" - -#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 { - -/// 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) { - if (matrix.rows() != matrix.cols()) { - return false; - } - return (matrix.adjoint() * 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); - -/** - * 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 deleted file mode 100644 index 7b8e91c34f..0000000000 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h +++ /dev/null @@ -1,54 +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 - -/// 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 theta, double phi, double lambda); -/// `U2(phi, lambda) == U(pi/2, phi, lambda)`. -[[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); -[[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::Matrix2cd H_GATE{{FRAC1_SQRT2, FRAC1_SQRT2}, - {FRAC1_SQRT2, -FRAC1_SQRT2}}; - -/// Kronecker-embed a 2x2 on wire ``qubitId`` (identity on the other wire). -[[nodiscard]] Eigen::Matrix4cd -expandToTwoQubits(const Eigen::Matrix2cd& singleQubitMatrix, QubitId qubitId); - -/// 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/Utils/Utils.h b/mlir/include/mlir/Dialect/Utils/Utils.h index 3d976a5a63..8adb0643a0 100644 --- a/mlir/include/mlir/Dialect/Utils/Utils.h +++ b/mlir/include/mlir/Dialect/Utils/Utils.h @@ -19,7 +19,9 @@ namespace mlir::utils { -constexpr auto TOLERANCE = 1e-15; +/// Default absolute tolerance for MLIR dialect numerics (matrix checks, +/// angles). +constexpr auto TOLERANCE = 1e-14; inline Value constantFromScalar(OpBuilder& builder, Location loc, double v) { return arith::ConstantOp::create(builder, loc, builder.getF64FloatAttr(v)); diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp new file mode 100644 index 0000000000..94a32a6ac4 --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -0,0 +1,402 @@ +/* + * 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/IR/QCOOps.h" +#include "mlir/Dialect/QCO/IR/QCOUnitaryMatrixInterfaces.h" +#include "mlir/Dialect/Utils/Utils.h" + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace mlir::qco::decomposition { + +//===----------------------------------------------------------------------===// +// Euler decomposition (angles) +//===----------------------------------------------------------------------===// + +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: + // The `u` gate parameterization is derived from the standard Z-Y-Z form. + return paramsZYZ(matrix); + case EulerBasis::ZSX: + case EulerBasis::ZSXX: + return paramsPSX(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 lambda = ang1 - ang2; + return {theta, phi, lambda, 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, lambda, phase] = paramsZYZ(matrix); + return {theta, phi + (std::numbers::pi / 2.0), + lambda - (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, lambda, phase] = paramsZYZ(matZYZ); + auto newPhi = helpers::mod2pi(phi + std::numbers::pi, 0.); + auto newLambda = helpers::mod2pi(lambda + std::numbers::pi, 0.); + return { + theta, + newPhi, + newLambda, + phase + ((newPhi + newLambda - phi - lambda) / 2.), + }; +} + +std::array +EulerDecomposition::paramsPSX(const Eigen::Matrix2cd& matrix) { + 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, lambda, phaseZXZ] = paramsZXZ(matZXZ); + return {theta, phi, lambda, phase + phaseZXZ}; +} + +//===----------------------------------------------------------------------===// +// Euler synthesis (IR emission) +//===----------------------------------------------------------------------===// + +namespace { + +Value getOrCreateF64Constant(OpBuilder& builder, Location loc, double value) { + return arith::ConstantOp::create(builder, loc, + builder.getF64FloatAttr(value)); +} + +void emitGPhaseIfNeeded(OpBuilder& builder, Location loc, double phase) { + if (std::abs(phase) <= mlir::utils::TOLERANCE) { + return; + } + auto phaseVal = getOrCreateF64Constant(builder, loc, phase); + builder.create(loc, phaseVal); +} + +double phaseToMatchTarget(const Eigen::Matrix2cd& target, + const Eigen::Matrix2cd& emitted) { + // If `target == s * emitted`, then `target * emitted^H == s * I`. + const auto s = (target * emitted.adjoint())(0, 0); + return std::arg(s); +} + +void accumulateEmittedMatrix(Operation* op, Eigen::Matrix2cd& emitted) { + auto iface = dyn_cast(op); + if (!iface) { + llvm::reportFatalInternalError( + "Expected emitted op to implement UnitaryMatrixOpInterface"); + } + + Eigen::Matrix2cd m; + if (!iface.getUnitaryMatrix2x2(m)) { + llvm::reportFatalInternalError( + "Expected emitted 1q op to have constant unitary matrix"); + } + + // Execution order: first emitted op applied first => multiply on the left. + emitted = m * emitted; +} + +Value emitRZ(OpBuilder& builder, Location loc, Value qubit, double angle, + Eigen::Matrix2cd& emitted) { + auto v = getOrCreateF64Constant(builder, loc, angle); + auto op = builder.create(loc, qubit, v); + accumulateEmittedMatrix(op, emitted); + return op.getQubitOut(); +} + +Value emitRY(OpBuilder& builder, Location loc, Value qubit, double angle, + Eigen::Matrix2cd& emitted) { + auto v = getOrCreateF64Constant(builder, loc, angle); + auto op = builder.create(loc, qubit, v); + accumulateEmittedMatrix(op, emitted); + return op.getQubitOut(); +} + +Value emitRX(OpBuilder& builder, Location loc, Value qubit, double angle, + Eigen::Matrix2cd& emitted) { + auto v = getOrCreateF64Constant(builder, loc, angle); + auto op = builder.create(loc, qubit, v); + accumulateEmittedMatrix(op, emitted); + return op.getQubitOut(); +} + +Value emitSX(OpBuilder& builder, Location loc, Value qubit, + Eigen::Matrix2cd& emitted) { + auto op = builder.create(loc, qubit); + accumulateEmittedMatrix(op, emitted); + return op.getQubitOut(); +} + +Value emitX(OpBuilder& builder, Location loc, Value qubit, + Eigen::Matrix2cd& emitted) { + auto op = builder.create(loc, qubit); + accumulateEmittedMatrix(op, emitted); + return op.getQubitOut(); +} + +Value emitU(OpBuilder& builder, Location loc, Value qubit, double theta, + double phi, double lambda, Eigen::Matrix2cd& emitted) { + auto thetaV = getOrCreateF64Constant(builder, loc, theta); + auto phiV = getOrCreateF64Constant(builder, loc, phi); + auto lambdaV = getOrCreateF64Constant(builder, loc, lambda); + auto op = builder.create(loc, qubit, thetaV, phiV, lambdaV); + accumulateEmittedMatrix(op, emitted); + return op.getQubitOut(); +} + +Value emitKAK(OpBuilder& builder, Location loc, Value qubit, + const Eigen::Matrix2cd& targetMatrix, double theta, double phi, + double lambda, EulerBasis basis, bool simplify) { + const double eps = simplify ? mlir::utils::TOLERANCE : -1.0; + Eigen::Matrix2cd emitted = Eigen::Matrix2cd::Identity(); + + auto emitK = [&](double a) { + const double canonical = helpers::mod2pi(a, eps); + if (std::abs(canonical) > eps) { + switch (basis) { + case EulerBasis::ZYZ: + case EulerBasis::ZXZ: + qubit = emitRZ(builder, loc, qubit, canonical, emitted); + break; + case EulerBasis::XZX: + case EulerBasis::XYX: + qubit = emitRX(builder, loc, qubit, canonical, emitted); + break; + default: + llvm::reportFatalInternalError("Invalid K gate for KAK emission"); + } + } + }; + + auto emitA = [&](double a) { + switch (basis) { + case EulerBasis::ZYZ: + qubit = emitRY(builder, loc, qubit, a, emitted); + break; + case EulerBasis::ZXZ: + qubit = emitRX(builder, loc, qubit, a, emitted); + break; + case EulerBasis::XZX: + qubit = emitRZ(builder, loc, qubit, a, emitted); + break; + case EulerBasis::XYX: + qubit = emitRY(builder, loc, qubit, a, emitted); + break; + default: + llvm::reportFatalInternalError("Invalid A gate for KAK emission"); + } + }; + + if (std::abs(theta) <= eps) { + emitK(lambda + phi); + emitGPhaseIfNeeded(builder, loc, phaseToMatchTarget(targetMatrix, emitted)); + return qubit; + } + + emitK(lambda); + emitA(theta); + emitK(phi); + emitGPhaseIfNeeded(builder, loc, phaseToMatchTarget(targetMatrix, emitted)); + return qubit; +} + +Value emitPSXGen(OpBuilder& builder, Location loc, Value qubit, + const Eigen::Matrix2cd& targetMatrix, double theta, double phi, + double lambda, bool allowXShortcut, bool simplify) { + const double eps = simplify ? mlir::utils::TOLERANCE : -1.0; + Eigen::Matrix2cd emitted = Eigen::Matrix2cd::Identity(); + + auto emitRzAsP = [&](double angle) { + const double canonicalAngle = helpers::mod2pi(angle, eps); + if (std::abs(canonicalAngle) > eps) { + qubit = emitRZ(builder, loc, qubit, canonicalAngle, emitted); + } + }; + + if (std::abs(theta) <= eps) { + emitRzAsP(lambda + phi); + emitGPhaseIfNeeded(builder, loc, phaseToMatchTarget(targetMatrix, emitted)); + return qubit; + } + + if (std::abs(theta - (std::numbers::pi / 2.0)) < eps) { + emitRzAsP(lambda - (std::numbers::pi / 2.0)); + qubit = emitSX(builder, loc, qubit, emitted); + emitRzAsP(phi + (std::numbers::pi / 2.0)); + emitGPhaseIfNeeded(builder, loc, phaseToMatchTarget(targetMatrix, emitted)); + return qubit; + } + + // General double-`sx` case: reparameterize angles, then fix global phase from + // the accumulated unitary. + if (std::abs(theta - std::numbers::pi) < eps) { + phi -= lambda; + lambda = 0.0; + } + if (std::abs(helpers::mod2pi(lambda + std::numbers::pi, eps)) < eps || + std::abs(helpers::mod2pi(phi, eps)) < eps) { + lambda += std::numbers::pi; + theta = -theta; + phi += std::numbers::pi; + } + + theta += std::numbers::pi; + phi += std::numbers::pi; + + emitRzAsP(lambda); + + if (allowXShortcut && std::abs(helpers::mod2pi(theta, eps)) < eps) { + qubit = emitX(builder, loc, qubit, emitted); + } else { + qubit = emitSX(builder, loc, qubit, emitted); + emitRzAsP(theta); + qubit = emitSX(builder, loc, qubit, emitted); + } + + emitRzAsP(phi); + emitGPhaseIfNeeded(builder, loc, phaseToMatchTarget(targetMatrix, emitted)); + return qubit; +} + +} // namespace + +std::optional parseEulerBasis(StringRef basis) { + const auto b = basis.lower(); + if (b == "zyz") { + return EulerBasis::ZYZ; + } + if (b == "zxz") { + return EulerBasis::ZXZ; + } + if (b == "xzx") { + return EulerBasis::XZX; + } + if (b == "xyx") { + return EulerBasis::XYX; + } + if (b == "u") { + return EulerBasis::U; + } + if (b == "zsx") { + return EulerBasis::ZSX; + } + if (b == "zsxx") { + return EulerBasis::ZSXX; + } + return std::nullopt; +} + +Value synthesizeUnitary1QEuler(OpBuilder& builder, Location loc, Value qubit, + const Eigen::Matrix2cd& targetMatrix, + EulerBasis basis, bool simplify) { + const auto [theta, phi, lambda, /*phase=*/phase] = + EulerDecomposition::anglesFromUnitary(targetMatrix, basis); + + switch (basis) { + case EulerBasis::ZYZ: + case EulerBasis::ZXZ: + case EulerBasis::XZX: + case EulerBasis::XYX: + qubit = emitKAK(builder, loc, qubit, targetMatrix, theta, phi, lambda, + basis, simplify); + break; + case EulerBasis::U: { + Eigen::Matrix2cd emitted = Eigen::Matrix2cd::Identity(); + qubit = emitU(builder, loc, qubit, theta, phi, lambda, emitted); + emitGPhaseIfNeeded(builder, loc, phaseToMatchTarget(targetMatrix, emitted)); + break; + } + case EulerBasis::ZSX: + qubit = emitPSXGen(builder, loc, qubit, targetMatrix, theta, phi, lambda, + /*allowXShortcut=*/false, simplify); + break; + case EulerBasis::ZSXX: + qubit = emitPSXGen(builder, loc, qubit, targetMatrix, theta, phi, lambda, + /*allowXShortcut=*/true, simplify); + break; + } + + // `anglesFromUnitary` returns a phase term for exact reconstruction; some + // emission modes compute the phase from matrices directly, but for bases that + // already incorporate it we keep it available for debugging parity. + (void)phase; + return qubit; +} + +} // namespace mlir::qco::decomposition 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 5b6db92610..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]] 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 deleted file mode 100644 index 27ea5bf699..0000000000 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/EulerDecomposition.cpp +++ /dev/null @@ -1,318 +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(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 = {theta, phi, lambda}}}, - .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 `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 deleted file mode 100644 index bec566df84..0000000000 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Helpers.cpp +++ /dev/null @@ -1,65 +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/Transforms/Decomposition/GateKind.h" - -#include - -#include -#include -#include -#include - -namespace mlir::qco::helpers { - -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; -} - -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; -} - -std::size_t getComplexity(decomposition::GateKind type, - std::size_t numOfQubits) { - if (type == decomposition::GateKind::GPhase) { - return 0; - } - if (numOfQubits > 1) { - // Multi-qubit operations dominate the heuristic cost model. - constexpr std::size_t multiQubitFactor = 10; - return (numOfQubits - 1) * multiQubitFactor; - } - 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 deleted file mode 100644 index e8293bbbd7..0000000000 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.cpp +++ /dev/null @@ -1,229 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#include "mlir/Dialect/QCO/Transforms/Decomposition/UnitaryMatrices.h" - -#include "mlir/Dialect/QCO/Transforms/Decomposition/Gate.h" -#include "mlir/Dialect/QCO/Transforms/Decomposition/GateKind.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.)}}}}; -} - -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}}}; -} - -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::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); - 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); - const double phi = gate.parameter[0]; - const double lambda = gate.parameter[1]; - return u2Matrix(phi, lambda); - } - if (gate.type == GateKind::H) { - return H_GATE; - } - 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) { - if (gate.qubitId.empty()) { - if (gate.type == GateKind::I) { - return Eigen::Matrix4cd::Identity(); - } - llvm::reportFatalInternalError( - "Invalid gate: empty qubit IDs are only allowed for identity"); - } - if (gate.qubitId.size() == 1) { - return expandToTwoQubits(getSingleQubitMatrix(gate), gate.qubitId[0]); - } - if (gate.qubitId.size() == 2) { - const bool validPair01 = gate.qubitId == SmallVector{0, 1}; - const bool validPair10 = gate.qubitId == 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 (`cx`) is directional: swapping `{control, target}` - // changes the operator. We therefore handle both orderings explicitly. - // - // The two matrices below represent `cx` for the two possible - // `Gate::qubitId` orderings. Qubit 0 is the MSB of the 4x4 computational - // basis (matching `UnitaryOpInterface::getUnitaryMatrix4x4`), so - // `{0,1}` and `{1,0}` lead to different basis-layout matrices. - 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}}; - } - llvm::reportFatalInternalError("Invalid qubit IDs for CX gate"); - } - if (gate.type == GateKind::Z) { - // Controlled-Z (`cz`) is symmetric in its two qubits; swapping `{0,1}` - // and - // `{1,0}` yields the same operator, so one matrix is sufficient. - 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 From 9c10cb6a69abc965839722d74e7454f7ffee30b6 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 19 May 2026 16:37:47 +0200 Subject: [PATCH 10/68] =?UTF-8?q?=E2=9C=A8=20Add=20fuse-single-qubit-unita?= =?UTF-8?q?ry-runs=20pass=20for=20Euler=20resynthesis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fuse maximal runs of constant 1Q unitaries on a wire into basis-native gates, with configurable Euler basis and exact global-phase correction via gphase. --- .../mlir/Dialect/QCO/Transforms/Passes.td | 23 +++ .../lib/Dialect/QCO/Transforms/CMakeLists.txt | 1 + .../FuseSingleQubitUnitaryRuns.cpp | 187 ++++++++++++++++++ 3 files changed, 211 insertions(+) create mode 100644 mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index 54fccffdc7..27686c7dfb 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -40,6 +40,29 @@ def MergeSingleQubitRotationGates }]; } +def FuseSingleQubitUnitaryRuns + : Pass<"fuse-single-qubit-unitary-runs", "mlir::ModuleOp"> { + let dependentDialects = ["mlir::qco::QCODialect", + "::mlir::arith::ArithDialect", + "::mlir::qtensor::QTensorDialect"]; + let summary = "Fuse single-qubit unitary runs using Euler resynthesis"; + let description = [{ + Collects maximal runs of consecutive single-qubit unitary operations on the + same qubit wire, composes their constant unitary matrices, and replaces each + run with an equivalent sequence of basis gates. + + The emitted basis is controlled via the `basis` option (e.g. `zyz`, `zsxx`). + A `gphase` correction is inserted when needed so the rewritten sequence + matches the composed matrix exactly (not only up to global phase). + + Currently, only operations whose unitary matrix can be obtained at compile + time are fused. + }]; + let options = [Option< + "basis", "basis", "std::string", "\"zyz\"", + "Target Euler basis (zyz, zxz, xzx, xyx, u, zsx, zsxx).">]; +} + //===----------------------------------------------------------------------===// // Transpilation Passes //===----------------------------------------------------------------------===// diff --git a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt index fc6ee74b9d..2582fc9dc4 100644 --- a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt +++ b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt @@ -17,6 +17,7 @@ add_mlir_library( MLIRQCOUtils MLIRArithDialect MLIRMathDialect + Eigen3::Eigen DEPENDS MLIRQCOTransformsIncGen) diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp new file mode 100644 index 0000000000..fa17d41b8f --- /dev/null +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM + * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QCO/IR/QCOInterfaces.h" +#include "mlir/Dialect/QCO/IR/QCOUnitaryMatrixInterfaces.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" +#include "mlir/Dialect/QCO/Transforms/Passes.h" +#include "mlir/Dialect/QCO/Utils/WireIterator.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace mlir::qco { + +#define GEN_PASS_DEF_FUSESINGLEQUBITUNITARYRUNS +#include "mlir/Dialect/QCO/Transforms/Passes.h.inc" + +namespace { + +bool isFuseCandidate(UnitaryOpInterface op) { + if (!op || !op.isSingleQubit()) { + return false; + } + return isa(op.getOperation()); +} + +std::optional getConstMatrix(UnitaryOpInterface op) { + if (!isa(op.getOperation())) { + return std::nullopt; + } + Eigen::Matrix2cd m; + if (!cast(op.getOperation()) + .getUnitaryMatrix2x2(m)) { + return std::nullopt; + } + return m; +} + +/// Compose a run of unitary ops (execution order) into a single matrix. +std::optional composeRun(ArrayRef run) { + Eigen::Matrix2cd composed = Eigen::Matrix2cd::Identity(); + for (auto op : run) { + auto m = getConstMatrix(op); + if (!m) { + return std::nullopt; + } + // Execution order: first op applied first => multiply on the left. + composed = (*m) * composed; + } + return composed; +} + +struct FuseSingleQubitUnitaryRunsPass final + : impl::FuseSingleQubitUnitaryRunsBase { + using Base::Base; + + explicit FuseSingleQubitUnitaryRunsPass( + FuseSingleQubitUnitaryRunsOptions options) + : Base(std::move(options)) {} + + void runOnOperation() override { + auto module = getOperation(); + + const auto parsed = decomposition::parseEulerBasis(this->basis); + if (!parsed) { + module.emitError() + << "Invalid Euler basis '" << this->basis + << "'. Expected one of: zyz, zxz, xzx, xyx, u, zsx, zsxx."; + signalPassFailure(); + return; + } + + SmallVector wireStarts; + module.walk([&](AllocOp op) { wireStarts.push_back(op.getResult()); }); + module.walk([&](StaticOp op) { wireStarts.push_back(op.getQubit()); }); + module.walk( + [&](qtensor::ExtractOp op) { wireStarts.push_back(op.getResult()); }); + module.walk([&](func::FuncOp func) { + if (func.empty()) { + return; + } + for (BlockArgument arg : func.getBody().front().getArguments()) { + if (isa(arg.getType())) { + wireStarts.push_back(arg); + } + } + }); + + // Collect runs first, rewrite afterwards. + SmallVector, 16> runs; + DenseSet seen; + + for (Value start : wireStarts) { + if (!start) { + continue; + } + + SmallVector current; + Block* currentBlock = nullptr; + + WireIterator it(start); + ++it; // Move to the first op on the wire. + for (; it != std::default_sentinel; ++it) { + Operation* op = *it; + + if (currentBlock == nullptr) { + currentBlock = op->getBlock(); + } + if (op->getBlock() != currentBlock) { + break; + } + + if (seen.contains(op)) { + // Wire may be reached from multiple starts; flush any partial run. + if (current.size() > 1) { + runs.push_back(std::move(current)); + } + current.clear(); + continue; + } + if (!isa(op)) { + if (current.size() > 1) { + runs.push_back(std::move(current)); + } + current.clear(); + continue; + } + + auto iface = cast(op); + if (!isFuseCandidate(iface)) { + if (current.size() > 1) { + runs.push_back(std::move(current)); + } + current.clear(); + continue; + } + + current.push_back(iface); + seen.insert(op); + } + + if (current.size() > 1) { + runs.push_back(std::move(current)); + } + } + + for (auto& run : runs) { + if (run.empty() || run.front().getOperation()->getParentOp() == nullptr) { + continue; + } + + auto composed = composeRun(run); + if (!composed) { + continue; + } + + OpBuilder builder(run.front().getOperation()); + Value qubit = decomposition::synthesizeUnitary1QEuler( + builder, run.front().getLoc(), run.front().getInputTarget(0), + *composed, *parsed); + + run.back().getOutputTarget(0).replaceAllUsesWith(qubit); + for (auto& it : std::ranges::reverse_view(run)) { + it.getOperation()->erase(); + } + } + } +}; + +} // namespace +} // namespace mlir::qco From 54ca07d9b8fad5b4070c0d10292405e1c3f5d81e Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 19 May 2026 16:38:01 +0200 Subject: [PATCH 11/68] =?UTF-8?q?=E2=9C=85=20Add=20tests=20for=20Euler=20s?= =?UTF-8?q?ynthesis=20and=20fuse-single-qubit-unitary-runs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Transforms/Decomposition/CMakeLists.txt | 7 +- .../Decomposition/decomposition_test_utils.h | 54 -- .../test_decomposition_helpers.cpp | 59 -- .../test_euler_decomposition.cpp | 646 ++++++++++++------ .../Decomposition/test_euler_helpers.cpp | 36 + mlir/unittests/programs/qco_programs.cpp | 36 + mlir/unittests/programs/qco_programs.h | 10 + 7 files changed, 541 insertions(+), 307 deletions(-) delete mode 100644 mlir/unittests/Dialect/QCO/Transforms/Decomposition/decomposition_test_utils.h delete mode 100644 mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_helpers.cpp create mode 100644 mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_helpers.cpp diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt index 83fe424bab..a12a32b5a3 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt @@ -7,9 +7,12 @@ # Licensed under the MIT License set(target_name mqt-core-mlir-unittest-decomposition) -add_executable(${target_name} test_decomposition_helpers.cpp test_euler_decomposition.cpp) +add_executable(${target_name} test_euler_decomposition.cpp test_euler_helpers.cpp) -target_link_libraries(${target_name} PRIVATE GTest::gtest_main MLIRQCOTransforms Eigen3::Eigen) +target_link_libraries(${target_name} PRIVATE GTest::gtest_main MLIRQCOPrograms MLIRQCOTransforms + Eigen3::Eigen) +target_link_libraries(${target_name} PRIVATE MLIRPass MLIRFuncDialect MLIRArithDialect MLIRIR + MLIRSupport MLIRQTensorDialect) 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 deleted file mode 100644 index 724634523c..0000000000 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/decomposition_test_utils.h +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) 2023 - 2026 Chair for Design Automation, TUM - * Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH - * All rights reserved. - * - * SPDX-License-Identifier: MIT - * - * Licensed under the MIT License - */ - -#pragma once - -#include "mlir/Dialect/QCO/Transforms/Decomposition/Helpers.h" - -#include -#include - -#include -#include -#include - -namespace mlir::qco::decomposition_test { - -template -[[nodiscard]] MatrixType randomUnitaryMatrix(std::mt19937& rng) { - static_assert(MatrixType::RowsAtCompileTime != Eigen::Dynamic && - MatrixType::ColsAtCompileTime != Eigen::Dynamic, - "randomUnitaryMatrix requires fixed-size matrices"); - static_assert(MatrixType::RowsAtCompileTime == MatrixType::ColsAtCompileTime, - "randomUnitaryMatrix requires square matrices"); - std::normal_distribution normalDist(0.0, 1.0); - MatrixType randomMatrix; - for (auto& x : randomMatrix.reshaped()) { - x = 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}; - } - const MatrixType unitaryMatrix = qMatrix * dMatrix; - assert(helpers::isUnitaryMatrix(unitaryMatrix)); - return unitaryMatrix; -} - -} // namespace mlir::qco::decomposition_test 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 ac63477f76..0000000000 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_decomposition_helpers.cpp +++ /dev/null @@ -1,59 +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/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, GetComplexitySingleQubitAndGphase) { - EXPECT_EQ(getComplexity(GateKind::X, 1), 1U); - EXPECT_EQ(getComplexity(GateKind::GPhase, 1), 0U); - EXPECT_EQ(getComplexity(GateKind::GPhase, 2), 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)); -} - -TEST(DecompositionHelpersTest, IsUnitaryMatrixAcceptsUnitary) { - const Eigen::Matrix2cd m = Eigen::Matrix2cd::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 88e0b214e0..2e0d78e7ae 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -8,250 +8,512 @@ * 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 "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h" +#include "mlir/Dialect/QCO/IR/QCODialect.h" +#include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/IR/QCOUnitaryMatrixInterfaces.h" +#include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" +#include "mlir/Dialect/QCO/Transforms/Passes.h" +#include "mlir/Dialect/Utils/Utils.h" +#include "qco_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::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; - } +namespace { + +template +[[nodiscard]] MatrixType randomUnitaryMatrix(std::mt19937& rng) { + static_assert(MatrixType::RowsAtCompileTime != Eigen::Dynamic && + MatrixType::ColsAtCompileTime != Eigen::Dynamic, + "randomUnitaryMatrix requires fixed-size matrices"); + static_assert(MatrixType::RowsAtCompileTime == MatrixType::ColsAtCompileTime, + "randomUnitaryMatrix requires square matrices"); + std::normal_distribution normalDist(0.0, 1.0); + MatrixType randomMatrix; + for (auto& x : randomMatrix.reshaped()) { + x = std::complex(normalDist(rng), normalDist(rng)); } - return count; + 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}; + } + const MatrixType unitaryMatrix = qMatrix * dMatrix; + assert(helpers::isUnitaryMatrix(unitaryMatrix)); + return unitaryMatrix; } -/// Compare ``seq.getUnitaryMatrix()`` to ``u`` embedded on qubit 0 (4×4 -/// layout). -static bool sequenceMatchesSingleQubitMatrix(const Eigen::Matrix2cd& u, - const OneQubitGateSequence& seq) { - const Eigen::Matrix4cd expanded = expandToTwoQubits(u, 0); - return expanded.isApprox(seq.getUnitaryMatrix()); -} +struct SynthesisFixture { + std::unique_ptr context; -// 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; - } + void setUp() { + DialectRegistry registry; + registry.insert(); + context = std::make_unique(); + context->appendDialectRegistry(registry); + context->loadAllAvailableDialects(); + } +}; - matrix *= helpers::globalPhaseFactor(sequence.globalPhase); - return matrix; +template void forEachBasis(Fn fn) { + const std::array bases = {"zyz", "zxz", "xzx", "xyx", + "u", "zsx", "zsxx"}; + for (const char* basis : bases) { + fn(StringRef{basis}); } +} -protected: - void SetUp() override { - eulerBasis = std::get<0>(GetParam()); - originalMatrix = std::get<1>(GetParam())(); +bool isAllowedBasisGate(Operation& op, StringRef basis) { + // Always allow global phase as correction term. + if (isa(op)) { + return true; } - Eigen::Matrix2cd originalMatrix; - EulerBasis eulerBasis{}; -}; + const auto b = basis.lower(); + if (b == "zyz") { + return isa(op); + } + if (b == "zxz") { + return isa(op); + } + if (b == "xzx") { + return isa(op); + } + if (b == "xyx") { + return isa(op); + } + if (b == "u") { + return isa(op); + } + if (b == "zsx") { + return isa(op); + } + if (b == "zsxx") { + return isa(op); + } + return false; +} -TEST_P(EulerDecompositionTest, TestExact) { - auto decomposition = EulerDecomposition::generateCircuit( - eulerBasis, originalMatrix, false, std::nullopt); - auto restoredMatrix = restore(decomposition); +void expectBasisGatesOnly(func::FuncOp funcOp, StringRef basis) { + auto& block = funcOp.getBody().front(); + for (Operation& op : block.without_terminator()) { + if (isa(op)) { + continue; + } + + // Allow the separator modifiers themselves. + if (isa(op)) { + continue; + } - EXPECT_TRUE(restoredMatrix.isApprox(originalMatrix)) - << "RESULT:\n" - << restoredMatrix << '\n'; + // Only check ops that claim to carry a unitary matrix (i.e., actual gates). + if (isa(op)) { + EXPECT_TRUE(isAllowedBasisGate(op, basis)) + << "basis=" << basis.str() + << " unexpected gate: " << op.getName().getStringRef().str(); + } + } } -TEST(EulerDecompositionTest, Random) { - constexpr auto maxIterations = 10000; - std::mt19937 rng{12345678UL}; +Eigen::Matrix2cd compute1QMatrixFromFunction(func::FuncOp funcOp) { + Eigen::Matrix2cd acc = Eigen::Matrix2cd::Identity(); + std::complex global{1.0, 0.0}; - 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'; + auto& block = funcOp.getBody().front(); + for (Operation& op : block.without_terminator()) { + if (isa(op)) { + continue; + } + + if (isa(op)) { + continue; + } + + if (auto gphase = dyn_cast(op)) { + if (auto m = gphase.getUnitaryMatrix()) { + global *= (*m)(0, 0); + } + continue; + } + + if (auto iface = dyn_cast(op)) { + // All ops in this test should be 1q ops after synthesis. + const auto maybeM = iface.getUnitaryMatrix(); + if (!maybeM) { + ADD_FAILURE() << "Expected constant unitary matrix for op: " + << op.getName().getStringRef().str(); + return Eigen::Matrix2cd::Zero(); + } + acc = (*maybeM) * acc; + continue; + } } + + return global * acc; } -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; +LogicalResult runFuse(ModuleOp module, StringRef basis) { + PassManager pm(module.getContext()); + qco::FuseSingleQubitUnitaryRunsOptions opts; + opts.basis = basis.str(); + pm.addPass(qco::createFuseSingleQubitUnitaryRuns(opts)); + return pm.run(module); +} - const auto angles = - EulerDecomposition::anglesFromUnitary(hadamard, EulerBasis::ZYZ); - OneQubitGateSequence zyzSeq{ - .gates = - { - {.type = GateKind::RZ, .parameter = {angles[2]}}, - {.type = GateKind::RY, .parameter = {angles[0]}}, - {.type = GateKind::RZ, .parameter = {angles[1]}}, - }, - .globalPhase = angles[3], - }; - const Eigen::Matrix2cd reconstructed = - EulerDecompositionTest::restore(zyzSeq); +OwningOpRef buildProgram(MLIRContext* ctx, + void (*fn)(QCOProgramBuilder&)) { + QCOProgramBuilder builder(ctx); + builder.initialize(); + fn(builder); + return builder.finalize(); +} - EXPECT_TRUE(reconstructed.isApprox(hadamard)); +func::FuncOp lookupMain(ModuleOp module) { + auto func = module.lookupSymbol("main"); + EXPECT_TRUE(func) << "Expected a 'main' function"; + return func; } -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)) * - decomposition::uMatrix(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(expanded.isApprox(u3Seq.getUnitaryMatrix())); - EXPECT_TRUE(expanded.isApprox(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); +template +void runFuseOnProgramForAllBases(MLIRContext* ctx, + void (*program)(QCOProgramBuilder&), + ChecksT checksAfter) { + forEachBasis([&](StringRef basis) { + auto owned = buildProgram(ctx, program); + if (!static_cast(owned)) { + ADD_FAILURE() << "Failed to build program for basis=" << basis.str(); + return; + } + ModuleOp module = *owned; + if (failed(verify(module))) { + ADD_FAILURE() << "Verifier failed for basis=" << basis.str(); + return; } + + auto funcOp = lookupMain(module); + if (!funcOp) { + ADD_FAILURE() << "Missing 'main' for basis=" << basis.str(); + return; + } + + const Eigen::Matrix2cd original = compute1QMatrixFromFunction(funcOp); + + if (failed(runFuse(module, basis))) { + ADD_FAILURE() << "Fuse pass failed for basis=" << basis.str(); + return; + } + if (failed(verify(module))) { + ADD_FAILURE() << "Verifier failed after fuse for basis=" << basis.str(); + return; + } + + funcOp = lookupMain(module); + if (!funcOp) { + ADD_FAILURE() << "Missing 'main' after fuse for basis=" << basis.str(); + return; + } + + checksAfter(funcOp, basis, original); + }); +} + +[[nodiscard]] Eigen::Matrix2cd rxMatrix(double theta) { + const auto halfTheta = theta / 2.0; + const std::complex cosHalf{std::cos(halfTheta), 0.0}; + const std::complex iSinHalf{0.0, -std::sin(halfTheta)}; + return Eigen::Matrix2cd{{cosHalf, iSinHalf}, {iSinHalf, cosHalf}}; +} + +[[nodiscard]] Eigen::Matrix2cd ryMatrix(double theta) { + const auto halfTheta = theta / 2.0; + const std::complex cosHalf{std::cos(halfTheta), 0.0}; + const std::complex sinHalf{std::sin(halfTheta), 0.0}; + return Eigen::Matrix2cd{{cosHalf, -sinHalf}, {sinHalf, cosHalf}}; +} + +[[nodiscard]] Eigen::Matrix2cd rzMatrix(double theta) { + return Eigen::Matrix2cd{{{std::cos(theta / 2.0), -std::sin(theta / 2.0)}, 0}, + {0, {std::cos(theta / 2.0), std::sin(theta / 2.0)}}}; +} + +struct SynthesizedCircuit { + OwningOpRef module; + func::FuncOp func; +}; + +[[nodiscard]] SynthesizedCircuit +synthesizeMatrix(MLIRContext* ctx, const Eigen::Matrix2cd& matrix, + EulerBasis basis, bool simplify) { + OwningOpRef module = ModuleOp::create(UnknownLoc::get(ctx)); + OpBuilder builder(ctx); + builder.setInsertionPointToStart(module->getBody()); + + auto qubitTy = QubitType::get(ctx); + auto funcTy = builder.getFunctionType({qubitTy}, {qubitTy}); + auto func = builder.create(module->getLoc(), "main", funcTy); + auto* entry = func.addEntryBlock(); + + builder.setInsertionPointToStart(entry); + Value q = entry->getArgument(0); + q = synthesizeUnitary1QEuler(builder, module->getLoc(), q, matrix, basis, + simplify); + builder.create(module->getLoc(), q); + return SynthesizedCircuit{.module = std::move(module), .func = func}; +} + +template +[[nodiscard]] std::size_t countOps(func::FuncOp funcOp) { + std::size_t count = 0; + funcOp.walk([&](OpTy) { ++count; }); + return count; +} + +[[nodiscard]] std::size_t countUnitaryMatrixOps(func::FuncOp funcOp) { + std::size_t count = 0; + funcOp.walk([&](UnitaryMatrixOpInterface) { ++count; }); + return count; +} + +} // namespace + +TEST(EulerSynthesisTest, RandomReconstructionAllBases) { + SynthesisFixture fx; + fx.setUp(); + + std::mt19937 rng{12345678UL}; + constexpr int iterations = 200; + + for (int i = 0; i < iterations; ++i) { + const auto original = randomUnitaryMatrix(rng); + + forEachBasis([&](StringRef basisStr) { + const auto parsed = mlir::qco::decomposition::parseEulerBasis(basisStr); + ASSERT_TRUE(parsed) << "basis=" << basisStr.str(); + + auto module = ModuleOp::create(UnknownLoc::get(fx.context.get())); + MLIRContext* ctx = module.getContext(); + + OpBuilder builder(ctx); + builder.setInsertionPointToStart(module.getBody()); + + auto qubitTy = QubitType::get(ctx); + auto funcTy = builder.getFunctionType({qubitTy}, {qubitTy}); + auto func = builder.create(module.getLoc(), "main", funcTy); + auto* entry = func.addEntryBlock(); + + builder.setInsertionPointToStart(entry); + Value q = entry->getArgument(0); + q = mlir::qco::decomposition::synthesizeUnitary1QEuler( + builder, module.getLoc(), q, original, *parsed); + builder.create(module.getLoc(), q); + + ASSERT_TRUE(succeeded(verify(module))) << "basis=" << basisStr.str(); + + const auto restored = compute1QMatrixFromFunction(func); + EXPECT_TRUE(restored.isApprox(original, mlir::utils::TOLERANCE)) + << "basis=" << basisStr.str(); + }); } } -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(seq.gates.size(), 1U); - EXPECT_EQ(countGatesOfType(seq, GateKind::X), 1U); - EXPECT_EQ(countGatesOfType(seq, GateKind::RZ), 0U); - EXPECT_EQ(countGatesOfType(seq, GateKind::SX), 0U); - EXPECT_EQ(countGatesOfType(seq, GateKind::H), 0U); - EXPECT_EQ(countGatesOfType(seq, GateKind::U), 0U); - EXPECT_TRUE(sequenceMatchesSingleQubitMatrix(pauliX, seq)); +TEST(FuseSingleQubitUnitaryRunsTest, ReconstructsOriginalRunAllBases) { + SynthesisFixture fx; + fx.setUp(); + + runFuseOnProgramForAllBases( + fx.context.get(), &mlir::qco::singleQubitRunWithSingleQubitGate, + /*checksAfter=*/ + [&](func::FuncOp funcOp, StringRef basis, + const Eigen::Matrix2cd& original) { + const auto restored = compute1QMatrixFromFunction(funcOp); + EXPECT_TRUE(restored.isApprox(original, mlir::utils::TOLERANCE)) + << "basis=" << basis.str(); + expectBasisGatesOnly(funcOp, basis); + }); } -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(EulerSynthesisTest, ZsxxPauliXUsesSingleXGate) { + SynthesisFixture fx; + fx.setUp(); + + const Eigen::Matrix2cd pauliX = XOp::getUnitaryMatrix(); + const auto circuit = + synthesizeMatrix(fx.context.get(), pauliX, EulerBasis::ZSXX, + /*simplify=*/true); + + ASSERT_TRUE(succeeded(verify(*circuit.module))); + EXPECT_EQ(countUnitaryMatrixOps(circuit.func), 1U); + EXPECT_EQ(countOps(circuit.func), 1U); + EXPECT_EQ(countOps(circuit.func), 0U); + EXPECT_EQ(countOps(circuit.func), 0U); + EXPECT_EQ(countOps(circuit.func), 0U); + EXPECT_TRUE(compute1QMatrixFromFunction(circuit.func) + .isApprox(pauliX, mlir::utils::TOLERANCE)); } -TEST(EulerDecompositionTest, UAndU321MatchU3Reconstruction) { - std::mt19937 rng(99991); +TEST(EulerSynthesisTest, UGateReconstruction) { + SynthesisFixture fx; + fx.setUp(); + + 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)); + const auto circuit = synthesizeMatrix(fx.context.get(), u, EulerBasis::U, + /*simplify=*/true); + ASSERT_TRUE(succeeded(verify(*circuit.module))); + EXPECT_LE(countOps(circuit.func), 1U); + EXPECT_TRUE(compute1QMatrixFromFunction(circuit.func) + .isApprox(u, mlir::utils::TOLERANCE)); } } -TEST(EulerDecompositionTest, AnglesFromUnitaryXZXReconstructsRx) { - const Eigen::Matrix2cd u = rxMatrix(0.7); - const auto angles = EulerDecomposition::anglesFromUnitary(u, EulerBasis::XZX); - OneQubitGateSequence seq{ - .gates = - { - {.type = GateKind::RX, .parameter = {angles[2]}}, - {.type = GateKind::RZ, .parameter = {angles[0]}}, - {.type = GateKind::RX, .parameter = {angles[1]}}, - }, - .globalPhase = angles[3], +TEST(EulerDecompositionTest, ZYZAnglesFromUnitaryReconstructHadamard) { + SynthesisFixture fx; + fx.setUp(); + + const Eigen::Matrix2cd hadamard = HOp::getUnitaryMatrix(); + const auto [theta, phi, lambda, phase] = + EulerDecomposition::anglesFromUnitary(hadamard, EulerBasis::ZYZ); + + auto module = ModuleOp::create(UnknownLoc::get(fx.context.get())); + OpBuilder builder(fx.context.get()); + builder.setInsertionPointToStart(module.getBody()); + const Location loc = module.getLoc(); + + auto qubitTy = QubitType::get(fx.context.get()); + auto funcTy = builder.getFunctionType({qubitTy}, {qubitTy}); + auto func = builder.create(loc, "main", funcTy); + auto* entry = func.addEntryBlock(); + builder.setInsertionPointToStart(entry); + + Value q = entry->getArgument(0); + auto mkAngle = [&](double angle) -> Value { + return builder + .create(loc, builder.getF64FloatAttr(angle)) + .getResult(); }; - EXPECT_TRUE(EulerDecompositionTest::restore(seq).isApprox(u)); + q = builder.create(loc, q, mkAngle(lambda)).getQubitOut(); + q = builder.create(loc, q, mkAngle(theta)).getQubitOut(); + q = builder.create(loc, q, mkAngle(phi)).getQubitOut(); + if (std::abs(phase) > mlir::utils::TOLERANCE) { + Value phaseVal = mkAngle(phase); + builder.create(loc, phaseVal); + } + builder.create(loc, q); + + ASSERT_TRUE(succeeded(verify(module))); + EXPECT_TRUE(compute1QMatrixFromFunction(func).isApprox( + hadamard, mlir::utils::TOLERANCE)); } -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()); +// NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_P` at global scope +class EulerSynthesisExactTest + : public testing::TestWithParam< + std::tuple> {}; + +TEST_P(EulerSynthesisExactTest, WithoutSimplification) { + SynthesisFixture fx; + fx.setUp(); + + const auto [basis, matrixFactory] = GetParam(); + const Eigen::Matrix2cd original = matrixFactory(); + const auto circuit = synthesizeMatrix(fx.context.get(), original, basis, + /*simplify=*/false); + + ASSERT_TRUE(succeeded(verify(*circuit.module))); + EXPECT_TRUE(compute1QMatrixFromFunction(circuit.func) + .isApprox(original, mlir::utils::TOLERANCE)); } 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; }))); + SingleQubitMatrices, EulerSynthesisExactTest, + 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 HOp::getUnitaryMatrix(); }))); + +TEST(FuseSingleQubitUnitaryRunsTest, DoesNotFuseAcrossCtrlAllBases) { + SynthesisFixture fx; + fx.setUp(); + + runFuseOnProgramForAllBases( + fx.context.get(), &mlir::qco::singleQubitRunsSplitByTwoQGate, + /*checksAfter=*/ + [&](func::FuncOp funcOp, StringRef basis, + const Eigen::Matrix2cd& original) { + int numCtrl = 0; + funcOp.walk([&](CtrlOp) { ++numCtrl; }); + EXPECT_EQ(numCtrl, 1) << "basis=" << basis.str(); + EXPECT_TRUE(compute1QMatrixFromFunction(funcOp).isApprox( + original, mlir::utils::TOLERANCE)) + << "basis=" << basis.str(); + expectBasisGatesOnly(funcOp, basis); + }); +} + +TEST(FuseSingleQubitUnitaryRunsTest, DoesNotFuseAcrossBarrierAllBases) { + SynthesisFixture fx; + fx.setUp(); + + runFuseOnProgramForAllBases( + fx.context.get(), &mlir::qco::singleQubitRunsSplitByBarrier, + /*checksAfter=*/ + [&](func::FuncOp funcOp, StringRef basis, + const Eigen::Matrix2cd& original) { + EXPECT_EQ(countOps(funcOp), 1U) << "basis=" << basis.str(); + EXPECT_TRUE(compute1QMatrixFromFunction(funcOp).isApprox( + original, mlir::utils::TOLERANCE)) + << "basis=" << basis.str(); + expectBasisGatesOnly(funcOp, basis); + }); +} + +TEST(FuseSingleQubitUnitaryRunsTest, InvalidBasisFailsPass) { + SynthesisFixture fx; + fx.setUp(); + + auto owned = buildProgram(fx.context.get(), + &mlir::qco::singleQubitRunWithSingleQubitGate); + ASSERT_TRUE(static_cast(owned)); + ModuleOp module = *owned; + ASSERT_TRUE(succeeded(verify(module))); + + EXPECT_TRUE(failed(runFuse(module, "not-a-basis"))); +} diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_helpers.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_helpers.cpp new file mode 100644 index 0000000000..e48b99562c --- /dev/null +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_helpers.cpp @@ -0,0 +1,36 @@ +/* + * 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 +#include + +#include +#include + +using namespace mlir::qco::helpers; + +TEST(EulerHelpersTest, 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(EulerHelpersTest, IsUnitaryMatrixRejectsNonUnitary) { + Eigen::Matrix2cd m; + m << 2.0, 0.0, 0.0, 2.0; + EXPECT_FALSE(isUnitaryMatrix(m)); +} + +TEST(EulerHelpersTest, IsUnitaryMatrixAcceptsUnitary) { + const Eigen::Matrix2cd m = Eigen::Matrix2cd::Identity(); + EXPECT_TRUE(isUnitaryMatrix(m)); +} diff --git a/mlir/unittests/programs/qco_programs.cpp b/mlir/unittests/programs/qco_programs.cpp index afb5a6bf59..4094b7e977 100644 --- a/mlir/unittests/programs/qco_programs.cpp +++ b/mlir/unittests/programs/qco_programs.cpp @@ -2344,4 +2344,40 @@ void nestedIfOpForLoop(QCOProgramBuilder& b) { }); } +void singleQubitRunWithSingleQubitGate(QCOProgramBuilder& b) { + auto q = b.allocQubitRegister(1); + q[0] = b.h(q[0]); + q[0] = b.t(q[0]); + q[0] = b.rz(0.123, q[0]); + // Keep `inv` inside the single-qubit run so it gets fused/resynthesized too. + q[0] = b.inv({q[0]}, [&](ValueRange targets) -> SmallVector { + return {b.sx(targets[0])}; + })[0]; + q[0] = b.ry(-0.456, q[0]); +} + +void singleQubitRunsSplitByTwoQGate(QCOProgramBuilder& b) { + auto q = b.allocQubitRegister(2); + q[0] = b.h(q[0]); + q[0] = b.t(q[0]); + // Use `ctrl` as a separator between runs. + const auto& [ctrlOut, tgtOut] = + b.ctrl({q[1]}, {q[0]}, [&](ValueRange targets) -> SmallVector { + return {b.x(targets[0])}; + }); + q[1] = ctrlOut[0]; + q[0] = tgtOut[0]; + q[0] = b.rz(0.321, q[0]); + q[0] = b.sx(q[0]); +} + +void singleQubitRunsSplitByBarrier(QCOProgramBuilder& b) { + auto q = b.allocQubitRegister(1); + q[0] = b.h(q[0]); + q[0] = b.t(q[0]); + q[0] = b.barrier({q[0]})[0]; + q[0] = b.rz(0.321, q[0]); + q[0] = b.sx(q[0]); +} + } // namespace mlir::qco diff --git a/mlir/unittests/programs/qco_programs.h b/mlir/unittests/programs/qco_programs.h index a056d91ca6..9519a55310 100644 --- a/mlir/unittests/programs/qco_programs.h +++ b/mlir/unittests/programs/qco_programs.h @@ -1075,4 +1075,14 @@ void qtensorInsertExtractIndexMismatch(QCOProgramBuilder& b); /// Inserts a qubit into a tensor and extracts it immediately at the same index. void qtensorInsertExtractSameIndex(QCOProgramBuilder& b); +// --- Single-qubit run merging --------------------------------------------- // + +/// Creates a single-qubit run with a single-qubit gate. +void singleQubitRunWithSingleQubitGate(QCOProgramBuilder& b); + +/// Creates a single-qubit run with a two-qubit gate. +void singleQubitRunsSplitByTwoQGate(QCOProgramBuilder& b); + +/// Creates two single-qubit runs separated by a barrier on the same wire. +void singleQubitRunsSplitByBarrier(QCOProgramBuilder& b); } // namespace mlir::qco From 1c01e71dcc576cfbf89f8d8f736cd206ddba5c23 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 19 May 2026 16:38:18 +0200 Subject: [PATCH 12/68] =?UTF-8?q?=F0=9F=93=9D=20Document=20fuse-single-qub?= =?UTF-8?q?it-unitary-runs=20in=20CHANGELOG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b913a1dc9d..3a1f38b50d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ This project adheres to [Semantic Versioning], with the exception that minor rel ### Added +- ✨ Add a `fuse-single-qubit-unitary-runs` pass for fusing compile-time single-qubit unitary runs via Euler resynthesis ([#1672]) ([**@simon1hofmann**]) - ✨ Add a `hadamard-lifting` pass for lifting Hadamard gates above Pauli gates ([#1605]) ([**@lirem101**], [**@burgholzer**]) - ✨ Add a `merge-single-qubit-rotation-gates` pass for merging consecutive rotation gates using quaternions ([#1407], [#1674]) ([**@J4MMlE**], [**@denialhaag**], [**@MatthiasReumann**]) - ✨ Add conversions between `jeff` and QCO ([#1479], [#1548], [#1565], [#1637], [#1676]) ([**@denialhaag**], [**@burgholzer**]) @@ -396,6 +397,7 @@ _📚 Refer to the [GitHub Release Notes](https://github.com/munich-quantum-tool [#1675]: https://github.com/munich-quantum-toolkit/core/pull/1675 [#1674]: https://github.com/munich-quantum-toolkit/core/pull/1674 [#1673]: https://github.com/munich-quantum-toolkit/core/pull/1673 +[#1672]: https://github.com/munich-quantum-toolkit/core/pull/1672 [#1664]: https://github.com/munich-quantum-toolkit/core/pull/1664 [#1662]: https://github.com/munich-quantum-toolkit/core/pull/1662 [#1652]: https://github.com/munich-quantum-toolkit/core/pull/1652 From a92a2b64ea81285f10efae7a5f3f5aa048f82f5c Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 19 May 2026 17:07:38 +0200 Subject: [PATCH 13/68] =?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/Euler.cpp | 59 ++++++++++--------- .../FuseSingleQubitUnitaryRuns.cpp | 49 +++++++-------- .../test_euler_decomposition.cpp | 47 ++++++++------- 3 files changed, 82 insertions(+), 73 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index 94a32a6ac4..088db7e4f7 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -18,11 +18,17 @@ #include #include #include +#include +#include +#include +#include +#include #include #include #include #include +#include namespace mlir::qco::decomposition { @@ -133,14 +139,13 @@ EulerDecomposition::paramsXZX(const Eigen::Matrix2cd& matrix) { // Euler synthesis (IR emission) //===----------------------------------------------------------------------===// -namespace { - -Value getOrCreateF64Constant(OpBuilder& builder, Location loc, double value) { +static Value getOrCreateF64Constant(OpBuilder& builder, Location loc, + double value) { return arith::ConstantOp::create(builder, loc, builder.getF64FloatAttr(value)); } -void emitGPhaseIfNeeded(OpBuilder& builder, Location loc, double phase) { +static void emitGPhaseIfNeeded(OpBuilder& builder, Location loc, double phase) { if (std::abs(phase) <= mlir::utils::TOLERANCE) { return; } @@ -148,14 +153,14 @@ void emitGPhaseIfNeeded(OpBuilder& builder, Location loc, double phase) { builder.create(loc, phaseVal); } -double phaseToMatchTarget(const Eigen::Matrix2cd& target, - const Eigen::Matrix2cd& emitted) { +static double phaseToMatchTarget(const Eigen::Matrix2cd& target, + const Eigen::Matrix2cd& emitted) { // If `target == s * emitted`, then `target * emitted^H == s * I`. const auto s = (target * emitted.adjoint())(0, 0); return std::arg(s); } -void accumulateEmittedMatrix(Operation* op, Eigen::Matrix2cd& emitted) { +static void accumulateEmittedMatrix(Operation* op, Eigen::Matrix2cd& emitted) { auto iface = dyn_cast(op); if (!iface) { llvm::reportFatalInternalError( @@ -172,46 +177,46 @@ void accumulateEmittedMatrix(Operation* op, Eigen::Matrix2cd& emitted) { emitted = m * emitted; } -Value emitRZ(OpBuilder& builder, Location loc, Value qubit, double angle, - Eigen::Matrix2cd& emitted) { +static Value emitRZ(OpBuilder& builder, Location loc, Value qubit, double angle, + Eigen::Matrix2cd& emitted) { auto v = getOrCreateF64Constant(builder, loc, angle); auto op = builder.create(loc, qubit, v); accumulateEmittedMatrix(op, emitted); return op.getQubitOut(); } -Value emitRY(OpBuilder& builder, Location loc, Value qubit, double angle, - Eigen::Matrix2cd& emitted) { +static Value emitRY(OpBuilder& builder, Location loc, Value qubit, double angle, + Eigen::Matrix2cd& emitted) { auto v = getOrCreateF64Constant(builder, loc, angle); auto op = builder.create(loc, qubit, v); accumulateEmittedMatrix(op, emitted); return op.getQubitOut(); } -Value emitRX(OpBuilder& builder, Location loc, Value qubit, double angle, - Eigen::Matrix2cd& emitted) { +static Value emitRX(OpBuilder& builder, Location loc, Value qubit, double angle, + Eigen::Matrix2cd& emitted) { auto v = getOrCreateF64Constant(builder, loc, angle); auto op = builder.create(loc, qubit, v); accumulateEmittedMatrix(op, emitted); return op.getQubitOut(); } -Value emitSX(OpBuilder& builder, Location loc, Value qubit, - Eigen::Matrix2cd& emitted) { +static Value emitSX(OpBuilder& builder, Location loc, Value qubit, + Eigen::Matrix2cd& emitted) { auto op = builder.create(loc, qubit); accumulateEmittedMatrix(op, emitted); return op.getQubitOut(); } -Value emitX(OpBuilder& builder, Location loc, Value qubit, - Eigen::Matrix2cd& emitted) { +static Value emitX(OpBuilder& builder, Location loc, Value qubit, + Eigen::Matrix2cd& emitted) { auto op = builder.create(loc, qubit); accumulateEmittedMatrix(op, emitted); return op.getQubitOut(); } -Value emitU(OpBuilder& builder, Location loc, Value qubit, double theta, - double phi, double lambda, Eigen::Matrix2cd& emitted) { +static Value emitU(OpBuilder& builder, Location loc, Value qubit, double theta, + double phi, double lambda, Eigen::Matrix2cd& emitted) { auto thetaV = getOrCreateF64Constant(builder, loc, theta); auto phiV = getOrCreateF64Constant(builder, loc, phi); auto lambdaV = getOrCreateF64Constant(builder, loc, lambda); @@ -220,9 +225,10 @@ Value emitU(OpBuilder& builder, Location loc, Value qubit, double theta, return op.getQubitOut(); } -Value emitKAK(OpBuilder& builder, Location loc, Value qubit, - const Eigen::Matrix2cd& targetMatrix, double theta, double phi, - double lambda, EulerBasis basis, bool simplify) { +static Value emitKAK(OpBuilder& builder, Location loc, Value qubit, + const Eigen::Matrix2cd& targetMatrix, double theta, + double phi, double lambda, EulerBasis basis, + bool simplify) { const double eps = simplify ? mlir::utils::TOLERANCE : -1.0; Eigen::Matrix2cd emitted = Eigen::Matrix2cd::Identity(); @@ -276,9 +282,10 @@ Value emitKAK(OpBuilder& builder, Location loc, Value qubit, return qubit; } -Value emitPSXGen(OpBuilder& builder, Location loc, Value qubit, - const Eigen::Matrix2cd& targetMatrix, double theta, double phi, - double lambda, bool allowXShortcut, bool simplify) { +static Value emitPSXGen(OpBuilder& builder, Location loc, Value qubit, + const Eigen::Matrix2cd& targetMatrix, double theta, + double phi, double lambda, bool allowXShortcut, + bool simplify) { const double eps = simplify ? mlir::utils::TOLERANCE : -1.0; Eigen::Matrix2cd emitted = Eigen::Matrix2cd::Identity(); @@ -334,8 +341,6 @@ Value emitPSXGen(OpBuilder& builder, Location loc, Value qubit, return qubit; } -} // namespace - std::optional parseEulerBasis(StringRef basis) { const auto b = basis.lower(); if (b == "zyz") { diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp index fa17d41b8f..737eb6c9b5 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp @@ -10,37 +10,39 @@ #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/IR/QCOUnitaryMatrixInterfaces.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/QCO/Utils/WireIterator.h" +#include "mlir/Dialect/QTensor/IR/QTensorOps.h" #include -#include #include +#include #include #include -#include +#include #include -#include +#include +#include #include +#include namespace mlir::qco { #define GEN_PASS_DEF_FUSESINGLEQUBITUNITARYRUNS #include "mlir/Dialect/QCO/Transforms/Passes.h.inc" -namespace { - -bool isFuseCandidate(UnitaryOpInterface op) { +static bool isFuseCandidate(UnitaryOpInterface op) { if (!op || !op.isSingleQubit()) { return false; } return isa(op.getOperation()); } -std::optional getConstMatrix(UnitaryOpInterface op) { +static std::optional getConstMatrix(UnitaryOpInterface op) { if (!isa(op.getOperation())) { return std::nullopt; } @@ -53,7 +55,8 @@ std::optional getConstMatrix(UnitaryOpInterface op) { } /// Compose a run of unitary ops (execution order) into a single matrix. -std::optional composeRun(ArrayRef run) { +static std::optional +composeRun(ArrayRef run) { Eigen::Matrix2cd composed = Eigen::Matrix2cd::Identity(); for (auto op : run) { auto m = getConstMatrix(op); @@ -74,6 +77,7 @@ struct FuseSingleQubitUnitaryRunsPass final FuseSingleQubitUnitaryRunsOptions options) : Base(std::move(options)) {} +protected: void runOnOperation() override { auto module = getOperation(); @@ -106,6 +110,15 @@ struct FuseSingleQubitUnitaryRunsPass final SmallVector, 16> runs; DenseSet seen; + auto flushRun = [&](SmallVector& current) { + if (current.size() > 1) { + runs.push_back(std::move(current)); + current = SmallVector(); + } else { + current.clear(); + } + }; + for (Value start : wireStarts) { if (!start) { continue; @@ -128,26 +141,17 @@ struct FuseSingleQubitUnitaryRunsPass final if (seen.contains(op)) { // Wire may be reached from multiple starts; flush any partial run. - if (current.size() > 1) { - runs.push_back(std::move(current)); - } - current.clear(); + flushRun(current); continue; } if (!isa(op)) { - if (current.size() > 1) { - runs.push_back(std::move(current)); - } - current.clear(); + flushRun(current); continue; } auto iface = cast(op); if (!isFuseCandidate(iface)) { - if (current.size() > 1) { - runs.push_back(std::move(current)); - } - current.clear(); + flushRun(current); continue; } @@ -155,9 +159,7 @@ struct FuseSingleQubitUnitaryRunsPass final seen.insert(op); } - if (current.size() > 1) { - runs.push_back(std::move(current)); - } + flushRun(current); } for (auto& run : runs) { @@ -183,5 +185,4 @@ struct FuseSingleQubitUnitaryRunsPass final } }; -} // namespace } // namespace mlir::qco 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 2e0d78e7ae..2d94cbdae0 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -22,11 +22,16 @@ #include #include #include +#include #include #include +#include #include +#include +#include #include #include +#include #include #include @@ -34,19 +39,19 @@ #include #include #include +#include #include #include #include #include +#include using namespace mlir; using namespace mlir::qco; using namespace mlir::qco::decomposition; -namespace { - template -[[nodiscard]] MatrixType randomUnitaryMatrix(std::mt19937& rng) { +[[nodiscard]] static MatrixType randomUnitaryMatrix(std::mt19937& rng) { static_assert(MatrixType::RowsAtCompileTime != Eigen::Dynamic && MatrixType::ColsAtCompileTime != Eigen::Dynamic, "randomUnitaryMatrix requires fixed-size matrices"); @@ -87,7 +92,7 @@ struct SynthesisFixture { } }; -template void forEachBasis(Fn fn) { +template static void forEachBasis(Fn fn) { const std::array bases = {"zyz", "zxz", "xzx", "xyx", "u", "zsx", "zsxx"}; for (const char* basis : bases) { @@ -95,7 +100,7 @@ template void forEachBasis(Fn fn) { } } -bool isAllowedBasisGate(Operation& op, StringRef basis) { +static bool isAllowedBasisGate(Operation& op, StringRef basis) { // Always allow global phase as correction term. if (isa(op)) { return true; @@ -126,7 +131,7 @@ bool isAllowedBasisGate(Operation& op, StringRef basis) { return false; } -void expectBasisGatesOnly(func::FuncOp funcOp, StringRef basis) { +static void expectBasisGatesOnly(func::FuncOp funcOp, StringRef basis) { auto& block = funcOp.getBody().front(); for (Operation& op : block.without_terminator()) { if (isa(op)) { @@ -147,7 +152,7 @@ void expectBasisGatesOnly(func::FuncOp funcOp, StringRef basis) { } } -Eigen::Matrix2cd compute1QMatrixFromFunction(func::FuncOp funcOp) { +static Eigen::Matrix2cd compute1QMatrixFromFunction(func::FuncOp funcOp) { Eigen::Matrix2cd acc = Eigen::Matrix2cd::Identity(); std::complex global{1.0, 0.0}; @@ -184,7 +189,7 @@ Eigen::Matrix2cd compute1QMatrixFromFunction(func::FuncOp funcOp) { return global * acc; } -LogicalResult runFuse(ModuleOp module, StringRef basis) { +static LogicalResult runFuse(ModuleOp module, StringRef basis) { PassManager pm(module.getContext()); qco::FuseSingleQubitUnitaryRunsOptions opts; opts.basis = basis.str(); @@ -192,24 +197,24 @@ LogicalResult runFuse(ModuleOp module, StringRef basis) { return pm.run(module); } -OwningOpRef buildProgram(MLIRContext* ctx, - void (*fn)(QCOProgramBuilder&)) { +static OwningOpRef buildProgram(MLIRContext* ctx, + void (*fn)(QCOProgramBuilder&)) { QCOProgramBuilder builder(ctx); builder.initialize(); fn(builder); return builder.finalize(); } -func::FuncOp lookupMain(ModuleOp module) { +static func::FuncOp lookupMain(ModuleOp module) { auto func = module.lookupSymbol("main"); EXPECT_TRUE(func) << "Expected a 'main' function"; return func; } template -void runFuseOnProgramForAllBases(MLIRContext* ctx, - void (*program)(QCOProgramBuilder&), - ChecksT checksAfter) { +static void runFuseOnProgramForAllBases(MLIRContext* ctx, + void (*program)(QCOProgramBuilder&), + ChecksT checksAfter) { forEachBasis([&](StringRef basis) { auto owned = buildProgram(ctx, program); if (!static_cast(owned)) { @@ -249,21 +254,21 @@ void runFuseOnProgramForAllBases(MLIRContext* ctx, }); } -[[nodiscard]] Eigen::Matrix2cd rxMatrix(double theta) { +[[nodiscard]] static Eigen::Matrix2cd rxMatrix(double theta) { const auto halfTheta = theta / 2.0; const std::complex cosHalf{std::cos(halfTheta), 0.0}; const std::complex iSinHalf{0.0, -std::sin(halfTheta)}; return Eigen::Matrix2cd{{cosHalf, iSinHalf}, {iSinHalf, cosHalf}}; } -[[nodiscard]] Eigen::Matrix2cd ryMatrix(double theta) { +[[nodiscard]] static Eigen::Matrix2cd ryMatrix(double theta) { const auto halfTheta = theta / 2.0; const std::complex cosHalf{std::cos(halfTheta), 0.0}; const std::complex sinHalf{std::sin(halfTheta), 0.0}; return Eigen::Matrix2cd{{cosHalf, -sinHalf}, {sinHalf, cosHalf}}; } -[[nodiscard]] Eigen::Matrix2cd rzMatrix(double theta) { +[[nodiscard]] static Eigen::Matrix2cd rzMatrix(double theta) { return Eigen::Matrix2cd{{{std::cos(theta / 2.0), -std::sin(theta / 2.0)}, 0}, {0, {std::cos(theta / 2.0), std::sin(theta / 2.0)}}}; } @@ -273,7 +278,7 @@ struct SynthesizedCircuit { func::FuncOp func; }; -[[nodiscard]] SynthesizedCircuit +[[nodiscard]] static SynthesizedCircuit synthesizeMatrix(MLIRContext* ctx, const Eigen::Matrix2cd& matrix, EulerBasis basis, bool simplify) { OwningOpRef module = ModuleOp::create(UnknownLoc::get(ctx)); @@ -294,20 +299,18 @@ synthesizeMatrix(MLIRContext* ctx, const Eigen::Matrix2cd& matrix, } template -[[nodiscard]] std::size_t countOps(func::FuncOp funcOp) { +[[nodiscard]] static std::size_t countOps(func::FuncOp funcOp) { std::size_t count = 0; funcOp.walk([&](OpTy) { ++count; }); return count; } -[[nodiscard]] std::size_t countUnitaryMatrixOps(func::FuncOp funcOp) { +[[nodiscard]] static std::size_t countUnitaryMatrixOps(func::FuncOp funcOp) { std::size_t count = 0; funcOp.walk([&](UnitaryMatrixOpInterface) { ++count; }); return count; } -} // namespace - TEST(EulerSynthesisTest, RandomReconstructionAllBases) { SynthesisFixture fx; fx.setUp(); From 577c094f91c8ed917d0d8119d22da82e3d4b3935 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 19 May 2026 17:16:15 +0200 Subject: [PATCH 14/68] =?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 --- .../FuseSingleQubitUnitaryRuns.cpp | 4 ++ .../test_euler_decomposition.cpp | 38 ++++++++++--------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp index 737eb6c9b5..8748f05954 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp @@ -69,6 +69,8 @@ composeRun(ArrayRef run) { return composed; } +namespace { + struct FuseSingleQubitUnitaryRunsPass final : impl::FuseSingleQubitUnitaryRunsBase { using Base::Base; @@ -185,4 +187,6 @@ struct FuseSingleQubitUnitaryRunsPass final } }; +} // namespace + } // namespace mlir::qco 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 2d94cbdae0..955062a1ce 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -50,6 +50,27 @@ using namespace mlir; using namespace mlir::qco; using namespace mlir::qco::decomposition; +namespace { + +struct SynthesisFixture { + std::unique_ptr context; + + void setUp() { + DialectRegistry registry; + registry.insert(); + context = std::make_unique(); + context->appendDialectRegistry(registry); + context->loadAllAvailableDialects(); + } +}; + +struct SynthesizedCircuit { + OwningOpRef module; + func::FuncOp func; +}; + +} // namespace + template [[nodiscard]] static MatrixType randomUnitaryMatrix(std::mt19937& rng) { static_assert(MatrixType::RowsAtCompileTime != Eigen::Dynamic && @@ -80,18 +101,6 @@ template return unitaryMatrix; } -struct SynthesisFixture { - std::unique_ptr context; - - void setUp() { - DialectRegistry registry; - registry.insert(); - context = std::make_unique(); - context->appendDialectRegistry(registry); - context->loadAllAvailableDialects(); - } -}; - template static void forEachBasis(Fn fn) { const std::array bases = {"zyz", "zxz", "xzx", "xyx", "u", "zsx", "zsxx"}; @@ -273,11 +282,6 @@ static void runFuseOnProgramForAllBases(MLIRContext* ctx, {0, {std::cos(theta / 2.0), std::sin(theta / 2.0)}}}; } -struct SynthesizedCircuit { - OwningOpRef module; - func::FuncOp func; -}; - [[nodiscard]] static SynthesizedCircuit synthesizeMatrix(MLIRContext* ctx, const Eigen::Matrix2cd& matrix, EulerBasis basis, bool simplify) { From 6e7fa7fd5804adfc7c2f5f5ae370de5a3fcc4914 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Tue, 19 May 2026 17:32:25 +0200 Subject: [PATCH 15/68] =?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/Dialect/QCO/IR/QCOUnitaryMatrixInterfaces.td | 6 +++--- .../NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp | 2 +- mlir/unittests/programs/qco_programs.h | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/IR/QCOUnitaryMatrixInterfaces.td b/mlir/include/mlir/Dialect/QCO/IR/QCOUnitaryMatrixInterfaces.td index eff36c0338..0b0aa9e311 100644 --- a/mlir/include/mlir/Dialect/QCO/IR/QCOUnitaryMatrixInterfaces.td +++ b/mlir/include/mlir/Dialect/QCO/IR/QCOUnitaryMatrixInterfaces.td @@ -6,8 +6,8 @@ // // Licensed under the MIT License -#ifndef MLIR_DIALECT_QCO_IR_QCOUNIMARYMATRIXINTERFACES_TD -#define MLIR_DIALECT_QCO_IR_QCOUNIMARYMATRIXINTERFACES_TD +#ifndef MLIR_DIALECT_QCO_IR_QCOUNITARYMATRIXINTERFACES_TD +#define MLIR_DIALECT_QCO_IR_QCOUNITARYMATRIXINTERFACES_TD include "mlir/IR/OpBase.td" @@ -136,4 +136,4 @@ def UnitaryMatrixOpInterface : OpInterface<"UnitaryMatrixOpInterface"> { }]; } -#endif // MLIR_DIALECT_QCO_IR_QCOUNIMARYMATRIXINTERFACES_TD +#endif // MLIR_DIALECT_QCO_IR_QCOUNITARYMATRIXINTERFACES_TD diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp index 8748f05954..4b05df72ad 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp @@ -152,7 +152,7 @@ struct FuseSingleQubitUnitaryRunsPass final } auto iface = cast(op); - if (!isFuseCandidate(iface)) { + if (!isFuseCandidate(iface) || !getConstMatrix(iface).has_value()) { flushRun(current); continue; } diff --git a/mlir/unittests/programs/qco_programs.h b/mlir/unittests/programs/qco_programs.h index 9519a55310..32feec9f78 100644 --- a/mlir/unittests/programs/qco_programs.h +++ b/mlir/unittests/programs/qco_programs.h @@ -1080,7 +1080,7 @@ void qtensorInsertExtractSameIndex(QCOProgramBuilder& b); /// Creates a single-qubit run with a single-qubit gate. void singleQubitRunWithSingleQubitGate(QCOProgramBuilder& b); -/// Creates a single-qubit run with a two-qubit gate. +/// Creates two single-qubit runs separated by a two-qubit gate. void singleQubitRunsSplitByTwoQGate(QCOProgramBuilder& b); /// Creates two single-qubit runs separated by a barrier on the same wire. From 418b17eaf6b6d2363d27bf9006f9937b977cf5cc Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 3 Jun 2026 09:53:52 +0200 Subject: [PATCH 16/68] =?UTF-8?q?=E2=9C=85=20Refactor=20QCO=20decompositio?= =?UTF-8?q?n=20tests=20and=20update=20dependencies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Transforms/Decomposition/CMakeLists.txt | 6 +- .../test_euler_decomposition.cpp | 132 ++++++++++++------ .../Decomposition/test_euler_helpers.cpp | 36 ----- mlir/unittests/programs/qco_programs.cpp | 36 ----- mlir/unittests/programs/qco_programs.h | 11 -- 5 files changed, 92 insertions(+), 129 deletions(-) delete mode 100644 mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_helpers.cpp diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt index a12a32b5a3..11a49eb044 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/CMakeLists.txt @@ -7,10 +7,10 @@ # Licensed under the MIT License set(target_name mqt-core-mlir-unittest-decomposition) -add_executable(${target_name} test_euler_decomposition.cpp test_euler_helpers.cpp) +add_executable(${target_name} test_euler_decomposition.cpp) -target_link_libraries(${target_name} PRIVATE GTest::gtest_main MLIRQCOPrograms MLIRQCOTransforms - Eigen3::Eigen) +target_link_libraries(${target_name} PRIVATE GTest::gtest_main MLIRQCOProgramBuilder + MLIRQCOTransforms Eigen3::Eigen) target_link_libraries(${target_name} PRIVATE MLIRPass MLIRFuncDialect MLIRArithDialect MLIRIR MLIRSupport MLIRQTensorDialect) 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 955062a1ce..1110d24e18 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -15,11 +15,11 @@ #include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/Utils/Utils.h" -#include "qco_programs.h" #include #include #include +#include #include #include #include @@ -140,6 +140,13 @@ static bool isAllowedBasisGate(Operation& op, StringRef basis) { return false; } +[[nodiscard]] static bool isTwoQubitGate(Operation& op) { + if (auto u = dyn_cast(op)) { + return u.isTwoQubit(); + } + return false; +} + static void expectBasisGatesOnly(func::FuncOp funcOp, StringRef basis) { auto& block = funcOp.getBody().front(); for (Operation& op : block.without_terminator()) { @@ -147,8 +154,7 @@ static void expectBasisGatesOnly(func::FuncOp funcOp, StringRef basis) { continue; } - // Allow the separator modifiers themselves. - if (isa(op)) { + if (isTwoQubitGate(op)) { continue; } @@ -171,7 +177,7 @@ static Eigen::Matrix2cd compute1QMatrixFromFunction(func::FuncOp funcOp) { continue; } - if (isa(op)) { + if (isTwoQubitGate(op)) { continue; } @@ -206,6 +212,36 @@ static LogicalResult runFuse(ModuleOp module, StringRef basis) { return pm.run(module); } +static void singleQubitRunWithSingleQubitGate(QCOProgramBuilder& b) { + auto q = b.allocQubitRegister(1); + q[0] = b.h(q[0]); + q[0] = b.t(q[0]); + q[0] = b.rz(0.123, q[0]); + // Keep `inv` inside the single-qubit run so it gets fused/resynthesized too. + q[0] = b.inv({q[0]}, [&](ValueRange targets) -> SmallVector { + return {b.sx(targets[0])}; + })[0]; + q[0] = b.ry(-0.456, q[0]); +} + +static void singleQubitRunsSplitByTwoQGate(QCOProgramBuilder& b) { + auto q = b.allocQubitRegister(2); + q[0] = b.h(q[0]); + q[0] = b.t(q[0]); + std::tie(q[0], q[1]) = b.swap(q[0], q[1]); + q[0] = b.rz(0.321, q[0]); + q[0] = b.sx(q[0]); +} + +static void singleQubitRunsSplitByBarrier(QCOProgramBuilder& b) { + auto q = b.allocQubitRegister(1); + q[0] = b.h(q[0]); + q[0] = b.t(q[0]); + q[0] = b.barrier({q[0]})[0]; + q[0] = b.rz(0.321, q[0]); + q[0] = b.sx(q[0]); +} + static OwningOpRef buildProgram(MLIRContext* ctx, void (*fn)(QCOProgramBuilder&)) { QCOProgramBuilder builder(ctx); @@ -263,23 +299,16 @@ static void runFuseOnProgramForAllBases(MLIRContext* ctx, }); } -[[nodiscard]] static Eigen::Matrix2cd rxMatrix(double theta) { - const auto halfTheta = theta / 2.0; - const std::complex cosHalf{std::cos(halfTheta), 0.0}; - const std::complex iSinHalf{0.0, -std::sin(halfTheta)}; - return Eigen::Matrix2cd{{cosHalf, iSinHalf}, {iSinHalf, cosHalf}}; -} - -[[nodiscard]] static Eigen::Matrix2cd ryMatrix(double theta) { - const auto halfTheta = theta / 2.0; - const std::complex cosHalf{std::cos(halfTheta), 0.0}; - const std::complex sinHalf{std::sin(halfTheta), 0.0}; - return Eigen::Matrix2cd{{cosHalf, -sinHalf}, {sinHalf, cosHalf}}; -} - -[[nodiscard]] static Eigen::Matrix2cd rzMatrix(double theta) { - return Eigen::Matrix2cd{{{std::cos(theta / 2.0), -std::sin(theta / 2.0)}, 0}, - {0, {std::cos(theta / 2.0), std::sin(theta / 2.0)}}}; +template +[[nodiscard]] static Eigen::Matrix2cd rotationMatrix(MLIRContext* ctx, + double theta) { + OpBuilder builder(ctx); + auto module = ModuleOp::create(UnknownLoc::get(ctx)); + builder.setInsertionPointToStart(module.getBody()); + const Location loc = module.getLoc(); + Value q = builder.create(loc).getResult(); + auto op = builder.create(loc, q, theta); + return *cast(op).getUnitaryMatrix(); } [[nodiscard]] static SynthesizedCircuit @@ -309,6 +338,16 @@ template return count; } +[[nodiscard]] static std::size_t countTwoQubitGates(func::FuncOp funcOp) { + std::size_t count = 0; + funcOp.walk([&](UnitaryOpInterface op) { + if (op.isTwoQubit()) { + ++count; + } + }); + return count; +} + [[nodiscard]] static std::size_t countUnitaryMatrixOps(func::FuncOp funcOp) { std::size_t count = 0; funcOp.walk([&](UnitaryMatrixOpInterface) { ++count; }); @@ -360,7 +399,7 @@ TEST(FuseSingleQubitUnitaryRunsTest, ReconstructsOriginalRunAllBases) { fx.setUp(); runFuseOnProgramForAllBases( - fx.context.get(), &mlir::qco::singleQubitRunWithSingleQubitGate, + fx.context.get(), &singleQubitRunWithSingleQubitGate, /*checksAfter=*/ [&](func::FuncOp funcOp, StringRef basis, const Eigen::Matrix2cd& original) { @@ -448,14 +487,14 @@ TEST(EulerDecompositionTest, ZYZAnglesFromUnitaryReconstructHadamard) { // NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_P` at global scope class EulerSynthesisExactTest : public testing::TestWithParam< - std::tuple> {}; + std::tuple> {}; TEST_P(EulerSynthesisExactTest, WithoutSimplification) { SynthesisFixture fx; fx.setUp(); - const auto [basis, matrixFactory] = GetParam(); - const Eigen::Matrix2cd original = matrixFactory(); + const auto [basis, matrixFn] = GetParam(); + const Eigen::Matrix2cd original = matrixFn(fx.context.get()); const auto circuit = synthesizeMatrix(fx.context.get(), original, basis, /*simplify=*/false); @@ -466,28 +505,35 @@ TEST_P(EulerSynthesisExactTest, WithoutSimplification) { INSTANTIATE_TEST_SUITE_P( SingleQubitMatrices, EulerSynthesisExactTest, - 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 HOp::getUnitaryMatrix(); }))); - -TEST(FuseSingleQubitUnitaryRunsTest, DoesNotFuseAcrossCtrlAllBases) { + testing::Combine(testing::Values(EulerBasis::XYX, EulerBasis::XZX, + EulerBasis::ZYZ, EulerBasis::ZXZ), + testing::Values( + [](MLIRContext* /*ctx*/) -> Eigen::Matrix2cd { + return Eigen::Matrix2cd::Identity(); + }, + [](MLIRContext* ctx) -> Eigen::Matrix2cd { + return rotationMatrix(ctx, 2.0); + }, + [](MLIRContext* ctx) -> Eigen::Matrix2cd { + return rotationMatrix(ctx, 0.5); + }, + [](MLIRContext* ctx) -> Eigen::Matrix2cd { + return rotationMatrix(ctx, 3.14); + }, + [](MLIRContext* /*ctx*/) -> Eigen::Matrix2cd { + return HOp::getUnitaryMatrix(); + }))); + +TEST(FuseSingleQubitUnitaryRunsTest, DoesNotFuseAcrossTwoQGateAllBases) { SynthesisFixture fx; fx.setUp(); runFuseOnProgramForAllBases( - fx.context.get(), &mlir::qco::singleQubitRunsSplitByTwoQGate, + fx.context.get(), &singleQubitRunsSplitByTwoQGate, /*checksAfter=*/ [&](func::FuncOp funcOp, StringRef basis, const Eigen::Matrix2cd& original) { - int numCtrl = 0; - funcOp.walk([&](CtrlOp) { ++numCtrl; }); - EXPECT_EQ(numCtrl, 1) << "basis=" << basis.str(); + EXPECT_EQ(countTwoQubitGates(funcOp), 1U) << "basis=" << basis.str(); EXPECT_TRUE(compute1QMatrixFromFunction(funcOp).isApprox( original, mlir::utils::TOLERANCE)) << "basis=" << basis.str(); @@ -500,7 +546,7 @@ TEST(FuseSingleQubitUnitaryRunsTest, DoesNotFuseAcrossBarrierAllBases) { fx.setUp(); runFuseOnProgramForAllBases( - fx.context.get(), &mlir::qco::singleQubitRunsSplitByBarrier, + fx.context.get(), &singleQubitRunsSplitByBarrier, /*checksAfter=*/ [&](func::FuncOp funcOp, StringRef basis, const Eigen::Matrix2cd& original) { @@ -516,8 +562,8 @@ TEST(FuseSingleQubitUnitaryRunsTest, InvalidBasisFailsPass) { SynthesisFixture fx; fx.setUp(); - auto owned = buildProgram(fx.context.get(), - &mlir::qco::singleQubitRunWithSingleQubitGate); + auto owned = + buildProgram(fx.context.get(), &singleQubitRunWithSingleQubitGate); ASSERT_TRUE(static_cast(owned)); ModuleOp module = *owned; ASSERT_TRUE(succeeded(verify(module))); diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_helpers.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_helpers.cpp deleted file mode 100644 index e48b99562c..0000000000 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_helpers.cpp +++ /dev/null @@ -1,36 +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 -#include - -#include -#include - -using namespace mlir::qco::helpers; - -TEST(EulerHelpersTest, 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(EulerHelpersTest, IsUnitaryMatrixRejectsNonUnitary) { - Eigen::Matrix2cd m; - m << 2.0, 0.0, 0.0, 2.0; - EXPECT_FALSE(isUnitaryMatrix(m)); -} - -TEST(EulerHelpersTest, IsUnitaryMatrixAcceptsUnitary) { - const Eigen::Matrix2cd m = Eigen::Matrix2cd::Identity(); - EXPECT_TRUE(isUnitaryMatrix(m)); -} diff --git a/mlir/unittests/programs/qco_programs.cpp b/mlir/unittests/programs/qco_programs.cpp index d0e7c1ac5d..0ad96fbb10 100644 --- a/mlir/unittests/programs/qco_programs.cpp +++ b/mlir/unittests/programs/qco_programs.cpp @@ -2353,40 +2353,4 @@ void nestedIfOpForLoop(QCOProgramBuilder& b) { }); } -void singleQubitRunWithSingleQubitGate(QCOProgramBuilder& b) { - auto q = b.allocQubitRegister(1); - q[0] = b.h(q[0]); - q[0] = b.t(q[0]); - q[0] = b.rz(0.123, q[0]); - // Keep `inv` inside the single-qubit run so it gets fused/resynthesized too. - q[0] = b.inv({q[0]}, [&](ValueRange targets) -> SmallVector { - return {b.sx(targets[0])}; - })[0]; - q[0] = b.ry(-0.456, q[0]); -} - -void singleQubitRunsSplitByTwoQGate(QCOProgramBuilder& b) { - auto q = b.allocQubitRegister(2); - q[0] = b.h(q[0]); - q[0] = b.t(q[0]); - // Use `ctrl` as a separator between runs. - const auto& [ctrlOut, tgtOut] = - b.ctrl({q[1]}, {q[0]}, [&](ValueRange targets) -> SmallVector { - return {b.x(targets[0])}; - }); - q[1] = ctrlOut[0]; - q[0] = tgtOut[0]; - q[0] = b.rz(0.321, q[0]); - q[0] = b.sx(q[0]); -} - -void singleQubitRunsSplitByBarrier(QCOProgramBuilder& b) { - auto q = b.allocQubitRegister(1); - q[0] = b.h(q[0]); - q[0] = b.t(q[0]); - q[0] = b.barrier({q[0]})[0]; - q[0] = b.rz(0.321, q[0]); - q[0] = b.sx(q[0]); -} - } // namespace mlir::qco diff --git a/mlir/unittests/programs/qco_programs.h b/mlir/unittests/programs/qco_programs.h index ff9ba11792..f4dc65e58d 100644 --- a/mlir/unittests/programs/qco_programs.h +++ b/mlir/unittests/programs/qco_programs.h @@ -1077,15 +1077,4 @@ void qtensorInsertExtractIndexMismatch(QCOProgramBuilder& b); /// Inserts a qubit into a tensor and extracts it immediately at the same index. void qtensorInsertExtractSameIndex(QCOProgramBuilder& b); - -// --- Single-qubit run merging --------------------------------------------- // - -/// Creates a single-qubit run with a single-qubit gate. -void singleQubitRunWithSingleQubitGate(QCOProgramBuilder& b); - -/// Creates two single-qubit runs separated by a two-qubit gate. -void singleQubitRunsSplitByTwoQGate(QCOProgramBuilder& b); - -/// Creates two single-qubit runs separated by a barrier on the same wire. -void singleQubitRunsSplitByBarrier(QCOProgramBuilder& b); } // namespace mlir::qco From 7da85cd9c6191f4cd1fab44397c8a3da7582b993 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 3 Jun 2026 10:25:26 +0200 Subject: [PATCH 17/68] =?UTF-8?q?=F0=9F=8E=A8=20Refactor=20Euler=20basis?= =?UTF-8?q?=20handling=20in=20QCO=20transformations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Updated the EulerBasis enum to remove ZSX and clarify ZSXX description. - Simplified function signatures in Euler.cpp and related files by removing the 'simplify' parameter. - Adjusted error messages and test cases to reflect the changes in Euler basis options. - Cleaned up unnecessary code and comments for better readability. --- .../QCO/Transforms/Decomposition/Euler.h | 42 ++--------- .../mlir/Dialect/QCO/Transforms/Passes.td | 5 +- .../QCO/Transforms/Decomposition/Euler.cpp | 74 ++++++------------- .../FuseSingleQubitUnitaryRuns.cpp | 5 +- .../test_euler_decomposition.cpp | 39 +++------- 5 files changed, 43 insertions(+), 122 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h index aed91b529b..9ae931b463 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h @@ -26,17 +26,11 @@ namespace mlir::qco::decomposition { /** * Target gate set for single-qubit Euler synthesis. * - * Selects which QCO operations `synthesizeUnitary1QEuler` emits when lowering a - * 2×2 unitary. Angle extraction (`EulerDecomposition::anglesFromUnitary`) and - * IR emission are separate; several enumerators share the same - * `(theta, phi, lambda, phase)` computation and differ only in the emitted - * sequence. - * * - **KAK bases** (`ZYZ`, `ZXZ`, `XZX`, `XYX`): Euler decompositions named by * the middle and outer rotation axes. * - **`U`**: single `u(theta, phi, lambda)` gate (angles from the Z-Y-Z form). - * - **`ZSX` / `ZSXX`**: `rz`/`sx` templates; `ZSXX` may emit `x` instead of - * `sx · rz · sx` when simplification is on. + * - **`ZSXX`**: `rz · sx · rz · sx · rz`, or `rz · x · rz` when the middle + * angle is ~0. */ enum class EulerBasis : std::uint8_t { ZYZ = 0, ///< `rz · ry · rz`. @@ -44,8 +38,7 @@ enum class EulerBasis : std::uint8_t { XZX = 2, ///< `rx · rz · rx`. XYX = 3, ///< `rx · ry · rx`. U = 4, ///< `u(theta, phi, lambda)`. - ZSXX = 5, ///< `rz · sx · rz · sx · rz`. - ZSX = 6, ///< `rz · sx · rz · sx · rz`. + ZSXX = 5, ///< `rz · sx · rz · sx · rz`, or `rz · x · rz` if middle angle ~0. }; } // namespace mlir::qco::decomposition @@ -55,18 +48,6 @@ enum class EulerBasis : std::uint8_t { // scattering tiny utilities across multiple files. namespace mlir::qco::helpers { -/// Check whether `matrix` is unitary within `tolerance` (i.e. `M^H M` is -/// approximately `I`, using Eigen's `isIdentity`). -template -[[nodiscard]] inline bool -isUnitaryMatrix(const Eigen::Matrix& matrix, - double tolerance = mlir::utils::TOLERANCE) { - if (matrix.rows() != matrix.cols()) { - return false; - } - return (matrix.adjoint() * matrix).isIdentity(tolerance); -} - /** * Wrap angle into interval [-pi, pi). If within atol of the endpoint, clamp to * -pi. @@ -104,13 +85,7 @@ isUnitaryMatrix(const Eigen::Matrix& matrix, namespace mlir::qco::decomposition { -/** - * Extract Euler parameters from single-qubit unitary matrices. - * - * Angle extraction is separate from IR emission (`synthesizeUnitary1QEuler`), - * which tracks global phase via `qco.gphase` so the synthesized circuit matches - * the target matrix exactly (not only up to global phase). - */ +/// Extract Euler parameters from single-qubit unitary matrices. class EulerDecomposition { public: /** @@ -141,7 +116,7 @@ class EulerDecomposition { paramsXZX(const Eigen::Matrix2cd& matrix); /** - * Extract Euler angles for `ZSX` / `ZSXX` (`rz` / `sx`) synthesis. + * Extract Euler angles for `ZSXX` (`rz` / `sx`) synthesis. * * Reuses `(theta, phi, lambda)` from `paramsZYZ` and sets the scalar phase to * `phase - 0.5 * (theta + phi + lambda)` so `emitPSXGen` reproduces `matrix` @@ -173,12 +148,9 @@ class EulerDecomposition { /// The emitted circuit includes a `qco.gphase` correction when needed so the /// overall unitary matches `targetMatrix` exactly (not only up to global /// phase). -/// -/// When `simplify` is false, near-zero rotations and basis-specific shortcuts -/// are not applied. [[nodiscard]] Value synthesizeUnitary1QEuler(OpBuilder& builder, Location loc, Value qubit, - const Eigen::Matrix2cd& targetMatrix, EulerBasis basis, - bool simplify = true); + const Eigen::Matrix2cd& targetMatrix, + EulerBasis basis); } // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index d7ea0da8de..175214355b 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -58,9 +58,8 @@ def FuseSingleQubitUnitaryRuns Currently, only operations whose unitary matrix can be obtained at compile time are fused. }]; - let options = [Option< - "basis", "basis", "std::string", "\"zyz\"", - "Target Euler basis (zyz, zxz, xzx, xyx, u, zsx, zsxx).">]; + let options = [Option<"basis", "basis", "std::string", "\"zyz\"", + "Target Euler basis (zyz, zxz, xzx, xyx, u, zsxx).">]; } def QuantumLoopUnroll diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index 088db7e4f7..d44ba189cb 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -51,7 +51,6 @@ EulerDecomposition::anglesFromUnitary(const Eigen::Matrix2cd& matrix, case EulerBasis::U: // The `u` gate parameterization is derived from the standard Z-Y-Z form. return paramsZYZ(matrix); - case EulerBasis::ZSX: case EulerBasis::ZSXX: return paramsPSX(matrix); } @@ -227,26 +226,22 @@ static Value emitU(OpBuilder& builder, Location loc, Value qubit, double theta, static Value emitKAK(OpBuilder& builder, Location loc, Value qubit, const Eigen::Matrix2cd& targetMatrix, double theta, - double phi, double lambda, EulerBasis basis, - bool simplify) { - const double eps = simplify ? mlir::utils::TOLERANCE : -1.0; + double phi, double lambda, EulerBasis basis) { Eigen::Matrix2cd emitted = Eigen::Matrix2cd::Identity(); auto emitK = [&](double a) { - const double canonical = helpers::mod2pi(a, eps); - if (std::abs(canonical) > eps) { - switch (basis) { - case EulerBasis::ZYZ: - case EulerBasis::ZXZ: - qubit = emitRZ(builder, loc, qubit, canonical, emitted); - break; - case EulerBasis::XZX: - case EulerBasis::XYX: - qubit = emitRX(builder, loc, qubit, canonical, emitted); - break; - default: - llvm::reportFatalInternalError("Invalid K gate for KAK emission"); - } + const double canonical = helpers::mod2pi(a, mlir::utils::TOLERANCE); + switch (basis) { + case EulerBasis::ZYZ: + case EulerBasis::ZXZ: + qubit = emitRZ(builder, loc, qubit, canonical, emitted); + break; + case EulerBasis::XZX: + case EulerBasis::XYX: + qubit = emitRX(builder, loc, qubit, canonical, emitted); + break; + default: + llvm::reportFatalInternalError("Invalid K gate for KAK emission"); } }; @@ -269,12 +264,6 @@ static Value emitKAK(OpBuilder& builder, Location loc, Value qubit, } }; - if (std::abs(theta) <= eps) { - emitK(lambda + phi); - emitGPhaseIfNeeded(builder, loc, phaseToMatchTarget(targetMatrix, emitted)); - return qubit; - } - emitK(lambda); emitA(theta); emitK(phi); @@ -284,24 +273,15 @@ static Value emitKAK(OpBuilder& builder, Location loc, Value qubit, static Value emitPSXGen(OpBuilder& builder, Location loc, Value qubit, const Eigen::Matrix2cd& targetMatrix, double theta, - double phi, double lambda, bool allowXShortcut, - bool simplify) { - const double eps = simplify ? mlir::utils::TOLERANCE : -1.0; + double phi, double lambda) { + constexpr double eps = mlir::utils::TOLERANCE; Eigen::Matrix2cd emitted = Eigen::Matrix2cd::Identity(); auto emitRzAsP = [&](double angle) { const double canonicalAngle = helpers::mod2pi(angle, eps); - if (std::abs(canonicalAngle) > eps) { - qubit = emitRZ(builder, loc, qubit, canonicalAngle, emitted); - } + qubit = emitRZ(builder, loc, qubit, canonicalAngle, emitted); }; - if (std::abs(theta) <= eps) { - emitRzAsP(lambda + phi); - emitGPhaseIfNeeded(builder, loc, phaseToMatchTarget(targetMatrix, emitted)); - return qubit; - } - if (std::abs(theta - (std::numbers::pi / 2.0)) < eps) { emitRzAsP(lambda - (std::numbers::pi / 2.0)); qubit = emitSX(builder, loc, qubit, emitted); @@ -328,7 +308,8 @@ static Value emitPSXGen(OpBuilder& builder, Location loc, Value qubit, emitRzAsP(lambda); - if (allowXShortcut && std::abs(helpers::mod2pi(theta, eps)) < eps) { + // `sx · rz(θ) · sx` equals `x` when θ ≡ 0 (mod 2π). + if (std::abs(helpers::mod2pi(theta, eps)) < eps) { qubit = emitX(builder, loc, qubit, emitted); } else { qubit = emitSX(builder, loc, qubit, emitted); @@ -358,9 +339,6 @@ std::optional parseEulerBasis(StringRef basis) { if (b == "u") { return EulerBasis::U; } - if (b == "zsx") { - return EulerBasis::ZSX; - } if (b == "zsxx") { return EulerBasis::ZSXX; } @@ -369,7 +347,7 @@ std::optional parseEulerBasis(StringRef basis) { Value synthesizeUnitary1QEuler(OpBuilder& builder, Location loc, Value qubit, const Eigen::Matrix2cd& targetMatrix, - EulerBasis basis, bool simplify) { + EulerBasis basis) { const auto [theta, phi, lambda, /*phase=*/phase] = EulerDecomposition::anglesFromUnitary(targetMatrix, basis); @@ -378,8 +356,8 @@ Value synthesizeUnitary1QEuler(OpBuilder& builder, Location loc, Value qubit, case EulerBasis::ZXZ: case EulerBasis::XZX: case EulerBasis::XYX: - qubit = emitKAK(builder, loc, qubit, targetMatrix, theta, phi, lambda, - basis, simplify); + qubit = + emitKAK(builder, loc, qubit, targetMatrix, theta, phi, lambda, basis); break; case EulerBasis::U: { Eigen::Matrix2cd emitted = Eigen::Matrix2cd::Identity(); @@ -387,19 +365,11 @@ Value synthesizeUnitary1QEuler(OpBuilder& builder, Location loc, Value qubit, emitGPhaseIfNeeded(builder, loc, phaseToMatchTarget(targetMatrix, emitted)); break; } - case EulerBasis::ZSX: - qubit = emitPSXGen(builder, loc, qubit, targetMatrix, theta, phi, lambda, - /*allowXShortcut=*/false, simplify); - break; case EulerBasis::ZSXX: - qubit = emitPSXGen(builder, loc, qubit, targetMatrix, theta, phi, lambda, - /*allowXShortcut=*/true, simplify); + qubit = emitPSXGen(builder, loc, qubit, targetMatrix, theta, phi, lambda); break; } - // `anglesFromUnitary` returns a phase term for exact reconstruction; some - // emission modes compute the phase from matrices directly, but for bases that - // already incorporate it we keep it available for debugging parity. (void)phase; return qubit; } diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp index 4b05df72ad..4a239685f1 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp @@ -85,9 +85,8 @@ struct FuseSingleQubitUnitaryRunsPass final const auto parsed = decomposition::parseEulerBasis(this->basis); if (!parsed) { - module.emitError() - << "Invalid Euler basis '" << this->basis - << "'. Expected one of: zyz, zxz, xzx, xyx, u, zsx, zsxx."; + module.emitError() << "Invalid Euler basis '" << this->basis + << "'. Expected one of: zyz, zxz, xzx, xyx, u, zsxx."; signalPassFailure(); return; } diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp index 1110d24e18..d217e7d08e 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -35,7 +35,6 @@ #include #include -#include #include #include #include @@ -96,14 +95,12 @@ template 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; + return qMatrix * dMatrix; } template static void forEachBasis(Fn fn) { - const std::array bases = {"zyz", "zxz", "xzx", "xyx", - "u", "zsx", "zsxx"}; + const std::array bases = {"zyz", "zxz", "xzx", + "xyx", "u", "zsxx"}; for (const char* basis : bases) { fn(StringRef{basis}); } @@ -131,9 +128,6 @@ static bool isAllowedBasisGate(Operation& op, StringRef basis) { if (b == "u") { return isa(op); } - if (b == "zsx") { - return isa(op); - } if (b == "zsxx") { return isa(op); } @@ -313,7 +307,7 @@ template [[nodiscard]] static SynthesizedCircuit synthesizeMatrix(MLIRContext* ctx, const Eigen::Matrix2cd& matrix, - EulerBasis basis, bool simplify) { + EulerBasis basis) { OwningOpRef module = ModuleOp::create(UnknownLoc::get(ctx)); OpBuilder builder(ctx); builder.setInsertionPointToStart(module->getBody()); @@ -325,8 +319,7 @@ synthesizeMatrix(MLIRContext* ctx, const Eigen::Matrix2cd& matrix, builder.setInsertionPointToStart(entry); Value q = entry->getArgument(0); - q = synthesizeUnitary1QEuler(builder, module->getLoc(), q, matrix, basis, - simplify); + q = synthesizeUnitary1QEuler(builder, module->getLoc(), q, matrix, basis); builder.create(module->getLoc(), q); return SynthesizedCircuit{.module = std::move(module), .func = func}; } @@ -348,12 +341,6 @@ template return count; } -[[nodiscard]] static std::size_t countUnitaryMatrixOps(func::FuncOp funcOp) { - std::size_t count = 0; - funcOp.walk([&](UnitaryMatrixOpInterface) { ++count; }); - return count; -} - TEST(EulerSynthesisTest, RandomReconstructionAllBases) { SynthesisFixture fx; fx.setUp(); @@ -410,21 +397,17 @@ TEST(FuseSingleQubitUnitaryRunsTest, ReconstructsOriginalRunAllBases) { }); } -TEST(EulerSynthesisTest, ZsxxPauliXUsesSingleXGate) { +TEST(EulerSynthesisTest, ZsxxPauliXUsesXGateShortcut) { SynthesisFixture fx; fx.setUp(); const Eigen::Matrix2cd pauliX = XOp::getUnitaryMatrix(); const auto circuit = - synthesizeMatrix(fx.context.get(), pauliX, EulerBasis::ZSXX, - /*simplify=*/true); + synthesizeMatrix(fx.context.get(), pauliX, EulerBasis::ZSXX); ASSERT_TRUE(succeeded(verify(*circuit.module))); - EXPECT_EQ(countUnitaryMatrixOps(circuit.func), 1U); EXPECT_EQ(countOps(circuit.func), 1U); - EXPECT_EQ(countOps(circuit.func), 0U); EXPECT_EQ(countOps(circuit.func), 0U); - EXPECT_EQ(countOps(circuit.func), 0U); EXPECT_TRUE(compute1QMatrixFromFunction(circuit.func) .isApprox(pauliX, mlir::utils::TOLERANCE)); } @@ -436,8 +419,7 @@ TEST(EulerSynthesisTest, UGateReconstruction) { std::mt19937 rng{99991}; for (int i = 0; i < 32; ++i) { const auto u = randomUnitaryMatrix(rng); - const auto circuit = synthesizeMatrix(fx.context.get(), u, EulerBasis::U, - /*simplify=*/true); + const auto circuit = synthesizeMatrix(fx.context.get(), u, EulerBasis::U); ASSERT_TRUE(succeeded(verify(*circuit.module))); EXPECT_LE(countOps(circuit.func), 1U); EXPECT_TRUE(compute1QMatrixFromFunction(circuit.func) @@ -489,14 +471,13 @@ class EulerSynthesisExactTest : public testing::TestWithParam< std::tuple> {}; -TEST_P(EulerSynthesisExactTest, WithoutSimplification) { +TEST_P(EulerSynthesisExactTest, ReconstructsReferenceMatrices) { SynthesisFixture fx; fx.setUp(); const auto [basis, matrixFn] = GetParam(); const Eigen::Matrix2cd original = matrixFn(fx.context.get()); - const auto circuit = synthesizeMatrix(fx.context.get(), original, basis, - /*simplify=*/false); + const auto circuit = synthesizeMatrix(fx.context.get(), original, basis); ASSERT_TRUE(succeeded(verify(*circuit.module))); EXPECT_TRUE(compute1QMatrixFromFunction(circuit.func) From 5e2cbdc95c760f71e297c13522506a724e0dae25 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 3 Jun 2026 10:36:15 +0200 Subject: [PATCH 18/68] =?UTF-8?q?=F0=9F=8E=A8=20Refactor=20Euler=20decompo?= =?UTF-8?q?sition=20operations=20in=20QCO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Removed unnecessary constant creation functions and replaced them with direct calls to operation constructors for RZ, RY, RX, U, SX, and X operations. - Updated the `accumulateEmittedMatrix` function to accept an interface directly, simplifying the code. - Renamed the `emitRzAsP` function to `emitCanonicalRZ` for clarity and consistency. - Cleaned up code for better readability and maintainability. --- .../QCO/Transforms/Decomposition/Euler.cpp | 57 ++++++------------- 1 file changed, 18 insertions(+), 39 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index d44ba189cb..ab7a19a287 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include #include @@ -138,18 +137,11 @@ EulerDecomposition::paramsXZX(const Eigen::Matrix2cd& matrix) { // Euler synthesis (IR emission) //===----------------------------------------------------------------------===// -static Value getOrCreateF64Constant(OpBuilder& builder, Location loc, - double value) { - return arith::ConstantOp::create(builder, loc, - builder.getF64FloatAttr(value)); -} - static void emitGPhaseIfNeeded(OpBuilder& builder, Location loc, double phase) { if (std::abs(phase) <= mlir::utils::TOLERANCE) { return; } - auto phaseVal = getOrCreateF64Constant(builder, loc, phase); - builder.create(loc, phaseVal); + GPhaseOp::create(builder, loc, phase); } static double phaseToMatchTarget(const Eigen::Matrix2cd& target, @@ -159,15 +151,10 @@ static double phaseToMatchTarget(const Eigen::Matrix2cd& target, return std::arg(s); } -static void accumulateEmittedMatrix(Operation* op, Eigen::Matrix2cd& emitted) { - auto iface = dyn_cast(op); - if (!iface) { - llvm::reportFatalInternalError( - "Expected emitted op to implement UnitaryMatrixOpInterface"); - } - +static void accumulateEmittedMatrix(UnitaryMatrixOpInterface op, + Eigen::Matrix2cd& emitted) { Eigen::Matrix2cd m; - if (!iface.getUnitaryMatrix2x2(m)) { + if (!op.getUnitaryMatrix2x2(m)) { llvm::reportFatalInternalError( "Expected emitted 1q op to have constant unitary matrix"); } @@ -178,48 +165,42 @@ static void accumulateEmittedMatrix(Operation* op, Eigen::Matrix2cd& emitted) { static Value emitRZ(OpBuilder& builder, Location loc, Value qubit, double angle, Eigen::Matrix2cd& emitted) { - auto v = getOrCreateF64Constant(builder, loc, angle); - auto op = builder.create(loc, qubit, v); + auto op = RZOp::create(builder, loc, qubit, angle); accumulateEmittedMatrix(op, emitted); return op.getQubitOut(); } static Value emitRY(OpBuilder& builder, Location loc, Value qubit, double angle, Eigen::Matrix2cd& emitted) { - auto v = getOrCreateF64Constant(builder, loc, angle); - auto op = builder.create(loc, qubit, v); + auto op = RYOp::create(builder, loc, qubit, angle); accumulateEmittedMatrix(op, emitted); return op.getQubitOut(); } static Value emitRX(OpBuilder& builder, Location loc, Value qubit, double angle, Eigen::Matrix2cd& emitted) { - auto v = getOrCreateF64Constant(builder, loc, angle); - auto op = builder.create(loc, qubit, v); + auto op = RXOp::create(builder, loc, qubit, angle); accumulateEmittedMatrix(op, emitted); return op.getQubitOut(); } static Value emitSX(OpBuilder& builder, Location loc, Value qubit, Eigen::Matrix2cd& emitted) { - auto op = builder.create(loc, qubit); + auto op = SXOp::create(builder, loc, qubit); accumulateEmittedMatrix(op, emitted); return op.getQubitOut(); } static Value emitX(OpBuilder& builder, Location loc, Value qubit, Eigen::Matrix2cd& emitted) { - auto op = builder.create(loc, qubit); + auto op = XOp::create(builder, loc, qubit); accumulateEmittedMatrix(op, emitted); return op.getQubitOut(); } static Value emitU(OpBuilder& builder, Location loc, Value qubit, double theta, double phi, double lambda, Eigen::Matrix2cd& emitted) { - auto thetaV = getOrCreateF64Constant(builder, loc, theta); - auto phiV = getOrCreateF64Constant(builder, loc, phi); - auto lambdaV = getOrCreateF64Constant(builder, loc, lambda); - auto op = builder.create(loc, qubit, thetaV, phiV, lambdaV); + auto op = UOp::create(builder, loc, qubit, theta, phi, lambda); accumulateEmittedMatrix(op, emitted); return op.getQubitOut(); } @@ -248,6 +229,7 @@ static Value emitKAK(OpBuilder& builder, Location loc, Value qubit, auto emitA = [&](double a) { switch (basis) { case EulerBasis::ZYZ: + case EulerBasis::XYX: qubit = emitRY(builder, loc, qubit, a, emitted); break; case EulerBasis::ZXZ: @@ -256,9 +238,6 @@ static Value emitKAK(OpBuilder& builder, Location loc, Value qubit, case EulerBasis::XZX: qubit = emitRZ(builder, loc, qubit, a, emitted); break; - case EulerBasis::XYX: - qubit = emitRY(builder, loc, qubit, a, emitted); - break; default: llvm::reportFatalInternalError("Invalid A gate for KAK emission"); } @@ -277,15 +256,15 @@ static Value emitPSXGen(OpBuilder& builder, Location loc, Value qubit, constexpr double eps = mlir::utils::TOLERANCE; Eigen::Matrix2cd emitted = Eigen::Matrix2cd::Identity(); - auto emitRzAsP = [&](double angle) { + auto emitCanonicalRZ = [&](double angle) { const double canonicalAngle = helpers::mod2pi(angle, eps); qubit = emitRZ(builder, loc, qubit, canonicalAngle, emitted); }; if (std::abs(theta - (std::numbers::pi / 2.0)) < eps) { - emitRzAsP(lambda - (std::numbers::pi / 2.0)); + emitCanonicalRZ(lambda - (std::numbers::pi / 2.0)); qubit = emitSX(builder, loc, qubit, emitted); - emitRzAsP(phi + (std::numbers::pi / 2.0)); + emitCanonicalRZ(phi + (std::numbers::pi / 2.0)); emitGPhaseIfNeeded(builder, loc, phaseToMatchTarget(targetMatrix, emitted)); return qubit; } @@ -306,18 +285,18 @@ static Value emitPSXGen(OpBuilder& builder, Location loc, Value qubit, theta += std::numbers::pi; phi += std::numbers::pi; - emitRzAsP(lambda); + emitCanonicalRZ(lambda); // `sx · rz(θ) · sx` equals `x` when θ ≡ 0 (mod 2π). if (std::abs(helpers::mod2pi(theta, eps)) < eps) { qubit = emitX(builder, loc, qubit, emitted); } else { qubit = emitSX(builder, loc, qubit, emitted); - emitRzAsP(theta); + emitCanonicalRZ(theta); qubit = emitSX(builder, loc, qubit, emitted); } - emitRzAsP(phi); + emitCanonicalRZ(phi); emitGPhaseIfNeeded(builder, loc, phaseToMatchTarget(targetMatrix, emitted)); return qubit; } @@ -348,7 +327,7 @@ std::optional parseEulerBasis(StringRef basis) { Value synthesizeUnitary1QEuler(OpBuilder& builder, Location loc, Value qubit, const Eigen::Matrix2cd& targetMatrix, EulerBasis basis) { - const auto [theta, phi, lambda, /*phase=*/phase] = + const auto [theta, phi, lambda, phase] = EulerDecomposition::anglesFromUnitary(targetMatrix, basis); switch (basis) { From 8eb937cccaae8adf29a96418611b5b6059326496 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 3 Jun 2026 10:40:10 +0200 Subject: [PATCH 19/68] =?UTF-8?q?=F0=9F=8E=A8=20Refactor=20matrix=20handli?= =?UTF-8?q?ng=20and=20wire=20start=20collection=20in=20QCO=20transformatio?= =?UTF-8?q?ns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FuseSingleQubitUnitaryRuns.cpp | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp index 4a239685f1..a28afef762 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp @@ -43,12 +43,12 @@ static bool isFuseCandidate(UnitaryOpInterface op) { } static std::optional getConstMatrix(UnitaryOpInterface op) { - if (!isa(op.getOperation())) { + auto matrixOp = dyn_cast(op.getOperation()); + if (!matrixOp) { return std::nullopt; } Eigen::Matrix2cd m; - if (!cast(op.getOperation()) - .getUnitaryMatrix2x2(m)) { + if (!matrixOp.getUnitaryMatrix2x2(m)) { return std::nullopt; } return m; @@ -92,17 +92,18 @@ struct FuseSingleQubitUnitaryRunsPass final } SmallVector wireStarts; - module.walk([&](AllocOp op) { wireStarts.push_back(op.getResult()); }); - module.walk([&](StaticOp op) { wireStarts.push_back(op.getQubit()); }); - module.walk( - [&](qtensor::ExtractOp op) { wireStarts.push_back(op.getResult()); }); + module.walk([&](AllocOp op) { wireStarts.emplace_back(op.getResult()); }); + module.walk([&](StaticOp op) { wireStarts.emplace_back(op.getQubit()); }); + module.walk([&](qtensor::ExtractOp op) { + wireStarts.emplace_back(op.getResult()); + }); module.walk([&](func::FuncOp func) { if (func.empty()) { return; } for (BlockArgument arg : func.getBody().front().getArguments()) { if (isa(arg.getType())) { - wireStarts.push_back(arg); + wireStarts.emplace_back(arg); } } }); @@ -113,7 +114,7 @@ struct FuseSingleQubitUnitaryRunsPass final auto flushRun = [&](SmallVector& current) { if (current.size() > 1) { - runs.push_back(std::move(current)); + runs.emplace_back(std::move(current)); current = SmallVector(); } else { current.clear(); @@ -156,7 +157,7 @@ struct FuseSingleQubitUnitaryRunsPass final continue; } - current.push_back(iface); + current.emplace_back(iface); seen.insert(op); } From e287b803f8b72f5cb73a3ef330e6c043a03ff069 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 3 Jun 2026 18:40:52 +0200 Subject: [PATCH 20/68] =?UTF-8?q?=F0=9F=8E=A8=20Enhance=20Euler=20decompos?= =?UTF-8?q?ition=20in=20QCO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/Decomposition/Euler.h | 192 +++--- .../QCO/Transforms/Decomposition/Euler.cpp | 546 +++++++++++------- .../FuseSingleQubitUnitaryRuns.cpp | 1 + .../test_euler_decomposition.cpp | 44 +- 4 files changed, 444 insertions(+), 339 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h index 9ae931b463..dbcc78121f 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h @@ -11,143 +11,133 @@ #pragma once #include -#include #include #include #include -#include -#include #include -#include #include namespace mlir::qco::decomposition { + /** - * Target gate set for single-qubit Euler synthesis. - * - * - **KAK bases** (`ZYZ`, `ZXZ`, `XZX`, `XYX`): Euler decompositions named by - * the middle and outer rotation axes. - * - **`U`**: single `u(theta, phi, lambda)` gate (angles from the Z-Y-Z form). - * - **`ZSXX`**: `rz · sx · rz · sx · rz`, or `rz · x · rz` when the middle - * angle is ~0. + * @brief Euler angles `(theta, phi, lambda)` and global phase for a 2x2 + * unitary. */ -enum class EulerBasis : std::uint8_t { - ZYZ = 0, ///< `rz · ry · rz`. - ZXZ = 1, ///< `rz · rx · rz`. - XZX = 2, ///< `rx · rz · rx`. - XYX = 3, ///< `rx · ry · rx`. - U = 4, ///< `u(theta, phi, lambda)`. - ZSXX = 5, ///< `rz · sx · rz · sx · rz`, or `rz · x · rz` if middle angle ~0. +struct EulerAngles { + double theta = 0.0; + double phi = 0.0; + double lambda = 0.0; + double phase = 0.0; }; -} // namespace mlir::qco::decomposition - -// NOTE: We keep the small numeric helpers in this header because Euler -// decomposition/synthesis is the only current user and we want to avoid -// scattering tiny utilities across multiple files. -namespace mlir::qco::helpers { - /** - * Wrap angle into interval [-pi, pi). If within atol of the endpoint, clamp to - * -pi. + * @brief Native gate sets for single-qubit Euler synthesis. */ -[[nodiscard]] inline double mod2pi(double angle, - double atol = mlir::utils::TOLERANCE) { - // Wrap angle into the half-open interval [-pi, pi). - // For non-finite values, keep the original (caller error / upstream issue). - if (!std::isfinite(angle)) { - return angle; - } - - constexpr double pi = std::numbers::pi; - constexpr double twoPi = 2.0 * std::numbers::pi; - - // Euclidean remainder of (angle + pi) modulo 2pi, then shift back by pi. - // This ensures correct wrapping for negative angles as well. - double r = std::fmod(angle + pi, twoPi); - if (r < 0.0) { - r += twoPi; - } - double wrapped = r - pi; - - // Canonicalize the upper endpoint back to -pi so callers always receive a - // half-open interval [-pi, pi). We use an epsilon guard since rounding can - // produce wrapped ~= +pi. - if (wrapped >= pi - atol) { - wrapped = -pi; - } - - return wrapped; -} - -} // namespace mlir::qco::helpers - -namespace mlir::qco::decomposition { +enum class EulerBasis : std::uint8_t { + ZYZ = 0, ///< `RZ(phi) * RY(theta) * RZ(lambda)`. + ZXZ = 1, ///< `RZ(phi) * RX(theta) * RZ(lambda)`. + XZX = 2, ///< `RX(phi) * RZ(theta) * RX(lambda)`. + XYX = 3, ///< `RX(phi) * RY(theta) * RX(lambda)`. + U = 4, ///< `U(theta, phi, lambda)`. + ZSXX = + 5, ///< ZYZ-equivalent chain over `RZ`, `SX`, and `X` (see `paramsPSX`). +}; -/// Extract Euler parameters from single-qubit unitary matrices. +/** + * @brief Extracts Euler parameters from single-qubit unitary matrices. + */ class EulerDecomposition { + friend Value synthesizeUnitary1QEuler(OpBuilder& builder, Location loc, + Value qubit, + const Eigen::Matrix2cd& targetMatrix, + EulerBasis basis); + public: /** - * Extract canonical Euler parameters for `matrix` in the requested basis. + * @brief Extracts `(theta, phi, lambda, phase)` for 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. + * @param matrix The single-qubit unitary to decompose. + * @param basis The target Euler basis. + * @return The extracted Euler angles and global phase. */ - [[nodiscard]] static std::array + [[nodiscard]] static EulerAngles 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); + /** + * @brief Extracts parameters for `RZ(phi) * RY(theta) * RZ(lambda)`. + * + * @param matrix The single-qubit unitary to decompose. + * @return The extracted Euler angles and global phase. + */ + [[nodiscard]] static EulerAngles paramsZYZ(const Eigen::Matrix2cd& matrix); - /// Extract parameters for a `rx(phi) · ry(theta) · rx(lambda)` factorization. - [[nodiscard]] static std::array - paramsXYX(const Eigen::Matrix2cd& matrix); + /** + * @brief Extracts parameters for a `U(theta, phi, lambda)` factorization. + * + * @param matrix The single-qubit unitary to decompose. + * @return The extracted Euler angles and global phase. + */ + [[nodiscard]] static EulerAngles paramsU(const Eigen::Matrix2cd& matrix); - /// Extract parameters for a `rx(phi) · rz(theta) · rx(lambda)` factorization. - [[nodiscard]] static std::array - paramsXZX(const Eigen::Matrix2cd& matrix); + /** + * @brief Extracts parameters for `RZ(phi) * RX(theta) * RZ(lambda)`. + * + * @param matrix The single-qubit unitary to decompose. + * @return The extracted Euler angles and global phase. + */ + [[nodiscard]] static EulerAngles paramsZXZ(const Eigen::Matrix2cd& matrix); /** - * Extract Euler angles for `ZSXX` (`rz` / `sx`) synthesis. + * @brief Extracts parameters for `RX(phi) * RY(theta) * RX(lambda)`. * - * Reuses `(theta, phi, lambda)` from `paramsZYZ` and sets the scalar phase to - * `phase - 0.5 * (theta + phi + lambda)` so `emitPSXGen` reproduces `matrix` - * exactly, including the global phase induced by the `rz`/`sx` - * parameterization. + * @param matrix The single-qubit unitary to decompose. + * @return The extracted Euler angles and global phase. + */ + [[nodiscard]] static EulerAngles paramsXYX(const Eigen::Matrix2cd& matrix); + + /** + * @brief Extracts parameters for `RX(phi) * RZ(theta) * RX(lambda)`. * - * @note Adapted from `params_u1x_inner` in the IBM Qiskit framework. - * (C) Copyright IBM 2022 + * @param matrix The single-qubit unitary to decompose. + * @return The extracted Euler angles and global phase. + */ + [[nodiscard]] static EulerAngles paramsXZX(const Eigen::Matrix2cd& matrix); + + /** + * @brief Extracts ZYZ-equivalent angles and global phase for `ZSXX` + * synthesis. * - * 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. + * Returns the same `(theta, phi, lambda)` as `paramsZYZ`; `phase` includes + * the offset to the native `RZ`/`SX`/`X` chain emitted for `ZSXX`. * - * Any modifications or derivative works of this code must retain this - * copyright notice, and modified files need to carry a notice - * indicating that they have been altered from the originals. + * @param matrix The single-qubit unitary to decompose. + * @return The extracted Euler angles and global phase. */ - [[nodiscard]] static std::array - paramsPSX(const Eigen::Matrix2cd& matrix); + [[nodiscard]] static EulerAngles paramsPSX(const Eigen::Matrix2cd& matrix); }; -/// Parse a user-facing basis string (e.g. "zyz", "zsxx") into an Euler basis. +/** + * @brief Parses a user-facing basis string (e.g. "zyz", "zsxx"). + * + * @param basis The basis name (case-insensitive). + * @return The parsed Euler basis, or `std::nullopt` if unrecognized. + */ [[nodiscard]] std::optional parseEulerBasis(StringRef basis); -/// Emit an Euler-basis gate sequence implementing `targetMatrix` on `qubit`. -/// Returns the output qubit value. -/// -/// The emitted circuit includes a `qco.gphase` correction when needed so the -/// overall unitary matches `targetMatrix` exactly (not only up to global -/// phase). +/** + * @brief Emits gates reconstructing `targetMatrix` in the given basis. + * + * Includes a global phase (`qco.gphase`) when needed for an exact match. + * + * @param builder Builder used to create the operations. + * @param loc Source location for the created operations. + * @param qubit Input qubit value. + * @param targetMatrix The single-qubit unitary to synthesize. + * @param basis The target Euler basis. + * @return The transformed qubit value. + */ [[nodiscard]] Value synthesizeUnitary1QEuler(OpBuilder& builder, Location loc, Value qubit, const Eigen::Matrix2cd& targetMatrix, diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index ab7a19a287..a00988133b 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -11,215 +11,281 @@ #include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" -#include "mlir/Dialect/QCO/IR/QCOUnitaryMatrixInterfaces.h" #include "mlir/Dialect/Utils/Utils.h" #include -#include #include #include #include -#include #include #include -#include #include -#include +#include #include #include namespace mlir::qco::decomposition { -//===----------------------------------------------------------------------===// -// Euler decomposition (angles) -//===----------------------------------------------------------------------===// +namespace { -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: - // The `u` gate parameterization is derived from the standard Z-Y-Z form. - return paramsZYZ(matrix); - case EulerBasis::ZSXX: - return paramsPSX(matrix); +/** + * @brief Wraps `angle` into `[-pi, pi)`, mapping `+pi` (within `atol`) to + * `-pi`. + * + * @param angle The angle to wrap, in radians. + * @param atol Absolute tolerance for snapping `+pi` to `-pi`. + * @return The wrapped angle in `[-pi, pi)`. + */ +[[nodiscard]] double mod2pi(double angle, + double atol = mlir::utils::TOLERANCE) { + if (!std::isfinite(angle)) { + return angle; } - 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 lambda = ang1 - ang2; - return {theta, phi, lambda, phase}; -} + constexpr double pi = std::numbers::pi; + constexpr double twoPi = 2.0 * std::numbers::pi; -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, lambda, phase] = paramsZYZ(matrix); - return {theta, phi + (std::numbers::pi / 2.0), - lambda - (std::numbers::pi / 2.0), phase}; -} + double r = std::fmod(angle + pi, twoPi); + if (r < 0.0) { + r += twoPi; + } + double wrapped = r - pi; -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, lambda, phase] = paramsZYZ(matZYZ); - auto newPhi = helpers::mod2pi(phi + std::numbers::pi, 0.); - auto newLambda = helpers::mod2pi(lambda + std::numbers::pi, 0.); - return { - theta, - newPhi, - newLambda, - phase + ((newPhi + newLambda - phi - lambda) / 2.), - }; -} + if (wrapped >= pi - atol) { + wrapped = -pi; + } -std::array -EulerDecomposition::paramsPSX(const Eigen::Matrix2cd& matrix) { - const auto [theta, phi, lambda, phase] = paramsZYZ(matrix); - return {theta, phi, lambda, phase - (0.5 * (theta + phi + lambda))}; + return wrapped; } -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, lambda, phaseZXZ] = paramsZXZ(matZXZ); - return {theta, phi, lambda, phase + phaseZXZ}; +/** + * @brief Conjugates a single-qubit matrix by Hadamard (`H * m * H`). + * + * Maps X-Y-X / X-Z-X decompositions to Z-Y-Z / Z-X-Z. + * + * @param m The single-qubit matrix to conjugate. + * @return `H * m * H`. + */ +[[nodiscard]] Eigen::Matrix2cd hadamardConjugate(const Eigen::Matrix2cd& m) { + const auto a = m(0, 0); + const auto b = m(0, 1); + const auto c = m(1, 0); + const auto d = m(1, 1); + return Eigen::Matrix2cd{{0.5 * (a + b + c + d), 0.5 * (a - b + c - d)}, + {0.5 * (a + b - c - d), 0.5 * (a - b - c + d)}}; } -//===----------------------------------------------------------------------===// -// Euler synthesis (IR emission) -//===----------------------------------------------------------------------===// - -static void emitGPhaseIfNeeded(OpBuilder& builder, Location loc, double phase) { +/** + * @brief Emits a `GPhaseOp` when `phase` is non-negligible. + * + * @param builder Builder used to create the operation. + * @param loc Source location for the created operation. + * @param phase Global phase in radians; skipped when within tolerance of zero. + */ +void emitGPhaseIfNeeded(OpBuilder& builder, Location loc, double phase) { if (std::abs(phase) <= mlir::utils::TOLERANCE) { return; } GPhaseOp::create(builder, loc, phase); } -static double phaseToMatchTarget(const Eigen::Matrix2cd& target, - const Eigen::Matrix2cd& emitted) { - // If `target == s * emitted`, then `target * emitted^H == s * I`. - const auto s = (target * emitted.adjoint())(0, 0); - return std::arg(s); -} - -static void accumulateEmittedMatrix(UnitaryMatrixOpInterface op, - Eigen::Matrix2cd& emitted) { - Eigen::Matrix2cd m; - if (!op.getUnitaryMatrix2x2(m)) { - llvm::reportFatalInternalError( - "Expected emitted 1q op to have constant unitary matrix"); +/** + * @brief Planned RZ-middle-RZ chain; fields are angles in circuit (time) order. + */ +struct PSXSequence { + enum class Middle : std::uint8_t { OneSX, X, SXRZSX }; + Middle middle = Middle::SXRZSX; + double firstRZ = 0.0; + double midRZ = 0.0; + double lastRZ = 0.0; +}; + +/** + * @brief Builds the RZ/SX chain realizing `RZ(phi)*RY(theta)*RZ(lambda)`. + * + * Uses the identity `SX*RZ(theta+pi)*SX = Z*RY(theta)`. `theta` from + * `paramsZYZ` lies in `[0, pi]`: `pi/2` collapses to a single SX; `pi` becomes + * an X gate (since `SX*SX = X`). + * + * @param theta Y-rotation angle in `[0, pi]`. + * @param phi Trailing Z-rotation angle. + * @param lambda Leading Z-rotation angle. + * @return The planned PSX sequence. + */ +[[nodiscard]] PSXSequence sequenceFromZYZForPSX(double theta, double phi, + double lambda) { + constexpr double eps = mlir::utils::TOLERANCE; + constexpr double halfPi = std::numbers::pi / 2.0; + constexpr double pi = std::numbers::pi; + + if (std::abs(theta - halfPi) < eps) { + return {.middle = PSXSequence::Middle::OneSX, + .firstRZ = lambda - halfPi, + .midRZ = 0.0, + .lastRZ = phi + halfPi}; } - - // Execution order: first emitted op applied first => multiply on the left. - emitted = m * emitted; + if (std::abs(theta - pi) < eps) { + return {.middle = PSXSequence::Middle::X, + .firstRZ = lambda, + .midRZ = 0.0, + .lastRZ = phi + pi}; + } + return {.middle = PSXSequence::Middle::SXRZSX, + .firstRZ = lambda, + .midRZ = theta + pi, + .lastRZ = phi + pi}; } -static Value emitRZ(OpBuilder& builder, Location loc, Value qubit, double angle, - Eigen::Matrix2cd& emitted) { - auto op = RZOp::create(builder, loc, qubit, angle); - accumulateEmittedMatrix(op, emitted); - return op.getQubitOut(); +/** + * @brief Global phase between `UOp(theta, phi, lambda)` and the ZYZ product. + * + * Relates `UOp` to `RZ(phi)*RY(theta)*RZ(lambda)` on the same angles. + * + * @param phi The `phi` Euler angle. + * @param lambda The `lambda` Euler angle. + * @return The global-phase offset in radians. + */ +[[nodiscard]] double globalPhaseOffsetForU(double phi, double lambda) { + return -0.5 * (phi + lambda); } -static Value emitRY(OpBuilder& builder, Location loc, Value qubit, double angle, - Eigen::Matrix2cd& emitted) { - auto op = RYOp::create(builder, loc, qubit, angle); - accumulateEmittedMatrix(op, emitted); - return op.getQubitOut(); +/** + * @brief Global phase contributed by wrapping an RZ angle with `mod2pi`. + * + * `mod2pi(angle) - angle` is a multiple of `2*pi`, so the emitted + * `RZ(mod2pi(angle))` equals `exp(i*(mod2pi(angle)-angle)/2) * RZ(angle)`. + * + * @param angle The unwrapped RZ angle. + * @return The global-phase contribution in radians. + */ +[[nodiscard]] double globalPhaseFromRZWrap(double angle) { + constexpr double eps = mlir::utils::TOLERANCE; + return 0.5 * (mod2pi(angle, eps) - angle); } -static Value emitRX(OpBuilder& builder, Location loc, Value qubit, double angle, - Eigen::Matrix2cd& emitted) { - auto op = RXOp::create(builder, loc, qubit, angle); - accumulateEmittedMatrix(op, emitted); - return op.getQubitOut(); +/** + * @brief Global phase between the ZYZ product and the emitted PSX product. + * + * @param seq The planned PSX sequence. + * @return The global-phase offset in radians. + */ +[[nodiscard]] double globalPhaseOffsetForPSX(const PSXSequence& seq) { + constexpr double halfPi = std::numbers::pi / 2.0; + constexpr double quarterPi = std::numbers::pi / 4.0; + + switch (seq.middle) { + case PSXSequence::Middle::OneSX: + // `SX = exp(i*pi/4)*RZ(-pi/2)*RY(pi/2)*RZ(pi/2)`; the outer RZ angles + // absorb the +-pi/2, leaving the exp(i*pi/4) phase. RZ wraps add too. + return -quarterPi + globalPhaseFromRZWrap(seq.firstRZ) + + globalPhaseFromRZWrap(seq.lastRZ); + case PSXSequence::Middle::X: + // `X` swaps the diagonal, so the wraps enter with opposite signs. + return -halfPi + globalPhaseFromRZWrap(seq.lastRZ) - + globalPhaseFromRZWrap(seq.firstRZ); + case PSXSequence::Middle::SXRZSX: + // `SX*RZ(theta+pi)*SX = Z*RY(theta)`; all three RZ wraps add. + return halfPi + globalPhaseFromRZWrap(seq.firstRZ) + + globalPhaseFromRZWrap(seq.midRZ) + globalPhaseFromRZWrap(seq.lastRZ); + } + llvm::reportFatalInternalError("Unhandled PSX middle gate"); } -static Value emitSX(OpBuilder& builder, Location loc, Value qubit, - Eigen::Matrix2cd& emitted) { - auto op = SXOp::create(builder, loc, qubit); - accumulateEmittedMatrix(op, emitted); - return op.getQubitOut(); +/** + * @brief Global phase between the ZYZ product and the emitted PSX product. + * + * @param theta Y-rotation angle from `paramsZYZ`. + * @param phi Trailing Z-rotation angle from `paramsZYZ`. + * @param lambda Leading Z-rotation angle from `paramsZYZ`. + * @return The global-phase offset in radians. + */ +[[nodiscard]] double globalPhaseOffsetForPSX(double theta, double phi, + double lambda) { + return globalPhaseOffsetForPSX(sequenceFromZYZForPSX(theta, phi, lambda)); } -static Value emitX(OpBuilder& builder, Location loc, Value qubit, - Eigen::Matrix2cd& emitted) { - auto op = XOp::create(builder, loc, qubit); - accumulateEmittedMatrix(op, emitted); - return op.getQubitOut(); +/** + * @brief Invokes callbacks for each gate of `seq` in circuit (time) order. + * + * @param seq The planned PSX sequence. + * @param onRZ Called with each RZ angle. + * @param onSX Called for each SX gate. + * @param onX Called for each X gate. + */ +void visitSequenceInTimeOrder(const PSXSequence& seq, + llvm::function_ref onRZ, + llvm::function_ref onSX, + llvm::function_ref onX) { + onRZ(seq.firstRZ); + switch (seq.middle) { + case PSXSequence::Middle::OneSX: + onSX(); + onRZ(seq.lastRZ); + break; + case PSXSequence::Middle::X: + onX(); + onRZ(seq.lastRZ); + break; + case PSXSequence::Middle::SXRZSX: + onSX(); + onRZ(seq.midRZ); + onSX(); + onRZ(seq.lastRZ); + break; + } } -static Value emitU(OpBuilder& builder, Location loc, Value qubit, double theta, - double phi, double lambda, Eigen::Matrix2cd& emitted) { - auto op = UOp::create(builder, loc, qubit, theta, phi, lambda); - accumulateEmittedMatrix(op, emitted); - return op.getQubitOut(); +/** + * @brief Emits the RZ/SX/X gates of `seq` followed by the global phase. + * + * @param builder Builder used to create the operations. + * @param loc Source location for the created operations. + * @param qubit Input qubit value. + * @param seq The planned PSX sequence. + * @param phase Global phase to emit, in radians. + * @return The transformed qubit value. + */ +[[nodiscard]] Value emitFromPSXSequence(OpBuilder& builder, Location loc, + Value qubit, const PSXSequence& seq, + double phase) { + constexpr double eps = mlir::utils::TOLERANCE; + visitSequenceInTimeOrder( + seq, + [&](const double angle) { + qubit = + RZOp::create(builder, loc, qubit, mod2pi(angle, eps)).getQubitOut(); + }, + [&] { qubit = SXOp::create(builder, loc, qubit).getQubitOut(); }, + [&] { qubit = XOp::create(builder, loc, qubit).getQubitOut(); }); + emitGPhaseIfNeeded(builder, loc, phase); + return qubit; } -static Value emitKAK(OpBuilder& builder, Location loc, Value qubit, - const Eigen::Matrix2cd& targetMatrix, double theta, - double phi, double lambda, EulerBasis basis) { - Eigen::Matrix2cd emitted = Eigen::Matrix2cd::Identity(); - +/** + * @brief Emits a K-A-K rotation triple plus global phase for `basis`. + * + * @param builder Builder used to create the operations. + * @param loc Source location for the created operations. + * @param qubit Input qubit value. + * @param theta Middle (A) rotation angle. + * @param phi Trailing (K) rotation angle. + * @param lambda Leading (K) rotation angle. + * @param phase Global phase to emit, in radians. + * @param basis Euler basis selecting the K and A rotation axes. + * @return The transformed qubit value. + */ +Value emitKAK(OpBuilder& builder, Location loc, Value qubit, double theta, + double phi, double lambda, double phase, EulerBasis basis) { auto emitK = [&](double a) { - const double canonical = helpers::mod2pi(a, mlir::utils::TOLERANCE); switch (basis) { case EulerBasis::ZYZ: case EulerBasis::ZXZ: - qubit = emitRZ(builder, loc, qubit, canonical, emitted); + qubit = RZOp::create(builder, loc, qubit, a).getQubitOut(); break; case EulerBasis::XZX: case EulerBasis::XYX: - qubit = emitRX(builder, loc, qubit, canonical, emitted); + qubit = RXOp::create(builder, loc, qubit, a).getQubitOut(); break; default: llvm::reportFatalInternalError("Invalid K gate for KAK emission"); @@ -230,13 +296,13 @@ static Value emitKAK(OpBuilder& builder, Location loc, Value qubit, switch (basis) { case EulerBasis::ZYZ: case EulerBasis::XYX: - qubit = emitRY(builder, loc, qubit, a, emitted); + qubit = RYOp::create(builder, loc, qubit, a).getQubitOut(); break; case EulerBasis::ZXZ: - qubit = emitRX(builder, loc, qubit, a, emitted); + qubit = RXOp::create(builder, loc, qubit, a).getQubitOut(); break; case EulerBasis::XZX: - qubit = emitRZ(builder, loc, qubit, a, emitted); + qubit = RZOp::create(builder, loc, qubit, a).getQubitOut(); break; default: llvm::reportFatalInternalError("Invalid A gate for KAK emission"); @@ -246,79 +312,116 @@ static Value emitKAK(OpBuilder& builder, Location loc, Value qubit, emitK(lambda); emitA(theta); emitK(phi); - emitGPhaseIfNeeded(builder, loc, phaseToMatchTarget(targetMatrix, emitted)); + emitGPhaseIfNeeded(builder, loc, phase); return qubit; } -static Value emitPSXGen(OpBuilder& builder, Location loc, Value qubit, - const Eigen::Matrix2cd& targetMatrix, double theta, - double phi, double lambda) { - constexpr double eps = mlir::utils::TOLERANCE; - Eigen::Matrix2cd emitted = Eigen::Matrix2cd::Identity(); +} // namespace - auto emitCanonicalRZ = [&](double angle) { - const double canonicalAngle = helpers::mod2pi(angle, eps); - qubit = emitRZ(builder, loc, qubit, canonicalAngle, emitted); - }; +//===----------------------------------------------------------------------===// +// Euler decomposition (angles) +//===----------------------------------------------------------------------===// - if (std::abs(theta - (std::numbers::pi / 2.0)) < eps) { - emitCanonicalRZ(lambda - (std::numbers::pi / 2.0)); - qubit = emitSX(builder, loc, qubit, emitted); - emitCanonicalRZ(phi + (std::numbers::pi / 2.0)); - emitGPhaseIfNeeded(builder, loc, phaseToMatchTarget(targetMatrix, emitted)); - return qubit; +EulerAngles +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: + return paramsU(matrix); + case EulerBasis::ZSXX: + return paramsPSX(matrix); } + llvm::reportFatalInternalError( + "Unsupported Euler basis for angle computation in decomposition!"); +} - // General double-`sx` case: reparameterize angles, then fix global phase from - // the accumulated unitary. - if (std::abs(theta - std::numbers::pi) < eps) { - phi -= lambda; - lambda = 0.0; - } - if (std::abs(helpers::mod2pi(lambda + std::numbers::pi, eps)) < eps || - std::abs(helpers::mod2pi(phi, eps)) < eps) { - lambda += std::numbers::pi; - theta = -theta; - phi += std::numbers::pi; - } +EulerAngles EulerDecomposition::paramsZYZ(const Eigen::Matrix2cd& matrix) { + // det(U) = exp(2i*phase); invert the Z-Y-Z parameterization of U's entries. + const std::complex det = + matrix(0, 0) * matrix(1, 1) - matrix(0, 1) * matrix(1, 0); + const auto detArg = std::arg(det); + 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 lambda = ang1 - ang2; + return {.theta = theta, .phi = phi, .lambda = lambda, .phase = phase}; +} - theta += std::numbers::pi; - phi += std::numbers::pi; +EulerAngles EulerDecomposition::paramsZXZ(const Eigen::Matrix2cd& matrix) { + // ZXZ from ZYZ via RY(theta) = RZ(pi/2)*RX(theta)*RZ(-pi/2). + const auto zyz = paramsZYZ(matrix); + return {.theta = zyz.theta, + .phi = zyz.phi + (std::numbers::pi / 2.0), + .lambda = zyz.lambda - (std::numbers::pi / 2.0), + .phase = zyz.phase}; +} - emitCanonicalRZ(lambda); +EulerAngles EulerDecomposition::paramsXYX(const Eigen::Matrix2cd& matrix) { + // H*RY(theta)*H = RY(-theta): shift outer angles by pi and fix global phase. + const auto zyz = paramsZYZ(hadamardConjugate(matrix)); + const auto newPhi = mod2pi(zyz.phi + std::numbers::pi, 0.); + const auto newLambda = mod2pi(zyz.lambda + std::numbers::pi, 0.); + return {.theta = zyz.theta, + .phi = newPhi, + .lambda = newLambda, + .phase = + zyz.phase + ((newPhi + newLambda - zyz.phi - zyz.lambda) / 2.)}; +} - // `sx · rz(θ) · sx` equals `x` when θ ≡ 0 (mod 2π). - if (std::abs(helpers::mod2pi(theta, eps)) < eps) { - qubit = emitX(builder, loc, qubit, emitted); - } else { - qubit = emitSX(builder, loc, qubit, emitted); - emitCanonicalRZ(theta); - qubit = emitSX(builder, loc, qubit, emitted); - } +EulerAngles EulerDecomposition::paramsXZX(const Eigen::Matrix2cd& matrix) { + // X-Z-X -> Z-X-Z under H conjugation (no Y sign flip, unlike paramsXYX). + return paramsZXZ(hadamardConjugate(matrix)); +} - emitCanonicalRZ(phi); - emitGPhaseIfNeeded(builder, loc, phaseToMatchTarget(targetMatrix, emitted)); - return qubit; +EulerAngles EulerDecomposition::paramsU(const Eigen::Matrix2cd& matrix) { + const auto zyz = paramsZYZ(matrix); + return {.theta = zyz.theta, + .phi = zyz.phi, + .lambda = zyz.lambda, + .phase = zyz.phase + globalPhaseOffsetForU(zyz.phi, zyz.lambda)}; +} + +EulerAngles EulerDecomposition::paramsPSX(const Eigen::Matrix2cd& matrix) { + const auto zyz = paramsZYZ(matrix); + return {.theta = zyz.theta, + .phi = zyz.phi, + .lambda = zyz.lambda, + .phase = zyz.phase + + globalPhaseOffsetForPSX(zyz.theta, zyz.phi, zyz.lambda)}; } +//===----------------------------------------------------------------------===// +// Euler synthesis (IR emission) +//===----------------------------------------------------------------------===// + std::optional parseEulerBasis(StringRef basis) { - const auto b = basis.lower(); - if (b == "zyz") { + if (basis.equals_insensitive("zyz")) { return EulerBasis::ZYZ; } - if (b == "zxz") { + if (basis.equals_insensitive("zxz")) { return EulerBasis::ZXZ; } - if (b == "xzx") { + if (basis.equals_insensitive("xzx")) { return EulerBasis::XZX; } - if (b == "xyx") { + if (basis.equals_insensitive("xyx")) { return EulerBasis::XYX; } - if (b == "u") { + if (basis.equals_insensitive("u")) { return EulerBasis::U; } - if (b == "zsxx") { + if (basis.equals_insensitive("zsxx")) { return EulerBasis::ZSXX; } return std::nullopt; @@ -327,7 +430,14 @@ std::optional parseEulerBasis(StringRef basis) { Value synthesizeUnitary1QEuler(OpBuilder& builder, Location loc, Value qubit, const Eigen::Matrix2cd& targetMatrix, EulerBasis basis) { - const auto [theta, phi, lambda, phase] = + if (basis == EulerBasis::ZSXX) { + const auto zyz = EulerDecomposition::paramsZYZ(targetMatrix); + const auto seq = sequenceFromZYZForPSX(zyz.theta, zyz.phi, zyz.lambda); + return emitFromPSXSequence(builder, loc, qubit, seq, + zyz.phase + globalPhaseOffsetForPSX(seq)); + } + + const auto angles = EulerDecomposition::anglesFromUnitary(targetMatrix, basis); switch (basis) { @@ -335,21 +445,19 @@ Value synthesizeUnitary1QEuler(OpBuilder& builder, Location loc, Value qubit, case EulerBasis::ZXZ: case EulerBasis::XZX: case EulerBasis::XYX: - qubit = - emitKAK(builder, loc, qubit, targetMatrix, theta, phi, lambda, basis); + qubit = emitKAK(builder, loc, qubit, angles.theta, angles.phi, + angles.lambda, angles.phase, basis); break; - case EulerBasis::U: { - Eigen::Matrix2cd emitted = Eigen::Matrix2cd::Identity(); - qubit = emitU(builder, loc, qubit, theta, phi, lambda, emitted); - emitGPhaseIfNeeded(builder, loc, phaseToMatchTarget(targetMatrix, emitted)); + case EulerBasis::U: + qubit = UOp::create(builder, loc, qubit, angles.theta, angles.phi, + angles.lambda) + .getQubitOut(); + emitGPhaseIfNeeded(builder, loc, angles.phase); break; - } case EulerBasis::ZSXX: - qubit = emitPSXGen(builder, loc, qubit, targetMatrix, theta, phi, lambda); - break; + llvm_unreachable("ZSXX handled above"); } - (void)phase; return qubit; } diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp index a28afef762..0911a9917e 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp @@ -18,6 +18,7 @@ #include "mlir/Dialect/QTensor/IR/QTensorOps.h" #include +#include #include #include #include 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 d217e7d08e..aea4ea30ac 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -10,6 +10,7 @@ #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/IR/QCOUnitaryMatrixInterfaces.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" @@ -19,7 +20,6 @@ #include #include #include -#include #include #include #include @@ -39,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -486,24 +487,29 @@ TEST_P(EulerSynthesisExactTest, ReconstructsReferenceMatrices) { INSTANTIATE_TEST_SUITE_P( SingleQubitMatrices, EulerSynthesisExactTest, - testing::Combine(testing::Values(EulerBasis::XYX, EulerBasis::XZX, - EulerBasis::ZYZ, EulerBasis::ZXZ), - testing::Values( - [](MLIRContext* /*ctx*/) -> Eigen::Matrix2cd { - return Eigen::Matrix2cd::Identity(); - }, - [](MLIRContext* ctx) -> Eigen::Matrix2cd { - return rotationMatrix(ctx, 2.0); - }, - [](MLIRContext* ctx) -> Eigen::Matrix2cd { - return rotationMatrix(ctx, 0.5); - }, - [](MLIRContext* ctx) -> Eigen::Matrix2cd { - return rotationMatrix(ctx, 3.14); - }, - [](MLIRContext* /*ctx*/) -> Eigen::Matrix2cd { - return HOp::getUnitaryMatrix(); - }))); + testing::Combine( + testing::Values(EulerBasis::XYX, EulerBasis::XZX, EulerBasis::ZYZ, + EulerBasis::ZXZ, EulerBasis::U, EulerBasis::ZSXX), + testing::Values( + [](MLIRContext* /*ctx*/) -> Eigen::Matrix2cd { + return Eigen::Matrix2cd::Identity(); + }, + [](MLIRContext* ctx) -> Eigen::Matrix2cd { + return rotationMatrix(ctx, 2.0); + }, + // RY(pi/2) hits the ZSXX single-SX branch (theta == pi/2). + [](MLIRContext* ctx) -> Eigen::Matrix2cd { + return rotationMatrix(ctx, std::numbers::pi / 2.0); + }, + [](MLIRContext* ctx) -> Eigen::Matrix2cd { + return rotationMatrix(ctx, 0.5); + }, + [](MLIRContext* ctx) -> Eigen::Matrix2cd { + return rotationMatrix(ctx, 3.14); + }, + [](MLIRContext* /*ctx*/) -> Eigen::Matrix2cd { + return HOp::getUnitaryMatrix(); + }))); TEST(FuseSingleQubitUnitaryRunsTest, DoesNotFuseAcrossTwoQGateAllBases) { SynthesisFixture fx; From 20d5835dea02437e2e27e0382aae099bf3bd8549 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 3 Jun 2026 18:54:12 +0200 Subject: [PATCH 21/68] =?UTF-8?q?=F0=9F=9A=A8=20fix=20clang-tidy=20warning?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/Decomposition/Euler.cpp | 49 ++++++++++--------- .../FuseSingleQubitUnitaryRuns.cpp | 2 +- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index a00988133b..6c75331a4e 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -14,6 +14,7 @@ #include "mlir/Dialect/Utils/Utils.h" #include +#include #include #include #include @@ -21,14 +22,13 @@ #include #include +#include #include #include #include namespace mlir::qco::decomposition { -namespace { - /** * @brief Wraps `angle` into `[-pi, pi)`, mapping `+pi` (within `atol`) to * `-pi`. @@ -37,8 +37,8 @@ namespace { * @param atol Absolute tolerance for snapping `+pi` to `-pi`. * @return The wrapped angle in `[-pi, pi)`. */ -[[nodiscard]] double mod2pi(double angle, - double atol = mlir::utils::TOLERANCE) { +[[nodiscard]] static double mod2pi(double angle, + double atol = mlir::utils::TOLERANCE) { if (!std::isfinite(angle)) { return angle; } @@ -67,7 +67,8 @@ namespace { * @param m The single-qubit matrix to conjugate. * @return `H * m * H`. */ -[[nodiscard]] Eigen::Matrix2cd hadamardConjugate(const Eigen::Matrix2cd& m) { +[[nodiscard]] static Eigen::Matrix2cd +hadamardConjugate(const Eigen::Matrix2cd& m) { const auto a = m(0, 0); const auto b = m(0, 1); const auto c = m(1, 0); @@ -83,7 +84,7 @@ namespace { * @param loc Source location for the created operation. * @param phase Global phase in radians; skipped when within tolerance of zero. */ -void emitGPhaseIfNeeded(OpBuilder& builder, Location loc, double phase) { +static void emitGPhaseIfNeeded(OpBuilder& builder, Location loc, double phase) { if (std::abs(phase) <= mlir::utils::TOLERANCE) { return; } @@ -113,8 +114,8 @@ struct PSXSequence { * @param lambda Leading Z-rotation angle. * @return The planned PSX sequence. */ -[[nodiscard]] PSXSequence sequenceFromZYZForPSX(double theta, double phi, - double lambda) { +[[nodiscard]] static PSXSequence sequenceFromZYZForPSX(double theta, double phi, + double lambda) { constexpr double eps = mlir::utils::TOLERANCE; constexpr double halfPi = std::numbers::pi / 2.0; constexpr double pi = std::numbers::pi; @@ -146,7 +147,7 @@ struct PSXSequence { * @param lambda The `lambda` Euler angle. * @return The global-phase offset in radians. */ -[[nodiscard]] double globalPhaseOffsetForU(double phi, double lambda) { +[[nodiscard]] static double globalPhaseOffsetForU(double phi, double lambda) { return -0.5 * (phi + lambda); } @@ -159,7 +160,7 @@ struct PSXSequence { * @param angle The unwrapped RZ angle. * @return The global-phase contribution in radians. */ -[[nodiscard]] double globalPhaseFromRZWrap(double angle) { +[[nodiscard]] static double globalPhaseFromRZWrap(double angle) { constexpr double eps = mlir::utils::TOLERANCE; return 0.5 * (mod2pi(angle, eps) - angle); } @@ -170,7 +171,7 @@ struct PSXSequence { * @param seq The planned PSX sequence. * @return The global-phase offset in radians. */ -[[nodiscard]] double globalPhaseOffsetForPSX(const PSXSequence& seq) { +[[nodiscard]] static double globalPhaseOffsetForPSX(const PSXSequence& seq) { constexpr double halfPi = std::numbers::pi / 2.0; constexpr double quarterPi = std::numbers::pi / 4.0; @@ -200,8 +201,8 @@ struct PSXSequence { * @param lambda Leading Z-rotation angle from `paramsZYZ`. * @return The global-phase offset in radians. */ -[[nodiscard]] double globalPhaseOffsetForPSX(double theta, double phi, - double lambda) { +[[nodiscard]] static double globalPhaseOffsetForPSX(double theta, double phi, + double lambda) { return globalPhaseOffsetForPSX(sequenceFromZYZForPSX(theta, phi, lambda)); } @@ -213,10 +214,10 @@ struct PSXSequence { * @param onSX Called for each SX gate. * @param onX Called for each X gate. */ -void visitSequenceInTimeOrder(const PSXSequence& seq, - llvm::function_ref onRZ, - llvm::function_ref onSX, - llvm::function_ref onX) { +static void visitSequenceInTimeOrder(const PSXSequence& seq, + llvm::function_ref onRZ, + llvm::function_ref onSX, + llvm::function_ref onX) { onRZ(seq.firstRZ); switch (seq.middle) { case PSXSequence::Middle::OneSX: @@ -246,9 +247,10 @@ void visitSequenceInTimeOrder(const PSXSequence& seq, * @param phase Global phase to emit, in radians. * @return The transformed qubit value. */ -[[nodiscard]] Value emitFromPSXSequence(OpBuilder& builder, Location loc, - Value qubit, const PSXSequence& seq, - double phase) { +[[nodiscard]] static Value emitFromPSXSequence(OpBuilder& builder, Location loc, + Value qubit, + const PSXSequence& seq, + double phase) { constexpr double eps = mlir::utils::TOLERANCE; visitSequenceInTimeOrder( seq, @@ -275,8 +277,9 @@ void visitSequenceInTimeOrder(const PSXSequence& seq, * @param basis Euler basis selecting the K and A rotation axes. * @return The transformed qubit value. */ -Value emitKAK(OpBuilder& builder, Location loc, Value qubit, double theta, - double phi, double lambda, double phase, EulerBasis basis) { +static Value emitKAK(OpBuilder& builder, Location loc, Value qubit, + double theta, double phi, double lambda, double phase, + EulerBasis basis) { auto emitK = [&](double a) { switch (basis) { case EulerBasis::ZYZ: @@ -316,8 +319,6 @@ Value emitKAK(OpBuilder& builder, Location loc, Value qubit, double theta, return qubit; } -} // namespace - //===----------------------------------------------------------------------===// // Euler decomposition (angles) //===----------------------------------------------------------------------===// diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp index 0911a9917e..44376a971f 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp @@ -18,7 +18,7 @@ #include "mlir/Dialect/QTensor/IR/QTensorOps.h" #include -#include +#include // IWYU pragma: keep (Passes.h.inc) #include #include #include From 1b541d32060c6f7cc39c224720fab0cbe0572507 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 3 Jun 2026 19:04:32 +0200 Subject: [PATCH 22/68] =?UTF-8?q?=F0=9F=9A=A8=20fix=20clang-tidy=20warning?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index 6c75331a4e..a1f995cb24 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -91,6 +91,8 @@ static void emitGPhaseIfNeeded(OpBuilder& builder, Location loc, double phase) { GPhaseOp::create(builder, loc, phase); } +namespace { + /** * @brief Planned RZ-middle-RZ chain; fields are angles in circuit (time) order. */ @@ -102,6 +104,8 @@ struct PSXSequence { double lastRZ = 0.0; }; +} // namespace + /** * @brief Builds the RZ/SX chain realizing `RZ(phi)*RY(theta)*RZ(lambda)`. * From 3d54872859782a6b2b983563e183da37962b244f Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 4 Jun 2026 11:59:41 +0200 Subject: [PATCH 23/68] =?UTF-8?q?=E2=9A=A1=EF=B8=8FIncrease=20single=20qub?= =?UTF-8?q?it=20fusion=20performance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/Decomposition/Euler.h | 17 + .../mlir/Dialect/QCO/Transforms/Passes.td | 6 +- mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp | 9 +- mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp | 9 +- .../QCO/Transforms/Decomposition/Euler.cpp | 23 ++ .../FuseSingleQubitUnitaryRuns.cpp | 292 ++++++++++++------ mlir/lib/Dialect/QCO/Utils/CMakeLists.txt | 3 +- mlir/lib/Dialect/QCO/Utils/WireIterator.cpp | 5 +- .../test_euler_decomposition.cpp | 159 +++++++++- 9 files changed, 407 insertions(+), 116 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h index dbcc78121f..048796976f 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h @@ -15,6 +15,7 @@ #include #include +#include #include #include @@ -143,4 +144,20 @@ synthesizeUnitary1QEuler(OpBuilder& builder, Location loc, Value qubit, const Eigen::Matrix2cd& targetMatrix, EulerBasis basis); +/** + * @brief Number of gates `synthesizeUnitary1QEuler` emits for `targetMatrix`. + * + * Counts only the single-qubit basis gates; the optional global-phase + * (`qco.gphase`) correction is excluded. This is the canonical length of the + * synthesized run and lets callers decide whether an in-basis run is longer + * than necessary. + * + * @param targetMatrix The single-qubit unitary that would be synthesized. + * @param basis The target Euler basis. + * @return The number of emitted basis gates (1 for `U`, 3 for the KAK bases, + * 3 or 5 for `ZSXX`). + */ +[[nodiscard]] std::size_t +synthesisGateCount(const Eigen::Matrix2cd& targetMatrix, EulerBasis basis); + } // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index 175214355b..957836c749 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -47,9 +47,9 @@ def FuseSingleQubitUnitaryRuns "::mlir::qtensor::QTensorDialect"]; let summary = "Fuse single-qubit unitary runs using Euler resynthesis"; let description = [{ - Collects maximal runs of consecutive single-qubit unitary operations on the - same qubit wire, composes their constant unitary matrices, and replaces each - run with an equivalent sequence of basis gates. + Matches maximal runs of consecutive single-qubit unitary operations on the + same qubit wire (anchored at each run head), composes their constant unitary + matrices, and replaces each run with an equivalent sequence of basis gates. The emitted basis is controlled via the `basis` option (e.g. `zyz`, `zsxx`). A `gphase` correction is inserted when needed so the rewritten sequence diff --git a/mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp b/mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp index 7bbbeabeba..9a3a70d9f8 100644 --- a/mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp @@ -346,9 +346,12 @@ std::optional CtrlOp::getUnitaryMatrix() { if (!bodyUnitary) { return std::nullopt; } - auto&& targetMatrix = - cast(bodyUnitary.getOperation()) - .getUnitaryMatrix(); + auto matrixOp = + dyn_cast(bodyUnitary.getOperation()); + if (!matrixOp) { + return std::nullopt; + } + auto targetMatrix = matrixOp.getUnitaryMatrix(); if (!targetMatrix) { return std::nullopt; } diff --git a/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp b/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp index 66f12e72a0..975dd58c24 100644 --- a/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp @@ -407,9 +407,12 @@ std::optional InvOp::getUnitaryMatrix() { if (!bodyUnitary) { return std::nullopt; } - auto&& targetMatrix = - cast(bodyUnitary.getOperation()) - .getUnitaryMatrix(); + auto matrixOp = + dyn_cast(bodyUnitary.getOperation()); + if (!matrixOp) { + return std::nullopt; + } + auto targetMatrix = matrixOp.getUnitaryMatrix(); if (!targetMatrix) { return std::nullopt; } diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index a1f995cb24..5ebcd80f53 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -23,6 +23,7 @@ #include #include +#include #include #include #include @@ -466,4 +467,26 @@ Value synthesizeUnitary1QEuler(OpBuilder& builder, Location loc, Value qubit, return qubit; } +std::size_t synthesisGateCount(const Eigen::Matrix2cd& targetMatrix, + EulerBasis basis) { + switch (basis) { + case EulerBasis::U: + return 1; + case EulerBasis::ZYZ: + case EulerBasis::ZXZ: + case EulerBasis::XZX: + case EulerBasis::XYX: + // emitKAK always emits the full K-A-K rotation triple. + return 3; + case EulerBasis::ZSXX: { + const auto angles = + EulerDecomposition::anglesFromUnitary(targetMatrix, EulerBasis::ZSXX); + const auto seq = + sequenceFromZYZForPSX(angles.theta, angles.phi, angles.lambda); + return seq.middle == PSXSequence::Middle::SXRZSX ? 5U : 3U; + } + } + llvm::reportFatalInternalError("Unhandled Euler basis in synthesisGateCount"); +} + } // namespace mlir::qco::decomposition diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp index 44376a971f..390a709864 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp @@ -8,23 +8,25 @@ * 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/IR/QCOUnitaryMatrixInterfaces.h" #include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/QCO/Utils/WireIterator.h" -#include "mlir/Dialect/QTensor/IR/QTensorOps.h" #include +#include +#include +#include #include // IWYU pragma: keep (Passes.h.inc) -#include #include #include #include +#include #include #include +#include #include #include @@ -36,13 +38,43 @@ namespace mlir::qco { #define GEN_PASS_DEF_FUSESINGLEQUBITUNITARYRUNS #include "mlir/Dialect/QCO/Transforms/Passes.h.inc" +/** + * @brief Whether `op` lives inside an `inv`/`ctrl` modifier body. + * + * A modifier exposes its body's combined unitary through `getUnitaryMatrix` and + * is fused as a single, atomic run member (when single-qubit). Fusing the gates + * inside its body would invalidate that matrix, so body gates are never run + * members themselves. + * + * @param op The operation to test. + * @return `true` if `op`'s parent is an `inv` or `ctrl` op. + */ +static bool isNestedInModifierRegion(Operation* op) { + Operation* parent = op->getParentOp(); + return parent != nullptr && isa(parent); +} + +/** + * @brief Whether `op` may participate in a fusable single-qubit run. + * + * @param op The unitary operation to test. + * @return `true` for a single-qubit, matrix-backed unitary that lives directly + * on a wire (not inside a modifier body). + */ static bool isFuseCandidate(UnitaryOpInterface op) { - if (!op || !op.isSingleQubit()) { + if (!op || !op.isSingleQubit() || isNestedInModifierRegion(op)) { return false; } return isa(op.getOperation()); } +/** + * @brief Returns the compile-time 2x2 unitary matrix of `op`, if available. + * + * @param op The unitary operation to query. + * @return The constant matrix, or `std::nullopt` if `op` is not matrix-backed + * or its matrix is not known at compile time. + */ static std::optional getConstMatrix(UnitaryOpInterface op) { auto matrixOp = dyn_cast(op.getOperation()); if (!matrixOp) { @@ -55,23 +87,163 @@ static std::optional getConstMatrix(UnitaryOpInterface op) { return m; } -/// Compose a run of unitary ops (execution order) into a single matrix. -static std::optional -composeRun(ArrayRef run) { +/** + * @brief Whether `op` can participate in a fusable run. + * + * @param op The operation to test. + * @return `true` for a single-qubit, matrix-backed unitary outside a modifier + * body whose matrix is known at compile time. + */ +static bool isRunMember(Operation* op) { + auto iface = dyn_cast(op); + return iface && isFuseCandidate(iface) && getConstMatrix(iface).has_value(); +} + +/** + * @brief Composes a run of unitary ops into a single matrix. + * + * @param run The run members in execution (circuit) order. + * @return The product of the members' matrices. + */ +static Eigen::Matrix2cd composeRun(ArrayRef run) { Eigen::Matrix2cd composed = Eigen::Matrix2cd::Identity(); for (auto op : run) { - auto m = getConstMatrix(op); - if (!m) { - return std::nullopt; - } // Execution order: first op applied first => multiply on the left. - composed = (*m) * composed; + composed = (*getConstMatrix(op)) * composed; } return composed; } +/** + * @brief Whether `op` is one of the gates the target `basis` emits. + * + * The gate sets mirror `emitKAK` and `emitFromPSXSequence` in `Euler.cpp`. The + * greedy driver re-visits the gates produced by a rewrite, so this lets the + * pattern detect a run that is already expressed entirely in the target basis + * and avoid re-fusing the gates it just produced. + * + * @param op The operation to classify. + * @param basis The target Euler basis. + * @return `true` if `op` is a gate the `basis` emits. + */ +static bool isTargetBasisGate(Operation* op, decomposition::EulerBasis basis) { + using decomposition::EulerBasis; + return TypeSwitch(op) + .Case([&](auto) { + return basis == EulerBasis::ZYZ || basis == EulerBasis::ZXZ || + basis == EulerBasis::XZX || basis == EulerBasis::ZSXX; + }) + .Case([&](auto) { + return basis == EulerBasis::ZYZ || basis == EulerBasis::XYX; + }) + .Case([&](auto) { + return basis == EulerBasis::ZXZ || basis == EulerBasis::XZX || + basis == EulerBasis::XYX; + }) + .Case([&](auto) { return basis == EulerBasis::U; }) + .Case([&](auto) { return basis == EulerBasis::ZSXX; }) + .Default([](auto) { return false; }); +} + namespace { +/** + * @brief Replaces a maximal single-qubit unitary run with its Euler synthesis. + */ +struct FuseSingleQubitUnitaryRunsPattern final + : OpInterfaceRewritePattern { + FuseSingleQubitUnitaryRunsPattern(MLIRContext* context, + decomposition::EulerBasis basis) + : OpInterfaceRewritePattern(context), basis(basis) {} + + decomposition::EulerBasis basis; + + /** + * @brief Whether `op` is the head of a run. + * + * A run head is a fusable op whose predecessor on the wire is not itself a + * fusable run member, so each run is matched exactly once at its start. + * + * @param op The candidate run head. + * @return `true` if `op` starts a run. + */ + static bool isRunStart(UnitaryOpInterface op) { + if (!isRunMember(op.getOperation())) { + return false; + } + Operation* pred = op.getInputTarget(0).getDefiningOp(); + return pred == nullptr || !isRunMember(pred); + } + + /** + * @brief Collects the maximal run of fusable ops starting at `start`. + * + * Follows the wire forward while staying within the same block. + * + * @param start The run head (must satisfy `isRunStart`). + * @return The run members in circuit order. + */ + static SmallVector collectRun(UnitaryOpInterface start) { + SmallVector run{start}; + Block* block = start->getBlock(); + for (WireIterator it = std::next(WireIterator(start.getOutputTarget(0))); + it != std::default_sentinel; ++it) { + Operation* op = it.operation(); + if (op->getBlock() != block || !isRunMember(op)) { + break; + } + run.emplace_back(cast(op)); + } + return run; + } + + /** + * @brief Fuses the run anchored at `op` into its Euler resynthesis. + * + * @param op The matched unitary operation. + * @param rewriter Pattern rewriter used to apply the transformation. + * @return `success()` if a run was fused, `failure()` otherwise. + */ + LogicalResult matchAndRewrite(UnitaryOpInterface op, + PatternRewriter& rewriter) const override { + if (!isRunStart(op)) { + return failure(); + } + + auto run = collectRun(op); + const Eigen::Matrix2cd composed = composeRun(run); + + // Resynthesize a run when it either contains a gate outside the target + // basis (so it is not yet expressed in the native gate set) or is already + // in-basis but longer than the canonical Euler form (so fusing shortens + // it). A run that is in-basis and already at canonical length is left + // untouched, which is also what keeps the greedy driver from re-matching + // the gates this pattern just produced. + const bool hasNonBasisGate = + llvm::any_of(run, [&](UnitaryOpInterface member) { + return !isTargetBasisGate(member.getOperation(), basis); + }); + if (!hasNonBasisGate && + run.size() <= decomposition::synthesisGateCount(composed, basis)) { + return failure(); + } + + OpBuilder::InsertionGuard guard(rewriter); + rewriter.setInsertionPoint(op.getOperation()); + const Value qubit = decomposition::synthesizeUnitary1QEuler( + rewriter, op.getLoc(), op.getInputTarget(0), composed, basis); + + rewriter.replaceAllUsesWith(run.back().getOutputTarget(0), qubit); + for (UnitaryOpInterface member : std::ranges::reverse_view(run)) { + rewriter.eraseOp(member.getOperation()); + } + return success(); + } +}; + +/** + * @brief Pass that fuses single-qubit unitary runs via Euler resynthesis. + */ struct FuseSingleQubitUnitaryRunsPass final : impl::FuseSingleQubitUnitaryRunsBase { using Base::Base; @@ -92,98 +264,12 @@ struct FuseSingleQubitUnitaryRunsPass final return; } - SmallVector wireStarts; - module.walk([&](AllocOp op) { wireStarts.emplace_back(op.getResult()); }); - module.walk([&](StaticOp op) { wireStarts.emplace_back(op.getQubit()); }); - module.walk([&](qtensor::ExtractOp op) { - wireStarts.emplace_back(op.getResult()); - }); - module.walk([&](func::FuncOp func) { - if (func.empty()) { - return; - } - for (BlockArgument arg : func.getBody().front().getArguments()) { - if (isa(arg.getType())) { - wireStarts.emplace_back(arg); - } - } - }); - - // Collect runs first, rewrite afterwards. - SmallVector, 16> runs; - DenseSet seen; - - auto flushRun = [&](SmallVector& current) { - if (current.size() > 1) { - runs.emplace_back(std::move(current)); - current = SmallVector(); - } else { - current.clear(); - } - }; - - for (Value start : wireStarts) { - if (!start) { - continue; - } - - SmallVector current; - Block* currentBlock = nullptr; - - WireIterator it(start); - ++it; // Move to the first op on the wire. - for (; it != std::default_sentinel; ++it) { - Operation* op = *it; - - if (currentBlock == nullptr) { - currentBlock = op->getBlock(); - } - if (op->getBlock() != currentBlock) { - break; - } - - if (seen.contains(op)) { - // Wire may be reached from multiple starts; flush any partial run. - flushRun(current); - continue; - } - if (!isa(op)) { - flushRun(current); - continue; - } - - auto iface = cast(op); - if (!isFuseCandidate(iface) || !getConstMatrix(iface).has_value()) { - flushRun(current); - continue; - } - - current.emplace_back(iface); - seen.insert(op); - } - - flushRun(current); - } + RewritePatternSet patterns(&getContext()); + patterns.add(patterns.getContext(), + *parsed); - for (auto& run : runs) { - if (run.empty() || run.front().getOperation()->getParentOp() == nullptr) { - continue; - } - - auto composed = composeRun(run); - if (!composed) { - continue; - } - - OpBuilder builder(run.front().getOperation()); - Value qubit = decomposition::synthesizeUnitary1QEuler( - builder, run.front().getLoc(), run.front().getInputTarget(0), - *composed, *parsed); - - run.back().getOutputTarget(0).replaceAllUsesWith(qubit); - for (auto& it : std::ranges::reverse_view(run)) { - it.getOperation()->erase(); - } + if (failed(applyPatternsGreedily(module, std::move(patterns)))) { + signalPassFailure(); } } }; diff --git a/mlir/lib/Dialect/QCO/Utils/CMakeLists.txt b/mlir/lib/Dialect/QCO/Utils/CMakeLists.txt index 9b94885f1b..26a14e17e4 100644 --- a/mlir/lib/Dialect/QCO/Utils/CMakeLists.txt +++ b/mlir/lib/Dialect/QCO/Utils/CMakeLists.txt @@ -18,7 +18,8 @@ add_mlir_dialect_library( MLIRQCOInterfacesIncGen LINK_LIBS PUBLIC - MLIRQCODialect) + MLIRQCODialect + MLIRSCFDialect) mqt_mlir_target_use_project_options(MLIRQCOUtils) diff --git a/mlir/lib/Dialect/QCO/Utils/WireIterator.cpp b/mlir/lib/Dialect/QCO/Utils/WireIterator.cpp index 45522cd8bf..b1d030a7af 100644 --- a/mlir/lib/Dialect/QCO/Utils/WireIterator.cpp +++ b/mlir/lib/Dialect/QCO/Utils/WireIterator.cpp @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -43,7 +44,7 @@ void WireIterator::forward() { op_ = *(qubit_.user_begin()); // A sink/insert defines the end of the qubit wire (dynamic and static). - if (isa(op_)) { + if (isa(op_)) { isSentinel_ = true; return; } @@ -72,7 +73,7 @@ void WireIterator::backward() { // For sinks/deallocations/inserts, qubit_ is an OpOperand. Hence, only get // the def-op. - if (isa(op_)) { + if (isa(op_)) { op_ = qubit_.getDefiningOp(); return; } diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp index aea4ea30ac..e2cf0259d9 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -57,7 +58,8 @@ struct SynthesisFixture { void setUp() { DialectRegistry registry; - registry.insert(); + registry.insert(); context = std::make_unique(); context->appendDialectRegistry(registry); context->loadAllAvailableDialects(); @@ -142,6 +144,44 @@ static bool isAllowedBasisGate(Operation& op, StringRef basis) { return false; } +// A single-qubit gate carrying a unitary matrix (excludes barriers, the +// 2-qubit boundary gate, and the global-phase correction). +[[nodiscard]] static bool isOneQubitGate(Operation& op) { + if (isa(op) || !isa(op)) { + return false; + } + auto u = dyn_cast(op); + return u && u.isSingleQubit(); +} + +// Asserts that at least one 1Q gate remains on each side of the first op +// matching `isBoundary` in the function's entry block. +template +static void expectOneQubitGatesAroundBoundary(func::FuncOp funcOp, + StringRef basis, + BoundaryPred isBoundary) { + auto& block = funcOp.getBody().front(); + std::size_t before = 0; + std::size_t after = 0; + bool seenBoundary = false; + for (Operation& op : block.without_terminator()) { + if (!seenBoundary && isBoundary(op)) { + seenBoundary = true; + continue; + } + if (!isOneQubitGate(op)) { + continue; + } + if (seenBoundary) { + ++after; + } else { + ++before; + } + } + EXPECT_GE(before, 1U) << "basis=" << basis.str(); + EXPECT_GE(after, 1U) << "basis=" << basis.str(); +} + static void expectBasisGatesOnly(func::FuncOp funcOp, StringRef basis) { auto& block = funcOp.getBody().front(); for (Operation& op : block.without_terminator()) { @@ -237,6 +277,50 @@ static void singleQubitRunsSplitByBarrier(QCOProgramBuilder& b) { q[0] = b.sx(q[0]); } +// A lone gate that is not part of any of the target bases. The pass should +// still resynthesize it into the requested basis. +static void singleNonBasisGate(QCOProgramBuilder& b) { + auto q = b.allocQubitRegister(1); + q[0] = b.h(q[0]); +} + +// A run made up solely of `RZ`/`RY` gates: already in the `zyz` basis, but +// longer than the canonical three-gate Euler form, so it should still shrink. +static void overlongZyzRun(QCOProgramBuilder& b) { + auto q = b.allocQubitRegister(1); + q[0] = b.rz(0.3, q[0]); + q[0] = b.ry(0.5, q[0]); + q[0] = b.rz(0.7, q[0]); + q[0] = b.ry(0.9, q[0]); + q[0] = b.rz(1.1, q[0]); + q[0] = b.ry(1.3, q[0]); +} + +static void singleQubitRunInScfFor(QCOProgramBuilder& b) { + auto q = b.allocQubitRegister(1); + b.scfFor(0, 1, 1, ValueRange{q[0]}, [&](Value, ValueRange iterArgs) { + Value wire = iterArgs[0]; + wire = b.h(wire); + wire = b.t(wire); + wire = b.rz(0.123, wire); + return SmallVector{wire}; + }); +} + +[[nodiscard]] static std::size_t countUOpsInScfFor(func::FuncOp funcOp) { + std::size_t count = 0; + funcOp.walk([&](UOp op) { + for (Operation* parent = op->getParentOp(); parent != nullptr; + parent = parent->getParentOp()) { + if (parent->getName().getStringRef() == "scf.for") { + ++count; + break; + } + } + }); + return count; +} + static OwningOpRef buildProgram(MLIRContext* ctx, void (*fn)(QCOProgramBuilder&)) { QCOProgramBuilder builder(ctx); @@ -382,6 +466,29 @@ TEST(EulerSynthesisTest, RandomReconstructionAllBases) { } } +TEST(FuseSingleQubitUnitaryRunsTest, FusesRunInScfForBody) { + SynthesisFixture fx; + fx.setUp(); + + auto owned = buildProgram(fx.context.get(), &singleQubitRunInScfFor); + ASSERT_TRUE(owned); + ModuleOp module = *owned; + ASSERT_TRUE(succeeded(verify(module))); + + auto funcOp = lookupMain(module); + ASSERT_TRUE(funcOp); + const Eigen::Matrix2cd original = compute1QMatrixFromFunction(funcOp); + + ASSERT_TRUE(succeeded(runFuse(module, "u"))); + ASSERT_TRUE(succeeded(verify(module))); + + funcOp = lookupMain(module); + ASSERT_TRUE(funcOp); + EXPECT_GE(countUOpsInScfFor(funcOp), 1U); + EXPECT_TRUE(compute1QMatrixFromFunction(funcOp).isApprox( + original, mlir::utils::TOLERANCE)); +} + TEST(FuseSingleQubitUnitaryRunsTest, ReconstructsOriginalRunAllBases) { SynthesisFixture fx; fx.setUp(); @@ -398,6 +505,52 @@ TEST(FuseSingleQubitUnitaryRunsTest, ReconstructsOriginalRunAllBases) { }); } +TEST(FuseSingleQubitUnitaryRunsTest, ResynthesizesLoneNonBasisGateAllBases) { + SynthesisFixture fx; + fx.setUp(); + + runFuseOnProgramForAllBases( + fx.context.get(), &singleNonBasisGate, + /*checksAfter=*/ + [&](func::FuncOp funcOp, StringRef basis, + const Eigen::Matrix2cd& original) { + EXPECT_EQ(countOps(funcOp), 0U) + << "basis=" << basis.str() << " left a non-basis gate"; + EXPECT_TRUE(compute1QMatrixFromFunction(funcOp).isApprox( + original, mlir::utils::TOLERANCE)) + << "basis=" << basis.str(); + expectBasisGatesOnly(funcOp, basis); + }); +} + +TEST(FuseSingleQubitUnitaryRunsTest, FusesOverlongInBasisRun) { + SynthesisFixture fx; + fx.setUp(); + + auto owned = buildProgram(fx.context.get(), &overlongZyzRun); + ASSERT_TRUE(owned); + ModuleOp module = *owned; + ASSERT_TRUE(succeeded(verify(module))); + + auto funcOp = lookupMain(module); + ASSERT_TRUE(funcOp); + const Eigen::Matrix2cd original = compute1QMatrixFromFunction(funcOp); + const std::size_t before = countOps(funcOp) + countOps(funcOp); + ASSERT_EQ(before, 6U); + + ASSERT_TRUE(succeeded(runFuse(module, "zyz"))); + ASSERT_TRUE(succeeded(verify(module))); + + funcOp = lookupMain(module); + ASSERT_TRUE(funcOp); + const std::size_t after = countOps(funcOp) + countOps(funcOp); + EXPECT_LE(after, 3U); + EXPECT_LT(after, before); + EXPECT_TRUE(compute1QMatrixFromFunction(funcOp).isApprox( + original, mlir::utils::TOLERANCE)); + expectBasisGatesOnly(funcOp, "zyz"); +} + TEST(EulerSynthesisTest, ZsxxPauliXUsesXGateShortcut) { SynthesisFixture fx; fx.setUp(); @@ -525,6 +678,8 @@ TEST(FuseSingleQubitUnitaryRunsTest, DoesNotFuseAcrossTwoQGateAllBases) { original, mlir::utils::TOLERANCE)) << "basis=" << basis.str(); expectBasisGatesOnly(funcOp, basis); + expectOneQubitGatesAroundBoundary( + funcOp, basis, [](Operation& op) { return isTwoQubitGate(op); }); }); } @@ -542,6 +697,8 @@ TEST(FuseSingleQubitUnitaryRunsTest, DoesNotFuseAcrossBarrierAllBases) { original, mlir::utils::TOLERANCE)) << "basis=" << basis.str(); expectBasisGatesOnly(funcOp, basis); + expectOneQubitGatesAroundBoundary( + funcOp, basis, [](Operation& op) { return isa(op); }); }); } From 608491fe8f9e3127aa33dc5ea96da53d861a7617 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 4 Jun 2026 12:55:45 +0200 Subject: [PATCH 24/68] =?UTF-8?q?=F0=9F=8E=A8=20Enhance=20WireIterator=20t?= =?UTF-8?q?o=20include=20YieldOp=20in=20boundary=20checks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/lib/Dialect/QCO/Utils/WireIterator.cpp | 12 +++--- .../test_euler_decomposition.cpp | 43 +++++++++++-------- 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Utils/WireIterator.cpp b/mlir/lib/Dialect/QCO/Utils/WireIterator.cpp index b1d030a7af..1e489d2017 100644 --- a/mlir/lib/Dialect/QCO/Utils/WireIterator.cpp +++ b/mlir/lib/Dialect/QCO/Utils/WireIterator.cpp @@ -26,8 +26,10 @@ namespace mlir::qco { Value WireIterator::qubit() const { - // A sink/deallocation/insert doesn't have an OpResult. - if (op_ != nullptr && (isa(op_))) { + // Boundary ops (sink/deallocation/insert/yield) consume the wire via an + // operand and have no OpResult, matching the boundaries in forward/backward. + if (op_ != nullptr && + (isa(op_))) { return nullptr; } return qubit_; @@ -43,7 +45,7 @@ void WireIterator::forward() { assert(qubit_.hasOneUse() && "expected linear typing"); op_ = *(qubit_.user_begin()); - // A sink/insert defines the end of the qubit wire (dynamic and static). + // A sink/insert/yield defines the end of the qubit wire (dynamic and static). if (isa(op_)) { isSentinel_ = true; return; @@ -71,8 +73,8 @@ void WireIterator::backward() { return; } - // For sinks/deallocations/inserts, qubit_ is an OpOperand. Hence, only get - // the def-op. + // For sinks/deallocations/inserts/yields, qubit_ is an OpOperand. Hence, only + // get the def-op. if (isa(op_)) { op_ = qubit_.getDefiningOp(); return; diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp index e2cf0259d9..1c690a77c2 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -205,37 +206,45 @@ static void expectBasisGatesOnly(func::FuncOp funcOp, StringRef basis) { static Eigen::Matrix2cd compute1QMatrixFromFunction(func::FuncOp funcOp) { Eigen::Matrix2cd acc = Eigen::Matrix2cd::Identity(); std::complex global{1.0, 0.0}; - - auto& block = funcOp.getBody().front(); - for (Operation& op : block.without_terminator()) { - if (isa(op)) { - continue; + bool failed = false; + + // Walk every block, descending into nested regions (e.g. `scf.for` bodies) so + // loop-body gates contribute too. The pre-order walk lets us account for a + // matrix-backed modifier (`inv`/`ctrl`) via its combined matrix and then skip + // its body, avoiding double-counting the gates nested inside it. The 1q gates + // of these tests live in a single block, so the walk order matches the + // execution order. + funcOp.walk([&](Operation* op) -> WalkResult { + if (isa(*op) || isTwoQubitGate(*op)) { + return WalkResult::advance(); } - if (isTwoQubitGate(op)) { - continue; - } - - if (auto gphase = dyn_cast(op)) { + if (auto gphase = dyn_cast(*op)) { if (auto m = gphase.getUnitaryMatrix()) { global *= (*m)(0, 0); } - continue; + return WalkResult::advance(); } - if (auto iface = dyn_cast(op)) { - // All ops in this test should be 1q ops after synthesis. + if (auto iface = dyn_cast(*op)) { + // All matrix-backed ops in these tests should be 1q ops after synthesis. const auto maybeM = iface.getUnitaryMatrix(); if (!maybeM) { ADD_FAILURE() << "Expected constant unitary matrix for op: " - << op.getName().getStringRef().str(); - return Eigen::Matrix2cd::Zero(); + << op->getName().getStringRef().str(); + failed = true; + return WalkResult::interrupt(); } acc = (*maybeM) * acc; - continue; + return WalkResult::skip(); } - } + return WalkResult::advance(); + }); + + if (failed) { + return Eigen::Matrix2cd::Zero(); + } return global * acc; } From 98266bdf19523ffd732013863a9a2df0683dec3b Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 4 Jun 2026 15:13:29 +0200 Subject: [PATCH 25/68] =?UTF-8?q?=F0=9F=93=9D=20Refine=20Euler=20decomposi?= =?UTF-8?q?tion=20and=20synthesis=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/Decomposition/Euler.h | 55 +++---- .../QCO/Transforms/Decomposition/Euler.cpp | 143 +++++++++--------- .../FuseSingleQubitUnitaryRuns.cpp | 65 ++++---- .../test_euler_decomposition.cpp | 74 ++++----- 4 files changed, 153 insertions(+), 184 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h index 048796976f..dd161514ed 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h @@ -36,13 +36,12 @@ struct EulerAngles { * @brief Native gate sets for single-qubit Euler synthesis. */ enum class EulerBasis : std::uint8_t { - ZYZ = 0, ///< `RZ(phi) * RY(theta) * RZ(lambda)`. - ZXZ = 1, ///< `RZ(phi) * RX(theta) * RZ(lambda)`. - XZX = 2, ///< `RX(phi) * RZ(theta) * RX(lambda)`. - XYX = 3, ///< `RX(phi) * RY(theta) * RX(lambda)`. - U = 4, ///< `U(theta, phi, lambda)`. - ZSXX = - 5, ///< ZYZ-equivalent chain over `RZ`, `SX`, and `X` (see `paramsPSX`). + ZYZ = 0, ///< `RZ(phi) * RY(theta) * RZ(lambda)`. + ZXZ = 1, ///< `RZ(phi) * RX(theta) * RZ(lambda)`. + XZX = 2, ///< `RX(phi) * RZ(theta) * RX(lambda)`. + XYX = 3, ///< `RX(phi) * RY(theta) * RX(lambda)`. + U = 4, ///< `U(theta, phi, lambda)`. + ZSXX = 5, ///< `RZ` / `SX` / `X` chain equivalent to ZYZ. }; /** @@ -75,7 +74,7 @@ class EulerDecomposition { [[nodiscard]] static EulerAngles paramsZYZ(const Eigen::Matrix2cd& matrix); /** - * @brief Extracts parameters for a `U(theta, phi, lambda)` factorization. + * @brief Extracts parameters for `U(theta, phi, lambda)`. * * @param matrix The single-qubit unitary to decompose. * @return The extracted Euler angles and global phase. @@ -105,39 +104,28 @@ class EulerDecomposition { * @return The extracted Euler angles and global phase. */ [[nodiscard]] static EulerAngles paramsXZX(const Eigen::Matrix2cd& matrix); - - /** - * @brief Extracts ZYZ-equivalent angles and global phase for `ZSXX` - * synthesis. - * - * Returns the same `(theta, phi, lambda)` as `paramsZYZ`; `phase` includes - * the offset to the native `RZ`/`SX`/`X` chain emitted for `ZSXX`. - * - * @param matrix The single-qubit unitary to decompose. - * @return The extracted Euler angles and global phase. - */ - [[nodiscard]] static EulerAngles paramsPSX(const Eigen::Matrix2cd& matrix); }; /** - * @brief Parses a user-facing basis string (e.g. "zyz", "zsxx"). + * @brief Parses a basis name (e.g. `zyz`, `zsxx`; case-insensitive). * - * @param basis The basis name (case-insensitive). - * @return The parsed Euler basis, or `std::nullopt` if unrecognized. + * @param basis The basis name. + * @return The parsed basis, or `std::nullopt` if unrecognized. */ [[nodiscard]] std::optional parseEulerBasis(StringRef basis); /** - * @brief Emits gates reconstructing `targetMatrix` in the given basis. + * @brief Synthesizes `targetMatrix` as gates in `basis`. * - * Includes a global phase (`qco.gphase`) when needed for an exact match. + * Emits `qco.gphase` when needed so the result matches exactly, not only up to + * global phase. * - * @param builder Builder used to create the operations. - * @param loc Source location for the created operations. + * @param builder Builder for the emitted operations. + * @param loc Location for the emitted operations. * @param qubit Input qubit value. * @param targetMatrix The single-qubit unitary to synthesize. * @param basis The target Euler basis. - * @return The transformed qubit value. + * @return The output qubit value. */ [[nodiscard]] Value synthesizeUnitary1QEuler(OpBuilder& builder, Location loc, Value qubit, @@ -145,17 +133,14 @@ synthesizeUnitary1QEuler(OpBuilder& builder, Location loc, Value qubit, EulerBasis basis); /** - * @brief Number of gates `synthesizeUnitary1QEuler` emits for `targetMatrix`. + * @brief Number of basis gates `synthesizeUnitary1QEuler` would emit. * - * Counts only the single-qubit basis gates; the optional global-phase - * (`qco.gphase`) correction is excluded. This is the canonical length of the - * synthesized run and lets callers decide whether an in-basis run is longer - * than necessary. + * Excludes `qco.gphase`. Used by the fuse pass to detect overlong in-basis + * runs. * * @param targetMatrix The single-qubit unitary that would be synthesized. * @param basis The target Euler basis. - * @return The number of emitted basis gates (1 for `U`, 3 for the KAK bases, - * 3 or 5 for `ZSXX`). + * @return The gate count (1 for `U`, 3 for KAK bases, 3 or 5 for `ZSXX`). */ [[nodiscard]] std::size_t synthesisGateCount(const Eigen::Matrix2cd& targetMatrix, EulerBasis basis); diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index 5ebcd80f53..08f4579b5e 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -35,7 +35,7 @@ namespace mlir::qco::decomposition { * `-pi`. * * @param angle The angle to wrap, in radians. - * @param atol Absolute tolerance for snapping `+pi` to `-pi`. + * @param atol Tolerance for snapping `+pi` to `-pi`. * @return The wrapped angle in `[-pi, pi)`. */ [[nodiscard]] static double mod2pi(double angle, @@ -63,7 +63,7 @@ namespace mlir::qco::decomposition { /** * @brief Conjugates a single-qubit matrix by Hadamard (`H * m * H`). * - * Maps X-Y-X / X-Z-X decompositions to Z-Y-Z / Z-X-Z. + * Maps XYX / XZX parameterizations to ZYZ / ZXZ. * * @param m The single-qubit matrix to conjugate. * @return `H * m * H`. @@ -79,11 +79,11 @@ hadamardConjugate(const Eigen::Matrix2cd& m) { } /** - * @brief Emits a `GPhaseOp` when `phase` is non-negligible. + * @brief Emits `qco.gphase` when `phase` is outside tolerance. * - * @param builder Builder used to create the operation. - * @param loc Source location for the created operation. - * @param phase Global phase in radians; skipped when within tolerance of zero. + * @param builder Builder for the operation. + * @param loc Location of the operation. + * @param phase Global phase in radians. */ static void emitGPhaseIfNeeded(OpBuilder& builder, Location loc, double phase) { if (std::abs(phase) <= mlir::utils::TOLERANCE) { @@ -95,7 +95,7 @@ static void emitGPhaseIfNeeded(OpBuilder& builder, Location loc, double phase) { namespace { /** - * @brief Planned RZ-middle-RZ chain; fields are angles in circuit (time) order. + * @brief Planned PSX (`RZ` / `SX` / `X`) chain; angles in circuit order. */ struct PSXSequence { enum class Middle : std::uint8_t { OneSX, X, SXRZSX }; @@ -108,11 +108,33 @@ struct PSXSequence { } // namespace /** - * @brief Builds the RZ/SX chain realizing `RZ(phi)*RY(theta)*RZ(lambda)`. + * @brief Classifies the PSX middle-gate case from ZYZ `theta`. * - * Uses the identity `SX*RZ(theta+pi)*SX = Z*RY(theta)`. `theta` from - * `paramsZYZ` lies in `[0, pi]`: `pi/2` collapses to a single SX; `pi` becomes - * an X gate (since `SX*SX = X`). + * For `theta` in `[0, pi]`: `pi/2` → one `SX`, `pi` → `X`, otherwise + * `SX*RZ*SX`. + * + * @param theta Y-rotation angle from `paramsZYZ`. + * @return The PSX middle-gate case. + */ +[[nodiscard]] static PSXSequence::Middle +classifyPSXMiddleFromZYZTheta(double theta) { + constexpr double eps = mlir::utils::TOLERANCE; + constexpr double halfPi = std::numbers::pi / 2.0; + constexpr double pi = std::numbers::pi; + + if (std::abs(theta - halfPi) < eps) { + return PSXSequence::Middle::OneSX; + } + if (std::abs(theta - pi) < eps) { + return PSXSequence::Middle::X; + } + return PSXSequence::Middle::SXRZSX; +} + +/** + * @brief Builds the PSX sequence for `RZ(phi)*RY(theta)*RZ(lambda)`. + * + * Uses `SX*RZ(theta+pi)*SX = Z*RY(theta)`. * * @param theta Y-rotation angle in `[0, pi]`. * @param phi Trailing Z-rotation angle. @@ -121,35 +143,34 @@ struct PSXSequence { */ [[nodiscard]] static PSXSequence sequenceFromZYZForPSX(double theta, double phi, double lambda) { - constexpr double eps = mlir::utils::TOLERANCE; constexpr double halfPi = std::numbers::pi / 2.0; constexpr double pi = std::numbers::pi; - if (std::abs(theta - halfPi) < eps) { + switch (classifyPSXMiddleFromZYZTheta(theta)) { + case PSXSequence::Middle::OneSX: return {.middle = PSXSequence::Middle::OneSX, .firstRZ = lambda - halfPi, .midRZ = 0.0, .lastRZ = phi + halfPi}; - } - if (std::abs(theta - pi) < eps) { + case PSXSequence::Middle::X: return {.middle = PSXSequence::Middle::X, .firstRZ = lambda, .midRZ = 0.0, .lastRZ = phi + pi}; + case PSXSequence::Middle::SXRZSX: + return {.middle = PSXSequence::Middle::SXRZSX, + .firstRZ = lambda, + .midRZ = theta + pi, + .lastRZ = phi + pi}; } - return {.middle = PSXSequence::Middle::SXRZSX, - .firstRZ = lambda, - .midRZ = theta + pi, - .lastRZ = phi + pi}; + llvm::reportFatalInternalError("Unhandled PSX middle gate"); } /** - * @brief Global phase between `UOp(theta, phi, lambda)` and the ZYZ product. - * - * Relates `UOp` to `RZ(phi)*RY(theta)*RZ(lambda)` on the same angles. + * @brief Global phase offset of `UOp` vs `RZ(phi)*RY(theta)*RZ(lambda)`. * - * @param phi The `phi` Euler angle. - * @param lambda The `lambda` Euler angle. + * @param phi Trailing Z-rotation angle. + * @param lambda Leading Z-rotation angle. * @return The global-phase offset in radians. */ [[nodiscard]] static double globalPhaseOffsetForU(double phi, double lambda) { @@ -157,10 +178,10 @@ struct PSXSequence { } /** - * @brief Global phase contributed by wrapping an RZ angle with `mod2pi`. + * @brief Global phase from wrapping an RZ angle with `mod2pi`. * - * `mod2pi(angle) - angle` is a multiple of `2*pi`, so the emitted - * `RZ(mod2pi(angle))` equals `exp(i*(mod2pi(angle)-angle)/2) * RZ(angle)`. + * `RZ(angle + 2*pi) = -RZ(angle)`, so `RZ(mod2pi(angle))` differs from + * `RZ(angle)` by `exp(i*(mod2pi(angle) - angle)/2)`. * * @param angle The unwrapped RZ angle. * @return The global-phase contribution in radians. @@ -171,7 +192,7 @@ struct PSXSequence { } /** - * @brief Global phase between the ZYZ product and the emitted PSX product. + * @brief Global phase offset of the PSX chain vs the ZYZ product. * * @param seq The planned PSX sequence. * @return The global-phase offset in radians. @@ -199,20 +220,7 @@ struct PSXSequence { } /** - * @brief Global phase between the ZYZ product and the emitted PSX product. - * - * @param theta Y-rotation angle from `paramsZYZ`. - * @param phi Trailing Z-rotation angle from `paramsZYZ`. - * @param lambda Leading Z-rotation angle from `paramsZYZ`. - * @return The global-phase offset in radians. - */ -[[nodiscard]] static double globalPhaseOffsetForPSX(double theta, double phi, - double lambda) { - return globalPhaseOffsetForPSX(sequenceFromZYZForPSX(theta, phi, lambda)); -} - -/** - * @brief Invokes callbacks for each gate of `seq` in circuit (time) order. + * @brief Invokes callbacks for each gate of `seq` in circuit order. * * @param seq The planned PSX sequence. * @param onRZ Called with each RZ angle. @@ -243,14 +251,14 @@ static void visitSequenceInTimeOrder(const PSXSequence& seq, } /** - * @brief Emits the RZ/SX/X gates of `seq` followed by the global phase. + * @brief Emits the gates of `seq` and optional `gphase`. * - * @param builder Builder used to create the operations. - * @param loc Source location for the created operations. + * @param builder Builder for the operations. + * @param loc Location of the operations. * @param qubit Input qubit value. * @param seq The planned PSX sequence. - * @param phase Global phase to emit, in radians. - * @return The transformed qubit value. + * @param phase Global phase in radians. + * @return The output qubit value. */ [[nodiscard]] static Value emitFromPSXSequence(OpBuilder& builder, Location loc, Value qubit, @@ -270,17 +278,17 @@ static void visitSequenceInTimeOrder(const PSXSequence& seq, } /** - * @brief Emits a K-A-K rotation triple plus global phase for `basis`. + * @brief Emits a K-A-K rotation triple and optional `gphase` for `basis`. * - * @param builder Builder used to create the operations. - * @param loc Source location for the created operations. + * @param builder Builder for the operations. + * @param loc Location of the operations. * @param qubit Input qubit value. * @param theta Middle (A) rotation angle. * @param phi Trailing (K) rotation angle. * @param lambda Leading (K) rotation angle. - * @param phase Global phase to emit, in radians. - * @param basis Euler basis selecting the K and A rotation axes. - * @return The transformed qubit value. + * @param phase Global phase in radians. + * @param basis Euler basis selecting the rotation axes. + * @return The output qubit value. */ static Value emitKAK(OpBuilder& builder, Location loc, Value qubit, double theta, double phi, double lambda, double phase, @@ -342,8 +350,14 @@ EulerDecomposition::anglesFromUnitary(const Eigen::Matrix2cd& matrix, return paramsZXZ(matrix); case EulerBasis::U: return paramsU(matrix); - case EulerBasis::ZSXX: - return paramsPSX(matrix); + case EulerBasis::ZSXX: { + const auto zyz = paramsZYZ(matrix); + const auto seq = sequenceFromZYZForPSX(zyz.theta, zyz.phi, zyz.lambda); + return {.theta = zyz.theta, + .phi = zyz.phi, + .lambda = zyz.lambda, + .phase = zyz.phase + globalPhaseOffsetForPSX(seq)}; + } } llvm::reportFatalInternalError( "Unsupported Euler basis for angle computation in decomposition!"); @@ -398,15 +412,6 @@ EulerAngles EulerDecomposition::paramsU(const Eigen::Matrix2cd& matrix) { .phase = zyz.phase + globalPhaseOffsetForU(zyz.phi, zyz.lambda)}; } -EulerAngles EulerDecomposition::paramsPSX(const Eigen::Matrix2cd& matrix) { - const auto zyz = paramsZYZ(matrix); - return {.theta = zyz.theta, - .phi = zyz.phi, - .lambda = zyz.lambda, - .phase = zyz.phase + - globalPhaseOffsetForPSX(zyz.theta, zyz.phi, zyz.lambda)}; -} - //===----------------------------------------------------------------------===// // Euler synthesis (IR emission) //===----------------------------------------------------------------------===// @@ -479,11 +484,11 @@ std::size_t synthesisGateCount(const Eigen::Matrix2cd& targetMatrix, // emitKAK always emits the full K-A-K rotation triple. return 3; case EulerBasis::ZSXX: { - const auto angles = - EulerDecomposition::anglesFromUnitary(targetMatrix, EulerBasis::ZSXX); - const auto seq = - sequenceFromZYZForPSX(angles.theta, angles.phi, angles.lambda); - return seq.middle == PSXSequence::Middle::SXRZSX ? 5U : 3U; + const double theta = 2. * std::atan2(std::abs(targetMatrix(1, 0)), + std::abs(targetMatrix(0, 0))); + return classifyPSXMiddleFromZYZTheta(theta) == PSXSequence::Middle::SXRZSX + ? 5U + : 3U; } } llvm::reportFatalInternalError("Unhandled Euler basis in synthesisGateCount"); diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp index 390a709864..a176e8b1b7 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp @@ -39,15 +39,13 @@ namespace mlir::qco { #include "mlir/Dialect/QCO/Transforms/Passes.h.inc" /** - * @brief Whether `op` lives inside an `inv`/`ctrl` modifier body. + * @brief Whether `op` is inside an `inv`/`ctrl` body. * - * A modifier exposes its body's combined unitary through `getUnitaryMatrix` and - * is fused as a single, atomic run member (when single-qubit). Fusing the gates - * inside its body would invalidate that matrix, so body gates are never run - * members themselves. + * The modifier's combined unitary is fused as one run member; gates inside its + * body are not separate run members. * * @param op The operation to test. - * @return `true` if `op`'s parent is an `inv` or `ctrl` op. + * @return `true` if the parent is `inv` or `ctrl`. */ static bool isNestedInModifierRegion(Operation* op) { Operation* parent = op->getParentOp(); @@ -58,8 +56,8 @@ static bool isNestedInModifierRegion(Operation* op) { * @brief Whether `op` may participate in a fusable single-qubit run. * * @param op The unitary operation to test. - * @return `true` for a single-qubit, matrix-backed unitary that lives directly - * on a wire (not inside a modifier body). + * @return `true` for a single-qubit, matrix-backed unitary on the wire, outside + * a modifier body. */ static bool isFuseCandidate(UnitaryOpInterface op) { if (!op || !op.isSingleQubit() || isNestedInModifierRegion(op)) { @@ -72,8 +70,7 @@ static bool isFuseCandidate(UnitaryOpInterface op) { * @brief Returns the compile-time 2x2 unitary matrix of `op`, if available. * * @param op The unitary operation to query. - * @return The constant matrix, or `std::nullopt` if `op` is not matrix-backed - * or its matrix is not known at compile time. + * @return The matrix, or `std::nullopt` if not known at compile time. */ static std::optional getConstMatrix(UnitaryOpInterface op) { auto matrixOp = dyn_cast(op.getOperation()); @@ -91,8 +88,7 @@ static std::optional getConstMatrix(UnitaryOpInterface op) { * @brief Whether `op` can participate in a fusable run. * * @param op The operation to test. - * @return `true` for a single-qubit, matrix-backed unitary outside a modifier - * body whose matrix is known at compile time. + * @return `true` for a fuse candidate with a known compile-time matrix. */ static bool isRunMember(Operation* op) { auto iface = dyn_cast(op); @@ -102,29 +98,27 @@ static bool isRunMember(Operation* op) { /** * @brief Composes a run of unitary ops into a single matrix. * - * @param run The run members in execution (circuit) order. - * @return The product of the members' matrices. + * @param run The run members in circuit order. + * @return The product of their matrices. */ static Eigen::Matrix2cd composeRun(ArrayRef run) { Eigen::Matrix2cd composed = Eigen::Matrix2cd::Identity(); for (auto op : run) { - // Execution order: first op applied first => multiply on the left. + // First gate in the run is applied first (left factor). composed = (*getConstMatrix(op)) * composed; } return composed; } /** - * @brief Whether `op` is one of the gates the target `basis` emits. + * @brief Whether `op` is a gate the target `basis` emits. * - * The gate sets mirror `emitKAK` and `emitFromPSXSequence` in `Euler.cpp`. The - * greedy driver re-visits the gates produced by a rewrite, so this lets the - * pattern detect a run that is already expressed entirely in the target basis - * and avoid re-fusing the gates it just produced. + * Gate sets match `emitKAK` and `emitFromPSXSequence` in `Euler.cpp`. Used to + * skip runs that are already in the target basis at canonical length. * * @param op The operation to classify. * @param basis The target Euler basis. - * @return `true` if `op` is a gate the `basis` emits. + * @return `true` if `op` is emitted by synthesis in `basis`. */ static bool isTargetBasisGate(Operation* op, decomposition::EulerBasis basis) { using decomposition::EulerBasis; @@ -148,7 +142,9 @@ static bool isTargetBasisGate(Operation* op, decomposition::EulerBasis basis) { namespace { /** - * @brief Replaces a maximal single-qubit unitary run with its Euler synthesis. + * @brief Fuses maximal single-qubit unitary runs via Euler resynthesis. + * + * Matches at each run head so each run is rewritten once. */ struct FuseSingleQubitUnitaryRunsPattern final : OpInterfaceRewritePattern { @@ -161,11 +157,8 @@ struct FuseSingleQubitUnitaryRunsPattern final /** * @brief Whether `op` is the head of a run. * - * A run head is a fusable op whose predecessor on the wire is not itself a - * fusable run member, so each run is matched exactly once at its start. - * * @param op The candidate run head. - * @return `true` if `op` starts a run. + * @return `true` if the wire predecessor is not a run member. */ static bool isRunStart(UnitaryOpInterface op) { if (!isRunMember(op.getOperation())) { @@ -176,11 +169,9 @@ struct FuseSingleQubitUnitaryRunsPattern final } /** - * @brief Collects the maximal run of fusable ops starting at `start`. - * - * Follows the wire forward while staying within the same block. + * @brief Collects the maximal fusable run starting at `start`. * - * @param start The run head (must satisfy `isRunStart`). + * @param start The run head. * @return The run members in circuit order. */ static SmallVector collectRun(UnitaryOpInterface start) { @@ -198,10 +189,13 @@ struct FuseSingleQubitUnitaryRunsPattern final } /** - * @brief Fuses the run anchored at `op` into its Euler resynthesis. + * @brief Fuses the run anchored at `op` when beneficial. + * + * Fuses if the run contains a non-basis gate or is longer than the canonical + * synthesis for its composed matrix. * * @param op The matched unitary operation. - * @param rewriter Pattern rewriter used to apply the transformation. + * @param rewriter The pattern rewriter. * @return `success()` if a run was fused, `failure()` otherwise. */ LogicalResult matchAndRewrite(UnitaryOpInterface op, @@ -212,13 +206,6 @@ struct FuseSingleQubitUnitaryRunsPattern final auto run = collectRun(op); const Eigen::Matrix2cd composed = composeRun(run); - - // Resynthesize a run when it either contains a gate outside the target - // basis (so it is not yet expressed in the native gate set) or is already - // in-basis but longer than the canonical Euler form (so fusing shortens - // it). A run that is in-basis and already at canonical length is left - // untouched, which is also what keeps the greedy driver from re-matching - // the gates this pattern just produced. const bool hasNonBasisGate = llvm::any_of(run, [&](UnitaryOpInterface member) { return !isTargetBasisGate(member.getOperation(), basis); 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 1c690a77c2..1aafd01aad 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -111,7 +111,7 @@ template static void forEachBasis(Fn fn) { } static bool isAllowedBasisGate(Operation& op, StringRef basis) { - // Always allow global phase as correction term. + // `gphase` is always allowed. if (isa(op)) { return true; } @@ -145,8 +145,7 @@ static bool isAllowedBasisGate(Operation& op, StringRef basis) { return false; } -// A single-qubit gate carrying a unitary matrix (excludes barriers, the -// 2-qubit boundary gate, and the global-phase correction). +// Matrix-backed 1Q gate (not barrier, 2Q, or `gphase`). [[nodiscard]] static bool isOneQubitGate(Operation& op) { if (isa(op) || !isa(op)) { return false; @@ -155,8 +154,7 @@ static bool isAllowedBasisGate(Operation& op, StringRef basis) { return u && u.isSingleQubit(); } -// Asserts that at least one 1Q gate remains on each side of the first op -// matching `isBoundary` in the function's entry block. +// At least one 1Q gate before and after the first `isBoundary` op in `main`. template static void expectOneQubitGatesAroundBoundary(func::FuncOp funcOp, StringRef basis, @@ -194,7 +192,7 @@ static void expectBasisGatesOnly(func::FuncOp funcOp, StringRef basis) { continue; } - // Only check ops that claim to carry a unitary matrix (i.e., actual gates). + // Matrix-backed ops must be allowed basis gates. if (isa(op)) { EXPECT_TRUE(isAllowedBasisGate(op, basis)) << "basis=" << basis.str() @@ -208,12 +206,8 @@ static Eigen::Matrix2cd compute1QMatrixFromFunction(func::FuncOp funcOp) { std::complex global{1.0, 0.0}; bool failed = false; - // Walk every block, descending into nested regions (e.g. `scf.for` bodies) so - // loop-body gates contribute too. The pre-order walk lets us account for a - // matrix-backed modifier (`inv`/`ctrl`) via its combined matrix and then skip - // its body, avoiding double-counting the gates nested inside it. The 1q gates - // of these tests live in a single block, so the walk order matches the - // execution order. + // Include nested regions (`scf.for`); skip `inv`/`ctrl` bodies after the + // modifier op (combined matrix already counted). funcOp.walk([&](Operation* op) -> WalkResult { if (isa(*op) || isTwoQubitGate(*op)) { return WalkResult::advance(); @@ -227,7 +221,6 @@ static Eigen::Matrix2cd compute1QMatrixFromFunction(func::FuncOp funcOp) { } if (auto iface = dyn_cast(*op)) { - // All matrix-backed ops in these tests should be 1q ops after synthesis. const auto maybeM = iface.getUnitaryMatrix(); if (!maybeM) { ADD_FAILURE() << "Expected constant unitary matrix for op: " @@ -261,7 +254,7 @@ static void singleQubitRunWithSingleQubitGate(QCOProgramBuilder& b) { q[0] = b.h(q[0]); q[0] = b.t(q[0]); q[0] = b.rz(0.123, q[0]); - // Keep `inv` inside the single-qubit run so it gets fused/resynthesized too. + // `inv` is part of the fusable run. q[0] = b.inv({q[0]}, [&](ValueRange targets) -> SmallVector { return {b.sx(targets[0])}; })[0]; @@ -286,15 +279,13 @@ static void singleQubitRunsSplitByBarrier(QCOProgramBuilder& b) { q[0] = b.sx(q[0]); } -// A lone gate that is not part of any of the target bases. The pass should -// still resynthesize it into the requested basis. +// Single `H` gate — not in any target basis; should still be resynthesized. static void singleNonBasisGate(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.h(q[0]); } -// A run made up solely of `RZ`/`RY` gates: already in the `zyz` basis, but -// longer than the canonical three-gate Euler form, so it should still shrink. +// Six `RZ`/`RY` gates in `zyz` basis — longer than canonical (3). static void overlongZyzRun(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.rz(0.3, q[0]); @@ -649,29 +640,30 @@ TEST_P(EulerSynthesisExactTest, ReconstructsReferenceMatrices) { INSTANTIATE_TEST_SUITE_P( SingleQubitMatrices, EulerSynthesisExactTest, - testing::Combine( - testing::Values(EulerBasis::XYX, EulerBasis::XZX, EulerBasis::ZYZ, - EulerBasis::ZXZ, EulerBasis::U, EulerBasis::ZSXX), - testing::Values( - [](MLIRContext* /*ctx*/) -> Eigen::Matrix2cd { - return Eigen::Matrix2cd::Identity(); - }, - [](MLIRContext* ctx) -> Eigen::Matrix2cd { - return rotationMatrix(ctx, 2.0); - }, - // RY(pi/2) hits the ZSXX single-SX branch (theta == pi/2). - [](MLIRContext* ctx) -> Eigen::Matrix2cd { - return rotationMatrix(ctx, std::numbers::pi / 2.0); - }, - [](MLIRContext* ctx) -> Eigen::Matrix2cd { - return rotationMatrix(ctx, 0.5); - }, - [](MLIRContext* ctx) -> Eigen::Matrix2cd { - return rotationMatrix(ctx, 3.14); - }, - [](MLIRContext* /*ctx*/) -> Eigen::Matrix2cd { - return HOp::getUnitaryMatrix(); - }))); + testing::Combine(testing::Values(EulerBasis::XYX, EulerBasis::XZX, + EulerBasis::ZYZ, EulerBasis::ZXZ, + EulerBasis::U, EulerBasis::ZSXX), + testing::Values( + [](MLIRContext* /*ctx*/) -> Eigen::Matrix2cd { + return Eigen::Matrix2cd::Identity(); + }, + [](MLIRContext* ctx) -> Eigen::Matrix2cd { + return rotationMatrix(ctx, 2.0); + }, + // RY(pi/2): ZSXX single-SX branch. + [](MLIRContext* ctx) -> Eigen::Matrix2cd { + return rotationMatrix(ctx, + std::numbers::pi / 2.0); + }, + [](MLIRContext* ctx) -> Eigen::Matrix2cd { + return rotationMatrix(ctx, 0.5); + }, + [](MLIRContext* ctx) -> Eigen::Matrix2cd { + return rotationMatrix(ctx, 3.14); + }, + [](MLIRContext* /*ctx*/) -> Eigen::Matrix2cd { + return HOp::getUnitaryMatrix(); + }))); TEST(FuseSingleQubitUnitaryRunsTest, DoesNotFuseAcrossTwoQGateAllBases) { SynthesisFixture fx; From 0302530d4febd713eb106119a40e16a54fa0337d Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 10 Jun 2026 09:44:27 +0200 Subject: [PATCH 26/68] =?UTF-8?q?=F0=9F=8E=A8=20missing=20new=20line?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/unittests/programs/qco_programs.h | 1 + 1 file changed, 1 insertion(+) diff --git a/mlir/unittests/programs/qco_programs.h b/mlir/unittests/programs/qco_programs.h index f4dc65e58d..b4197c5a7f 100644 --- a/mlir/unittests/programs/qco_programs.h +++ b/mlir/unittests/programs/qco_programs.h @@ -1077,4 +1077,5 @@ void qtensorInsertExtractIndexMismatch(QCOProgramBuilder& b); /// Inserts a qubit into a tensor and extracts it immediately at the same index. void qtensorInsertExtractSameIndex(QCOProgramBuilder& b); + } // namespace mlir::qco From b8d506d6239e28d15158596d753a7ffa1321131a Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 10 Jun 2026 09:56:46 +0200 Subject: [PATCH 27/68] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20linter=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp | 1 + .../QCO/Transforms/Decomposition/test_euler_decomposition.cpp | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index a9c3019bd1..5e8009b194 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -11,6 +11,7 @@ #include "mlir/Dialect/QCO/Transforms/Decomposition/Euler.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" #include "mlir/Dialect/Utils/Utils.h" #include 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 10a8c99b96..462f5ef368 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -70,6 +70,8 @@ struct SynthesizedCircuit { func::FuncOp func; }; +} // namespace + [[nodiscard]] static Matrix2x2 scaleMatrix(const Matrix2x2& matrix, Complex scale) { return Matrix2x2::fromElements(scale * matrix(0, 0), scale * matrix(0, 1), @@ -88,8 +90,6 @@ struct SynthesizedCircuit { return unitary.getUnitaryMatrix2x2(matrix); } -} // namespace - [[nodiscard]] static Matrix2x2 rzMatrix(double theta) { const auto m00 = std::polar(1.0, -theta / 2.0); const auto m11 = std::polar(1.0, theta / 2.0); From 3496c5f262b084453d40d798daa59454311368ed Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 10 Jun 2026 11:07:49 +0200 Subject: [PATCH 28/68] =?UTF-8?q?=F0=9F=8E=A8=20Enhance=20Euler=20decompos?= =?UTF-8?q?ition:=20update=20documentation,=20add=20support=20for=20`OnlyR?= =?UTF-8?q?Z`=20case,=20and=20improve=20synthesis=20gate=20count=20logic?= =?UTF-8?q?=20for=20`ZSXX`=20basis.=20Introduce=20utility=20functions=20fo?= =?UTF-8?q?r=20constant=202x2=20matrices=20in=20tests.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/Decomposition/Euler.h | 5 +- .../QCO/Transforms/Decomposition/Euler.cpp | 41 +++-- .../FuseSingleQubitUnitaryRuns.cpp | 6 +- .../test_euler_decomposition.cpp | 148 ++++++++++++++++-- 4 files changed, 168 insertions(+), 32 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h index 7781bd10e7..81bd828bb2 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h @@ -56,7 +56,10 @@ class EulerDecomposition { public: /** - * @brief Extracts `(theta, phi, lambda, phase)` for the requested basis. + * @brief Extracts `(theta, phi, lambda, phase)` for KAK and `U` bases. + * + * Does not support `EulerBasis::ZSXX`; use `synthesizeUnitary1QEuler` or + * `synthesisGateCount` instead. * * @param matrix The single-qubit unitary to decompose. * @param basis The target Euler basis. diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index 5e8009b194..e127c922e1 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -97,7 +97,7 @@ namespace { * @brief Planned PSX (`RZ` / `SX` / `X`) chain; angles in circuit order. */ struct PSXSequence { - enum class Middle : std::uint8_t { OneSX, X, SXRZSX }; + enum class Middle : std::uint8_t { OnlyRZ, OneSX, X, SXRZSX }; Middle middle = Middle::SXRZSX; double firstRZ = 0.0; double midRZ = 0.0; @@ -109,8 +109,8 @@ struct PSXSequence { /** * @brief Classifies the PSX middle-gate case from ZYZ `theta`. * - * For `theta` in `[0, pi]`: `pi/2` → one `SX`, `pi` → `X`, otherwise - * `SX*RZ*SX`. + * For `theta` in `[0, pi]`: `0` → pure `RZ` chain, `pi/2` → one `SX`, `pi` → + * `X`, otherwise `SX*RZ*SX`. * * @param theta Y-rotation angle from `paramsZYZ`. * @return The PSX middle-gate case. @@ -121,6 +121,9 @@ classifyPSXMiddleFromZYZTheta(double theta) { constexpr double halfPi = std::numbers::pi / 2.0; constexpr double pi = std::numbers::pi; + if (theta < eps) { + return PSXSequence::Middle::OnlyRZ; + } if (std::abs(theta - halfPi) < eps) { return PSXSequence::Middle::OneSX; } @@ -146,6 +149,11 @@ classifyPSXMiddleFromZYZTheta(double theta) { constexpr double pi = std::numbers::pi; switch (classifyPSXMiddleFromZYZTheta(theta)) { + case PSXSequence::Middle::OnlyRZ: + return {.middle = PSXSequence::Middle::OnlyRZ, + .firstRZ = lambda, + .midRZ = 0.0, + .lastRZ = phi}; case PSXSequence::Middle::OneSX: return {.middle = PSXSequence::Middle::OneSX, .firstRZ = lambda - halfPi, @@ -201,6 +209,9 @@ classifyPSXMiddleFromZYZTheta(double theta) { constexpr double quarterPi = std::numbers::pi / 4.0; switch (seq.middle) { + case PSXSequence::Middle::OnlyRZ: + return globalPhaseFromRZWrap(seq.firstRZ) + + globalPhaseFromRZWrap(seq.lastRZ); case PSXSequence::Middle::OneSX: // `SX = exp(i*pi/4)*RZ(-pi/2)*RY(pi/2)*RZ(pi/2)`; the outer RZ angles // absorb the +-pi/2, leaving the exp(i*pi/4) phase. RZ wraps add too. @@ -232,6 +243,9 @@ static void visitSequenceInTimeOrder(const PSXSequence& seq, llvm::function_ref onX) { onRZ(seq.firstRZ); switch (seq.middle) { + case PSXSequence::Middle::OnlyRZ: + onRZ(seq.lastRZ); + break; case PSXSequence::Middle::OneSX: onSX(); onRZ(seq.lastRZ); @@ -348,14 +362,6 @@ EulerAngles EulerDecomposition::anglesFromUnitary(const Matrix2x2& matrix, return paramsZXZ(matrix); case EulerBasis::U: return paramsU(matrix); - case EulerBasis::ZSXX: { - const auto zyz = paramsZYZ(matrix); - const auto seq = sequenceFromZYZForPSX(zyz.theta, zyz.phi, zyz.lambda); - return {.theta = zyz.theta, - .phi = zyz.phi, - .lambda = zyz.lambda, - .phase = zyz.phase + globalPhaseOffsetForPSX(seq)}; - } } llvm::reportFatalInternalError( "Unsupported Euler basis for angle computation in decomposition!"); @@ -484,9 +490,16 @@ std::size_t synthesisGateCount(const Matrix2x2& targetMatrix, case EulerBasis::ZSXX: { const double theta = 2. * std::atan2(std::abs(targetMatrix(1, 0)), std::abs(targetMatrix(0, 0))); - return classifyPSXMiddleFromZYZTheta(theta) == PSXSequence::Middle::SXRZSX - ? 5U - : 3U; + switch (classifyPSXMiddleFromZYZTheta(theta)) { + case PSXSequence::Middle::OnlyRZ: + return 2U; + case PSXSequence::Middle::SXRZSX: + return 5U; + case PSXSequence::Middle::OneSX: + case PSXSequence::Middle::X: + return 3U; + } + llvm::reportFatalInternalError("Unhandled PSX middle gate"); } } llvm::reportFatalInternalError("Unhandled Euler basis in synthesisGateCount"); diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp index e8e5712a51..18ed75ce69 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp @@ -44,11 +44,11 @@ namespace mlir::qco { * body are not separate run members. * * @param op The operation to test. - * @return `true` if the parent is `inv` or `ctrl`. + * @return `true` if any ancestor is `inv` or `ctrl`. */ static bool isNestedInModifierRegion(Operation* op) { - Operation* parent = op->getParentOp(); - return parent != nullptr && isa(parent); + return op != nullptr && (op->getParentOfType() != nullptr || + op->getParentOfType() != nullptr); } /** 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 462f5ef368..504d40f4d3 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -78,6 +78,25 @@ struct SynthesizedCircuit { scale * matrix(1, 0), scale * matrix(1, 1)); } +[[nodiscard]] static bool tryGetConstant2x2Matrix(UnitaryOpInterface unitary, + Matrix2x2& out) { + if (unitary.getUnitaryMatrix2x2(out)) { + return true; + } + DynamicMatrix dynamic; + if (!unitary.getUnitaryMatrixDynamic(dynamic) || dynamic.rows() != 2 || + dynamic.cols() != 2) { + return false; + } + for (std::size_t row = 0; row < 2; ++row) { + for (std::size_t col = 0; col < 2; ++col) { + out(row, col) = dynamic(static_cast(row), + static_cast(col)); + } + } + return true; +} + [[nodiscard]] static bool hasConstant2x2Matrix(Operation& op) { if (isa(op)) { return false; @@ -87,7 +106,7 @@ struct SynthesizedCircuit { return false; } Matrix2x2 matrix; - return unitary.getUnitaryMatrix2x2(matrix); + return tryGetConstant2x2Matrix(unitary, matrix); } [[nodiscard]] static Matrix2x2 rzMatrix(double theta) { @@ -191,6 +210,14 @@ static void expectOneQubitGatesAroundBoundary(func::FuncOp funcOp, EXPECT_GE(after, 1U) << "basis=" << basis.str(); } +[[nodiscard]] static bool isSingleQubitUnitaryOp(Operation& op) { + if (isa(op)) { + return false; + } + auto unitary = dyn_cast(op); + return unitary && unitary.isSingleQubit(); +} + static void expectBasisGatesOnly(func::FuncOp funcOp, StringRef basis) { auto& block = funcOp.getBody().front(); for (Operation& op : block.without_terminator()) { @@ -198,16 +225,21 @@ static void expectBasisGatesOnly(func::FuncOp funcOp, StringRef basis) { continue; } - if (isTwoQubitGate(op)) { + if (isTwoQubitGate(op) || isa(op)) { continue; } - // Matrix-backed ops must be allowed basis gates. - if (hasConstant2x2Matrix(op)) { - EXPECT_TRUE(isAllowedBasisGate(op, basis)) - << "basis=" << basis.str() - << " unexpected gate: " << op.getName().getStringRef().str(); + if (!isSingleQubitUnitaryOp(op)) { + continue; } + + Matrix2x2 matrix; + ASSERT_TRUE(tryGetConstant2x2Matrix(cast(op), matrix)) + << "basis=" << basis.str() << " missing constant matrix for: " + << op.getName().getStringRef().str(); + EXPECT_TRUE(isAllowedBasisGate(op, basis)) + << "basis=" << basis.str() + << " unexpected gate: " << op.getName().getStringRef().str(); } } @@ -219,7 +251,11 @@ static Matrix2x2 compute1QMatrixFromFunction(func::FuncOp funcOp) { // Include nested regions (`scf.for`); skip `inv`/`ctrl` bodies after the // modifier op (combined matrix already counted). funcOp.walk([&](Operation* op) -> WalkResult { - if (isa(*op) || isTwoQubitGate(*op)) { + if (isa(*op)) { + return WalkResult::advance(); + } + + if (isa(*op)) { return WalkResult::advance(); } @@ -230,19 +266,37 @@ static Matrix2x2 compute1QMatrixFromFunction(func::FuncOp funcOp) { return WalkResult::advance(); } - if (isa(*op)) { + if (isa(*op)) { + auto unitary = cast(*op); + if (unitary.isSingleQubit()) { + Matrix2x2 matrix; + if (!tryGetConstant2x2Matrix(unitary, matrix)) { + ADD_FAILURE() << "Expected constant unitary matrix for op: " + << op->getName().getStringRef().str(); + failed = true; + return WalkResult::interrupt(); + } + acc = matrix * acc; + } + return WalkResult::skip(); + } + + if (isTwoQubitGate(*op)) { return WalkResult::advance(); } if (auto unitary = dyn_cast(*op)) { - Matrix2x2 matrix; - if (!unitary.getUnitaryMatrix2x2(matrix)) { + if (!unitary.isSingleQubit()) { return WalkResult::advance(); } - acc = matrix * acc; - if (isa(*op)) { - return WalkResult::skip(); + Matrix2x2 matrix; + if (!tryGetConstant2x2Matrix(unitary, matrix)) { + ADD_FAILURE() << "Expected constant unitary matrix for op: " + << op->getName().getStringRef().str(); + failed = true; + return WalkResult::interrupt(); } + acc = matrix * acc; return WalkResult::advance(); } @@ -310,6 +364,15 @@ static void overlongZyzRun(QCOProgramBuilder& b) { q[0] = b.ry(1.3, q[0]); } +// `SX`/`RZ(pi)`/`SX` in `zsxx` basis — composes to `Z` (pure-Z, theta = 0). +// Consecutive-`RZ` merge patterns do not apply across `SX` gates. +static void overlongZsxxMixedPureZRun(QCOProgramBuilder& b) { + auto q = b.allocQubitRegister(1); + q[0] = b.sx(q[0]); + q[0] = b.rz(std::numbers::pi, q[0]); + q[0] = b.sx(q[0]); +} + static void singleQubitRunInScfFor(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.scfFor(0, 1, 1, ValueRange{q[0]}, [&](Value, ValueRange iterArgs) { @@ -563,6 +626,45 @@ TEST(FuseSingleQubitUnitaryRunsTest, FusesOverlongInBasisRun) { expectBasisGatesOnly(funcOp, "zyz"); } +TEST(FuseSingleQubitUnitaryRunsTest, + FusesOverlongZsxxMixedRunComposingToPureZ) { + SynthesisFixture fx; + fx.setUp(); + + auto owned = buildProgram(fx.context.get(), &overlongZsxxMixedPureZRun); + ASSERT_TRUE(owned); + ModuleOp module = *owned; + ASSERT_TRUE(succeeded(verify(module))); + + auto funcOp = lookupMain(module); + ASSERT_TRUE(funcOp); + const Matrix2x2 original = compute1QMatrixFromFunction(funcOp); + EXPECT_EQ(synthesisGateCount(original, EulerBasis::ZSXX), 2U); + const std::size_t beforeGates = + countOps(funcOp) + countOps(funcOp) + countOps(funcOp); + ASSERT_EQ(beforeGates, 3U); + EXPECT_EQ(countOps(funcOp), 2U); + EXPECT_EQ(countOps(funcOp), 1U); + EXPECT_EQ(countOps(funcOp), 0U); + + ASSERT_TRUE(succeeded(runFuse(module, "zsxx"))); + ASSERT_TRUE(succeeded(verify(module))); + + funcOp = lookupMain(module); + ASSERT_TRUE(funcOp); + const std::size_t afterGates = + countOps(funcOp) + countOps(funcOp) + countOps(funcOp); + // `OnlyRZ` synthesis emits `RZ(pi)` then `RZ(0)`; the latter is identity. + EXPECT_EQ(countOps(funcOp), 1U); + EXPECT_EQ(countOps(funcOp), 0U); + EXPECT_EQ(countOps(funcOp), 0U); + EXPECT_LE(afterGates, synthesisGateCount(original, EulerBasis::ZSXX)); + EXPECT_LT(afterGates, beforeGates); + EXPECT_TRUE(compute1QMatrixFromFunction(funcOp).isApprox( + original, mlir::utils::TOLERANCE)); + expectBasisGatesOnly(funcOp, "zsxx"); +} + TEST(EulerSynthesisTest, ZsxxPauliXUsesXGateShortcut) { SynthesisFixture fx; fx.setUp(); @@ -578,6 +680,24 @@ TEST(EulerSynthesisTest, ZsxxPauliXUsesXGateShortcut) { .isApprox(pauliX, mlir::utils::TOLERANCE)); } +TEST(EulerSynthesisTest, ZsxxPureZRotationUsesOnlyRZShortcut) { + SynthesisFixture fx; + fx.setUp(); + + const Matrix2x2 pureZ = rzMatrix(0.3) * rzMatrix(0.7); + EXPECT_EQ(synthesisGateCount(pureZ, EulerBasis::ZSXX), 2U); + + const auto circuit = + synthesizeMatrix(fx.context.get(), pureZ, EulerBasis::ZSXX); + + ASSERT_TRUE(succeeded(verify(*circuit.module))); + EXPECT_EQ(countOps(circuit.func), 2U); + EXPECT_EQ(countOps(circuit.func), 0U); + EXPECT_EQ(countOps(circuit.func), 0U); + EXPECT_TRUE(compute1QMatrixFromFunction(circuit.func) + .isApprox(pureZ, mlir::utils::TOLERANCE)); +} + TEST(EulerSynthesisTest, UGateReconstruction) { SynthesisFixture fx; fx.setUp(); From 5d08032fd07b1e623d21d6154ee6c1ac9adc6271 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 10 Jun 2026 11:15:12 +0200 Subject: [PATCH 29/68] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20linter=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/Decomposition/test_euler_decomposition.cpp | 1 + 1 file changed, 1 insertion(+) 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 504d40f4d3..e9b250a14b 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include #include #include From 5c6b317bde3c874af8ad0d63f34d0ff2d79c3a3b Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 10 Jun 2026 14:05:24 +0200 Subject: [PATCH 30/68] =?UTF-8?q?=F0=9F=8E=A8=20Enhance=20QCO=20matrix=20h?= =?UTF-8?q?andling:=20add=20`assignFrom`=20methods=20for=20`DynamicMatrix`?= =?UTF-8?q?=20in=20`Matrix1x1`,=20`Matrix2x2`,=20and=20`Matrix4x4`.=20Impr?= =?UTF-8?q?ove=20`synthesisGateCount`=20logic=20and=20introduce=20utility?= =?UTF-8?q?=20functions=20for=20gate=20counting=20in=20Euler=20decompositi?= =?UTF-8?q?on.=20Update=20fuse=20pass=20to=20utilize=20new=20logic=20for?= =?UTF-8?q?=20determining=20run=20shortening.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mlir/Dialect/QCO/IR/QCOInterfaces.td | 15 + .../QCO/Transforms/Decomposition/Euler.h | 43 +- .../mlir/Dialect/QCO/Transforms/Passes.td | 6 +- mlir/include/mlir/Dialect/QCO/Utils/Matrix.h | 26 ++ .../QCO/Transforms/Decomposition/Euler.cpp | 133 ++++-- .../FuseSingleQubitUnitaryRuns.cpp | 2 +- mlir/lib/Dialect/QCO/Utils/Matrix.cpp | 33 ++ .../test_euler_decomposition.cpp | 396 +++++++++--------- .../Dialect/QCO/Utils/test_unitary_matrix.cpp | 24 ++ 9 files changed, 446 insertions(+), 232 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/IR/QCOInterfaces.td b/mlir/include/mlir/Dialect/QCO/IR/QCOInterfaces.td index 3384d7a048..e00e52334d 100644 --- a/mlir/include/mlir/Dialect/QCO/IR/QCOInterfaces.td +++ b/mlir/include/mlir/Dialect/QCO/IR/QCOInterfaces.td @@ -39,6 +39,11 @@ def UnitaryOpInterface : OpInterface<"UnitaryOpInterface"> { return true; } return false; + } else if constexpr (std::is_same_v) { + if (auto matrix = $_op.getUnitaryMatrix()) { + return out.assignFrom(*matrix); + } + return false; } else { return false; } @@ -63,6 +68,11 @@ def UnitaryOpInterface : OpInterface<"UnitaryOpInterface"> { return true; } return false; + } else if constexpr (std::is_same_v) { + if (auto matrix = $_op.getUnitaryMatrix()) { + return out.assignFrom(*matrix); + } + return false; } else { return false; } @@ -87,6 +97,11 @@ def UnitaryOpInterface : OpInterface<"UnitaryOpInterface"> { return true; } return false; + } else if constexpr (std::is_same_v) { + if (auto matrix = $_op.getUnitaryMatrix()) { + return out.assignFrom(*matrix); + } + return false; } else { return false; } diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h index 81bd828bb2..77575b310e 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h @@ -136,15 +136,52 @@ class EulerDecomposition { const Matrix2x2& targetMatrix, EulerBasis basis); +/** + * @brief Upper bound on basis gates `synthesizeUnitary1QEuler` may emit. + * + * @param basis The target Euler basis. + * @return `1` for `U`, `3` for KAK bases, `5` for `ZSXX`. + */ +[[nodiscard]] constexpr std::size_t maxSynthesisGateCount(EulerBasis basis) { + switch (basis) { + case EulerBasis::U: + return 1; + case EulerBasis::ZYZ: + case EulerBasis::ZXZ: + case EulerBasis::XZX: + case EulerBasis::XYX: + return 3; + case EulerBasis::ZSXX: + return 5; + } +} + +/** + * @brief Whether an in-basis run would shorten after Euler resynthesis. + * + * Uses `maxSynthesisGateCount` as a cheap shortcut before falling back to + * `synthesisGateCount`. + * + * @param runSize Number of gates in the run. + * @param composed Composed unitary of the run. + * @param basis The target Euler basis. + * @return `true` when resynthesis would emit fewer basis gates than @p runSize. + */ +[[nodiscard]] bool wouldShortenInBasisRun(std::size_t runSize, + const Matrix2x2& composed, + EulerBasis basis); + /** * @brief Number of basis gates `synthesizeUnitary1QEuler` would emit. * - * Excludes `qco.gphase`. Used by the fuse pass to detect overlong in-basis - * runs. + * Excludes `qco.gphase` and near-zero rotations that synthesis skips. * * @param targetMatrix The single-qubit unitary that would be synthesized. * @param basis The target Euler basis. - * @return The gate count (1 for `U`, 3 for KAK bases, 3 or 5 for `ZSXX`). + * @return The gate count (1 for `U`, up to 3 for KAK bases, up to 5 for + * `ZSXX`). For `ZSXX`, pure-Z (`OnlyRZ`) compositions count non-zero + * `RZ` gates only (1 or 2); `OneSX` and `X` shortcuts count 3; the + * generic case counts up to 5. */ [[nodiscard]] std::size_t synthesisGateCount(const Matrix2x2& targetMatrix, EulerBasis basis); diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index 957836c749..31ccbe85a4 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -49,7 +49,11 @@ def FuseSingleQubitUnitaryRuns let description = [{ Matches maximal runs of consecutive single-qubit unitary operations on the same qubit wire (anchored at each run head), composes their constant unitary - matrices, and replaces each run with an equivalent sequence of basis gates. + matrices, and replaces a run with an equivalent sequence of basis gates when + beneficial: when the run contains a gate outside the target `basis`, or when + its length exceeds `synthesisGateCount` for the composed matrix. Runs that + are already in the target `basis` and no longer than that canonical count + are left unchanged. The emitted basis is controlled via the `basis` option (e.g. `zyz`, `zsxx`). A `gphase` correction is inserted when needed so the rewritten sequence diff --git a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h index 2305a06346..51229befc0 100644 --- a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h +++ b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h @@ -25,6 +25,8 @@ using Complex = std::complex; /// Default absolute tolerance for matrix comparisons. inline constexpr double MATRIX_TOLERANCE = 1e-14; +class DynamicMatrix; + /** * @brief 1x1 matrix for global-phase gates. * @@ -66,6 +68,14 @@ struct Matrix1x1 { */ [[nodiscard]] bool isApprox(const Matrix1x1& other, double tol = MATRIX_TOLERANCE) const; + + /** + * @brief Replaces this matrix with a copy of a 1x1 dynamic matrix. + * + * @param src Source matrix. + * @return `true` when @p src is 1x1. + */ + [[nodiscard]] bool assignFrom(const DynamicMatrix& src); }; /** @@ -157,6 +167,14 @@ struct Matrix2x2 { */ [[nodiscard]] bool isApprox(const Matrix2x2& other, double tol = MATRIX_TOLERANCE) const; + + /** + * @brief Replaces this matrix with a copy of a 2x2 dynamic matrix. + * + * @param src Source matrix. + * @return `true` when @p src is 2x2. + */ + [[nodiscard]] bool assignFrom(const DynamicMatrix& src); }; /** @@ -268,6 +286,14 @@ struct Matrix4x4 { */ [[nodiscard]] bool isApprox(const Matrix4x4& other, double tol = MATRIX_TOLERANCE) const; + + /** + * @brief Replaces this matrix with a copy of a 4x4 dynamic matrix. + * + * @param src Source matrix. + * @return `true` when @p src is 4x4. + */ + [[nodiscard]] bool assignFrom(const DynamicMatrix& src); }; /** diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index e127c922e1..2261f1b242 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -91,6 +91,16 @@ static void emitGPhaseIfNeeded(OpBuilder& builder, Location loc, double phase) { GPhaseOp::create(builder, loc, phase); } +/** + * @brief Whether `angle` is numerically zero for gate-emission purposes. + * + * @param angle Rotation angle in radians. + * @return `true` when no rotation gate should be emitted. + */ +[[nodiscard]] static bool isNearZeroRotationAngle(double angle) { + return std::abs(angle) <= mlir::utils::TOLERANCE; +} + namespace { /** @@ -109,10 +119,11 @@ struct PSXSequence { /** * @brief Classifies the PSX middle-gate case from ZYZ `theta`. * - * For `theta` in `[0, pi]`: `0` → pure `RZ` chain, `pi/2` → one `SX`, `pi` → - * `X`, otherwise `SX*RZ*SX`. + * Shortcut branches are checked in fixed order (`OnlyRZ`, `OneSX`, `X`) so + * `theta` values within `TOLERANCE` of `0`, `pi/2`, or `pi` always pick the + * same case. * - * @param theta Y-rotation angle from `paramsZYZ`. + * @param theta Y-rotation angle from `paramsZYZ` in `[0, pi]`. * @return The PSX middle-gate case. */ [[nodiscard]] static PSXSequence::Middle @@ -124,10 +135,10 @@ classifyPSXMiddleFromZYZTheta(double theta) { if (theta < eps) { return PSXSequence::Middle::OnlyRZ; } - if (std::abs(theta - halfPi) < eps) { + if (std::abs(theta - halfPi) <= eps) { return PSXSequence::Middle::OneSX; } - if (std::abs(theta - pi) < eps) { + if (std::abs(theta - pi) <= eps) { return PSXSequence::Middle::X; } return PSXSequence::Middle::SXRZSX; @@ -281,8 +292,11 @@ static void visitSequenceInTimeOrder(const PSXSequence& seq, visitSequenceInTimeOrder( seq, [&](const double angle) { - qubit = - RZOp::create(builder, loc, qubit, mod2pi(angle, eps)).getQubitOut(); + const double wrapped = mod2pi(angle, eps); + if (isNearZeroRotationAngle(wrapped)) { + return; + } + qubit = RZOp::create(builder, loc, qubit, wrapped).getQubitOut(); }, [&] { qubit = SXOp::create(builder, loc, qubit).getQubitOut(); }, [&] { qubit = XOp::create(builder, loc, qubit).getQubitOut(); }); @@ -307,6 +321,9 @@ static Value emitKAK(OpBuilder& builder, Location loc, Value qubit, double theta, double phi, double lambda, double phase, EulerBasis basis) { auto emitK = [&](double a) { + if (isNearZeroRotationAngle(a)) { + return; + } switch (basis) { case EulerBasis::ZYZ: case EulerBasis::ZXZ: @@ -322,6 +339,9 @@ static Value emitKAK(OpBuilder& builder, Location loc, Value qubit, }; auto emitA = [&](double a) { + if (isNearZeroRotationAngle(a)) { + return; + } switch (basis) { case EulerBasis::ZYZ: case EulerBasis::XYX: @@ -376,7 +396,13 @@ EulerAngles EulerDecomposition::paramsZYZ(const Matrix2x2& matrix) { 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)); + constexpr double eps = mlir::utils::TOLERANCE; + double ang2 = 0.0; + if (std::abs(matrix(1, 0)) > eps) { + ang2 = std::arg(matrix(1, 0)); + } else if (std::abs(matrix(0, 1)) > eps) { + ang2 = std::arg(matrix(0, 1)); + } const auto phi = ang1 + ang2 - detArg; const auto lambda = ang1 - ang2; return {.theta = theta, .phi = phi, .lambda = lambda, .phase = phase}; @@ -394,6 +420,8 @@ EulerAngles EulerDecomposition::paramsZXZ(const Matrix2x2& matrix) { EulerAngles EulerDecomposition::paramsXYX(const Matrix2x2& matrix) { // H*RY(theta)*H = RY(-theta): shift outer angles by pi and fix global phase. const auto zyz = paramsZYZ(hadamardConjugate(matrix)); + // Keep atol=0 so `phase` tracks the unwrapped ZYZ angles; snapping to pi + // would change the recorded global-phase correction. const auto newPhi = mod2pi(zyz.phi + std::numbers::pi, 0.); const auto newLambda = mod2pi(zyz.lambda + std::numbers::pi, 0.); return {.theta = zyz.theta, @@ -476,31 +504,86 @@ Value synthesizeUnitary1QEuler(OpBuilder& builder, Location loc, Value qubit, return qubit; } +/** + * @brief Counts non-identity K-A-K rotations `emitKAK` would emit. + */ +[[nodiscard]] static std::size_t countKAKGates(double theta, double phi, + double lambda) { + std::size_t count = 0; + if (!isNearZeroRotationAngle(lambda)) { + ++count; + } + if (!isNearZeroRotationAngle(theta)) { + ++count; + } + if (!isNearZeroRotationAngle(phi)) { + ++count; + } + return count; +} + +/** + * @brief Counts non-zero `RZ` slots in a PSX sequence angle. + */ +[[nodiscard]] static std::size_t countNonZeroPSXAngle(double angle) { + constexpr double eps = mlir::utils::TOLERANCE; + return isNearZeroRotationAngle(mod2pi(angle, eps)) ? 0 : 1; +} + +/** + * @brief Counts basis gates `emitFromPSXSequence` would emit for `seq`. + */ +[[nodiscard]] static std::size_t countPSXSequenceGates(const PSXSequence& seq) { + switch (seq.middle) { + case PSXSequence::Middle::OnlyRZ: + return countNonZeroPSXAngle(seq.firstRZ) + countNonZeroPSXAngle(seq.lastRZ); + case PSXSequence::Middle::OneSX: + case PSXSequence::Middle::X: + return countNonZeroPSXAngle(seq.firstRZ) + 1 + + countNonZeroPSXAngle(seq.lastRZ); + case PSXSequence::Middle::SXRZSX: + return countNonZeroPSXAngle(seq.firstRZ) + 1 + + countNonZeroPSXAngle(seq.midRZ) + 1 + + countNonZeroPSXAngle(seq.lastRZ); + } + llvm::reportFatalInternalError("Unhandled PSX middle gate in gate count"); +} + +bool wouldShortenInBasisRun(const std::size_t runSize, + const Matrix2x2& composed, EulerBasis basis) { + if (runSize > maxSynthesisGateCount(basis)) { + return true; + } + return runSize > synthesisGateCount(composed, basis); +} + std::size_t synthesisGateCount(const Matrix2x2& targetMatrix, EulerBasis basis) { - switch (basis) { - case EulerBasis::U: + if (basis == EulerBasis::U) { return 1; + } + + if (targetMatrix.isApprox(Matrix2x2::identity())) { + return 0; + } + + switch (basis) { case EulerBasis::ZYZ: case EulerBasis::ZXZ: case EulerBasis::XZX: - case EulerBasis::XYX: - // emitKAK always emits the full K-A-K rotation triple. - return 3; + case EulerBasis::XYX: { + const auto angles = + EulerDecomposition::anglesFromUnitary(targetMatrix, basis); + return countKAKGates(angles.theta, angles.phi, angles.lambda); + } case EulerBasis::ZSXX: { - const double theta = 2. * std::atan2(std::abs(targetMatrix(1, 0)), - std::abs(targetMatrix(0, 0))); - switch (classifyPSXMiddleFromZYZTheta(theta)) { - case PSXSequence::Middle::OnlyRZ: - return 2U; - case PSXSequence::Middle::SXRZSX: - return 5U; - case PSXSequence::Middle::OneSX: - case PSXSequence::Middle::X: - return 3U; - } - llvm::reportFatalInternalError("Unhandled PSX middle gate"); + const auto zyz = + EulerDecomposition::anglesFromUnitary(targetMatrix, EulerBasis::ZYZ); + const auto seq = sequenceFromZYZForPSX(zyz.theta, zyz.phi, zyz.lambda); + return countPSXSequenceGates(seq); } + case EulerBasis::U: + llvm_unreachable("handled above"); } llvm::reportFatalInternalError("Unhandled Euler basis in synthesisGateCount"); } diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp index 18ed75ce69..6b6eacdd61 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp @@ -208,7 +208,7 @@ struct FuseSingleQubitUnitaryRunsPattern final return !isTargetBasisGate(member.getOperation(), basis); }); if (!hasNonBasisGate && - run.size() <= decomposition::synthesisGateCount(composed, basis)) { + !decomposition::wouldShortenInBasisRun(run.size(), composed, basis)) { return failure(); } diff --git a/mlir/lib/Dialect/QCO/Utils/Matrix.cpp b/mlir/lib/Dialect/QCO/Utils/Matrix.cpp index 01d1b35ea4..c75be25b0e 100644 --- a/mlir/lib/Dialect/QCO/Utils/Matrix.cpp +++ b/mlir/lib/Dialect/QCO/Utils/Matrix.cpp @@ -63,6 +63,23 @@ isApproxFixedImpl(const std::int64_t dim, ArrayRef data, return entriesAreApprox(data, other, tol); } +template +[[nodiscard]] static bool +assignFromDynamicImpl(const DynamicMatrix& src, + std::array& dst) { + if (src.rows() != static_cast(Dim) || + src.cols() != static_cast(Dim)) { + return false; + } + for (std::size_t row = 0; row < Dim; ++row) { + for (std::size_t col = 0; col < Dim; ++col) { + dst[(row * Dim) + col] = + src(static_cast(row), static_cast(col)); + } + } + return true; +} + /// Returns @p dim as `size_t` after asserting it is non-negative and squarable. [[nodiscard]] static std::size_t checkedDim(const std::int64_t dim) { assert(dim >= 0 && "DynamicMatrix dimension must be non-negative"); @@ -123,6 +140,14 @@ bool Matrix1x1::isApprox(const Matrix1x1& other, const double tol) const { return std::abs(value - other.value) <= tol; } +bool Matrix1x1::assignFrom(const DynamicMatrix& src) { + if (src.rows() != 1 || src.cols() != 1) { + return false; + } + value = src(0, 0); + return true; +} + Matrix2x2 Matrix2x2::fromElements(const Complex& m00, const Complex& m01, const Complex& m10, const Complex& m11) { return {{m00, m01, m10, m11}}; @@ -159,6 +184,10 @@ bool Matrix2x2::isApprox(const Matrix2x2& other, const double tol) const { return entriesAreApprox(data, other.data, tol); } +bool Matrix2x2::assignFrom(const DynamicMatrix& src) { + return assignFromDynamicImpl(src, data); +} + Matrix4x4 Matrix4x4::fromElements(const Complex& m00, const Complex& m01, const Complex& m02, const Complex& m03, const Complex& m10, const Complex& m11, @@ -231,6 +260,10 @@ bool Matrix4x4::isApprox(const Matrix4x4& other, const double tol) const { return entriesAreApprox(data, other.data, tol); } +bool Matrix4x4::assignFrom(const DynamicMatrix& src) { + return assignFromDynamicImpl(src, data); +} + DynamicMatrix::DynamicMatrix() : impl_(std::make_unique()) {} DynamicMatrix::DynamicMatrix(const std::int64_t dim) 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 e9b250a14b..a1585ed114 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -38,12 +38,13 @@ #include #include #include -#include +#include #include #include #include #include #include +#include #include #include @@ -79,25 +80,6 @@ struct SynthesizedCircuit { scale * matrix(1, 0), scale * matrix(1, 1)); } -[[nodiscard]] static bool tryGetConstant2x2Matrix(UnitaryOpInterface unitary, - Matrix2x2& out) { - if (unitary.getUnitaryMatrix2x2(out)) { - return true; - } - DynamicMatrix dynamic; - if (!unitary.getUnitaryMatrixDynamic(dynamic) || dynamic.rows() != 2 || - dynamic.cols() != 2) { - return false; - } - for (std::size_t row = 0; row < 2; ++row) { - for (std::size_t col = 0; col < 2; ++col) { - out(row, col) = dynamic(static_cast(row), - static_cast(col)); - } - } - return true; -} - [[nodiscard]] static bool hasConstant2x2Matrix(Operation& op) { if (isa(op)) { return false; @@ -107,7 +89,7 @@ struct SynthesizedCircuit { return false; } Matrix2x2 matrix; - return tryGetConstant2x2Matrix(unitary, matrix); + return unitary.getUnitaryMatrix2x2(matrix); } [[nodiscard]] static Matrix2x2 rzMatrix(double theta) { @@ -235,7 +217,7 @@ static void expectBasisGatesOnly(func::FuncOp funcOp, StringRef basis) { } Matrix2x2 matrix; - ASSERT_TRUE(tryGetConstant2x2Matrix(cast(op), matrix)) + ASSERT_TRUE(cast(op).getUnitaryMatrix2x2(matrix)) << "basis=" << basis.str() << " missing constant matrix for: " << op.getName().getStringRef().str(); EXPECT_TRUE(isAllowedBasisGate(op, basis)) @@ -271,7 +253,7 @@ static Matrix2x2 compute1QMatrixFromFunction(func::FuncOp funcOp) { auto unitary = cast(*op); if (unitary.isSingleQubit()) { Matrix2x2 matrix; - if (!tryGetConstant2x2Matrix(unitary, matrix)) { + if (!unitary.getUnitaryMatrix2x2(matrix)) { ADD_FAILURE() << "Expected constant unitary matrix for op: " << op->getName().getStringRef().str(); failed = true; @@ -291,7 +273,7 @@ static Matrix2x2 compute1QMatrixFromFunction(func::FuncOp funcOp) { return WalkResult::advance(); } Matrix2x2 matrix; - if (!tryGetConstant2x2Matrix(unitary, matrix)) { + if (!unitary.getUnitaryMatrix2x2(matrix)) { ADD_FAILURE() << "Expected constant unitary matrix for op: " << op->getName().getStringRef().str(); failed = true; @@ -355,7 +337,7 @@ static void singleNonBasisGate(QCOProgramBuilder& b) { } // Six `RZ`/`RY` gates in `zyz` basis — longer than canonical (3). -static void overlongZyzRun(QCOProgramBuilder& b) { +static void overlongZYZRun(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.rz(0.3, q[0]); q[0] = b.ry(0.5, q[0]); @@ -367,7 +349,7 @@ static void overlongZyzRun(QCOProgramBuilder& b) { // `SX`/`RZ(pi)`/`SX` in `zsxx` basis — composes to `Z` (pure-Z, theta = 0). // Consecutive-`RZ` merge patterns do not apply across `SX` gates. -static void overlongZsxxMixedPureZRun(QCOProgramBuilder& b) { +static void overlongZSXXMixedPureZRun(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.sx(q[0]); q[0] = b.rz(std::numbers::pi, q[0]); @@ -413,46 +395,55 @@ static func::FuncOp lookupMain(ModuleOp module) { return func; } -template -static void runFuseOnProgramForAllBases(MLIRContext* ctx, - void (*program)(QCOProgramBuilder&), - ChecksT checksAfter) { - forEachBasis([&](StringRef basis) { - auto owned = buildProgram(ctx, program); - if (!static_cast(owned)) { - ADD_FAILURE() << "Failed to build program for basis=" << basis.str(); - return; - } - ModuleOp module = *owned; - if (failed(verify(module))) { - ADD_FAILURE() << "Verifier failed for basis=" << basis.str(); - return; - } +static void expectMatrixPreserved(func::FuncOp funcOp, + const Matrix2x2& original, StringRef label) { + EXPECT_TRUE(compute1QMatrixFromFunction(funcOp).isApprox( + original, mlir::utils::TOLERANCE)) + << label.str(); +} - auto funcOp = lookupMain(module); - if (!funcOp) { - ADD_FAILURE() << "Missing 'main' for basis=" << basis.str(); - return; - } +template +static void +runFuseOnProgram(MLIRContext* ctx, void (*program)(QCOProgramBuilder&), + StringRef basis, BeforeT beforeFuse, AfterT afterFuse) { + auto owned = buildProgram(ctx, program); + ASSERT_TRUE(owned); + ModuleOp module = *owned; + ASSERT_TRUE(succeeded(verify(module))); - const Matrix2x2 original = compute1QMatrixFromFunction(funcOp); + auto funcOp = lookupMain(module); + ASSERT_TRUE(funcOp); + const Matrix2x2 original = compute1QMatrixFromFunction(funcOp); + beforeFuse(funcOp, original); - if (failed(runFuse(module, basis))) { - ADD_FAILURE() << "Fuse pass failed for basis=" << basis.str(); - return; - } - if (failed(verify(module))) { - ADD_FAILURE() << "Verifier failed after fuse for basis=" << basis.str(); - return; - } + ASSERT_TRUE(succeeded(runFuse(module, basis))); + ASSERT_TRUE(succeeded(verify(module))); - funcOp = lookupMain(module); - if (!funcOp) { - ADD_FAILURE() << "Missing 'main' after fuse for basis=" << basis.str(); - return; - } + funcOp = lookupMain(module); + ASSERT_TRUE(funcOp); + afterFuse(funcOp, original); +} - checksAfter(funcOp, basis, original); +template +static void runFuseOnProgram(MLIRContext* ctx, + void (*program)(QCOProgramBuilder&), + StringRef basis, ChecksT afterFuse) { + runFuseOnProgram( + ctx, program, basis, [](func::FuncOp, const Matrix2x2&) {}, + [&](func::FuncOp funcOp, const Matrix2x2& original) { + afterFuse(funcOp, original); + }); +} + +template +static void runFuseOnProgramForAllBases(MLIRContext* ctx, + void (*program)(QCOProgramBuilder&), + ChecksT checksAfter) { + forEachBasis([&](StringRef basis) { + runFuseOnProgram(ctx, program, basis, + [&](func::FuncOp funcOp, const Matrix2x2& original) { + checksAfter(funcOp, basis, original); + }); }); } @@ -494,6 +485,21 @@ template return count; } +[[nodiscard]] static std::size_t countZSXXBasisGates(func::FuncOp funcOp) { + return countOps(funcOp) + countOps(funcOp) + + countOps(funcOp); +} + +template +static void expectSynthesizedMatrix(MLIRContext* ctx, const Matrix2x2& matrix, + EulerBasis basis, + ExtraChecksT extraChecks) { + const auto circuit = synthesizeMatrix(ctx, matrix, basis); + ASSERT_TRUE(succeeded(verify(*circuit.module))); + extraChecks(circuit.func, matrix); + expectMatrixPreserved(circuit.func, matrix, "synthesis"); +} + [[nodiscard]] static std::size_t countTwoQubitGates(func::FuncOp funcOp) { std::size_t count = 0; funcOp.walk([&](UnitaryOpInterface op) { @@ -515,31 +521,14 @@ TEST(EulerSynthesisTest, RandomReconstructionAllBases) { const auto original = randomUnitaryMatrix(rng); forEachBasis([&](StringRef basisStr) { - const auto parsed = mlir::qco::decomposition::parseEulerBasis(basisStr); + const auto parsed = parseEulerBasis(basisStr); ASSERT_TRUE(parsed) << "basis=" << basisStr.str(); - auto module = ModuleOp::create(UnknownLoc::get(fx.context.get())); - MLIRContext* ctx = module.getContext(); - - OpBuilder builder(ctx); - builder.setInsertionPointToStart(module.getBody()); - - auto qubitTy = QubitType::get(ctx); - auto funcTy = builder.getFunctionType({qubitTy}, {qubitTy}); - auto func = builder.create(module.getLoc(), "main", funcTy); - auto* entry = func.addEntryBlock(); - - builder.setInsertionPointToStart(entry); - Value q = entry->getArgument(0); - q = mlir::qco::decomposition::synthesizeUnitary1QEuler( - builder, module.getLoc(), q, original, *parsed); - builder.create(module.getLoc(), q); - - ASSERT_TRUE(succeeded(verify(module))) << "basis=" << basisStr.str(); - - const auto restored = compute1QMatrixFromFunction(func); - EXPECT_TRUE(restored.isApprox(original, mlir::utils::TOLERANCE)) + const auto circuit = + synthesizeMatrix(fx.context.get(), original, *parsed); + ASSERT_TRUE(succeeded(verify(*circuit.module))) << "basis=" << basisStr.str(); + expectMatrixPreserved(circuit.func, original, basisStr); }); } } @@ -548,23 +537,11 @@ TEST(FuseSingleQubitUnitaryRunsTest, FusesRunInScfForBody) { SynthesisFixture fx; fx.setUp(); - auto owned = buildProgram(fx.context.get(), &singleQubitRunInScfFor); - ASSERT_TRUE(owned); - ModuleOp module = *owned; - ASSERT_TRUE(succeeded(verify(module))); - - auto funcOp = lookupMain(module); - ASSERT_TRUE(funcOp); - const Matrix2x2 original = compute1QMatrixFromFunction(funcOp); - - ASSERT_TRUE(succeeded(runFuse(module, "u"))); - ASSERT_TRUE(succeeded(verify(module))); - - funcOp = lookupMain(module); - ASSERT_TRUE(funcOp); - EXPECT_GE(countUOpsInScfFor(funcOp), 1U); - EXPECT_TRUE(compute1QMatrixFromFunction(funcOp).isApprox( - original, mlir::utils::TOLERANCE)); + runFuseOnProgram(fx.context.get(), &singleQubitRunInScfFor, "u", + [&](func::FuncOp funcOp, const Matrix2x2& original) { + EXPECT_GE(countUOpsInScfFor(funcOp), 1U); + expectMatrixPreserved(funcOp, original, "scf.for"); + }); } TEST(FuseSingleQubitUnitaryRunsTest, ReconstructsOriginalRunAllBases) { @@ -573,11 +550,9 @@ TEST(FuseSingleQubitUnitaryRunsTest, ReconstructsOriginalRunAllBases) { runFuseOnProgramForAllBases( fx.context.get(), &singleQubitRunWithSingleQubitGate, - /*checksAfter=*/ [&](func::FuncOp funcOp, StringRef basis, const Matrix2x2& original) { - const auto restored = compute1QMatrixFromFunction(funcOp); - EXPECT_TRUE(restored.isApprox(original, mlir::utils::TOLERANCE)) - << "basis=" << basis.str(); + EXPECT_EQ(countOps(funcOp), 0U) << "basis=" << basis.str(); + expectMatrixPreserved(funcOp, original, basis); expectBasisGatesOnly(funcOp, basis); }); } @@ -592,9 +567,7 @@ TEST(FuseSingleQubitUnitaryRunsTest, ResynthesizesLoneNonBasisGateAllBases) { [&](func::FuncOp funcOp, StringRef basis, const Matrix2x2& original) { EXPECT_EQ(countOps(funcOp), 0U) << "basis=" << basis.str() << " left a non-basis gate"; - EXPECT_TRUE(compute1QMatrixFromFunction(funcOp).isApprox( - original, mlir::utils::TOLERANCE)) - << "basis=" << basis.str(); + expectMatrixPreserved(funcOp, original, basis); expectBasisGatesOnly(funcOp, basis); }); } @@ -603,102 +576,130 @@ TEST(FuseSingleQubitUnitaryRunsTest, FusesOverlongInBasisRun) { SynthesisFixture fx; fx.setUp(); - auto owned = buildProgram(fx.context.get(), &overlongZyzRun); - ASSERT_TRUE(owned); - ModuleOp module = *owned; - ASSERT_TRUE(succeeded(verify(module))); - - auto funcOp = lookupMain(module); - ASSERT_TRUE(funcOp); - const Matrix2x2 original = compute1QMatrixFromFunction(funcOp); - const std::size_t before = countOps(funcOp) + countOps(funcOp); - ASSERT_EQ(before, 6U); - - ASSERT_TRUE(succeeded(runFuse(module, "zyz"))); - ASSERT_TRUE(succeeded(verify(module))); - - funcOp = lookupMain(module); - ASSERT_TRUE(funcOp); - const std::size_t after = countOps(funcOp) + countOps(funcOp); - EXPECT_LE(after, 3U); - EXPECT_LT(after, before); - EXPECT_TRUE(compute1QMatrixFromFunction(funcOp).isApprox( - original, mlir::utils::TOLERANCE)); - expectBasisGatesOnly(funcOp, "zyz"); + runFuseOnProgram( + fx.context.get(), &overlongZYZRun, "zyz", + [](func::FuncOp funcOp, const Matrix2x2&) { + const std::size_t before = + countOps(funcOp) + countOps(funcOp); + ASSERT_EQ(before, 6U); + }, + [](func::FuncOp funcOp, const Matrix2x2& original) { + const std::size_t after = + countOps(funcOp) + countOps(funcOp); + EXPECT_LE(after, 3U); + EXPECT_LT(after, 6U); + expectMatrixPreserved(funcOp, original, "zyz"); + expectBasisGatesOnly(funcOp, "zyz"); + }); } TEST(FuseSingleQubitUnitaryRunsTest, - FusesOverlongZsxxMixedRunComposingToPureZ) { + FusesOverlongZSXXMixedRunComposingToPureZ) { SynthesisFixture fx; fx.setUp(); - auto owned = buildProgram(fx.context.get(), &overlongZsxxMixedPureZRun); - ASSERT_TRUE(owned); - ModuleOp module = *owned; - ASSERT_TRUE(succeeded(verify(module))); - - auto funcOp = lookupMain(module); - ASSERT_TRUE(funcOp); - const Matrix2x2 original = compute1QMatrixFromFunction(funcOp); - EXPECT_EQ(synthesisGateCount(original, EulerBasis::ZSXX), 2U); - const std::size_t beforeGates = - countOps(funcOp) + countOps(funcOp) + countOps(funcOp); - ASSERT_EQ(beforeGates, 3U); - EXPECT_EQ(countOps(funcOp), 2U); - EXPECT_EQ(countOps(funcOp), 1U); - EXPECT_EQ(countOps(funcOp), 0U); - - ASSERT_TRUE(succeeded(runFuse(module, "zsxx"))); - ASSERT_TRUE(succeeded(verify(module))); - - funcOp = lookupMain(module); - ASSERT_TRUE(funcOp); - const std::size_t afterGates = - countOps(funcOp) + countOps(funcOp) + countOps(funcOp); - // `OnlyRZ` synthesis emits `RZ(pi)` then `RZ(0)`; the latter is identity. - EXPECT_EQ(countOps(funcOp), 1U); - EXPECT_EQ(countOps(funcOp), 0U); - EXPECT_EQ(countOps(funcOp), 0U); - EXPECT_LE(afterGates, synthesisGateCount(original, EulerBasis::ZSXX)); - EXPECT_LT(afterGates, beforeGates); - EXPECT_TRUE(compute1QMatrixFromFunction(funcOp).isApprox( - original, mlir::utils::TOLERANCE)); - expectBasisGatesOnly(funcOp, "zsxx"); + runFuseOnProgram( + fx.context.get(), &overlongZSXXMixedPureZRun, "zsxx", + [](func::FuncOp funcOp, const Matrix2x2& original) { + EXPECT_EQ(synthesisGateCount(original, EulerBasis::ZSXX), 1U); + ASSERT_EQ(countZSXXBasisGates(funcOp), 3U); + EXPECT_EQ(countOps(funcOp), 2U); + EXPECT_EQ(countOps(funcOp), 1U); + EXPECT_EQ(countOps(funcOp), 0U); + }, + [](func::FuncOp funcOp, const Matrix2x2& original) { + // `OnlyRZ` synthesis emits `RZ(pi)` then `RZ(0)`; the latter is + // identity. + EXPECT_EQ(countOps(funcOp), 1U); + EXPECT_EQ(countOps(funcOp), 0U); + EXPECT_EQ(countOps(funcOp), 0U); + EXPECT_LE(countZSXXBasisGates(funcOp), + synthesisGateCount(original, EulerBasis::ZSXX)); + expectMatrixPreserved(funcOp, original, "zsxx"); + expectBasisGatesOnly(funcOp, "zsxx"); + }); } -TEST(EulerSynthesisTest, ZsxxPauliXUsesXGateShortcut) { +TEST(EulerSynthesisTest, ProfitabilityAndIdentityGateCount) { SynthesisFixture fx; fx.setUp(); - const Matrix2x2 pauliX = XOp::getUnitaryMatrix(); - const auto circuit = - synthesizeMatrix(fx.context.get(), pauliX, EulerBasis::ZSXX); + const Matrix2x2 identity = Matrix2x2::identity(); + EXPECT_TRUE(wouldShortenInBasisRun(6, identity, EulerBasis::ZYZ)); + EXPECT_TRUE(wouldShortenInBasisRun(2, identity, EulerBasis::ZYZ)); + EXPECT_FALSE( + wouldShortenInBasisRun(1, XOp::getUnitaryMatrix(), EulerBasis::ZSXX)); + EXPECT_EQ(synthesisGateCount(identity, EulerBasis::ZYZ), 0U); + + expectSynthesizedMatrix(fx.context.get(), identity, EulerBasis::ZYZ, + [](func::FuncOp funcOp, const Matrix2x2&) { + EXPECT_EQ(countOps(funcOp), 0U); + EXPECT_EQ(countOps(funcOp), 0U); + }); +} + +struct ZSXXShortcutCase { + std::string_view label; + std::function makeMatrix; + std::size_t expectedSynthesisCount; + std::size_t expectedRz; + std::size_t expectedSx; + std::size_t expectedX; +}; - ASSERT_TRUE(succeeded(verify(*circuit.module))); - EXPECT_EQ(countOps(circuit.func), 1U); - EXPECT_EQ(countOps(circuit.func), 0U); - EXPECT_TRUE(compute1QMatrixFromFunction(circuit.func) - .isApprox(pauliX, mlir::utils::TOLERANCE)); -} +// NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_P` at global scope +class ZSXXShortcutTest : public testing::TestWithParam {}; -TEST(EulerSynthesisTest, ZsxxPureZRotationUsesOnlyRZShortcut) { +TEST_P(ZSXXShortcutTest, SynthesisMatchesGateCount) { SynthesisFixture fx; fx.setUp(); - const Matrix2x2 pureZ = rzMatrix(0.3) * rzMatrix(0.7); - EXPECT_EQ(synthesisGateCount(pureZ, EulerBasis::ZSXX), 2U); - - const auto circuit = - synthesizeMatrix(fx.context.get(), pureZ, EulerBasis::ZSXX); - - ASSERT_TRUE(succeeded(verify(*circuit.module))); - EXPECT_EQ(countOps(circuit.func), 2U); - EXPECT_EQ(countOps(circuit.func), 0U); - EXPECT_EQ(countOps(circuit.func), 0U); - EXPECT_TRUE(compute1QMatrixFromFunction(circuit.func) - .isApprox(pureZ, mlir::utils::TOLERANCE)); + const auto& testCase = GetParam(); + const Matrix2x2 matrix = testCase.makeMatrix(fx.context.get()); + EXPECT_EQ(synthesisGateCount(matrix, EulerBasis::ZSXX), + testCase.expectedSynthesisCount); + + expectSynthesizedMatrix( + fx.context.get(), matrix, EulerBasis::ZSXX, + [&](func::FuncOp funcOp, const Matrix2x2& original) { + EXPECT_EQ(countOps(funcOp), testCase.expectedRz); + EXPECT_EQ(countOps(funcOp), testCase.expectedSx); + EXPECT_EQ(countOps(funcOp), testCase.expectedX); + EXPECT_EQ(countZSXXBasisGates(funcOp), + synthesisGateCount(original, EulerBasis::ZSXX)); + }); } +INSTANTIATE_TEST_SUITE_P( + ZSXXShortcuts, ZSXXShortcutTest, + testing::Values(ZSXXShortcutCase{"PauliX", + [](MLIRContext*) -> Matrix2x2 { + return XOp::getUnitaryMatrix(); + }, + 1, 0, 0, 1}, + ZSXXShortcutCase{"PureZ", + [](MLIRContext*) -> Matrix2x2 { + return rzMatrix(0.3) * rzMatrix(0.7); + }, + 2, 2, 0, 0}, + ZSXXShortcutCase{"RyHalfPi", + [](MLIRContext* ctx) -> Matrix2x2 { + return rotationMatrix( + ctx, std::numbers::pi / 2.0); + }, + 3, 2, 1, 0}, + ZSXXShortcutCase{"RyNearHalfPi", + [](MLIRContext* ctx) -> Matrix2x2 { + return rotationMatrix( + ctx, + (std::numbers::pi / 2.0) + + (0.5 * mlir::utils::TOLERANCE)); + }, + 3, 2, 1, 0}), + [](const testing::TestParamInfo& info) { + return std::string(info.param.label); + }); + TEST(EulerSynthesisTest, UGateReconstruction) { SynthesisFixture fx; fx.setUp(); @@ -706,11 +707,10 @@ TEST(EulerSynthesisTest, UGateReconstruction) { std::mt19937 rng{99991}; for (int i = 0; i < 32; ++i) { const auto u = randomUnitaryMatrix(rng); - const auto circuit = synthesizeMatrix(fx.context.get(), u, EulerBasis::U); - ASSERT_TRUE(succeeded(verify(*circuit.module))); - EXPECT_LE(countOps(circuit.func), 1U); - EXPECT_TRUE(compute1QMatrixFromFunction(circuit.func) - .isApprox(u, mlir::utils::TOLERANCE)); + expectSynthesizedMatrix(fx.context.get(), u, EulerBasis::U, + [](func::FuncOp funcOp, const Matrix2x2&) { + EXPECT_LE(countOps(funcOp), 1U); + }); } } @@ -764,11 +764,8 @@ TEST_P(EulerSynthesisExactTest, ReconstructsReferenceMatrices) { const auto [basis, matrixFn] = GetParam(); const Matrix2x2 original = matrixFn(fx.context.get()); - const auto circuit = synthesizeMatrix(fx.context.get(), original, basis); - - ASSERT_TRUE(succeeded(verify(*circuit.module))); - EXPECT_TRUE(compute1QMatrixFromFunction(circuit.func) - .isApprox(original, mlir::utils::TOLERANCE)); + expectSynthesizedMatrix(fx.context.get(), original, basis, + [](func::FuncOp, const Matrix2x2&) {}); } INSTANTIATE_TEST_SUITE_P( @@ -805,9 +802,7 @@ TEST(FuseSingleQubitUnitaryRunsTest, DoesNotFuseAcrossTwoQGateAllBases) { /*checksAfter=*/ [&](func::FuncOp funcOp, StringRef basis, const Matrix2x2& original) { EXPECT_EQ(countTwoQubitGates(funcOp), 1U) << "basis=" << basis.str(); - EXPECT_TRUE(compute1QMatrixFromFunction(funcOp).isApprox( - original, mlir::utils::TOLERANCE)) - << "basis=" << basis.str(); + expectMatrixPreserved(funcOp, original, basis); expectBasisGatesOnly(funcOp, basis); expectOneQubitGatesAroundBoundary( funcOp, basis, [](Operation& op) { return isTwoQubitGate(op); }); @@ -820,12 +815,9 @@ TEST(FuseSingleQubitUnitaryRunsTest, DoesNotFuseAcrossBarrierAllBases) { runFuseOnProgramForAllBases( fx.context.get(), &singleQubitRunsSplitByBarrier, - /*checksAfter=*/ [&](func::FuncOp funcOp, StringRef basis, const Matrix2x2& original) { EXPECT_EQ(countOps(funcOp), 1U) << "basis=" << basis.str(); - EXPECT_TRUE(compute1QMatrixFromFunction(funcOp).isApprox( - original, mlir::utils::TOLERANCE)) - << "basis=" << basis.str(); + expectMatrixPreserved(funcOp, original, basis); expectBasisGatesOnly(funcOp, basis); expectOneQubitGatesAroundBoundary( funcOp, basis, [](Operation& op) { return isa(op); }); diff --git a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp index 8008dc7561..ceb1208e9e 100644 --- a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp +++ b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp @@ -218,6 +218,30 @@ TEST(DynamicMatrix, IsApproxRejectsMismatchedExtents) { EXPECT_FALSE(DynamicMatrix::identity(1).isApprox(DynamicMatrix::identity(2))); } +TEST(Matrix2x2, AssignFromDynamicMatrix) { + const Matrix2x2 x = pauliX(); + + DynamicMatrix dynamic; + dynamic.assignFrom(x); + + Matrix2x2 out = Matrix2x2::identity(); + EXPECT_TRUE(out.assignFrom(dynamic)); + EXPECT_TRUE(out.isApprox(x)); + EXPECT_FALSE(out.assignFrom(DynamicMatrix::identity(3))); +} + +TEST(Matrix4x4, AssignFromDynamicMatrix) { + const Matrix4x4 swap = swapMatrix(); + + DynamicMatrix dynamic; + dynamic.assignFrom(swap); + + Matrix4x4 out = Matrix4x4::identity(); + EXPECT_TRUE(out.assignFrom(dynamic)); + EXPECT_TRUE(out.isApprox(swap)); + EXPECT_FALSE(out.assignFrom(DynamicMatrix::identity(2))); +} + TEST(DynamicMatrix, IsApproxOverloads) { const Matrix2x2 x = pauliX(); const Matrix4x4 swap = swapMatrix(); From 2bab298bf37acb747f31a221b0d845ec09d5bc67 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 10 Jun 2026 15:11:22 +0200 Subject: [PATCH 31/68] =?UTF-8?q?=F0=9F=8E=A8=20Multiple=20small=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/Decomposition/Euler.h | 50 +- .../mlir/Dialect/QCO/Transforms/Passes.td | 6 +- .../QCO/Transforms/Decomposition/Euler.cpp | 164 ++-- .../FuseSingleQubitUnitaryRuns.cpp | 11 +- mlir/lib/Dialect/QCO/Utils/WireIterator.cpp | 8 +- .../test_euler_decomposition.cpp | 753 +++++++++--------- 6 files changed, 499 insertions(+), 493 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h index 77575b310e..d0fbec3845 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h @@ -42,7 +42,7 @@ enum class EulerBasis : std::uint8_t { XZX = 2, ///< `RX(phi) * RZ(theta) * RX(lambda)`. XYX = 3, ///< `RX(phi) * RY(theta) * RX(lambda)`. U = 4, ///< `U(theta, phi, lambda)`. - ZSXX = 5, ///< `RZ` / `SX` / `X` chain equivalent to ZYZ. + ZSXX = 5, ///< `RZ` / `SX` / `X` synthesis via ZYZ decomposition. }; /** @@ -58,9 +58,6 @@ class EulerDecomposition { /** * @brief Extracts `(theta, phi, lambda, phase)` for KAK and `U` bases. * - * Does not support `EulerBasis::ZSXX`; use `synthesizeUnitary1QEuler` or - * `synthesisGateCount` instead. - * * @param matrix The single-qubit unitary to decompose. * @param basis The target Euler basis. * @return The extracted Euler angles and global phase. @@ -78,20 +75,20 @@ class EulerDecomposition { [[nodiscard]] static EulerAngles paramsZYZ(const Matrix2x2& matrix); /** - * @brief Extracts parameters for `U(theta, phi, lambda)`. + * @brief Extracts parameters for `RZ(phi) * RX(theta) * RZ(lambda)`. * * @param matrix The single-qubit unitary to decompose. * @return The extracted Euler angles and global phase. */ - [[nodiscard]] static EulerAngles paramsU(const Matrix2x2& matrix); + [[nodiscard]] static EulerAngles paramsZXZ(const Matrix2x2& matrix); /** - * @brief Extracts parameters for `RZ(phi) * RX(theta) * RZ(lambda)`. + * @brief Extracts parameters for `RX(phi) * RZ(theta) * RX(lambda)`. * * @param matrix The single-qubit unitary to decompose. * @return The extracted Euler angles and global phase. */ - [[nodiscard]] static EulerAngles paramsZXZ(const Matrix2x2& matrix); + [[nodiscard]] static EulerAngles paramsXZX(const Matrix2x2& matrix); /** * @brief Extracts parameters for `RX(phi) * RY(theta) * RX(lambda)`. @@ -102,12 +99,12 @@ class EulerDecomposition { [[nodiscard]] static EulerAngles paramsXYX(const Matrix2x2& matrix); /** - * @brief Extracts parameters for `RX(phi) * RZ(theta) * RX(lambda)`. + * @brief Extracts parameters for `U(theta, phi, lambda)`. * * @param matrix The single-qubit unitary to decompose. * @return The extracted Euler angles and global phase. */ - [[nodiscard]] static EulerAngles paramsXZX(const Matrix2x2& matrix); + [[nodiscard]] static EulerAngles paramsU(const Matrix2x2& matrix); }; /** @@ -136,6 +133,21 @@ class EulerDecomposition { const Matrix2x2& targetMatrix, EulerBasis basis); +/** + * @brief Number of basis gates `synthesizeUnitary1QEuler` would emit. + * + * Excludes `qco.gphase` and near-zero rotations that synthesis skips. + * + * @param targetMatrix The single-qubit unitary that would be synthesized. + * @param basis The target Euler basis. + * @return The gate count (1 for `U`, up to 3 for KAK bases, up to 5 for + * `ZSXX`). For `ZSXX`, pure-Z (`OnlyRZ`) compositions count non-zero + * `RZ` gates only (1 or 2); `OneSX` and `X` shortcuts count 3; the + * generic case counts up to 5. + */ +[[nodiscard]] std::size_t synthesisGateCount(const Matrix2x2& targetMatrix, + EulerBasis basis); + /** * @brief Upper bound on basis gates `synthesizeUnitary1QEuler` may emit. * @@ -165,25 +177,11 @@ class EulerDecomposition { * @param runSize Number of gates in the run. * @param composed Composed unitary of the run. * @param basis The target Euler basis. - * @return `true` when resynthesis would emit fewer basis gates than @p runSize. + * @return `true` when Euler resynthesis would emit fewer basis gates than @p + * runSize. */ [[nodiscard]] bool wouldShortenInBasisRun(std::size_t runSize, const Matrix2x2& composed, EulerBasis basis); -/** - * @brief Number of basis gates `synthesizeUnitary1QEuler` would emit. - * - * Excludes `qco.gphase` and near-zero rotations that synthesis skips. - * - * @param targetMatrix The single-qubit unitary that would be synthesized. - * @param basis The target Euler basis. - * @return The gate count (1 for `U`, up to 3 for KAK bases, up to 5 for - * `ZSXX`). For `ZSXX`, pure-Z (`OnlyRZ`) compositions count non-zero - * `RZ` gates only (1 or 2); `OneSX` and `X` shortcuts count 3; the - * generic case counts up to 5. - */ -[[nodiscard]] std::size_t synthesisGateCount(const Matrix2x2& targetMatrix, - EulerBasis basis); - } // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index 31ccbe85a4..ce733719f9 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -51,9 +51,9 @@ def FuseSingleQubitUnitaryRuns same qubit wire (anchored at each run head), composes their constant unitary matrices, and replaces a run with an equivalent sequence of basis gates when beneficial: when the run contains a gate outside the target `basis`, or when - its length exceeds `synthesisGateCount` for the composed matrix. Runs that - are already in the target `basis` and no longer than that canonical count - are left unchanged. + Euler resynthesis would shorten it (`wouldShortenInBasisRun`). Runs that are + already in the target `basis` and no shorter than the canonical synthesis + length are left unchanged. The emitted basis is controlled via the `basis` option (e.g. `zyz`, `zsxx`). A `gphase` correction is inserted when needed so the rewritten sequence diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index 2261f1b242..7c6e24e53b 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -104,9 +104,9 @@ static void emitGPhaseIfNeeded(OpBuilder& builder, Location loc, double phase) { namespace { /** - * @brief Planned PSX (`RZ` / `SX` / `X`) chain; angles in circuit order. + * @brief Planned ZSXX (`RZ` / `SX` / `X`) chain; angles in circuit order. */ -struct PSXSequence { +struct ZSXXSequence { enum class Middle : std::uint8_t { OnlyRZ, OneSX, X, SXRZSX }; Middle middle = Middle::SXRZSX; double firstRZ = 0.0; @@ -117,71 +117,71 @@ struct PSXSequence { } // namespace /** - * @brief Classifies the PSX middle-gate case from ZYZ `theta`. + * @brief Classifies the ZSXX middle-gate case from ZYZ `theta`. * * Shortcut branches are checked in fixed order (`OnlyRZ`, `OneSX`, `X`) so * `theta` values within `TOLERANCE` of `0`, `pi/2`, or `pi` always pick the * same case. * * @param theta Y-rotation angle from `paramsZYZ` in `[0, pi]`. - * @return The PSX middle-gate case. + * @return The ZSXX middle-gate case. */ -[[nodiscard]] static PSXSequence::Middle -classifyPSXMiddleFromZYZTheta(double theta) { +[[nodiscard]] static ZSXXSequence::Middle +classifyZSXXMiddleFromZYZTheta(double theta) { constexpr double eps = mlir::utils::TOLERANCE; constexpr double halfPi = std::numbers::pi / 2.0; constexpr double pi = std::numbers::pi; if (theta < eps) { - return PSXSequence::Middle::OnlyRZ; + return ZSXXSequence::Middle::OnlyRZ; } if (std::abs(theta - halfPi) <= eps) { - return PSXSequence::Middle::OneSX; + return ZSXXSequence::Middle::OneSX; } if (std::abs(theta - pi) <= eps) { - return PSXSequence::Middle::X; + return ZSXXSequence::Middle::X; } - return PSXSequence::Middle::SXRZSX; + return ZSXXSequence::Middle::SXRZSX; } /** - * @brief Builds the PSX sequence for `RZ(phi)*RY(theta)*RZ(lambda)`. + * @brief Builds the ZSXX sequence for `RZ(phi)*RY(theta)*RZ(lambda)`. * * Uses `SX*RZ(theta+pi)*SX = Z*RY(theta)`. * * @param theta Y-rotation angle in `[0, pi]`. * @param phi Trailing Z-rotation angle. * @param lambda Leading Z-rotation angle. - * @return The planned PSX sequence. + * @return The planned ZSXX sequence. */ -[[nodiscard]] static PSXSequence sequenceFromZYZForPSX(double theta, double phi, - double lambda) { +[[nodiscard]] static ZSXXSequence +sequenceFromZYZForZSXX(double theta, double phi, double lambda) { constexpr double halfPi = std::numbers::pi / 2.0; constexpr double pi = std::numbers::pi; - switch (classifyPSXMiddleFromZYZTheta(theta)) { - case PSXSequence::Middle::OnlyRZ: - return {.middle = PSXSequence::Middle::OnlyRZ, + switch (classifyZSXXMiddleFromZYZTheta(theta)) { + case ZSXXSequence::Middle::OnlyRZ: + return {.middle = ZSXXSequence::Middle::OnlyRZ, .firstRZ = lambda, .midRZ = 0.0, .lastRZ = phi}; - case PSXSequence::Middle::OneSX: - return {.middle = PSXSequence::Middle::OneSX, + case ZSXXSequence::Middle::OneSX: + return {.middle = ZSXXSequence::Middle::OneSX, .firstRZ = lambda - halfPi, .midRZ = 0.0, .lastRZ = phi + halfPi}; - case PSXSequence::Middle::X: - return {.middle = PSXSequence::Middle::X, + case ZSXXSequence::Middle::X: + return {.middle = ZSXXSequence::Middle::X, .firstRZ = lambda, .midRZ = 0.0, .lastRZ = phi + pi}; - case PSXSequence::Middle::SXRZSX: - return {.middle = PSXSequence::Middle::SXRZSX, + case ZSXXSequence::Middle::SXRZSX: + return {.middle = ZSXXSequence::Middle::SXRZSX, .firstRZ = lambda, .midRZ = theta + pi, .lastRZ = phi + pi}; } - llvm::reportFatalInternalError("Unhandled PSX middle gate"); + llvm::reportFatalInternalError("Unhandled ZSXX middle gate"); } /** @@ -210,62 +210,62 @@ classifyPSXMiddleFromZYZTheta(double theta) { } /** - * @brief Global phase offset of the PSX chain vs the ZYZ product. + * @brief Global phase offset of the ZSXX chain vs. the ZYZ product. * - * @param seq The planned PSX sequence. + * @param seq The planned ZSXX sequence. * @return The global-phase offset in radians. */ -[[nodiscard]] static double globalPhaseOffsetForPSX(const PSXSequence& seq) { +[[nodiscard]] static double globalPhaseOffsetForZSXX(const ZSXXSequence& seq) { constexpr double halfPi = std::numbers::pi / 2.0; constexpr double quarterPi = std::numbers::pi / 4.0; switch (seq.middle) { - case PSXSequence::Middle::OnlyRZ: + case ZSXXSequence::Middle::OnlyRZ: return globalPhaseFromRZWrap(seq.firstRZ) + globalPhaseFromRZWrap(seq.lastRZ); - case PSXSequence::Middle::OneSX: + case ZSXXSequence::Middle::OneSX: // `SX = exp(i*pi/4)*RZ(-pi/2)*RY(pi/2)*RZ(pi/2)`; the outer RZ angles // absorb the +-pi/2, leaving the exp(i*pi/4) phase. RZ wraps add too. return -quarterPi + globalPhaseFromRZWrap(seq.firstRZ) + globalPhaseFromRZWrap(seq.lastRZ); - case PSXSequence::Middle::X: + case ZSXXSequence::Middle::X: // `X` swaps the diagonal, so the wraps enter with opposite signs. return -halfPi + globalPhaseFromRZWrap(seq.lastRZ) - globalPhaseFromRZWrap(seq.firstRZ); - case PSXSequence::Middle::SXRZSX: + case ZSXXSequence::Middle::SXRZSX: // `SX*RZ(theta+pi)*SX = Z*RY(theta)`; all three RZ wraps add. return halfPi + globalPhaseFromRZWrap(seq.firstRZ) + globalPhaseFromRZWrap(seq.midRZ) + globalPhaseFromRZWrap(seq.lastRZ); } - llvm::reportFatalInternalError("Unhandled PSX middle gate"); + llvm::reportFatalInternalError("Unhandled ZSXX middle gate"); } /** * @brief Invokes callbacks for each gate of `seq` in circuit order. * - * @param seq The planned PSX sequence. + * @param seq The planned ZSXX sequence. * @param onRZ Called with each RZ angle. * @param onSX Called for each SX gate. * @param onX Called for each X gate. */ -static void visitSequenceInTimeOrder(const PSXSequence& seq, +static void visitSequenceInTimeOrder(const ZSXXSequence& seq, llvm::function_ref onRZ, llvm::function_ref onSX, llvm::function_ref onX) { onRZ(seq.firstRZ); switch (seq.middle) { - case PSXSequence::Middle::OnlyRZ: + case ZSXXSequence::Middle::OnlyRZ: onRZ(seq.lastRZ); break; - case PSXSequence::Middle::OneSX: + case ZSXXSequence::Middle::OneSX: onSX(); onRZ(seq.lastRZ); break; - case PSXSequence::Middle::X: + case ZSXXSequence::Middle::X: onX(); onRZ(seq.lastRZ); break; - case PSXSequence::Middle::SXRZSX: + case ZSXXSequence::Middle::SXRZSX: onSX(); onRZ(seq.midRZ); onSX(); @@ -280,14 +280,14 @@ static void visitSequenceInTimeOrder(const PSXSequence& seq, * @param builder Builder for the operations. * @param loc Location of the operations. * @param qubit Input qubit value. - * @param seq The planned PSX sequence. + * @param seq The planned ZSXX sequence. * @param phase Global phase in radians. * @return The output qubit value. */ -[[nodiscard]] static Value emitFromPSXSequence(OpBuilder& builder, Location loc, - Value qubit, - const PSXSequence& seq, - double phase) { +[[nodiscard]] static Value emitFromZSXXSequence(OpBuilder& builder, + Location loc, Value qubit, + const ZSXXSequence& seq, + double phase) { constexpr double eps = mlir::utils::TOLERANCE; visitSequenceInTimeOrder( seq, @@ -372,14 +372,14 @@ static Value emitKAK(OpBuilder& builder, Location loc, Value qubit, EulerAngles EulerDecomposition::anglesFromUnitary(const Matrix2x2& 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::XZX: + return paramsXZX(matrix); + case EulerBasis::XYX: + return paramsXYX(matrix); case EulerBasis::U: return paramsU(matrix); } @@ -417,6 +417,11 @@ EulerAngles EulerDecomposition::paramsZXZ(const Matrix2x2& matrix) { .phase = zyz.phase}; } +EulerAngles EulerDecomposition::paramsXZX(const Matrix2x2& matrix) { + // X-Z-X -> Z-X-Z under H conjugation (no Y sign flip, unlike paramsXYX). + return paramsZXZ(hadamardConjugate(matrix)); +} + EulerAngles EulerDecomposition::paramsXYX(const Matrix2x2& matrix) { // H*RY(theta)*H = RY(-theta): shift outer angles by pi and fix global phase. const auto zyz = paramsZYZ(hadamardConjugate(matrix)); @@ -431,11 +436,6 @@ EulerAngles EulerDecomposition::paramsXYX(const Matrix2x2& matrix) { zyz.phase + ((newPhi + newLambda - zyz.phi - zyz.lambda) / 2.)}; } -EulerAngles EulerDecomposition::paramsXZX(const Matrix2x2& matrix) { - // X-Z-X -> Z-X-Z under H conjugation (no Y sign flip, unlike paramsXYX). - return paramsZXZ(hadamardConjugate(matrix)); -} - EulerAngles EulerDecomposition::paramsU(const Matrix2x2& matrix) { const auto zyz = paramsZYZ(matrix); return {.theta = zyz.theta, @@ -475,9 +475,9 @@ Value synthesizeUnitary1QEuler(OpBuilder& builder, Location loc, Value qubit, EulerBasis basis) { if (basis == EulerBasis::ZSXX) { const auto zyz = EulerDecomposition::paramsZYZ(targetMatrix); - const auto seq = sequenceFromZYZForPSX(zyz.theta, zyz.phi, zyz.lambda); - return emitFromPSXSequence(builder, loc, qubit, seq, - zyz.phase + globalPhaseOffsetForPSX(seq)); + const auto seq = sequenceFromZYZForZSXX(zyz.theta, zyz.phi, zyz.lambda); + return emitFromZSXXSequence(builder, loc, qubit, seq, + zyz.phase + globalPhaseOffsetForZSXX(seq)); } const auto angles = @@ -523,38 +523,32 @@ Value synthesizeUnitary1QEuler(OpBuilder& builder, Location loc, Value qubit, } /** - * @brief Counts non-zero `RZ` slots in a PSX sequence angle. + * @brief Counts non-zero `RZ` slots in a ZSXX sequence angle. */ -[[nodiscard]] static std::size_t countNonZeroPSXAngle(double angle) { +[[nodiscard]] static std::size_t countNonZeroZSXXAngle(double angle) { constexpr double eps = mlir::utils::TOLERANCE; return isNearZeroRotationAngle(mod2pi(angle, eps)) ? 0 : 1; } /** - * @brief Counts basis gates `emitFromPSXSequence` would emit for `seq`. + * @brief Counts basis gates `emitFromZSXXSequence` would emit for `seq`. */ -[[nodiscard]] static std::size_t countPSXSequenceGates(const PSXSequence& seq) { +[[nodiscard]] static std::size_t +countZSXXSequenceGates(const ZSXXSequence& seq) { switch (seq.middle) { - case PSXSequence::Middle::OnlyRZ: - return countNonZeroPSXAngle(seq.firstRZ) + countNonZeroPSXAngle(seq.lastRZ); - case PSXSequence::Middle::OneSX: - case PSXSequence::Middle::X: - return countNonZeroPSXAngle(seq.firstRZ) + 1 + - countNonZeroPSXAngle(seq.lastRZ); - case PSXSequence::Middle::SXRZSX: - return countNonZeroPSXAngle(seq.firstRZ) + 1 + - countNonZeroPSXAngle(seq.midRZ) + 1 + - countNonZeroPSXAngle(seq.lastRZ); + case ZSXXSequence::Middle::OnlyRZ: + return countNonZeroZSXXAngle(seq.firstRZ) + + countNonZeroZSXXAngle(seq.lastRZ); + case ZSXXSequence::Middle::OneSX: + case ZSXXSequence::Middle::X: + return countNonZeroZSXXAngle(seq.firstRZ) + 1 + + countNonZeroZSXXAngle(seq.lastRZ); + case ZSXXSequence::Middle::SXRZSX: + return countNonZeroZSXXAngle(seq.firstRZ) + 1 + + countNonZeroZSXXAngle(seq.midRZ) + 1 + + countNonZeroZSXXAngle(seq.lastRZ); } - llvm::reportFatalInternalError("Unhandled PSX middle gate in gate count"); -} - -bool wouldShortenInBasisRun(const std::size_t runSize, - const Matrix2x2& composed, EulerBasis basis) { - if (runSize > maxSynthesisGateCount(basis)) { - return true; - } - return runSize > synthesisGateCount(composed, basis); + llvm::reportFatalInternalError("Unhandled ZSXX middle gate in gate count"); } std::size_t synthesisGateCount(const Matrix2x2& targetMatrix, @@ -579,13 +573,19 @@ std::size_t synthesisGateCount(const Matrix2x2& targetMatrix, case EulerBasis::ZSXX: { const auto zyz = EulerDecomposition::anglesFromUnitary(targetMatrix, EulerBasis::ZYZ); - const auto seq = sequenceFromZYZForPSX(zyz.theta, zyz.phi, zyz.lambda); - return countPSXSequenceGates(seq); + const auto seq = sequenceFromZYZForZSXX(zyz.theta, zyz.phi, zyz.lambda); + return countZSXXSequenceGates(seq); } - case EulerBasis::U: - llvm_unreachable("handled above"); } llvm::reportFatalInternalError("Unhandled Euler basis in synthesisGateCount"); } +bool wouldShortenInBasisRun(const std::size_t runSize, + const Matrix2x2& composed, EulerBasis basis) { + if (runSize > maxSynthesisGateCount(basis)) { + return true; + } + return runSize > synthesisGateCount(composed, basis); +} + } // namespace mlir::qco::decomposition diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp index 6b6eacdd61..4af90bb9d1 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp @@ -40,9 +40,6 @@ namespace mlir::qco { /** * @brief Whether `op` is inside an `inv`/`ctrl` body. * - * The modifier's combined unitary is fused as one run member; gates inside its - * body are not separate run members. - * * @param op The operation to test. * @return `true` if any ancestor is `inv` or `ctrl`. */ @@ -110,12 +107,12 @@ static Matrix2x2 composeRun(ArrayRef run) { /** * @brief Whether `op` is a gate the target `basis` emits. * - * Gate sets match `emitKAK` and `emitFromPSXSequence` in `Euler.cpp`. Used to + * Gate sets match `emitKAK` and `emitFromZSXXSequence` in `Euler.cpp`. Used to * skip runs that are already in the target basis at canonical length. * * @param op The operation to classify. * @param basis The target Euler basis. - * @return `true` if `op` is emitted by synthesis in `basis`. + * @return `true` if `op` is in the gate set Euler synthesis emits for `basis`. */ static bool isTargetBasisGate(Operation* op, decomposition::EulerBasis basis) { using decomposition::EulerBasis; @@ -188,8 +185,8 @@ struct FuseSingleQubitUnitaryRunsPattern final /** * @brief Fuses the run anchored at `op` when beneficial. * - * Fuses if the run contains a non-basis gate or is longer than the canonical - * synthesis for its composed matrix. + * Fuses if the run contains a non-basis gate or Euler resynthesis would + * shorten it (`wouldShortenInBasisRun`). * * @param op The matched unitary operation. * @param rewriter The pattern rewriter. diff --git a/mlir/lib/Dialect/QCO/Utils/WireIterator.cpp b/mlir/lib/Dialect/QCO/Utils/WireIterator.cpp index 1e489d2017..bb7bb6932f 100644 --- a/mlir/lib/Dialect/QCO/Utils/WireIterator.cpp +++ b/mlir/lib/Dialect/QCO/Utils/WireIterator.cpp @@ -45,8 +45,9 @@ void WireIterator::forward() { assert(qubit_.hasOneUse() && "expected linear typing"); op_ = *(qubit_.user_begin()); - // A sink/insert/yield defines the end of the qubit wire (dynamic and static). - if (isa(op_)) { + // A sink/insert/yield or region entry defines the end of the qubit wire. + if (isa(op_)) { isSentinel_ = true; return; } @@ -75,7 +76,8 @@ void WireIterator::backward() { // For sinks/deallocations/inserts/yields, qubit_ is an OpOperand. Hence, only // get the def-op. - if (isa(op_)) { + if (isa(op_)) { op_ = qubit_.getDefiningOp(); return; } diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp index a1585ed114..5c08d3c1b4 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -72,25 +72,16 @@ struct SynthesizedCircuit { func::FuncOp func; }; -} // namespace - -[[nodiscard]] static Matrix2x2 scaleMatrix(const Matrix2x2& matrix, - Complex scale) { - return Matrix2x2::fromElements(scale * matrix(0, 0), scale * matrix(0, 1), - scale * matrix(1, 0), scale * matrix(1, 1)); -} +struct ZSXXShortcutCase { + std::string_view label; + std::function makeMatrix; + std::size_t expectedSynthesisCount; + std::size_t expectedRZ; + std::size_t expectedSX; + std::size_t expectedX; +}; -[[nodiscard]] static bool hasConstant2x2Matrix(Operation& op) { - if (isa(op)) { - return false; - } - auto unitary = dyn_cast(op); - if (!unitary || !unitary.isSingleQubit()) { - return false; - } - Matrix2x2 matrix; - return unitary.getUnitaryMatrix2x2(matrix); -} +} // namespace [[nodiscard]] static Matrix2x2 rzMatrix(double theta) { const auto m00 = std::polar(1.0, -theta / 2.0); @@ -115,12 +106,24 @@ struct SynthesizedCircuit { globalPhase * su2(1, 1)); } -template static void forEachBasis(Fn fn) { - const std::array bases = {"zyz", "zxz", "xzx", - "xyx", "u", "zsxx"}; - for (const char* basis : bases) { - fn(StringRef{basis}); +template +[[nodiscard]] static Matrix2x2 rotationMatrix(MLIRContext* ctx, double theta) { + OpBuilder builder(ctx); + auto module = ModuleOp::create(UnknownLoc::get(ctx)); + builder.setInsertionPointToStart(module.getBody()); + const Location loc = module.getLoc(); + Value q = builder.create(loc).getResult(); + auto op = builder.create(loc, q, theta); + const auto matrix = cast(op).getUnitaryMatrix(); + EXPECT_TRUE(matrix.has_value()); + return *matrix; +} + +[[nodiscard]] static bool isTwoQubitGate(Operation& op) { + if (auto u = dyn_cast(op)) { + return u.isTwoQubit(); } + return false; } static bool isAllowedBasisGate(Operation& op, StringRef basis) { @@ -151,78 +154,11 @@ static bool isAllowedBasisGate(Operation& op, StringRef basis) { return false; } -[[nodiscard]] static bool isTwoQubitGate(Operation& op) { - if (auto u = dyn_cast(op)) { - return u.isTwoQubit(); - } - return false; -} - -// Matrix-backed 1Q gate (not barrier, 2Q, or `gphase`). -[[nodiscard]] static bool isOneQubitGate(Operation& op) { - if (isa(op)) { - return false; - } - return hasConstant2x2Matrix(op); -} - -// At least one 1Q gate before and after the first `isBoundary` op in `main`. -template -static void expectOneQubitGatesAroundBoundary(func::FuncOp funcOp, - StringRef basis, - BoundaryPred isBoundary) { - auto& block = funcOp.getBody().front(); - std::size_t before = 0; - std::size_t after = 0; - bool seenBoundary = false; - for (Operation& op : block.without_terminator()) { - if (!seenBoundary && isBoundary(op)) { - seenBoundary = true; - continue; - } - if (!isOneQubitGate(op)) { - continue; - } - if (seenBoundary) { - ++after; - } else { - ++before; - } - } - EXPECT_GE(before, 1U) << "basis=" << basis.str(); - EXPECT_GE(after, 1U) << "basis=" << basis.str(); -} - -[[nodiscard]] static bool isSingleQubitUnitaryOp(Operation& op) { - if (isa(op)) { - return false; - } - auto unitary = dyn_cast(op); - return unitary && unitary.isSingleQubit(); -} - -static void expectBasisGatesOnly(func::FuncOp funcOp, StringRef basis) { - auto& block = funcOp.getBody().front(); - for (Operation& op : block.without_terminator()) { - if (isa(op)) { - continue; - } - - if (isTwoQubitGate(op) || isa(op)) { - continue; - } - - if (!isSingleQubitUnitaryOp(op)) { - continue; - } - - Matrix2x2 matrix; - ASSERT_TRUE(cast(op).getUnitaryMatrix2x2(matrix)) - << "basis=" << basis.str() << " missing constant matrix for: " - << op.getName().getStringRef().str(); - EXPECT_TRUE(isAllowedBasisGate(op, basis)) - << "basis=" << basis.str() - << " unexpected gate: " << op.getName().getStringRef().str(); +template static void forEachBasis(Fn fn) { + const std::array bases = {"zyz", "zxz", "xzx", + "xyx", "u", "zsxx"}; + for (const char* basis : bases) { + fn(StringRef{basis}); } } @@ -289,15 +225,132 @@ static Matrix2x2 compute1QMatrixFromFunction(func::FuncOp funcOp) { if (failed) { return Matrix2x2::fromElements(0, 0, 0, 0); } - return scaleMatrix(acc, global); + return Matrix2x2::fromElements(global * acc(0, 0), global * acc(0, 1), + global * acc(1, 0), global * acc(1, 1)); } -static LogicalResult runFuse(ModuleOp module, StringRef basis) { - PassManager pm(module.getContext()); - qco::FuseSingleQubitUnitaryRunsOptions opts; - opts.basis = basis.str(); - pm.addPass(qco::createFuseSingleQubitUnitaryRuns(opts)); - return pm.run(module); +static void expectMatrixPreserved(func::FuncOp funcOp, + const Matrix2x2& original, StringRef label) { + EXPECT_TRUE(compute1QMatrixFromFunction(funcOp).isApprox( + original, mlir::utils::TOLERANCE)) + << label.str(); +} + +static void expectBasisGatesOnly(func::FuncOp funcOp, StringRef basis) { + auto& block = funcOp.getBody().front(); + for (Operation& op : block.without_terminator()) { + if (isa(op)) { + continue; + } + + if (isTwoQubitGate(op) || isa(op)) { + continue; + } + + if (isa(op)) { + continue; + } + auto unitaryOp = dyn_cast(op); + if (!unitaryOp || !unitaryOp.isSingleQubit()) { + continue; + } + + Matrix2x2 matrix; + ASSERT_TRUE(unitaryOp.getUnitaryMatrix2x2(matrix)) + << "basis=" << basis.str() << " missing constant matrix for: " + << op.getName().getStringRef().str(); + EXPECT_TRUE(isAllowedBasisGate(op, basis)) + << "basis=" << basis.str() + << " unexpected gate: " << op.getName().getStringRef().str(); + } +} + +// At least one 1Q gate before and after the first `isBoundary` op in `main`. +template +static void expectOneQubitGatesAroundBoundary(func::FuncOp funcOp, + StringRef basis, + BoundaryPred isBoundary) { + auto& block = funcOp.getBody().front(); + std::size_t before = 0; + std::size_t after = 0; + bool seenBoundary = false; + for (Operation& op : block.without_terminator()) { + if (!seenBoundary && isBoundary(op)) { + seenBoundary = true; + continue; + } + if (isa(op)) { + continue; + } + auto unitaryOp = dyn_cast(op); + if (!unitaryOp || !unitaryOp.isSingleQubit()) { + continue; + } + Matrix2x2 matrix; + if (!unitaryOp.getUnitaryMatrix2x2(matrix)) { + continue; + } + if (seenBoundary) { + ++after; + } else { + ++before; + } + } + EXPECT_GE(before, 1U) << "basis=" << basis.str(); + EXPECT_GE(after, 1U) << "basis=" << basis.str(); +} + +template +[[nodiscard]] static std::size_t countOps(func::FuncOp funcOp) { + std::size_t count = 0; + funcOp.walk([&](OpTy) { ++count; }); + return count; +} + +[[nodiscard]] static std::size_t countZSXXBasisGates(func::FuncOp funcOp) { + return countOps(funcOp) + countOps(funcOp) + + countOps(funcOp); +} + +[[nodiscard]] static bool isInsideScfFor(Operation* op) { + return op != nullptr && op->getParentOfType() != nullptr; +} + +template +[[nodiscard]] static std::size_t countOpsInScfFor(func::FuncOp funcOp) { + std::size_t count = 0; + funcOp.walk([&](OpTy op) { + if (isInsideScfFor(op.getOperation())) { + ++count; + } + }); + return count; +} + +static void expectOneQubitGatesInAndOutsideScfFor(func::FuncOp funcOp, + StringRef basis) { + std::size_t outside = 0; + std::size_t inside = 0; + funcOp.walk([&](Operation* op) { + if (isa(*op)) { + return; + } + auto unitary = dyn_cast(op); + if (!unitary || !unitary.isSingleQubit()) { + return; + } + Matrix2x2 matrix; + if (!unitary.getUnitaryMatrix2x2(matrix)) { + return; + } + if (isInsideScfFor(op)) { + ++inside; + } else { + ++outside; + } + }); + EXPECT_GE(outside, 1U) << "basis=" << basis.str(); + EXPECT_GE(inside, 1U) << "basis=" << basis.str(); } static void singleQubitRunWithSingleQubitGate(QCOProgramBuilder& b) { @@ -367,60 +420,73 @@ static void singleQubitRunInScfFor(QCOProgramBuilder& b) { }); } -[[nodiscard]] static std::size_t countUOpsInScfFor(func::FuncOp funcOp) { - std::size_t count = 0; - funcOp.walk([&](UOp op) { - for (Operation* parent = op->getParentOp(); parent != nullptr; - parent = parent->getParentOp()) { - if (parent->getName().getStringRef() == "scf.for") { - ++count; - break; - } - } +static void singleQubitRunsSplitByScfFor(QCOProgramBuilder& b) { + auto q = b.allocQubitRegister(1); + q[0] = b.h(q[0]); + q[0] = b.t(q[0]); + b.scfFor(0, 1, 1, ValueRange{q[0]}, [&](Value, ValueRange iterArgs) { + Value wire = iterArgs[0]; + wire = b.rz(0.321, wire); + wire = b.sx(wire); + return SmallVector{wire}; }); - return count; } -static OwningOpRef buildProgram(MLIRContext* ctx, - void (*fn)(QCOProgramBuilder&)) { - QCOProgramBuilder builder(ctx); - builder.initialize(); - fn(builder); - return builder.finalize(); +[[nodiscard]] static SynthesizedCircuit +synthesizeMatrix(MLIRContext* ctx, const Matrix2x2& matrix, EulerBasis basis) { + OwningOpRef module = ModuleOp::create(UnknownLoc::get(ctx)); + OpBuilder builder(ctx); + builder.setInsertionPointToStart(module->getBody()); + + auto qubitTy = QubitType::get(ctx); + auto funcTy = builder.getFunctionType({qubitTy}, {qubitTy}); + auto func = builder.create(module->getLoc(), "main", funcTy); + auto* entry = func.addEntryBlock(); + + builder.setInsertionPointToStart(entry); + Value q = entry->getArgument(0); + q = synthesizeUnitary1QEuler(builder, module->getLoc(), q, matrix, basis); + builder.create(module->getLoc(), q); + return SynthesizedCircuit{.module = std::move(module), .func = func}; } -static func::FuncOp lookupMain(ModuleOp module) { - auto func = module.lookupSymbol("main"); - EXPECT_TRUE(func) << "Expected a 'main' function"; - return func; +template +static void expectSynthesizedMatrix(MLIRContext* ctx, const Matrix2x2& matrix, + EulerBasis basis, + ExtraChecksT extraChecks) { + const auto circuit = synthesizeMatrix(ctx, matrix, basis); + ASSERT_TRUE(succeeded(verify(*circuit.module))); + extraChecks(circuit.func, matrix); + expectMatrixPreserved(circuit.func, matrix, "synthesis"); } -static void expectMatrixPreserved(func::FuncOp funcOp, - const Matrix2x2& original, StringRef label) { - EXPECT_TRUE(compute1QMatrixFromFunction(funcOp).isApprox( - original, mlir::utils::TOLERANCE)) - << label.str(); +static LogicalResult runFuse(ModuleOp module, StringRef basis) { + PassManager pm(module.getContext()); + qco::FuseSingleQubitUnitaryRunsOptions opts; + opts.basis = basis.str(); + pm.addPass(qco::createFuseSingleQubitUnitaryRuns(opts)); + return pm.run(module); } template static void runFuseOnProgram(MLIRContext* ctx, void (*program)(QCOProgramBuilder&), StringRef basis, BeforeT beforeFuse, AfterT afterFuse) { - auto owned = buildProgram(ctx, program); + auto owned = QCOProgramBuilder::build(ctx, program); ASSERT_TRUE(owned); ModuleOp module = *owned; ASSERT_TRUE(succeeded(verify(module))); - auto funcOp = lookupMain(module); - ASSERT_TRUE(funcOp); + auto funcOp = module.lookupSymbol("main"); + ASSERT_TRUE(funcOp) << "Expected a 'main' function"; const Matrix2x2 original = compute1QMatrixFromFunction(funcOp); beforeFuse(funcOp, original); ASSERT_TRUE(succeeded(runFuse(module, basis))); ASSERT_TRUE(succeeded(verify(module))); - funcOp = lookupMain(module); - ASSERT_TRUE(funcOp); + funcOp = module.lookupSymbol("main"); + ASSERT_TRUE(funcOp) << "Expected a 'main' function"; afterFuse(funcOp, original); } @@ -447,69 +513,158 @@ static void runFuseOnProgramForAllBases(MLIRContext* ctx, }); } -template -[[nodiscard]] static Matrix2x2 rotationMatrix(MLIRContext* ctx, double theta) { - OpBuilder builder(ctx); - auto module = ModuleOp::create(UnknownLoc::get(ctx)); - builder.setInsertionPointToStart(module.getBody()); - const Location loc = module.getLoc(); - Value q = builder.create(loc).getResult(); - auto op = builder.create(loc, q, theta); - const auto matrix = cast(op).getUnitaryMatrix(); - EXPECT_TRUE(matrix.has_value()); - return *matrix; -} - -[[nodiscard]] static SynthesizedCircuit -synthesizeMatrix(MLIRContext* ctx, const Matrix2x2& matrix, EulerBasis basis) { - OwningOpRef module = ModuleOp::create(UnknownLoc::get(ctx)); - OpBuilder builder(ctx); - builder.setInsertionPointToStart(module->getBody()); +TEST(EulerDecompositionTest, ZYZAnglesFromUnitaryReconstructHadamard) { + SynthesisFixture fx; + fx.setUp(); - auto qubitTy = QubitType::get(ctx); + const Matrix2x2 hadamard = HOp::getUnitaryMatrix(); + const auto [theta, phi, lambda, phase] = + EulerDecomposition::anglesFromUnitary(hadamard, EulerBasis::ZYZ); + + auto module = ModuleOp::create(UnknownLoc::get(fx.context.get())); + OpBuilder builder(fx.context.get()); + builder.setInsertionPointToStart(module.getBody()); + const Location loc = module.getLoc(); + + auto qubitTy = QubitType::get(fx.context.get()); auto funcTy = builder.getFunctionType({qubitTy}, {qubitTy}); - auto func = builder.create(module->getLoc(), "main", funcTy); + auto func = builder.create(loc, "main", funcTy); auto* entry = func.addEntryBlock(); - builder.setInsertionPointToStart(entry); + Value q = entry->getArgument(0); - q = synthesizeUnitary1QEuler(builder, module->getLoc(), q, matrix, basis); - builder.create(module->getLoc(), q); - return SynthesizedCircuit{.module = std::move(module), .func = func}; -} + auto mkAngle = [&](double angle) -> Value { + return builder + .create(loc, builder.getF64FloatAttr(angle)) + .getResult(); + }; + q = builder.create(loc, q, mkAngle(lambda)).getQubitOut(); + q = builder.create(loc, q, mkAngle(theta)).getQubitOut(); + q = builder.create(loc, q, mkAngle(phi)).getQubitOut(); + if (std::abs(phase) > mlir::utils::TOLERANCE) { + Value phaseVal = mkAngle(phase); + builder.create(loc, phaseVal); + } + builder.create(loc, q); -template -[[nodiscard]] static std::size_t countOps(func::FuncOp funcOp) { - std::size_t count = 0; - funcOp.walk([&](OpTy) { ++count; }); - return count; + ASSERT_TRUE(succeeded(verify(module))); + EXPECT_TRUE(compute1QMatrixFromFunction(func).isApprox( + hadamard, mlir::utils::TOLERANCE)); } -[[nodiscard]] static std::size_t countZSXXBasisGates(func::FuncOp funcOp) { - return countOps(funcOp) + countOps(funcOp) + - countOps(funcOp); +TEST(EulerSynthesisTest, ProfitabilityAndIdentityGateCount) { + SynthesisFixture fx; + fx.setUp(); + + const Matrix2x2 identity = Matrix2x2::identity(); + EXPECT_TRUE(wouldShortenInBasisRun(6, identity, EulerBasis::ZYZ)); + EXPECT_TRUE(wouldShortenInBasisRun(2, identity, EulerBasis::ZYZ)); + EXPECT_FALSE( + wouldShortenInBasisRun(1, XOp::getUnitaryMatrix(), EulerBasis::ZSXX)); + EXPECT_EQ(synthesisGateCount(identity, EulerBasis::ZYZ), 0U); } -template -static void expectSynthesizedMatrix(MLIRContext* ctx, const Matrix2x2& matrix, - EulerBasis basis, - ExtraChecksT extraChecks) { - const auto circuit = synthesizeMatrix(ctx, matrix, basis); - ASSERT_TRUE(succeeded(verify(*circuit.module))); - extraChecks(circuit.func, matrix); - expectMatrixPreserved(circuit.func, matrix, "synthesis"); +class ZSXXShortcutTest : public testing::TestWithParam {}; + +TEST_P(ZSXXShortcutTest, SynthesisMatchesGateCount) { + SynthesisFixture fx; + fx.setUp(); + + const auto& testCase = GetParam(); + const Matrix2x2 matrix = testCase.makeMatrix(fx.context.get()); + EXPECT_EQ(synthesisGateCount(matrix, EulerBasis::ZSXX), + testCase.expectedSynthesisCount); + + expectSynthesizedMatrix( + fx.context.get(), matrix, EulerBasis::ZSXX, + [&](func::FuncOp funcOp, const Matrix2x2& original) { + EXPECT_EQ(countOps(funcOp), testCase.expectedRZ); + EXPECT_EQ(countOps(funcOp), testCase.expectedSX); + EXPECT_EQ(countOps(funcOp), testCase.expectedX); + EXPECT_EQ(countZSXXBasisGates(funcOp), + synthesisGateCount(original, EulerBasis::ZSXX)); + }); } -[[nodiscard]] static std::size_t countTwoQubitGates(func::FuncOp funcOp) { - std::size_t count = 0; - funcOp.walk([&](UnitaryOpInterface op) { - if (op.isTwoQubit()) { - ++count; - } - }); - return count; +INSTANTIATE_TEST_SUITE_P( + ZSXXShortcuts, ZSXXShortcutTest, + testing::Values(ZSXXShortcutCase{"PauliX", + [](MLIRContext*) -> Matrix2x2 { + return XOp::getUnitaryMatrix(); + }, + 1, 0, 0, 1}, + ZSXXShortcutCase{"PureZ", + [](MLIRContext*) -> Matrix2x2 { + return rzMatrix(0.3) * rzMatrix(0.7); + }, + 2, 2, 0, 0}, + ZSXXShortcutCase{"RyHalfPi", + [](MLIRContext* ctx) -> Matrix2x2 { + return rotationMatrix( + ctx, std::numbers::pi / 2.0); + }, + 3, 2, 1, 0}, + ZSXXShortcutCase{"RyNearHalfPi", + [](MLIRContext* ctx) -> Matrix2x2 { + return rotationMatrix( + ctx, + (std::numbers::pi / 2.0) + + (0.5 * mlir::utils::TOLERANCE)); + }, + 3, 2, 1, 0}), + [](const testing::TestParamInfo& info) { + return std::string(info.param.label); + }); + +// NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_P` at global scope +class EulerSynthesisExactTest + : public testing::TestWithParam< + std::tuple> {}; + +TEST_P(EulerSynthesisExactTest, ReconstructsReferenceMatrices) { + SynthesisFixture fx; + fx.setUp(); + + const auto [basis, matrixFn] = GetParam(); + const Matrix2x2 original = matrixFn(fx.context.get()); + expectSynthesizedMatrix(fx.context.get(), original, basis, + [&](func::FuncOp funcOp, const Matrix2x2& matrix) { + if (basis == EulerBasis::U) { + EXPECT_LE(countOps(funcOp), 1U); + } + if (basis == EulerBasis::ZYZ && + matrix.isApprox(Matrix2x2::identity())) { + EXPECT_EQ(countOps(funcOp), 0U); + EXPECT_EQ(countOps(funcOp), 0U); + } + }); } +INSTANTIATE_TEST_SUITE_P( + SingleQubitMatrices, EulerSynthesisExactTest, + testing::Combine( + testing::Values(EulerBasis::ZYZ, EulerBasis::ZXZ, EulerBasis::XZX, + EulerBasis::XYX, EulerBasis::U, EulerBasis::ZSXX), + testing::Values([](MLIRContext* /*ctx*/) + -> Matrix2x2 { return Matrix2x2::identity(); }, + [](MLIRContext* ctx) -> Matrix2x2 { + return rotationMatrix(ctx, 2.0); + }, + // RY(pi/2): ZSXX single-SX branch. + [](MLIRContext* ctx) -> Matrix2x2 { + return rotationMatrix(ctx, + std::numbers::pi / 2.0); + }, + [](MLIRContext* ctx) -> Matrix2x2 { + return rotationMatrix(ctx, 0.5); + }, + [](MLIRContext* ctx) -> Matrix2x2 { + return rotationMatrix(ctx, 3.14); + }, + [](MLIRContext* /*ctx*/) -> Matrix2x2 { + return HOp::getUnitaryMatrix(); + }))); + TEST(EulerSynthesisTest, RandomReconstructionAllBases) { SynthesisFixture fx; fx.setUp(); @@ -533,15 +688,17 @@ TEST(EulerSynthesisTest, RandomReconstructionAllBases) { } } -TEST(FuseSingleQubitUnitaryRunsTest, FusesRunInScfForBody) { +TEST(FuseSingleQubitUnitaryRunsTest, InvalidBasisFailsPass) { SynthesisFixture fx; fx.setUp(); - runFuseOnProgram(fx.context.get(), &singleQubitRunInScfFor, "u", - [&](func::FuncOp funcOp, const Matrix2x2& original) { - EXPECT_GE(countUOpsInScfFor(funcOp), 1U); - expectMatrixPreserved(funcOp, original, "scf.for"); - }); + auto owned = QCOProgramBuilder::build(fx.context.get(), + &singleQubitRunWithSingleQubitGate); + ASSERT_TRUE(static_cast(owned)); + ModuleOp module = *owned; + ASSERT_TRUE(succeeded(verify(module))); + + EXPECT_TRUE(failed(runFuse(module, "not-a-basis"))); } TEST(FuseSingleQubitUnitaryRunsTest, ReconstructsOriginalRunAllBases) { @@ -563,7 +720,6 @@ TEST(FuseSingleQubitUnitaryRunsTest, ResynthesizesLoneNonBasisGateAllBases) { runFuseOnProgramForAllBases( fx.context.get(), &singleNonBasisGate, - /*checksAfter=*/ [&](func::FuncOp funcOp, StringRef basis, const Matrix2x2& original) { EXPECT_EQ(countOps(funcOp), 0U) << "basis=" << basis.str() << " left a non-basis gate"; @@ -587,7 +743,6 @@ TEST(FuseSingleQubitUnitaryRunsTest, FusesOverlongInBasisRun) { const std::size_t after = countOps(funcOp) + countOps(funcOp); EXPECT_LE(after, 3U); - EXPECT_LT(after, 6U); expectMatrixPreserved(funcOp, original, "zyz"); expectBasisGatesOnly(funcOp, "zyz"); }); @@ -620,188 +775,20 @@ TEST(FuseSingleQubitUnitaryRunsTest, }); } -TEST(EulerSynthesisTest, ProfitabilityAndIdentityGateCount) { - SynthesisFixture fx; - fx.setUp(); - - const Matrix2x2 identity = Matrix2x2::identity(); - EXPECT_TRUE(wouldShortenInBasisRun(6, identity, EulerBasis::ZYZ)); - EXPECT_TRUE(wouldShortenInBasisRun(2, identity, EulerBasis::ZYZ)); - EXPECT_FALSE( - wouldShortenInBasisRun(1, XOp::getUnitaryMatrix(), EulerBasis::ZSXX)); - EXPECT_EQ(synthesisGateCount(identity, EulerBasis::ZYZ), 0U); - - expectSynthesizedMatrix(fx.context.get(), identity, EulerBasis::ZYZ, - [](func::FuncOp funcOp, const Matrix2x2&) { - EXPECT_EQ(countOps(funcOp), 0U); - EXPECT_EQ(countOps(funcOp), 0U); - }); -} - -struct ZSXXShortcutCase { - std::string_view label; - std::function makeMatrix; - std::size_t expectedSynthesisCount; - std::size_t expectedRz; - std::size_t expectedSx; - std::size_t expectedX; -}; - -// NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_P` at global scope -class ZSXXShortcutTest : public testing::TestWithParam {}; - -TEST_P(ZSXXShortcutTest, SynthesisMatchesGateCount) { - SynthesisFixture fx; - fx.setUp(); - - const auto& testCase = GetParam(); - const Matrix2x2 matrix = testCase.makeMatrix(fx.context.get()); - EXPECT_EQ(synthesisGateCount(matrix, EulerBasis::ZSXX), - testCase.expectedSynthesisCount); - - expectSynthesizedMatrix( - fx.context.get(), matrix, EulerBasis::ZSXX, - [&](func::FuncOp funcOp, const Matrix2x2& original) { - EXPECT_EQ(countOps(funcOp), testCase.expectedRz); - EXPECT_EQ(countOps(funcOp), testCase.expectedSx); - EXPECT_EQ(countOps(funcOp), testCase.expectedX); - EXPECT_EQ(countZSXXBasisGates(funcOp), - synthesisGateCount(original, EulerBasis::ZSXX)); - }); -} - -INSTANTIATE_TEST_SUITE_P( - ZSXXShortcuts, ZSXXShortcutTest, - testing::Values(ZSXXShortcutCase{"PauliX", - [](MLIRContext*) -> Matrix2x2 { - return XOp::getUnitaryMatrix(); - }, - 1, 0, 0, 1}, - ZSXXShortcutCase{"PureZ", - [](MLIRContext*) -> Matrix2x2 { - return rzMatrix(0.3) * rzMatrix(0.7); - }, - 2, 2, 0, 0}, - ZSXXShortcutCase{"RyHalfPi", - [](MLIRContext* ctx) -> Matrix2x2 { - return rotationMatrix( - ctx, std::numbers::pi / 2.0); - }, - 3, 2, 1, 0}, - ZSXXShortcutCase{"RyNearHalfPi", - [](MLIRContext* ctx) -> Matrix2x2 { - return rotationMatrix( - ctx, - (std::numbers::pi / 2.0) + - (0.5 * mlir::utils::TOLERANCE)); - }, - 3, 2, 1, 0}), - [](const testing::TestParamInfo& info) { - return std::string(info.param.label); - }); - -TEST(EulerSynthesisTest, UGateReconstruction) { - SynthesisFixture fx; - fx.setUp(); - - std::mt19937 rng{99991}; - for (int i = 0; i < 32; ++i) { - const auto u = randomUnitaryMatrix(rng); - expectSynthesizedMatrix(fx.context.get(), u, EulerBasis::U, - [](func::FuncOp funcOp, const Matrix2x2&) { - EXPECT_LE(countOps(funcOp), 1U); - }); - } -} - -TEST(EulerDecompositionTest, ZYZAnglesFromUnitaryReconstructHadamard) { - SynthesisFixture fx; - fx.setUp(); - - const Matrix2x2 hadamard = HOp::getUnitaryMatrix(); - const auto [theta, phi, lambda, phase] = - EulerDecomposition::anglesFromUnitary(hadamard, EulerBasis::ZYZ); - - auto module = ModuleOp::create(UnknownLoc::get(fx.context.get())); - OpBuilder builder(fx.context.get()); - builder.setInsertionPointToStart(module.getBody()); - const Location loc = module.getLoc(); - - auto qubitTy = QubitType::get(fx.context.get()); - auto funcTy = builder.getFunctionType({qubitTy}, {qubitTy}); - auto func = builder.create(loc, "main", funcTy); - auto* entry = func.addEntryBlock(); - builder.setInsertionPointToStart(entry); - - Value q = entry->getArgument(0); - auto mkAngle = [&](double angle) -> Value { - return builder - .create(loc, builder.getF64FloatAttr(angle)) - .getResult(); - }; - q = builder.create(loc, q, mkAngle(lambda)).getQubitOut(); - q = builder.create(loc, q, mkAngle(theta)).getQubitOut(); - q = builder.create(loc, q, mkAngle(phi)).getQubitOut(); - if (std::abs(phase) > mlir::utils::TOLERANCE) { - Value phaseVal = mkAngle(phase); - builder.create(loc, phaseVal); - } - builder.create(loc, q); - - ASSERT_TRUE(succeeded(verify(module))); - EXPECT_TRUE(compute1QMatrixFromFunction(func).isApprox( - hadamard, mlir::utils::TOLERANCE)); -} - -// NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_P` at global scope -class EulerSynthesisExactTest - : public testing::TestWithParam< - std::tuple> {}; - -TEST_P(EulerSynthesisExactTest, ReconstructsReferenceMatrices) { - SynthesisFixture fx; - fx.setUp(); - - const auto [basis, matrixFn] = GetParam(); - const Matrix2x2 original = matrixFn(fx.context.get()); - expectSynthesizedMatrix(fx.context.get(), original, basis, - [](func::FuncOp, const Matrix2x2&) {}); -} - -INSTANTIATE_TEST_SUITE_P( - SingleQubitMatrices, EulerSynthesisExactTest, - testing::Combine( - testing::Values(EulerBasis::XYX, EulerBasis::XZX, EulerBasis::ZYZ, - EulerBasis::ZXZ, EulerBasis::U, EulerBasis::ZSXX), - testing::Values([](MLIRContext* /*ctx*/) - -> Matrix2x2 { return Matrix2x2::identity(); }, - [](MLIRContext* ctx) -> Matrix2x2 { - return rotationMatrix(ctx, 2.0); - }, - // RY(pi/2): ZSXX single-SX branch. - [](MLIRContext* ctx) -> Matrix2x2 { - return rotationMatrix(ctx, - std::numbers::pi / 2.0); - }, - [](MLIRContext* ctx) -> Matrix2x2 { - return rotationMatrix(ctx, 0.5); - }, - [](MLIRContext* ctx) -> Matrix2x2 { - return rotationMatrix(ctx, 3.14); - }, - [](MLIRContext* /*ctx*/) -> Matrix2x2 { - return HOp::getUnitaryMatrix(); - }))); - TEST(FuseSingleQubitUnitaryRunsTest, DoesNotFuseAcrossTwoQGateAllBases) { SynthesisFixture fx; fx.setUp(); runFuseOnProgramForAllBases( fx.context.get(), &singleQubitRunsSplitByTwoQGate, - /*checksAfter=*/ [&](func::FuncOp funcOp, StringRef basis, const Matrix2x2& original) { - EXPECT_EQ(countTwoQubitGates(funcOp), 1U) << "basis=" << basis.str(); + std::size_t twoQubitGates = 0; + funcOp.walk([&](UnitaryOpInterface op) { + if (op.isTwoQubit()) { + ++twoQubitGates; + } + }); + EXPECT_EQ(twoQubitGates, 1U) << "basis=" << basis.str(); expectMatrixPreserved(funcOp, original, basis); expectBasisGatesOnly(funcOp, basis); expectOneQubitGatesAroundBoundary( @@ -824,15 +811,37 @@ TEST(FuseSingleQubitUnitaryRunsTest, DoesNotFuseAcrossBarrierAllBases) { }); } -TEST(FuseSingleQubitUnitaryRunsTest, InvalidBasisFailsPass) { +TEST(FuseSingleQubitUnitaryRunsTest, FusesRunInScfForBody) { SynthesisFixture fx; fx.setUp(); - auto owned = - buildProgram(fx.context.get(), &singleQubitRunWithSingleQubitGate); - ASSERT_TRUE(static_cast(owned)); - ModuleOp module = *owned; - ASSERT_TRUE(succeeded(verify(module))); + runFuseOnProgram( + fx.context.get(), &singleQubitRunInScfFor, "u", + [](func::FuncOp funcOp, const Matrix2x2&) { + EXPECT_EQ(countOpsInScfFor(funcOp), 1U); + EXPECT_EQ(countOpsInScfFor(funcOp), 1U); + EXPECT_EQ(countOpsInScfFor(funcOp), 1U); + EXPECT_EQ(countOpsInScfFor(funcOp), 0U); + }, + [](func::FuncOp funcOp, const Matrix2x2& original) { + EXPECT_EQ(countOpsInScfFor(funcOp), 1U); + EXPECT_EQ(countOpsInScfFor(funcOp), 0U); + EXPECT_EQ(countOpsInScfFor(funcOp), 0U); + EXPECT_EQ(countOpsInScfFor(funcOp), 0U); + expectMatrixPreserved(funcOp, original, "scf.for"); + }); +} - EXPECT_TRUE(failed(runFuse(module, "not-a-basis"))); +TEST(FuseSingleQubitUnitaryRunsTest, DoesNotFuseAcrossScfForAllBases) { + SynthesisFixture fx; + fx.setUp(); + + runFuseOnProgramForAllBases( + fx.context.get(), &singleQubitRunsSplitByScfFor, + [&](func::FuncOp funcOp, StringRef basis, const Matrix2x2& original) { + EXPECT_EQ(countOps(funcOp), 1U) << "basis=" << basis.str(); + expectMatrixPreserved(funcOp, original, basis); + expectBasisGatesOnly(funcOp, basis); + expectOneQubitGatesInAndOutsideScfFor(funcOp, basis); + }); } From 981fdc89fc6529545de7d25d8c4f4de92a3cdcb5 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 10 Jun 2026 15:14:07 +0200 Subject: [PATCH 32/68] =?UTF-8?q?=E2=98=82=EF=B8=8F=20increase=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Dialect/QCO/Utils/test_unitary_matrix.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp index ceb1208e9e..b7cf87e9bc 100644 --- a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp +++ b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp @@ -218,6 +218,18 @@ TEST(DynamicMatrix, IsApproxRejectsMismatchedExtents) { EXPECT_FALSE(DynamicMatrix::identity(1).isApprox(DynamicMatrix::identity(2))); } +TEST(Matrix1x1, AssignFromDynamicMatrix) { + const Matrix1x1 phase = Matrix1x1::fromElements(0.25 + 0.5i); + + DynamicMatrix dynamic; + dynamic.assignFrom(phase); + + Matrix1x1 out = Matrix1x1::fromElements(1.0); + EXPECT_TRUE(out.assignFrom(dynamic)); + EXPECT_TRUE(out.isApprox(phase)); + EXPECT_FALSE(out.assignFrom(DynamicMatrix::identity(2))); +} + TEST(Matrix2x2, AssignFromDynamicMatrix) { const Matrix2x2 x = pauliX(); From ee5199d1c1e955a9206338f12f30a24d4e0f5c6b Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Wed, 10 Jun 2026 16:02:40 +0200 Subject: [PATCH 33/68] =?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 --- .../test_euler_decomposition.cpp | 47 ++++++++++--------- .../Dialect/QCO/Utils/test_unitary_matrix.cpp | 2 +- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp index 5c08d3c1b4..ec44337dd5 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -81,6 +81,8 @@ struct ZSXXShortcutCase { std::size_t expectedX; }; +class ZSXXShortcutTest : public testing::TestWithParam {}; + } // namespace [[nodiscard]] static Matrix2x2 rzMatrix(double theta) { @@ -115,7 +117,10 @@ template Value q = builder.create(loc).getResult(); auto op = builder.create(loc, q, theta); const auto matrix = cast(op).getUnitaryMatrix(); - EXPECT_TRUE(matrix.has_value()); + if (!matrix) { + ADD_FAILURE() << "Expected constant unitary matrix"; + return Matrix2x2::identity(); + } return *matrix; } @@ -237,32 +242,34 @@ static void expectMatrixPreserved(func::FuncOp funcOp, } static void expectBasisGatesOnly(func::FuncOp funcOp, StringRef basis) { - auto& block = funcOp.getBody().front(); - for (Operation& op : block.without_terminator()) { - if (isa(op)) { - continue; + funcOp.walk([&](Operation* op) -> WalkResult { + if (isa(*op)) { + return WalkResult::advance(); } - - if (isTwoQubitGate(op) || isa(op)) { - continue; + if (isTwoQubitGate(*op)) { + return WalkResult::advance(); } - - if (isa(op)) { - continue; + if (isa(*op)) { + return WalkResult::skip(); } - auto unitaryOp = dyn_cast(op); + + auto unitaryOp = dyn_cast(*op); if (!unitaryOp || !unitaryOp.isSingleQubit()) { - continue; + return WalkResult::advance(); } Matrix2x2 matrix; - ASSERT_TRUE(unitaryOp.getUnitaryMatrix2x2(matrix)) - << "basis=" << basis.str() << " missing constant matrix for: " - << op.getName().getStringRef().str(); - EXPECT_TRUE(isAllowedBasisGate(op, basis)) + if (!unitaryOp.getUnitaryMatrix2x2(matrix)) { + ADD_FAILURE() << "basis=" << basis.str() + << " missing constant matrix for: " + << op->getName().getStringRef().str(); + return WalkResult::interrupt(); + } + EXPECT_TRUE(isAllowedBasisGate(*op, basis)) << "basis=" << basis.str() - << " unexpected gate: " << op.getName().getStringRef().str(); - } + << " unexpected gate: " << op->getName().getStringRef().str(); + return WalkResult::advance(); + }); } // At least one 1Q gate before and after the first `isBoundary` op in `main`. @@ -564,8 +571,6 @@ TEST(EulerSynthesisTest, ProfitabilityAndIdentityGateCount) { EXPECT_EQ(synthesisGateCount(identity, EulerBasis::ZYZ), 0U); } -class ZSXXShortcutTest : public testing::TestWithParam {}; - TEST_P(ZSXXShortcutTest, SynthesisMatchesGateCount) { SynthesisFixture fx; fx.setUp(); diff --git a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp index b7cf87e9bc..375813f103 100644 --- a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp +++ b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp @@ -219,7 +219,7 @@ TEST(DynamicMatrix, IsApproxRejectsMismatchedExtents) { } TEST(Matrix1x1, AssignFromDynamicMatrix) { - const Matrix1x1 phase = Matrix1x1::fromElements(0.25 + 0.5i); + const Matrix1x1 phase = Matrix1x1::fromElements(Complex{0.25, 0.5}); DynamicMatrix dynamic; dynamic.assignFrom(phase); From 37383f1b5b483836e6c3959de5f0c13a0c971380 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 11 Jun 2026 14:41:03 +0200 Subject: [PATCH 34/68] =?UTF-8?q?=E2=9C=A8=20Implement=20composeSingleQubi?= =?UTF-8?q?tBodyMatrix=20function=20for=20InvOp=20and=20enhance=20getUnita?= =?UTF-8?q?ryMatrix=20logic.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp | 67 +++++++++++++++++-- .../Dialect/QCO/IR/test_qco_ir_matrix.cpp | 14 ++++ 2 files changed, 75 insertions(+), 6 deletions(-) diff --git a/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp b/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp index 4bb5bf4d9f..30005e3a25 100644 --- a/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp @@ -403,15 +403,70 @@ void InvOp::getCanonicalizationPatterns(RewritePatternSet& results, CancelNestedInv, EraseEmptyInv>(context); } -std::optional InvOp::getUnitaryMatrix() { - auto bodyUnitary = utils::getSoleBodyUnitary(*getBody()); - if (!bodyUnitary) { +namespace { + +/** + * @brief Composes compile-time single-qubit unitaries in a modifier body. + */ +[[nodiscard]] std::optional +composeSingleQubitBodyMatrix(Block& block) { + Matrix2x2 acc = Matrix2x2::identity(); + std::complex global{1.0, 0.0}; + bool found = false; + for (Operation& op : block.without_terminator()) { + if (isa(op)) { + continue; + } + if (auto gphase = dyn_cast(op)) { + if (auto matrix = gphase.getUnitaryMatrix()) { + global *= (*matrix)(0, 0); + } + continue; + } + auto unitary = dyn_cast(op); + if (!unitary || !unitary.isSingleQubit()) { + return std::nullopt; + } + Matrix2x2 matrix; + if (!unitary.getUnitaryMatrix2x2(matrix)) { + return std::nullopt; + } + acc = matrix * acc; + found = true; + } + if (!found && global == std::complex{1.0, 0.0}) { return std::nullopt; } - const auto targetMatrix = bodyUnitary.getUnitaryMatrix(); - if (!targetMatrix) { + return Matrix2x2::fromElements(global * acc(0, 0), global * acc(0, 1), + global * acc(1, 0), global * acc(1, 1)); +} + +} // namespace + +std::optional InvOp::getUnitaryMatrix() { + if (auto bodyUnitary = + utils::getSoleBodyUnitary(*getBody())) { + if (const auto targetMatrix = + bodyUnitary.getUnitaryMatrix()) { + return targetMatrix->adjoint(); + } + Matrix2x2 matrix; + if (bodyUnitary.getUnitaryMatrix2x2(matrix)) { + DynamicMatrix result; + result.assignFrom(matrix.adjoint()); + return result; + } return std::nullopt; } - return targetMatrix->adjoint(); + if (getNumTargets() != 1) { + return std::nullopt; + } + const auto bodyMatrix = composeSingleQubitBodyMatrix(*getBody()); + if (!bodyMatrix) { + return std::nullopt; + } + DynamicMatrix result; + result.assignFrom(bodyMatrix->adjoint()); + return result; } diff --git a/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp b/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp index 91c31f9899..893ecff7c9 100644 --- a/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp +++ b/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp @@ -115,6 +115,20 @@ TEST_F(QCOMatrixTest, InverseIswapOpMatrix) { ASSERT_TRUE(matrix->isApprox(expected)); } + +TEST_F(QCOMatrixTest, InverseTwoXOpMatrix) { + auto moduleOp = QCOProgramBuilder::build(context.get(), inverseTwoX); + ASSERT_TRUE(moduleOp); + + auto funcOp = *moduleOp->getBody()->getOps().begin(); + auto invOp = *funcOp.getBody().getOps().begin(); + const auto matrix = invOp.getUnitaryMatrix(); + ASSERT_TRUE(matrix); + + DynamicMatrix expected; + expected.assignFrom(Matrix2x2::identity()); + ASSERT_TRUE(matrix->isApprox(expected)); +} /// @} /// \name QCO/Operations/StandardGates/DcxOp.cpp From 215ad55425eec61ffe1db8553f69324ce15b61c3 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 11 Jun 2026 14:44:03 +0200 Subject: [PATCH 35/68] =?UTF-8?q?=E2=9C=A8=20Fuse=20modifiers=20with=20mul?= =?UTF-8?q?tiple=20single=20unitary=20operations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FuseSingleQubitUnitaryRuns.cpp | 42 ++-- .../test_euler_decomposition.cpp | 217 ++++++++++++++---- 2 files changed, 197 insertions(+), 62 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp index 4af90bb9d1..cd8f4a54e6 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp @@ -38,30 +38,13 @@ namespace mlir::qco { #include "mlir/Dialect/QCO/Transforms/Passes.h.inc" /** - * @brief Whether `op` is inside an `inv`/`ctrl` body. + * @brief Whether `op` is inside an `inv` body. * * @param op The operation to test. - * @return `true` if any ancestor is `inv` or `ctrl`. + * @return `true` if any ancestor is `inv`. */ -static bool isNestedInModifierRegion(Operation* op) { - return op != nullptr && (op->getParentOfType() != nullptr || - op->getParentOfType() != nullptr); -} - -/** - * @brief Whether `op` may participate in a fusable single-qubit run. - * - * @param op The unitary operation to test. - * @return `true` for a single-qubit, matrix-backed unitary on the wire, outside - * a modifier body. - */ -static bool isFuseCandidate(UnitaryOpInterface op) { - if (!op || !op.isSingleQubit() || isNestedInModifierRegion(op) || - isa(op.getOperation())) { - return false; - } - Matrix2x2 matrix; - return op.getUnitaryMatrix2x2(matrix); +static bool isInsideInvBody(Operation* op) { + return op != nullptr && op->getParentOfType() != nullptr; } /** @@ -78,6 +61,20 @@ static std::optional getConstMatrix(UnitaryOpInterface op) { return matrix; } +/** + * @brief Whether `op` may participate in a fusable single-qubit run. + * + * @param op The unitary operation to test. + * @return `true` for a single-qubit, matrix-backed unitary on the wire, + * including inside `inv`/`ctrl` bodies. + */ +static bool isFuseCandidate(UnitaryOpInterface op) { + if (!op || !op.isSingleQubit() || isa(op.getOperation())) { + return false; + } + return getConstMatrix(op).has_value(); +} + /** * @brief Whether `op` can participate in a fusable run. * @@ -158,6 +155,9 @@ struct FuseSingleQubitUnitaryRunsPattern final if (!isRunMember(op.getOperation())) { return false; } + if (isInsideInvBody(op.getOperation())) { + return false; + } Operation* pred = op.getInputTarget(0).getDefiningOp(); return pred == nullptr || !isRunMember(pred); } 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 ec44337dd5..3e718b1ff4 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -18,6 +18,7 @@ #include "mlir/Dialect/Utils/Utils.h" #include +#include #include #include #include @@ -172,8 +173,6 @@ static Matrix2x2 compute1QMatrixFromFunction(func::FuncOp funcOp) { std::complex global{1.0, 0.0}; bool failed = false; - // Include nested regions (`scf.for`); skip `inv`/`ctrl` bodies after the - // modifier op (combined matrix already counted). funcOp.walk([&](Operation* op) -> WalkResult { if (isa(*op)) { return WalkResult::advance(); @@ -272,11 +271,34 @@ static void expectBasisGatesOnly(func::FuncOp funcOp, StringRef basis) { }); } -// At least one 1Q gate before and after the first `isBoundary` op in `main`. +/// Composed unitary of the `h; t` segment in `singleQubitRunsSplitByBarrier`, +/// `singleQubitRunsSplitByTwoQGate`, and `singleQubitRunsSplitByScfFor`. +[[nodiscard]] static Matrix2x2 splitFixtureHtSegmentMatrix() { + return TOp::getUnitaryMatrix() * HOp::getUnitaryMatrix(); +} + +/// Composed unitary of the `rz(0.321); sx` segment in the same split fixtures. +[[nodiscard]] static Matrix2x2 splitFixtureRzSxSegmentMatrix() { + return SXOp::getUnitaryMatrix() * rzMatrix(0.321); +} + +/// Composed unitary of `overlongZSXXMixedPureZRun` (`sx; rz(pi); sx` → pure Z). +[[nodiscard]] static Matrix2x2 overlongZsxxPureZRunMatrix() { + return SXOp::getUnitaryMatrix() * rzMatrix(std::numbers::pi) * + SXOp::getUnitaryMatrix(); +} + +[[nodiscard]] static std::size_t +expectedFusedGateCount(const Matrix2x2& segment, EulerBasis basis) { + return synthesisGateCount(segment, basis); +} + template static void expectOneQubitGatesAroundBoundary(func::FuncOp funcOp, StringRef basis, - BoundaryPred isBoundary) { + BoundaryPred isBoundary, + std::size_t expectedBefore, + std::size_t expectedAfter) { auto& block = funcOp.getBody().front(); std::size_t before = 0; std::size_t after = 0; @@ -303,8 +325,8 @@ static void expectOneQubitGatesAroundBoundary(func::FuncOp funcOp, ++before; } } - EXPECT_GE(before, 1U) << "basis=" << basis.str(); - EXPECT_GE(after, 1U) << "basis=" << basis.str(); + EXPECT_EQ(before, expectedBefore) << "basis=" << basis.str(); + EXPECT_EQ(after, expectedAfter) << "basis=" << basis.str(); } template @@ -323,6 +345,14 @@ template return op != nullptr && op->getParentOfType() != nullptr; } +[[nodiscard]] static bool isInsideInv(Operation* op) { + return op != nullptr && op->getParentOfType() != nullptr; +} + +[[nodiscard]] static bool isInsideCtrl(Operation* op) { + return op != nullptr && op->getParentOfType() != nullptr; +} + template [[nodiscard]] static std::size_t countOpsInScfFor(func::FuncOp funcOp) { std::size_t count = 0; @@ -334,8 +364,22 @@ template return count; } +template +[[nodiscard]] static std::size_t countOpsInRegion(func::FuncOp funcOp, + InRegionPred inRegion) { + std::size_t count = 0; + funcOp.walk([&](OpTy op) { + if (inRegion(op.getOperation())) { + ++count; + } + }); + return count; +} + static void expectOneQubitGatesInAndOutsideScfFor(func::FuncOp funcOp, - StringRef basis) { + StringRef basis, + std::size_t expectedOutside, + std::size_t expectedInside) { std::size_t outside = 0; std::size_t inside = 0; funcOp.walk([&](Operation* op) { @@ -356,8 +400,8 @@ static void expectOneQubitGatesInAndOutsideScfFor(func::FuncOp funcOp, ++outside; } }); - EXPECT_GE(outside, 1U) << "basis=" << basis.str(); - EXPECT_GE(inside, 1U) << "basis=" << basis.str(); + EXPECT_EQ(outside, expectedOutside) << "basis=" << basis.str(); + EXPECT_EQ(inside, expectedInside) << "basis=" << basis.str(); } static void singleQubitRunWithSingleQubitGate(QCOProgramBuilder& b) { @@ -427,6 +471,35 @@ static void singleQubitRunInScfFor(QCOProgramBuilder& b) { }); } +static void xInverseTwoX(QCOProgramBuilder& b) { + auto q = b.allocQubitRegister(1); + q[0] = b.x(q[0]); + q[0] = b.inv({q[0]}, [&](ValueRange targets) { + Value wire = b.x(targets[0]); + wire = b.x(wire); + return SmallVector{wire}; + })[0]; + q[0] = b.x(q[0]); +} + +static void controlledInverseHt(QCOProgramBuilder& b) { + auto q = b.allocQubitRegister(2); + b.ctrl(q[0], q[1], [&](ValueRange targets) { + auto wire = b.inv({targets[0]}, [&](ValueRange innerTargets) { + auto inner = b.h(innerTargets[0]); + inner = b.t(inner); + return SmallVector{inner}; + })[0]; + return SmallVector{wire}; + }); +} + +static void controlledH(QCOProgramBuilder& b) { + auto q = b.allocQubitRegister(2); + b.ctrl(q[0], q[1], + [&](ValueRange targets) { return SmallVector{b.h(targets[0])}; }); +} + static void singleQubitRunsSplitByScfFor(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.h(q[0]); @@ -497,26 +570,16 @@ runFuseOnProgram(MLIRContext* ctx, void (*program)(QCOProgramBuilder&), afterFuse(funcOp, original); } -template -static void runFuseOnProgram(MLIRContext* ctx, - void (*program)(QCOProgramBuilder&), - StringRef basis, ChecksT afterFuse) { - runFuseOnProgram( - ctx, program, basis, [](func::FuncOp, const Matrix2x2&) {}, - [&](func::FuncOp funcOp, const Matrix2x2& original) { - afterFuse(funcOp, original); - }); -} - template static void runFuseOnProgramForAllBases(MLIRContext* ctx, void (*program)(QCOProgramBuilder&), ChecksT checksAfter) { forEachBasis([&](StringRef basis) { - runFuseOnProgram(ctx, program, basis, - [&](func::FuncOp funcOp, const Matrix2x2& original) { - checksAfter(funcOp, basis, original); - }); + runFuseOnProgram( + ctx, program, basis, [](func::FuncOp, const Matrix2x2&) {}, + [&](func::FuncOp funcOp, const Matrix2x2& original) { + checksAfter(funcOp, basis, original); + }); }); } @@ -587,7 +650,7 @@ TEST_P(ZSXXShortcutTest, SynthesisMatchesGateCount) { EXPECT_EQ(countOps(funcOp), testCase.expectedSX); EXPECT_EQ(countOps(funcOp), testCase.expectedX); EXPECT_EQ(countZSXXBasisGates(funcOp), - synthesisGateCount(original, EulerBasis::ZSXX)); + expectedFusedGateCount(original, EulerBasis::ZSXX)); }); } @@ -635,7 +698,8 @@ TEST_P(EulerSynthesisExactTest, ReconstructsReferenceMatrices) { expectSynthesizedMatrix(fx.context.get(), original, basis, [&](func::FuncOp funcOp, const Matrix2x2& matrix) { if (basis == EulerBasis::U) { - EXPECT_LE(countOps(funcOp), 1U); + EXPECT_EQ(countOps(funcOp), + expectedFusedGateCount(matrix, basis)); } if (basis == EulerBasis::ZYZ && matrix.isApprox(Matrix2x2::identity())) { @@ -740,14 +804,11 @@ TEST(FuseSingleQubitUnitaryRunsTest, FusesOverlongInBasisRun) { runFuseOnProgram( fx.context.get(), &overlongZYZRun, "zyz", [](func::FuncOp funcOp, const Matrix2x2&) { - const std::size_t before = - countOps(funcOp) + countOps(funcOp); - ASSERT_EQ(before, 6U); + ASSERT_EQ(countOps(funcOp) + countOps(funcOp), 6U); }, [](func::FuncOp funcOp, const Matrix2x2& original) { - const std::size_t after = - countOps(funcOp) + countOps(funcOp); - EXPECT_LE(after, 3U); + EXPECT_EQ(countOps(funcOp) + countOps(funcOp), + expectedFusedGateCount(original, EulerBasis::ZYZ)); expectMatrixPreserved(funcOp, original, "zyz"); expectBasisGatesOnly(funcOp, "zyz"); }); @@ -760,21 +821,22 @@ TEST(FuseSingleQubitUnitaryRunsTest, runFuseOnProgram( fx.context.get(), &overlongZSXXMixedPureZRun, "zsxx", - [](func::FuncOp funcOp, const Matrix2x2& original) { - EXPECT_EQ(synthesisGateCount(original, EulerBasis::ZSXX), 1U); + [](func::FuncOp funcOp, const Matrix2x2& /*original*/) { + EXPECT_EQ(expectedFusedGateCount(overlongZsxxPureZRunMatrix(), + EulerBasis::ZSXX), + 1U); ASSERT_EQ(countZSXXBasisGates(funcOp), 3U); EXPECT_EQ(countOps(funcOp), 2U); EXPECT_EQ(countOps(funcOp), 1U); EXPECT_EQ(countOps(funcOp), 0U); }, [](func::FuncOp funcOp, const Matrix2x2& original) { - // `OnlyRZ` synthesis emits `RZ(pi)` then `RZ(0)`; the latter is - // identity. EXPECT_EQ(countOps(funcOp), 1U); EXPECT_EQ(countOps(funcOp), 0U); EXPECT_EQ(countOps(funcOp), 0U); - EXPECT_LE(countZSXXBasisGates(funcOp), - synthesisGateCount(original, EulerBasis::ZSXX)); + EXPECT_EQ(countZSXXBasisGates(funcOp), + expectedFusedGateCount(overlongZsxxPureZRunMatrix(), + EulerBasis::ZSXX)); expectMatrixPreserved(funcOp, original, "zsxx"); expectBasisGatesOnly(funcOp, "zsxx"); }); @@ -796,8 +858,12 @@ TEST(FuseSingleQubitUnitaryRunsTest, DoesNotFuseAcrossTwoQGateAllBases) { EXPECT_EQ(twoQubitGates, 1U) << "basis=" << basis.str(); expectMatrixPreserved(funcOp, original, basis); expectBasisGatesOnly(funcOp, basis); + const auto parsed = parseEulerBasis(basis); + ASSERT_TRUE(parsed) << "basis=" << basis.str(); expectOneQubitGatesAroundBoundary( - funcOp, basis, [](Operation& op) { return isTwoQubitGate(op); }); + funcOp, basis, [](Operation& op) { return isTwoQubitGate(op); }, + expectedFusedGateCount(splitFixtureHtSegmentMatrix(), *parsed), + expectedFusedGateCount(splitFixtureRzSxSegmentMatrix(), *parsed)); }); } @@ -811,8 +877,72 @@ TEST(FuseSingleQubitUnitaryRunsTest, DoesNotFuseAcrossBarrierAllBases) { EXPECT_EQ(countOps(funcOp), 1U) << "basis=" << basis.str(); expectMatrixPreserved(funcOp, original, basis); expectBasisGatesOnly(funcOp, basis); + const auto parsed = parseEulerBasis(basis); + ASSERT_TRUE(parsed) << "basis=" << basis.str(); expectOneQubitGatesAroundBoundary( - funcOp, basis, [](Operation& op) { return isa(op); }); + funcOp, basis, [](Operation& op) { return isa(op); }, + expectedFusedGateCount(splitFixtureHtSegmentMatrix(), *parsed), + expectedFusedGateCount(splitFixtureRzSxSegmentMatrix(), *parsed)); + }); +} + +TEST(FuseSingleQubitUnitaryRunsTest, EliminatesIdentityInvMultiOpBody) { + SynthesisFixture fx; + fx.setUp(); + + runFuseOnProgram( + fx.context.get(), xInverseTwoX, "u", + [](func::FuncOp funcOp, const Matrix2x2&) { + EXPECT_EQ(countOps(funcOp), 4U); + EXPECT_EQ(countOps(funcOp), 1U); + EXPECT_EQ(countOps(funcOp), 0U); + }, + [](func::FuncOp funcOp, const Matrix2x2& original) { + EXPECT_EQ(countOps(funcOp), 0U); + EXPECT_EQ(countOps(funcOp), 0U); + EXPECT_EQ(countOps(funcOp), + expectedFusedGateCount(original, EulerBasis::U)); + expectMatrixPreserved(funcOp, original, "x-inv-xx-x"); + }); +} + +TEST(FuseSingleQubitUnitaryRunsTest, FusesSingleNonBasisGateInCtrlBody) { + SynthesisFixture fx; + fx.setUp(); + + runFuseOnProgram( + fx.context.get(), controlledH, "u", + [](func::FuncOp funcOp, const Matrix2x2&) { + EXPECT_EQ(countOps(funcOp), 1U); + EXPECT_EQ(countOpsInRegion(funcOp, isInsideCtrl), 1U); + EXPECT_EQ(countOpsInRegion(funcOp, isInsideCtrl), 0U); + }, + [](func::FuncOp funcOp, const Matrix2x2&) { + EXPECT_EQ(countOps(funcOp), 1U); + EXPECT_EQ(countOpsInRegion(funcOp, isInsideCtrl), 0U); + EXPECT_EQ(countOpsInRegion(funcOp, isInsideCtrl), 1U); + }); +} + +TEST(FuseSingleQubitUnitaryRunsTest, FusesRunInCtrlBody) { + SynthesisFixture fx; + fx.setUp(); + + runFuseOnProgram( + fx.context.get(), controlledInverseHt, "u", + [](func::FuncOp funcOp, const Matrix2x2&) { + EXPECT_EQ(countOps(funcOp), 1U); + EXPECT_EQ(countOpsInRegion(funcOp, isInsideCtrl), 1U); + EXPECT_EQ(countOpsInRegion(funcOp, isInsideInv), 1U); + EXPECT_EQ(countOpsInRegion(funcOp, isInsideInv), 1U); + EXPECT_EQ(countOpsInRegion(funcOp, isInsideCtrl), 0U); + }, + [](func::FuncOp funcOp, const Matrix2x2&) { + EXPECT_EQ(countOps(funcOp), 1U); + EXPECT_EQ(countOpsInRegion(funcOp, isInsideCtrl), 0U); + EXPECT_EQ(countOpsInRegion(funcOp, isInsideInv), 0U); + EXPECT_EQ(countOpsInRegion(funcOp, isInsideInv), 0U); + EXPECT_EQ(countOpsInRegion(funcOp, isInsideCtrl), 1U); }); } @@ -847,6 +977,11 @@ TEST(FuseSingleQubitUnitaryRunsTest, DoesNotFuseAcrossScfForAllBases) { EXPECT_EQ(countOps(funcOp), 1U) << "basis=" << basis.str(); expectMatrixPreserved(funcOp, original, basis); expectBasisGatesOnly(funcOp, basis); - expectOneQubitGatesInAndOutsideScfFor(funcOp, basis); + const auto parsed = parseEulerBasis(basis); + ASSERT_TRUE(parsed) << "basis=" << basis.str(); + expectOneQubitGatesInAndOutsideScfFor( + funcOp, basis, + expectedFusedGateCount(splitFixtureHtSegmentMatrix(), *parsed), + expectedFusedGateCount(splitFixtureRzSxSegmentMatrix(), *parsed)); }); } From 4cff97e6d88c490f938940b6d2f2d8ff3d20bf1c Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 11 Jun 2026 15:00:42 +0200 Subject: [PATCH 36/68] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20linter=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp | 7 ++--- .../test_euler_decomposition.cpp | 30 +++++++++---------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp b/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp index 30005e3a25..04c35cd73c 100644 --- a/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp @@ -29,6 +29,7 @@ #include #include +#include #include #include #include @@ -403,12 +404,10 @@ void InvOp::getCanonicalizationPatterns(RewritePatternSet& results, CancelNestedInv, EraseEmptyInv>(context); } -namespace { - /** * @brief Composes compile-time single-qubit unitaries in a modifier body. */ -[[nodiscard]] std::optional +[[nodiscard]] static std::optional composeSingleQubitBodyMatrix(Block& block) { Matrix2x2 acc = Matrix2x2::identity(); std::complex global{1.0, 0.0}; @@ -441,8 +440,6 @@ composeSingleQubitBodyMatrix(Block& block) { global * acc(1, 0), global * acc(1, 1)); } -} // namespace - std::optional InvOp::getUnitaryMatrix() { if (auto bodyUnitary = utils::getSoleBodyUnitary(*getBody())) { 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 3e718b1ff4..8777218d1b 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -273,17 +273,17 @@ static void expectBasisGatesOnly(func::FuncOp funcOp, StringRef basis) { /// Composed unitary of the `h; t` segment in `singleQubitRunsSplitByBarrier`, /// `singleQubitRunsSplitByTwoQGate`, and `singleQubitRunsSplitByScfFor`. -[[nodiscard]] static Matrix2x2 splitFixtureHtSegmentMatrix() { +[[nodiscard]] static Matrix2x2 splitFixtureHTSegmentMatrix() { return TOp::getUnitaryMatrix() * HOp::getUnitaryMatrix(); } /// Composed unitary of the `rz(0.321); sx` segment in the same split fixtures. -[[nodiscard]] static Matrix2x2 splitFixtureRzSxSegmentMatrix() { +[[nodiscard]] static Matrix2x2 splitFixtureRZSXSegmentMatrix() { return SXOp::getUnitaryMatrix() * rzMatrix(0.321); } /// Composed unitary of `overlongZSXXMixedPureZRun` (`sx; rz(pi); sx` → pure Z). -[[nodiscard]] static Matrix2x2 overlongZsxxPureZRunMatrix() { +[[nodiscard]] static Matrix2x2 overlongZSXXPureZRunMatrix() { return SXOp::getUnitaryMatrix() * rzMatrix(std::numbers::pi) * SXOp::getUnitaryMatrix(); } @@ -482,7 +482,7 @@ static void xInverseTwoX(QCOProgramBuilder& b) { q[0] = b.x(q[0]); } -static void controlledInverseHt(QCOProgramBuilder& b) { +static void controlledInverseHT(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.ctrl(q[0], q[1], [&](ValueRange targets) { auto wire = b.inv({targets[0]}, [&](ValueRange innerTargets) { @@ -666,13 +666,13 @@ INSTANTIATE_TEST_SUITE_P( return rzMatrix(0.3) * rzMatrix(0.7); }, 2, 2, 0, 0}, - ZSXXShortcutCase{"RyHalfPi", + ZSXXShortcutCase{"RYHalfPi", [](MLIRContext* ctx) -> Matrix2x2 { return rotationMatrix( ctx, std::numbers::pi / 2.0); }, 3, 2, 1, 0}, - ZSXXShortcutCase{"RyNearHalfPi", + ZSXXShortcutCase{"RYNearHalfPi", [](MLIRContext* ctx) -> Matrix2x2 { return rotationMatrix( ctx, @@ -822,7 +822,7 @@ TEST(FuseSingleQubitUnitaryRunsTest, runFuseOnProgram( fx.context.get(), &overlongZSXXMixedPureZRun, "zsxx", [](func::FuncOp funcOp, const Matrix2x2& /*original*/) { - EXPECT_EQ(expectedFusedGateCount(overlongZsxxPureZRunMatrix(), + EXPECT_EQ(expectedFusedGateCount(overlongZSXXPureZRunMatrix(), EulerBasis::ZSXX), 1U); ASSERT_EQ(countZSXXBasisGates(funcOp), 3U); @@ -835,7 +835,7 @@ TEST(FuseSingleQubitUnitaryRunsTest, EXPECT_EQ(countOps(funcOp), 0U); EXPECT_EQ(countOps(funcOp), 0U); EXPECT_EQ(countZSXXBasisGates(funcOp), - expectedFusedGateCount(overlongZsxxPureZRunMatrix(), + expectedFusedGateCount(overlongZSXXPureZRunMatrix(), EulerBasis::ZSXX)); expectMatrixPreserved(funcOp, original, "zsxx"); expectBasisGatesOnly(funcOp, "zsxx"); @@ -862,8 +862,8 @@ TEST(FuseSingleQubitUnitaryRunsTest, DoesNotFuseAcrossTwoQGateAllBases) { ASSERT_TRUE(parsed) << "basis=" << basis.str(); expectOneQubitGatesAroundBoundary( funcOp, basis, [](Operation& op) { return isTwoQubitGate(op); }, - expectedFusedGateCount(splitFixtureHtSegmentMatrix(), *parsed), - expectedFusedGateCount(splitFixtureRzSxSegmentMatrix(), *parsed)); + expectedFusedGateCount(splitFixtureHTSegmentMatrix(), *parsed), + expectedFusedGateCount(splitFixtureRZSXSegmentMatrix(), *parsed)); }); } @@ -881,8 +881,8 @@ TEST(FuseSingleQubitUnitaryRunsTest, DoesNotFuseAcrossBarrierAllBases) { ASSERT_TRUE(parsed) << "basis=" << basis.str(); expectOneQubitGatesAroundBoundary( funcOp, basis, [](Operation& op) { return isa(op); }, - expectedFusedGateCount(splitFixtureHtSegmentMatrix(), *parsed), - expectedFusedGateCount(splitFixtureRzSxSegmentMatrix(), *parsed)); + expectedFusedGateCount(splitFixtureHTSegmentMatrix(), *parsed), + expectedFusedGateCount(splitFixtureRZSXSegmentMatrix(), *parsed)); }); } @@ -929,7 +929,7 @@ TEST(FuseSingleQubitUnitaryRunsTest, FusesRunInCtrlBody) { fx.setUp(); runFuseOnProgram( - fx.context.get(), controlledInverseHt, "u", + fx.context.get(), controlledInverseHT, "u", [](func::FuncOp funcOp, const Matrix2x2&) { EXPECT_EQ(countOps(funcOp), 1U); EXPECT_EQ(countOpsInRegion(funcOp, isInsideCtrl), 1U); @@ -981,7 +981,7 @@ TEST(FuseSingleQubitUnitaryRunsTest, DoesNotFuseAcrossScfForAllBases) { ASSERT_TRUE(parsed) << "basis=" << basis.str(); expectOneQubitGatesInAndOutsideScfFor( funcOp, basis, - expectedFusedGateCount(splitFixtureHtSegmentMatrix(), *parsed), - expectedFusedGateCount(splitFixtureRzSxSegmentMatrix(), *parsed)); + expectedFusedGateCount(splitFixtureHTSegmentMatrix(), *parsed), + expectedFusedGateCount(splitFixtureRZSXSegmentMatrix(), *parsed)); }); } From fa5444e1662565f4498b61f26bd59f228a473134 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 11 Jun 2026 15:26:10 +0200 Subject: [PATCH 37/68] =?UTF-8?q?=E2=98=82=EF=B8=8F=20Increase=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp | 5 +- .../Dialect/QCO/IR/test_qco_ir_matrix.cpp | 98 +++++++++++++++++++ mlir/unittests/programs/qco_programs.cpp | 34 +++++++ mlir/unittests/programs/qco_programs.h | 16 +++ 4 files changed, 149 insertions(+), 4 deletions(-) diff --git a/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp b/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp index 04c35cd73c..82245a26c5 100644 --- a/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp @@ -423,11 +423,8 @@ composeSingleQubitBodyMatrix(Block& block) { continue; } auto unitary = dyn_cast(op); - if (!unitary || !unitary.isSingleQubit()) { - return std::nullopt; - } Matrix2x2 matrix; - if (!unitary.getUnitaryMatrix2x2(matrix)) { + if (!unitary || !unitary.getUnitaryMatrix2x2(matrix)) { return std::nullopt; } acc = matrix * acc; diff --git a/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp b/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp index 893ecff7c9..8221c1b1b4 100644 --- a/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp +++ b/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp @@ -129,6 +129,104 @@ TEST_F(QCOMatrixTest, InverseTwoXOpMatrix) { expected.assignFrom(Matrix2x2::identity()); ASSERT_TRUE(matrix->isApprox(expected)); } + +TEST_F(QCOMatrixTest, InverseXOpMatrix) { + auto moduleOp = QCOProgramBuilder::build(context.get(), inverseX); + ASSERT_TRUE(moduleOp); + + auto funcOp = *moduleOp->getBody()->getOps().begin(); + auto invOp = *funcOp.getBody().getOps().begin(); + const auto matrix = invOp.getUnitaryMatrix(); + ASSERT_TRUE(matrix); + + DynamicMatrix expected; + expected.assignFrom(XOp::getUnitaryMatrix()); + ASSERT_TRUE(matrix->isApprox(expected)); +} + +TEST_F(QCOMatrixTest, InverseSxOpMatrix) { + auto moduleOp = QCOProgramBuilder::build(context.get(), inverseSx); + ASSERT_TRUE(moduleOp); + + auto funcOp = *moduleOp->getBody()->getOps().begin(); + auto invOp = *funcOp.getBody().getOps().begin(); + const auto matrix = invOp.getUnitaryMatrix(); + ASSERT_TRUE(matrix); + + DynamicMatrix expected; + expected.assignFrom(SXdgOp::getUnitaryMatrix()); + ASSERT_TRUE(matrix->isApprox(expected)); +} + +TEST_F(QCOMatrixTest, InverseTwoXWithBarrierOpMatrix) { + auto moduleOp = + QCOProgramBuilder::build(context.get(), inverseTwoXWithBarrier); + ASSERT_TRUE(moduleOp); + + auto funcOp = *moduleOp->getBody()->getOps().begin(); + auto invOp = *funcOp.getBody().getOps().begin(); + const auto matrix = invOp.getUnitaryMatrix(); + ASSERT_TRUE(matrix); + + DynamicMatrix expected; + expected.assignFrom(Matrix2x2::identity()); + ASSERT_TRUE(matrix->isApprox(expected)); +} + +TEST_F(QCOMatrixTest, InverseGphaseXOpMatrix) { + auto moduleOp = QCOProgramBuilder::build(context.get(), inverseGphaseX); + ASSERT_TRUE(moduleOp); + + auto funcOp = *moduleOp->getBody()->getOps().begin(); + auto invOp = *funcOp.getBody().getOps().begin(); + const auto matrix = invOp.getUnitaryMatrix(); + ASSERT_TRUE(matrix); + + const auto composeGlobal = std::polar(1.0, -0.123); + Matrix2x2 body = XOp::getUnitaryMatrix(); + body(0, 0) *= composeGlobal; + body(0, 1) *= composeGlobal; + body(1, 0) *= composeGlobal; + body(1, 1) *= composeGlobal; + + DynamicMatrix expected; + expected.assignFrom(body.adjoint()); + ASSERT_TRUE(matrix->isApprox(expected)); +} + +TEST_F(QCOMatrixTest, InverseGphaseBarrierOpMatrix) { + auto moduleOp = QCOProgramBuilder::build(context.get(), inverseGphaseBarrier); + ASSERT_TRUE(moduleOp); + + auto funcOp = *moduleOp->getBody()->getOps().begin(); + auto invOp = *funcOp.getBody().getOps().begin(); + const auto matrix = invOp.getUnitaryMatrix(); + ASSERT_TRUE(matrix); + + const auto global = std::conj(std::polar(1.0, 0.123)); + DynamicMatrix expected; + expected.assignFrom(Matrix2x2::fromElements(global, 0, 0, global)); + ASSERT_TRUE(matrix->isApprox(expected)); +} + +TEST_F(QCOMatrixTest, InverseTwoBarriersInInvOpMatrix) { + auto moduleOp = + QCOProgramBuilder::build(context.get(), inverseTwoBarriersInInv); + ASSERT_TRUE(moduleOp); + + auto funcOp = *moduleOp->getBody()->getOps().begin(); + auto invOp = *funcOp.getBody().getOps().begin(); + EXPECT_FALSE(invOp.getUnitaryMatrix()); +} + +TEST_F(QCOMatrixTest, InvTwoOpMatrix) { + auto moduleOp = QCOProgramBuilder::build(context.get(), invTwo); + ASSERT_TRUE(moduleOp); + + auto funcOp = *moduleOp->getBody()->getOps().begin(); + auto invOp = *funcOp.getBody().getOps().begin(); + EXPECT_FALSE(invOp.getUnitaryMatrix()); +} /// @} /// \name QCO/Operations/StandardGates/DcxOp.cpp diff --git a/mlir/unittests/programs/qco_programs.cpp b/mlir/unittests/programs/qco_programs.cpp index 523f071f8a..457dc1ff27 100644 --- a/mlir/unittests/programs/qco_programs.cpp +++ b/mlir/unittests/programs/qco_programs.cpp @@ -319,6 +319,40 @@ void inverseTwoX(QCOProgramBuilder& b) { }); } +void inverseTwoXWithBarrier(QCOProgramBuilder& b) { + auto q = b.allocQubitRegister(1); + b.inv(q[0], [&](ValueRange qubits) { + auto q0 = b.x(qubits[0]); + q0 = b.barrier({q0})[0]; + q0 = b.x(q0); + return SmallVector{q0}; + }); +} + +void inverseGphaseX(QCOProgramBuilder& b) { + auto q = b.allocQubitRegister(1); + b.inv(q[0], [&](ValueRange qubits) { + b.gphase(-0.123); + return SmallVector{b.x(qubits[0])}; + }); +} + +void inverseGphaseBarrier(QCOProgramBuilder& b) { + auto q = b.allocQubitRegister(1); + b.inv(q[0], [&](ValueRange qubits) -> SmallVector { + b.gphase(0.123); + return {b.barrier({qubits[0]})[0]}; + }); +} + +void inverseTwoBarriersInInv(QCOProgramBuilder& b) { + auto q = b.allocQubitRegister(1); + b.inv(q[0], [&](ValueRange qubits) -> SmallVector { + auto q0 = b.barrier({qubits[0]})[0]; + return {b.barrier({q0})[0]}; + }); +} + void y(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.y(q[0]); diff --git a/mlir/unittests/programs/qco_programs.h b/mlir/unittests/programs/qco_programs.h index f562cfff8a..1c3677dee9 100644 --- a/mlir/unittests/programs/qco_programs.h +++ b/mlir/unittests/programs/qco_programs.h @@ -177,6 +177,22 @@ void controlledTwoX(QCOProgramBuilder& b); /// gates. void inverseTwoX(QCOProgramBuilder& b); +/// Creates a circuit with an inverse modifier applied to two X gates separated +/// by a barrier. +void inverseTwoXWithBarrier(QCOProgramBuilder& b); + +/// Creates a circuit with an inverse modifier applied to a global phase and an +/// X gate. +void inverseGphaseX(QCOProgramBuilder& b); + +/// Creates a circuit with an inverse modifier applied to a global phase and a +/// barrier. +void inverseGphaseBarrier(QCOProgramBuilder& b); + +/// Creates a circuit with an inverse modifier applied to two consecutive +/// barriers. +void inverseTwoBarriersInInv(QCOProgramBuilder& b); + // --- YOp ------------------------------------------------------------------ // /// Creates a circuit with just a Y gate. From 74002cd01f631bc58a3027d12ebb95d668f338f0 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 11 Jun 2026 16:10:32 +0200 Subject: [PATCH 38/68] =?UTF-8?q?=F0=9F=8E=A8=20Simplify=20code=20and=20re?= =?UTF-8?q?move=20testcase?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp | 12 ++---------- .../Dialect/QCO/IR/test_qco_ir_matrix.cpp | 15 --------------- mlir/unittests/programs/qco_programs.cpp | 10 ---------- mlir/unittests/programs/qco_programs.h | 4 ---- 4 files changed, 2 insertions(+), 39 deletions(-) diff --git a/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp b/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp index 82245a26c5..509df96b30 100644 --- a/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp @@ -422,11 +422,9 @@ composeSingleQubitBodyMatrix(Block& block) { } continue; } - auto unitary = dyn_cast(op); + auto unitary = cast(op); Matrix2x2 matrix; - if (!unitary || !unitary.getUnitaryMatrix2x2(matrix)) { - return std::nullopt; - } + unitary.getUnitaryMatrix2x2(matrix); acc = matrix * acc; found = true; } @@ -444,12 +442,6 @@ std::optional InvOp::getUnitaryMatrix() { bodyUnitary.getUnitaryMatrix()) { return targetMatrix->adjoint(); } - Matrix2x2 matrix; - if (bodyUnitary.getUnitaryMatrix2x2(matrix)) { - DynamicMatrix result; - result.assignFrom(matrix.adjoint()); - return result; - } return std::nullopt; } diff --git a/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp b/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp index 8221c1b1b4..cb88f51b92 100644 --- a/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp +++ b/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp @@ -158,21 +158,6 @@ TEST_F(QCOMatrixTest, InverseSxOpMatrix) { ASSERT_TRUE(matrix->isApprox(expected)); } -TEST_F(QCOMatrixTest, InverseTwoXWithBarrierOpMatrix) { - auto moduleOp = - QCOProgramBuilder::build(context.get(), inverseTwoXWithBarrier); - ASSERT_TRUE(moduleOp); - - auto funcOp = *moduleOp->getBody()->getOps().begin(); - auto invOp = *funcOp.getBody().getOps().begin(); - const auto matrix = invOp.getUnitaryMatrix(); - ASSERT_TRUE(matrix); - - DynamicMatrix expected; - expected.assignFrom(Matrix2x2::identity()); - ASSERT_TRUE(matrix->isApprox(expected)); -} - TEST_F(QCOMatrixTest, InverseGphaseXOpMatrix) { auto moduleOp = QCOProgramBuilder::build(context.get(), inverseGphaseX); ASSERT_TRUE(moduleOp); diff --git a/mlir/unittests/programs/qco_programs.cpp b/mlir/unittests/programs/qco_programs.cpp index 457dc1ff27..373e77797e 100644 --- a/mlir/unittests/programs/qco_programs.cpp +++ b/mlir/unittests/programs/qco_programs.cpp @@ -319,16 +319,6 @@ void inverseTwoX(QCOProgramBuilder& b) { }); } -void inverseTwoXWithBarrier(QCOProgramBuilder& b) { - auto q = b.allocQubitRegister(1); - b.inv(q[0], [&](ValueRange qubits) { - auto q0 = b.x(qubits[0]); - q0 = b.barrier({q0})[0]; - q0 = b.x(q0); - return SmallVector{q0}; - }); -} - void inverseGphaseX(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.inv(q[0], [&](ValueRange qubits) { diff --git a/mlir/unittests/programs/qco_programs.h b/mlir/unittests/programs/qco_programs.h index 1c3677dee9..a169b310c2 100644 --- a/mlir/unittests/programs/qco_programs.h +++ b/mlir/unittests/programs/qco_programs.h @@ -177,10 +177,6 @@ void controlledTwoX(QCOProgramBuilder& b); /// gates. void inverseTwoX(QCOProgramBuilder& b); -/// Creates a circuit with an inverse modifier applied to two X gates separated -/// by a barrier. -void inverseTwoXWithBarrier(QCOProgramBuilder& b); - /// Creates a circuit with an inverse modifier applied to a global phase and an /// X gate. void inverseGphaseX(QCOProgramBuilder& b); From a19abaa85b5357c56dfe0b43a57374af81861a34 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 11 Jun 2026 16:56:15 +0200 Subject: [PATCH 39/68] =?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/Decomposition/Euler.h | 18 +++++ mlir/include/mlir/Dialect/Utils/Utils.h | 7 +- mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp | 15 ++-- .../QCO/Transforms/Decomposition/Euler.cpp | 66 +++++++---------- .../Dialect/QCO/IR/test_qco_ir_matrix.cpp | 24 +++++++ .../test_euler_decomposition.cpp | 71 +++++++++++++++++-- 6 files changed, 148 insertions(+), 53 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h index d0fbec3845..bcdb9965a7 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h @@ -45,6 +45,24 @@ enum class EulerBasis : std::uint8_t { ZSXX = 5, ///< `RZ` / `SX` / `X` synthesis via ZYZ decomposition. }; +/** + * @brief Middle-gate case for ZSXX synthesis from ZYZ `theta`. + */ +enum class ZSXXMiddleGate : std::uint8_t { + OnlyRZ, + OneSX, + X, + SXRZSX, +}; + +/** + * @brief Classifies the ZSXX middle-gate case from ZYZ `theta`. + * + * @param theta Y-rotation angle from `paramsZYZ` in `[0, pi]`. + * @return The ZSXX middle-gate case. + */ +[[nodiscard]] ZSXXMiddleGate classifyZSXXMiddleFromZYZTheta(double theta); + /** * @brief Extracts Euler parameters from single-qubit unitary matrices. */ diff --git a/mlir/include/mlir/Dialect/Utils/Utils.h b/mlir/include/mlir/Dialect/Utils/Utils.h index a5848f6c80..4d129f69fc 100644 --- a/mlir/include/mlir/Dialect/Utils/Utils.h +++ b/mlir/include/mlir/Dialect/Utils/Utils.h @@ -19,7 +19,6 @@ #include #include -#include #include #include #include @@ -200,8 +199,10 @@ inline void printTargetAliasing(OpAsmPrinter& printer, Region& region, */ inline Value getValueFromBlockArgument(Value qubit, ValueRange qubits) { if (auto blockArg = dyn_cast(qubit)) { - assert(blockArg.getArgNumber() < qubits.size() && - "block argument index must be within qubits range"); + if (blockArg.getArgNumber() >= qubits.size()) { + llvm::reportFatalUsageError( + "block argument index must be within qubits range"); + } return qubits[blockArg.getArgNumber()]; } return qubit; diff --git a/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp b/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp index 509df96b30..a83432abd6 100644 --- a/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp @@ -417,14 +417,21 @@ composeSingleQubitBodyMatrix(Block& block) { continue; } if (auto gphase = dyn_cast(op)) { - if (auto matrix = gphase.getUnitaryMatrix()) { - global *= (*matrix)(0, 0); + const auto matrix = gphase.getUnitaryMatrix(); + if (!matrix) { + return std::nullopt; } + global *= (*matrix)(0, 0); continue; } - auto unitary = cast(op); + auto unitary = dyn_cast(op); + if (!unitary) { + return std::nullopt; + } Matrix2x2 matrix; - unitary.getUnitaryMatrix2x2(matrix); + if (!unitary.getUnitaryMatrix2x2(matrix)) { + return std::nullopt; + } acc = matrix * acc; found = true; } diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index 7c6e24e53b..598cc7d9e2 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -107,8 +107,7 @@ namespace { * @brief Planned ZSXX (`RZ` / `SX` / `X`) chain; angles in circuit order. */ struct ZSXXSequence { - enum class Middle : std::uint8_t { OnlyRZ, OneSX, X, SXRZSX }; - Middle middle = Middle::SXRZSX; + ZSXXMiddleGate middle = ZSXXMiddleGate::SXRZSX; double firstRZ = 0.0; double midRZ = 0.0; double lastRZ = 0.0; @@ -116,32 +115,21 @@ struct ZSXXSequence { } // namespace -/** - * @brief Classifies the ZSXX middle-gate case from ZYZ `theta`. - * - * Shortcut branches are checked in fixed order (`OnlyRZ`, `OneSX`, `X`) so - * `theta` values within `TOLERANCE` of `0`, `pi/2`, or `pi` always pick the - * same case. - * - * @param theta Y-rotation angle from `paramsZYZ` in `[0, pi]`. - * @return The ZSXX middle-gate case. - */ -[[nodiscard]] static ZSXXSequence::Middle -classifyZSXXMiddleFromZYZTheta(double theta) { +ZSXXMiddleGate classifyZSXXMiddleFromZYZTheta(double theta) { constexpr double eps = mlir::utils::TOLERANCE; constexpr double halfPi = std::numbers::pi / 2.0; constexpr double pi = std::numbers::pi; - if (theta < eps) { - return ZSXXSequence::Middle::OnlyRZ; + if (std::abs(theta) <= eps) { + return ZSXXMiddleGate::OnlyRZ; } if (std::abs(theta - halfPi) <= eps) { - return ZSXXSequence::Middle::OneSX; + return ZSXXMiddleGate::OneSX; } if (std::abs(theta - pi) <= eps) { - return ZSXXSequence::Middle::X; + return ZSXXMiddleGate::X; } - return ZSXXSequence::Middle::SXRZSX; + return ZSXXMiddleGate::SXRZSX; } /** @@ -160,23 +148,23 @@ sequenceFromZYZForZSXX(double theta, double phi, double lambda) { constexpr double pi = std::numbers::pi; switch (classifyZSXXMiddleFromZYZTheta(theta)) { - case ZSXXSequence::Middle::OnlyRZ: - return {.middle = ZSXXSequence::Middle::OnlyRZ, + case ZSXXMiddleGate::OnlyRZ: + return {.middle = ZSXXMiddleGate::OnlyRZ, .firstRZ = lambda, .midRZ = 0.0, .lastRZ = phi}; - case ZSXXSequence::Middle::OneSX: - return {.middle = ZSXXSequence::Middle::OneSX, + case ZSXXMiddleGate::OneSX: + return {.middle = ZSXXMiddleGate::OneSX, .firstRZ = lambda - halfPi, .midRZ = 0.0, .lastRZ = phi + halfPi}; - case ZSXXSequence::Middle::X: - return {.middle = ZSXXSequence::Middle::X, + case ZSXXMiddleGate::X: + return {.middle = ZSXXMiddleGate::X, .firstRZ = lambda, .midRZ = 0.0, .lastRZ = phi + pi}; - case ZSXXSequence::Middle::SXRZSX: - return {.middle = ZSXXSequence::Middle::SXRZSX, + case ZSXXMiddleGate::SXRZSX: + return {.middle = ZSXXMiddleGate::SXRZSX, .firstRZ = lambda, .midRZ = theta + pi, .lastRZ = phi + pi}; @@ -220,19 +208,19 @@ sequenceFromZYZForZSXX(double theta, double phi, double lambda) { constexpr double quarterPi = std::numbers::pi / 4.0; switch (seq.middle) { - case ZSXXSequence::Middle::OnlyRZ: + case ZSXXMiddleGate::OnlyRZ: return globalPhaseFromRZWrap(seq.firstRZ) + globalPhaseFromRZWrap(seq.lastRZ); - case ZSXXSequence::Middle::OneSX: + case ZSXXMiddleGate::OneSX: // `SX = exp(i*pi/4)*RZ(-pi/2)*RY(pi/2)*RZ(pi/2)`; the outer RZ angles // absorb the +-pi/2, leaving the exp(i*pi/4) phase. RZ wraps add too. return -quarterPi + globalPhaseFromRZWrap(seq.firstRZ) + globalPhaseFromRZWrap(seq.lastRZ); - case ZSXXSequence::Middle::X: + case ZSXXMiddleGate::X: // `X` swaps the diagonal, so the wraps enter with opposite signs. return -halfPi + globalPhaseFromRZWrap(seq.lastRZ) - globalPhaseFromRZWrap(seq.firstRZ); - case ZSXXSequence::Middle::SXRZSX: + case ZSXXMiddleGate::SXRZSX: // `SX*RZ(theta+pi)*SX = Z*RY(theta)`; all three RZ wraps add. return halfPi + globalPhaseFromRZWrap(seq.firstRZ) + globalPhaseFromRZWrap(seq.midRZ) + globalPhaseFromRZWrap(seq.lastRZ); @@ -254,18 +242,18 @@ static void visitSequenceInTimeOrder(const ZSXXSequence& seq, llvm::function_ref onX) { onRZ(seq.firstRZ); switch (seq.middle) { - case ZSXXSequence::Middle::OnlyRZ: + case ZSXXMiddleGate::OnlyRZ: onRZ(seq.lastRZ); break; - case ZSXXSequence::Middle::OneSX: + case ZSXXMiddleGate::OneSX: onSX(); onRZ(seq.lastRZ); break; - case ZSXXSequence::Middle::X: + case ZSXXMiddleGate::X: onX(); onRZ(seq.lastRZ); break; - case ZSXXSequence::Middle::SXRZSX: + case ZSXXMiddleGate::SXRZSX: onSX(); onRZ(seq.midRZ); onSX(); @@ -536,14 +524,14 @@ Value synthesizeUnitary1QEuler(OpBuilder& builder, Location loc, Value qubit, [[nodiscard]] static std::size_t countZSXXSequenceGates(const ZSXXSequence& seq) { switch (seq.middle) { - case ZSXXSequence::Middle::OnlyRZ: + case ZSXXMiddleGate::OnlyRZ: return countNonZeroZSXXAngle(seq.firstRZ) + countNonZeroZSXXAngle(seq.lastRZ); - case ZSXXSequence::Middle::OneSX: - case ZSXXSequence::Middle::X: + case ZSXXMiddleGate::OneSX: + case ZSXXMiddleGate::X: return countNonZeroZSXXAngle(seq.firstRZ) + 1 + countNonZeroZSXXAngle(seq.lastRZ); - case ZSXXSequence::Middle::SXRZSX: + case ZSXXMiddleGate::SXRZSX: return countNonZeroZSXXAngle(seq.firstRZ) + 1 + countNonZeroZSXXAngle(seq.midRZ) + 1 + countNonZeroZSXXAngle(seq.lastRZ); diff --git a/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp b/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp index cb88f51b92..e772b945ab 100644 --- a/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp +++ b/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -212,6 +213,29 @@ TEST_F(QCOMatrixTest, InvTwoOpMatrix) { auto invOp = *funcOp.getBody().getOps().begin(); EXPECT_FALSE(invOp.getUnitaryMatrix()); } + +TEST_F(QCOMatrixTest, InverseDynamicRzXOpMatrix) { + constexpr auto mlirCode = R"( + module { + func.func @test(%theta: f64) -> !qco.qubit { + %q_in = qco.alloc : !qco.qubit + %q_out = qco.inv (%q = %q_in) { + %q_1 = qco.rz(%theta) %q : !qco.qubit -> !qco.qubit + %q_2 = qco.x %q_1 : !qco.qubit -> !qco.qubit + qco.yield %q_2 : !qco.qubit + } : {!qco.qubit} -> {!qco.qubit} + return %q_out : !qco.qubit + } + } + )"; + + auto moduleOp = parseSourceString(mlirCode, context.get()); + ASSERT_TRUE(moduleOp); + + auto funcOp = *moduleOp->getBody()->getOps().begin(); + auto invOp = *funcOp.getBody().getOps().begin(); + EXPECT_FALSE(invOp.getUnitaryMatrix()); +} /// @} /// \name QCO/Operations/StandardGates/DcxOp.cpp 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 8777218d1b..ee04683397 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -168,12 +168,13 @@ template static void forEachBasis(Fn fn) { } } -static Matrix2x2 compute1QMatrixFromFunction(func::FuncOp funcOp) { +template +static Matrix2x2 compute1QUnitaryMatrix(WalkRange& range) { Matrix2x2 acc = Matrix2x2::identity(); std::complex global{1.0, 0.0}; bool failed = false; - funcOp.walk([&](Operation* op) -> WalkResult { + range.template walk([&](Operation* op) -> WalkResult { if (isa(*op)) { return WalkResult::advance(); } @@ -233,6 +234,19 @@ static Matrix2x2 compute1QMatrixFromFunction(func::FuncOp funcOp) { global * acc(1, 0), global * acc(1, 1)); } +static Matrix2x2 compute1QMatrixFromFunction(func::FuncOp funcOp) { + return compute1QUnitaryMatrix(funcOp); +} + +[[nodiscard]] static Matrix2x2 +compute1QMatrixFromCtrlBody(func::FuncOp funcOp) { + for (CtrlOp ctrl : funcOp.getOps()) { + return compute1QUnitaryMatrix(ctrl.getRegion()); + } + ADD_FAILURE() << "Expected CtrlOp in function"; + return Matrix2x2::fromElements(0, 0, 0, 0); +} + static void expectMatrixPreserved(func::FuncOp funcOp, const Matrix2x2& original, StringRef label) { EXPECT_TRUE(compute1QMatrixFromFunction(funcOp).isApprox( @@ -240,6 +254,12 @@ static void expectMatrixPreserved(func::FuncOp funcOp, << label.str(); } +static void expectCtrlBodyMatrixPreserved(func::FuncOp funcOp, + const Matrix2x2& original) { + EXPECT_TRUE(compute1QMatrixFromCtrlBody(funcOp).isApprox( + original, mlir::utils::TOLERANCE)); +} + static void expectBasisGatesOnly(func::FuncOp funcOp, StringRef basis) { funcOp.walk([&](Operation* op) -> WalkResult { if (isa(*op)) { @@ -634,6 +654,23 @@ TEST(EulerSynthesisTest, ProfitabilityAndIdentityGateCount) { EXPECT_EQ(synthesisGateCount(identity, EulerBasis::ZYZ), 0U); } +TEST(EulerSynthesisTest, ClassifyZSXXMiddleFromZYZThetaBoundaries) { + using decomposition::classifyZSXXMiddleFromZYZTheta; + using decomposition::ZSXXMiddleGate; + + constexpr double halfPi = std::numbers::pi / 2.0; + constexpr double pi = std::numbers::pi; + constexpr double tol = 0.5 * mlir::utils::TOLERANCE; + + EXPECT_EQ(classifyZSXXMiddleFromZYZTheta(tol), ZSXXMiddleGate::OnlyRZ); + EXPECT_EQ(classifyZSXXMiddleFromZYZTheta(mlir::utils::TOLERANCE), + ZSXXMiddleGate::OnlyRZ); + EXPECT_EQ(classifyZSXXMiddleFromZYZTheta(halfPi + tol), + ZSXXMiddleGate::OneSX); + EXPECT_EQ(classifyZSXXMiddleFromZYZTheta(pi - tol), ZSXXMiddleGate::X); + EXPECT_EQ(classifyZSXXMiddleFromZYZTheta(pi), ZSXXMiddleGate::X); +} + TEST_P(ZSXXShortcutTest, SynthesisMatchesGateCount) { SynthesisFixture fx; fx.setUp(); @@ -679,7 +716,21 @@ INSTANTIATE_TEST_SUITE_P( (std::numbers::pi / 2.0) + (0.5 * mlir::utils::TOLERANCE)); }, - 3, 2, 1, 0}), + 3, 2, 1, 0}, + ZSXXShortcutCase{"RYNearZero", + [](MLIRContext* ctx) -> Matrix2x2 { + return rotationMatrix( + ctx, 0.5 * mlir::utils::TOLERANCE); + }, + 0, 0, 0, 0}, + ZSXXShortcutCase{"RYNearPi", + [](MLIRContext* ctx) -> Matrix2x2 { + return rotationMatrix( + ctx, + std::numbers::pi - + (0.5 * mlir::utils::TOLERANCE)); + }, + 2, 1, 0, 1}), [](const testing::TestParamInfo& info) { return std::string(info.param.label); }); @@ -910,17 +961,20 @@ TEST(FuseSingleQubitUnitaryRunsTest, FusesSingleNonBasisGateInCtrlBody) { SynthesisFixture fx; fx.setUp(); + Matrix2x2 ctrlBodyBefore; runFuseOnProgram( fx.context.get(), controlledH, "u", - [](func::FuncOp funcOp, const Matrix2x2&) { + [&](func::FuncOp funcOp, const Matrix2x2&) { EXPECT_EQ(countOps(funcOp), 1U); EXPECT_EQ(countOpsInRegion(funcOp, isInsideCtrl), 1U); EXPECT_EQ(countOpsInRegion(funcOp, isInsideCtrl), 0U); + ctrlBodyBefore = compute1QMatrixFromCtrlBody(funcOp); }, - [](func::FuncOp funcOp, const Matrix2x2&) { + [&](func::FuncOp funcOp, const Matrix2x2&) { EXPECT_EQ(countOps(funcOp), 1U); EXPECT_EQ(countOpsInRegion(funcOp, isInsideCtrl), 0U); EXPECT_EQ(countOpsInRegion(funcOp, isInsideCtrl), 1U); + expectCtrlBodyMatrixPreserved(funcOp, ctrlBodyBefore); }); } @@ -928,21 +982,24 @@ TEST(FuseSingleQubitUnitaryRunsTest, FusesRunInCtrlBody) { SynthesisFixture fx; fx.setUp(); + Matrix2x2 ctrlBodyBefore; runFuseOnProgram( fx.context.get(), controlledInverseHT, "u", - [](func::FuncOp funcOp, const Matrix2x2&) { + [&](func::FuncOp funcOp, const Matrix2x2&) { EXPECT_EQ(countOps(funcOp), 1U); EXPECT_EQ(countOpsInRegion(funcOp, isInsideCtrl), 1U); EXPECT_EQ(countOpsInRegion(funcOp, isInsideInv), 1U); EXPECT_EQ(countOpsInRegion(funcOp, isInsideInv), 1U); EXPECT_EQ(countOpsInRegion(funcOp, isInsideCtrl), 0U); + ctrlBodyBefore = compute1QMatrixFromCtrlBody(funcOp); }, - [](func::FuncOp funcOp, const Matrix2x2&) { + [&](func::FuncOp funcOp, const Matrix2x2&) { EXPECT_EQ(countOps(funcOp), 1U); EXPECT_EQ(countOpsInRegion(funcOp, isInsideCtrl), 0U); EXPECT_EQ(countOpsInRegion(funcOp, isInsideInv), 0U); EXPECT_EQ(countOpsInRegion(funcOp, isInsideInv), 0U); EXPECT_EQ(countOpsInRegion(funcOp, isInsideCtrl), 1U); + expectCtrlBodyMatrixPreserved(funcOp, ctrlBodyBefore); }); } From 9e2502d29cb1662e2977d51d81d4fb2e724e7770 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Thu, 11 Jun 2026 17:04:55 +0200 Subject: [PATCH 40/68] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20linter=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index 598cc7d9e2..e181ba1f90 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -24,7 +24,6 @@ #include #include #include -#include #include #include From a0a9a832a08ded68e6743551e827c4795919be97 Mon Sep 17 00:00:00 2001 From: burgholzer Date: Fri, 12 Jun 2026 21:30:33 +0200 Subject: [PATCH 41/68] :rotating_light: Please local clang-tidy Signed-off-by: burgholzer --- .../mlir/Dialect/QCO/Transforms/Decomposition/Euler.h | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h index bcdb9965a7..fd1b4629db 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h @@ -172,10 +172,9 @@ class EulerDecomposition { * @param basis The target Euler basis. * @return `1` for `U`, `3` for KAK bases, `5` for `ZSXX`. */ -[[nodiscard]] constexpr std::size_t maxSynthesisGateCount(EulerBasis basis) { +[[nodiscard]] constexpr std::size_t +maxSynthesisGateCount(const EulerBasis basis) { switch (basis) { - case EulerBasis::U: - return 1; case EulerBasis::ZYZ: case EulerBasis::ZXZ: case EulerBasis::XZX: @@ -183,6 +182,9 @@ class EulerDecomposition { return 3; case EulerBasis::ZSXX: return 5; + case EulerBasis::U: + default: + return 1; } } From fa850557c2d131885daa88d8d2a291072c583108 Mon Sep 17 00:00:00 2001 From: burgholzer Date: Fri, 12 Jun 2026 21:34:57 +0200 Subject: [PATCH 42/68] :art: Drop redundant namespace qualifiers Signed-off-by: burgholzer --- .../QCO/Transforms/Decomposition/Euler.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index e181ba1f90..2ddbabf62b 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -37,8 +37,8 @@ namespace mlir::qco::decomposition { * @param atol Tolerance for snapping `+pi` to `-pi`. * @return The wrapped angle in `[-pi, pi)`. */ -[[nodiscard]] static double mod2pi(double angle, - double atol = mlir::utils::TOLERANCE) { +[[nodiscard]] static double mod2pi(const double angle, + const double atol = utils::TOLERANCE) { if (!std::isfinite(angle)) { return angle; } @@ -84,7 +84,7 @@ namespace mlir::qco::decomposition { * @param phase Global phase in radians. */ static void emitGPhaseIfNeeded(OpBuilder& builder, Location loc, double phase) { - if (std::abs(phase) <= mlir::utils::TOLERANCE) { + if (std::abs(phase) <= utils::TOLERANCE) { return; } GPhaseOp::create(builder, loc, phase); @@ -97,7 +97,7 @@ static void emitGPhaseIfNeeded(OpBuilder& builder, Location loc, double phase) { * @return `true` when no rotation gate should be emitted. */ [[nodiscard]] static bool isNearZeroRotationAngle(double angle) { - return std::abs(angle) <= mlir::utils::TOLERANCE; + return std::abs(angle) <= utils::TOLERANCE; } namespace { @@ -115,7 +115,7 @@ struct ZSXXSequence { } // namespace ZSXXMiddleGate classifyZSXXMiddleFromZYZTheta(double theta) { - constexpr double eps = mlir::utils::TOLERANCE; + constexpr double eps = utils::TOLERANCE; constexpr double halfPi = std::numbers::pi / 2.0; constexpr double pi = std::numbers::pi; @@ -192,7 +192,7 @@ sequenceFromZYZForZSXX(double theta, double phi, double lambda) { * @return The global-phase contribution in radians. */ [[nodiscard]] static double globalPhaseFromRZWrap(double angle) { - constexpr double eps = mlir::utils::TOLERANCE; + constexpr double eps = utils::TOLERANCE; return 0.5 * (mod2pi(angle, eps) - angle); } @@ -275,7 +275,7 @@ static void visitSequenceInTimeOrder(const ZSXXSequence& seq, Location loc, Value qubit, const ZSXXSequence& seq, double phase) { - constexpr double eps = mlir::utils::TOLERANCE; + constexpr double eps = utils::TOLERANCE; visitSequenceInTimeOrder( seq, [&](const double angle) { @@ -383,7 +383,7 @@ EulerAngles EulerDecomposition::paramsZYZ(const Matrix2x2& matrix) { const auto theta = 2. * std::atan2(std::abs(matrix(1, 0)), std::abs(matrix(0, 0))); const auto ang1 = std::arg(matrix(1, 1)); - constexpr double eps = mlir::utils::TOLERANCE; + constexpr double eps = utils::TOLERANCE; double ang2 = 0.0; if (std::abs(matrix(1, 0)) > eps) { ang2 = std::arg(matrix(1, 0)); @@ -513,7 +513,7 @@ Value synthesizeUnitary1QEuler(OpBuilder& builder, Location loc, Value qubit, * @brief Counts non-zero `RZ` slots in a ZSXX sequence angle. */ [[nodiscard]] static std::size_t countNonZeroZSXXAngle(double angle) { - constexpr double eps = mlir::utils::TOLERANCE; + constexpr double eps = utils::TOLERANCE; return isNearZeroRotationAngle(mod2pi(angle, eps)) ? 0 : 1; } From 98886a5921349bf80edf5707c860105bdd837164 Mon Sep 17 00:00:00 2001 From: burgholzer Date: Fri, 12 Jun 2026 21:36:25 +0200 Subject: [PATCH 43/68] :art: Consistently use `isNearZeroRotationAngle` Signed-off-by: burgholzer --- .../QCO/Transforms/Decomposition/Euler.cpp | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index 2ddbabf62b..d0b732a2c5 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -76,6 +76,16 @@ namespace mlir::qco::decomposition { 0.5 * (a + b - c - d), 0.5 * (a - b - c + d)); } +/** + * @brief Whether `angle` is numerically zero for gate-emission purposes. + * + * @param angle Rotation angle in radians. + * @return `true` when no rotation gate should be emitted. + */ +[[nodiscard]] static bool isNearZeroRotationAngle(double angle) { + return std::abs(angle) <= utils::TOLERANCE; +} + /** * @brief Emits `qco.gphase` when `phase` is outside tolerance. * @@ -84,22 +94,12 @@ namespace mlir::qco::decomposition { * @param phase Global phase in radians. */ static void emitGPhaseIfNeeded(OpBuilder& builder, Location loc, double phase) { - if (std::abs(phase) <= utils::TOLERANCE) { + if (isNearZeroRotationAngle(phase)) { return; } GPhaseOp::create(builder, loc, phase); } -/** - * @brief Whether `angle` is numerically zero for gate-emission purposes. - * - * @param angle Rotation angle in radians. - * @return `true` when no rotation gate should be emitted. - */ -[[nodiscard]] static bool isNearZeroRotationAngle(double angle) { - return std::abs(angle) <= utils::TOLERANCE; -} - namespace { /** @@ -114,18 +114,17 @@ struct ZSXXSequence { } // namespace -ZSXXMiddleGate classifyZSXXMiddleFromZYZTheta(double theta) { - constexpr double eps = utils::TOLERANCE; +ZSXXMiddleGate classifyZSXXMiddleFromZYZTheta(const double theta) { constexpr double halfPi = std::numbers::pi / 2.0; constexpr double pi = std::numbers::pi; - if (std::abs(theta) <= eps) { + if (isNearZeroRotationAngle(theta)) { return ZSXXMiddleGate::OnlyRZ; } - if (std::abs(theta - halfPi) <= eps) { + if (isNearZeroRotationAngle(theta - halfPi)) { return ZSXXMiddleGate::OneSX; } - if (std::abs(theta - pi) <= eps) { + if (isNearZeroRotationAngle(theta - pi)) { return ZSXXMiddleGate::X; } return ZSXXMiddleGate::SXRZSX; From 93ef9eb96df77775c29bd223adb944c61de70e6b Mon Sep 17 00:00:00 2001 From: burgholzer Date: Fri, 12 Jun 2026 22:11:59 +0200 Subject: [PATCH 44/68] :art: Code quality improvements and cleanup Signed-off-by: burgholzer --- .../QCO/Transforms/Decomposition/Euler.cpp | 97 +++++++++---------- .../FuseSingleQubitUnitaryRuns.cpp | 4 +- 2 files changed, 50 insertions(+), 51 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index d0b732a2c5..2290126cfe 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -140,8 +140,9 @@ ZSXXMiddleGate classifyZSXXMiddleFromZYZTheta(const double theta) { * @param lambda Leading Z-rotation angle. * @return The planned ZSXX sequence. */ -[[nodiscard]] static ZSXXSequence -sequenceFromZYZForZSXX(double theta, double phi, double lambda) { +[[nodiscard]] static ZSXXSequence sequenceFromZYZForZSXX(const double theta, + const double phi, + const double lambda) { constexpr double halfPi = std::numbers::pi / 2.0; constexpr double pi = std::numbers::pi; @@ -177,7 +178,8 @@ sequenceFromZYZForZSXX(double theta, double phi, double lambda) { * @param lambda Leading Z-rotation angle. * @return The global-phase offset in radians. */ -[[nodiscard]] static double globalPhaseOffsetForU(double phi, double lambda) { +[[nodiscard]] static double globalPhaseOffsetForU(const double phi, + const double lambda) { return -0.5 * (phi + lambda); } @@ -190,9 +192,8 @@ sequenceFromZYZForZSXX(double theta, double phi, double lambda) { * @param angle The unwrapped RZ angle. * @return The global-phase contribution in radians. */ -[[nodiscard]] static double globalPhaseFromRZWrap(double angle) { - constexpr double eps = utils::TOLERANCE; - return 0.5 * (mod2pi(angle, eps) - angle); +[[nodiscard]] static double globalPhaseFromRZWrap(const double angle) { + return 0.5 * (mod2pi(angle) - angle); } /** @@ -273,12 +274,11 @@ static void visitSequenceInTimeOrder(const ZSXXSequence& seq, [[nodiscard]] static Value emitFromZSXXSequence(OpBuilder& builder, Location loc, Value qubit, const ZSXXSequence& seq, - double phase) { - constexpr double eps = utils::TOLERANCE; + const double phase) { visitSequenceInTimeOrder( seq, [&](const double angle) { - const double wrapped = mod2pi(angle, eps); + const double wrapped = mod2pi(angle); if (isNearZeroRotationAngle(wrapped)) { return; } @@ -304,8 +304,8 @@ static void visitSequenceInTimeOrder(const ZSXXSequence& seq, * @return The output qubit value. */ static Value emitKAK(OpBuilder& builder, Location loc, Value qubit, - double theta, double phi, double lambda, double phase, - EulerBasis basis) { + const double theta, const double phi, const double lambda, + const double phase, const EulerBasis basis) { auto emitK = [&](double a) { if (isNearZeroRotationAngle(a)) { return; @@ -356,7 +356,7 @@ static Value emitKAK(OpBuilder& builder, Location loc, Value qubit, //===----------------------------------------------------------------------===// EulerAngles EulerDecomposition::anglesFromUnitary(const Matrix2x2& matrix, - EulerBasis basis) { + const EulerBasis basis) { switch (basis) { case EulerBasis::ZYZ: return paramsZYZ(matrix); @@ -368,9 +368,10 @@ EulerAngles EulerDecomposition::anglesFromUnitary(const Matrix2x2& matrix, return paramsXYX(matrix); case EulerBasis::U: return paramsU(matrix); + default: + llvm::reportFatalInternalError( + "Unsupported Euler basis for angle computation in decomposition!"); } - llvm::reportFatalInternalError( - "Unsupported Euler basis for angle computation in decomposition!"); } EulerAngles EulerDecomposition::paramsZYZ(const Matrix2x2& matrix) { @@ -382,11 +383,10 @@ EulerAngles EulerDecomposition::paramsZYZ(const Matrix2x2& matrix) { const auto theta = 2. * std::atan2(std::abs(matrix(1, 0)), std::abs(matrix(0, 0))); const auto ang1 = std::arg(matrix(1, 1)); - constexpr double eps = utils::TOLERANCE; double ang2 = 0.0; - if (std::abs(matrix(1, 0)) > eps) { + if (std::abs(matrix(1, 0)) > utils::TOLERANCE) { ang2 = std::arg(matrix(1, 0)); - } else if (std::abs(matrix(0, 1)) > eps) { + } else if (std::abs(matrix(0, 1)) > utils::TOLERANCE) { ang2 = std::arg(matrix(0, 1)); } const auto phi = ang1 + ang2 - detArg; @@ -396,11 +396,11 @@ EulerAngles EulerDecomposition::paramsZYZ(const Matrix2x2& matrix) { EulerAngles EulerDecomposition::paramsZXZ(const Matrix2x2& matrix) { // ZXZ from ZYZ via RY(theta) = RZ(pi/2)*RX(theta)*RZ(-pi/2). - const auto zyz = paramsZYZ(matrix); - return {.theta = zyz.theta, - .phi = zyz.phi + (std::numbers::pi / 2.0), - .lambda = zyz.lambda - (std::numbers::pi / 2.0), - .phase = zyz.phase}; + const auto [theta, phi, lambda, phase] = paramsZYZ(matrix); + return {.theta = theta, + .phi = phi + (std::numbers::pi / 2.0), + .lambda = lambda - (std::numbers::pi / 2.0), + .phase = phase}; } EulerAngles EulerDecomposition::paramsXZX(const Matrix2x2& matrix) { @@ -410,24 +410,23 @@ EulerAngles EulerDecomposition::paramsXZX(const Matrix2x2& matrix) { EulerAngles EulerDecomposition::paramsXYX(const Matrix2x2& matrix) { // H*RY(theta)*H = RY(-theta): shift outer angles by pi and fix global phase. - const auto zyz = paramsZYZ(hadamardConjugate(matrix)); + const auto [theta, phi, lambda, phase] = paramsZYZ(hadamardConjugate(matrix)); // Keep atol=0 so `phase` tracks the unwrapped ZYZ angles; snapping to pi // would change the recorded global-phase correction. - const auto newPhi = mod2pi(zyz.phi + std::numbers::pi, 0.); - const auto newLambda = mod2pi(zyz.lambda + std::numbers::pi, 0.); - return {.theta = zyz.theta, + const auto newPhi = mod2pi(phi + std::numbers::pi, 0.); + const auto newLambda = mod2pi(lambda + std::numbers::pi, 0.); + return {.theta = theta, .phi = newPhi, .lambda = newLambda, - .phase = - zyz.phase + ((newPhi + newLambda - zyz.phi - zyz.lambda) / 2.)}; + .phase = phase + ((newPhi + newLambda - phi - lambda) / 2.)}; } EulerAngles EulerDecomposition::paramsU(const Matrix2x2& matrix) { - const auto zyz = paramsZYZ(matrix); - return {.theta = zyz.theta, - .phi = zyz.phi, - .lambda = zyz.lambda, - .phase = zyz.phase + globalPhaseOffsetForU(zyz.phi, zyz.lambda)}; + const auto [theta, phi, lambda, phase] = paramsZYZ(matrix); + return {.theta = theta, + .phi = phi, + .lambda = lambda, + .phase = phase + globalPhaseOffsetForU(phi, lambda)}; } //===----------------------------------------------------------------------===// @@ -458,15 +457,16 @@ std::optional parseEulerBasis(StringRef basis) { Value synthesizeUnitary1QEuler(OpBuilder& builder, Location loc, Value qubit, const Matrix2x2& targetMatrix, - EulerBasis basis) { + const EulerBasis basis) { if (basis == EulerBasis::ZSXX) { - const auto zyz = EulerDecomposition::paramsZYZ(targetMatrix); - const auto seq = sequenceFromZYZForZSXX(zyz.theta, zyz.phi, zyz.lambda); + const auto [theta, phi, lambda, phase] = + EulerDecomposition::paramsZYZ(targetMatrix); + const auto seq = sequenceFromZYZForZSXX(theta, phi, lambda); return emitFromZSXXSequence(builder, loc, qubit, seq, - zyz.phase + globalPhaseOffsetForZSXX(seq)); + phase + globalPhaseOffsetForZSXX(seq)); } - const auto angles = + const auto [theta, phi, lambda, phase] = EulerDecomposition::anglesFromUnitary(targetMatrix, basis); switch (basis) { @@ -474,14 +474,11 @@ Value synthesizeUnitary1QEuler(OpBuilder& builder, Location loc, Value qubit, case EulerBasis::ZXZ: case EulerBasis::XZX: case EulerBasis::XYX: - qubit = emitKAK(builder, loc, qubit, angles.theta, angles.phi, - angles.lambda, angles.phase, basis); + qubit = emitKAK(builder, loc, qubit, theta, phi, lambda, phase, basis); break; case EulerBasis::U: - qubit = UOp::create(builder, loc, qubit, angles.theta, angles.phi, - angles.lambda) - .getQubitOut(); - emitGPhaseIfNeeded(builder, loc, angles.phase); + qubit = UOp::create(builder, loc, qubit, theta, phi, lambda).getQubitOut(); + emitGPhaseIfNeeded(builder, loc, phase); break; case EulerBasis::ZSXX: llvm_unreachable("ZSXX handled above"); @@ -493,8 +490,8 @@ Value synthesizeUnitary1QEuler(OpBuilder& builder, Location loc, Value qubit, /** * @brief Counts non-identity K-A-K rotations `emitKAK` would emit. */ -[[nodiscard]] static std::size_t countKAKGates(double theta, double phi, - double lambda) { +[[nodiscard]] static std::size_t +countKAKGates(const double theta, const double phi, const double lambda) { std::size_t count = 0; if (!isNearZeroRotationAngle(lambda)) { ++count; @@ -538,7 +535,7 @@ countZSXXSequenceGates(const ZSXXSequence& seq) { } std::size_t synthesisGateCount(const Matrix2x2& targetMatrix, - EulerBasis basis) { + const EulerBasis basis) { if (basis == EulerBasis::U) { return 1; } @@ -562,12 +559,14 @@ std::size_t synthesisGateCount(const Matrix2x2& targetMatrix, const auto seq = sequenceFromZYZForZSXX(zyz.theta, zyz.phi, zyz.lambda); return countZSXXSequenceGates(seq); } + default: + llvm::reportFatalInternalError( + "Unhandled Euler basis in synthesisGateCount"); } - llvm::reportFatalInternalError("Unhandled Euler basis in synthesisGateCount"); } bool wouldShortenInBasisRun(const std::size_t runSize, - const Matrix2x2& composed, EulerBasis basis) { + const Matrix2x2& composed, const EulerBasis basis) { if (runSize > maxSynthesisGateCount(basis)) { return true; } diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp index cd8f4a54e6..4b255c868a 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp @@ -237,9 +237,9 @@ struct FuseSingleQubitUnitaryRunsPass final void runOnOperation() override { auto module = getOperation(); - const auto parsed = decomposition::parseEulerBasis(this->basis); + const auto parsed = decomposition::parseEulerBasis(basis); if (!parsed) { - module.emitError() << "Invalid Euler basis '" << this->basis + module.emitError() << "Invalid Euler basis '" << basis << "'. Expected one of: zyz, zxz, xzx, xyx, u, zsxx."; signalPassFailure(); return; From 093ba8b4888477ea63eab155363b49c3f2b76363 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 15 Jun 2026 13:42:38 +0200 Subject: [PATCH 45/68] =?UTF-8?q?=E2=9C=A8=20Enhance=20matrix=20operations?= =?UTF-8?q?=20with=20element-wise=20scaling=20and=20adjoint=20functionalit?= =?UTF-8?q?y?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/include/mlir/Dialect/QCO/Utils/Matrix.h | 91 +++++++++++++ mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp | 67 +++++----- mlir/lib/Dialect/QCO/Utils/Matrix.cpp | 122 +++++++++++++++--- .../Dialect/QCO/IR/test_qco_ir_matrix.cpp | 10 +- .../test_euler_decomposition.cpp | 3 +- .../Dialect/QCO/Utils/test_unitary_matrix.cpp | 55 ++++++++ 6 files changed, 285 insertions(+), 63 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h index 51229befc0..8f49d372d7 100644 --- a/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h +++ b/mlir/include/mlir/Dialect/QCO/Utils/Matrix.h @@ -60,6 +60,26 @@ struct Matrix1x1 { */ [[nodiscard]] Complex operator()(std::size_t row, std::size_t col) const; + /** + * @brief Element-wise scaling by a complex scalar. + * @param scalar Factor applied to the matrix entry. + * @return Scaled copy of this matrix. + */ + [[nodiscard]] Matrix1x1 operator*(const Complex& scalar) const; + + /** + * @brief Element-wise in-place scaling by a complex scalar. + * @param scalar Factor applied to the matrix entry. + * @return Reference to this matrix. + */ + Matrix1x1& operator*=(const Complex& scalar); + + /** + * @brief Returns the conjugate transpose (adjoint) of this matrix. + * @return Adjoint matrix `A^\dagger`. + */ + [[nodiscard]] Matrix1x1 adjoint() const; + /** * @brief Checks approximate equality using an absolute tolerance. * @param other Matrix to compare against. @@ -137,6 +157,26 @@ struct Matrix2x2 { */ [[nodiscard]] Matrix2x2 operator*(const Matrix2x2& rhs) const; + /** + * @brief Premultiplies by a matrix: `*this = lhs * *this`. + * @param lhs Left-hand factor. + */ + void premultiplyBy(const Matrix2x2& lhs); + + /** + * @brief Element-wise scaling by a complex scalar. + * @param scalar Factor applied to every matrix entry. + * @return Scaled copy of this matrix. + */ + [[nodiscard]] Matrix2x2 operator*(const Complex& scalar) const; + + /** + * @brief Element-wise in-place scaling by a complex scalar. + * @param scalar Factor applied to every matrix entry. + * @return Reference to this matrix. + */ + Matrix2x2& operator*=(const Complex& scalar); + /** * @brief Returns the conjugate transpose (adjoint) of this matrix. * @return Adjoint matrix `A^\dagger`. @@ -256,6 +296,26 @@ struct Matrix4x4 { */ [[nodiscard]] Matrix4x4 operator*(const Matrix4x4& rhs) const; + /** + * @brief Premultiplies by a matrix: `*this = lhs * *this`. + * @param lhs Left-hand factor. + */ + void premultiplyBy(const Matrix4x4& lhs); + + /** + * @brief Element-wise scaling by a complex scalar. + * @param scalar Factor applied to every matrix entry. + * @return Scaled copy of this matrix. + */ + [[nodiscard]] Matrix4x4 operator*(const Complex& scalar) const; + + /** + * @brief Element-wise in-place scaling by a complex scalar. + * @param scalar Factor applied to every matrix entry. + * @return Reference to this matrix. + */ + Matrix4x4& operator*=(const Complex& scalar); + /** * @brief Returns the conjugate transpose (adjoint) of this matrix. * @return Adjoint matrix `A^\dagger`. @@ -314,6 +374,18 @@ class DynamicMatrix { */ explicit DynamicMatrix(std::int64_t dim); + /** + * @brief Creates a dynamic matrix from a fixed 2x2 matrix. + * @param src Source matrix. + */ + explicit DynamicMatrix(const Matrix2x2& src); + + /** + * @brief Creates a dynamic matrix from a fixed 4x4 matrix. + * @param src Source matrix. + */ + explicit DynamicMatrix(const Matrix4x4& src); + /// Copy constructor. DynamicMatrix(const DynamicMatrix& other); /// Move constructor. @@ -332,6 +404,13 @@ class DynamicMatrix { */ [[nodiscard]] static DynamicMatrix identity(std::int64_t dim); + /** + * @brief Creates a dynamic matrix holding the adjoint of a 2x2 matrix. + * @param src Source matrix. + * @return Adjoint matrix `src^\dagger`. + */ + [[nodiscard]] static DynamicMatrix fromAdjoint(const Matrix2x2& src); + /** * @brief Returns the number of rows. * @return Matrix dimension. @@ -412,6 +491,18 @@ class DynamicMatrix { */ void assignFrom(const DynamicMatrix& src); + /** + * @brief Checks approximate equality against a fixed 1x1 matrix. + * + * Returns false if this matrix is not 1x1. + * + * @param other Fixed-size matrix to compare against. + * @param tol Maximum allowed complex modulus of the entry difference. + * @return True if dimensions match and the entry differs by at most @p tol. + */ + [[nodiscard]] bool isApprox(const Matrix1x1& other, + double tol = MATRIX_TOLERANCE) const; + /** * @brief Checks approximate equality against a fixed 2x2 matrix. * diff --git a/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp b/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp index a83432abd6..5cf179af01 100644 --- a/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp @@ -405,41 +405,48 @@ void InvOp::getCanonicalizationPatterns(RewritePatternSet& results, } /** - * @brief Composes compile-time single-qubit unitaries in a modifier body. + * @brief Composes compile-time single-qubit unitaries and returns the inverse. */ -[[nodiscard]] static std::optional -composeSingleQubitBodyMatrix(Block& block) { +[[nodiscard]] static std::optional +composeInvertedSingleQubitBodyMatrix(Block& block) { Matrix2x2 acc = Matrix2x2::identity(); - std::complex global{1.0, 0.0}; + Complex global{1.0, 0.0}; bool found = false; for (Operation& op : block.without_terminator()) { - if (isa(op)) { - continue; - } - if (auto gphase = dyn_cast(op)) { - const auto matrix = gphase.getUnitaryMatrix(); - if (!matrix) { - return std::nullopt; - } - global *= (*matrix)(0, 0); - continue; - } - auto unitary = dyn_cast(op); - if (!unitary) { - return std::nullopt; - } - Matrix2x2 matrix; - if (!unitary.getUnitaryMatrix2x2(matrix)) { + if (!TypeSwitch(&op) + .Case([](auto) { return true; }) + .Case([&](GPhaseOp gphase) { + const auto matrix = gphase.getUnitaryMatrix(); + if (!matrix) { + return false; + } + global *= (*matrix)(0, 0); + return true; + }) + .Case([&](UnitaryOpInterface unitary) { + Matrix2x2 matrix; + if (!unitary.getUnitaryMatrix2x2(matrix)) { + return false; + } + acc.premultiplyBy(matrix); + found = true; + return true; + }) + .Default([](Operation* operation) { + const auto usesQubit = [](Value value) { + return llvm::isa(value.getType()); + }; + return !llvm::any_of(operation->getOperands(), usesQubit) && + !llvm::any_of(operation->getResults(), usesQubit); + })) { return std::nullopt; } - acc = matrix * acc; - found = true; } - if (!found && global == std::complex{1.0, 0.0}) { + if (!found && global == Complex{1.0, 0.0}) { return std::nullopt; } - return Matrix2x2::fromElements(global * acc(0, 0), global * acc(0, 1), - global * acc(1, 0), global * acc(1, 1)); + acc *= global; + return DynamicMatrix::fromAdjoint(acc); } std::optional InvOp::getUnitaryMatrix() { @@ -455,11 +462,5 @@ std::optional InvOp::getUnitaryMatrix() { if (getNumTargets() != 1) { return std::nullopt; } - const auto bodyMatrix = composeSingleQubitBodyMatrix(*getBody()); - if (!bodyMatrix) { - return std::nullopt; - } - DynamicMatrix result; - result.assignFrom(bodyMatrix->adjoint()); - return result; + return composeInvertedSingleQubitBodyMatrix(*getBody()); } diff --git a/mlir/lib/Dialect/QCO/Utils/Matrix.cpp b/mlir/lib/Dialect/QCO/Utils/Matrix.cpp index c75be25b0e..8df45840a3 100644 --- a/mlir/lib/Dialect/QCO/Utils/Matrix.cpp +++ b/mlir/lib/Dialect/QCO/Utils/Matrix.cpp @@ -55,7 +55,7 @@ static void assignFixedImpl(std::int64_t& dim, SmallVector& data, template [[nodiscard]] static bool -isApproxFixedImpl(const std::int64_t dim, ArrayRef data, +isApproxFixedImpl(const std::int64_t dim, const SmallVector& data, const std::array& other, const double tol) { if (std::cmp_not_equal(dim, Dim)) { return false; @@ -80,6 +80,35 @@ assignFromDynamicImpl(const DynamicMatrix& src, return true; } +/// Writes the row-major product `lhs * rhs` into @p out (2x2, fully unrolled). +static void +multiply2x2(const std::array& lhs, + const std::array& rhs, + std::array& out) { + out[0] = lhs[0] * rhs[0] + lhs[1] * rhs[2]; + out[1] = lhs[0] * rhs[1] + lhs[1] * rhs[3]; + out[2] = lhs[2] * rhs[0] + lhs[3] * rhs[2]; + out[3] = lhs[2] * rhs[1] + lhs[3] * rhs[3]; +} + +/// Writes the row-major product `lhs * rhs` into @p out (4x4, unrolled rows). +static void +multiply4x4(const std::array& lhs, + const std::array& rhs, + std::array& out) { + for (std::size_t row = 0; row < Matrix4x4::K_ROWS; ++row) { + const std::size_t rowBase = row * Matrix4x4::K_COLS; + const Complex& a0 = lhs[rowBase + 0]; + const Complex& a1 = lhs[rowBase + 1]; + const Complex& a2 = lhs[rowBase + 2]; + const Complex& a3 = lhs[rowBase + 3]; + out[rowBase + 0] = a0 * rhs[0] + a1 * rhs[4] + a2 * rhs[8] + a3 * rhs[12]; + out[rowBase + 1] = a0 * rhs[1] + a1 * rhs[5] + a2 * rhs[9] + a3 * rhs[13]; + out[rowBase + 2] = a0 * rhs[2] + a1 * rhs[6] + a2 * rhs[10] + a3 * rhs[14]; + out[rowBase + 3] = a0 * rhs[3] + a1 * rhs[7] + a2 * rhs[11] + a3 * rhs[15]; + } +} + /// Returns @p dim as `size_t` after asserting it is non-negative and squarable. [[nodiscard]] static std::size_t checkedDim(const std::int64_t dim) { assert(dim >= 0 && "DynamicMatrix dimension must be non-negative"); @@ -148,6 +177,17 @@ bool Matrix1x1::assignFrom(const DynamicMatrix& src) { return true; } +Matrix1x1 Matrix1x1::operator*(const Complex& scalar) const { + return fromElements(value * scalar); +} + +Matrix1x1& Matrix1x1::operator*=(const Complex& scalar) { + value *= scalar; + return *this; +} + +Matrix1x1 Matrix1x1::adjoint() const { return fromElements(std::conj(value)); } + Matrix2x2 Matrix2x2::fromElements(const Complex& m00, const Complex& m01, const Complex& m10, const Complex& m11) { return {{m00, m01, m10, m11}}; @@ -163,10 +203,27 @@ Complex Matrix2x2::operator()(const std::size_t row, } Matrix2x2 Matrix2x2::operator*(const Matrix2x2& rhs) const { - return fromElements(data[0] * rhs.data[0] + data[1] * rhs.data[2], - data[0] * rhs.data[1] + data[1] * rhs.data[3], - data[2] * rhs.data[0] + data[3] * rhs.data[2], - data[2] * rhs.data[1] + data[3] * rhs.data[3]); + Matrix2x2 out{}; + multiply2x2(data, rhs.data, out.data); + return out; +} + +void Matrix2x2::premultiplyBy(const Matrix2x2& lhs) { + const std::array rhs = data; + multiply2x2(lhs.data, rhs, data); +} + +Matrix2x2 Matrix2x2::operator*(const Complex& scalar) const { + Matrix2x2 out = *this; + out *= scalar; + return out; +} + +Matrix2x2& Matrix2x2::operator*=(const Complex& scalar) { + for (Complex& entry : data) { + entry *= scalar; + } + return *this; } Matrix2x2 Matrix2x2::adjoint() const { @@ -211,24 +268,28 @@ Complex Matrix4x4::operator()(const std::size_t row, Matrix4x4 Matrix4x4::operator*(const Matrix4x4& rhs) const { Matrix4x4 out{}; - for (std::size_t row = 0; row < K_ROWS; ++row) { - const std::size_t rowBase = row * K_COLS; - const Complex& a0 = data[rowBase + 0]; - const Complex& a1 = data[rowBase + 1]; - const Complex& a2 = data[rowBase + 2]; - const Complex& a3 = data[rowBase + 3]; - out.data[rowBase + 0] = a0 * rhs.data[0] + a1 * rhs.data[4] + - a2 * rhs.data[8] + a3 * rhs.data[12]; - out.data[rowBase + 1] = a0 * rhs.data[1] + a1 * rhs.data[5] + - a2 * rhs.data[9] + a3 * rhs.data[13]; - out.data[rowBase + 2] = a0 * rhs.data[2] + a1 * rhs.data[6] + - a2 * rhs.data[10] + a3 * rhs.data[14]; - out.data[rowBase + 3] = a0 * rhs.data[3] + a1 * rhs.data[7] + - a2 * rhs.data[11] + a3 * rhs.data[15]; - } + multiply4x4(data, rhs.data, out.data); return out; } +void Matrix4x4::premultiplyBy(const Matrix4x4& lhs) { + const std::array rhs = data; + multiply4x4(lhs.data, rhs, data); +} + +Matrix4x4 Matrix4x4::operator*(const Complex& scalar) const { + Matrix4x4 out = *this; + out *= scalar; + return out; +} + +Matrix4x4& Matrix4x4::operator*=(const Complex& scalar) { + for (Complex& entry : data) { + entry *= scalar; + } + return *this; +} + Matrix4x4 Matrix4x4::adjoint() const { Matrix4x4 out{}; adjointInto(data, out.data, K_ROWS); @@ -272,6 +333,16 @@ DynamicMatrix::DynamicMatrix(const std::int64_t dim) impl_->data.assign(checkedStorageSize(dim), Complex{}); } +DynamicMatrix::DynamicMatrix(const Matrix2x2& src) + : impl_(std::make_unique()) { + assignFrom(src); +} + +DynamicMatrix::DynamicMatrix(const Matrix4x4& src) + : impl_(std::make_unique()) { + assignFrom(src); +} + DynamicMatrix::DynamicMatrix(const DynamicMatrix& other) : impl_(std::make_unique(*other.impl_)) {} @@ -302,6 +373,10 @@ DynamicMatrix DynamicMatrix::identity(const std::int64_t dim) { return matrix; } +DynamicMatrix DynamicMatrix::fromAdjoint(const Matrix2x2& src) { + return DynamicMatrix(src.adjoint()); +} + Complex& DynamicMatrix::operator()(const std::int64_t row, const std::int64_t col) { return impl_->data[static_cast((row * impl_->dim) + col)]; @@ -354,6 +429,13 @@ void DynamicMatrix::assignFrom(const DynamicMatrix& src) { *impl_ = *src.impl_; } +bool DynamicMatrix::isApprox(const Matrix1x1& other, const double tol) const { + if (impl_->dim != 1) { + return false; + } + return std::abs(impl_->data[0] - other.value) <= tol; +} + bool DynamicMatrix::isApprox(const Matrix2x2& other, const double tol) const { return isApproxFixedImpl( diff --git a/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp b/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp index e772b945ab..e196b6c1b8 100644 --- a/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp +++ b/mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp @@ -169,15 +169,9 @@ TEST_F(QCOMatrixTest, InverseGphaseXOpMatrix) { ASSERT_TRUE(matrix); const auto composeGlobal = std::polar(1.0, -0.123); - Matrix2x2 body = XOp::getUnitaryMatrix(); - body(0, 0) *= composeGlobal; - body(0, 1) *= composeGlobal; - body(1, 0) *= composeGlobal; - body(1, 1) *= composeGlobal; + const Matrix2x2 body = XOp::getUnitaryMatrix() * composeGlobal; - DynamicMatrix expected; - expected.assignFrom(body.adjoint()); - ASSERT_TRUE(matrix->isApprox(expected)); + ASSERT_TRUE(matrix->isApprox(DynamicMatrix::fromAdjoint(body))); } TEST_F(QCOMatrixTest, InverseGphaseBarrierOpMatrix) { 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 ee04683397..08829babd2 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -230,8 +230,7 @@ static Matrix2x2 compute1QUnitaryMatrix(WalkRange& range) { if (failed) { return Matrix2x2::fromElements(0, 0, 0, 0); } - return Matrix2x2::fromElements(global * acc(0, 0), global * acc(0, 1), - global * acc(1, 0), global * acc(1, 1)); + return acc * global; } static Matrix2x2 compute1QMatrixFromFunction(func::FuncOp funcOp) { diff --git a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp index 375813f103..58649975be 100644 --- a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp +++ b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp @@ -53,6 +53,16 @@ TEST(UnitaryMatrix1x1, IsApprox) { EXPECT_TRUE(a.isApprox(b)); EXPECT_FALSE(a.isApprox(Matrix1x1::fromElements(2.0))); EXPECT_TRUE(a.isApprox(Matrix1x1::fromElements(1.1), 0.2)); + EXPECT_EQ((Matrix1x1::fromElements(0.5) * 2.0)(0, 0), 1.0); + Matrix1x1 scaled = Matrix1x1::fromElements(0.5); + scaled *= 2.0; + EXPECT_EQ(scaled(0, 0), 1.0); +} + +TEST(UnitaryMatrix1x1, Adjoint) { + const Matrix1x1 phase = Matrix1x1::fromElements(Complex{0.25, 0.5}); + EXPECT_TRUE(phase.adjoint().isApprox( + Matrix1x1::fromElements(std::conj(phase.value)))); } TEST(UnitaryMatrix2x2, IdentityAndAccess) { @@ -70,6 +80,12 @@ TEST(UnitaryMatrix2x2, MultiplyAdjointTraceDeterminant) { EXPECT_TRUE((x * x).isApprox(identity)); EXPECT_TRUE((identity * x).isApprox(x)); + EXPECT_TRUE((x * std::exp(1i * 0.5)) + .isApprox(Matrix2x2::fromElements(0, std::exp(1i * 0.5), + std::exp(1i * 0.5), 0))); + Matrix2x2 scaled = x; + scaled *= std::exp(1i * 0.5); + EXPECT_TRUE(scaled.isApprox(x * std::exp(1i * 0.5))); EXPECT_TRUE(x.adjoint().isApprox(x)); EXPECT_EQ(x.trace(), Complex(0.0, 0.0)); EXPECT_EQ(identity.trace(), Complex(2.0, 0.0)); @@ -77,6 +93,15 @@ TEST(UnitaryMatrix2x2, MultiplyAdjointTraceDeterminant) { EXPECT_EQ(identity.determinant(), Complex(1.0, 0.0)); } +TEST(UnitaryMatrix2x2, PremultiplyBy) { + const Matrix2x2 x = pauliX(); + const Matrix2x2 y = Matrix2x2::fromElements(1, 0, 0, std::exp(1i * 0.5)); + Matrix2x2 acc = Matrix2x2::identity(); + acc.premultiplyBy(x); + acc.premultiplyBy(y); + EXPECT_TRUE(acc.isApprox(y * x)); +} + TEST(UnitaryMatrix2x2, IsApprox) { const Matrix2x2 a = Matrix2x2::identity(); Matrix2x2 b = a; @@ -90,6 +115,7 @@ TEST(UnitaryMatrix4x4, IdentityAndAccess) { EXPECT_TRUE(identity.isApprox( Matrix4x4::fromElements(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1))); EXPECT_EQ(identity(2, 2), 1.0); + EXPECT_TRUE((swapMatrix() * 2.0)(0, 0) == 2.0); } TEST(UnitaryMatrix4x4, MultiplyAdjointTraceDeterminant) { @@ -98,10 +124,22 @@ TEST(UnitaryMatrix4x4, MultiplyAdjointTraceDeterminant) { EXPECT_TRUE((swap * swap).isApprox(identity)); EXPECT_TRUE(swap.adjoint().isApprox(swap)); + Matrix4x4 scaled = swap; + scaled *= 2.0; + EXPECT_TRUE(scaled.isApprox(swap * 2.0)); EXPECT_EQ(identity.trace(), Complex(4.0, 0.0)); EXPECT_EQ(identity.determinant(), Complex(1.0, 0.0)); } +TEST(UnitaryMatrix4x4, PremultiplyBy) { + const Matrix4x4 swap = swapMatrix(); + const Matrix4x4 phase = Matrix4x4::identity() * std::exp(1i * 0.25); + Matrix4x4 acc = Matrix4x4::identity(); + acc.premultiplyBy(swap); + acc.premultiplyBy(phase); + EXPECT_TRUE(acc.isApprox(phase * swap)); +} + TEST(UnitaryMatrix4x4, IsApprox) { const Matrix4x4 a = Matrix4x4::identity(); Matrix4x4 b = a; @@ -153,6 +191,16 @@ TEST(DynamicMatrix, IdentityAndElementAccess) { EXPECT_EQ(mutableMatrix(1, 1), 0.5); } +TEST(DynamicMatrix, FromAdjoint) { + const Matrix2x2 x = pauliX(); + EXPECT_TRUE(DynamicMatrix::fromAdjoint(x).isApprox(x.adjoint())); + const Complex global = std::polar(1.0, 0.25); + EXPECT_TRUE( + DynamicMatrix::fromAdjoint(x * global).isApprox((x * global).adjoint())); + EXPECT_TRUE(DynamicMatrix(x).isApprox(x)); + EXPECT_TRUE(DynamicMatrix(swapMatrix()).isApprox(swapMatrix())); +} + TEST(DynamicMatrix, AssignFrom) { DynamicMatrix dynamic; @@ -255,9 +303,15 @@ TEST(Matrix4x4, AssignFromDynamicMatrix) { } TEST(DynamicMatrix, IsApproxOverloads) { + const Matrix1x1 phase = Matrix1x1::fromElements(Complex{0.25, 0.5}); const Matrix2x2 x = pauliX(); const Matrix4x4 swap = swapMatrix(); + DynamicMatrix as1x1; + as1x1.assignFrom(phase); + EXPECT_TRUE(as1x1.isApprox(phase)); + EXPECT_FALSE(as1x1.isApprox(Matrix1x1::fromElements(1.0))); + DynamicMatrix as2x2; as2x2.assignFrom(x); EXPECT_TRUE(as2x2.isApprox(x)); @@ -269,6 +323,7 @@ TEST(DynamicMatrix, IsApproxOverloads) { EXPECT_FALSE(as4x4.isApprox(Matrix4x4::identity())); DynamicMatrix wrongDim = DynamicMatrix::identity(3); + EXPECT_FALSE(wrongDim.isApprox(phase)); EXPECT_FALSE(wrongDim.isApprox(x)); EXPECT_FALSE(wrongDim.isApprox(swap)); From 39a8d4fa456f7e17cdefbef90262f53dc3d51489 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 15 Jun 2026 13:59:10 +0200 Subject: [PATCH 46/68] =?UTF-8?q?=F0=9F=8E=A8=20Update=20default=20toleran?= =?UTF-8?q?ce=20for=20MLIR=20dialect=20numerics=20and=20revert=20error=20h?= =?UTF-8?q?andling=20in=20block=20argument=20retrieval?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/include/mlir/Dialect/Utils/Utils.h | 13 ++++++------- .../Decomposition/test_euler_decomposition.cpp | 12 ++++++------ 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/mlir/include/mlir/Dialect/Utils/Utils.h b/mlir/include/mlir/Dialect/Utils/Utils.h index 4d129f69fc..4135ca1769 100644 --- a/mlir/include/mlir/Dialect/Utils/Utils.h +++ b/mlir/include/mlir/Dialect/Utils/Utils.h @@ -19,15 +19,16 @@ #include #include +#include #include #include #include namespace mlir::utils { -/// Default absolute tolerance for MLIR dialect numerics (matrix checks, -/// angles). -constexpr auto TOLERANCE = 1e-14; +/// Default absolute tolerance for MLIR dialect numerics (angle wrapping, +/// phase-zero checks). +constexpr auto TOLERANCE = 1e-15; inline Value constantFromScalar(OpBuilder& builder, Location loc, double v) { return arith::ConstantOp::create(builder, loc, builder.getF64FloatAttr(v)); @@ -199,10 +200,8 @@ inline void printTargetAliasing(OpAsmPrinter& printer, Region& region, */ inline Value getValueFromBlockArgument(Value qubit, ValueRange qubits) { if (auto blockArg = dyn_cast(qubit)) { - if (blockArg.getArgNumber() >= qubits.size()) { - llvm::reportFatalUsageError( - "block argument index must be within qubits range"); - } + assert(blockArg.getArgNumber() < qubits.size() && + "block argument index must be within qubits range"); return qubits[blockArg.getArgNumber()]; } return qubit; 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 08829babd2..eb8d207ddb 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -248,15 +248,15 @@ compute1QMatrixFromCtrlBody(func::FuncOp funcOp) { static void expectMatrixPreserved(func::FuncOp funcOp, const Matrix2x2& original, StringRef label) { - EXPECT_TRUE(compute1QMatrixFromFunction(funcOp).isApprox( - original, mlir::utils::TOLERANCE)) + EXPECT_TRUE( + compute1QMatrixFromFunction(funcOp).isApprox(original, MATRIX_TOLERANCE)) << label.str(); } static void expectCtrlBodyMatrixPreserved(func::FuncOp funcOp, const Matrix2x2& original) { - EXPECT_TRUE(compute1QMatrixFromCtrlBody(funcOp).isApprox( - original, mlir::utils::TOLERANCE)); + EXPECT_TRUE( + compute1QMatrixFromCtrlBody(funcOp).isApprox(original, MATRIX_TOLERANCE)); } static void expectBasisGatesOnly(func::FuncOp funcOp, StringRef basis) { @@ -637,8 +637,8 @@ TEST(EulerDecompositionTest, ZYZAnglesFromUnitaryReconstructHadamard) { builder.create(loc, q); ASSERT_TRUE(succeeded(verify(module))); - EXPECT_TRUE(compute1QMatrixFromFunction(func).isApprox( - hadamard, mlir::utils::TOLERANCE)); + EXPECT_TRUE( + compute1QMatrixFromFunction(func).isApprox(hadamard, MATRIX_TOLERANCE)); } TEST(EulerSynthesisTest, ProfitabilityAndIdentityGateCount) { From 1d076c7a9e1ff8cbc95d2628c4c24a02b42422e2 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 15 Jun 2026 15:39:11 +0200 Subject: [PATCH 47/68] =?UTF-8?q?=F0=9F=8E=A8=20Refactor=20Euler=20synthes?= =?UTF-8?q?is=20functions=20and=20improve=20gate=20fusion=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/Decomposition/Euler.h | 166 +---- .../mlir/Dialect/QCO/Transforms/Passes.td | 2 +- .../QCO/Transforms/Decomposition/Euler.cpp | 691 ++++++++---------- .../FuseSingleQubitUnitaryRuns.cpp | 20 +- .../test_euler_decomposition.cpp | 310 ++++---- 5 files changed, 501 insertions(+), 688 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h index fd1b4629db..680b679bdb 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h @@ -22,17 +22,6 @@ namespace mlir::qco::decomposition { -/** - * @brief Euler angles `(theta, phi, lambda)` and global phase for a 2x2 - * unitary. - */ -struct EulerAngles { - double theta = 0.0; - double phi = 0.0; - double lambda = 0.0; - double phase = 0.0; -}; - /** * @brief Native gate sets for single-qubit Euler synthesis. */ @@ -45,86 +34,6 @@ enum class EulerBasis : std::uint8_t { ZSXX = 5, ///< `RZ` / `SX` / `X` synthesis via ZYZ decomposition. }; -/** - * @brief Middle-gate case for ZSXX synthesis from ZYZ `theta`. - */ -enum class ZSXXMiddleGate : std::uint8_t { - OnlyRZ, - OneSX, - X, - SXRZSX, -}; - -/** - * @brief Classifies the ZSXX middle-gate case from ZYZ `theta`. - * - * @param theta Y-rotation angle from `paramsZYZ` in `[0, pi]`. - * @return The ZSXX middle-gate case. - */ -[[nodiscard]] ZSXXMiddleGate classifyZSXXMiddleFromZYZTheta(double theta); - -/** - * @brief Extracts Euler parameters from single-qubit unitary matrices. - */ -class EulerDecomposition { - friend Value synthesizeUnitary1QEuler(OpBuilder& builder, Location loc, - Value qubit, - const Matrix2x2& targetMatrix, - EulerBasis basis); - -public: - /** - * @brief Extracts `(theta, phi, lambda, phase)` for KAK and `U` 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, - EulerBasis basis); - -private: - /** - * @brief Extracts parameters for `RZ(phi) * RY(theta) * RZ(lambda)`. - * - * @param matrix The single-qubit unitary to decompose. - * @return The extracted Euler angles and global phase. - */ - [[nodiscard]] static EulerAngles paramsZYZ(const Matrix2x2& matrix); - - /** - * @brief Extracts parameters for `RZ(phi) * RX(theta) * RZ(lambda)`. - * - * @param matrix The single-qubit unitary to decompose. - * @return The extracted Euler angles and global phase. - */ - [[nodiscard]] static EulerAngles paramsZXZ(const Matrix2x2& matrix); - - /** - * @brief Extracts parameters for `RX(phi) * RZ(theta) * RX(lambda)`. - * - * @param matrix The single-qubit unitary to decompose. - * @return The extracted Euler angles and global phase. - */ - [[nodiscard]] static EulerAngles paramsXZX(const Matrix2x2& matrix); - - /** - * @brief Extracts parameters for `RX(phi) * RY(theta) * RX(lambda)`. - * - * @param matrix The single-qubit unitary to decompose. - * @return The extracted Euler angles and global phase. - */ - [[nodiscard]] static EulerAngles paramsXYX(const Matrix2x2& matrix); - - /** - * @brief Extracts parameters for `U(theta, phi, lambda)`. - * - * @param matrix The single-qubit unitary to decompose. - * @return The extracted Euler angles and global phase. - */ - [[nodiscard]] static EulerAngles paramsU(const Matrix2x2& matrix); -}; - /** * @brief Parses a basis name (e.g. `zyz`, `zsxx`; case-insensitive). * @@ -134,74 +43,25 @@ class EulerDecomposition { [[nodiscard]] std::optional parseEulerBasis(StringRef basis); /** - * @brief Synthesizes `targetMatrix` as gates in `basis`. + * @brief Synthesizes a composed single-qubit unitary as gates in @p basis. * - * Emits `qco.gphase` when needed so the result matches exactly, not only up to - * global phase. + * Decomposes @p composed once. Returns `std::nullopt` when @p hasNonBasisGate + * is false and resynthesis would not shorten a run of @p runSize gates; + * otherwise emits gates (including `qco.gphase` when needed) and returns the + * output qubit. * * @param builder Builder for the emitted operations. * @param loc Location for the emitted operations. * @param qubit Input qubit value. - * @param targetMatrix The single-qubit unitary to synthesize. - * @param basis The target Euler basis. - * @return The output qubit value. - */ -[[nodiscard]] Value synthesizeUnitary1QEuler(OpBuilder& builder, Location loc, - Value qubit, - const Matrix2x2& targetMatrix, - EulerBasis basis); - -/** - * @brief Number of basis gates `synthesizeUnitary1QEuler` would emit. - * - * Excludes `qco.gphase` and near-zero rotations that synthesis skips. - * - * @param targetMatrix The single-qubit unitary that would be synthesized. - * @param basis The target Euler basis. - * @return The gate count (1 for `U`, up to 3 for KAK bases, up to 5 for - * `ZSXX`). For `ZSXX`, pure-Z (`OnlyRZ`) compositions count non-zero - * `RZ` gates only (1 or 2); `OneSX` and `X` shortcuts count 3; the - * generic case counts up to 5. - */ -[[nodiscard]] std::size_t synthesisGateCount(const Matrix2x2& targetMatrix, - EulerBasis basis); - -/** - * @brief Upper bound on basis gates `synthesizeUnitary1QEuler` may emit. - * - * @param basis The target Euler basis. - * @return `1` for `U`, `3` for KAK bases, `5` for `ZSXX`. - */ -[[nodiscard]] constexpr std::size_t -maxSynthesisGateCount(const EulerBasis basis) { - switch (basis) { - case EulerBasis::ZYZ: - case EulerBasis::ZXZ: - case EulerBasis::XZX: - case EulerBasis::XYX: - return 3; - case EulerBasis::ZSXX: - return 5; - case EulerBasis::U: - default: - return 1; - } -} - -/** - * @brief Whether an in-basis run would shorten after Euler resynthesis. - * - * Uses `maxSynthesisGateCount` as a cheap shortcut before falling back to - * `synthesisGateCount`. - * - * @param runSize Number of gates in the run. - * @param composed Composed unitary of the run. + * @param composed Composed unitary to synthesize. + * @param runSize Number of gates in the run (for fusion profitability). + * @param hasNonBasisGate Whether the run contains a gate outside @p basis. * @param basis The target Euler basis. - * @return `true` when Euler resynthesis would emit fewer basis gates than @p - * runSize. + * @return The synthesized qubit, or `std::nullopt` if synthesis is skipped. */ -[[nodiscard]] bool wouldShortenInBasisRun(std::size_t runSize, - const Matrix2x2& composed, - EulerBasis basis); +[[nodiscard]] std::optional +synthesizeUnitary1QEuler(OpBuilder& builder, Location loc, Value qubit, + const Matrix2x2& composed, std::size_t runSize, + bool hasNonBasisGate, EulerBasis basis); } // namespace mlir::qco::decomposition diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index ce733719f9..fafb906a77 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -51,7 +51,7 @@ def FuseSingleQubitUnitaryRuns same qubit wire (anchored at each run head), composes their constant unitary matrices, and replaces a run with an equivalent sequence of basis gates when beneficial: when the run contains a gate outside the target `basis`, or when - Euler resynthesis would shorten it (`wouldShortenInBasisRun`). Runs that are + Euler resynthesis would shorten it (`synthesizeUnitary1QEuler`). Runs that are already in the target `basis` and no shorter than the canonical synthesis length are left unchanged. diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index 2290126cfe..e81631d85e 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -14,31 +14,31 @@ #include "mlir/Dialect/QCO/Utils/Matrix.h" #include "mlir/Dialect/Utils/Utils.h" -#include +#include #include #include #include #include #include +#include #include #include #include +#include #include #include namespace mlir::qco::decomposition { /** - * @brief Wraps `angle` into `[-pi, pi)`, mapping `+pi` (within `atol`) to + * @brief Wraps `angle` into `[-pi, pi)`, mapping `+pi` (within tolerance) to * `-pi`. * * @param angle The angle to wrap, in radians. - * @param atol Tolerance for snapping `+pi` to `-pi`. * @return The wrapped angle in `[-pi, pi)`. */ -[[nodiscard]] static double mod2pi(const double angle, - const double atol = utils::TOLERANCE) { +[[nodiscard]] static double mod2pi(const double angle) { if (!std::isfinite(angle)) { return angle; } @@ -52,7 +52,7 @@ namespace mlir::qco::decomposition { } double wrapped = r - pi; - if (wrapped >= pi - atol) { + if (wrapped >= pi - utils::TOLERANCE) { wrapped = -pi; } @@ -100,284 +100,42 @@ static void emitGPhaseIfNeeded(OpBuilder& builder, Location loc, double phase) { GPhaseOp::create(builder, loc, phase); } -namespace { - -/** - * @brief Planned ZSXX (`RZ` / `SX` / `X`) chain; angles in circuit order. - */ -struct ZSXXSequence { - ZSXXMiddleGate middle = ZSXXMiddleGate::SXRZSX; - double firstRZ = 0.0; - double midRZ = 0.0; - double lastRZ = 0.0; -}; - -} // namespace - -ZSXXMiddleGate classifyZSXXMiddleFromZYZTheta(const double theta) { - constexpr double halfPi = std::numbers::pi / 2.0; - constexpr double pi = std::numbers::pi; - - if (isNearZeroRotationAngle(theta)) { - return ZSXXMiddleGate::OnlyRZ; - } - if (isNearZeroRotationAngle(theta - halfPi)) { - return ZSXXMiddleGate::OneSX; - } - if (isNearZeroRotationAngle(theta - pi)) { - return ZSXXMiddleGate::X; - } - return ZSXXMiddleGate::SXRZSX; -} - -/** - * @brief Builds the ZSXX sequence for `RZ(phi)*RY(theta)*RZ(lambda)`. - * - * Uses `SX*RZ(theta+pi)*SX = Z*RY(theta)`. - * - * @param theta Y-rotation angle in `[0, pi]`. - * @param phi Trailing Z-rotation angle. - * @param lambda Leading Z-rotation angle. - * @return The planned ZSXX sequence. - */ -[[nodiscard]] static ZSXXSequence sequenceFromZYZForZSXX(const double theta, - const double phi, - const double lambda) { - constexpr double halfPi = std::numbers::pi / 2.0; - constexpr double pi = std::numbers::pi; - - switch (classifyZSXXMiddleFromZYZTheta(theta)) { - case ZSXXMiddleGate::OnlyRZ: - return {.middle = ZSXXMiddleGate::OnlyRZ, - .firstRZ = lambda, - .midRZ = 0.0, - .lastRZ = phi}; - case ZSXXMiddleGate::OneSX: - return {.middle = ZSXXMiddleGate::OneSX, - .firstRZ = lambda - halfPi, - .midRZ = 0.0, - .lastRZ = phi + halfPi}; - case ZSXXMiddleGate::X: - return {.middle = ZSXXMiddleGate::X, - .firstRZ = lambda, - .midRZ = 0.0, - .lastRZ = phi + pi}; - case ZSXXMiddleGate::SXRZSX: - return {.middle = ZSXXMiddleGate::SXRZSX, - .firstRZ = lambda, - .midRZ = theta + pi, - .lastRZ = phi + pi}; - } - llvm::reportFatalInternalError("Unhandled ZSXX middle gate"); -} - /** * @brief Global phase offset of `UOp` vs `RZ(phi)*RY(theta)*RZ(lambda)`. * - * @param phi Trailing Z-rotation angle. - * @param lambda Leading Z-rotation angle. - * @return The global-phase offset in radians. + * @param phi Middle-axis angle from Z-Y-Z decomposition. + * @param lambda Outer-axis angle from Z-Y-Z decomposition. + * @return Phase correction to add to the Z-Y-Z global phase. */ [[nodiscard]] static double globalPhaseOffsetForU(const double phi, const double lambda) { return -0.5 * (phi + lambda); } -/** - * @brief Global phase from wrapping an RZ angle with `mod2pi`. - * - * `RZ(angle + 2*pi) = -RZ(angle)`, so `RZ(mod2pi(angle))` differs from - * `RZ(angle)` by `exp(i*(mod2pi(angle) - angle)/2)`. - * - * @param angle The unwrapped RZ angle. - * @return The global-phase contribution in radians. - */ -[[nodiscard]] static double globalPhaseFromRZWrap(const double angle) { - return 0.5 * (mod2pi(angle) - angle); -} - -/** - * @brief Global phase offset of the ZSXX chain vs. the ZYZ product. - * - * @param seq The planned ZSXX sequence. - * @return The global-phase offset in radians. - */ -[[nodiscard]] static double globalPhaseOffsetForZSXX(const ZSXXSequence& seq) { - constexpr double halfPi = std::numbers::pi / 2.0; - constexpr double quarterPi = std::numbers::pi / 4.0; - - switch (seq.middle) { - case ZSXXMiddleGate::OnlyRZ: - return globalPhaseFromRZWrap(seq.firstRZ) + - globalPhaseFromRZWrap(seq.lastRZ); - case ZSXXMiddleGate::OneSX: - // `SX = exp(i*pi/4)*RZ(-pi/2)*RY(pi/2)*RZ(pi/2)`; the outer RZ angles - // absorb the +-pi/2, leaving the exp(i*pi/4) phase. RZ wraps add too. - return -quarterPi + globalPhaseFromRZWrap(seq.firstRZ) + - globalPhaseFromRZWrap(seq.lastRZ); - case ZSXXMiddleGate::X: - // `X` swaps the diagonal, so the wraps enter with opposite signs. - return -halfPi + globalPhaseFromRZWrap(seq.lastRZ) - - globalPhaseFromRZWrap(seq.firstRZ); - case ZSXXMiddleGate::SXRZSX: - // `SX*RZ(theta+pi)*SX = Z*RY(theta)`; all three RZ wraps add. - return halfPi + globalPhaseFromRZWrap(seq.firstRZ) + - globalPhaseFromRZWrap(seq.midRZ) + globalPhaseFromRZWrap(seq.lastRZ); - } - llvm::reportFatalInternalError("Unhandled ZSXX middle gate"); -} - -/** - * @brief Invokes callbacks for each gate of `seq` in circuit order. - * - * @param seq The planned ZSXX sequence. - * @param onRZ Called with each RZ angle. - * @param onSX Called for each SX gate. - * @param onX Called for each X gate. - */ -static void visitSequenceInTimeOrder(const ZSXXSequence& seq, - llvm::function_ref onRZ, - llvm::function_ref onSX, - llvm::function_ref onX) { - onRZ(seq.firstRZ); - switch (seq.middle) { - case ZSXXMiddleGate::OnlyRZ: - onRZ(seq.lastRZ); - break; - case ZSXXMiddleGate::OneSX: - onSX(); - onRZ(seq.lastRZ); - break; - case ZSXXMiddleGate::X: - onX(); - onRZ(seq.lastRZ); - break; - case ZSXXMiddleGate::SXRZSX: - onSX(); - onRZ(seq.midRZ); - onSX(); - onRZ(seq.lastRZ); - break; - } -} +//===----------------------------------------------------------------------===// +// Euler decomposition (angles) +//===----------------------------------------------------------------------===// /** - * @brief Emits the gates of `seq` and optional `gphase`. - * - * @param builder Builder for the operations. - * @param loc Location of the operations. - * @param qubit Input qubit value. - * @param seq The planned ZSXX sequence. - * @param phase Global phase in radians. - * @return The output qubit value. + * @brief Euler angles `(theta, phi, lambda)` and global phase for a 2x2 + * unitary. */ -[[nodiscard]] static Value emitFromZSXXSequence(OpBuilder& builder, - Location loc, Value qubit, - const ZSXXSequence& seq, - const double phase) { - visitSequenceInTimeOrder( - seq, - [&](const double angle) { - const double wrapped = mod2pi(angle); - if (isNearZeroRotationAngle(wrapped)) { - return; - } - qubit = RZOp::create(builder, loc, qubit, wrapped).getQubitOut(); - }, - [&] { qubit = SXOp::create(builder, loc, qubit).getQubitOut(); }, - [&] { qubit = XOp::create(builder, loc, qubit).getQubitOut(); }); - emitGPhaseIfNeeded(builder, loc, phase); - return qubit; -} +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 Emits a K-A-K rotation triple and optional `gphase` for `basis`. + * @brief Z-Y-Z Euler angles and global phase for a 2x2 unitary. * - * @param builder Builder for the operations. - * @param loc Location of the operations. - * @param qubit Input qubit value. - * @param theta Middle (A) rotation angle. - * @param phi Trailing (K) rotation angle. - * @param lambda Leading (K) rotation angle. - * @param phase Global phase in radians. - * @param basis Euler basis selecting the rotation axes. - * @return The output qubit value. + * @param matrix Single-qubit unitary to decompose. + * @return Z-Y-Z angles and global phase. */ -static Value emitKAK(OpBuilder& builder, Location loc, Value qubit, - const double theta, const double phi, const double lambda, - const double phase, const EulerBasis basis) { - auto emitK = [&](double a) { - if (isNearZeroRotationAngle(a)) { - return; - } - switch (basis) { - case EulerBasis::ZYZ: - case EulerBasis::ZXZ: - qubit = RZOp::create(builder, loc, qubit, a).getQubitOut(); - break; - case EulerBasis::XZX: - case EulerBasis::XYX: - qubit = RXOp::create(builder, loc, qubit, a).getQubitOut(); - break; - default: - llvm::reportFatalInternalError("Invalid K gate for KAK emission"); - } - }; - - auto emitA = [&](double a) { - if (isNearZeroRotationAngle(a)) { - return; - } - switch (basis) { - case EulerBasis::ZYZ: - case EulerBasis::XYX: - qubit = RYOp::create(builder, loc, qubit, a).getQubitOut(); - break; - case EulerBasis::ZXZ: - qubit = RXOp::create(builder, loc, qubit, a).getQubitOut(); - break; - case EulerBasis::XZX: - qubit = RZOp::create(builder, loc, qubit, a).getQubitOut(); - break; - default: - llvm::reportFatalInternalError("Invalid A gate for KAK emission"); - } - }; - - emitK(lambda); - emitA(theta); - emitK(phi); - emitGPhaseIfNeeded(builder, loc, phase); - return qubit; -} - -//===----------------------------------------------------------------------===// -// Euler decomposition (angles) -//===----------------------------------------------------------------------===// - -EulerAngles EulerDecomposition::anglesFromUnitary(const Matrix2x2& matrix, - const EulerBasis basis) { - switch (basis) { - case EulerBasis::ZYZ: - return paramsZYZ(matrix); - case EulerBasis::ZXZ: - return paramsZXZ(matrix); - case EulerBasis::XZX: - return paramsXZX(matrix); - case EulerBasis::XYX: - return paramsXYX(matrix); - case EulerBasis::U: - return paramsU(matrix); - default: - llvm::reportFatalInternalError( - "Unsupported Euler basis for angle computation in decomposition!"); - } -} - -EulerAngles EulerDecomposition::paramsZYZ(const Matrix2x2& matrix) { +[[nodiscard]] static EulerAngles paramsZYZ(const Matrix2x2& matrix) { // det(U) = exp(2i*phase); invert the Z-Y-Z parameterization of U's entries. - const std::complex det = - matrix(0, 0) * matrix(1, 1) - matrix(0, 1) * matrix(1, 0); + const Complex det = matrix.determinant(); const auto detArg = std::arg(det); const auto phase = 0.5 * detArg; const auto theta = @@ -394,8 +152,13 @@ EulerAngles EulerDecomposition::paramsZYZ(const Matrix2x2& matrix) { return {.theta = theta, .phi = phi, .lambda = lambda, .phase = phase}; } -EulerAngles EulerDecomposition::paramsZXZ(const Matrix2x2& matrix) { - // ZXZ from ZYZ via RY(theta) = RZ(pi/2)*RX(theta)*RZ(-pi/2). +/** + * @brief Z-X-Z Euler angles via `RY(theta) = RZ(pi/2)*RX(theta)*RZ(-pi/2)`. + * + * @param matrix Single-qubit unitary to decompose. + * @return Z-X-Z angles and global phase. + */ +[[nodiscard]] static EulerAngles paramsZXZ(const Matrix2x2& matrix) { const auto [theta, phi, lambda, phase] = paramsZYZ(matrix); return {.theta = theta, .phi = phi + (std::numbers::pi / 2.0), @@ -403,25 +166,40 @@ EulerAngles EulerDecomposition::paramsZXZ(const Matrix2x2& matrix) { .phase = phase}; } -EulerAngles EulerDecomposition::paramsXZX(const Matrix2x2& matrix) { - // X-Z-X -> Z-X-Z under H conjugation (no Y sign flip, unlike paramsXYX). +/** + * @brief X-Z-X Euler angles (Z-X-Z under H conjugation, no Y sign flip). + * + * @param matrix Single-qubit unitary to decompose. + * @return X-Z-X angles and global phase. + */ +[[nodiscard]] static EulerAngles paramsXZX(const Matrix2x2& matrix) { return paramsZXZ(hadamardConjugate(matrix)); } -EulerAngles EulerDecomposition::paramsXYX(const Matrix2x2& matrix) { - // H*RY(theta)*H = RY(-theta): shift outer angles by pi and fix global phase. +/** + * @brief X-Y-X Euler angles via `H*RY(theta)*H = RY(-theta)`. + * + * @param matrix Single-qubit unitary to decompose. + * @return X-Y-X angles and global phase. + */ +[[nodiscard]] static EulerAngles paramsXYX(const Matrix2x2& matrix) { + // Shift outer angles by pi and fix global phase. const auto [theta, phi, lambda, phase] = paramsZYZ(hadamardConjugate(matrix)); - // Keep atol=0 so `phase` tracks the unwrapped ZYZ angles; snapping to pi - // would change the recorded global-phase correction. - const auto newPhi = mod2pi(phi + std::numbers::pi, 0.); - const auto newLambda = mod2pi(lambda + std::numbers::pi, 0.); + const auto newPhi = mod2pi(phi + std::numbers::pi); + const auto newLambda = mod2pi(lambda + std::numbers::pi); return {.theta = theta, .phi = newPhi, .lambda = newLambda, .phase = phase + ((newPhi + newLambda - phi - lambda) / 2.)}; } -EulerAngles EulerDecomposition::paramsU(const Matrix2x2& matrix) { +/** + * @brief `U`-basis angles (Z-Y-Z angles with a `U`-vs-`RZ·RY·RZ` phase fix). + * + * @param matrix Single-qubit unitary to decompose. + * @return `U`-gate angles and global phase. + */ +[[nodiscard]] static EulerAngles paramsU(const Matrix2x2& matrix) { const auto [theta, phi, lambda, phase] = paramsZYZ(matrix); return {.theta = theta, .phi = phi, @@ -429,148 +207,287 @@ EulerAngles EulerDecomposition::paramsU(const Matrix2x2& matrix) { .phase = phase + globalPhaseOffsetForU(phi, lambda)}; } -//===----------------------------------------------------------------------===// -// Euler synthesis (IR emission) -//===----------------------------------------------------------------------===// - -std::optional parseEulerBasis(StringRef basis) { - if (basis.equals_insensitive("zyz")) { - return EulerBasis::ZYZ; - } - if (basis.equals_insensitive("zxz")) { - return EulerBasis::ZXZ; - } - if (basis.equals_insensitive("xzx")) { - return EulerBasis::XZX; - } - if (basis.equals_insensitive("xyx")) { - return EulerBasis::XYX; - } - if (basis.equals_insensitive("u")) { - return EulerBasis::U; - } - if (basis.equals_insensitive("zsxx")) { - return EulerBasis::ZSXX; - } - return std::nullopt; -} - -Value synthesizeUnitary1QEuler(OpBuilder& builder, Location loc, Value qubit, - const Matrix2x2& targetMatrix, - const EulerBasis basis) { - if (basis == EulerBasis::ZSXX) { - const auto [theta, phi, lambda, phase] = - EulerDecomposition::paramsZYZ(targetMatrix); - const auto seq = sequenceFromZYZForZSXX(theta, phi, lambda); - return emitFromZSXXSequence(builder, loc, qubit, seq, - phase + globalPhaseOffsetForZSXX(seq)); - } - - const auto [theta, phi, lambda, phase] = - EulerDecomposition::anglesFromUnitary(targetMatrix, basis); - +/** + * @brief Extracts `(theta, phi, lambda, phase)` for KAK and `U` 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) { switch (basis) { case EulerBasis::ZYZ: + return paramsZYZ(matrix); case EulerBasis::ZXZ: + return paramsZXZ(matrix); case EulerBasis::XZX: + return paramsXZX(matrix); case EulerBasis::XYX: - qubit = emitKAK(builder, loc, qubit, theta, phi, lambda, phase, basis); - break; + return paramsXYX(matrix); case EulerBasis::U: - qubit = UOp::create(builder, loc, qubit, theta, phi, lambda).getQubitOut(); - emitGPhaseIfNeeded(builder, loc, phase); - break; - case EulerBasis::ZSXX: - llvm_unreachable("ZSXX handled above"); + return paramsU(matrix); + default: + llvm::reportFatalInternalError( + "Unsupported Euler basis for angle computation in decomposition!"); } - - return qubit; } +//===----------------------------------------------------------------------===// +// Euler synthesis (plan + emit) +//===----------------------------------------------------------------------===// + +namespace { + /** - * @brief Counts non-identity K-A-K rotations `emitKAK` would emit. + * @brief One gate in a planned single-qubit synthesis sequence. + * + * `RZ`/`RY`/`RX` use @p theta as the rotation angle; `U` uses all three angles. */ -[[nodiscard]] static std::size_t -countKAKGates(const double theta, const double phi, const double lambda) { - std::size_t count = 0; - if (!isNearZeroRotationAngle(lambda)) { - ++count; - } - if (!isNearZeroRotationAngle(theta)) { - ++count; - } - if (!isNearZeroRotationAngle(phi)) { - ++count; +struct SynthesisStep { + enum class Kind : std::uint8_t { RZ, RY, RX, SX, X, U }; + + Kind kind = Kind::RZ; + double theta = 0.0; + double phi = 0.0; + double lambda = 0.0; +}; + +/** @brief Planned single-qubit Euler synthesis (gate list + optional `gphase`). + */ +struct Unitary1QEulerPlan { + llvm::SmallVector steps; + double phase = 0.0; + + /// @brief Number of native gates in the planned sequence (excludes `gphase`). + [[nodiscard]] std::size_t gateCount() const { return steps.size(); } +}; + +/** + * @brief Appends a rotation step when @p angle is outside tolerance. + * + * @param steps Planned gate sequence to extend. + * @param kind Rotation axis (`RZ`, `RY`, or `RX`). + * @param angle Rotation angle in radians. + */ +void appendRotationIf(llvm::SmallVectorImpl& steps, + const SynthesisStep::Kind kind, const double angle) { + if (!isNearZeroRotationAngle(angle)) { + steps.push_back({.kind = kind, .theta = angle}); } - return count; } /** - * @brief Counts non-zero `RZ` slots in a ZSXX sequence angle. + * @brief Appends the three KAK rotations for @p basis to @p steps. + * + * Uses @p angles as outer–middle–outer rotations + * (`K(phi) * A(theta) * K(lambda)` with axes from @p basis). + * + * @param steps Planned gate sequence to extend. + * @param angles Decomposed Euler angles and global phase. + * @param basis Target KAK basis (`ZYZ`, `ZXZ`, `XZX`, or `XYX`). */ -[[nodiscard]] static std::size_t countNonZeroZSXXAngle(double angle) { - constexpr double eps = utils::TOLERANCE; - return isNearZeroRotationAngle(mod2pi(angle, eps)) ? 0 : 1; +void appendKAKSteps(llvm::SmallVectorImpl& steps, + const EulerAngles& angles, const EulerBasis basis) { + using Kind = SynthesisStep::Kind; + // Outer (K) and middle (A) rotation axes per KAK basis. + struct KAKAxes { + Kind outer; + Kind middle; + }; + const auto axes = [&]() -> KAKAxes { + switch (basis) { + case EulerBasis::ZYZ: + return {.outer = Kind::RZ, .middle = Kind::RY}; + case EulerBasis::ZXZ: + return {.outer = Kind::RZ, .middle = Kind::RX}; + case EulerBasis::XZX: + return {.outer = Kind::RX, .middle = Kind::RZ}; + case EulerBasis::XYX: + return {.outer = Kind::RX, .middle = Kind::RY}; + default: + llvm::reportFatalInternalError("Invalid Euler basis for KAK planning"); + } + }(); + + appendRotationIf(steps, axes.outer, angles.lambda); + appendRotationIf(steps, axes.middle, angles.theta); + appendRotationIf(steps, axes.outer, angles.phi); } /** - * @brief Counts basis gates `emitFromZSXXSequence` would emit for `seq`. + * @brief Fills @p plan with an `RZ` / `SX` / `X` gate sequence from Z-Y-Z + * angles. + * + * Implements the canonical ZSXX synthesis cases (identity, `theta = 0`, + * `theta = pi/2`, `theta = pi`, and general) and sets @p plan.phase for any + * global-phase correction. + * + * @param plan Synthesis plan to populate. + * @param zyz Z-Y-Z Euler angles of the target unitary. */ -[[nodiscard]] static std::size_t -countZSXXSequenceGates(const ZSXXSequence& seq) { - switch (seq.middle) { - case ZSXXMiddleGate::OnlyRZ: - return countNonZeroZSXXAngle(seq.firstRZ) + - countNonZeroZSXXAngle(seq.lastRZ); - case ZSXXMiddleGate::OneSX: - case ZSXXMiddleGate::X: - return countNonZeroZSXXAngle(seq.firstRZ) + 1 + - countNonZeroZSXXAngle(seq.lastRZ); - case ZSXXMiddleGate::SXRZSX: - return countNonZeroZSXXAngle(seq.firstRZ) + 1 + - countNonZeroZSXXAngle(seq.midRZ) + 1 + - countNonZeroZSXXAngle(seq.lastRZ); +void planZSXX(Unitary1QEulerPlan& plan, const EulerAngles& zyz) { + constexpr double pi = std::numbers::pi; + constexpr double halfPi = std::numbers::pi / 2.0; + constexpr double quarterPi = std::numbers::pi / 4.0; + + const auto theta = zyz.theta; + const auto phi = zyz.phi; + const auto lambda = zyz.lambda; + const auto pushRZ = [&](const double angle) { + appendRotationIf(plan.steps, SynthesisStep::Kind::RZ, angle); + }; + const auto pushSX = [&] { + plan.steps.push_back({.kind = SynthesisStep::Kind::SX}); + }; + const auto pushX = [&] { + plan.steps.push_back({.kind = SynthesisStep::Kind::X}); + }; + + if (isNearZeroRotationAngle(theta) && isNearZeroRotationAngle(phi) && + isNearZeroRotationAngle(lambda)) { + plan.phase = zyz.phase; + return; } - llvm::reportFatalInternalError("Unhandled ZSXX middle gate in gate count"); -} -std::size_t synthesisGateCount(const Matrix2x2& targetMatrix, - const EulerBasis basis) { - if (basis == EulerBasis::U) { - return 1; + if (isNearZeroRotationAngle(theta)) { + pushRZ(lambda); + pushRZ(phi); + plan.phase = zyz.phase; + return; + } + + if (isNearZeroRotationAngle(theta - halfPi)) { + pushRZ(lambda - halfPi); + pushSX(); + pushRZ(phi + halfPi); + plan.phase = zyz.phase - quarterPi; + return; + } + + if (isNearZeroRotationAngle(theta - pi)) { + pushRZ(lambda); + pushX(); + pushRZ(phi + pi); + plan.phase = zyz.phase - halfPi; + return; } + pushRZ(lambda); + pushSX(); + pushRZ(theta + pi); + pushSX(); + pushRZ(phi + pi); + plan.phase = zyz.phase + halfPi; +} + +/** + * @brief Builds a gate plan for @p targetMatrix in @p basis without emitting + * IR. + * + * @param targetMatrix Single-qubit unitary to synthesize. + * @param basis Native gate basis. + * @return Planned gate sequence and optional global phase. + */ +[[nodiscard]] Unitary1QEulerPlan +planUnitary1QEuler(const Matrix2x2& targetMatrix, const EulerBasis basis) { + Unitary1QEulerPlan plan; if (targetMatrix.isApprox(Matrix2x2::identity())) { - return 0; + return plan; } - switch (basis) { - case EulerBasis::ZYZ: - case EulerBasis::ZXZ: - case EulerBasis::XZX: - case EulerBasis::XYX: { - const auto angles = - EulerDecomposition::anglesFromUnitary(targetMatrix, basis); - return countKAKGates(angles.theta, angles.phi, angles.lambda); + if (basis == EulerBasis::ZSXX) { + planZSXX(plan, anglesFromUnitary(targetMatrix, EulerBasis::ZYZ)); + return plan; } - case EulerBasis::ZSXX: { - const auto zyz = - EulerDecomposition::anglesFromUnitary(targetMatrix, EulerBasis::ZYZ); - const auto seq = sequenceFromZYZForZSXX(zyz.theta, zyz.phi, zyz.lambda); - return countZSXXSequenceGates(seq); + + const EulerAngles angles = anglesFromUnitary(targetMatrix, basis); + plan.phase = angles.phase; + + if (basis == EulerBasis::U) { + plan.steps.push_back({.kind = SynthesisStep::Kind::U, + .theta = angles.theta, + .phi = angles.phi, + .lambda = angles.lambda}); + return plan; } - default: - llvm::reportFatalInternalError( - "Unhandled Euler basis in synthesisGateCount"); + + appendKAKSteps(plan.steps, angles, basis); + return plan; +} + +/** + * @brief Emits the gates described by @p plan and returns the output qubit. + * + * @param builder Builder for the emitted operations. + * @param loc Location for the emitted operations. + * @param qubit Input qubit value. + * @param plan Precomputed synthesis plan. + * @return Qubit value after all planned gates (and `gphase` when needed). + */ +[[nodiscard]] Value emitUnitary1QEulerPlan(OpBuilder& builder, Location loc, + Value qubit, + const Unitary1QEulerPlan& plan) { + for (const SynthesisStep& step : plan.steps) { + switch (step.kind) { + case SynthesisStep::Kind::RZ: + qubit = RZOp::create(builder, loc, qubit, step.theta).getQubitOut(); + break; + case SynthesisStep::Kind::RY: + qubit = RYOp::create(builder, loc, qubit, step.theta).getQubitOut(); + break; + case SynthesisStep::Kind::RX: + qubit = RXOp::create(builder, loc, qubit, step.theta).getQubitOut(); + break; + case SynthesisStep::Kind::SX: + qubit = SXOp::create(builder, loc, qubit).getQubitOut(); + break; + case SynthesisStep::Kind::X: + qubit = XOp::create(builder, loc, qubit).getQubitOut(); + break; + case SynthesisStep::Kind::U: + qubit = + UOp::create(builder, loc, qubit, step.theta, step.phi, step.lambda) + .getQubitOut(); + break; + } } + emitGPhaseIfNeeded(builder, loc, plan.phase); + return qubit; +} + +} // namespace + +std::optional parseEulerBasis(StringRef basis) { + struct EulerBasisName { + const char* name; + EulerBasis value; + }; + constexpr std::array eulerBasisTable{{ + {.name = "zyz", .value = EulerBasis::ZYZ}, + {.name = "zxz", .value = EulerBasis::ZXZ}, + {.name = "xzx", .value = EulerBasis::XZX}, + {.name = "xyx", .value = EulerBasis::XYX}, + {.name = "u", .value = EulerBasis::U}, + {.name = "zsxx", .value = EulerBasis::ZSXX}, + }}; + for (const EulerBasisName& entry : eulerBasisTable) { + if (basis.equals_insensitive(entry.name)) { + return entry.value; + } + } + return std::nullopt; } -bool wouldShortenInBasisRun(const std::size_t runSize, - const Matrix2x2& composed, const EulerBasis basis) { - if (runSize > maxSynthesisGateCount(basis)) { - return true; +std::optional +synthesizeUnitary1QEuler(OpBuilder& builder, Location loc, Value qubit, + const Matrix2x2& composed, const std::size_t runSize, + const bool hasNonBasisGate, const EulerBasis basis) { + const Unitary1QEulerPlan plan = planUnitary1QEuler(composed, basis); + if (!hasNonBasisGate && runSize <= plan.gateCount()) { + return std::nullopt; } - return runSize > synthesisGateCount(composed, basis); + return emitUnitary1QEulerPlan(builder, loc, qubit, plan); } } // namespace mlir::qco::decomposition diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp index 4b255c868a..0317f21ab7 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp @@ -104,7 +104,7 @@ static Matrix2x2 composeRun(ArrayRef run) { /** * @brief Whether `op` is a gate the target `basis` emits. * - * Gate sets match `emitKAK` and `emitFromZSXXSequence` in `Euler.cpp`. Used to + * Gate sets match the synthesis step kinds in `Euler.cpp`. Used to * skip runs that are already in the target basis at canonical length. * * @param op The operation to classify. @@ -186,7 +186,7 @@ struct FuseSingleQubitUnitaryRunsPattern final * @brief Fuses the run anchored at `op` when beneficial. * * Fuses if the run contains a non-basis gate or Euler resynthesis would - * shorten it (`wouldShortenInBasisRun`). + * shorten it (@ref synthesizeUnitary1QEuler). * * @param op The matched unitary operation. * @param rewriter The pattern rewriter. @@ -204,17 +204,17 @@ struct FuseSingleQubitUnitaryRunsPattern final llvm::any_of(run, [&](UnitaryOpInterface member) { return !isTargetBasisGate(member.getOperation(), basis); }); - if (!hasNonBasisGate && - !decomposition::wouldShortenInBasisRun(run.size(), composed, basis)) { - return failure(); - } - OpBuilder::InsertionGuard guard(rewriter); rewriter.setInsertionPoint(op.getOperation()); - const Value qubit = decomposition::synthesizeUnitary1QEuler( - rewriter, op.getLoc(), op.getInputTarget(0), composed, basis); + const std::optional qubitOut = + decomposition::synthesizeUnitary1QEuler( + rewriter, op.getLoc(), op.getInputTarget(0), composed, run.size(), + hasNonBasisGate, basis); + if (!qubitOut) { + return failure(); + } - rewriter.replaceAllUsesWith(run.back().getOutputTarget(0), qubit); + rewriter.replaceAllUsesWith(run.back().getOutputTarget(0), *qubitOut); for (UnitaryOpInterface member : std::ranges::reverse_view(run)) { rewriter.eraseOp(member.getOperation()); } 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 eb8d207ddb..52f68a2682 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -76,7 +76,6 @@ struct SynthesizedCircuit { struct ZSXXShortcutCase { std::string_view label; std::function makeMatrix; - std::size_t expectedSynthesisCount; std::size_t expectedRZ; std::size_t expectedSX; std::size_t expectedX; @@ -307,11 +306,6 @@ static void expectBasisGatesOnly(func::FuncOp funcOp, StringRef basis) { SXOp::getUnitaryMatrix(); } -[[nodiscard]] static std::size_t -expectedFusedGateCount(const Matrix2x2& segment, EulerBasis basis) { - return synthesisGateCount(segment, basis); -} - template static void expectOneQubitGatesAroundBoundary(func::FuncOp funcOp, StringRef basis, @@ -360,6 +354,25 @@ template countOps(funcOp); } +[[nodiscard]] static std::size_t countBasisGates(func::FuncOp funcOp, + EulerBasis basis) { + switch (basis) { + case EulerBasis::ZYZ: + return countOps(funcOp) + countOps(funcOp); + case EulerBasis::ZXZ: + return countOps(funcOp) + countOps(funcOp); + case EulerBasis::XZX: + return countOps(funcOp) + countOps(funcOp); + case EulerBasis::XYX: + return countOps(funcOp) + countOps(funcOp); + case EulerBasis::U: + return countOps(funcOp); + case EulerBasis::ZSXX: + return countZSXXBasisGates(funcOp); + } + return 0; +} + [[nodiscard]] static bool isInsideScfFor(Operation* op) { return op != nullptr && op->getParentOfType() != nullptr; } @@ -459,6 +472,20 @@ static void singleNonBasisGate(QCOProgramBuilder& b) { q[0] = b.h(q[0]); } +// Single `X` in `zsxx` basis — already at canonical synthesis length (1). +static void singlePauliX(QCOProgramBuilder& b) { + auto q = b.allocQubitRegister(1); + q[0] = b.x(q[0]); +} + +// One `RZ`/`RY`/`RZ` triple in `zyz` basis — already at canonical length (3). +static void canonicalZYZRun(QCOProgramBuilder& b) { + auto q = b.allocQubitRegister(1); + q[0] = b.rz(0.3, q[0]); + q[0] = b.ry(0.5, q[0]); + q[0] = b.rz(0.7, q[0]); +} + // Six `RZ`/`RY` gates in `zyz` basis — longer than canonical (3). static void overlongZYZRun(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); @@ -544,7 +571,13 @@ synthesizeMatrix(MLIRContext* ctx, const Matrix2x2& matrix, EulerBasis basis) { builder.setInsertionPointToStart(entry); Value q = entry->getArgument(0); - q = synthesizeUnitary1QEuler(builder, module->getLoc(), q, matrix, basis); + const std::optional qubitOut = synthesizeUnitary1QEuler( + builder, module->getLoc(), q, matrix, 0, true, basis); + if (!qubitOut) { + llvm::report_fatal_error( + "synthesizeUnitary1QEuler failed during test synthesis"); + } + q = *qubitOut; builder.create(module->getLoc(), q); return SynthesizedCircuit{.module = std::move(module), .func = func}; } @@ -559,6 +592,12 @@ static void expectSynthesizedMatrix(MLIRContext* ctx, const Matrix2x2& matrix, expectMatrixPreserved(circuit.func, matrix, "synthesis"); } +[[nodiscard]] static std::size_t +expectedFusedGateCount(MLIRContext* ctx, const Matrix2x2& segment, + EulerBasis basis) { + return countBasisGates(synthesizeMatrix(ctx, segment, basis).func, basis); +} + static LogicalResult runFuse(ModuleOp module, StringRef basis) { PassManager pm(module.getContext()); qco::FuseSingleQubitUnitaryRunsOptions opts; @@ -602,72 +641,13 @@ static void runFuseOnProgramForAllBases(MLIRContext* ctx, }); } -TEST(EulerDecompositionTest, ZYZAnglesFromUnitaryReconstructHadamard) { - SynthesisFixture fx; - fx.setUp(); - - const Matrix2x2 hadamard = HOp::getUnitaryMatrix(); - const auto [theta, phi, lambda, phase] = - EulerDecomposition::anglesFromUnitary(hadamard, EulerBasis::ZYZ); - - auto module = ModuleOp::create(UnknownLoc::get(fx.context.get())); - OpBuilder builder(fx.context.get()); - builder.setInsertionPointToStart(module.getBody()); - const Location loc = module.getLoc(); - - auto qubitTy = QubitType::get(fx.context.get()); - auto funcTy = builder.getFunctionType({qubitTy}, {qubitTy}); - auto func = builder.create(loc, "main", funcTy); - auto* entry = func.addEntryBlock(); - builder.setInsertionPointToStart(entry); - - Value q = entry->getArgument(0); - auto mkAngle = [&](double angle) -> Value { - return builder - .create(loc, builder.getF64FloatAttr(angle)) - .getResult(); - }; - q = builder.create(loc, q, mkAngle(lambda)).getQubitOut(); - q = builder.create(loc, q, mkAngle(theta)).getQubitOut(); - q = builder.create(loc, q, mkAngle(phi)).getQubitOut(); - if (std::abs(phase) > mlir::utils::TOLERANCE) { - Value phaseVal = mkAngle(phase); - builder.create(loc, phaseVal); - } - builder.create(loc, q); - - ASSERT_TRUE(succeeded(verify(module))); - EXPECT_TRUE( - compute1QMatrixFromFunction(func).isApprox(hadamard, MATRIX_TOLERANCE)); -} - -TEST(EulerSynthesisTest, ProfitabilityAndIdentityGateCount) { +TEST(EulerSynthesisTest, IdentityGateCount) { SynthesisFixture fx; fx.setUp(); const Matrix2x2 identity = Matrix2x2::identity(); - EXPECT_TRUE(wouldShortenInBasisRun(6, identity, EulerBasis::ZYZ)); - EXPECT_TRUE(wouldShortenInBasisRun(2, identity, EulerBasis::ZYZ)); - EXPECT_FALSE( - wouldShortenInBasisRun(1, XOp::getUnitaryMatrix(), EulerBasis::ZSXX)); - EXPECT_EQ(synthesisGateCount(identity, EulerBasis::ZYZ), 0U); -} - -TEST(EulerSynthesisTest, ClassifyZSXXMiddleFromZYZThetaBoundaries) { - using decomposition::classifyZSXXMiddleFromZYZTheta; - using decomposition::ZSXXMiddleGate; - - constexpr double halfPi = std::numbers::pi / 2.0; - constexpr double pi = std::numbers::pi; - constexpr double tol = 0.5 * mlir::utils::TOLERANCE; - - EXPECT_EQ(classifyZSXXMiddleFromZYZTheta(tol), ZSXXMiddleGate::OnlyRZ); - EXPECT_EQ(classifyZSXXMiddleFromZYZTheta(mlir::utils::TOLERANCE), - ZSXXMiddleGate::OnlyRZ); - EXPECT_EQ(classifyZSXXMiddleFromZYZTheta(halfPi + tol), - ZSXXMiddleGate::OneSX); - EXPECT_EQ(classifyZSXXMiddleFromZYZTheta(pi - tol), ZSXXMiddleGate::X); - EXPECT_EQ(classifyZSXXMiddleFromZYZTheta(pi), ZSXXMiddleGate::X); + EXPECT_EQ(expectedFusedGateCount(fx.context.get(), identity, EulerBasis::ZYZ), + 0U); } TEST_P(ZSXXShortcutTest, SynthesisMatchesGateCount) { @@ -676,8 +656,6 @@ TEST_P(ZSXXShortcutTest, SynthesisMatchesGateCount) { const auto& testCase = GetParam(); const Matrix2x2 matrix = testCase.makeMatrix(fx.context.get()); - EXPECT_EQ(synthesisGateCount(matrix, EulerBasis::ZSXX), - testCase.expectedSynthesisCount); expectSynthesizedMatrix( fx.context.get(), matrix, EulerBasis::ZSXX, @@ -686,50 +664,59 @@ TEST_P(ZSXXShortcutTest, SynthesisMatchesGateCount) { EXPECT_EQ(countOps(funcOp), testCase.expectedSX); EXPECT_EQ(countOps(funcOp), testCase.expectedX); EXPECT_EQ(countZSXXBasisGates(funcOp), - expectedFusedGateCount(original, EulerBasis::ZSXX)); + expectedFusedGateCount(fx.context.get(), original, + EulerBasis::ZSXX)); }); } INSTANTIATE_TEST_SUITE_P( ZSXXShortcuts, ZSXXShortcutTest, - testing::Values(ZSXXShortcutCase{"PauliX", - [](MLIRContext*) -> Matrix2x2 { - return XOp::getUnitaryMatrix(); - }, - 1, 0, 0, 1}, - ZSXXShortcutCase{"PureZ", - [](MLIRContext*) -> Matrix2x2 { - return rzMatrix(0.3) * rzMatrix(0.7); - }, - 2, 2, 0, 0}, - ZSXXShortcutCase{"RYHalfPi", - [](MLIRContext* ctx) -> Matrix2x2 { - return rotationMatrix( - ctx, std::numbers::pi / 2.0); - }, - 3, 2, 1, 0}, - ZSXXShortcutCase{"RYNearHalfPi", - [](MLIRContext* ctx) -> Matrix2x2 { - return rotationMatrix( - ctx, - (std::numbers::pi / 2.0) + - (0.5 * mlir::utils::TOLERANCE)); - }, - 3, 2, 1, 0}, - ZSXXShortcutCase{"RYNearZero", - [](MLIRContext* ctx) -> Matrix2x2 { - return rotationMatrix( - ctx, 0.5 * mlir::utils::TOLERANCE); - }, - 0, 0, 0, 0}, - ZSXXShortcutCase{"RYNearPi", - [](MLIRContext* ctx) -> Matrix2x2 { - return rotationMatrix( - ctx, - std::numbers::pi - - (0.5 * mlir::utils::TOLERANCE)); - }, - 2, 1, 0, 1}), + testing::Values( + ZSXXShortcutCase{ + "Identity", + [](MLIRContext*) -> Matrix2x2 { return Matrix2x2::identity(); }, 0, + 0, 0}, + ZSXXShortcutCase{ + "PauliX", + [](MLIRContext*) -> Matrix2x2 { return XOp::getUnitaryMatrix(); }, + 0, 0, 1}, + ZSXXShortcutCase{"PureZ", + [](MLIRContext*) -> Matrix2x2 { + return rzMatrix(0.3) * rzMatrix(0.7); + }, + 2, 0, 0}, + ZSXXShortcutCase{"ZYZNearZeroTheta", + [](MLIRContext*) -> Matrix2x2 { + constexpr double tol = 0.5 * mlir::utils::TOLERANCE; + return rzMatrix(0.4) * ryMatrix(tol) * rzMatrix(0.3); + }, + 2, 0, 0}, + ZSXXShortcutCase{"RYHalfPi", + [](MLIRContext* ctx) -> Matrix2x2 { + return rotationMatrix(ctx, + std::numbers::pi / 2.0); + }, + 2, 1, 0}, + ZSXXShortcutCase{"RYNearHalfPi", + [](MLIRContext* ctx) -> Matrix2x2 { + return rotationMatrix( + ctx, (std::numbers::pi / 2.0) + + (0.5 * mlir::utils::TOLERANCE)); + }, + 2, 1, 0}, + ZSXXShortcutCase{"RYNearZero", + [](MLIRContext* ctx) -> Matrix2x2 { + return rotationMatrix( + ctx, 0.5 * mlir::utils::TOLERANCE); + }, + 0, 0, 0}, + ZSXXShortcutCase{"RYNearPi", + [](MLIRContext* ctx) -> Matrix2x2 { + return rotationMatrix( + ctx, std::numbers::pi - + (0.5 * mlir::utils::TOLERANCE)); + }, + 1, 0, 1}), [](const testing::TestParamInfo& info) { return std::string(info.param.label); }); @@ -745,18 +732,22 @@ TEST_P(EulerSynthesisExactTest, ReconstructsReferenceMatrices) { const auto [basis, matrixFn] = GetParam(); const Matrix2x2 original = matrixFn(fx.context.get()); - expectSynthesizedMatrix(fx.context.get(), original, basis, - [&](func::FuncOp funcOp, const Matrix2x2& matrix) { - if (basis == EulerBasis::U) { - EXPECT_EQ(countOps(funcOp), - expectedFusedGateCount(matrix, basis)); - } - if (basis == EulerBasis::ZYZ && - matrix.isApprox(Matrix2x2::identity())) { - EXPECT_EQ(countOps(funcOp), 0U); - EXPECT_EQ(countOps(funcOp), 0U); - } - }); + expectSynthesizedMatrix( + fx.context.get(), original, basis, + [&](func::FuncOp funcOp, const Matrix2x2& matrix) { + if (basis == EulerBasis::U) { + EXPECT_EQ(countOps(funcOp), + expectedFusedGateCount(fx.context.get(), matrix, basis)); + } + if (basis == EulerBasis::ZYZ && + matrix.isApprox(Matrix2x2::identity())) { + EXPECT_EQ(countOps(funcOp), 0U); + EXPECT_EQ(countOps(funcOp), 0U); + } + if (basis == EulerBasis::U && matrix.isApprox(Matrix2x2::identity())) { + EXPECT_EQ(countOps(funcOp), 0U); + } + }); } INSTANTIATE_TEST_SUITE_P( @@ -856,9 +847,45 @@ TEST(FuseSingleQubitUnitaryRunsTest, FusesOverlongInBasisRun) { [](func::FuncOp funcOp, const Matrix2x2&) { ASSERT_EQ(countOps(funcOp) + countOps(funcOp), 6U); }, - [](func::FuncOp funcOp, const Matrix2x2& original) { + [&](func::FuncOp funcOp, const Matrix2x2& original) { + EXPECT_EQ(countOps(funcOp) + countOps(funcOp), + expectedFusedGateCount(fx.context.get(), original, + EulerBasis::ZYZ)); + expectMatrixPreserved(funcOp, original, "zyz"); + expectBasisGatesOnly(funcOp, "zyz"); + }); +} + +TEST(FuseSingleQubitUnitaryRunsTest, DoesNotFuseCanonicalInBasisRun) { + SynthesisFixture fx; + fx.setUp(); + + runFuseOnProgram( + fx.context.get(), &singlePauliX, "zsxx", + [](func::FuncOp funcOp, const Matrix2x2&) { + ASSERT_EQ(countOps(funcOp), 1U); + EXPECT_EQ(countOps(funcOp), 0U); + EXPECT_EQ(countOps(funcOp), 0U); + }, + [&](func::FuncOp funcOp, const Matrix2x2& original) { + EXPECT_EQ(countOps(funcOp), 1U); + EXPECT_EQ(countZSXXBasisGates(funcOp), + expectedFusedGateCount(fx.context.get(), original, + EulerBasis::ZSXX)); + expectMatrixPreserved(funcOp, original, "zsxx"); + expectBasisGatesOnly(funcOp, "zsxx"); + }); + + runFuseOnProgram( + fx.context.get(), &canonicalZYZRun, "zyz", + [](func::FuncOp funcOp, const Matrix2x2&) { + ASSERT_EQ(countOps(funcOp) + countOps(funcOp), 3U); + }, + [&](func::FuncOp funcOp, const Matrix2x2& original) { + EXPECT_EQ(countOps(funcOp) + countOps(funcOp), 3U); EXPECT_EQ(countOps(funcOp) + countOps(funcOp), - expectedFusedGateCount(original, EulerBasis::ZYZ)); + expectedFusedGateCount(fx.context.get(), original, + EulerBasis::ZYZ)); expectMatrixPreserved(funcOp, original, "zyz"); expectBasisGatesOnly(funcOp, "zyz"); }); @@ -871,8 +898,9 @@ TEST(FuseSingleQubitUnitaryRunsTest, runFuseOnProgram( fx.context.get(), &overlongZSXXMixedPureZRun, "zsxx", - [](func::FuncOp funcOp, const Matrix2x2& /*original*/) { - EXPECT_EQ(expectedFusedGateCount(overlongZSXXPureZRunMatrix(), + [&](func::FuncOp funcOp, const Matrix2x2& /*original*/) { + EXPECT_EQ(expectedFusedGateCount(fx.context.get(), + overlongZSXXPureZRunMatrix(), EulerBasis::ZSXX), 1U); ASSERT_EQ(countZSXXBasisGates(funcOp), 3U); @@ -880,12 +908,13 @@ TEST(FuseSingleQubitUnitaryRunsTest, EXPECT_EQ(countOps(funcOp), 1U); EXPECT_EQ(countOps(funcOp), 0U); }, - [](func::FuncOp funcOp, const Matrix2x2& original) { + [&](func::FuncOp funcOp, const Matrix2x2& original) { EXPECT_EQ(countOps(funcOp), 1U); EXPECT_EQ(countOps(funcOp), 0U); EXPECT_EQ(countOps(funcOp), 0U); EXPECT_EQ(countZSXXBasisGates(funcOp), - expectedFusedGateCount(overlongZSXXPureZRunMatrix(), + expectedFusedGateCount(fx.context.get(), + overlongZSXXPureZRunMatrix(), EulerBasis::ZSXX)); expectMatrixPreserved(funcOp, original, "zsxx"); expectBasisGatesOnly(funcOp, "zsxx"); @@ -912,8 +941,10 @@ TEST(FuseSingleQubitUnitaryRunsTest, DoesNotFuseAcrossTwoQGateAllBases) { ASSERT_TRUE(parsed) << "basis=" << basis.str(); expectOneQubitGatesAroundBoundary( funcOp, basis, [](Operation& op) { return isTwoQubitGate(op); }, - expectedFusedGateCount(splitFixtureHTSegmentMatrix(), *parsed), - expectedFusedGateCount(splitFixtureRZSXSegmentMatrix(), *parsed)); + expectedFusedGateCount(fx.context.get(), + splitFixtureHTSegmentMatrix(), *parsed), + expectedFusedGateCount(fx.context.get(), + splitFixtureRZSXSegmentMatrix(), *parsed)); }); } @@ -931,8 +962,10 @@ TEST(FuseSingleQubitUnitaryRunsTest, DoesNotFuseAcrossBarrierAllBases) { ASSERT_TRUE(parsed) << "basis=" << basis.str(); expectOneQubitGatesAroundBoundary( funcOp, basis, [](Operation& op) { return isa(op); }, - expectedFusedGateCount(splitFixtureHTSegmentMatrix(), *parsed), - expectedFusedGateCount(splitFixtureRZSXSegmentMatrix(), *parsed)); + expectedFusedGateCount(fx.context.get(), + splitFixtureHTSegmentMatrix(), *parsed), + expectedFusedGateCount(fx.context.get(), + splitFixtureRZSXSegmentMatrix(), *parsed)); }); } @@ -947,11 +980,12 @@ TEST(FuseSingleQubitUnitaryRunsTest, EliminatesIdentityInvMultiOpBody) { EXPECT_EQ(countOps(funcOp), 1U); EXPECT_EQ(countOps(funcOp), 0U); }, - [](func::FuncOp funcOp, const Matrix2x2& original) { + [&](func::FuncOp funcOp, const Matrix2x2& original) { EXPECT_EQ(countOps(funcOp), 0U); EXPECT_EQ(countOps(funcOp), 0U); - EXPECT_EQ(countOps(funcOp), - expectedFusedGateCount(original, EulerBasis::U)); + EXPECT_EQ( + countOps(funcOp), + expectedFusedGateCount(fx.context.get(), original, EulerBasis::U)); expectMatrixPreserved(funcOp, original, "x-inv-xx-x"); }); } @@ -1037,7 +1071,9 @@ TEST(FuseSingleQubitUnitaryRunsTest, DoesNotFuseAcrossScfForAllBases) { ASSERT_TRUE(parsed) << "basis=" << basis.str(); expectOneQubitGatesInAndOutsideScfFor( funcOp, basis, - expectedFusedGateCount(splitFixtureHTSegmentMatrix(), *parsed), - expectedFusedGateCount(splitFixtureRZSXSegmentMatrix(), *parsed)); + expectedFusedGateCount(fx.context.get(), + splitFixtureHTSegmentMatrix(), *parsed), + expectedFusedGateCount(fx.context.get(), + splitFixtureRZSXSegmentMatrix(), *parsed)); }); } From b8b6616ee8198511df4d03f70de98175f68d94fc Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 15 Jun 2026 16:20:26 +0200 Subject: [PATCH 48/68] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Improve=20fusable=20?= =?UTF-8?q?run=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FuseSingleQubitUnitaryRuns.cpp | 178 ++++++++++-------- 1 file changed, 102 insertions(+), 76 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp index 0317f21ab7..714fb68478 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp @@ -14,9 +14,8 @@ #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" #include "mlir/Dialect/QCO/Utils/WireIterator.h" +#include "mlir/Dialect/Utils/Utils.h" -#include -#include #include #include // IWYU pragma: keep (Passes.h.inc) #include @@ -27,9 +26,9 @@ #include #include +#include #include #include -#include #include namespace mlir::qco { @@ -38,67 +37,59 @@ namespace mlir::qco { #include "mlir/Dialect/QCO/Transforms/Passes.h.inc" /** - * @brief Whether `op` is inside an `inv` body. + * @brief Whether `op` has a compile-time 2x2 unitary without synthesizing it. * - * @param op The operation to test. - * @return `true` if any ancestor is `inv`. - */ -static bool isInsideInvBody(Operation* op) { - return op != nullptr && op->getParentOfType() != nullptr; -} - -/** - * @brief Returns the compile-time 2x2 unitary matrix of `op`, if available. + * For parameterized gates, only checks that operands are `arith.constant` + * scalars. Other static single-qubit gates need no matrix query. `inv` is the + * only parameter-free fuse candidate that may still lack a compile-time matrix. * - * @param op The unitary operation to query. - * @return The matrix, or `std::nullopt` if not known at compile time. + * @param op The unitary operation to test. + * @return `true` when `getUnitaryMatrix()` would succeed. */ -static std::optional getConstMatrix(UnitaryOpInterface op) { - Matrix2x2 matrix; - if (!op.getUnitaryMatrix2x2(matrix)) { - return std::nullopt; +static bool hasCompileTimeUnitaryMatrix2x2(UnitaryOpInterface op) { + for (size_t i = 0; i < op.getNumParams(); ++i) { + if (!mlir::utils::valueToDouble(op.getParameter(i))) { + return false; + } + } + if (op.getNumParams() > 0 || !isa(op.getOperation())) { + return true; } - return matrix; + Matrix2x2 unused; + return op.getUnitaryMatrix2x2(unused); } /** - * @brief Whether `op` may participate in a fusable single-qubit run. + * @brief Whether `op` is a fusable single-qubit run member on the wire. * * @param op The unitary operation to test. - * @return `true` for a single-qubit, matrix-backed unitary on the wire, - * including inside `inv`/`ctrl` bodies. + * @return `true` for a single-qubit unitary with a compile-time 2x2 matrix, + * excluding barriers. */ -static bool isFuseCandidate(UnitaryOpInterface op) { - if (!op || !op.isSingleQubit() || isa(op.getOperation())) { - return false; - } - return getConstMatrix(op).has_value(); +static bool isRunMember(UnitaryOpInterface op) { + return op && op.isSingleQubit() && !isa(op.getOperation()) && + hasCompileTimeUnitaryMatrix2x2(op); } /** - * @brief Whether `op` can participate in a fusable run. + * @brief Whether `op` is a fusable single-qubit run member on the wire. * * @param op The operation to test. - * @return `true` for a fuse candidate with a known compile-time matrix. + * @return `true` for a single-qubit unitary with a compile-time 2x2 matrix, + * excluding barriers. */ static bool isRunMember(Operation* op) { - auto iface = dyn_cast(op); - return iface && isFuseCandidate(iface) && getConstMatrix(iface).has_value(); + return isRunMember(dyn_cast(op)); } /** - * @brief Composes a run of unitary ops into a single matrix. + * @brief Whether @p op sits in the body region of an `inv`. * - * @param run The run members in circuit order. - * @return The product of their matrices. + * Run heads are not started inside `inv` bodies; the enclosing `InvOp` already + * exposes the composed body unitary on the parent wire. */ -static Matrix2x2 composeRun(ArrayRef run) { - Matrix2x2 composed = Matrix2x2::identity(); - for (auto op : run) { - // First gate in the run is applied first (left factor). - composed = (*getConstMatrix(op)) * composed; - } - return composed; +static bool isInsideInvBody(Operation* op) { + return op != nullptr && op->getParentOfType() != nullptr; } /** @@ -130,6 +121,67 @@ static bool isTargetBasisGate(Operation* op, decomposition::EulerBasis basis) { .Default([](auto) { return false; }); } +/** + * @brief Composed unitary and metadata for a fusable run, without storing ops. + */ +struct FusableRunScan { + Matrix2x2 composed = Matrix2x2::identity(); + std::size_t gateCount = 0; + bool hasNonBasisGate = false; + UnitaryOpInterface tail; +}; + +/** + * @brief Walks the wire from @p head, composing matrices and run metadata. + * + * `WireIterator` stops at region boundaries, so members are consecutive on the + * wire. Only @p tail is retained for replacement and erasure. + * + * @param head First gate of the run. + * @param basis Target Euler basis (for non-basis detection). + * @return Composed matrix, gate count, and run tail. + */ +static FusableRunScan scanFusableRun(UnitaryOpInterface head, + decomposition::EulerBasis basis) { + FusableRunScan scan; + const auto accumulate = [&](UnitaryOpInterface member) { + const auto matrix = member.getUnitaryMatrix(); + assert(matrix && "run member must have a compile-time 2x2 matrix"); + scan.composed.premultiplyBy(*matrix); + scan.hasNonBasisGate |= !isTargetBasisGate(member.getOperation(), basis); + scan.tail = member; + ++scan.gateCount; + }; + + accumulate(head); + for (WireIterator it = std::next(WireIterator(head.getOutputTarget(0))); + it != std::default_sentinel; ++it) { + Operation* memberOp = it.operation(); + if (!isRunMember(memberOp)) { + break; + } + accumulate(cast(memberOp)); + } + return scan; +} + +/** + * @brief Erases a contiguous run from tail back to @p head. + */ +static void eraseFusableRun(PatternRewriter& rewriter, UnitaryOpInterface head, + UnitaryOpInterface tail) { + // Erase tail-first so each op is dead (its successor is already gone) when + // removed; capture the predecessor before erasing the current op. + UnitaryOpInterface current = tail; + while (current.getOperation() != head.getOperation()) { + auto pred = + cast(current.getInputTarget(0).getDefiningOp()); + rewriter.eraseOp(current.getOperation()); + current = pred; + } + rewriter.eraseOp(head.getOperation()); +} + namespace { /** @@ -149,10 +201,11 @@ struct FuseSingleQubitUnitaryRunsPattern final * @brief Whether `op` is the head of a run. * * @param op The candidate run head. - * @return `true` if the wire predecessor is not a run member. + * @return `true` if `op` is a run member outside any `inv` body whose wire + * predecessor is not a run member. */ static bool isRunStart(UnitaryOpInterface op) { - if (!isRunMember(op.getOperation())) { + if (!isRunMember(op)) { return false; } if (isInsideInvBody(op.getOperation())) { @@ -162,26 +215,6 @@ struct FuseSingleQubitUnitaryRunsPattern final return pred == nullptr || !isRunMember(pred); } - /** - * @brief Collects the maximal fusable run starting at `start`. - * - * @param start The run head. - * @return The run members in circuit order. - */ - static SmallVector collectRun(UnitaryOpInterface start) { - SmallVector run{start}; - Block* block = start->getBlock(); - for (WireIterator it = std::next(WireIterator(start.getOutputTarget(0))); - it != std::default_sentinel; ++it) { - Operation* op = it.operation(); - if (op->getBlock() != block || !isRunMember(op)) { - break; - } - run.emplace_back(cast(op)); - } - return run; - } - /** * @brief Fuses the run anchored at `op` when beneficial. * @@ -198,26 +231,19 @@ struct FuseSingleQubitUnitaryRunsPattern final return failure(); } - auto run = collectRun(op); - const Matrix2x2 composed = composeRun(run); - const bool hasNonBasisGate = - llvm::any_of(run, [&](UnitaryOpInterface member) { - return !isTargetBasisGate(member.getOperation(), basis); - }); + FusableRunScan run = scanFusableRun(op, basis); OpBuilder::InsertionGuard guard(rewriter); rewriter.setInsertionPoint(op.getOperation()); const std::optional qubitOut = decomposition::synthesizeUnitary1QEuler( - rewriter, op.getLoc(), op.getInputTarget(0), composed, run.size(), - hasNonBasisGate, basis); + rewriter, op.getLoc(), op.getInputTarget(0), run.composed, + run.gateCount, run.hasNonBasisGate, basis); if (!qubitOut) { return failure(); } - rewriter.replaceAllUsesWith(run.back().getOutputTarget(0), *qubitOut); - for (UnitaryOpInterface member : std::ranges::reverse_view(run)) { - rewriter.eraseOp(member.getOperation()); - } + rewriter.replaceAllUsesWith(run.tail.getOutputTarget(0), *qubitOut); + eraseFusableRun(rewriter, op, run.tail); return success(); } }; From 257bc0ffa5ec06ba72b71d163040c577f5708ad8 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 15 Jun 2026 16:46:57 +0200 Subject: [PATCH 49/68] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20linter=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp | 2 +- .../QCO/Transforms/Decomposition/Euler.cpp | 27 +++++++++++-------- .../FuseSingleQubitUnitaryRuns.cpp | 25 ++++++++++------- .../test_euler_decomposition.cpp | 1 + .../Dialect/QCO/Utils/test_unitary_matrix.cpp | 1 + 5 files changed, 34 insertions(+), 22 deletions(-) diff --git a/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp b/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp index 5cf179af01..a08bc352d6 100644 --- a/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp @@ -434,7 +434,7 @@ composeInvertedSingleQubitBodyMatrix(Block& block) { }) .Default([](Operation* operation) { const auto usesQubit = [](Value value) { - return llvm::isa(value.getType()); + return isa(value.getType()); }; return !llvm::any_of(operation->getOperands(), usesQubit) && !llvm::any_of(operation->getResults(), usesQubit); diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index e81631d85e..9fe9455ea0 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -120,6 +120,8 @@ static void emitGPhaseIfNeeded(OpBuilder& builder, Location loc, double phase) { * @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. @@ -127,6 +129,8 @@ struct EulerAngles { double phase = 0.0; ///< Global phase in radians. }; +} // namespace + /** * @brief Z-Y-Z Euler angles and global phase for a 2x2 unitary. * @@ -263,6 +267,8 @@ struct Unitary1QEulerPlan { [[nodiscard]] std::size_t gateCount() const { return steps.size(); } }; +} // namespace + /** * @brief Appends a rotation step when @p angle is outside tolerance. * @@ -270,8 +276,9 @@ struct Unitary1QEulerPlan { * @param kind Rotation axis (`RZ`, `RY`, or `RX`). * @param angle Rotation angle in radians. */ -void appendRotationIf(llvm::SmallVectorImpl& steps, - const SynthesisStep::Kind kind, const double angle) { +static void appendRotationIf(llvm::SmallVectorImpl& steps, + const SynthesisStep::Kind kind, + const double angle) { if (!isNearZeroRotationAngle(angle)) { steps.push_back({.kind = kind, .theta = angle}); } @@ -287,8 +294,8 @@ void appendRotationIf(llvm::SmallVectorImpl& steps, * @param angles Decomposed Euler angles and global phase. * @param basis Target KAK basis (`ZYZ`, `ZXZ`, `XZX`, or `XYX`). */ -void appendKAKSteps(llvm::SmallVectorImpl& steps, - const EulerAngles& angles, const EulerBasis basis) { +static void appendKAKSteps(llvm::SmallVectorImpl& steps, + const EulerAngles& angles, const EulerBasis basis) { using Kind = SynthesisStep::Kind; // Outer (K) and middle (A) rotation axes per KAK basis. struct KAKAxes { @@ -326,7 +333,7 @@ void appendKAKSteps(llvm::SmallVectorImpl& steps, * @param plan Synthesis plan to populate. * @param zyz Z-Y-Z Euler angles of the target unitary. */ -void planZSXX(Unitary1QEulerPlan& plan, const EulerAngles& zyz) { +static void planZSXX(Unitary1QEulerPlan& plan, const EulerAngles& zyz) { constexpr double pi = std::numbers::pi; constexpr double halfPi = std::numbers::pi / 2.0; constexpr double quarterPi = std::numbers::pi / 4.0; @@ -389,7 +396,7 @@ void planZSXX(Unitary1QEulerPlan& plan, const EulerAngles& zyz) { * @param basis Native gate basis. * @return Planned gate sequence and optional global phase. */ -[[nodiscard]] Unitary1QEulerPlan +[[nodiscard]] static Unitary1QEulerPlan planUnitary1QEuler(const Matrix2x2& targetMatrix, const EulerBasis basis) { Unitary1QEulerPlan plan; if (targetMatrix.isApprox(Matrix2x2::identity())) { @@ -425,9 +432,9 @@ planUnitary1QEuler(const Matrix2x2& targetMatrix, const EulerBasis basis) { * @param plan Precomputed synthesis plan. * @return Qubit value after all planned gates (and `gphase` when needed). */ -[[nodiscard]] Value emitUnitary1QEulerPlan(OpBuilder& builder, Location loc, - Value qubit, - const Unitary1QEulerPlan& plan) { +[[nodiscard]] static Value +emitUnitary1QEulerPlan(OpBuilder& builder, Location loc, Value qubit, + const Unitary1QEulerPlan& plan) { for (const SynthesisStep& step : plan.steps) { switch (step.kind) { case SynthesisStep::Kind::RZ: @@ -456,8 +463,6 @@ planUnitary1QEuler(const Matrix2x2& targetMatrix, const EulerBasis basis) { return qubit; } -} // namespace - std::optional parseEulerBasis(StringRef basis) { struct EulerBasisName { const char* name; diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp index 714fb68478..bf8c66b220 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp @@ -27,6 +27,7 @@ #include #include +#include #include #include #include @@ -36,6 +37,20 @@ namespace mlir::qco { #define GEN_PASS_DEF_FUSESINGLEQUBITUNITARYRUNS #include "mlir/Dialect/QCO/Transforms/Passes.h.inc" +namespace { + +/** + * @brief Composed unitary and metadata for a fusable run, without storing ops. + */ +struct FusableRunScan { + Matrix2x2 composed = Matrix2x2::identity(); + std::size_t gateCount = 0; + bool hasNonBasisGate = false; + UnitaryOpInterface tail; +}; + +} // namespace + /** * @brief Whether `op` has a compile-time 2x2 unitary without synthesizing it. * @@ -121,16 +136,6 @@ static bool isTargetBasisGate(Operation* op, decomposition::EulerBasis basis) { .Default([](auto) { return false; }); } -/** - * @brief Composed unitary and metadata for a fusable run, without storing ops. - */ -struct FusableRunScan { - Matrix2x2 composed = Matrix2x2::identity(); - std::size_t gateCount = 0; - bool hasNonBasisGate = false; - UnitaryOpInterface tail; -}; - /** * @brief Walks the wire from @p head, composing matrices and run metadata. * 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 52f68a2682..f639e36906 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include diff --git a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp index 58649975be..afa0792415 100644 --- a/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp +++ b/mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp @@ -12,6 +12,7 @@ #include +#include #include #include From 9c1a6593b7321aca700363674308f7724239c4e5 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 15 Jun 2026 17:25:10 +0200 Subject: [PATCH 50/68] =?UTF-8?q?=F0=9F=8E=A8=20Refactor=20Euler=20decompo?= =?UTF-8?q?sition=20and=20fuse=20single-qubit=20run=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/Decomposition/Euler.cpp | 16 +-- .../FuseSingleQubitUnitaryRuns.cpp | 104 ++++++++---------- .../test_euler_decomposition.cpp | 53 +++++++++ 3 files changed, 103 insertions(+), 70 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index 9fe9455ea0..45ed9ab1e2 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -100,18 +100,6 @@ static void emitGPhaseIfNeeded(OpBuilder& builder, Location loc, double phase) { GPhaseOp::create(builder, loc, phase); } -/** - * @brief Global phase offset of `UOp` vs `RZ(phi)*RY(theta)*RZ(lambda)`. - * - * @param phi Middle-axis angle from Z-Y-Z decomposition. - * @param lambda Outer-axis angle from Z-Y-Z decomposition. - * @return Phase correction to add to the Z-Y-Z global phase. - */ -[[nodiscard]] static double globalPhaseOffsetForU(const double phi, - const double lambda) { - return -0.5 * (phi + lambda); -} - //===----------------------------------------------------------------------===// // Euler decomposition (angles) //===----------------------------------------------------------------------===// @@ -204,11 +192,13 @@ struct EulerAngles { * @return `U`-gate angles and global phase. */ [[nodiscard]] static EulerAngles paramsU(const Matrix2x2& matrix) { + // `U` differs from RZ(phi)*RY(theta)*RZ(lambda) by a global phase of + // -(phi + lambda)/2. const auto [theta, phi, lambda, phase] = paramsZYZ(matrix); return {.theta = theta, .phi = phi, .lambda = lambda, - .phase = phase + globalPhaseOffsetForU(phi, lambda)}; + .phase = phase - (0.5 * (phi + lambda))}; } /** diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp index bf8c66b220..b3b0a08df4 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -52,66 +53,44 @@ struct FusableRunScan { } // namespace /** - * @brief Whether `op` has a compile-time 2x2 unitary without synthesizing it. + * @brief Whether `op` can take part in a fusable single-qubit run. * - * For parameterized gates, only checks that operands are `arith.constant` - * scalars. Other static single-qubit gates need no matrix query. `inv` is the - * only parameter-free fuse candidate that may still lack a compile-time matrix. + * A run member is a non-barrier, single-qubit unitary whose 2x2 matrix is known + * at compile time. Parameterized gates only need constant parameters (no matrix + * is built); `inv` is the only parameter-free op that may still lack a constant + * matrix, so it is queried directly. An `inv` that hides a barrier in its body + * is rejected: its 2x2 matrix ignores the barrier, so absorbing the modifier + * would silently drop it. Such bodies instead fuse around the barrier in place. * - * @param op The unitary operation to test. - * @return `true` when `getUnitaryMatrix()` would succeed. + * @param op The operation to test. May be null (e.g. a missing predecessor). + * @return `true` for a non-barrier single-qubit unitary with a compile-time 2x2 + * matrix that does not hide a barrier inside an `inv` body. */ -static bool hasCompileTimeUnitaryMatrix2x2(UnitaryOpInterface op) { - for (size_t i = 0; i < op.getNumParams(); ++i) { - if (!mlir::utils::valueToDouble(op.getParameter(i))) { +static bool isRunMember(Operation* op) { + auto gate = dyn_cast_or_null(op); + if (!gate || !gate.isSingleQubit() || isa(op)) { + return false; + } + for (size_t i = 0; i < gate.getNumParams(); ++i) { + if (!mlir::utils::valueToDouble(gate.getParameter(i))) { return false; } } - if (op.getNumParams() > 0 || !isa(op.getOperation())) { + if (gate.getNumParams() > 0 || !isa(op)) { return true; } + const bool hidesBarrier = op->walk([](BarrierOp) { + return WalkResult::interrupt(); + }).wasInterrupted(); Matrix2x2 unused; - return op.getUnitaryMatrix2x2(unused); + return !hidesBarrier && gate.getUnitaryMatrix2x2(unused); } /** - * @brief Whether `op` is a fusable single-qubit run member on the wire. + * @brief Whether `op` is a gate that Euler synthesis emits for `basis`. * - * @param op The unitary operation to test. - * @return `true` for a single-qubit unitary with a compile-time 2x2 matrix, - * excluding barriers. - */ -static bool isRunMember(UnitaryOpInterface op) { - return op && op.isSingleQubit() && !isa(op.getOperation()) && - hasCompileTimeUnitaryMatrix2x2(op); -} - -/** - * @brief Whether `op` is a fusable single-qubit run member on the wire. - * - * @param op The operation to test. - * @return `true` for a single-qubit unitary with a compile-time 2x2 matrix, - * excluding barriers. - */ -static bool isRunMember(Operation* op) { - return isRunMember(dyn_cast(op)); -} - -/** - * @brief Whether @p op sits in the body region of an `inv`. - * - * Run heads are not started inside `inv` bodies; the enclosing `InvOp` already - * exposes the composed body unitary on the parent wire. - */ -static bool isInsideInvBody(Operation* op) { - return op != nullptr && op->getParentOfType() != nullptr; -} - -/** - * @brief Whether `op` is a gate the target `basis` emits. - * - * Gate sets match the synthesis step kinds in `Euler.cpp`. Used to - * skip runs that are already in the target basis at canonical length. + * Mirrors the synthesis step kinds in `Euler.cpp`; used to detect runs that are + * already in the target basis at canonical length. * * @param op The operation to classify. * @param basis The target Euler basis. @@ -137,10 +116,10 @@ static bool isTargetBasisGate(Operation* op, decomposition::EulerBasis basis) { } /** - * @brief Walks the wire from @p head, composing matrices and run metadata. + * @brief Walks the wire from @p head, composing the run's matrix and metadata. * - * `WireIterator` stops at region boundaries, so members are consecutive on the - * wire. Only @p tail is retained for replacement and erasure. + * `WireIterator` stops at region boundaries, so run members are consecutive on + * the wire. Only the run tail is retained, for replacement and erasure. * * @param head First gate of the run. * @param basis Target Euler basis (for non-basis detection). @@ -203,21 +182,32 @@ struct FuseSingleQubitUnitaryRunsPattern final decomposition::EulerBasis basis; /** - * @brief Whether `op` is the head of a run. + * @brief Whether `op` starts a run. + * + * A run does not start inside the body of a single-qubit `inv`: that modifier + * is itself a run member, so the run on the parent wire absorbs the whole + * `inv` as one unitary and fusing its body in place would be redundant. A + * multi-qubit `inv` cannot be absorbed that way (it has no compile-time 2x2 + * matrix), so single-qubit chains inside its body are fused locally and may + * start runs. * * @param op The candidate run head. - * @return `true` if `op` is a run member outside any `inv` body whose wire - * predecessor is not a run member. + * @return `true` if `op` is a run member whose wire predecessor is not itself + * a run member, and which is not inside the body of a fusable single-qubit + * `inv`. */ static bool isRunStart(UnitaryOpInterface op) { - if (!isRunMember(op)) { + if (!isRunMember(op.getOperation())) { return false; } - if (isInsideInvBody(op.getOperation())) { + // A single-qubit `inv` is itself a run member, so the parent wire's run + // already absorbs the whole modifier; only the bodies of multi-qubit `inv` + // modifiers (which cannot be absorbed) host their own runs. + if (auto inv = op->getParentOfType(); + inv && isRunMember(inv.getOperation())) { return false; } - Operation* pred = op.getInputTarget(0).getDefiningOp(); - return pred == nullptr || !isRunMember(pred); + return !isRunMember(op.getInputTarget(0).getDefiningOp()); } /** 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 f639e36906..b9fc788bd6 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -246,6 +246,14 @@ compute1QMatrixFromCtrlBody(func::FuncOp funcOp) { return Matrix2x2::fromElements(0, 0, 0, 0); } +[[nodiscard]] static Matrix2x2 compute1QMatrixFromInvBody(func::FuncOp funcOp) { + for (InvOp inv : funcOp.getOps()) { + return compute1QUnitaryMatrix(inv.getRegion()); + } + ADD_FAILURE() << "Expected InvOp in function"; + return Matrix2x2::fromElements(0, 0, 0, 0); +} + static void expectMatrixPreserved(func::FuncOp funcOp, const Matrix2x2& original, StringRef label) { EXPECT_TRUE( @@ -259,6 +267,12 @@ static void expectCtrlBodyMatrixPreserved(func::FuncOp funcOp, compute1QMatrixFromCtrlBody(funcOp).isApprox(original, MATRIX_TOLERANCE)); } +static void expectInvBodyMatrixPreserved(func::FuncOp funcOp, + const Matrix2x2& original) { + EXPECT_TRUE( + compute1QMatrixFromInvBody(funcOp).isApprox(original, MATRIX_TOLERANCE)); +} + static void expectBasisGatesOnly(func::FuncOp funcOp, StringRef basis) { funcOp.walk([&](Operation* op) -> WalkResult { if (isa(*op)) { @@ -529,6 +543,21 @@ static void xInverseTwoX(QCOProgramBuilder& b) { q[0] = b.x(q[0]); } +// A two-qubit inverse modifier whose body holds a single-qubit `h; t` chain on +// one target (the other target passes through). The modifier is not a fusable +// single-qubit run member, so the inner chain must fuse on its own. +static void inverseMultiQubitBodySingleQubitRun(QCOProgramBuilder& b) { + auto q = b.allocQubitRegister(2); + auto outs = + b.inv({q[0], q[1]}, [&](ValueRange targets) -> SmallVector { + Value wire = b.h(targets[0]); + wire = b.t(wire); + return {wire, targets[1]}; + }); + q[0] = outs[0]; + q[1] = outs[1]; +} + static void controlledInverseHT(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.ctrl(q[0], q[1], [&](ValueRange targets) { @@ -991,6 +1020,30 @@ TEST(FuseSingleQubitUnitaryRunsTest, EliminatesIdentityInvMultiOpBody) { }); } +TEST(FuseSingleQubitUnitaryRunsTest, FusesRunInMultiQubitInvBody) { + SynthesisFixture fx; + fx.setUp(); + + Matrix2x2 invBodyBefore; + runFuseOnProgram( + fx.context.get(), inverseMultiQubitBodySingleQubitRun, "u", + [&](func::FuncOp funcOp, const Matrix2x2&) { + EXPECT_EQ(countOps(funcOp), 1U); + EXPECT_EQ(countOpsInRegion(funcOp, isInsideInv), 1U); + EXPECT_EQ(countOpsInRegion(funcOp, isInsideInv), 1U); + EXPECT_EQ(countOpsInRegion(funcOp, isInsideInv), 0U); + invBodyBefore = compute1QMatrixFromInvBody(funcOp); + }, + [&](func::FuncOp funcOp, const Matrix2x2&) { + // The modifier survives; its single-qubit body chain fuses to one `u`. + EXPECT_EQ(countOps(funcOp), 1U); + EXPECT_EQ(countOpsInRegion(funcOp, isInsideInv), 0U); + EXPECT_EQ(countOpsInRegion(funcOp, isInsideInv), 0U); + EXPECT_EQ(countOpsInRegion(funcOp, isInsideInv), 1U); + expectInvBodyMatrixPreserved(funcOp, invBodyBefore); + }); +} + TEST(FuseSingleQubitUnitaryRunsTest, FusesSingleNonBasisGateInCtrlBody) { SynthesisFixture fx; fx.setUp(); From 902729f9077830d5ee1569043ba16632787fc01f Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 15 Jun 2026 17:31:42 +0200 Subject: [PATCH 51/68] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20linter=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp index b3b0a08df4..9d8376db12 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp @@ -23,8 +23,8 @@ #include #include #include -#include #include +#include #include #include From c7551bfa1e7e213642b6dee8de60cdece1938f81 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 15 Jun 2026 17:50:27 +0200 Subject: [PATCH 52/68] =?UTF-8?q?=F0=9F=8E=A8=20Update=20tolerance=20check?= =?UTF-8?q?=20for=20global=20complex=20value=20in=20InvOp.cpp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp b/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp index a08bc352d6..22afa94c41 100644 --- a/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp @@ -442,7 +442,7 @@ composeInvertedSingleQubitBodyMatrix(Block& block) { return std::nullopt; } } - if (!found && global == Complex{1.0, 0.0}) { + if (!found && std::abs(global - Complex{1.0, 0.0}) <= utils::TOLERANCE) { return std::nullopt; } acc *= global; From 2d71d9b5759c230819286c6b586e0663b9ad0801 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 15 Jun 2026 18:10:32 +0200 Subject: [PATCH 53/68] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20linter=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp b/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp index 22afa94c41..721a129f6f 100644 --- a/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp @@ -29,6 +29,7 @@ #include #include +#include #include #include #include From 3701236786a8dbb2da6f1dc7466990d439bbf940 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 15 Jun 2026 18:22:59 +0200 Subject: [PATCH 54/68] =?UTF-8?q?=E2=9C=85=20Refactor=20test=5Feuler=5Fdec?= =?UTF-8?q?omposition.cpp:=20streamline=20includes,=20rename=20fixture,=20?= =?UTF-8?q?and=20enhance=20matrix=20functions=20for=20clarity=20and=20cons?= =?UTF-8?q?istency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_euler_decomposition.cpp | 1211 ++++++++--------- 1 file changed, 539 insertions(+), 672 deletions(-) diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp index b9fc788bd6..540162fde2 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -26,15 +26,11 @@ #include #include #include -#include #include #include -#include #include -#include #include -#include -#include +#include #include #include @@ -48,15 +44,15 @@ #include #include #include -#include using namespace mlir; using namespace mlir::qco; using namespace mlir::qco::decomposition; +using enum EulerBasis; namespace { -struct SynthesisFixture { +struct TestFixture { std::unique_ptr context; void setUp() { @@ -67,11 +63,8 @@ struct SynthesisFixture { context->appendDialectRegistry(registry); context->loadAllAvailableDialects(); } -}; -struct SynthesizedCircuit { - OwningOpRef module; - func::FuncOp func; + [[nodiscard]] MLIRContext* ctx() const { return context.get(); } }; struct ZSXXShortcutCase { @@ -84,23 +77,20 @@ struct ZSXXShortcutCase { class ZSXXShortcutTest : public testing::TestWithParam {}; -} // namespace - -[[nodiscard]] static Matrix2x2 rzMatrix(double theta) { +[[nodiscard]] Matrix2x2 rzMatrix(const double theta) { const auto m00 = std::polar(1.0, -theta / 2.0); const auto m11 = std::polar(1.0, theta / 2.0); return Matrix2x2::fromElements(m00, 0, 0, m11); } -[[nodiscard]] static Matrix2x2 ryMatrix(double theta) { +[[nodiscard]] Matrix2x2 ryMatrix(const double theta) { const auto m00 = std::cos(theta / 2.0); const auto m01 = -std::sin(theta / 2.0); return Matrix2x2::fromElements(m00, m01, -m01, m00); } -[[nodiscard]] static Matrix2x2 randomUnitaryMatrix(std::mt19937& rng) { - std::uniform_real_distribution dist(-std::numbers::pi, - std::numbers::pi); +[[nodiscard]] Matrix2x2 randomUnitaryMatrix(std::mt19937& rng) { + std::uniform_real_distribution dist(-std::numbers::pi, std::numbers::pi); const Matrix2x2 su2 = rzMatrix(dist(rng)) * ryMatrix(dist(rng)) * rzMatrix(dist(rng)); const Complex globalPhase = std::polar(1.0, dist(rng)); @@ -110,14 +100,14 @@ class ZSXXShortcutTest : public testing::TestWithParam {}; } template -[[nodiscard]] static Matrix2x2 rotationMatrix(MLIRContext* ctx, double theta) { +[[nodiscard]] Matrix2x2 rotationMatrix(MLIRContext* ctx, const double theta) { OpBuilder builder(ctx); - auto module = ModuleOp::create(UnknownLoc::get(ctx)); - builder.setInsertionPointToStart(module.getBody()); - const Location loc = module.getLoc(); - Value q = builder.create(loc).getResult(); - auto op = builder.create(loc, q, theta); - const auto matrix = cast(op).getUnitaryMatrix(); + auto mlirModule = ModuleOp::create(UnknownLoc::get(ctx)); + builder.setInsertionPointToStart(mlirModule.getBody()); + const Location loc = mlirModule.getLoc(); + Value q = AllocOp::create(builder, loc).getResult(); + auto op = RotationOp::create(builder, loc, q, theta); + const auto matrix = op.getUnitaryMatrix(); if (!matrix) { ADD_FAILURE() << "Expected constant unitary matrix"; return Matrix2x2::identity(); @@ -125,345 +115,417 @@ template return *matrix; } -[[nodiscard]] static bool isTwoQubitGate(Operation& op) { - if (auto u = dyn_cast(op)) { - return u.isTwoQubit(); +template void forEachBasis(Fn fn) { + const std::array bases = {"zyz", "zxz", "xzx", + "xyx", "u", "zsxx"}; + for (const char* basis : bases) { + fn(StringRef{basis}); } - return false; } -static bool isAllowedBasisGate(Operation& op, StringRef basis) { - // `gphase` is always allowed. - if (isa(op)) { - return true; - } - - const auto b = basis.lower(); - if (b == "zyz") { +[[nodiscard]] bool isAllowedBasisGate(const Operation& op, EulerBasis basis) { + switch (basis) { + case ZYZ: return isa(op); - } - if (b == "zxz") { + case ZXZ: return isa(op); - } - if (b == "xzx") { + case XZX: return isa(op); - } - if (b == "xyx") { + case XYX: return isa(op); - } - if (b == "u") { + case U: return isa(op); - } - if (b == "zsxx") { + case ZSXX: return isa(op); } return false; } -template static void forEachBasis(Fn fn) { - const std::array bases = {"zyz", "zxz", "xzx", - "xyx", "u", "zsxx"}; - for (const char* basis : bases) { - fn(StringRef{basis}); - } +template [[nodiscard]] bool inParent(Operation* op) { + return op != nullptr && op->getParentOfType() != nullptr; } -template -static Matrix2x2 compute1QUnitaryMatrix(WalkRange& range) { - Matrix2x2 acc = Matrix2x2::identity(); - std::complex global{1.0, 0.0}; - bool failed = false; - - range.template walk([&](Operation* op) -> WalkResult { - if (isa(*op)) { - return WalkResult::advance(); - } +[[nodiscard]] WalkResult failMissingUnitaryMatrix(Operation* op, bool& failed) { + ADD_FAILURE() << "Expected constant unitary matrix for op: " + << op->getName().getStringRef().str(); + failed = true; + return WalkResult::interrupt(); +} - if (isa(*op)) { - return WalkResult::advance(); - } +[[nodiscard]] WalkResult +accumulateConstantSingleQubit(UnitaryOpInterface unitary, Operation* op, + Matrix2x2& acc, bool& failed) { + if (Matrix2x2 matrix; unitary.getUnitaryMatrix2x2(matrix)) { + acc = matrix * acc; + return WalkResult::advance(); + } + return failMissingUnitaryMatrix(op, failed); +} - if (auto gphase = dyn_cast(*op)) { - if (auto m = gphase.getUnitaryMatrix()) { - global *= (*m)(0, 0); - } - return WalkResult::advance(); +WalkResult visit1QUnitaryOp(Operation* op, Matrix2x2& acc, + std::complex& global, bool& failed) { + if (isa(*op)) { + return WalkResult::advance(); + } + if (auto gphase = dyn_cast(*op)) { + if (auto matrix = gphase.getUnitaryMatrix()) { + global *= (*matrix)(0, 0); } - - if (isa(*op)) { - auto unitary = cast(*op); - if (unitary.isSingleQubit()) { - Matrix2x2 matrix; - if (!unitary.getUnitaryMatrix2x2(matrix)) { - ADD_FAILURE() << "Expected constant unitary matrix for op: " - << op->getName().getStringRef().str(); - failed = true; - return WalkResult::interrupt(); - } - acc = matrix * acc; - } + return WalkResult::advance(); + } + auto unitary = dyn_cast(*op); + if (!unitary) { + return WalkResult::advance(); + } + if (isa(*op)) { + if (!unitary.isSingleQubit()) { return WalkResult::skip(); } + const WalkResult result = + accumulateConstantSingleQubit(unitary, op, acc, failed); + return failed ? result : WalkResult::skip(); + } + if (unitary.isTwoQubit()) { + return WalkResult::advance(); + } + const WalkResult result = + accumulateConstantSingleQubit(unitary, op, acc, failed); + return failed ? result : WalkResult::advance(); +} - if (isTwoQubitGate(*op)) { - return WalkResult::advance(); +WalkResult visitBasisGateOp(Operation* op, StringRef basis, + EulerBasis parsedBasis) { + if (isa(*op)) { + return WalkResult::advance(); + } + if (auto unitary = dyn_cast(*op)) { + if (unitary.isTwoQubit() || isa(*op)) { + return unitary.isTwoQubit() ? WalkResult::advance() : WalkResult::skip(); } - - if (auto unitary = dyn_cast(*op)) { - if (!unitary.isSingleQubit()) { - return WalkResult::advance(); - } - Matrix2x2 matrix; - if (!unitary.getUnitaryMatrix2x2(matrix)) { - ADD_FAILURE() << "Expected constant unitary matrix for op: " - << op->getName().getStringRef().str(); - failed = true; - return WalkResult::interrupt(); - } - acc = matrix * acc; + if (Matrix2x2 matrix; unitary.getUnitaryMatrix2x2(matrix)) { + EXPECT_TRUE(isAllowedBasisGate(*op, parsedBasis) || isa(*op)) + << "basis=" << basis.str() + << " unexpected gate: " << op->getName().getStringRef().str(); return WalkResult::advance(); } - - return WalkResult::advance(); - }); - - if (failed) { - return Matrix2x2::fromElements(0, 0, 0, 0); + ADD_FAILURE() << "basis=" << basis.str() << " missing constant matrix for: " + << op->getName().getStringRef().str(); + return WalkResult::interrupt(); } - return acc * global; + return WalkResult::advance(); } -static Matrix2x2 compute1QMatrixFromFunction(func::FuncOp funcOp) { - return compute1QUnitaryMatrix(funcOp); +void skipBeforeFuse(func::FuncOp /*funcOp*/, const Matrix2x2& /*original*/) { + // Pre-fuse checks are not required for this scenario. } -[[nodiscard]] static Matrix2x2 -compute1QMatrixFromCtrlBody(func::FuncOp funcOp) { - for (CtrlOp ctrl : funcOp.getOps()) { - return compute1QUnitaryMatrix(ctrl.getRegion()); +template +Matrix2x2 compute1QUnitaryMatrix(WalkRange& range) { + Matrix2x2 acc = Matrix2x2::identity(); + std::complex global{1.0, 0.0}; + bool failed = false; + + range.template walk( + [&acc, &global, &failed](Operation* op) { + return visit1QUnitaryOp(op, acc, global, failed); + }); + + if (failed) { + return Matrix2x2::fromElements(0, 0, 0, 0); } - ADD_FAILURE() << "Expected CtrlOp in function"; - return Matrix2x2::fromElements(0, 0, 0, 0); + return acc * global; } -[[nodiscard]] static Matrix2x2 compute1QMatrixFromInvBody(func::FuncOp funcOp) { - for (InvOp inv : funcOp.getOps()) { - return compute1QUnitaryMatrix(inv.getRegion()); +template +[[nodiscard]] Matrix2x2 matrixInParent(func::FuncOp funcOp) { + auto parents = funcOp.getOps(); + if (parents.begin() == parents.end()) { + ADD_FAILURE() << "Expected parent op in function"; + return Matrix2x2::fromElements(0, 0, 0, 0); } - ADD_FAILURE() << "Expected InvOp in function"; - return Matrix2x2::fromElements(0, 0, 0, 0); + return compute1QUnitaryMatrix((*parents.begin()).getRegion()); } -static void expectMatrixPreserved(func::FuncOp funcOp, - const Matrix2x2& original, StringRef label) { +void expectMatrixPreserved(func::FuncOp funcOp, const Matrix2x2& original, + StringRef label = {}) { EXPECT_TRUE( - compute1QMatrixFromFunction(funcOp).isApprox(original, MATRIX_TOLERANCE)) + compute1QUnitaryMatrix(funcOp).isApprox(original, MATRIX_TOLERANCE)) << label.str(); } -static void expectCtrlBodyMatrixPreserved(func::FuncOp funcOp, - const Matrix2x2& original) { - EXPECT_TRUE( - compute1QMatrixFromCtrlBody(funcOp).isApprox(original, MATRIX_TOLERANCE)); -} +void expectBasisGatesOnly(func::FuncOp funcOp, StringRef basis) { + const auto parsed = parseEulerBasis(basis); + ASSERT_TRUE(parsed) << basis.str(); -static void expectInvBodyMatrixPreserved(func::FuncOp funcOp, - const Matrix2x2& original) { - EXPECT_TRUE( - compute1QMatrixFromInvBody(funcOp).isApprox(original, MATRIX_TOLERANCE)); + funcOp.walk( + [basis, parsedBasis = *parsed](Operation* op) { + return visitBasisGateOp(op, basis, parsedBasis); + }); } -static void expectBasisGatesOnly(func::FuncOp funcOp, StringRef basis) { - funcOp.walk([&](Operation* op) -> WalkResult { - if (isa(*op)) { - return WalkResult::advance(); - } - if (isTwoQubitGate(*op)) { - return WalkResult::advance(); - } - if (isa(*op)) { - return WalkResult::skip(); - } - - auto unitaryOp = dyn_cast(*op); - if (!unitaryOp || !unitaryOp.isSingleQubit()) { - return WalkResult::advance(); - } - - Matrix2x2 matrix; - if (!unitaryOp.getUnitaryMatrix2x2(matrix)) { - ADD_FAILURE() << "basis=" << basis.str() - << " missing constant matrix for: " - << op->getName().getStringRef().str(); - return WalkResult::interrupt(); - } - EXPECT_TRUE(isAllowedBasisGate(*op, basis)) - << "basis=" << basis.str() - << " unexpected gate: " << op->getName().getStringRef().str(); - return WalkResult::advance(); - }); +void expectFusePreserved(func::FuncOp funcOp, const Matrix2x2& original, + StringRef basis) { + expectMatrixPreserved(funcOp, original, basis); + expectBasisGatesOnly(funcOp, basis); } -/// Composed unitary of the `h; t` segment in `singleQubitRunsSplitByBarrier`, -/// `singleQubitRunsSplitByTwoQGate`, and `singleQubitRunsSplitByScfFor`. -[[nodiscard]] static Matrix2x2 splitFixtureHTSegmentMatrix() { +[[nodiscard]] Matrix2x2 splitFixtureHTSegmentMatrix() { return TOp::getUnitaryMatrix() * HOp::getUnitaryMatrix(); } -/// Composed unitary of the `rz(0.321); sx` segment in the same split fixtures. -[[nodiscard]] static Matrix2x2 splitFixtureRZSXSegmentMatrix() { +[[nodiscard]] Matrix2x2 splitFixtureRZSXSegmentMatrix() { return SXOp::getUnitaryMatrix() * rzMatrix(0.321); } -/// Composed unitary of `overlongZSXXMixedPureZRun` (`sx; rz(pi); sx` → pure Z). -[[nodiscard]] static Matrix2x2 overlongZSXXPureZRunMatrix() { +[[nodiscard]] Matrix2x2 overlongZSXXPureZRunMatrix() { return SXOp::getUnitaryMatrix() * rzMatrix(std::numbers::pi) * SXOp::getUnitaryMatrix(); } -template -static void expectOneQubitGatesAroundBoundary(func::FuncOp funcOp, - StringRef basis, - BoundaryPred isBoundary, - std::size_t expectedBefore, - std::size_t expectedAfter) { - auto& block = funcOp.getBody().front(); - std::size_t before = 0; - std::size_t after = 0; - bool seenBoundary = false; - for (Operation& op : block.without_terminator()) { - if (!seenBoundary && isBoundary(op)) { - seenBoundary = true; - continue; - } - if (isa(op)) { - continue; - } - auto unitaryOp = dyn_cast(op); - if (!unitaryOp || !unitaryOp.isSingleQubit()) { - continue; - } - Matrix2x2 matrix; - if (!unitaryOp.getUnitaryMatrix2x2(matrix)) { - continue; - } - if (seenBoundary) { - ++after; - } else { - ++before; - } - } - EXPECT_EQ(before, expectedBefore) << "basis=" << basis.str(); - EXPECT_EQ(after, expectedAfter) << "basis=" << basis.str(); -} - template -[[nodiscard]] static std::size_t countOps(func::FuncOp funcOp) { +[[nodiscard]] std::size_t countOps(func::FuncOp funcOp) { std::size_t count = 0; - funcOp.walk([&](OpTy) { ++count; }); + funcOp.walk([&count](OpTy) { ++count; }); return count; } -[[nodiscard]] static std::size_t countZSXXBasisGates(func::FuncOp funcOp) { +[[nodiscard]] std::size_t countZYZGates(func::FuncOp funcOp) { + return countOps(funcOp) + countOps(funcOp); +} + +[[nodiscard]] std::size_t countZSXXGates(func::FuncOp funcOp) { return countOps(funcOp) + countOps(funcOp) + countOps(funcOp); } -[[nodiscard]] static std::size_t countBasisGates(func::FuncOp funcOp, - EulerBasis basis) { +[[nodiscard]] std::size_t countBasisGates(func::FuncOp funcOp, + EulerBasis basis) { switch (basis) { - case EulerBasis::ZYZ: - return countOps(funcOp) + countOps(funcOp); - case EulerBasis::ZXZ: + case ZYZ: + return countZYZGates(funcOp); + case ZXZ: return countOps(funcOp) + countOps(funcOp); - case EulerBasis::XZX: + case XZX: return countOps(funcOp) + countOps(funcOp); - case EulerBasis::XYX: + case XYX: return countOps(funcOp) + countOps(funcOp); - case EulerBasis::U: + case U: return countOps(funcOp); - case EulerBasis::ZSXX: - return countZSXXBasisGates(funcOp); + case ZSXX: + return countZSXXGates(funcOp); } return 0; } -[[nodiscard]] static bool isInsideScfFor(Operation* op) { - return op != nullptr && op->getParentOfType() != nullptr; +template +[[nodiscard]] std::size_t countInParent(func::FuncOp funcOp) { + std::size_t count = 0; + funcOp.walk([&count](OpTy op) { + if (inParent(op.getOperation())) { + ++count; + } + }); + return count; } -[[nodiscard]] static bool isInsideInv(Operation* op) { - return op != nullptr && op->getParentOfType() != nullptr; +struct SynthesizedCircuit { + OwningOpRef mlirModule; + func::FuncOp func; +}; + +[[nodiscard]] SynthesizedCircuit +synthesizeMatrix(MLIRContext* ctx, const Matrix2x2& matrix, EulerBasis basis) { + OwningOpRef mlirModule = ModuleOp::create(UnknownLoc::get(ctx)); + OpBuilder builder(ctx); + builder.setInsertionPointToStart(mlirModule->getBody()); + + auto qubitTy = QubitType::get(ctx); + auto funcTy = builder.getFunctionType({qubitTy}, {qubitTy}); + const Location loc = mlirModule->getLoc(); + auto func = func::FuncOp::create(builder, loc, "main", funcTy); + auto* entry = func.addEntryBlock(); + + builder.setInsertionPointToStart(entry); + Value q = entry->getArgument(0); + const std::optional qubitOut = + synthesizeUnitary1QEuler(builder, loc, q, matrix, 0, true, basis); + if (!qubitOut) { + llvm::report_fatal_error( + "synthesizeUnitary1QEuler failed during test synthesis"); + } + func::ReturnOp::create(builder, loc, *qubitOut); + return SynthesizedCircuit{.mlirModule = std::move(mlirModule), .func = func}; } -[[nodiscard]] static bool isInsideCtrl(Operation* op) { - return op != nullptr && op->getParentOfType() != nullptr; +[[nodiscard]] std::size_t expectedGateCount(MLIRContext* ctx, + const Matrix2x2& segment, + EulerBasis basis) { + return countBasisGates(synthesizeMatrix(ctx, segment, basis).func, basis); } -template -[[nodiscard]] static std::size_t countOpsInScfFor(func::FuncOp funcOp) { - std::size_t count = 0; - funcOp.walk([&](OpTy op) { - if (isInsideScfFor(op.getOperation())) { - ++count; - } - }); - return count; +void checkSynthesizedReferenceExtras(MLIRContext* ctx, func::FuncOp funcOp, + EulerBasis basis, + const Matrix2x2& matrix) { + if (basis == U) { + EXPECT_EQ(countOps(funcOp), expectedGateCount(ctx, matrix, basis)); + } + if (!matrix.isApprox(Matrix2x2::identity())) { + return; + } + if (basis == ZYZ) { + EXPECT_EQ(countZYZGates(funcOp), 0U); + } + if (basis == U) { + EXPECT_EQ(countOps(funcOp), 0U); + } } -template -[[nodiscard]] static std::size_t countOpsInRegion(func::FuncOp funcOp, - InRegionPred inRegion) { - std::size_t count = 0; - funcOp.walk([&](OpTy op) { - if (inRegion(op.getOperation())) { - ++count; - } - }); - return count; +template +void expectSynthesizedMatrix(MLIRContext* ctx, const Matrix2x2& matrix, + EulerBasis basis, ExtraChecksT extraChecks) { + const auto circuit = synthesizeMatrix(ctx, matrix, basis); + ASSERT_TRUE(succeeded(verify(*circuit.mlirModule))); + extraChecks(circuit.func, matrix); + expectMatrixPreserved(circuit.func, matrix, "synthesis"); } -static void expectOneQubitGatesInAndOutsideScfFor(func::FuncOp funcOp, - StringRef basis, - std::size_t expectedOutside, - std::size_t expectedInside) { +void expectSplitFixtureSegments(func::FuncOp funcOp, StringRef basis, + MLIRContext* ctx) { + const auto parsed = parseEulerBasis(basis); + ASSERT_TRUE(parsed) << basis.str(); + const std::size_t ht = + expectedGateCount(ctx, splitFixtureHTSegmentMatrix(), *parsed); + const std::size_t rzsx = + expectedGateCount(ctx, splitFixtureRZSXSegmentMatrix(), *parsed); + std::size_t outside = 0; std::size_t inside = 0; - funcOp.walk([&](Operation* op) { + funcOp.walk([&outside, &inside](Operation* op) { if (isa(*op)) { return; } auto unitary = dyn_cast(op); - if (!unitary || !unitary.isSingleQubit()) { - return; + if (Matrix2x2 matrix; unitary && unitary.isSingleQubit() && + unitary.getUnitaryMatrix2x2(matrix)) { + if (inParent(op)) { + ++inside; + } else { + ++outside; + } } - Matrix2x2 matrix; - if (!unitary.getUnitaryMatrix2x2(matrix)) { - return; + }); + EXPECT_EQ(outside, ht) << "basis=" << basis.str(); + EXPECT_EQ(inside, rzsx) << "basis=" << basis.str(); +} + +template +void expectSplitFixtureSegments(func::FuncOp funcOp, StringRef basis, + MLIRContext* ctx, BoundaryPred isBoundary) { + const auto parsed = parseEulerBasis(basis); + ASSERT_TRUE(parsed) << basis.str(); + const std::size_t ht = + expectedGateCount(ctx, splitFixtureHTSegmentMatrix(), *parsed); + const std::size_t rzsx = + expectedGateCount(ctx, splitFixtureRZSXSegmentMatrix(), *parsed); + + std::size_t before = 0; + std::size_t after = 0; + bool seenBoundary = false; + for (Operation& op : funcOp.getBody().front().without_terminator()) { + if (!seenBoundary && isBoundary(op)) { + seenBoundary = true; + continue; + } + if (isa(op)) { + continue; } - if (isInsideScfFor(op)) { - ++inside; - } else { - ++outside; + auto unitary = dyn_cast(op); + if (Matrix2x2 matrix; unitary && unitary.isSingleQubit() && + unitary.getUnitaryMatrix2x2(matrix)) { + if (seenBoundary) { + ++after; + } else { + ++before; + } } + } + EXPECT_EQ(before, ht) << "basis=" << basis.str(); + EXPECT_EQ(after, rzsx) << "basis=" << basis.str(); +} + +LogicalResult runFuse(ModuleOp mlirModule, StringRef basis) { + PassManager pm(mlirModule.getContext()); + qco::FuseSingleQubitUnitaryRunsOptions opts; + opts.basis = basis.str(); + pm.addPass(qco::createFuseSingleQubitUnitaryRuns(opts)); + return pm.run(mlirModule); +} + +template +void runFuseOnProgram(MLIRContext* ctx, ProgramT program, StringRef basis, + BeforeT beforeFuse, AfterT afterFuse) { + auto owned = QCOProgramBuilder::build(ctx, program); + ASSERT_TRUE(owned); + ModuleOp mlirModule = *owned; + ASSERT_TRUE(succeeded(verify(mlirModule))); + + auto funcOp = mlirModule.lookupSymbol("main"); + ASSERT_TRUE(funcOp); + const Matrix2x2 original = compute1QUnitaryMatrix(funcOp); + beforeFuse(funcOp, original); + + ASSERT_TRUE(succeeded(runFuse(mlirModule, basis))); + ASSERT_TRUE(succeeded(verify(mlirModule))); + + funcOp = mlirModule.lookupSymbol("main"); + ASSERT_TRUE(funcOp); + afterFuse(funcOp, original); +} + +template +void runFuseForAllBases(MLIRContext* ctx, ProgramT program, + ChecksT checksAfter) { + forEachBasis([&ctx, program, &checksAfter](StringRef basis) { + runFuseOnProgram( + ctx, program, basis, skipBeforeFuse, + [basis, &checksAfter](func::FuncOp funcOp, const Matrix2x2& original) { + checksAfter(funcOp, basis, original); + }); }); - EXPECT_EQ(outside, expectedOutside) << "basis=" << basis.str(); - EXPECT_EQ(inside, expectedInside) << "basis=" << basis.str(); } -static void singleQubitRunWithSingleQubitGate(QCOProgramBuilder& b) { +template +void runFuseInParent(MLIRContext* ctx, ProgramT program, BeforeT checkBefore, + AfterT checkAfter) { + Matrix2x2 bodyBefore; + runFuseOnProgram( + ctx, program, "u", + [&checkBefore, &bodyBefore](func::FuncOp funcOp, const Matrix2x2&) { + checkBefore(funcOp); + bodyBefore = matrixInParent(funcOp); + }, + [&checkAfter, &bodyBefore](func::FuncOp funcOp, const Matrix2x2&) { + checkAfter(funcOp); + EXPECT_TRUE(matrixInParent(funcOp).isApprox( + bodyBefore, MATRIX_TOLERANCE)); + }); +} + +// --- program builders ---------------------------------------------------- // + +void singleQubitRunWithSingleQubitGate(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.h(q[0]); q[0] = b.t(q[0]); q[0] = b.rz(0.123, q[0]); - // `inv` is part of the fusable run. - q[0] = b.inv({q[0]}, [&](ValueRange targets) -> SmallVector { + q[0] = b.inv({q[0]}, [&b](ValueRange targets) -> SmallVector { return {b.sx(targets[0])}; })[0]; q[0] = b.ry(-0.456, q[0]); } -static void singleQubitRunsSplitByTwoQGate(QCOProgramBuilder& b) { +void singleQubitRunsSplitByTwoQGate(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); q[0] = b.h(q[0]); q[0] = b.t(q[0]); @@ -472,7 +534,7 @@ static void singleQubitRunsSplitByTwoQGate(QCOProgramBuilder& b) { q[0] = b.sx(q[0]); } -static void singleQubitRunsSplitByBarrier(QCOProgramBuilder& b) { +void singleQubitRunsSplitByBarrier(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.h(q[0]); q[0] = b.t(q[0]); @@ -481,28 +543,24 @@ static void singleQubitRunsSplitByBarrier(QCOProgramBuilder& b) { q[0] = b.sx(q[0]); } -// Single `H` gate — not in any target basis; should still be resynthesized. -static void singleNonBasisGate(QCOProgramBuilder& b) { +void singleNonBasisGate(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.h(q[0]); } -// Single `X` in `zsxx` basis — already at canonical synthesis length (1). -static void singlePauliX(QCOProgramBuilder& b) { +void singlePauliX(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.x(q[0]); } -// One `RZ`/`RY`/`RZ` triple in `zyz` basis — already at canonical length (3). -static void canonicalZYZRun(QCOProgramBuilder& b) { +void canonicalZYZRun(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.rz(0.3, q[0]); q[0] = b.ry(0.5, q[0]); q[0] = b.rz(0.7, q[0]); } -// Six `RZ`/`RY` gates in `zyz` basis — longer than canonical (3). -static void overlongZYZRun(QCOProgramBuilder& b) { +void overlongZYZRun(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.rz(0.3, q[0]); q[0] = b.ry(0.5, q[0]); @@ -512,18 +570,16 @@ static void overlongZYZRun(QCOProgramBuilder& b) { q[0] = b.ry(1.3, q[0]); } -// `SX`/`RZ(pi)`/`SX` in `zsxx` basis — composes to `Z` (pure-Z, theta = 0). -// Consecutive-`RZ` merge patterns do not apply across `SX` gates. -static void overlongZSXXMixedPureZRun(QCOProgramBuilder& b) { +void overlongZSXXMixedPureZRun(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.sx(q[0]); q[0] = b.rz(std::numbers::pi, q[0]); q[0] = b.sx(q[0]); } -static void singleQubitRunInScfFor(QCOProgramBuilder& b) { +void singleQubitRunInScfFor(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); - b.scfFor(0, 1, 1, ValueRange{q[0]}, [&](Value, ValueRange iterArgs) { + b.scfFor(0, 1, 1, ValueRange{q[0]}, [&b](Value, ValueRange iterArgs) { Value wire = iterArgs[0]; wire = b.h(wire); wire = b.t(wire); @@ -532,10 +588,10 @@ static void singleQubitRunInScfFor(QCOProgramBuilder& b) { }); } -static void xInverseTwoX(QCOProgramBuilder& b) { +void xInverseTwoX(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.x(q[0]); - q[0] = b.inv({q[0]}, [&](ValueRange targets) { + q[0] = b.inv({q[0]}, [&b](ValueRange targets) { Value wire = b.x(targets[0]); wire = b.x(wire); return SmallVector{wire}; @@ -543,13 +599,10 @@ static void xInverseTwoX(QCOProgramBuilder& b) { q[0] = b.x(q[0]); } -// A two-qubit inverse modifier whose body holds a single-qubit `h; t` chain on -// one target (the other target passes through). The modifier is not a fusable -// single-qubit run member, so the inner chain must fuse on its own. -static void inverseMultiQubitBodySingleQubitRun(QCOProgramBuilder& b) { +void inverseMultiQubitBodySingleQubitRun(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); auto outs = - b.inv({q[0], q[1]}, [&](ValueRange targets) -> SmallVector { + b.inv({q[0], q[1]}, [&b](ValueRange targets) -> SmallVector { Value wire = b.h(targets[0]); wire = b.t(wire); return {wire, targets[1]}; @@ -558,10 +611,10 @@ static void inverseMultiQubitBodySingleQubitRun(QCOProgramBuilder& b) { q[1] = outs[1]; } -static void controlledInverseHT(QCOProgramBuilder& b) { +void controlledInverseHT(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); - b.ctrl(q[0], q[1], [&](ValueRange targets) { - auto wire = b.inv({targets[0]}, [&](ValueRange innerTargets) { + b.ctrl(q[0], q[1], [&b](ValueRange targets) { + auto wire = b.inv({targets[0]}, [&b](ValueRange innerTargets) { auto inner = b.h(innerTargets[0]); inner = b.t(inner); return SmallVector{inner}; @@ -570,17 +623,17 @@ static void controlledInverseHT(QCOProgramBuilder& b) { }); } -static void controlledH(QCOProgramBuilder& b) { +void controlledH(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.ctrl(q[0], q[1], - [&](ValueRange targets) { return SmallVector{b.h(targets[0])}; }); + [&b](ValueRange targets) { return SmallVector{b.h(targets[0])}; }); } -static void singleQubitRunsSplitByScfFor(QCOProgramBuilder& b) { +void singleQubitRunsSplitByScfFor(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.h(q[0]); q[0] = b.t(q[0]); - b.scfFor(0, 1, 1, ValueRange{q[0]}, [&](Value, ValueRange iterArgs) { + b.scfFor(0, 1, 1, ValueRange{q[0]}, [&b](Value, ValueRange iterArgs) { Value wire = iterArgs[0]; wire = b.rz(0.321, wire); wire = b.sx(wire); @@ -588,114 +641,22 @@ static void singleQubitRunsSplitByScfFor(QCOProgramBuilder& b) { }); } -[[nodiscard]] static SynthesizedCircuit -synthesizeMatrix(MLIRContext* ctx, const Matrix2x2& matrix, EulerBasis basis) { - OwningOpRef module = ModuleOp::create(UnknownLoc::get(ctx)); - OpBuilder builder(ctx); - builder.setInsertionPointToStart(module->getBody()); - - auto qubitTy = QubitType::get(ctx); - auto funcTy = builder.getFunctionType({qubitTy}, {qubitTy}); - auto func = builder.create(module->getLoc(), "main", funcTy); - auto* entry = func.addEntryBlock(); - - builder.setInsertionPointToStart(entry); - Value q = entry->getArgument(0); - const std::optional qubitOut = synthesizeUnitary1QEuler( - builder, module->getLoc(), q, matrix, 0, true, basis); - if (!qubitOut) { - llvm::report_fatal_error( - "synthesizeUnitary1QEuler failed during test synthesis"); - } - q = *qubitOut; - builder.create(module->getLoc(), q); - return SynthesizedCircuit{.module = std::move(module), .func = func}; -} - -template -static void expectSynthesizedMatrix(MLIRContext* ctx, const Matrix2x2& matrix, - EulerBasis basis, - ExtraChecksT extraChecks) { - const auto circuit = synthesizeMatrix(ctx, matrix, basis); - ASSERT_TRUE(succeeded(verify(*circuit.module))); - extraChecks(circuit.func, matrix); - expectMatrixPreserved(circuit.func, matrix, "synthesis"); -} - -[[nodiscard]] static std::size_t -expectedFusedGateCount(MLIRContext* ctx, const Matrix2x2& segment, - EulerBasis basis) { - return countBasisGates(synthesizeMatrix(ctx, segment, basis).func, basis); -} - -static LogicalResult runFuse(ModuleOp module, StringRef basis) { - PassManager pm(module.getContext()); - qco::FuseSingleQubitUnitaryRunsOptions opts; - opts.basis = basis.str(); - pm.addPass(qco::createFuseSingleQubitUnitaryRuns(opts)); - return pm.run(module); -} - -template -static void -runFuseOnProgram(MLIRContext* ctx, void (*program)(QCOProgramBuilder&), - StringRef basis, BeforeT beforeFuse, AfterT afterFuse) { - auto owned = QCOProgramBuilder::build(ctx, program); - ASSERT_TRUE(owned); - ModuleOp module = *owned; - ASSERT_TRUE(succeeded(verify(module))); - - auto funcOp = module.lookupSymbol("main"); - ASSERT_TRUE(funcOp) << "Expected a 'main' function"; - const Matrix2x2 original = compute1QMatrixFromFunction(funcOp); - beforeFuse(funcOp, original); - - ASSERT_TRUE(succeeded(runFuse(module, basis))); - ASSERT_TRUE(succeeded(verify(module))); - - funcOp = module.lookupSymbol("main"); - ASSERT_TRUE(funcOp) << "Expected a 'main' function"; - afterFuse(funcOp, original); -} - -template -static void runFuseOnProgramForAllBases(MLIRContext* ctx, - void (*program)(QCOProgramBuilder&), - ChecksT checksAfter) { - forEachBasis([&](StringRef basis) { - runFuseOnProgram( - ctx, program, basis, [](func::FuncOp, const Matrix2x2&) {}, - [&](func::FuncOp funcOp, const Matrix2x2& original) { - checksAfter(funcOp, basis, original); - }); - }); -} - -TEST(EulerSynthesisTest, IdentityGateCount) { - SynthesisFixture fx; - fx.setUp(); - - const Matrix2x2 identity = Matrix2x2::identity(); - EXPECT_EQ(expectedFusedGateCount(fx.context.get(), identity, EulerBasis::ZYZ), - 0U); -} +} // namespace TEST_P(ZSXXShortcutTest, SynthesisMatchesGateCount) { - SynthesisFixture fx; + TestFixture fx; fx.setUp(); - const auto& testCase = GetParam(); - const Matrix2x2 matrix = testCase.makeMatrix(fx.context.get()); + const Matrix2x2 matrix = testCase.makeMatrix(fx.ctx()); expectSynthesizedMatrix( - fx.context.get(), matrix, EulerBasis::ZSXX, - [&](func::FuncOp funcOp, const Matrix2x2& original) { + fx.ctx(), matrix, ZSXX, + [&testCase, &fx](func::FuncOp funcOp, const Matrix2x2& original) { EXPECT_EQ(countOps(funcOp), testCase.expectedRZ); EXPECT_EQ(countOps(funcOp), testCase.expectedSX); EXPECT_EQ(countOps(funcOp), testCase.expectedX); - EXPECT_EQ(countZSXXBasisGates(funcOp), - expectedFusedGateCount(fx.context.get(), original, - EulerBasis::ZSXX)); + EXPECT_EQ(countZSXXGates(funcOp), + expectedGateCount(fx.ctx(), original, ZSXX)); }); } @@ -751,77 +712,58 @@ INSTANTIATE_TEST_SUITE_P( return std::string(info.param.label); }); -// NOLINTNEXTLINE(misc-use-internal-linkage) -- gtest `TEST_P` at global scope class EulerSynthesisExactTest : public testing::TestWithParam< std::tuple> {}; TEST_P(EulerSynthesisExactTest, ReconstructsReferenceMatrices) { - SynthesisFixture fx; + TestFixture fx; fx.setUp(); - const auto [basis, matrixFn] = GetParam(); - const Matrix2x2 original = matrixFn(fx.context.get()); + const Matrix2x2 original = matrixFn(fx.ctx()); expectSynthesizedMatrix( - fx.context.get(), original, basis, - [&](func::FuncOp funcOp, const Matrix2x2& matrix) { - if (basis == EulerBasis::U) { - EXPECT_EQ(countOps(funcOp), - expectedFusedGateCount(fx.context.get(), matrix, basis)); - } - if (basis == EulerBasis::ZYZ && - matrix.isApprox(Matrix2x2::identity())) { - EXPECT_EQ(countOps(funcOp), 0U); - EXPECT_EQ(countOps(funcOp), 0U); - } - if (basis == EulerBasis::U && matrix.isApprox(Matrix2x2::identity())) { - EXPECT_EQ(countOps(funcOp), 0U); - } + fx.ctx(), original, basis, + [&fx, basis](func::FuncOp funcOp, const Matrix2x2& matrix) { + checkSynthesizedReferenceExtras(fx.ctx(), funcOp, basis, matrix); }); } INSTANTIATE_TEST_SUITE_P( SingleQubitMatrices, EulerSynthesisExactTest, - testing::Combine( - testing::Values(EulerBasis::ZYZ, EulerBasis::ZXZ, EulerBasis::XZX, - EulerBasis::XYX, EulerBasis::U, EulerBasis::ZSXX), - testing::Values([](MLIRContext* /*ctx*/) - -> Matrix2x2 { return Matrix2x2::identity(); }, - [](MLIRContext* ctx) -> Matrix2x2 { - return rotationMatrix(ctx, 2.0); - }, - // RY(pi/2): ZSXX single-SX branch. - [](MLIRContext* ctx) -> Matrix2x2 { - return rotationMatrix(ctx, - std::numbers::pi / 2.0); - }, - [](MLIRContext* ctx) -> Matrix2x2 { - return rotationMatrix(ctx, 0.5); - }, - [](MLIRContext* ctx) -> Matrix2x2 { - return rotationMatrix(ctx, 3.14); - }, - [](MLIRContext* /*ctx*/) -> Matrix2x2 { - return HOp::getUnitaryMatrix(); - }))); + testing::Combine(testing::Values(ZYZ, ZXZ, XZX, XYX, U, ZSXX), + testing::Values( + [](MLIRContext* /*ctx*/) -> Matrix2x2 { + return Matrix2x2::identity(); + }, + [](MLIRContext* ctx) -> Matrix2x2 { + return rotationMatrix(ctx, 2.0); + }, + [](MLIRContext* ctx) -> Matrix2x2 { + return rotationMatrix(ctx, + std::numbers::pi / 2.0); + }, + [](MLIRContext* ctx) -> Matrix2x2 { + return rotationMatrix(ctx, 0.5); + }, + [](MLIRContext* ctx) -> Matrix2x2 { + return rotationMatrix(ctx, 3.14); + }, + [](MLIRContext* /*ctx*/) -> Matrix2x2 { + return HOp::getUnitaryMatrix(); + }))); TEST(EulerSynthesisTest, RandomReconstructionAllBases) { - SynthesisFixture fx; + TestFixture fx; fx.setUp(); - std::mt19937 rng{12345678UL}; - constexpr int iterations = 200; - for (int i = 0; i < iterations; ++i) { + for (int i = 0; i < 200; ++i) { const auto original = randomUnitaryMatrix(rng); - - forEachBasis([&](StringRef basisStr) { + forEachBasis([&fx, &original](StringRef basisStr) { const auto parsed = parseEulerBasis(basisStr); ASSERT_TRUE(parsed) << "basis=" << basisStr.str(); - - const auto circuit = - synthesizeMatrix(fx.context.get(), original, *parsed); - ASSERT_TRUE(succeeded(verify(*circuit.module))) + const auto circuit = synthesizeMatrix(fx.ctx(), original, *parsed); + ASSERT_TRUE(succeeded(verify(*circuit.mlirModule))) << "basis=" << basisStr.str(); expectMatrixPreserved(circuit.func, original, basisStr); }); @@ -829,305 +771,230 @@ TEST(EulerSynthesisTest, RandomReconstructionAllBases) { } TEST(FuseSingleQubitUnitaryRunsTest, InvalidBasisFailsPass) { - SynthesisFixture fx; + TestFixture fx; fx.setUp(); - - auto owned = QCOProgramBuilder::build(fx.context.get(), - &singleQubitRunWithSingleQubitGate); - ASSERT_TRUE(static_cast(owned)); - ModuleOp module = *owned; - ASSERT_TRUE(succeeded(verify(module))); - - EXPECT_TRUE(failed(runFuse(module, "not-a-basis"))); -} - -TEST(FuseSingleQubitUnitaryRunsTest, ReconstructsOriginalRunAllBases) { - SynthesisFixture fx; - fx.setUp(); - - runFuseOnProgramForAllBases( - fx.context.get(), &singleQubitRunWithSingleQubitGate, - [&](func::FuncOp funcOp, StringRef basis, const Matrix2x2& original) { - EXPECT_EQ(countOps(funcOp), 0U) << "basis=" << basis.str(); - expectMatrixPreserved(funcOp, original, basis); - expectBasisGatesOnly(funcOp, basis); - }); + auto owned = + QCOProgramBuilder::build(fx.ctx(), &singleQubitRunWithSingleQubitGate); + ASSERT_TRUE(owned); + EXPECT_TRUE(failed(runFuse(*owned, "not-a-basis"))); } -TEST(FuseSingleQubitUnitaryRunsTest, ResynthesizesLoneNonBasisGateAllBases) { - SynthesisFixture fx; +TEST(FuseSingleQubitUnitaryRunsTest, FusesProgramsAllBases) { + TestFixture fx; fx.setUp(); - runFuseOnProgramForAllBases( - fx.context.get(), &singleNonBasisGate, - [&](func::FuncOp funcOp, StringRef basis, const Matrix2x2& original) { - EXPECT_EQ(countOps(funcOp), 0U) - << "basis=" << basis.str() << " left a non-basis gate"; - expectMatrixPreserved(funcOp, original, basis); - expectBasisGatesOnly(funcOp, basis); - }); + struct Case { + void (*program)(QCOProgramBuilder&); + void (*extra)(func::FuncOp, StringRef); + }; + const std::array cases = {{ + {.program = &singleQubitRunWithSingleQubitGate, + .extra = + [](func::FuncOp funcOp, StringRef basis) { + EXPECT_EQ(countOps(funcOp), 0U) << basis.str(); + }}, + {.program = &singleNonBasisGate, + .extra = + [](func::FuncOp funcOp, StringRef basis) { + EXPECT_EQ(countOps(funcOp), 0U) << basis.str(); + }}, + }}; + + for (const Case& testCase : cases) { + runFuseForAllBases(fx.ctx(), testCase.program, + [&testCase](func::FuncOp funcOp, StringRef basis, + const Matrix2x2& original) { + testCase.extra(funcOp, basis); + expectFusePreserved(funcOp, original, basis); + }); + } } TEST(FuseSingleQubitUnitaryRunsTest, FusesOverlongInBasisRun) { - SynthesisFixture fx; + TestFixture fx; fx.setUp(); - runFuseOnProgram( - fx.context.get(), &overlongZYZRun, "zyz", + fx.ctx(), &overlongZYZRun, "zyz", [](func::FuncOp funcOp, const Matrix2x2&) { - ASSERT_EQ(countOps(funcOp) + countOps(funcOp), 6U); + ASSERT_EQ(countZYZGates(funcOp), 6U); }, - [&](func::FuncOp funcOp, const Matrix2x2& original) { - EXPECT_EQ(countOps(funcOp) + countOps(funcOp), - expectedFusedGateCount(fx.context.get(), original, - EulerBasis::ZYZ)); - expectMatrixPreserved(funcOp, original, "zyz"); - expectBasisGatesOnly(funcOp, "zyz"); + [&fx](func::FuncOp funcOp, const Matrix2x2& original) { + EXPECT_EQ(countZYZGates(funcOp), + expectedGateCount(fx.ctx(), original, ZYZ)); + expectFusePreserved(funcOp, original, "zyz"); }); } TEST(FuseSingleQubitUnitaryRunsTest, DoesNotFuseCanonicalInBasisRun) { - SynthesisFixture fx; + TestFixture fx; fx.setUp(); - runFuseOnProgram( - fx.context.get(), &singlePauliX, "zsxx", - [](func::FuncOp funcOp, const Matrix2x2&) { - ASSERT_EQ(countOps(funcOp), 1U); - EXPECT_EQ(countOps(funcOp), 0U); - EXPECT_EQ(countOps(funcOp), 0U); - }, - [&](func::FuncOp funcOp, const Matrix2x2& original) { - EXPECT_EQ(countOps(funcOp), 1U); - EXPECT_EQ(countZSXXBasisGates(funcOp), - expectedFusedGateCount(fx.context.get(), original, - EulerBasis::ZSXX)); - expectMatrixPreserved(funcOp, original, "zsxx"); - expectBasisGatesOnly(funcOp, "zsxx"); - }); + runFuseOnProgram(fx.ctx(), &singlePauliX, "zsxx", skipBeforeFuse, + [](func::FuncOp funcOp, const Matrix2x2& original) { + EXPECT_EQ(countOps(funcOp), 1U); + expectFusePreserved(funcOp, original, "zsxx"); + }); - runFuseOnProgram( - fx.context.get(), &canonicalZYZRun, "zyz", - [](func::FuncOp funcOp, const Matrix2x2&) { - ASSERT_EQ(countOps(funcOp) + countOps(funcOp), 3U); - }, - [&](func::FuncOp funcOp, const Matrix2x2& original) { - EXPECT_EQ(countOps(funcOp) + countOps(funcOp), 3U); - EXPECT_EQ(countOps(funcOp) + countOps(funcOp), - expectedFusedGateCount(fx.context.get(), original, - EulerBasis::ZYZ)); - expectMatrixPreserved(funcOp, original, "zyz"); - expectBasisGatesOnly(funcOp, "zyz"); - }); + runFuseOnProgram(fx.ctx(), &canonicalZYZRun, "zyz", skipBeforeFuse, + [](func::FuncOp funcOp, const Matrix2x2& original) { + EXPECT_EQ(countZYZGates(funcOp), 3U); + expectFusePreserved(funcOp, original, "zyz"); + }); } TEST(FuseSingleQubitUnitaryRunsTest, FusesOverlongZSXXMixedRunComposingToPureZ) { - SynthesisFixture fx; + TestFixture fx; fx.setUp(); - runFuseOnProgram( - fx.context.get(), &overlongZSXXMixedPureZRun, "zsxx", - [&](func::FuncOp funcOp, const Matrix2x2& /*original*/) { - EXPECT_EQ(expectedFusedGateCount(fx.context.get(), - overlongZSXXPureZRunMatrix(), - EulerBasis::ZSXX), - 1U); - ASSERT_EQ(countZSXXBasisGates(funcOp), 3U); - EXPECT_EQ(countOps(funcOp), 2U); - EXPECT_EQ(countOps(funcOp), 1U); - EXPECT_EQ(countOps(funcOp), 0U); + fx.ctx(), &overlongZSXXMixedPureZRun, "zsxx", + [](func::FuncOp funcOp, const Matrix2x2&) { + ASSERT_EQ(countZSXXGates(funcOp), 3U); }, - [&](func::FuncOp funcOp, const Matrix2x2& original) { - EXPECT_EQ(countOps(funcOp), 1U); - EXPECT_EQ(countOps(funcOp), 0U); - EXPECT_EQ(countOps(funcOp), 0U); - EXPECT_EQ(countZSXXBasisGates(funcOp), - expectedFusedGateCount(fx.context.get(), - overlongZSXXPureZRunMatrix(), - EulerBasis::ZSXX)); - expectMatrixPreserved(funcOp, original, "zsxx"); - expectBasisGatesOnly(funcOp, "zsxx"); - }); -} - -TEST(FuseSingleQubitUnitaryRunsTest, DoesNotFuseAcrossTwoQGateAllBases) { - SynthesisFixture fx; - fx.setUp(); - - runFuseOnProgramForAllBases( - fx.context.get(), &singleQubitRunsSplitByTwoQGate, - [&](func::FuncOp funcOp, StringRef basis, const Matrix2x2& original) { - std::size_t twoQubitGates = 0; - funcOp.walk([&](UnitaryOpInterface op) { - if (op.isTwoQubit()) { - ++twoQubitGates; - } - }); - EXPECT_EQ(twoQubitGates, 1U) << "basis=" << basis.str(); - expectMatrixPreserved(funcOp, original, basis); - expectBasisGatesOnly(funcOp, basis); - const auto parsed = parseEulerBasis(basis); - ASSERT_TRUE(parsed) << "basis=" << basis.str(); - expectOneQubitGatesAroundBoundary( - funcOp, basis, [](Operation& op) { return isTwoQubitGate(op); }, - expectedFusedGateCount(fx.context.get(), - splitFixtureHTSegmentMatrix(), *parsed), - expectedFusedGateCount(fx.context.get(), - splitFixtureRZSXSegmentMatrix(), *parsed)); + [&fx](func::FuncOp funcOp, const Matrix2x2& original) { + EXPECT_EQ( + countZSXXGates(funcOp), + expectedGateCount(fx.ctx(), overlongZSXXPureZRunMatrix(), ZSXX)); + expectFusePreserved(funcOp, original, "zsxx"); }); } -TEST(FuseSingleQubitUnitaryRunsTest, DoesNotFuseAcrossBarrierAllBases) { - SynthesisFixture fx; +TEST(FuseSingleQubitUnitaryRunsTest, DoesNotFuseAcrossBoundariesAllBases) { + TestFixture fx; fx.setUp(); - runFuseOnProgramForAllBases( - fx.context.get(), &singleQubitRunsSplitByBarrier, - [&](func::FuncOp funcOp, StringRef basis, const Matrix2x2& original) { - EXPECT_EQ(countOps(funcOp), 1U) << "basis=" << basis.str(); - expectMatrixPreserved(funcOp, original, basis); - expectBasisGatesOnly(funcOp, basis); - const auto parsed = parseEulerBasis(basis); - ASSERT_TRUE(parsed) << "basis=" << basis.str(); - expectOneQubitGatesAroundBoundary( - funcOp, basis, [](Operation& op) { return isa(op); }, - expectedFusedGateCount(fx.context.get(), - splitFixtureHTSegmentMatrix(), *parsed), - expectedFusedGateCount(fx.context.get(), - splitFixtureRZSXSegmentMatrix(), *parsed)); - }); + struct Case { + void (*program)(QCOProgramBuilder&); + void (*check)(func::FuncOp, StringRef, MLIRContext*); + }; + const std::array cases = {{ + {.program = &singleQubitRunsSplitByTwoQGate, + .check = + [](func::FuncOp funcOp, StringRef basis, MLIRContext* ctx) { + std::size_t twoQ = 0; + funcOp.walk([&twoQ](UnitaryOpInterface op) { + if (op.isTwoQubit()) { + ++twoQ; + } + }); + EXPECT_EQ(twoQ, 1U) << basis.str(); + expectSplitFixtureSegments( + funcOp, basis, ctx, [](const Operation& op) { + if (auto unitary = dyn_cast(op)) { + return unitary.isTwoQubit(); + } + return false; + }); + }}, + {.program = &singleQubitRunsSplitByBarrier, + .check = + [](func::FuncOp funcOp, StringRef basis, MLIRContext* ctx) { + EXPECT_EQ(countOps(funcOp), 1U) << basis.str(); + expectSplitFixtureSegments( + funcOp, basis, ctx, + [](const Operation& op) { return isa(op); }); + }}, + {.program = &singleQubitRunsSplitByScfFor, + .check = + [](func::FuncOp funcOp, StringRef basis, MLIRContext* ctx) { + EXPECT_EQ(countOps(funcOp), 1U) << basis.str(); + expectSplitFixtureSegments(funcOp, basis, ctx); + }}, + }}; + + for (const Case& testCase : cases) { + runFuseForAllBases(fx.ctx(), testCase.program, + [&testCase, &fx](func::FuncOp funcOp, StringRef basis, + const Matrix2x2& original) { + testCase.check(funcOp, basis, fx.ctx()); + expectFusePreserved(funcOp, original, basis); + }); + } } TEST(FuseSingleQubitUnitaryRunsTest, EliminatesIdentityInvMultiOpBody) { - SynthesisFixture fx; + TestFixture fx; fx.setUp(); - runFuseOnProgram( - fx.context.get(), xInverseTwoX, "u", + fx.ctx(), xInverseTwoX, "u", [](func::FuncOp funcOp, const Matrix2x2&) { EXPECT_EQ(countOps(funcOp), 4U); EXPECT_EQ(countOps(funcOp), 1U); - EXPECT_EQ(countOps(funcOp), 0U); }, - [&](func::FuncOp funcOp, const Matrix2x2& original) { + [&fx](func::FuncOp funcOp, const Matrix2x2& original) { EXPECT_EQ(countOps(funcOp), 0U); EXPECT_EQ(countOps(funcOp), 0U); - EXPECT_EQ( - countOps(funcOp), - expectedFusedGateCount(fx.context.get(), original, EulerBasis::U)); + EXPECT_EQ(countOps(funcOp), + expectedGateCount(fx.ctx(), original, U)); expectMatrixPreserved(funcOp, original, "x-inv-xx-x"); }); } TEST(FuseSingleQubitUnitaryRunsTest, FusesRunInMultiQubitInvBody) { - SynthesisFixture fx; + TestFixture fx; fx.setUp(); - - Matrix2x2 invBodyBefore; - runFuseOnProgram( - fx.context.get(), inverseMultiQubitBodySingleQubitRun, "u", - [&](func::FuncOp funcOp, const Matrix2x2&) { + runFuseInParent( + fx.ctx(), inverseMultiQubitBodySingleQubitRun, + [](func::FuncOp funcOp) { EXPECT_EQ(countOps(funcOp), 1U); - EXPECT_EQ(countOpsInRegion(funcOp, isInsideInv), 1U); - EXPECT_EQ(countOpsInRegion(funcOp, isInsideInv), 1U); - EXPECT_EQ(countOpsInRegion(funcOp, isInsideInv), 0U); - invBodyBefore = compute1QMatrixFromInvBody(funcOp); + EXPECT_EQ((countInParent(funcOp)), 1U); + EXPECT_EQ((countInParent(funcOp)), 1U); + EXPECT_EQ((countInParent(funcOp)), 0U); }, - [&](func::FuncOp funcOp, const Matrix2x2&) { - // The modifier survives; its single-qubit body chain fuses to one `u`. + [](func::FuncOp funcOp) { EXPECT_EQ(countOps(funcOp), 1U); - EXPECT_EQ(countOpsInRegion(funcOp, isInsideInv), 0U); - EXPECT_EQ(countOpsInRegion(funcOp, isInsideInv), 0U); - EXPECT_EQ(countOpsInRegion(funcOp, isInsideInv), 1U); - expectInvBodyMatrixPreserved(funcOp, invBodyBefore); + EXPECT_EQ((countInParent(funcOp)), 1U); + EXPECT_EQ((countInParent(funcOp)), 0U); + EXPECT_EQ((countInParent(funcOp)), 0U); }); } -TEST(FuseSingleQubitUnitaryRunsTest, FusesSingleNonBasisGateInCtrlBody) { - SynthesisFixture fx; +TEST(FuseSingleQubitUnitaryRunsTest, FusesInCtrlBody) { + TestFixture fx; fx.setUp(); - Matrix2x2 ctrlBodyBefore; - runFuseOnProgram( - fx.context.get(), controlledH, "u", - [&](func::FuncOp funcOp, const Matrix2x2&) { - EXPECT_EQ(countOps(funcOp), 1U); - EXPECT_EQ(countOpsInRegion(funcOp, isInsideCtrl), 1U); - EXPECT_EQ(countOpsInRegion(funcOp, isInsideCtrl), 0U); - ctrlBodyBefore = compute1QMatrixFromCtrlBody(funcOp); + runFuseInParent( + fx.ctx(), controlledH, + [](func::FuncOp funcOp) { + EXPECT_EQ((countInParent(funcOp)), 1U); + EXPECT_EQ((countInParent(funcOp)), 0U); }, - [&](func::FuncOp funcOp, const Matrix2x2&) { - EXPECT_EQ(countOps(funcOp), 1U); - EXPECT_EQ(countOpsInRegion(funcOp, isInsideCtrl), 0U); - EXPECT_EQ(countOpsInRegion(funcOp, isInsideCtrl), 1U); - expectCtrlBodyMatrixPreserved(funcOp, ctrlBodyBefore); + [](func::FuncOp funcOp) { + EXPECT_EQ((countInParent(funcOp)), 1U); + EXPECT_EQ((countInParent(funcOp)), 0U); }); -} - -TEST(FuseSingleQubitUnitaryRunsTest, FusesRunInCtrlBody) { - SynthesisFixture fx; - fx.setUp(); - Matrix2x2 ctrlBodyBefore; - runFuseOnProgram( - fx.context.get(), controlledInverseHT, "u", - [&](func::FuncOp funcOp, const Matrix2x2&) { - EXPECT_EQ(countOps(funcOp), 1U); - EXPECT_EQ(countOpsInRegion(funcOp, isInsideCtrl), 1U); - EXPECT_EQ(countOpsInRegion(funcOp, isInsideInv), 1U); - EXPECT_EQ(countOpsInRegion(funcOp, isInsideInv), 1U); - EXPECT_EQ(countOpsInRegion(funcOp, isInsideCtrl), 0U); - ctrlBodyBefore = compute1QMatrixFromCtrlBody(funcOp); + runFuseInParent( + fx.ctx(), controlledInverseHT, + [](func::FuncOp funcOp) { + EXPECT_EQ((countInParent(funcOp)), 1U); + EXPECT_EQ((countInParent(funcOp)), 1U); + EXPECT_EQ((countInParent(funcOp)), 1U); + EXPECT_EQ((countInParent(funcOp)), 0U); }, - [&](func::FuncOp funcOp, const Matrix2x2&) { - EXPECT_EQ(countOps(funcOp), 1U); - EXPECT_EQ(countOpsInRegion(funcOp, isInsideCtrl), 0U); - EXPECT_EQ(countOpsInRegion(funcOp, isInsideInv), 0U); - EXPECT_EQ(countOpsInRegion(funcOp, isInsideInv), 0U); - EXPECT_EQ(countOpsInRegion(funcOp, isInsideCtrl), 1U); - expectCtrlBodyMatrixPreserved(funcOp, ctrlBodyBefore); + [](func::FuncOp funcOp) { + EXPECT_EQ((countInParent(funcOp)), 0U); + EXPECT_EQ((countInParent(funcOp)), 1U); + EXPECT_EQ((countInParent(funcOp)), 0U); + EXPECT_EQ((countInParent(funcOp)), 0U); }); } TEST(FuseSingleQubitUnitaryRunsTest, FusesRunInScfForBody) { - SynthesisFixture fx; + TestFixture fx; fx.setUp(); - - runFuseOnProgram( - fx.context.get(), &singleQubitRunInScfFor, "u", - [](func::FuncOp funcOp, const Matrix2x2&) { - EXPECT_EQ(countOpsInScfFor(funcOp), 1U); - EXPECT_EQ(countOpsInScfFor(funcOp), 1U); - EXPECT_EQ(countOpsInScfFor(funcOp), 1U); - EXPECT_EQ(countOpsInScfFor(funcOp), 0U); + runFuseInParent( + fx.ctx(), &singleQubitRunInScfFor, + [](func::FuncOp funcOp) { + EXPECT_EQ((countInParent(funcOp)), 1U); + EXPECT_EQ((countInParent(funcOp)), 1U); + EXPECT_EQ((countInParent(funcOp)), 1U); + EXPECT_EQ((countInParent(funcOp)), 0U); }, - [](func::FuncOp funcOp, const Matrix2x2& original) { - EXPECT_EQ(countOpsInScfFor(funcOp), 1U); - EXPECT_EQ(countOpsInScfFor(funcOp), 0U); - EXPECT_EQ(countOpsInScfFor(funcOp), 0U); - EXPECT_EQ(countOpsInScfFor(funcOp), 0U); - expectMatrixPreserved(funcOp, original, "scf.for"); - }); -} - -TEST(FuseSingleQubitUnitaryRunsTest, DoesNotFuseAcrossScfForAllBases) { - SynthesisFixture fx; - fx.setUp(); - - runFuseOnProgramForAllBases( - fx.context.get(), &singleQubitRunsSplitByScfFor, - [&](func::FuncOp funcOp, StringRef basis, const Matrix2x2& original) { - EXPECT_EQ(countOps(funcOp), 1U) << "basis=" << basis.str(); - expectMatrixPreserved(funcOp, original, basis); - expectBasisGatesOnly(funcOp, basis); - const auto parsed = parseEulerBasis(basis); - ASSERT_TRUE(parsed) << "basis=" << basis.str(); - expectOneQubitGatesInAndOutsideScfFor( - funcOp, basis, - expectedFusedGateCount(fx.context.get(), - splitFixtureHTSegmentMatrix(), *parsed), - expectedFusedGateCount(fx.context.get(), - splitFixtureRZSXSegmentMatrix(), *parsed)); + [](func::FuncOp funcOp) { + EXPECT_EQ((countInParent(funcOp)), 1U); + EXPECT_EQ((countInParent(funcOp)), 0U); + EXPECT_EQ((countInParent(funcOp)), 0U); + EXPECT_EQ((countInParent(funcOp)), 0U); }); } From c9b7a3c9ce9f971a252467d7d3e60a5f184a19f7 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 15 Jun 2026 18:33:16 +0200 Subject: [PATCH 55/68] =?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 --- .../test_euler_decomposition.cpp | 164 ++++++++++-------- 1 file changed, 89 insertions(+), 75 deletions(-) diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp index 540162fde2..cb26d8b0ec 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -28,8 +29,11 @@ #include #include #include +#include #include +#include #include +#include #include #include @@ -44,6 +48,7 @@ #include #include #include +#include using namespace mlir; using namespace mlir::qco; @@ -77,19 +82,30 @@ struct ZSXXShortcutCase { class ZSXXShortcutTest : public testing::TestWithParam {}; -[[nodiscard]] Matrix2x2 rzMatrix(const double theta) { +struct SynthesizedCircuit { + OwningOpRef mlirModule; + func::FuncOp func; +}; + +class EulerSynthesisExactTest + : public testing::TestWithParam< + std::tuple> {}; + +} // namespace + +[[nodiscard]] static Matrix2x2 rzMatrix(const double theta) { const auto m00 = std::polar(1.0, -theta / 2.0); const auto m11 = std::polar(1.0, theta / 2.0); return Matrix2x2::fromElements(m00, 0, 0, m11); } -[[nodiscard]] Matrix2x2 ryMatrix(const double theta) { +[[nodiscard]] static Matrix2x2 ryMatrix(const double theta) { const auto m00 = std::cos(theta / 2.0); const auto m01 = -std::sin(theta / 2.0); return Matrix2x2::fromElements(m00, m01, -m01, m00); } -[[nodiscard]] Matrix2x2 randomUnitaryMatrix(std::mt19937& rng) { +[[nodiscard]] static Matrix2x2 randomUnitaryMatrix(std::mt19937& rng) { std::uniform_real_distribution dist(-std::numbers::pi, std::numbers::pi); const Matrix2x2 su2 = rzMatrix(dist(rng)) * ryMatrix(dist(rng)) * rzMatrix(dist(rng)); @@ -100,7 +116,8 @@ class ZSXXShortcutTest : public testing::TestWithParam {}; } template -[[nodiscard]] Matrix2x2 rotationMatrix(MLIRContext* ctx, const double theta) { +[[nodiscard]] static Matrix2x2 rotationMatrix(MLIRContext* ctx, + const double theta) { OpBuilder builder(ctx); auto mlirModule = ModuleOp::create(UnknownLoc::get(ctx)); builder.setInsertionPointToStart(mlirModule.getBody()); @@ -115,7 +132,7 @@ template return *matrix; } -template void forEachBasis(Fn fn) { +template static void forEachBasis(Fn fn) { const std::array bases = {"zyz", "zxz", "xzx", "xyx", "u", "zsxx"}; for (const char* basis : bases) { @@ -123,7 +140,8 @@ template void forEachBasis(Fn fn) { } } -[[nodiscard]] bool isAllowedBasisGate(const Operation& op, EulerBasis basis) { +[[nodiscard]] static bool isAllowedBasisGate(const Operation& op, + EulerBasis basis) { switch (basis) { case ZYZ: return isa(op); @@ -141,18 +159,19 @@ template void forEachBasis(Fn fn) { return false; } -template [[nodiscard]] bool inParent(Operation* op) { +template [[nodiscard]] static bool inParent(Operation* op) { return op != nullptr && op->getParentOfType() != nullptr; } -[[nodiscard]] WalkResult failMissingUnitaryMatrix(Operation* op, bool& failed) { +[[nodiscard]] static WalkResult failMissingUnitaryMatrix(Operation* op, + bool& failed) { ADD_FAILURE() << "Expected constant unitary matrix for op: " << op->getName().getStringRef().str(); failed = true; return WalkResult::interrupt(); } -[[nodiscard]] WalkResult +[[nodiscard]] static WalkResult accumulateConstantSingleQubit(UnitaryOpInterface unitary, Operation* op, Matrix2x2& acc, bool& failed) { if (Matrix2x2 matrix; unitary.getUnitaryMatrix2x2(matrix)) { @@ -162,8 +181,8 @@ accumulateConstantSingleQubit(UnitaryOpInterface unitary, Operation* op, return failMissingUnitaryMatrix(op, failed); } -WalkResult visit1QUnitaryOp(Operation* op, Matrix2x2& acc, - std::complex& global, bool& failed) { +static WalkResult visit1QUnitaryOp(Operation* op, Matrix2x2& acc, + std::complex& global, bool& failed) { if (isa(*op)) { return WalkResult::advance(); } @@ -193,8 +212,8 @@ WalkResult visit1QUnitaryOp(Operation* op, Matrix2x2& acc, return failed ? result : WalkResult::advance(); } -WalkResult visitBasisGateOp(Operation* op, StringRef basis, - EulerBasis parsedBasis) { +static WalkResult visitBasisGateOp(Operation* op, StringRef basis, + EulerBasis parsedBasis) { if (isa(*op)) { return WalkResult::advance(); } @@ -215,12 +234,13 @@ WalkResult visitBasisGateOp(Operation* op, StringRef basis, return WalkResult::advance(); } -void skipBeforeFuse(func::FuncOp /*funcOp*/, const Matrix2x2& /*original*/) { +static void skipBeforeFuse(func::FuncOp /*funcOp*/, + const Matrix2x2& /*original*/) { // Pre-fuse checks are not required for this scenario. } template -Matrix2x2 compute1QUnitaryMatrix(WalkRange& range) { +static Matrix2x2 compute1QUnitaryMatrix(WalkRange& range) { Matrix2x2 acc = Matrix2x2::identity(); std::complex global{1.0, 0.0}; bool failed = false; @@ -237,7 +257,7 @@ Matrix2x2 compute1QUnitaryMatrix(WalkRange& range) { } template -[[nodiscard]] Matrix2x2 matrixInParent(func::FuncOp funcOp) { +[[nodiscard]] static Matrix2x2 matrixInParent(func::FuncOp funcOp) { auto parents = funcOp.getOps(); if (parents.begin() == parents.end()) { ADD_FAILURE() << "Expected parent op in function"; @@ -246,14 +266,15 @@ template return compute1QUnitaryMatrix((*parents.begin()).getRegion()); } -void expectMatrixPreserved(func::FuncOp funcOp, const Matrix2x2& original, - StringRef label = {}) { +static void expectMatrixPreserved(func::FuncOp funcOp, + const Matrix2x2& original, + StringRef label = {}) { EXPECT_TRUE( compute1QUnitaryMatrix(funcOp).isApprox(original, MATRIX_TOLERANCE)) << label.str(); } -void expectBasisGatesOnly(func::FuncOp funcOp, StringRef basis) { +static void expectBasisGatesOnly(func::FuncOp funcOp, StringRef basis) { const auto parsed = parseEulerBasis(basis); ASSERT_TRUE(parsed) << basis.str(); @@ -263,43 +284,43 @@ void expectBasisGatesOnly(func::FuncOp funcOp, StringRef basis) { }); } -void expectFusePreserved(func::FuncOp funcOp, const Matrix2x2& original, - StringRef basis) { +static void expectFusePreserved(func::FuncOp funcOp, const Matrix2x2& original, + StringRef basis) { expectMatrixPreserved(funcOp, original, basis); expectBasisGatesOnly(funcOp, basis); } -[[nodiscard]] Matrix2x2 splitFixtureHTSegmentMatrix() { +[[nodiscard]] static Matrix2x2 splitFixtureHTSegmentMatrix() { return TOp::getUnitaryMatrix() * HOp::getUnitaryMatrix(); } -[[nodiscard]] Matrix2x2 splitFixtureRZSXSegmentMatrix() { +[[nodiscard]] static Matrix2x2 splitFixtureRZSXSegmentMatrix() { return SXOp::getUnitaryMatrix() * rzMatrix(0.321); } -[[nodiscard]] Matrix2x2 overlongZSXXPureZRunMatrix() { +[[nodiscard]] static Matrix2x2 overlongZSXXPureZRunMatrix() { return SXOp::getUnitaryMatrix() * rzMatrix(std::numbers::pi) * SXOp::getUnitaryMatrix(); } template -[[nodiscard]] std::size_t countOps(func::FuncOp funcOp) { +[[nodiscard]] static std::size_t countOps(func::FuncOp funcOp) { std::size_t count = 0; funcOp.walk([&count](OpTy) { ++count; }); return count; } -[[nodiscard]] std::size_t countZYZGates(func::FuncOp funcOp) { +[[nodiscard]] static std::size_t countZYZGates(func::FuncOp funcOp) { return countOps(funcOp) + countOps(funcOp); } -[[nodiscard]] std::size_t countZSXXGates(func::FuncOp funcOp) { +[[nodiscard]] static std::size_t countZSXXGates(func::FuncOp funcOp) { return countOps(funcOp) + countOps(funcOp) + countOps(funcOp); } -[[nodiscard]] std::size_t countBasisGates(func::FuncOp funcOp, - EulerBasis basis) { +[[nodiscard]] static std::size_t countBasisGates(func::FuncOp funcOp, + EulerBasis basis) { switch (basis) { case ZYZ: return countZYZGates(funcOp); @@ -318,7 +339,7 @@ template } template -[[nodiscard]] std::size_t countInParent(func::FuncOp funcOp) { +[[nodiscard]] static std::size_t countInParent(func::FuncOp funcOp) { std::size_t count = 0; funcOp.walk([&count](OpTy op) { if (inParent(op.getOperation())) { @@ -328,12 +349,7 @@ template return count; } -struct SynthesizedCircuit { - OwningOpRef mlirModule; - func::FuncOp func; -}; - -[[nodiscard]] SynthesizedCircuit +[[nodiscard]] static SynthesizedCircuit synthesizeMatrix(MLIRContext* ctx, const Matrix2x2& matrix, EulerBasis basis) { OwningOpRef mlirModule = ModuleOp::create(UnknownLoc::get(ctx)); OpBuilder builder(ctx); @@ -357,15 +373,16 @@ synthesizeMatrix(MLIRContext* ctx, const Matrix2x2& matrix, EulerBasis basis) { return SynthesizedCircuit{.mlirModule = std::move(mlirModule), .func = func}; } -[[nodiscard]] std::size_t expectedGateCount(MLIRContext* ctx, - const Matrix2x2& segment, - EulerBasis basis) { +[[nodiscard]] static std::size_t expectedGateCount(MLIRContext* ctx, + const Matrix2x2& segment, + EulerBasis basis) { return countBasisGates(synthesizeMatrix(ctx, segment, basis).func, basis); } -void checkSynthesizedReferenceExtras(MLIRContext* ctx, func::FuncOp funcOp, - EulerBasis basis, - const Matrix2x2& matrix) { +static void checkSynthesizedReferenceExtras(MLIRContext* ctx, + func::FuncOp funcOp, + EulerBasis basis, + const Matrix2x2& matrix) { if (basis == U) { EXPECT_EQ(countOps(funcOp), expectedGateCount(ctx, matrix, basis)); } @@ -381,16 +398,17 @@ void checkSynthesizedReferenceExtras(MLIRContext* ctx, func::FuncOp funcOp, } template -void expectSynthesizedMatrix(MLIRContext* ctx, const Matrix2x2& matrix, - EulerBasis basis, ExtraChecksT extraChecks) { +static void expectSynthesizedMatrix(MLIRContext* ctx, const Matrix2x2& matrix, + EulerBasis basis, + ExtraChecksT extraChecks) { const auto circuit = synthesizeMatrix(ctx, matrix, basis); ASSERT_TRUE(succeeded(verify(*circuit.mlirModule))); extraChecks(circuit.func, matrix); expectMatrixPreserved(circuit.func, matrix, "synthesis"); } -void expectSplitFixtureSegments(func::FuncOp funcOp, StringRef basis, - MLIRContext* ctx) { +static void expectSplitFixtureSegments(func::FuncOp funcOp, StringRef basis, + MLIRContext* ctx) { const auto parsed = parseEulerBasis(basis); ASSERT_TRUE(parsed) << basis.str(); const std::size_t ht = @@ -419,8 +437,9 @@ void expectSplitFixtureSegments(func::FuncOp funcOp, StringRef basis, } template -void expectSplitFixtureSegments(func::FuncOp funcOp, StringRef basis, - MLIRContext* ctx, BoundaryPred isBoundary) { +static void expectSplitFixtureSegments(func::FuncOp funcOp, StringRef basis, + MLIRContext* ctx, + BoundaryPred isBoundary) { const auto parsed = parseEulerBasis(basis); ASSERT_TRUE(parsed) << basis.str(); const std::size_t ht = @@ -453,7 +472,7 @@ void expectSplitFixtureSegments(func::FuncOp funcOp, StringRef basis, EXPECT_EQ(after, rzsx) << "basis=" << basis.str(); } -LogicalResult runFuse(ModuleOp mlirModule, StringRef basis) { +static LogicalResult runFuse(ModuleOp mlirModule, StringRef basis) { PassManager pm(mlirModule.getContext()); qco::FuseSingleQubitUnitaryRunsOptions opts; opts.basis = basis.str(); @@ -462,8 +481,9 @@ LogicalResult runFuse(ModuleOp mlirModule, StringRef basis) { } template -void runFuseOnProgram(MLIRContext* ctx, ProgramT program, StringRef basis, - BeforeT beforeFuse, AfterT afterFuse) { +static void runFuseOnProgram(MLIRContext* ctx, ProgramT program, + StringRef basis, BeforeT beforeFuse, + AfterT afterFuse) { auto owned = QCOProgramBuilder::build(ctx, program); ASSERT_TRUE(owned); ModuleOp mlirModule = *owned; @@ -483,8 +503,8 @@ void runFuseOnProgram(MLIRContext* ctx, ProgramT program, StringRef basis, } template -void runFuseForAllBases(MLIRContext* ctx, ProgramT program, - ChecksT checksAfter) { +static void runFuseForAllBases(MLIRContext* ctx, ProgramT program, + ChecksT checksAfter) { forEachBasis([&ctx, program, &checksAfter](StringRef basis) { runFuseOnProgram( ctx, program, basis, skipBeforeFuse, @@ -496,8 +516,8 @@ void runFuseForAllBases(MLIRContext* ctx, ProgramT program, template -void runFuseInParent(MLIRContext* ctx, ProgramT program, BeforeT checkBefore, - AfterT checkAfter) { +static void runFuseInParent(MLIRContext* ctx, ProgramT program, + BeforeT checkBefore, AfterT checkAfter) { Matrix2x2 bodyBefore; runFuseOnProgram( ctx, program, "u", @@ -514,7 +534,7 @@ void runFuseInParent(MLIRContext* ctx, ProgramT program, BeforeT checkBefore, // --- program builders ---------------------------------------------------- // -void singleQubitRunWithSingleQubitGate(QCOProgramBuilder& b) { +static void singleQubitRunWithSingleQubitGate(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.h(q[0]); q[0] = b.t(q[0]); @@ -525,7 +545,7 @@ void singleQubitRunWithSingleQubitGate(QCOProgramBuilder& b) { q[0] = b.ry(-0.456, q[0]); } -void singleQubitRunsSplitByTwoQGate(QCOProgramBuilder& b) { +static void singleQubitRunsSplitByTwoQGate(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); q[0] = b.h(q[0]); q[0] = b.t(q[0]); @@ -534,7 +554,7 @@ void singleQubitRunsSplitByTwoQGate(QCOProgramBuilder& b) { q[0] = b.sx(q[0]); } -void singleQubitRunsSplitByBarrier(QCOProgramBuilder& b) { +static void singleQubitRunsSplitByBarrier(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.h(q[0]); q[0] = b.t(q[0]); @@ -543,24 +563,24 @@ void singleQubitRunsSplitByBarrier(QCOProgramBuilder& b) { q[0] = b.sx(q[0]); } -void singleNonBasisGate(QCOProgramBuilder& b) { +static void singleNonBasisGate(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.h(q[0]); } -void singlePauliX(QCOProgramBuilder& b) { +static void singlePauliX(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.x(q[0]); } -void canonicalZYZRun(QCOProgramBuilder& b) { +static void canonicalZYZRun(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.rz(0.3, q[0]); q[0] = b.ry(0.5, q[0]); q[0] = b.rz(0.7, q[0]); } -void overlongZYZRun(QCOProgramBuilder& b) { +static void overlongZYZRun(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.rz(0.3, q[0]); q[0] = b.ry(0.5, q[0]); @@ -570,14 +590,14 @@ void overlongZYZRun(QCOProgramBuilder& b) { q[0] = b.ry(1.3, q[0]); } -void overlongZSXXMixedPureZRun(QCOProgramBuilder& b) { +static void overlongZSXXMixedPureZRun(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.sx(q[0]); q[0] = b.rz(std::numbers::pi, q[0]); q[0] = b.sx(q[0]); } -void singleQubitRunInScfFor(QCOProgramBuilder& b) { +static void singleQubitRunInScfFor(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); b.scfFor(0, 1, 1, ValueRange{q[0]}, [&b](Value, ValueRange iterArgs) { Value wire = iterArgs[0]; @@ -588,7 +608,7 @@ void singleQubitRunInScfFor(QCOProgramBuilder& b) { }); } -void xInverseTwoX(QCOProgramBuilder& b) { +static void xInverseTwoX(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.x(q[0]); q[0] = b.inv({q[0]}, [&b](ValueRange targets) { @@ -599,7 +619,7 @@ void xInverseTwoX(QCOProgramBuilder& b) { q[0] = b.x(q[0]); } -void inverseMultiQubitBodySingleQubitRun(QCOProgramBuilder& b) { +static void inverseMultiQubitBodySingleQubitRun(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); auto outs = b.inv({q[0], q[1]}, [&b](ValueRange targets) -> SmallVector { @@ -611,7 +631,7 @@ void inverseMultiQubitBodySingleQubitRun(QCOProgramBuilder& b) { q[1] = outs[1]; } -void controlledInverseHT(QCOProgramBuilder& b) { +static void controlledInverseHT(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.ctrl(q[0], q[1], [&b](ValueRange targets) { auto wire = b.inv({targets[0]}, [&b](ValueRange innerTargets) { @@ -623,13 +643,13 @@ void controlledInverseHT(QCOProgramBuilder& b) { }); } -void controlledH(QCOProgramBuilder& b) { +static void controlledH(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(2); b.ctrl(q[0], q[1], [&b](ValueRange targets) { return SmallVector{b.h(targets[0])}; }); } -void singleQubitRunsSplitByScfFor(QCOProgramBuilder& b) { +static void singleQubitRunsSplitByScfFor(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); q[0] = b.h(q[0]); q[0] = b.t(q[0]); @@ -641,8 +661,6 @@ void singleQubitRunsSplitByScfFor(QCOProgramBuilder& b) { }); } -} // namespace - TEST_P(ZSXXShortcutTest, SynthesisMatchesGateCount) { TestFixture fx; fx.setUp(); @@ -712,10 +730,6 @@ INSTANTIATE_TEST_SUITE_P( return std::string(info.param.label); }); -class EulerSynthesisExactTest - : public testing::TestWithParam< - std::tuple> {}; - TEST_P(EulerSynthesisExactTest, ReconstructsReferenceMatrices) { TestFixture fx; fx.setUp(); From 91c818412326b5bb906b1f064a06bf1ddb055c1a Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 15 Jun 2026 18:45:11 +0200 Subject: [PATCH 56/68] =?UTF-8?q?=F0=9F=9A=A8=20Fix=20linter=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/Decomposition/test_euler_decomposition.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp index cb26d8b0ec..c671700b08 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -18,8 +18,6 @@ #include "mlir/Dialect/Utils/Utils.h" #include -#include -#include #include #include #include @@ -28,11 +26,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include From ed4121e363f8da2c645da57dc8c6edc695e7d849 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 15 Jun 2026 18:48:46 +0200 Subject: [PATCH 57/68] =?UTF-8?q?=F0=9F=93=9D=20Streamline=20docstrings=20?= =?UTF-8?q?and=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../QCO/Transforms/Decomposition/Euler.h | 9 ++-- .../QCO/Transforms/Decomposition/Euler.cpp | 13 ++--- .../FuseSingleQubitUnitaryRuns.cpp | 49 +++++++------------ 3 files changed, 27 insertions(+), 44 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h index 680b679bdb..8fb0018b28 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Decomposition/Euler.h @@ -45,16 +45,15 @@ enum class EulerBasis : std::uint8_t { /** * @brief Synthesizes a composed single-qubit unitary as gates in @p basis. * - * Decomposes @p composed once. Returns `std::nullopt` when @p hasNonBasisGate - * is false and resynthesis would not shorten a run of @p runSize gates; - * otherwise emits gates (including `qco.gphase` when needed) and returns the - * output qubit. + * Returns `std::nullopt` when @p hasNonBasisGate is false and resynthesis + * would not shorten a run of @p runSize gates; otherwise emits gates + * (including `qco.gphase` when needed). * * @param builder Builder for the emitted operations. * @param loc Location for the emitted operations. * @param qubit Input qubit value. * @param composed Composed unitary to synthesize. - * @param runSize Number of gates in the run (for fusion profitability). + * @param runSize Number of gates in the run. * @param hasNonBasisGate Whether the run contains a gate outside @p basis. * @param basis The target Euler basis. * @return The synthesized qubit, or `std::nullopt` if synthesis is skipped. diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index 45ed9ab1e2..60a9747406 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -126,7 +126,7 @@ struct EulerAngles { * @return Z-Y-Z angles and global phase. */ [[nodiscard]] static EulerAngles paramsZYZ(const Matrix2x2& matrix) { - // det(U) = exp(2i*phase); invert the Z-Y-Z parameterization of U's entries. + // det(U) = exp(2i*phase) const Complex det = matrix.determinant(); const auto detArg = std::arg(det); const auto phase = 0.5 * detArg; @@ -159,7 +159,7 @@ struct EulerAngles { } /** - * @brief X-Z-X Euler angles (Z-X-Z under H conjugation, no Y sign flip). + * @brief X-Z-X Euler angles (Z-X-Z under H conjugation). * * @param matrix Single-qubit unitary to decompose. * @return X-Z-X angles and global phase. @@ -277,8 +277,7 @@ static void appendRotationIf(llvm::SmallVectorImpl& steps, /** * @brief Appends the three KAK rotations for @p basis to @p steps. * - * Uses @p angles as outer–middle–outer rotations - * (`K(phi) * A(theta) * K(lambda)` with axes from @p basis). + * `K(phi) * A(theta) * K(lambda)` with axes from @p basis. * * @param steps Planned gate sequence to extend. * @param angles Decomposed Euler angles and global phase. @@ -287,7 +286,6 @@ static void appendRotationIf(llvm::SmallVectorImpl& steps, static void appendKAKSteps(llvm::SmallVectorImpl& steps, const EulerAngles& angles, const EulerBasis basis) { using Kind = SynthesisStep::Kind; - // Outer (K) and middle (A) rotation axes per KAK basis. struct KAKAxes { Kind outer; Kind middle; @@ -316,9 +314,8 @@ static void appendKAKSteps(llvm::SmallVectorImpl& steps, * @brief Fills @p plan with an `RZ` / `SX` / `X` gate sequence from Z-Y-Z * angles. * - * Implements the canonical ZSXX synthesis cases (identity, `theta = 0`, - * `theta = pi/2`, `theta = pi`, and general) and sets @p plan.phase for any - * global-phase correction. + * Canonical ZSXX cases: identity, `theta = 0`, `theta = pi/2`, `theta = pi`, + * and general. * * @param plan Synthesis plan to populate. * @param zyz Z-Y-Z Euler angles of the target unitary. diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp index 9d8376db12..b782947575 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp @@ -40,9 +40,7 @@ namespace mlir::qco { namespace { -/** - * @brief Composed unitary and metadata for a fusable run, without storing ops. - */ +/** Composed unitary and metadata for a fusable run (ops are not stored). */ struct FusableRunScan { Matrix2x2 composed = Matrix2x2::identity(); std::size_t gateCount = 0; @@ -55,16 +53,13 @@ struct FusableRunScan { /** * @brief Whether `op` can take part in a fusable single-qubit run. * - * A run member is a non-barrier, single-qubit unitary whose 2x2 matrix is known - * at compile time. Parameterized gates only need constant parameters (no matrix - * is built); `inv` is the only parameter-free op that may still lack a constant - * matrix, so it is queried directly. An `inv` that hides a barrier in its body - * is rejected: its 2x2 matrix ignores the barrier, so absorbing the modifier - * would silently drop it. Such bodies instead fuse around the barrier in place. + * Parameterized gates only need constant parameters; `inv` is the only + * parameter-free op that may still lack a constant matrix. An `inv` that hides + * a barrier in its body is rejected: its 2x2 matrix ignores the barrier, so + * absorbing the modifier would silently drop it. * - * @param op The operation to test. May be null (e.g. a missing predecessor). - * @return `true` for a non-barrier single-qubit unitary with a compile-time 2x2 - * matrix that does not hide a barrier inside an `inv` body. + * @param op The operation to test. May be null. + * @return Whether `op` is a fusable run member. */ static bool isRunMember(Operation* op) { auto gate = dyn_cast_or_null(op); @@ -89,12 +84,11 @@ static bool isRunMember(Operation* op) { /** * @brief Whether `op` is a gate that Euler synthesis emits for `basis`. * - * Mirrors the synthesis step kinds in `Euler.cpp`; used to detect runs that are - * already in the target basis at canonical length. + * Mirrors the synthesis step kinds in `Euler.cpp`. * * @param op The operation to classify. * @param basis The target Euler basis. - * @return `true` if `op` is in the gate set Euler synthesis emits for `basis`. + * @return Whether `op` is in the gate set for `basis`. */ static bool isTargetBasisGate(Operation* op, decomposition::EulerBasis basis) { using decomposition::EulerBasis; @@ -119,10 +113,10 @@ static bool isTargetBasisGate(Operation* op, decomposition::EulerBasis basis) { * @brief Walks the wire from @p head, composing the run's matrix and metadata. * * `WireIterator` stops at region boundaries, so run members are consecutive on - * the wire. Only the run tail is retained, for replacement and erasure. + * the wire. * * @param head First gate of the run. - * @param basis Target Euler basis (for non-basis detection). + * @param basis Target Euler basis. * @return Composed matrix, gate count, and run tail. */ static FusableRunScan scanFusableRun(UnitaryOpInterface head, @@ -151,11 +145,14 @@ static FusableRunScan scanFusableRun(UnitaryOpInterface head, /** * @brief Erases a contiguous run from tail back to @p head. + * + * @param rewriter The pattern rewriter. + * @param head First gate of the run. + * @param tail Last gate of the run. */ static void eraseFusableRun(PatternRewriter& rewriter, UnitaryOpInterface head, UnitaryOpInterface tail) { - // Erase tail-first so each op is dead (its successor is already gone) when - // removed; capture the predecessor before erasing the current op. + // Tail-first: each erased op is dead once its successor is gone. UnitaryOpInterface current = tail; while (current.getOperation() != head.getOperation()) { auto pred = @@ -170,8 +167,6 @@ namespace { /** * @brief Fuses maximal single-qubit unitary runs via Euler resynthesis. - * - * Matches at each run head so each run is rewritten once. */ struct FuseSingleQubitUnitaryRunsPattern final : OpInterfaceRewritePattern { @@ -186,23 +181,15 @@ struct FuseSingleQubitUnitaryRunsPattern final * * A run does not start inside the body of a single-qubit `inv`: that modifier * is itself a run member, so the run on the parent wire absorbs the whole - * `inv` as one unitary and fusing its body in place would be redundant. A - * multi-qubit `inv` cannot be absorbed that way (it has no compile-time 2x2 - * matrix), so single-qubit chains inside its body are fused locally and may - * start runs. + * `inv` as one unitary. Multi-qubit `inv` bodies host their own runs. * * @param op The candidate run head. - * @return `true` if `op` is a run member whose wire predecessor is not itself - * a run member, and which is not inside the body of a fusable single-qubit - * `inv`. + * @return Whether `op` anchors a maximal fusable run. */ static bool isRunStart(UnitaryOpInterface op) { if (!isRunMember(op.getOperation())) { return false; } - // A single-qubit `inv` is itself a run member, so the parent wire's run - // already absorbs the whole modifier; only the bodies of multi-qubit `inv` - // modifiers (which cannot be absorbed) host their own runs. if (auto inv = op->getParentOfType(); inv && isRunMember(inv.getOperation())) { return false; From 88436482b26416068567be12481447ed578410f0 Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 15 Jun 2026 19:07:46 +0200 Subject: [PATCH 58/68] =?UTF-8?q?=F0=9F=8E=A8=20Remove=20unnecessary=20llv?= =?UTF-8?q?m=20namespace=20from=20SmallVector=20usage=20in=20Euler.cpp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index 60a9747406..c5e0ae41df 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -14,7 +14,6 @@ #include "mlir/Dialect/QCO/Utils/Matrix.h" #include "mlir/Dialect/Utils/Utils.h" -#include #include #include #include @@ -250,7 +249,7 @@ struct SynthesisStep { /** @brief Planned single-qubit Euler synthesis (gate list + optional `gphase`). */ struct Unitary1QEulerPlan { - llvm::SmallVector steps; + SmallVector steps; double phase = 0.0; /// @brief Number of native gates in the planned sequence (excludes `gphase`). @@ -266,7 +265,7 @@ struct Unitary1QEulerPlan { * @param kind Rotation axis (`RZ`, `RY`, or `RX`). * @param angle Rotation angle in radians. */ -static void appendRotationIf(llvm::SmallVectorImpl& steps, +static void appendRotationIf(SmallVectorImpl& steps, const SynthesisStep::Kind kind, const double angle) { if (!isNearZeroRotationAngle(angle)) { @@ -283,7 +282,7 @@ static void appendRotationIf(llvm::SmallVectorImpl& steps, * @param angles Decomposed Euler angles and global phase. * @param basis Target KAK basis (`ZYZ`, `ZXZ`, `XZX`, or `XYX`). */ -static void appendKAKSteps(llvm::SmallVectorImpl& steps, +static void appendKAKSteps(SmallVectorImpl& steps, const EulerAngles& angles, const EulerBasis basis) { using Kind = SynthesisStep::Kind; struct KAKAxes { From 61cddb0c5af0d9b339e43c011c9f3e15ccd83a5e Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 15 Jun 2026 19:19:39 +0200 Subject: [PATCH 59/68] =?UTF-8?q?=F0=9F=8E=A8=20Update=20function=20signat?= =?UTF-8?q?ures=20in=20FuseSingleQubitUnitaryRuns.cpp=20to=20use=20const?= =?UTF-8?q?=20for=20EulerBasis=20parameter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp index b782947575..4717148e21 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp @@ -90,7 +90,8 @@ static bool isRunMember(Operation* op) { * @param basis The target Euler basis. * @return Whether `op` is in the gate set for `basis`. */ -static bool isTargetBasisGate(Operation* op, decomposition::EulerBasis basis) { +static bool isTargetBasisGate(Operation* op, + const decomposition::EulerBasis basis) { using decomposition::EulerBasis; return TypeSwitch(op) .Case([&](auto) { @@ -120,7 +121,7 @@ static bool isTargetBasisGate(Operation* op, decomposition::EulerBasis basis) { * @return Composed matrix, gate count, and run tail. */ static FusableRunScan scanFusableRun(UnitaryOpInterface head, - decomposition::EulerBasis basis) { + const decomposition::EulerBasis basis) { FusableRunScan scan; const auto accumulate = [&](UnitaryOpInterface member) { const auto matrix = member.getUnitaryMatrix(); @@ -171,7 +172,7 @@ namespace { struct FuseSingleQubitUnitaryRunsPattern final : OpInterfaceRewritePattern { FuseSingleQubitUnitaryRunsPattern(MLIRContext* context, - decomposition::EulerBasis basis) + const decomposition::EulerBasis basis) : OpInterfaceRewritePattern(context), basis(basis) {} decomposition::EulerBasis basis; From 217407e457f5e68434e5849fcd55a7411ec02d1b Mon Sep 17 00:00:00 2001 From: Simon Hofmann Date: Mon, 15 Jun 2026 19:22:29 +0200 Subject: [PATCH 60/68] =?UTF-8?q?=E2=9C=85=20Organize=20test=5Feuler=5Fdec?= =?UTF-8?q?omposition.cpp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_euler_decomposition.cpp | 468 +++++++++--------- 1 file changed, 241 insertions(+), 227 deletions(-) diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp index c671700b08..f8a5d51186 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -55,6 +55,11 @@ using namespace mlir::qco; using namespace mlir::qco::decomposition; using enum EulerBasis; +// File layout: +// 1. Fixtures and parametric test types +// 2. Euler synthesis support + tests +// 3. FuseSingleQubitUnitaryRuns support + tests + namespace { struct TestFixture { @@ -93,6 +98,10 @@ class EulerSynthesisExactTest } // namespace +//===----------------------------------------------------------------------===// +// Euler synthesis support +//===----------------------------------------------------------------------===// + [[nodiscard]] static Matrix2x2 rzMatrix(const double theta) { const auto m00 = std::polar(1.0, -theta / 2.0); const auto m11 = std::polar(1.0, theta / 2.0); @@ -139,30 +148,6 @@ template static void forEachBasis(Fn fn) { fn(StringRef{basis}); } } - -[[nodiscard]] static bool isAllowedBasisGate(const Operation& op, - EulerBasis basis) { - switch (basis) { - case ZYZ: - return isa(op); - case ZXZ: - return isa(op); - case XZX: - return isa(op); - case XYX: - return isa(op); - case U: - return isa(op); - case ZSXX: - return isa(op); - } - return false; -} - -template [[nodiscard]] static bool inParent(Operation* op) { - return op != nullptr && op->getParentOfType() != nullptr; -} - [[nodiscard]] static WalkResult failMissingUnitaryMatrix(Operation* op, bool& failed) { ADD_FAILURE() << "Expected constant unitary matrix for op: " @@ -211,34 +196,6 @@ static WalkResult visit1QUnitaryOp(Operation* op, Matrix2x2& acc, accumulateConstantSingleQubit(unitary, op, acc, failed); return failed ? result : WalkResult::advance(); } - -static WalkResult visitBasisGateOp(Operation* op, StringRef basis, - EulerBasis parsedBasis) { - if (isa(*op)) { - return WalkResult::advance(); - } - if (auto unitary = dyn_cast(*op)) { - if (unitary.isTwoQubit() || isa(*op)) { - return unitary.isTwoQubit() ? WalkResult::advance() : WalkResult::skip(); - } - if (Matrix2x2 matrix; unitary.getUnitaryMatrix2x2(matrix)) { - EXPECT_TRUE(isAllowedBasisGate(*op, parsedBasis) || isa(*op)) - << "basis=" << basis.str() - << " unexpected gate: " << op->getName().getStringRef().str(); - return WalkResult::advance(); - } - ADD_FAILURE() << "basis=" << basis.str() << " missing constant matrix for: " - << op->getName().getStringRef().str(); - return WalkResult::interrupt(); - } - return WalkResult::advance(); -} - -static void skipBeforeFuse(func::FuncOp /*funcOp*/, - const Matrix2x2& /*original*/) { - // Pre-fuse checks are not required for this scenario. -} - template static Matrix2x2 compute1QUnitaryMatrix(WalkRange& range) { Matrix2x2 acc = Matrix2x2::identity(); @@ -255,17 +212,6 @@ static Matrix2x2 compute1QUnitaryMatrix(WalkRange& range) { } return acc * global; } - -template -[[nodiscard]] static Matrix2x2 matrixInParent(func::FuncOp funcOp) { - auto parents = funcOp.getOps(); - if (parents.begin() == parents.end()) { - ADD_FAILURE() << "Expected parent op in function"; - return Matrix2x2::fromElements(0, 0, 0, 0); - } - return compute1QUnitaryMatrix((*parents.begin()).getRegion()); -} - static void expectMatrixPreserved(func::FuncOp funcOp, const Matrix2x2& original, StringRef label = {}) { @@ -273,36 +219,6 @@ static void expectMatrixPreserved(func::FuncOp funcOp, compute1QUnitaryMatrix(funcOp).isApprox(original, MATRIX_TOLERANCE)) << label.str(); } - -static void expectBasisGatesOnly(func::FuncOp funcOp, StringRef basis) { - const auto parsed = parseEulerBasis(basis); - ASSERT_TRUE(parsed) << basis.str(); - - funcOp.walk( - [basis, parsedBasis = *parsed](Operation* op) { - return visitBasisGateOp(op, basis, parsedBasis); - }); -} - -static void expectFusePreserved(func::FuncOp funcOp, const Matrix2x2& original, - StringRef basis) { - expectMatrixPreserved(funcOp, original, basis); - expectBasisGatesOnly(funcOp, basis); -} - -[[nodiscard]] static Matrix2x2 splitFixtureHTSegmentMatrix() { - return TOp::getUnitaryMatrix() * HOp::getUnitaryMatrix(); -} - -[[nodiscard]] static Matrix2x2 splitFixtureRZSXSegmentMatrix() { - return SXOp::getUnitaryMatrix() * rzMatrix(0.321); -} - -[[nodiscard]] static Matrix2x2 overlongZSXXPureZRunMatrix() { - return SXOp::getUnitaryMatrix() * rzMatrix(std::numbers::pi) * - SXOp::getUnitaryMatrix(); -} - template [[nodiscard]] static std::size_t countOps(func::FuncOp funcOp) { std::size_t count = 0; @@ -338,17 +254,6 @@ template return 0; } -template -[[nodiscard]] static std::size_t countInParent(func::FuncOp funcOp) { - std::size_t count = 0; - funcOp.walk([&count](OpTy op) { - if (inParent(op.getOperation())) { - ++count; - } - }); - return count; -} - [[nodiscard]] static SynthesizedCircuit synthesizeMatrix(MLIRContext* ctx, const Matrix2x2& matrix, EulerBasis basis) { OwningOpRef mlirModule = ModuleOp::create(UnknownLoc::get(ctx)); @@ -407,6 +312,234 @@ static void expectSynthesizedMatrix(MLIRContext* ctx, const Matrix2x2& matrix, expectMatrixPreserved(circuit.func, matrix, "synthesis"); } +//===----------------------------------------------------------------------===// +// Euler synthesis tests +//===----------------------------------------------------------------------===// + +TEST_P(ZSXXShortcutTest, SynthesisMatchesGateCount) { + TestFixture fx; + fx.setUp(); + const auto& testCase = GetParam(); + const Matrix2x2 matrix = testCase.makeMatrix(fx.ctx()); + + expectSynthesizedMatrix( + fx.ctx(), matrix, ZSXX, + [&testCase, &fx](func::FuncOp funcOp, const Matrix2x2& original) { + EXPECT_EQ(countOps(funcOp), testCase.expectedRZ); + EXPECT_EQ(countOps(funcOp), testCase.expectedSX); + EXPECT_EQ(countOps(funcOp), testCase.expectedX); + EXPECT_EQ(countZSXXGates(funcOp), + expectedGateCount(fx.ctx(), original, ZSXX)); + }); +} + +INSTANTIATE_TEST_SUITE_P( + ZSXXShortcuts, ZSXXShortcutTest, + testing::Values( + ZSXXShortcutCase{ + "Identity", + [](MLIRContext*) -> Matrix2x2 { return Matrix2x2::identity(); }, 0, + 0, 0}, + ZSXXShortcutCase{ + "PauliX", + [](MLIRContext*) -> Matrix2x2 { return XOp::getUnitaryMatrix(); }, + 0, 0, 1}, + ZSXXShortcutCase{"PureZ", + [](MLIRContext*) -> Matrix2x2 { + return rzMatrix(0.3) * rzMatrix(0.7); + }, + 2, 0, 0}, + ZSXXShortcutCase{"ZYZNearZeroTheta", + [](MLIRContext*) -> Matrix2x2 { + constexpr double tol = 0.5 * mlir::utils::TOLERANCE; + return rzMatrix(0.4) * ryMatrix(tol) * rzMatrix(0.3); + }, + 2, 0, 0}, + ZSXXShortcutCase{"RYHalfPi", + [](MLIRContext* ctx) -> Matrix2x2 { + return rotationMatrix(ctx, + std::numbers::pi / 2.0); + }, + 2, 1, 0}, + ZSXXShortcutCase{"RYNearHalfPi", + [](MLIRContext* ctx) -> Matrix2x2 { + return rotationMatrix( + ctx, (std::numbers::pi / 2.0) + + (0.5 * mlir::utils::TOLERANCE)); + }, + 2, 1, 0}, + ZSXXShortcutCase{"RYNearZero", + [](MLIRContext* ctx) -> Matrix2x2 { + return rotationMatrix( + ctx, 0.5 * mlir::utils::TOLERANCE); + }, + 0, 0, 0}, + ZSXXShortcutCase{"RYNearPi", + [](MLIRContext* ctx) -> Matrix2x2 { + return rotationMatrix( + ctx, std::numbers::pi - + (0.5 * mlir::utils::TOLERANCE)); + }, + 1, 0, 1}), + [](const testing::TestParamInfo& info) { + return std::string(info.param.label); + }); + +TEST_P(EulerSynthesisExactTest, ReconstructsReferenceMatrices) { + TestFixture fx; + fx.setUp(); + const auto [basis, matrixFn] = GetParam(); + const Matrix2x2 original = matrixFn(fx.ctx()); + expectSynthesizedMatrix( + fx.ctx(), original, basis, + [&fx, basis](func::FuncOp funcOp, const Matrix2x2& matrix) { + checkSynthesizedReferenceExtras(fx.ctx(), funcOp, basis, matrix); + }); +} + +INSTANTIATE_TEST_SUITE_P( + SingleQubitMatrices, EulerSynthesisExactTest, + testing::Combine(testing::Values(ZYZ, ZXZ, XZX, XYX, U, ZSXX), + testing::Values( + [](MLIRContext* /*ctx*/) -> Matrix2x2 { + return Matrix2x2::identity(); + }, + [](MLIRContext* ctx) -> Matrix2x2 { + return rotationMatrix(ctx, 2.0); + }, + [](MLIRContext* ctx) -> Matrix2x2 { + return rotationMatrix(ctx, + std::numbers::pi / 2.0); + }, + [](MLIRContext* ctx) -> Matrix2x2 { + return rotationMatrix(ctx, 0.5); + }, + [](MLIRContext* ctx) -> Matrix2x2 { + return rotationMatrix(ctx, 3.14); + }, + [](MLIRContext* /*ctx*/) -> Matrix2x2 { + return HOp::getUnitaryMatrix(); + }))); + +TEST(EulerSynthesisTest, RandomReconstructionAllBases) { + TestFixture fx; + fx.setUp(); + std::mt19937 rng{12345678UL}; + + for (int i = 0; i < 200; ++i) { + const auto original = randomUnitaryMatrix(rng); + forEachBasis([&fx, &original](StringRef basisStr) { + const auto parsed = parseEulerBasis(basisStr); + ASSERT_TRUE(parsed) << "basis=" << basisStr.str(); + const auto circuit = synthesizeMatrix(fx.ctx(), original, *parsed); + ASSERT_TRUE(succeeded(verify(*circuit.mlirModule))) + << "basis=" << basisStr.str(); + expectMatrixPreserved(circuit.func, original, basisStr); + }); + } +} + +//===----------------------------------------------------------------------===// +// FuseSingleQubitUnitaryRuns support +//===----------------------------------------------------------------------===// + +[[nodiscard]] static bool isAllowedBasisGate(const Operation& op, + EulerBasis basis) { + switch (basis) { + case ZYZ: + return isa(op); + case ZXZ: + return isa(op); + case XZX: + return isa(op); + case XYX: + return isa(op); + case U: + return isa(op); + case ZSXX: + return isa(op); + } + return false; +} + +template [[nodiscard]] static bool inParent(Operation* op) { + return op != nullptr && op->getParentOfType() != nullptr; +} + +static WalkResult visitBasisGateOp(Operation* op, StringRef basis, + EulerBasis parsedBasis) { + if (isa(*op)) { + return WalkResult::advance(); + } + if (auto unitary = dyn_cast(*op)) { + if (unitary.isTwoQubit() || isa(*op)) { + return unitary.isTwoQubit() ? WalkResult::advance() : WalkResult::skip(); + } + if (Matrix2x2 matrix; unitary.getUnitaryMatrix2x2(matrix)) { + EXPECT_TRUE(isAllowedBasisGate(*op, parsedBasis) || isa(*op)) + << "basis=" << basis.str() + << " unexpected gate: " << op->getName().getStringRef().str(); + return WalkResult::advance(); + } + ADD_FAILURE() << "basis=" << basis.str() << " missing constant matrix for: " + << op->getName().getStringRef().str(); + return WalkResult::interrupt(); + } + return WalkResult::advance(); +} + +static void skipBeforeFuse(func::FuncOp /*funcOp*/, + const Matrix2x2& /*original*/) { + // Pre-fuse checks are not required for this scenario. +} + +template +[[nodiscard]] static Matrix2x2 matrixInParent(func::FuncOp funcOp) { + auto parents = funcOp.getOps(); + if (parents.begin() == parents.end()) { + ADD_FAILURE() << "Expected parent op in function"; + return Matrix2x2::fromElements(0, 0, 0, 0); + } + return compute1QUnitaryMatrix((*parents.begin()).getRegion()); +} + +static void expectBasisGatesOnly(func::FuncOp funcOp, StringRef basis) { + const auto parsed = parseEulerBasis(basis); + ASSERT_TRUE(parsed) << basis.str(); + + funcOp.walk( + [basis, parsedBasis = *parsed](Operation* op) { + return visitBasisGateOp(op, basis, parsedBasis); + }); +} + +static void expectFusePreserved(func::FuncOp funcOp, const Matrix2x2& original, + StringRef basis) { + expectMatrixPreserved(funcOp, original, basis); + expectBasisGatesOnly(funcOp, basis); +} +[[nodiscard]] static Matrix2x2 splitFixtureHTSegmentMatrix() { + return TOp::getUnitaryMatrix() * HOp::getUnitaryMatrix(); +} + +[[nodiscard]] static Matrix2x2 splitFixtureRZSXSegmentMatrix() { + return SXOp::getUnitaryMatrix() * rzMatrix(0.321); +} + +[[nodiscard]] static Matrix2x2 overlongZSXXPureZRunMatrix() { + return SXOp::getUnitaryMatrix() * rzMatrix(std::numbers::pi) * + SXOp::getUnitaryMatrix(); +} +template +[[nodiscard]] static std::size_t countInParent(func::FuncOp funcOp) { + std::size_t count = 0; + funcOp.walk([&count](OpTy op) { + if (inParent(op.getOperation())) { + ++count; + } + }); + return count; +} static void expectSplitFixtureSegments(func::FuncOp funcOp, StringRef basis, MLIRContext* ctx) { const auto parsed = parseEulerBasis(basis); @@ -532,7 +665,7 @@ static void runFuseInParent(MLIRContext* ctx, ProgramT program, }); } -// --- program builders ---------------------------------------------------- // +// --- Fuse program fixtures --- // static void singleQubitRunWithSingleQubitGate(QCOProgramBuilder& b) { auto q = b.allocQubitRegister(1); @@ -661,128 +794,9 @@ static void singleQubitRunsSplitByScfFor(QCOProgramBuilder& b) { }); } -TEST_P(ZSXXShortcutTest, SynthesisMatchesGateCount) { - TestFixture fx; - fx.setUp(); - const auto& testCase = GetParam(); - const Matrix2x2 matrix = testCase.makeMatrix(fx.ctx()); - - expectSynthesizedMatrix( - fx.ctx(), matrix, ZSXX, - [&testCase, &fx](func::FuncOp funcOp, const Matrix2x2& original) { - EXPECT_EQ(countOps(funcOp), testCase.expectedRZ); - EXPECT_EQ(countOps(funcOp), testCase.expectedSX); - EXPECT_EQ(countOps(funcOp), testCase.expectedX); - EXPECT_EQ(countZSXXGates(funcOp), - expectedGateCount(fx.ctx(), original, ZSXX)); - }); -} - -INSTANTIATE_TEST_SUITE_P( - ZSXXShortcuts, ZSXXShortcutTest, - testing::Values( - ZSXXShortcutCase{ - "Identity", - [](MLIRContext*) -> Matrix2x2 { return Matrix2x2::identity(); }, 0, - 0, 0}, - ZSXXShortcutCase{ - "PauliX", - [](MLIRContext*) -> Matrix2x2 { return XOp::getUnitaryMatrix(); }, - 0, 0, 1}, - ZSXXShortcutCase{"PureZ", - [](MLIRContext*) -> Matrix2x2 { - return rzMatrix(0.3) * rzMatrix(0.7); - }, - 2, 0, 0}, - ZSXXShortcutCase{"ZYZNearZeroTheta", - [](MLIRContext*) -> Matrix2x2 { - constexpr double tol = 0.5 * mlir::utils::TOLERANCE; - return rzMatrix(0.4) * ryMatrix(tol) * rzMatrix(0.3); - }, - 2, 0, 0}, - ZSXXShortcutCase{"RYHalfPi", - [](MLIRContext* ctx) -> Matrix2x2 { - return rotationMatrix(ctx, - std::numbers::pi / 2.0); - }, - 2, 1, 0}, - ZSXXShortcutCase{"RYNearHalfPi", - [](MLIRContext* ctx) -> Matrix2x2 { - return rotationMatrix( - ctx, (std::numbers::pi / 2.0) + - (0.5 * mlir::utils::TOLERANCE)); - }, - 2, 1, 0}, - ZSXXShortcutCase{"RYNearZero", - [](MLIRContext* ctx) -> Matrix2x2 { - return rotationMatrix( - ctx, 0.5 * mlir::utils::TOLERANCE); - }, - 0, 0, 0}, - ZSXXShortcutCase{"RYNearPi", - [](MLIRContext* ctx) -> Matrix2x2 { - return rotationMatrix( - ctx, std::numbers::pi - - (0.5 * mlir::utils::TOLERANCE)); - }, - 1, 0, 1}), - [](const testing::TestParamInfo& info) { - return std::string(info.param.label); - }); - -TEST_P(EulerSynthesisExactTest, ReconstructsReferenceMatrices) { - TestFixture fx; - fx.setUp(); - const auto [basis, matrixFn] = GetParam(); - const Matrix2x2 original = matrixFn(fx.ctx()); - expectSynthesizedMatrix( - fx.ctx(), original, basis, - [&fx, basis](func::FuncOp funcOp, const Matrix2x2& matrix) { - checkSynthesizedReferenceExtras(fx.ctx(), funcOp, basis, matrix); - }); -} - -INSTANTIATE_TEST_SUITE_P( - SingleQubitMatrices, EulerSynthesisExactTest, - testing::Combine(testing::Values(ZYZ, ZXZ, XZX, XYX, U, ZSXX), - testing::Values( - [](MLIRContext* /*ctx*/) -> Matrix2x2 { - return Matrix2x2::identity(); - }, - [](MLIRContext* ctx) -> Matrix2x2 { - return rotationMatrix(ctx, 2.0); - }, - [](MLIRContext* ctx) -> Matrix2x2 { - return rotationMatrix(ctx, - std::numbers::pi / 2.0); - }, - [](MLIRContext* ctx) -> Matrix2x2 { - return rotationMatrix(ctx, 0.5); - }, - [](MLIRContext* ctx) -> Matrix2x2 { - return rotationMatrix(ctx, 3.14); - }, - [](MLIRContext* /*ctx*/) -> Matrix2x2 { - return HOp::getUnitaryMatrix(); - }))); - -TEST(EulerSynthesisTest, RandomReconstructionAllBases) { - TestFixture fx; - fx.setUp(); - std::mt19937 rng{12345678UL}; - - for (int i = 0; i < 200; ++i) { - const auto original = randomUnitaryMatrix(rng); - forEachBasis([&fx, &original](StringRef basisStr) { - const auto parsed = parseEulerBasis(basisStr); - ASSERT_TRUE(parsed) << "basis=" << basisStr.str(); - const auto circuit = synthesizeMatrix(fx.ctx(), original, *parsed); - ASSERT_TRUE(succeeded(verify(*circuit.mlirModule))) - << "basis=" << basisStr.str(); - expectMatrixPreserved(circuit.func, original, basisStr); - }); - } -} +//===----------------------------------------------------------------------===// +// FuseSingleQubitUnitaryRuns tests +//===----------------------------------------------------------------------===// TEST(FuseSingleQubitUnitaryRunsTest, InvalidBasisFailsPass) { TestFixture fx; From a22d8ec49145a8fce7e98a9dbc77b8520bf9c45c Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Wed, 17 Jun 2026 14:31:53 +0200 Subject: [PATCH 61/68] =?UTF-8?q?=E2=9A=A1=20Optimize=20the=20implementati?= =?UTF-8?q?on=20of=20the=20Euler=20decomposition?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Lukas Burgholzer --- .../QCO/Transforms/Decomposition/Euler.cpp | 282 ++++++++---------- .../test_euler_decomposition.cpp | 4 +- 2 files changed, 123 insertions(+), 163 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index c5e0ae41df..1fe26a15db 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -15,7 +15,6 @@ #include "mlir/Dialect/Utils/Utils.h" #include -#include #include #include #include @@ -81,7 +80,7 @@ namespace mlir::qco::decomposition { * @param angle Rotation angle in radians. * @return `true` when no rotation gate should be emitted. */ -[[nodiscard]] static bool isNearZeroRotationAngle(double angle) { +[[nodiscard]] static bool isNearZeroRotationAngle(const double angle) { return std::abs(angle) <= utils::TOLERANCE; } @@ -93,7 +92,7 @@ namespace mlir::qco::decomposition { * @param phase Global phase in radians. */ static void emitGPhaseIfNeeded(OpBuilder& builder, Location loc, double phase) { - if (isNearZeroRotationAngle(phase)) { + if (isNearZeroRotationAngle(mod2pi(phase))) { return; } GPhaseOp::create(builder, loc, phase); @@ -176,12 +175,10 @@ struct EulerAngles { [[nodiscard]] static EulerAngles paramsXYX(const Matrix2x2& matrix) { // Shift outer angles by pi and fix global phase. const auto [theta, phi, lambda, phase] = paramsZYZ(hadamardConjugate(matrix)); - const auto newPhi = mod2pi(phi + std::numbers::pi); - const auto newLambda = mod2pi(lambda + std::numbers::pi); return {.theta = theta, - .phi = newPhi, - .lambda = newLambda, - .phase = phase + ((newPhi + newLambda - phi - lambda) / 2.)}; + .phi = phi + std::numbers::pi, + .lambda = lambda + std::numbers::pi, + .phase = phase + std::numbers::pi}; } /** @@ -201,7 +198,7 @@ struct EulerAngles { } /** - * @brief Extracts `(theta, phi, lambda, phase)` for KAK and `U` bases. + * @brief Extracts `(theta, phi, lambda, phase)` for all Euler bases. * * @param matrix The single-qubit unitary to decompose. * @param basis The target Euler basis. @@ -211,6 +208,7 @@ struct EulerAngles { const EulerBasis basis) { switch (basis) { case EulerBasis::ZYZ: + case EulerBasis::ZSXX: return paramsZYZ(matrix); case EulerBasis::ZXZ: return paramsZXZ(matrix); @@ -221,8 +219,7 @@ struct EulerAngles { case EulerBasis::U: return paramsU(matrix); default: - llvm::reportFatalInternalError( - "Unsupported Euler basis for angle computation in decomposition!"); + llvm_unreachable("invalid Euler basis"); } } @@ -254,125 +251,114 @@ struct Unitary1QEulerPlan { /// @brief Number of native gates in the planned sequence (excludes `gphase`). [[nodiscard]] std::size_t gateCount() const { return steps.size(); } -}; - -} // namespace -/** - * @brief Appends a rotation step when @p angle is outside tolerance. - * - * @param steps Planned gate sequence to extend. - * @param kind Rotation axis (`RZ`, `RY`, or `RX`). - * @param angle Rotation angle in radians. - */ -static void appendRotationIf(SmallVectorImpl& steps, - const SynthesisStep::Kind kind, - const double angle) { - if (!isNearZeroRotationAngle(angle)) { - steps.push_back({.kind = kind, .theta = angle}); + /** + * @brief Appends a rotation step for non-negligible angles. + * + * @param kind The rotation axis (RZ/RY/RX) + * @param angle The rotation angle in radians. + */ + void appendRotation(const SynthesisStep::Kind kind, const double angle) { + if (!isNearZeroRotationAngle(angle)) { + steps.emplace_back(kind, angle); + } } -} -/** - * @brief Appends the three KAK rotations for @p basis to @p steps. - * - * `K(phi) * A(theta) * K(lambda)` with axes from @p basis. - * - * @param steps Planned gate sequence to extend. - * @param angles Decomposed Euler angles and global phase. - * @param basis Target KAK basis (`ZYZ`, `ZXZ`, `XZX`, or `XYX`). - */ -static void appendKAKSteps(SmallVectorImpl& steps, - const EulerAngles& angles, const EulerBasis basis) { - using Kind = SynthesisStep::Kind; - struct KAKAxes { - Kind outer; - Kind middle; - }; - const auto axes = [&]() -> KAKAxes { + /** + * @brief Appends the decomposition for @p basis based on @p angles. + * + * @param angles The angles to use for the decomposition. + * @param basis The basis to use for the decomposition. + */ + void appendDecomposition(const EulerAngles& angles, const EulerBasis basis) { + if (isNearZeroRotationAngle(angles.theta) && + isNearZeroRotationAngle(angles.phi) && + isNearZeroRotationAngle(angles.lambda)) { + phase = angles.phase; + return; + } + + if (isNearZeroRotationAngle(angles.theta)) { + switch (basis) { + case EulerBasis::ZYZ: + case EulerBasis::ZXZ: + case EulerBasis::ZSXX: + appendRotation(SynthesisStep::Kind::RZ, angles.phi + angles.lambda); + break; + + case EulerBasis::XZX: + case EulerBasis::XYX: + appendRotation(SynthesisStep::Kind::RX, angles.phi + angles.lambda); + break; + case EulerBasis::U: + steps.emplace_back(SynthesisStep::Kind::U, 0.0, angles.phi, + angles.lambda); + break; + } + phase = angles.phase; + return; + } + switch (basis) { case EulerBasis::ZYZ: - return {.outer = Kind::RZ, .middle = Kind::RY}; + appendRotation(SynthesisStep::Kind::RZ, angles.lambda); + steps.emplace_back(SynthesisStep::Kind::RY, angles.theta); + appendRotation(SynthesisStep::Kind::RZ, angles.phi); + phase = angles.phase; + break; case EulerBasis::ZXZ: - return {.outer = Kind::RZ, .middle = Kind::RX}; + appendRotation(SynthesisStep::Kind::RZ, angles.lambda); + steps.emplace_back(SynthesisStep::Kind::RX, angles.theta); + appendRotation(SynthesisStep::Kind::RZ, angles.phi); + phase = angles.phase; + break; case EulerBasis::XZX: - return {.outer = Kind::RX, .middle = Kind::RZ}; + appendRotation(SynthesisStep::Kind::RX, angles.lambda); + steps.emplace_back(SynthesisStep::Kind::RZ, angles.theta); + appendRotation(SynthesisStep::Kind::RX, angles.phi); + phase = angles.phase; + break; case EulerBasis::XYX: - return {.outer = Kind::RX, .middle = Kind::RY}; - default: - llvm::reportFatalInternalError("Invalid Euler basis for KAK planning"); + appendRotation(SynthesisStep::Kind::RX, angles.lambda); + steps.emplace_back(SynthesisStep::Kind::RY, angles.theta); + appendRotation(SynthesisStep::Kind::RX, angles.phi); + phase = angles.phase; + break; + case EulerBasis::U: + steps.emplace_back(SynthesisStep::Kind::U, angles.theta, angles.phi, + angles.lambda); + phase = angles.phase; + break; + case EulerBasis::ZSXX: { + constexpr double pi = std::numbers::pi; + constexpr double halfPi = std::numbers::pi / 2.0; + constexpr double quarterPi = std::numbers::pi / 4.0; + + if (isNearZeroRotationAngle(angles.theta - halfPi)) { + appendRotation(SynthesisStep::Kind::RZ, angles.lambda - halfPi); + steps.emplace_back(SynthesisStep::Kind::SX); + appendRotation(SynthesisStep::Kind::RZ, angles.phi + halfPi); + phase = angles.phase - quarterPi; + return; + } + + appendRotation(SynthesisStep::Kind::RZ, angles.lambda); + if (isNearZeroRotationAngle(angles.theta - pi)) { + steps.emplace_back(SynthesisStep::Kind::X); + phase = angles.phase - halfPi; + } else { + steps.emplace_back(SynthesisStep::Kind::SX); + appendRotation(SynthesisStep::Kind::RZ, angles.theta + pi); + steps.emplace_back(SynthesisStep::Kind::SX); + phase = angles.phase + halfPi; + } + appendRotation(SynthesisStep::Kind::RZ, angles.phi + pi); + break; + } } - }(); - - appendRotationIf(steps, axes.outer, angles.lambda); - appendRotationIf(steps, axes.middle, angles.theta); - appendRotationIf(steps, axes.outer, angles.phi); -} - -/** - * @brief Fills @p plan with an `RZ` / `SX` / `X` gate sequence from Z-Y-Z - * angles. - * - * Canonical ZSXX cases: identity, `theta = 0`, `theta = pi/2`, `theta = pi`, - * and general. - * - * @param plan Synthesis plan to populate. - * @param zyz Z-Y-Z Euler angles of the target unitary. - */ -static void planZSXX(Unitary1QEulerPlan& plan, const EulerAngles& zyz) { - constexpr double pi = std::numbers::pi; - constexpr double halfPi = std::numbers::pi / 2.0; - constexpr double quarterPi = std::numbers::pi / 4.0; - - const auto theta = zyz.theta; - const auto phi = zyz.phi; - const auto lambda = zyz.lambda; - const auto pushRZ = [&](const double angle) { - appendRotationIf(plan.steps, SynthesisStep::Kind::RZ, angle); - }; - const auto pushSX = [&] { - plan.steps.push_back({.kind = SynthesisStep::Kind::SX}); - }; - const auto pushX = [&] { - plan.steps.push_back({.kind = SynthesisStep::Kind::X}); - }; - - if (isNearZeroRotationAngle(theta) && isNearZeroRotationAngle(phi) && - isNearZeroRotationAngle(lambda)) { - plan.phase = zyz.phase; - return; - } - - if (isNearZeroRotationAngle(theta)) { - pushRZ(lambda); - pushRZ(phi); - plan.phase = zyz.phase; - return; - } - - if (isNearZeroRotationAngle(theta - halfPi)) { - pushRZ(lambda - halfPi); - pushSX(); - pushRZ(phi + halfPi); - plan.phase = zyz.phase - quarterPi; - return; - } - - if (isNearZeroRotationAngle(theta - pi)) { - pushRZ(lambda); - pushX(); - pushRZ(phi + pi); - plan.phase = zyz.phase - halfPi; - return; } - - pushRZ(lambda); - pushSX(); - pushRZ(theta + pi); - pushSX(); - pushRZ(phi + pi); - plan.phase = zyz.phase + halfPi; -} +}; +} // namespace /** * @brief Builds a gate plan for @p targetMatrix in @p basis without emitting @@ -389,23 +375,8 @@ planUnitary1QEuler(const Matrix2x2& targetMatrix, const EulerBasis basis) { return plan; } - if (basis == EulerBasis::ZSXX) { - planZSXX(plan, anglesFromUnitary(targetMatrix, EulerBasis::ZYZ)); - return plan; - } - const EulerAngles angles = anglesFromUnitary(targetMatrix, basis); - plan.phase = angles.phase; - - if (basis == EulerBasis::U) { - plan.steps.push_back({.kind = SynthesisStep::Kind::U, - .theta = angles.theta, - .phi = angles.phi, - .lambda = angles.lambda}); - return plan; - } - - appendKAKSteps(plan.steps, angles, basis); + plan.appendDecomposition(angles, basis); return plan; } @@ -421,16 +392,16 @@ planUnitary1QEuler(const Matrix2x2& targetMatrix, const EulerBasis basis) { [[nodiscard]] static Value emitUnitary1QEulerPlan(OpBuilder& builder, Location loc, Value qubit, const Unitary1QEulerPlan& plan) { - for (const SynthesisStep& step : plan.steps) { - switch (step.kind) { + for (const auto& [kind, theta, phi, lambda] : plan.steps) { + switch (kind) { case SynthesisStep::Kind::RZ: - qubit = RZOp::create(builder, loc, qubit, step.theta).getQubitOut(); + qubit = RZOp::create(builder, loc, qubit, theta).getQubitOut(); break; case SynthesisStep::Kind::RY: - qubit = RYOp::create(builder, loc, qubit, step.theta).getQubitOut(); + qubit = RYOp::create(builder, loc, qubit, theta).getQubitOut(); break; case SynthesisStep::Kind::RX: - qubit = RXOp::create(builder, loc, qubit, step.theta).getQubitOut(); + qubit = RXOp::create(builder, loc, qubit, theta).getQubitOut(); break; case SynthesisStep::Kind::SX: qubit = SXOp::create(builder, loc, qubit).getQubitOut(); @@ -440,8 +411,7 @@ emitUnitary1QEulerPlan(OpBuilder& builder, Location loc, Value qubit, break; case SynthesisStep::Kind::U: qubit = - UOp::create(builder, loc, qubit, step.theta, step.phi, step.lambda) - .getQubitOut(); + UOp::create(builder, loc, qubit, theta, phi, lambda).getQubitOut(); break; } } @@ -450,24 +420,14 @@ emitUnitary1QEulerPlan(OpBuilder& builder, Location loc, Value qubit, } std::optional parseEulerBasis(StringRef basis) { - struct EulerBasisName { - const char* name; - EulerBasis value; - }; - constexpr std::array eulerBasisTable{{ - {.name = "zyz", .value = EulerBasis::ZYZ}, - {.name = "zxz", .value = EulerBasis::ZXZ}, - {.name = "xzx", .value = EulerBasis::XZX}, - {.name = "xyx", .value = EulerBasis::XYX}, - {.name = "u", .value = EulerBasis::U}, - {.name = "zsxx", .value = EulerBasis::ZSXX}, - }}; - for (const EulerBasisName& entry : eulerBasisTable) { - if (basis.equals_insensitive(entry.name)) { - return entry.value; - } - } - return std::nullopt; + return StringSwitch>(basis.lower()) + .Case("zyz", EulerBasis::ZYZ) + .Case("zxz", EulerBasis::ZXZ) + .Case("xzx", EulerBasis::XZX) + .Case("xyx", EulerBasis::XYX) + .Case("u", EulerBasis::U) + .Case("zsxx", EulerBasis::ZSXX) + .Default(std::nullopt); } std::optional 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 f8a5d51186..e2b5c094c1 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -348,13 +348,13 @@ INSTANTIATE_TEST_SUITE_P( [](MLIRContext*) -> Matrix2x2 { return rzMatrix(0.3) * rzMatrix(0.7); }, - 2, 0, 0}, + 1, 0, 0}, ZSXXShortcutCase{"ZYZNearZeroTheta", [](MLIRContext*) -> Matrix2x2 { constexpr double tol = 0.5 * mlir::utils::TOLERANCE; return rzMatrix(0.4) * ryMatrix(tol) * rzMatrix(0.3); }, - 2, 0, 0}, + 1, 0, 0}, ZSXXShortcutCase{"RYHalfPi", [](MLIRContext* ctx) -> Matrix2x2 { return rotationMatrix(ctx, From 85fa0fca768167c3b9fb1af7eb7dfad202f2bedb Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Wed, 17 Jun 2026 14:32:27 +0200 Subject: [PATCH 62/68] =?UTF-8?q?=F0=9F=8E=A8=20Better=20debuggability=20b?= =?UTF-8?q?y=20printing=20matrices=20upon=20mismatch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Lukas Burgholzer --- .../Decomposition/test_euler_decomposition.cpp | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp index e2b5c094c1..de088d51e0 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -215,9 +215,21 @@ static Matrix2x2 compute1QUnitaryMatrix(WalkRange& range) { static void expectMatrixPreserved(func::FuncOp funcOp, const Matrix2x2& original, StringRef label = {}) { - EXPECT_TRUE( - compute1QUnitaryMatrix(funcOp).isApprox(original, MATRIX_TOLERANCE)) - << label.str(); + // Logging of the matrices + auto printMatrix = [](const Matrix2x2& matrix) { + std::ostringstream oss; + oss.precision(4); + oss << std::fixed << "[[" << matrix(0, 0) << ", " << matrix(0, 1) << "],\n" + << " [" << matrix(1, 0) << ", " << matrix(1, 1) << "]]"; + return oss.str(); + }; + const auto printOriginal = printMatrix(original); + const auto actual = compute1QUnitaryMatrix(funcOp.getBody()); + const auto printActual = printMatrix(actual); + EXPECT_TRUE(actual.isApprox(original)) + << "Matrix not preserved for " << label.str() << ":\nOriginal:\n" + << printOriginal << "\nActual:\n" + << printActual; } template [[nodiscard]] static std::size_t countOps(func::FuncOp funcOp) { From 9806784ab14e77ecce22bb92e46e1f80c065d2a7 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Wed, 17 Jun 2026 16:06:51 +0200 Subject: [PATCH 63/68] =?UTF-8?q?=F0=9F=9A=B8=20Add=20efficient=20check=20?= =?UTF-8?q?for=20compile-time=20availability=20of=20a=20unitary=20matrix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Lukas Burgholzer --- mlir/include/mlir/Dialect/QCO/IR/QCODialect.h | 17 +++++++++++++++-- .../mlir/Dialect/QCO/IR/QCOInterfaces.td | 3 +++ mlir/include/mlir/Dialect/QCO/IR/QCOOps.td | 3 +++ mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp | 7 +++++++ mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp | 11 +++++++++++ 5 files changed, 39 insertions(+), 2 deletions(-) diff --git a/mlir/include/mlir/Dialect/QCO/IR/QCODialect.h b/mlir/include/mlir/Dialect/QCO/IR/QCODialect.h index 9ec5b5f384..1b08a1715a 100644 --- a/mlir/include/mlir/Dialect/QCO/IR/QCODialect.h +++ b/mlir/include/mlir/Dialect/QCO/IR/QCODialect.h @@ -10,6 +10,8 @@ #pragma once +#include "mlir/Dialect/Utils/Utils.h" + #include #include #include @@ -127,6 +129,16 @@ template class TargetAndParameterArityTrait { llvm::reportFatalUsageError( "Given qubit is not an input of the operation"); } + + [[nodiscard]] bool hasCompileTimeKnownUnitaryMatrix() { + if constexpr (P == 0) { + return true; + } else { + return llvm::all_of(this->getParameters(), [](Value param) { + return utils::valueToDouble(param).has_value(); + }); + } + } }; }; @@ -151,8 +163,9 @@ inline func::FuncOp getEntryPoint(ModuleOp op) { }; for (auto func : op.getOps()) { - const auto passthrough = func->getAttrOfType(PASSTHROUGH_LABEL); - if (passthrough && llvm::any_of(passthrough, isEntry)) { + if (const auto passthrough = + func->getAttrOfType(PASSTHROUGH_LABEL); + passthrough && llvm::any_of(passthrough, isEntry)) { return func; } } diff --git a/mlir/include/mlir/Dialect/QCO/IR/QCOInterfaces.td b/mlir/include/mlir/Dialect/QCO/IR/QCOInterfaces.td index fef9040251..9450fcab74 100644 --- a/mlir/include/mlir/Dialect/QCO/IR/QCOInterfaces.td +++ b/mlir/include/mlir/Dialect/QCO/IR/QCOInterfaces.td @@ -203,6 +203,9 @@ def UnitaryOpInterface : OpInterface<"UnitaryOpInterface"> { "StringRef", "getBaseSymbol", (ins)>, // Unitary matrix helpers + InterfaceMethod<"Returns true if the operation has a compile-time known " + "unitary matrix representation, false otherwise.", + "bool", "hasCompileTimeKnownUnitaryMatrix", (ins)>, InterfaceMethod<"Populates the given 1x1 unitary matrix if possible.", "bool", "getUnitaryMatrix1x1", (ins "Matrix1x1&":$out), unitaryMatrix1x1MethodBody>, diff --git a/mlir/include/mlir/Dialect/QCO/IR/QCOOps.td b/mlir/include/mlir/Dialect/QCO/IR/QCOOps.td index 17bab8174b..75f4ed1469 100644 --- a/mlir/include/mlir/Dialect/QCO/IR/QCOOps.td +++ b/mlir/include/mlir/Dialect/QCO/IR/QCOOps.td @@ -1030,6 +1030,7 @@ def BarrierOp : QCOOp<"barrier", traits = [UnitaryOpInterface]> { static Value getParameter(size_t i) { llvm::reportFatalUsageError("BarrierOp has no parameters"); } static OperandRange getParameters() { return {nullptr, 0}; } [[nodiscard]] static StringRef getBaseSymbol() { return "barrier"; } + [[nodiscard]] bool hasCompileTimeKnownUnitaryMatrix() const { return true; } }]; let builders = [OpBuilder<(ins "ValueRange":$qubits)>]; @@ -1126,6 +1127,7 @@ def CtrlOp : QCOOp<"ctrl", Value getParameter(size_t i) { llvm::reportFatalUsageError("CtrlOp does not have parameters"); } OperandRange getParameters() { return {nullptr, 0}; } [[nodiscard]] static StringRef getBaseSymbol() { return "ctrl"; } + [[nodiscard]] bool hasCompileTimeKnownUnitaryMatrix(); [[nodiscard]] std::optional getUnitaryMatrix(); }]; @@ -1199,6 +1201,7 @@ def InvOp : QCOOp<"inv", traits = [UnitaryOpInterface, Value getParameter(size_t i) { llvm::reportFatalUsageError("InvOp does not have parameters"); } OperandRange getParameters() { return {nullptr, 0}; } [[nodiscard]] static StringRef getBaseSymbol() { return "inv"; } + [[nodiscard]] bool hasCompileTimeKnownUnitaryMatrix(); [[nodiscard]] std::optional getUnitaryMatrix(); }]; diff --git a/mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp b/mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp index 9c5a46837a..0bec883fae 100644 --- a/mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp @@ -293,6 +293,13 @@ void CtrlOp::getCanonicalizationPatterns(RewritePatternSet& results, results.add(context); } +bool CtrlOp::hasCompileTimeKnownUnitaryMatrix() { + return all_of(getBody()->getOps(), + [](UnitaryOpInterface op) { + return op.hasCompileTimeKnownUnitaryMatrix(); + }); +} + std::optional CtrlOp::getUnitaryMatrix() { auto bodyUnitary = utils::getSoleBodyUnitary(*getBody()); if (!bodyUnitary) { diff --git a/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp b/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp index 721a129f6f..c6d6a36679 100644 --- a/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp @@ -405,6 +405,13 @@ void InvOp::getCanonicalizationPatterns(RewritePatternSet& results, CancelNestedInv, EraseEmptyInv>(context); } +bool InvOp::hasCompileTimeKnownUnitaryMatrix() { + return all_of(getBody()->getOps(), + [](UnitaryOpInterface op) { + return op.hasCompileTimeKnownUnitaryMatrix(); + }); +} + /** * @brief Composes compile-time single-qubit unitaries and returns the inverse. */ @@ -451,6 +458,10 @@ composeInvertedSingleQubitBodyMatrix(Block& block) { } std::optional InvOp::getUnitaryMatrix() { + if (getNumBodyUnitaries() == 0) { + return DynamicMatrix::identity(1LL << getNumTargets()); + } + if (auto bodyUnitary = utils::getSoleBodyUnitary(*getBody())) { if (const auto targetMatrix = From 8e5b09edfd83f7bf980547e38438146344258257 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Wed, 17 Jun 2026 16:07:31 +0200 Subject: [PATCH 64/68] =?UTF-8?q?=F0=9F=A9=B9=20Ensure=20Barrier=20defines?= =?UTF-8?q?=20its=20unitary=20matrix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Lukas Burgholzer --- mlir/include/mlir/Dialect/QCO/IR/QCOOps.td | 1 + .../Dialect/QCO/IR/Operations/StandardGates/BarrierOp.cpp | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/mlir/include/mlir/Dialect/QCO/IR/QCOOps.td b/mlir/include/mlir/Dialect/QCO/IR/QCOOps.td index 75f4ed1469..f6283bf943 100644 --- a/mlir/include/mlir/Dialect/QCO/IR/QCOOps.td +++ b/mlir/include/mlir/Dialect/QCO/IR/QCOOps.td @@ -1031,6 +1031,7 @@ def BarrierOp : QCOOp<"barrier", traits = [UnitaryOpInterface]> { static OperandRange getParameters() { return {nullptr, 0}; } [[nodiscard]] static StringRef getBaseSymbol() { return "barrier"; } [[nodiscard]] bool hasCompileTimeKnownUnitaryMatrix() const { return true; } + [[nodiscard]] DynamicMatrix getUnitaryMatrix(); }]; let builders = [OpBuilder<(ins "ValueRange":$qubits)>]; diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/BarrierOp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/BarrierOp.cpp index 1b6b50fd6d..31fc229342 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/BarrierOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/BarrierOp.cpp @@ -109,3 +109,8 @@ void BarrierOp::getCanonicalizationPatterns(RewritePatternSet& results, MLIRContext* context) { results.add(context); } + +DynamicMatrix BarrierOp::getUnitaryMatrix() { + const auto numQubits = getQubitsIn().size(); + return DynamicMatrix::identity(1LL << numQubits); +} From 177e534f773439800ac76b382143b870ff77e6ed Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Wed, 17 Jun 2026 16:08:07 +0200 Subject: [PATCH 65/68] =?UTF-8?q?=F0=9F=9A=B8=20Add=20a=20`WireRange`=20co?= =?UTF-8?q?nstruct=20that=20wraps=20a=20`WireIterator`=20for=20easy=20use?= =?UTF-8?q?=20in=20range-based=20constructs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Lukas Burgholzer --- .../mlir/Dialect/QCO/Utils/WireIterator.h | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/mlir/include/mlir/Dialect/QCO/Utils/WireIterator.h b/mlir/include/mlir/Dialect/QCO/Utils/WireIterator.h index 70ba4b26ff..6e5b395413 100644 --- a/mlir/include/mlir/Dialect/QCO/Utils/WireIterator.h +++ b/mlir/include/mlir/Dialect/QCO/Utils/WireIterator.h @@ -116,4 +116,25 @@ template <> struct WireTraversalTraits { : !isa(it.operation()); } }; + +/** + * @brief A range over the def-use chain of a qubit wire, usable in range-based + * for-loops. + * + * Example: + * @code + * for (auto* op : WireRange(qubit)) { ... } + * @endcode + */ +struct WireRange { + explicit WireRange(Value qubit) : begin_(qubit) {} + + [[nodiscard]] WireIterator begin() const { return begin_; } + [[nodiscard]] static std::default_sentinel_t end() { + return std::default_sentinel; + } + +private: + WireIterator begin_; +}; } // namespace mlir::qco From f094f332e39348bcd845b434fe762c087fd92754 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Wed, 17 Jun 2026 16:10:49 +0200 Subject: [PATCH 66/68] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20the=20imp?= =?UTF-8?q?lementation=20of=20the=20single-qubit=20gate=20fusion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Lukas Burgholzer --- .../FuseSingleQubitUnitaryRuns.cpp | 96 +++++-------------- 1 file changed, 24 insertions(+), 72 deletions(-) diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp index 4717148e21..fd51d37f62 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp @@ -14,7 +14,6 @@ #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/QCO/Utils/Matrix.h" #include "mlir/Dialect/QCO/Utils/WireIterator.h" -#include "mlir/Dialect/Utils/Utils.h" #include #include // IWYU pragma: keep (Passes.h.inc) @@ -24,12 +23,10 @@ #include #include #include -#include #include #include #include -#include #include #include @@ -40,7 +37,7 @@ namespace mlir::qco { namespace { -/** Composed unitary and metadata for a fusable run (ops are not stored). */ +/** Composed unitary and metadata for a fusable run. */ struct FusableRunScan { Matrix2x2 composed = Matrix2x2::identity(); std::size_t gateCount = 0; @@ -51,41 +48,18 @@ struct FusableRunScan { } // namespace /** - * @brief Whether `op` can take part in a fusable single-qubit run. - * - * Parameterized gates only need constant parameters; `inv` is the only - * parameter-free op that may still lack a constant matrix. An `inv` that hides - * a barrier in its body is rejected: its 2x2 matrix ignores the barrier, so - * absorbing the modifier would silently drop it. - * - * @param op The operation to test. May be null. - * @return Whether `op` is a fusable run member. + * @brief Whether `gate` can take part in a fusable single-qubit run. */ -static bool isRunMember(Operation* op) { - auto gate = dyn_cast_or_null(op); - if (!gate || !gate.isSingleQubit() || isa(op)) { +static bool isRunMember(UnitaryOpInterface gate) { + if (!gate || !gate.isSingleQubit() || isa(gate.getOperation())) { return false; } - for (size_t i = 0; i < gate.getNumParams(); ++i) { - if (!mlir::utils::valueToDouble(gate.getParameter(i))) { - return false; - } - } - if (gate.getNumParams() > 0 || !isa(op)) { - return true; - } - const bool hidesBarrier = op->walk([](BarrierOp) { - return WalkResult::interrupt(); - }).wasInterrupted(); - Matrix2x2 unused; - return !hidesBarrier && gate.getUnitaryMatrix2x2(unused); + return gate.hasCompileTimeKnownUnitaryMatrix(); } /** * @brief Whether `op` is a gate that Euler synthesis emits for `basis`. * - * Mirrors the synthesis step kinds in `Euler.cpp`. - * * @param op The operation to classify. * @param basis The target Euler basis. * @return Whether `op` is in the gate set for `basis`. @@ -113,9 +87,6 @@ static bool isTargetBasisGate(Operation* op, /** * @brief Walks the wire from @p head, composing the run's matrix and metadata. * - * `WireIterator` stops at region boundaries, so run members are consecutive on - * the wire. - * * @param head First gate of the run. * @param basis Target Euler basis. * @return Composed matrix, gate count, and run tail. @@ -123,29 +94,23 @@ static bool isTargetBasisGate(Operation* op, static FusableRunScan scanFusableRun(UnitaryOpInterface head, const decomposition::EulerBasis basis) { FusableRunScan scan; - const auto accumulate = [&](UnitaryOpInterface member) { + for (auto* op : WireRange(head.getOutputTarget(0))) { + auto member = dyn_cast_or_null(op); + if (!member || !isRunMember(member)) { + break; + } const auto matrix = member.getUnitaryMatrix(); assert(matrix && "run member must have a compile-time 2x2 matrix"); scan.composed.premultiplyBy(*matrix); - scan.hasNonBasisGate |= !isTargetBasisGate(member.getOperation(), basis); + scan.hasNonBasisGate |= !isTargetBasisGate(op, basis); scan.tail = member; ++scan.gateCount; - }; - - accumulate(head); - for (WireIterator it = std::next(WireIterator(head.getOutputTarget(0))); - it != std::default_sentinel; ++it) { - Operation* memberOp = it.operation(); - if (!isRunMember(memberOp)) { - break; - } - accumulate(cast(memberOp)); } return scan; } /** - * @brief Erases a contiguous run from tail back to @p head. + * @brief Erases a contiguous run from @p tail back to @p head. * * @param rewriter The pattern rewriter. * @param head First gate of the run. @@ -154,14 +119,14 @@ static FusableRunScan scanFusableRun(UnitaryOpInterface head, static void eraseFusableRun(PatternRewriter& rewriter, UnitaryOpInterface head, UnitaryOpInterface tail) { // Tail-first: each erased op is dead once its successor is gone. - UnitaryOpInterface current = tail; - while (current.getOperation() != head.getOperation()) { - auto pred = - cast(current.getInputTarget(0).getDefiningOp()); - rewriter.eraseOp(current.getOperation()); - current = pred; + auto it = WireIterator(tail.getOutputTarget(0)); + auto* target = head.getOperation(); + while (*it != target) { + auto* current = *it; + --it; + rewriter.eraseOp(current); } - rewriter.eraseOp(head.getOperation()); + rewriter.eraseOp(target); } namespace { @@ -180,22 +145,12 @@ struct FuseSingleQubitUnitaryRunsPattern final /** * @brief Whether `op` starts a run. * - * A run does not start inside the body of a single-qubit `inv`: that modifier - * is itself a run member, so the run on the parent wire absorbs the whole - * `inv` as one unitary. Multi-qubit `inv` bodies host their own runs. - * * @param op The candidate run head. * @return Whether `op` anchors a maximal fusable run. */ static bool isRunStart(UnitaryOpInterface op) { - if (!isRunMember(op.getOperation())) { - return false; - } - if (auto inv = op->getParentOfType(); - inv && isRunMember(inv.getOperation())) { - return false; - } - return !isRunMember(op.getInputTarget(0).getDefiningOp()); + return isRunMember(op) && !isRunMember(dyn_cast_or_null( + op.getInputTarget(0).getDefiningOp())); } /** @@ -215,12 +170,9 @@ struct FuseSingleQubitUnitaryRunsPattern final } FusableRunScan run = scanFusableRun(op, basis); - OpBuilder::InsertionGuard guard(rewriter); - rewriter.setInsertionPoint(op.getOperation()); - const std::optional qubitOut = - decomposition::synthesizeUnitary1QEuler( - rewriter, op.getLoc(), op.getInputTarget(0), run.composed, - run.gateCount, run.hasNonBasisGate, basis); + const auto qubitOut = decomposition::synthesizeUnitary1QEuler( + rewriter, op.getLoc(), op.getInputTarget(0), run.composed, + run.gateCount, run.hasNonBasisGate, basis); if (!qubitOut) { return failure(); } From a04bc3de5c979ba6239f452eb67e3597370ed02e Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Wed, 17 Jun 2026 16:11:54 +0200 Subject: [PATCH 67/68] =?UTF-8?q?=F0=9F=93=9D=20Add=20to=20changelog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Lukas Burgholzer --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4840a05246..bebe7635f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ with the exception that minor releases may include breaking changes. - ✨ Add a `fuse-single-qubit-unitary-runs` pass for fusing compile-time single-qubit unitary runs via Euler resynthesis - ([#1672]) ([**@simon1hofmann**]) + ([#1672]) ([**@simon1hofmann**], [**@burgholzer**]) - ✨ Add QIR program format support to the DDSIM QDMI Device ([#1766]) ([**@rturrado**]) - 🚸 Add [CMake presets] to provide a standardized From 0f86d0533645800b62b1cf5e7ee8e1a81962eb1c Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Wed, 17 Jun 2026 16:26:57 +0200 Subject: [PATCH 68/68] =?UTF-8?q?=F0=9F=9A=A8=20Address=20clang-tidy=20war?= =?UTF-8?q?nings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Lukas Burgholzer --- mlir/lib/Dialect/QCO/IR/Operations/StandardGates/BarrierOp.cpp | 1 + mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp | 2 +- .../Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp | 1 - .../QCO/Transforms/Decomposition/test_euler_decomposition.cpp | 2 ++ 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/BarrierOp.cpp b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/BarrierOp.cpp index 31fc229342..6aed75b199 100644 --- a/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/BarrierOp.cpp +++ b/mlir/lib/Dialect/QCO/IR/Operations/StandardGates/BarrierOp.cpp @@ -9,6 +9,7 @@ */ #include "mlir/Dialect/QCO/IR/QCOOps.h" +#include "mlir/Dialect/QCO/Utils/Matrix.h" #include #include diff --git a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp index 1fe26a15db..085d493abf 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Decomposition/Euler.cpp @@ -15,11 +15,11 @@ #include "mlir/Dialect/Utils/Utils.h" #include +#include #include #include #include -#include #include #include #include diff --git a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp index fd51d37f62..9a91146384 100644 --- a/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/NativeSynthesis/FuseSingleQubitUnitaryRuns.cpp @@ -17,7 +17,6 @@ #include #include // IWYU pragma: keep (Passes.h.inc) -#include #include #include #include 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 de088d51e0..fa9ba1d4ea 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Decomposition/test_euler_decomposition.cpp @@ -41,10 +41,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include