Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions mlir/include/mlir/Dialect/MQTOpt/Transforms/Passes.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
6 changes: 6 additions & 0 deletions mlir/include/mlir/Dialect/MQTOpt/Transforms/Passes.td
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [{
}];
Comment on lines +39 to +41
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let summary = "This pass will perform various gate decompositions to simplify the used gate set.";
let description = [{
}];
let summary = "This pass performs various gate decompositions to translate quantum gates being used.";
let description = [{
}];

also needs a full 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 = [{
Expand Down
2 changes: 1 addition & 1 deletion mlir/lib/Dialect/MQTOpt/Transforms/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should generally avoid to add further dependencies to existing MQT Core libraries if at all possible. On the contrary, we'd rather like to replace the MQT::CoreIR dependency in the other libraries with the MLIR one.
The necessary functionality here should boil down to getting the unitary of an operation implementing the unitary interface.
This is something that should be directly built into the interface and its implementation. Otherwise, we will never get a clear separation of concerns.
At the moment, I see no need to refactor the DD code to rely on the MLIR representation.

In the context of providing the unitary for a gate, it might make sense to, additionally, adopt something fairly similar to OpenQASM's and Qiskit's definition property, which essentially describes how to build the gate from known, already defined gates (starting with just the global phase gate and the control, inverse, and power modifiers). This might be beyond the scope of this PR though.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understood that when you said that we already have the definitions that we want to re-use them. Good to know that you want to keep it separate 👍

add_compile_options(-fexceptions)

file(GLOB TRANSFORMS_SOURCES *.cpp)
Expand Down
45 changes: 45 additions & 0 deletions mlir/lib/Dialect/MQTOpt/Transforms/GateDecomposition.cpp
Original file line number Diff line number Diff line change
@@ -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 <mlir/IR/PatternMatch.h>
#include <mlir/Support/LLVM.h>
#include <mlir/Transforms/GreedyPatternRewriteDriver.h>
#include <utility>

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.
*/
Comment on lines +23 to +25
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs to be fixed

struct GateDecomposition final
: impl::GateDecompositionBase<GateDecomposition> {

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
264 changes: 264 additions & 0 deletions mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp
Original file line number Diff line number Diff line change
@@ -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 <iterator>

Check warning on line 15 in mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp

View workflow job for this annotation

GitHub Actions / 🇨‌ Lint / 🚨 Lint

mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp:15:1 [misc-include-cleaner]

included header iterator is not used directly
#include <llvm/ADT/STLExtras.h>
#include <mlir/Dialect/Arith/IR/Arith.h>
#include <mlir/IR/MLIRContext.h>
#include <mlir/IR/Operation.h>
#include <mlir/IR/PatternMatch.h>
#include <mlir/IR/ValueRange.h>
#include <mlir/Support/LLVM.h>
#include <mlir/Support/LogicalResult.h>

namespace mqt::ir::opt {
/**
* @brief This pattern TODO.
*/
struct EulerDecompositionPattern final
: mlir::OpInterfaceRewritePattern<UnitaryInterface> {

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();
}
Comment on lines +42 to +46
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is definitely not ideal as a termination criterion, as there may still be better decompositions, e.g., for a sequence of two gates.
I think it pays off to take some inspiration from https://github.com/Qiskit/qiskit/blob/stable/2.1/crates/synthesis/src/euler_one_qubit_decomposer.rs here.

Copy link
Collaborator Author

@taminob taminob Sep 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not entirely sure what you mean here, the file does not contain the creation/detection of the gate series, I think that's in https://github.com/Qiskit/qiskit/blob/stable/2.1/crates/transpiler/src/passes/unitary_synthesis.rs. I can have a look again if I can find their abort criterion.

A naive approach I thought of was alternatively checking if the gate series only consists of Z/Y (so the used rotation gates for the decomposition). Does not catch everything, but at least better than this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I meant to imply following the import chain for the one qubit gate sequence struct. 🙂

I wouldn't tie the abortion criterion to a specific decomposition.


dd::GateMatrix unitaryMatrix = dd::opToSingleQubitGateMatrix(qc::I);

Check warning on line 48 in mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp

View workflow job for this annotation

GitHub Actions / 🇨‌ Lint / 🚨 Lint

mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp:48:70 [misc-include-cleaner]

no header providing "qc::I" is directly included

Check warning on line 48 in mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp

View workflow job for this annotation

GitHub Actions / 🇨‌ Lint / 🚨 Lint

mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp:48:40 [misc-include-cleaner]

no header providing "dd::opToSingleQubitGateMatrix" is directly included

Check warning on line 48 in mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp

View workflow job for this annotation

GitHub Actions / 🇨‌ Lint / 🚨 Lint

mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp:48:9 [misc-include-cleaner]

no header providing "dd::GateMatrix" is directly included
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not rely on the dd::GateMatrix representation. MLIR/LLVM should have builtin support for working with matrices (or tensors) and their manipulations.
Maybe https://mlir.llvm.org/docs/Dialects/Linalg/ is a good first stop or https://www.stephendiehl.com/posts/mlir_linear_algebra/.

Generally, I would argue that we should be taking advantage of MLIR's infrastructure as much as possible.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the pointer, I'll look into it.

for (auto&& gate : series) {
if (auto gateMatrix = helpers::getUnitaryMatrix(gate)) {
unitaryMatrix = helpers::multiply(unitaryMatrix, *gateMatrix);
}
}

auto [decomposedGateSchematic, globalPhase] =
calculateRotationGates(unitaryMatrix);

// apply global phase
createOneParameterGate<GPhaseOp>(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<UnitaryInterface>
getSingleQubitSeries(UnitaryInterface op) {
llvm::SmallVector<UnitaryInterface> 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());

Check warning on line 97 in mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp

View workflow job for this annotation

GitHub Actions / 🇨‌ Lint / 🚨 Lint

mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp:97:5 [misc-include-cleaner]

no header providing "assert" is directly included
auto&& users = op->getUsers();
return llvm::dyn_cast<UnitaryInterface>(*users.begin());

Check warning on line 99 in mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp

View workflow job for this annotation

GitHub Actions / 🇨‌ Lint / 🚨 Lint

mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp:99:18 [misc-include-cleaner]

no header providing "llvm::dyn_cast" is directly included
}

/**
* @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 <typename OpType>
static OpType createOneParameterGate(mlir::PatternRewriter& rewriter,
mlir::Location location,
qc::fp parameter,

Check warning on line 113 in mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp

View workflow job for this annotation

GitHub Actions / 🇨‌ Lint / 🚨 Lint

mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp:113:44 [misc-include-cleaner]

no header providing "qc::fp" is directly included
mlir::ValueRange inQubits) {
auto parameterValue = rewriter.create<mlir::arith::ConstantOp>(
location, rewriter.getF64Type(), rewriter.getF64FloatAttr(parameter));

return rewriter.create<OpType>(
location, inQubits.getType(), mlir::TypeRange{}, mlir::TypeRange{},
mlir::DenseF64ArrayAttr{}, mlir::DenseBoolArrayAttr{},
mlir::ValueRange{parameterValue}, inQubits, mlir::ValueRange{},
mlir::ValueRange{});
}

[[nodiscard]] static llvm::SmallVector<UnitaryInterface, 3> createMlirGates(
mlir::PatternRewriter& rewriter,
const llvm::SmallVector<std::pair<qc::OpType, qc::fp>, 3>& schematic,

Check warning on line 127 in mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp

View workflow job for this annotation

GitHub Actions / 🇨‌ Lint / 🚨 Lint

mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp:127:45 [misc-include-cleaner]

no header providing "qc::OpType" is directly included

Check warning on line 127 in mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp

View workflow job for this annotation

GitHub Actions / 🇨‌ Lint / 🚨 Lint

mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp:127:36 [misc-include-cleaner]

no header providing "std::pair" is directly included
mlir::Value inQubit) {

Check warning on line 128 in mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp

View workflow job for this annotation

GitHub Actions / 🇨‌ Lint / 🚨 Lint

mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp:128:13 [misc-include-cleaner]

no header providing "mlir::Value" is directly included
llvm::SmallVector<UnitaryInterface, 3> result;
for (auto [type, angle] : schematic) {
if (type == qc::RZ) {
auto newRz = createOneParameterGate<RZOp>(rewriter, inQubit.getLoc(),
angle, {inQubit});
result.push_back(newRz);
} else if (type == qc::RY) {
auto newRy = createOneParameterGate<RYOp>(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<std::pair<qc::OpType, qc::fp>, 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<std::pair<qc::OpType, qc::fp>, 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<qc::fp, 4>
paramsZyzInner(dd::GateMatrix unitaryMatrix) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it would be quite useful to not limit this gate decomposition to ZYZ, but to also include further bases. Different quantum computers provide different gate sets, which is why the different decompositions are almost a requirement.

  • the general circuit_kak method in Qiskit already provides a very solid basis for ZYZ, ZXZ, XZX, XYX
  • beyond that one of the decompositions including the SX gate would be interesting (native to IBM systems). PSX

While it is not extremely important for this PR here itself, it is fairly important for setting up the respective infrastructure to expand it in the future.

Similar to Qiskit, the type of decomposition should be a parameter of the pattern (and subsequently of the pass). Or just of the pass and depending on that parameter, the respective pattern is added to the pipeline (this could make more sense).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that it is a good idea to add the other gates for the KAK decomposition to this pattern, but don't think it's a good idea to stuff every decomposition into this pattern. But you already said that it is probably out-of-scope for this PR anyway and it can be easily extended by adding additional patterns.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or just of the pass and depending on that parameter, the respective pattern is added to the pipeline (this could make more sense).

I believe the above is what makes sense for this PR. The available patterns in the beginning may only contain the different KAK decompositions, but the architecture should be flexible enough so that future PRs could easily add more patterns to the pass.

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<EulerDecompositionPattern>(patterns.getContext());
}

} // namespace mqt::ir::opt
Loading
Loading