Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
00ad822
Add SWAPAbsorption Pass
MatthiasReumann May 4, 2026
77250d1
🎨 pre-commit fixes
pre-commit-ci[bot] May 4, 2026
515e6bf
Fix linting
MatthiasReumann May 4, 2026
f6e1c39
Merge branch 'feat/swap-absorption-pass' of https://github.com/munich…
MatthiasReumann May 4, 2026
b0474ff
🎨 pre-commit fixes
pre-commit-ci[bot] May 4, 2026
10302c5
remove SwapAbsorbtion::insertStatics()
jmoosburger May 4, 2026
da710c7
change unittests to use static qubits
jmoosburger May 4, 2026
d793c73
SwapAbsorbtion: PassReordersTwoQubitCircuitWithLeadingSwap
jmoosburger May 5, 2026
de89411
🎨 pre-commit fixes
pre-commit-ci[bot] May 6, 2026
3dbe1a3
swap-absorb: PassAbsorbsTwoIndependentSwaps
jmoosburger May 6, 2026
eea2685
Merge branch 'main' into feat/swap-absorption-pass
jmoosburger May 27, 2026
3b2af5d
remove unused includes
jmoosburger May 27, 2026
6312c37
PassAbsorbsSwapWithLeadingSingleQubitGates
jmoosburger May 27, 2026
1a884d8
🎨 pre-commit fixes
pre-commit-ci[bot] May 28, 2026
c4bb64f
PassAbsorbsTwoDependentSwaps
jmoosburger May 28, 2026
a779361
🎨 pre-commit fixes
pre-commit-ci[bot] May 28, 2026
3bf15be
fix some lint warnings
jmoosburger May 29, 2026
ac2a49e
Merge remote-tracking branch 'refs/remotes/origin/feat/swap-absorptio…
jmoosburger May 29, 2026
03917b6
Fix linter error
denialhaag May 29, 2026
8948b78
add pass summary and description
jmoosburger Jun 1, 2026
982efb7
🎨 pre-commit fixes
pre-commit-ci[bot] Jun 1, 2026
343387d
remove empty pass options
jmoosburger Jun 2, 2026
a108a9b
remove unnecessary comment
jmoosburger Jun 2, 2026
5b9c7f7
enhance swap absobrtion according review
jmoosburger Jun 2, 2026
44dfccc
Merge branch 'main' into feat/swap-absorption-pass
MatthiasReumann Jun 3, 2026
4d762f5
Update CHANGELOG.md [skip ci]
MatthiasReumann Jun 3, 2026
806a4a9
Update pass tablegen
MatthiasReumann Jun 3, 2026
4ef7620
🎨 pre-commit fixes
pre-commit-ci[bot] Jun 3, 2026
6843409
Merge remote-tracking branch 'origin/main' into feat/swap-absorption-…
denialhaag Jun 5, 2026
4584e2d
Merge branch 'main' into feat/swap-absorption-pass [no ci]
MatthiasReumann Jun 15, 2026
80e5b3c
:twisted_rightwards_arrows: Merge main
burgholzer Jun 18, 2026
bee32c9
:zap: Streamline pass implementation
burgholzer Jun 18, 2026
ada1d3e
Merge branch 'main' into feat/swap-absorption-pass
MatthiasReumann Jun 19, 2026
4dac9d2
typo in changelog
jmoosburger Jun 20, 2026
bda8d0f
✅ Improve test stability (#1794)
denialhaag Jun 19, 2026
7b06a2c
⬆️🩹 Update minor stable updates to v2026.06.12 (#1796)
renovate[bot] Jun 20, 2026
6776770
⬆️🩹 Update patch updates (#1795)
renovate[bot] Jun 20, 2026
4b1cd34
replace c casts with mlir::casts
jmoosburger Jun 20, 2026
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
12 changes: 6 additions & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ repos:

## Check JSON schemata
- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.37.2
rev: 0.37.3
hooks:
- id: check-github-workflows
priority: 0
Expand All @@ -47,14 +47,14 @@ repos:

## Check pyproject.toml file
- repo: https://github.com/henryiii/validate-pyproject-schema-store
rev: 2026.05.28
rev: 2026.06.12
hooks:
- id: validate-pyproject
priority: 0

## Ensure uv.lock is up to date
- repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.11.19
rev: 0.11.21
hooks:
- id: uv-lock
priority: 0
Expand Down Expand Up @@ -104,7 +104,7 @@ repos:

## Format configuration files with prettier
- repo: https://github.com/rbubley/mirrors-prettier
rev: v3.8.3
rev: v3.8.4
hooks:
- id: prettier
types_or: [yaml, html, css, scss, javascript, json, json5]
Expand Down Expand Up @@ -145,7 +145,7 @@ repos:

## Format and lint Python files with ruff
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.16
rev: v0.15.17
hooks:
- id: ruff-format
types_or: [python, pyi, jupyter, markdown]
Expand All @@ -156,7 +156,7 @@ repos:

## Check Python types with ty
- repo: https://github.com/astral-sh/ty-pre-commit
rev: v0.0.47
rev: v0.0.49
hooks:
- id: ty
args: [--only-dev]
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ with the exception that minor releases may include breaking changes.

### Added

- ✨ Add an `absorb-swaps` pass for absorbing initial SWAPs ([#1750])
([**@jmoosburger**], [**@MatthiasReumann**], [**@burgholzer**])
- ✨ Add a `fuse-single-qubit-unitary-runs` pass
for fusing compile-time single-qubit unitary runs via Euler resynthesis
([#1672]) ([**@simon1hofmann**], [**@burgholzer**])
Expand Down Expand Up @@ -608,6 +610,7 @@ changelogs._
[#1765]: https://github.com/munich-quantum-toolkit/core/pull/1765
[#1762]: https://github.com/munich-quantum-toolkit/core/pull/1762
[#1751]: https://github.com/munich-quantum-toolkit/core/pull/1751
[#1750]: https://github.com/munich-quantum-toolkit/core/pull/1750
[#1749]: https://github.com/munich-quantum-toolkit/core/pull/1749
[#1748]: https://github.com/munich-quantum-toolkit/core/pull/1748
[#1737]: https://github.com/munich-quantum-toolkit/core/pull/1737
Expand Down Expand Up @@ -856,6 +859,7 @@ changelogs._
[**@keefehuang**]: https://github.com/keefehuang
[**@J4MMlE**]: https://github.com/J4MMlE
[**@rturrado**]: https://github.com/rturrado
[**@jmoosburger**]: https://github.com/jmoosburger

<!-- General links -->

Expand Down
14 changes: 14 additions & 0 deletions mlir/include/mlir/Dialect/QCO/Transforms/Passes.td
Original file line number Diff line number Diff line change
Expand Up @@ -200,4 +200,18 @@ def HadamardLifting : Pass<"hadamard-lifting", "mlir::ModuleOp"> {
}];
}

def SwapAbsorption : Pass<"absorb-swaps", "mlir::ModuleOp"> {
let dependentDialects = ["mlir::qco::QCODialect"];
let summary = "This pass absorbs SWAP operations into the initial "
"program-to-hardware mapping.";
let description = [{
For a SWAP operation exchanging static qubits q0 and q1, the pass replaces the use of the
first (second) input qubit with the second (first) output qubit of the SWAP and subsequently
removes the operation. As a result, the initial program-to-hardware mapping is changed.
This process is repeated until no more SWAP operations can be absorbed.

The pass assumes that the quantum program is already mapped to static qubits.
}];
Comment on lines +205 to +214

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think the wording here could be improved; maybe even the naming of the pass itself. As it stands, the name of the pass is not self explanatory.
"initial program-to-hardware mapping" is something that is oddly specific for a pass that just generally matches static operations and checks if any SWAPs are directly applied after the "allocation".

I am even wondering whether this should actually be limited to StaticOp. Why not extend the logic to AllocOp as well? In the pattern rewrite proposed in the other comment, that is likely to be fairly simple. Taking it one step further, qtensor.load could also qualify for such a simplification. And to go even further beyond, any mixture of StaticOp, AllocOp or LoadOp can equally be simplified.

}

#endif // MLIR_DIALECT_QCO_TRANSFORMS_PASSES_TD
74 changes: 74 additions & 0 deletions mlir/lib/Dialect/QCO/Transforms/Optimizations/SwapAbsorption.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright (c) 2023 - 2026 Chair for Design Automation, TUM
* Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH
* All rights reserved.
*
* SPDX-License-Identifier: MIT
*
* Licensed under the MIT License
*/

#include "mlir/Dialect/QCO/IR/QCOOps.h"
#include "mlir/Dialect/QCO/Transforms/Passes.h"
#include "mlir/Dialect/QCO/Utils/Drivers.h"
#include "mlir/Dialect/QCO/Utils/WireIterator.h"

#include <mlir/Dialect/Func/IR/FuncOps.h>
#include <mlir/IR/BuiltinOps.h>
#include <mlir/IR/PatternMatch.h>
#include <mlir/Support/LLVM.h>
#include <mlir/Support/WalkResult.h>

namespace mlir::qco {
#define GEN_PASS_DEF_SWAPABSORPTION
#include "mlir/Dialect/QCO/Transforms/Passes.h.inc"

namespace {
struct SwapAbsorption : impl::SwapAbsorptionBase<SwapAbsorption> {
using SwapAbsorptionBase::SwapAbsorptionBase;

protected:
void runOnOperation() override {
ModuleOp anchor = getOperation();
IRRewriter rewriter(&getContext());

for (auto func : anchor.getOps<func::FuncOp>()) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I am really wondering about the efficiency of this pass. I already pushed a couple of improvements, but I can't help it and feel like this is not the way to go.

Is there any particular reason why this pass cannot simply be one that matches on StaticOp and checks for subsequent SWAPOp that also have a StaticOp as there second input?
Something as simple as

/**
 * @brief Absorb SWAP operations into static qubit allocations
 */
struct RemoveSWAPAfterStaticAllocation final : OpRewritePattern<SWAPOp> {
  using OpRewritePattern::OpRewritePattern;

  LogicalResult matchAndRewrite(SWAPOp op,
                                PatternRewriter& rewriter) const override {
    auto qubit0 = op.getInputQubit(0);
    if (!isa<StaticOp>(qubit0.getDefiningOp())) {
      return failure();
    }
    auto qubit1 = op.getInputQubit(1);
    if (!isa<StaticOp>(qubit1.getDefiningOp())) {
      return failure();
    }
    rewriter.replaceOp(op, {qubit1, qubit0});
    return success();
  }
};

This feels way simpler and more efficient than the pass implementation here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

i'll have a look at that 👍

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I agree with @burgholzer here.

One could even probably make an argument to add this pattern to the canonicalization pass instead of adding a new pass altogether (and all the boilerplate which comes along with it). This would again improve performance by applying the patterns in the same driver run as all the other patterns.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

question in terms of clarification: this would run per operation in the circuit, right?

it definitely seems more efficient, but does it cover all our required cases?
example:

q0 --H--X-----      q1 --H--
        |
q1 --H--X--X--  ->  q2 --H--
           |
q2 --H-----X--      q0 --H--

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

You can still use the WireIterator in the pattern implementation. The above solution is just a blueprint, I assume.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would argue against a canonicalization pattern here. I had this suggestion in my review already. However, this would mean that the pattern would try to match every single swap operation in a circuit while it would only really match very little.
As such, this feels cleaner as a separate pass that is invoked at a specific point in time than a canonicalization run ever so often.

SmallVector<SWAPOp> readyToAbsorb;
SmallVector<WireIterator> wires;
do {
wires.clear();
for (auto op : func.getOps<StaticOp>()) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This will collect all static operations repeatedly in every iteration of the do...while loop, which seems fairly wasteful. Pretty sure there is a more efficient way to do this.

wires.emplace_back(op.getQubit());
}
if (wires.empty()) {
return;
}

readyToAbsorb.clear();
findSwapsReadyForAbsorption(wires, readyToAbsorb);

for (auto swapOp : readyToAbsorb) {
rewriter.replaceOp(swapOp,
{swapOp.getQubit1In(), swapOp.getQubit0In()});
}
} while (!readyToAbsorb.empty());
}
}

private:
static void findSwapsReadyForAbsorption(MutableArrayRef<WireIterator> wires,
SmallVector<SWAPOp>& readyToAbsorb) {
std::ignore = walkProgramGraph<WireDirection::Forward>(

Check warning on line 61 in mlir/lib/Dialect/QCO/Transforms/Optimizations/SwapAbsorption.cpp

View workflow job for this annotation

GitHub Actions / 🇨‌ Lint / 🚨 Lint

mlir/lib/Dialect/QCO/Transforms/Optimizations/SwapAbsorption.cpp:61:10 [misc-include-cleaner]

no header providing "std::ignore" is directly included
wires, [&](const ReadyRange& ready, ReleasedOps& released) {
for (const auto& [op, indices] : ready) {
if (isa<SWAPOp>(op)) {
readyToAbsorb.emplace_back(op);
}
released.emplace_back(op);
}
return WalkResult::interrupt();
});
}
};
} // namespace
} // namespace mlir::qco
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
# Licensed under the MIT License

set(target_name mqt-core-mlir-unittest-optimizations)
add_executable(${target_name} test_qco_hadamard_lifting.cpp
test_qco_merge_single_qubit_rotation.cpp test_quantum_loop_unroll.cpp)
add_executable(
${target_name} test_qco_hadamard_lifting.cpp test_qco_merge_single_qubit_rotation.cpp
test_quantum_loop_unroll.cpp test_swapabsorption.cpp)

target_link_libraries(
${target_name}
Expand Down

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

These tests do not really follow the structure of most of the other tests, which always build a reference program and use our IR verifier to check for equivalence.
I'd prefer to have this as uniform as possible.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

i'll have a look at that 👍

Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/*
* Copyright (c) 2023 - 2026 Chair for Design Automation, TUM
* Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH
* All rights reserved.
*
* SPDX-License-Identifier: MIT
*
* Licensed under the MIT License
*/

#include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h"
#include "mlir/Dialect/QCO/IR/QCODialect.h"
#include "mlir/Dialect/QCO/IR/QCOOps.h"
#include "mlir/Dialect/QCO/Transforms/Passes.h"

#include <gtest/gtest.h>
#include <llvm/Support/LogicalResult.h>
#include <mlir/Dialect/Arith/IR/Arith.h>
#include <mlir/Dialect/Func/IR/FuncOps.h>
#include <mlir/IR/BuiltinOps.h>
#include <mlir/IR/DialectRegistry.h>
#include <mlir/IR/OperationSupport.h>
#include <mlir/IR/OwningOpRef.h>
#include <mlir/IR/Value.h>
#include <mlir/Pass/PassManager.h>
#include <mlir/Support/LLVM.h>

#include <cassert>
#include <memory>

using namespace mlir;
using namespace mlir::qco;

namespace {

class SwapAbsorbPassTest : public testing::Test {

protected:
void SetUp() override {
DialectRegistry registry;
registry.insert<qco::QCODialect, arith::ArithDialect, func::FuncDialect>();
context = std::make_unique<MLIRContext>();
context->appendDialectRegistry(registry);
context->loadAllAvailableDialects();
}

static void applySwapAbsorb(OwningOpRef<ModuleOp>& moduleOp) {
PassManager pm(moduleOp->getContext());
pm.addPass(qco::createSwapAbsorption());
auto res = pm.run(*moduleOp);

ASSERT_TRUE(succeeded(res));
}

std::unique_ptr<MLIRContext> context;
};
}; // namespace

TEST_F(SwapAbsorbPassTest, PassDoesNotChangeSwaplessProgram) {

qco::QCOProgramBuilder builder(context.get());
builder.initialize();

const auto q00 = builder.staticQubit(0);
const auto q10 = builder.staticQubit(1);

const auto q01 = builder.h(q00);
const auto [q02, q11] = builder.cx(q01, q10);

builder.sink(q02);
builder.sink(q11);

auto moduleThroughPass = builder.finalize();
auto originalModule = moduleThroughPass->clone();

applySwapAbsorb(moduleThroughPass);
ASSERT_TRUE(mlir::OperationEquivalence::isEquivalentTo(
moduleThroughPass.get(), originalModule,
mlir::OperationEquivalence::Flags::IgnoreLocations));
}
Comment on lines +59 to +80

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Kind of a pointless test. Why would the pass change a program that does not contain the single operation the pass actually matches on?

@jmoosburger jmoosburger Jun 20, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

for me its a requirement that circuits without swaps remain untouched.
from the tests point of view it's not possible to determine where exactly the pass matches on.
you cannot be sure a future implementation of this pass accidentally changes a circuit without a swap unless you test it.

nevertheless, feel free to reach out, if you are uncomfortable with this. i'll remove this test then 👍


TEST_F(SwapAbsorbPassTest, PassReordersTwoQubitCircuitWithLeadingSwap) {

qco::QCOProgramBuilder builder(context.get());
builder.initialize();

const auto q00 = builder.staticQubit(0);
const auto q10 = builder.staticQubit(1);

const auto [q01, q11] = builder.swap(q00, q10);

const auto q02 = builder.id(q01);
const auto q12 = builder.id(q11);
Comment on lines +92 to +93

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Kind of pointless to use identity gates here, as they would generally be optimized away. Prefer to use non-trivial gates, if at all.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

it doesn't matter, which single qubit gate is used at this place, to test this particular pass. a gate is required to check, whether the pass reorders the circuit correctly. i took identity as it is the simplest one and does not add functionality to the test circuit


builder.sink(q02);
builder.sink(q12);

auto moduleThroughPass = builder.finalize();
applySwapAbsorb(moduleThroughPass);

ASSERT_EQ(q10, mlir::cast<IdOp>(q02.getDefiningOp()).getInputQubit(0));
ASSERT_EQ(q00, mlir::cast<IdOp>(q12.getDefiningOp()).getInputQubit(0));
}

TEST_F(SwapAbsorbPassTest, PassAbsorbsTwoIndependentSwaps) {

qco::QCOProgramBuilder builder(context.get());
builder.initialize();

const auto q00 = builder.staticQubit(0);
const auto q10 = builder.staticQubit(1);
const auto q20 = builder.staticQubit(2);
const auto q30 = builder.staticQubit(3);

const auto [q01, q11] = builder.swap(q00, q10);
const auto [q21, q31] = builder.swap(q20, q30);

const auto q02 = builder.id(q01);
const auto q12 = builder.id(q11);
const auto q22 = builder.id(q21);
const auto q32 = builder.id(q31);

builder.sink(q02);
builder.sink(q12);
builder.sink(q22);
builder.sink(q32);

auto moduleThroughPass = builder.finalize();
applySwapAbsorb(moduleThroughPass);

ASSERT_EQ(q10, mlir::cast<IdOp>(q02.getDefiningOp()).getInputQubit(0));
ASSERT_EQ(q00, mlir::cast<IdOp>(q12.getDefiningOp()).getInputQubit(0));
ASSERT_EQ(q30, mlir::cast<IdOp>(q22.getDefiningOp()).getInputQubit(0));
ASSERT_EQ(q20, mlir::cast<IdOp>(q32.getDefiningOp()).getInputQubit(0));
}

TEST_F(SwapAbsorbPassTest, PassAbsorbsSwapWithLeadingSingleQubitGates) {

qco::QCOProgramBuilder builder(context.get());
builder.initialize();

const auto q00 = builder.staticQubit(0);
const auto q10 = builder.staticQubit(1);

const auto q01 = builder.id(q00);
const auto q11 = builder.id(q10);

const auto [q02, q12] = builder.swap(q01, q11);

const auto q03 = builder.id(q02);
const auto q13 = builder.id(q12);

builder.sink(q03);
builder.sink(q13);

auto moduleThroughPass = builder.finalize();
applySwapAbsorb(moduleThroughPass);

ASSERT_EQ(q11, mlir::cast<IdOp>(q03.getDefiningOp()).getInputQubit(0));
ASSERT_EQ(q01, mlir::cast<IdOp>(q13.getDefiningOp()).getInputQubit(0));
}

TEST_F(SwapAbsorbPassTest, PassAbsorbsTwoDependentSwaps) {

qco::QCOProgramBuilder builder(context.get());
builder.initialize();

const auto q00 = builder.staticQubit(0);
const auto q10 = builder.staticQubit(1);
const auto q20 = builder.staticQubit(2);

const auto [q01, q11] = builder.swap(q00, q10);
const auto [q12, q21] = builder.swap(q11, q20);

const auto q02 = builder.id(q01);
const auto q13 = builder.id(q12);
const auto q22 = builder.id(q21);

builder.sink(q02);
builder.sink(q13);
builder.sink(q22);

auto moduleThroughPass = builder.finalize();
applySwapAbsorb(moduleThroughPass);

ASSERT_EQ(q20, mlir::cast<IdOp>(q13.getDefiningOp()).getInputQubit(0));
ASSERT_EQ(q00, mlir::cast<IdOp>(q22.getDefiningOp()).getInputQubit(0));
ASSERT_EQ(q10, mlir::cast<IdOp>(q02.getDefiningOp()).getInputQubit(0));
}
2 changes: 1 addition & 1 deletion test/algorithms/test_qpe.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ TEST_P(QPE, IQPETest) {
auto qc = qc::createIterativeQPE(lambda, precision);
ASSERT_EQ(qc.getNqubits(), 2U);

constexpr auto shots = 8192U;
constexpr auto shots = 16384U;
const auto measurements = dd::sample(qc, shots);

// sort the measurements
Expand Down
Loading
Loading