diff --git a/mlir/include/mlir/Dialect/MQTOpt/Transforms/Passes.h b/mlir/include/mlir/Dialect/MQTOpt/Transforms/Passes.h index 23ef1c3114..a00b5783d6 100644 --- a/mlir/include/mlir/Dialect/MQTOpt/Transforms/Passes.h +++ b/mlir/include/mlir/Dialect/MQTOpt/Transforms/Passes.h @@ -26,6 +26,7 @@ namespace mqt::ir::opt { #include "mlir/Dialect/MQTOpt/Transforms/Passes.h.inc" // IWYU pragma: export void populateGateEliminationPatterns(mlir::RewritePatternSet& patterns); +void populateGateDecompositionPatterns(mlir::RewritePatternSet& patterns); void populateMergeRotationGatesPatterns(mlir::RewritePatternSet& patterns); void populateElidePermutationsPatterns(mlir::RewritePatternSet& patterns); void populateQuantumSinkShiftPatterns(mlir::RewritePatternSet& patterns); diff --git a/mlir/include/mlir/Dialect/MQTOpt/Transforms/Passes.td b/mlir/include/mlir/Dialect/MQTOpt/Transforms/Passes.td index 48fbd4bd52..b8d56a4ed3 100644 --- a/mlir/include/mlir/Dialect/MQTOpt/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/MQTOpt/Transforms/Passes.td @@ -35,6 +35,12 @@ def GateElimination : Pass<"gate-elimination", "mlir::ModuleOp"> { }]; } +def GateDecomposition : Pass<"gate-decomposition", "mlir::ModuleOp"> { + let summary = "This pass will perform various gate decompositions to simplify the used gate set."; + let description = [{ + }]; +} + def MergeRotationGates : Pass<"merge-rotation-gates", "mlir::ModuleOp"> { let summary = "This pass searches for consecutive applications of rotation gates that can be merged."; let description = [{ diff --git a/mlir/lib/Dialect/MQTOpt/Transforms/CMakeLists.txt b/mlir/lib/Dialect/MQTOpt/Transforms/CMakeLists.txt index 5e86b17e08..0d196e92a6 100644 --- a/mlir/lib/Dialect/MQTOpt/Transforms/CMakeLists.txt +++ b/mlir/lib/Dialect/MQTOpt/Transforms/CMakeLists.txt @@ -7,7 +7,7 @@ # Licensed under the MIT License get_property(dialect_libs GLOBAL PROPERTY MLIR_DIALECT_LIBS) -set(LIBRARIES ${dialect_libs} MQT::CoreIR) +set(LIBRARIES ${dialect_libs} MQT::CoreIR MQT::CoreDD) add_compile_options(-fexceptions) file(GLOB TRANSFORMS_SOURCES *.cpp) diff --git a/mlir/lib/Dialect/MQTOpt/Transforms/GateDecomposition.cpp b/mlir/lib/Dialect/MQTOpt/Transforms/GateDecomposition.cpp new file mode 100644 index 0000000000..a612aca957 --- /dev/null +++ b/mlir/lib/Dialect/MQTOpt/Transforms/GateDecomposition.cpp @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM + * Copyright (c) 2025 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "mlir/Dialect/MQTOpt/Transforms/Passes.h" + +#include +#include +#include +#include + +namespace mqt::ir::opt { + +#define GEN_PASS_DEF_GATEDECOMPOSITION +#include "mlir/Dialect/MQTOpt/Transforms/Passes.h.inc" + +/** + * @brief This pass attempts to cancel consecutive self-inverse operations. + */ +struct GateDecomposition final + : impl::GateDecompositionBase { + + void runOnOperation() override { + // Get the current operation being operated on. + auto op = getOperation(); + auto* ctx = &getContext(); + + // Define the set of patterns to use. + mlir::RewritePatternSet patterns(ctx); + populateGateDecompositionPatterns(patterns); + + // Apply patterns in an iterative and greedy manner. + if (mlir::failed(mlir::applyPatternsGreedily(op, std::move(patterns)))) { + signalPassFailure(); + } + } +}; + +} // namespace mqt::ir::opt diff --git a/mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp b/mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp new file mode 100644 index 0000000000..ec950c25d4 --- /dev/null +++ b/mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp @@ -0,0 +1,264 @@ +/* + * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM + * Copyright (c) 2025 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#include "Helpers.h" +#include "mlir/Dialect/MQTOpt/IR/MQTOptDialect.h" +#include "mlir/Dialect/MQTOpt/Transforms/Passes.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mqt::ir::opt { +/** + * @brief This pattern TODO. + */ +struct EulerDecompositionPattern final + : mlir::OpInterfaceRewritePattern { + + explicit EulerDecompositionPattern(mlir::MLIRContext* context) + : OpInterfaceRewritePattern(context) {} + + mlir::LogicalResult + matchAndRewrite(UnitaryInterface op, + mlir::PatternRewriter& rewriter) const override { + if (!helpers::isSingleQubitOperation(op)) { + return mlir::failure(); + } + + auto series = getSingleQubitSeries(op); + if (series.size() <= 3) { + // TODO: find better way to prevent endless optimization loop + return mlir::failure(); + } + + dd::GateMatrix unitaryMatrix = dd::opToSingleQubitGateMatrix(qc::I); + for (auto&& gate : series) { + if (auto gateMatrix = helpers::getUnitaryMatrix(gate)) { + unitaryMatrix = helpers::multiply(unitaryMatrix, *gateMatrix); + } + } + + auto [decomposedGateSchematic, globalPhase] = + calculateRotationGates(unitaryMatrix); + + // apply global phase + createOneParameterGate(rewriter, op->getLoc(), globalPhase, {}); + + auto newGates = createMlirGates(rewriter, decomposedGateSchematic, + op.getInQubits().front()); + if (!newGates.empty()) { + // attach new gates by replacing the uses of the last gate of the series + rewriter.replaceAllOpUsesWith(series.back(), newGates.back()); + } else { + // gate series is equal to identity; remove it entirely + rewriter.replaceAllOpUsesWith(series.back(), op->getOperands()); + } + + // delete in reverse order since last use has been replaced and for the + // others the only use will be deleted before the operation + for (auto&& gate : llvm::reverse(series)) { + rewriter.eraseOp(gate); + } + + return mlir::success(); + } + + [[nodiscard]] static llvm::SmallVector + getSingleQubitSeries(UnitaryInterface op) { + llvm::SmallVector result = {op}; + while (op->hasOneUse()) { + op = getNextOperation(op); + if (op && helpers::isSingleQubitOperation(op)) { + result.push_back(op); + } else { + break; + } + } + return result; + } + + [[nodiscard]] static UnitaryInterface getNextOperation(UnitaryInterface op) { + // since there is only one output qubit in single qubit gates, there should + // only be one user + assert(op->hasOneUse()); + auto&& users = op->getUsers(); + return llvm::dyn_cast(*users.begin()); + } + + /** + * @brief Creates a new rotation gate with no controls. + * + * @tparam OpType The type of the operation to be created. + * @param op The first instance of the rotation gate. + * @param rewriter The pattern rewriter. + * @return A new rotation gate. + */ + template + static OpType createOneParameterGate(mlir::PatternRewriter& rewriter, + mlir::Location location, + qc::fp parameter, + mlir::ValueRange inQubits) { + auto parameterValue = rewriter.create( + location, rewriter.getF64Type(), rewriter.getF64FloatAttr(parameter)); + + return rewriter.create( + location, inQubits.getType(), mlir::TypeRange{}, mlir::TypeRange{}, + mlir::DenseF64ArrayAttr{}, mlir::DenseBoolArrayAttr{}, + mlir::ValueRange{parameterValue}, inQubits, mlir::ValueRange{}, + mlir::ValueRange{}); + } + + [[nodiscard]] static llvm::SmallVector createMlirGates( + mlir::PatternRewriter& rewriter, + const llvm::SmallVector, 3>& schematic, + mlir::Value inQubit) { + llvm::SmallVector result; + for (auto [type, angle] : schematic) { + if (type == qc::RZ) { + auto newRz = createOneParameterGate(rewriter, inQubit.getLoc(), + angle, {inQubit}); + result.push_back(newRz); + } else if (type == qc::RY) { + auto newRy = createOneParameterGate(rewriter, inQubit.getLoc(), + angle, {inQubit}); + result.push_back(newRy); + } else { + throw std::logic_error{"Unable to create MLIR gate in Euler " + "Decomposition (unsupported gate)"}; + } + inQubit = result.back().getOutQubits().front(); + } + return result; + } + + /** + * @note Adapted from circuit_kak() in the IBM Qiskit framework. + * (C) Copyright IBM 2022 + * + * This code is licensed under the Apache License, Version 2.0. You may + * obtain a copy of this license in the LICENSE.txt file in the root + * directory of this source tree or at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Any modifications or derivative works of this code must retain this + * copyright notice, and modified files need to carry a notice + * indicating that they have been altered from the originals. + */ + [[nodiscard]] static std::pair< + llvm::SmallVector, 3>, qc::fp> + calculateRotationGates(dd::GateMatrix unitaryMatrix) { + constexpr qc::fp angleZeroEpsilon = 1e-12; + + auto remEuclid = [](qc::fp a, qc::fp b) { + auto r = std::fmod(a, b); + return (r < 0.0) ? r + std::abs(b) : r; + }; + // Wrap angle into interval [-π,π). If within atol of the endpoint, clamp to + // -π + auto mod2pi = [&](qc::fp angle) -> qc::fp { + // remEuclid() isn't exactly the same as Python's % operator, but + // because the RHS here is a constant and positive it is effectively + // equivalent for this case + auto wrapped = remEuclid(angle + qc::PI, 2. * qc::PI) - qc::PI; + if (std::abs(wrapped - qc::PI) < angleZeroEpsilon) { + return -qc::PI; + } + return wrapped; + }; + + auto [theta, phi, lambda, phase] = paramsZyzInner(unitaryMatrix); + qc::fp globalPhase = phase - ((phi + lambda) / 2.); + + llvm::SmallVector, 3> gates; + if (std::abs(theta) < angleZeroEpsilon) { + lambda += phi; + lambda = mod2pi(lambda); + if (std::abs(lambda) > angleZeroEpsilon) { + gates.push_back({qc::RZ, lambda}); + globalPhase += lambda / 2.0; + } + return {gates, globalPhase}; + } + + if (std::abs(theta - qc::PI) < angleZeroEpsilon) { + globalPhase += phi; + lambda -= phi; + phi = 0.0; + } + if (std::abs(mod2pi(lambda + qc::PI)) < angleZeroEpsilon || + std::abs(mod2pi(phi + qc::PI)) < angleZeroEpsilon) { + lambda += qc::PI; + theta = -theta; + phi += qc::PI; + } + lambda = mod2pi(lambda); + if (std::abs(lambda) > angleZeroEpsilon) { + globalPhase += lambda / 2.0; + gates.push_back({qc::RZ, lambda}); + } + gates.push_back({qc::RY, theta}); + phi = mod2pi(phi); + if (std::abs(phi) > angleZeroEpsilon) { + globalPhase += phi / 2.0; + gates.push_back({qc::RZ, phi}); + } + return {gates, globalPhase}; + } + + /** + * @note Adapted from circuit_kak() in the IBM Qiskit framework. + * (C) Copyright IBM 2022 + * + * This code is licensed under the Apache License, Version 2.0. You may + * obtain a copy of this license in the LICENSE.txt file in the root + * directory of this source tree or at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Any modifications or derivative works of this code must retain this + * copyright notice, and modified files need to carry a notice + * indicating that they have been altered from the originals. + */ + [[nodiscard]] static std::array + paramsZyzInner(dd::GateMatrix unitaryMatrix) { + auto getIndex = [](auto x, auto y) { return (y * 2) + x; }; + auto determinant = [getIndex](auto&& matrix) { + return (matrix.at(getIndex(0, 0)) * matrix.at(getIndex(1, 1))) - + (matrix.at(getIndex(1, 0)) * matrix.at(getIndex(0, 1))); + }; + + auto detArg = std::arg(determinant(unitaryMatrix)); + auto phase = 0.5 * detArg; + auto theta = 2. * std::atan2(std::abs(unitaryMatrix.at(getIndex(1, 0))), + std::abs(unitaryMatrix.at(getIndex(0, 0)))); + auto ang1 = std::arg(unitaryMatrix.at(getIndex(1, 1))); + auto ang2 = std::arg(unitaryMatrix.at(getIndex(1, 0))); + auto phi = ang1 + ang2 - detArg; + auto lam = ang1 - ang2; + return {theta, phi, lam, phase}; + } +}; + +/** + * @brief Populates the given pattern set with patterns for gate elimination. + * + * @param patterns The pattern set to populate. + */ +void populateGateDecompositionPatterns(mlir::RewritePatternSet& patterns) { + patterns.add(patterns.getContext()); +} + +} // namespace mqt::ir::opt diff --git a/mlir/lib/Dialect/MQTOpt/Transforms/Helpers.h b/mlir/lib/Dialect/MQTOpt/Transforms/Helpers.h new file mode 100644 index 0000000000..80a76d28f4 --- /dev/null +++ b/mlir/lib/Dialect/MQTOpt/Transforms/Helpers.h @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2023 - 2025 Chair for Design Automation, TUM + * Copyright (c) 2025 Munich Quantum Software Company GmbH + * All rights reserved. + * + * SPDX-License-Identifier: MIT + * + * Licensed under the MIT License + */ + +#pragma once + +#include "dd/GateMatrixDefinitions.hpp" +#include "dd/Package.hpp" +#include "ir/Definitions.hpp" +#include "mlir/Dialect/MQTOpt/IR/MQTOptDialect.h" + +#include +#include +#include + +namespace mqt::ir::opt::helpers { + +std::optional mlirValueToFp(mlir::Value value); + +template +std::optional performMlirFloatBinaryOp(mlir::Value value, Func&& func) { + if (auto op = value.getDefiningOp()) { + auto lhs = mlirValueToFp(op.getLhs()); + auto rhs = mlirValueToFp(op.getRhs()); + if (lhs && rhs) { + return std::invoke(std::forward(func), *lhs, *rhs); + } + } + return std::nullopt; +} + +template +std::optional performMlirFloatUnaryOp(mlir::Value value, Func&& func) { + if (auto op = value.getDefiningOp()) { + if (auto operand = mlirValueToFp(op.getOperand())) { + return std::invoke(std::forward(func), *operand); + } + } + return std::nullopt; +} + +inline std::optional mlirValueToFp(mlir::Value value) { + if (auto op = value.getDefiningOp()) { + if (auto attr = llvm::dyn_cast(op.getValue())) { + return attr.getValueAsDouble(); + } + return std::nullopt; + } + if (auto result = performMlirFloatUnaryOp( + value, [](qc::fp a) { return -a; })) { + return result; + } + if (auto result = performMlirFloatUnaryOp( + value, [](qc::fp a) { return a; })) { + return result; + } + if (auto result = performMlirFloatUnaryOp( + value, [](qc::fp a) { return a; })) { + return result; + } + if (auto result = performMlirFloatBinaryOp( + value, [](qc::fp a, qc::fp b) { return std::max(a, b); })) { + return result; + } + if (auto result = performMlirFloatBinaryOp( + value, [](qc::fp a, qc::fp b) { return std::max(a, b); })) { + return result; + } + if (auto result = performMlirFloatBinaryOp( + value, [](qc::fp a, qc::fp b) { return std::min(a, b); })) { + return result; + } + if (auto result = performMlirFloatBinaryOp( + value, [](qc::fp a, qc::fp b) { return std::min(a, b); })) { + return result; + } + if (auto result = performMlirFloatBinaryOp( + value, [](qc::fp a, qc::fp b) { return std::fmod(a, b); })) { + return result; + } + if (auto result = performMlirFloatBinaryOp( + value, [](qc::fp a, qc::fp b) { return a + b; })) { + return result; + } + if (auto result = performMlirFloatBinaryOp( + value, [](qc::fp a, qc::fp b) { return a + b; })) { + return result; + } + if (auto result = performMlirFloatBinaryOp( + value, [](qc::fp a, qc::fp b) { return a + b; })) { + return result; + } + if (auto result = performMlirFloatBinaryOp( + value, [](qc::fp a, qc::fp b) { return a + b; })) { + return result; + } + return std::nullopt; +} + +[[nodiscard]] inline std::vector getParameters(UnitaryInterface op) { + std::vector parameters; + for (auto&& param : op.getParams()) { + if (auto value = helpers::mlirValueToFp(param)) { + parameters.push_back(*value); + } + } + return parameters; +} + +[[nodiscard]] inline qc::OpType getQcType(UnitaryInterface op) { + try { + const std::string type = op->getName().stripDialect().str(); + return qc::opTypeFromString(type); + } catch (const std::invalid_argument& /*exception*/) { + return qc::OpType::None; + } +} + +[[nodiscard]] inline bool isSingleQubitOperation(UnitaryInterface op) { + auto&& inQubits = op.getInQubits(); + auto&& outQubits = op.getOutQubits(); + bool isSingleQubitOp = + inQubits.size() == 1 && outQubits.size() == 1 && !op.isControlled(); + assert(isSingleQubitOp == qc::isSingleQubitGate(getQcType(op))); + return isSingleQubitOp; +} + +[[nodiscard]] inline std::optional +getUnitaryMatrix(UnitaryInterface op) { + auto type = getQcType(op); + auto parameters = getParameters(op); + + if (isSingleQubitOperation(op)) { + return dd::opToSingleQubitGateMatrix(type, parameters); + } + return std::nullopt; +} + +[[nodiscard]] inline dd::GateMatrix multiply(dd::GateMatrix lhs, + dd::GateMatrix rhs) { + return { + lhs.at(0) * rhs.at(0) + lhs.at(1) * rhs.at(2), + lhs.at(0) * rhs.at(1) + lhs.at(1) * rhs.at(3), + lhs.at(2) * rhs.at(0) + lhs.at(3) * rhs.at(2), + lhs.at(2) * rhs.at(1) + lhs.at(3) * rhs.at(3), + }; +} +} // namespace mqt::ir::opt::helpers diff --git a/mlir/test/Dialect/MQTOpt/Transforms/gate-decomposition.mlir b/mlir/test/Dialect/MQTOpt/Transforms/gate-decomposition.mlir new file mode 100644 index 0000000000..1e23a5d70f --- /dev/null +++ b/mlir/test/Dialect/MQTOpt/Transforms/gate-decomposition.mlir @@ -0,0 +1,105 @@ +// Copyright (c) 2023 - 2025 Chair for Design Automation, TUM +// Copyright (c) 2025 Munich Quantum Software Company GmbH +// All rights reserved. +// +// SPDX-License-Identifier: MIT +// +// Licensed under the MIT License + +// RUN: quantum-opt %s -split-input-file --gate-decomposition | FileCheck %s + +// ----- +// This test checks if single-qubit consecutive self-inverses are canceled correctly. + +module { + // CHECK-LABEL: func.func @testNegationSeries + func.func @testNegationSeries() { + // CHECK: %[[ANY:.*]] = arith.constant 0.000000e+00 + + // CHECK: %[[Q0_0:.*]] = mqtopt.allocQubit + + // CHECK: %[[Q0_1:.*]] = mqtopt.x() %[[Q0_0]] + // CHECK: %[[Q0_2:.*]] = mqtopt.x() %[[Q0_1]] + + // CHECK-NOT: %[[ANY:.*]] = mqtopt.x + + // CHECK: mqtopt.deallocQubit %[[Q0_2]] + + %q0_0 = mqtopt.allocQubit + + %q0_1 = mqtopt.x() %q0_0 : !mqtopt.Qubit + %q0_2 = mqtopt.x() %q0_1 : !mqtopt.Qubit + %q0_3 = mqtopt.x() %q0_2 : !mqtopt.Qubit + %q0_4 = mqtopt.x() %q0_3 : !mqtopt.Qubit + %q0_5 = mqtopt.x() %q0_4 : !mqtopt.Qubit + %q0_6 = mqtopt.x() %q0_5 : !mqtopt.Qubit + + mqtopt.deallocQubit %q0_6 + + return + } +} + +module { + // CHECK-LABEL: func.func @testMergeRotationSeries + func.func @testMergeRotationSeries() { + // CHECK: %[[C_0:.*]] = arith.constant 1.5707963267948966 + // CHECK: %[[C_1:.*]] = arith.constant 2.2831853071795867 + // CHECK: %[[C_2:.*]] = arith.constant -1.5707963267948966 + // CHECK: %[[C_3:.*]] = arith.constant -3.1415926535897931 + + // CHECK: %[[Q0_0:.*]] = mqtopt.allocQubit + + // CHECK: mqtopt.gphase(%[[C_3]]) + // CHECK: %[[Q0_1:.*]] = mqtopt.rz(%[[C_2]]) %[[Q0_0]] + // CHECK: %[[Q0_2:.*]] = mqtopt.ry(%[[C_1]]) %[[Q0_1]] + // CHECK: %[[Q0_3:.*]] = mqtopt.rz(%[[C_0]]) %[[Q0_2]] + + // CHECK-NOT: %[[ANY:.*]] = mqtopt.rz + // CHECK-NOT: %[[ANY:.*]] = mqtopt.ry + + // CHECK: mqtopt.deallocQubit %[[Q0_3]] + + %q0_0 = mqtopt.allocQubit + + %c_0 = arith.constant 1.000000e+00 : f64 + %q0_1 = mqtopt.rx(%c_0) %q0_0 : !mqtopt.Qubit + %q0_2 = mqtopt.rx(%c_0) %q0_1 : !mqtopt.Qubit + %q0_3 = mqtopt.rx(%c_0) %q0_2 : !mqtopt.Qubit + %q0_4 = mqtopt.rx(%c_0) %q0_3 : !mqtopt.Qubit + + mqtopt.deallocQubit %q0_4 + + return + } +} + + +module { + // CHECK-LABEL: func.func @testNoMergeSmallSeries + func.func @testNoMergeSmallSeries() { + // CHECK: %[[C_0:.*]] = arith.constant 1.000000e+00 + + // CHECK: %[[Q0_0:.*]] = mqtopt.allocQubit + + // CHECK: %[[Q0_1:.*]] = mqtopt.rx(%[[C_0]]) %[[Q0_0:.*]] + // CHECK: %[[Q0_2:.*]] = mqtopt.ry(%[[C_0]]) %[[Q0_1:.*]] + // CHECK: %[[Q0_3:.*]] = mqtopt.rz(%[[C_0]]) %[[Q0_2:.*]] + + // CHECK-NOT: %[[ANY:.*]] = mqtopt.rz + // CHECK-NOT: %[[ANY:.*]] = mqtopt.ry + + // CHECK: mqtopt.deallocQubit %[[Q0_3]] + + %q0_0 = mqtopt.allocQubit + + %c_0 = arith.constant 1.000000e+00 : f64 + %q0_1 = mqtopt.rx(%c_0) %q0_0 : !mqtopt.Qubit + %q0_2 = mqtopt.ry(%c_0) %q0_1 : !mqtopt.Qubit + %q0_3 = mqtopt.rz(%c_0) %q0_2 : !mqtopt.Qubit + + mqtopt.deallocQubit %q0_3 + + return + } +}