Skip to content

✨ Add Rotation Gate Merging using Quaternions#1407

Open
J4MMlE wants to merge 6 commits intomunich-quantum-toolkit:mainfrom
J4MMlE:quaternion-rotation-merging
Open

✨ Add Rotation Gate Merging using Quaternions#1407
J4MMlE wants to merge 6 commits intomunich-quantum-toolkit:mainfrom
J4MMlE:quaternion-rotation-merging

Conversation

@J4MMlE
Copy link

@J4MMlE J4MMlE commented Dec 28, 2025

Description

This PR extends the rotation merging pass in the MQTOpt dialect to support quaternion-based gate fusion. This is the first step toward closing #1029.

The existing rotation merge pass only merges consecutive rotation gates of the same type (e.g., rx + rx or ry + ry) by adding their angles.
This PR introduces quaternion-based merging, which can merge rotation gates of different types (currently only single qubit gates rx, ry, rz, u).

Quaternions are widely used to represent rotations in three-dimensional space and naturally map to qubit gate rotations around the Bloch sphere. The implementation:

  1. Converts rotation gates to quaternion representation
  2. Multiplies quaternions using the Hamilton product
  3. Converts the resulting quaternion back to a u gate. (This could also be done differently in the future, and directly decompose to the correct base gates by using the decomposition from ✨ Implement single-qubit gate decomposition pass #1182)

Since this optimization may only be beneficial on certain quantum architectures, it is disabled by default. It can be invoked using:

mqt-cc --mlir-merge-rotation-gates <input.mlir>

Checklist:

  • The pull request only contains commits that are focused and relevant to this change.
  • I have added appropriate tests that cover the new/changed functionality.
  • I have updated the documentation to reflect these changes.
  • I have added entries to the changelog for any noteworthy additions, changes, fixes, or removals.
    - [ ] I have added migration instructions to the upgrade guide (if needed).
  • The changes follow the project's style guidelines and introduce no new warnings.
  • The changes are fully tested and pass the CI checks.
  • I have reviewed my own code changes.

@codecov
Copy link

codecov bot commented Dec 28, 2025

Codecov Report

❌ Patch coverage is 97.35099% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...ct/QCO/Transforms/QuaternionMergeRotationGates.cpp 97.9% 3 Missing ⚠️
mlir/lib/Compiler/CompilerPipeline.cpp 75.0% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@burgholzer burgholzer added feature New feature or request c++ Anything related to C++ code MLIR Anything related to MLIR labels Dec 29, 2025
@burgholzer burgholzer added this to the MLIR Support milestone Dec 29, 2025
@burgholzer
Copy link
Member

Hey @J4MMlE 👋🏻
This is great to see!

How much of an ask would it be to directly base this pass on the QCO dialect and its infrastructure?
We'd like to stop adding features to the old dialects and instead only add them to the new ones as much as possible, so that we can remove the old dialects rather sooner than later.
This is not a must, but it would be highly appreciated.

@mergify mergify bot added the conflict label Jan 14, 2026
@J4MMlE J4MMlE force-pushed the quaternion-rotation-merging branch from 106575c to 7528b05 Compare January 14, 2026 22:38
@mergify mergify bot removed the conflict label Jan 14, 2026
@burgholzer burgholzer requested a review from DRovara January 17, 2026 00:07
@burgholzer burgholzer linked an issue Jan 17, 2026 that may be closed by this pull request
Copy link
Collaborator

@DRovara DRovara left a comment

Choose a reason for hiding this comment

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

Thanks a lot @J4MMlE for the effort! The code already looks super clean in my opinion and I like the way you have implemented everything.

I do have quite a few minor comments, but they are largely just related to comments in the code (docstrings/typos).

What I did not look at in much detail is the CMake setup. I guess it would make sense if @burgholzer (after your vacation, of course) could look into that - although probably it would be most efficient to do that only once the tests are ready, too (btw, I think the point from my top-level comment below might also be interesting to consider for you).

Anyways, @J4MMlE, once you have addressed the comments and added the tests, feel free to re-request my review and I will look at the code again. Thanks a lot in the meantime!

@mergify mergify bot added the conflict label Jan 20, 2026
@DRovara
Copy link
Collaborator

DRovara commented Jan 21, 2026

I just remembered one more comment I wanted to make that I forgot:

You talk about how you no longer have to explicitly filter for control qubits due to the new dialect structure.
However, right now, I believe your pass would not work at all with controlled gates anymore - I'm not sure if that's intended.

Imagine the following pseudo-code:

%q0_0, %q1_0 = alloc(2)
[...]
%q0_1, %q1_1 = qco.ctrl(%q0_0), (%q1c_0 = %q1_0) {
  %q1c_1 = qco.u(...) %q1c_0
  qco.yield %q1c_1 
}
%q0_2, %q1_2 = qco.ctrl(%q0_1), (%q1c2_0 = %q1_1) {
  %q1c2_1 = qco.u(...) %q1c2_0
  qco.yield %q1c2_1 
}

Here, the first u gate has qco.yield as its user. However, in short, the snippet above corresponds to:

controlled(q0) u(...) q1
controlled(q0) u(...) q1

so in theory, this can definitely be merged.

Now, I don't know if this is a flaw of the pass (maybe this situation should be checked explicitly), a flaw of the dialect implementation (maybe QCO should provide a way to get the actual successor gate, rather than the yield which we don't care much about), or if it's just out of scope for this pass.

My personal gut feeling is that it would be a nice helper method to implement for the QCO UnitaryOpInterface.

@J4MMlE J4MMlE force-pushed the quaternion-rotation-merging branch from aaa7096 to 94ea576 Compare January 21, 2026 19:33
@mergify mergify bot removed the conflict label Jan 21, 2026
@J4MMlE J4MMlE force-pushed the quaternion-rotation-merging branch from f0989ad to 045857a Compare January 21, 2026 19:57
Copy link
Collaborator

@DRovara DRovara left a comment

Choose a reason for hiding this comment

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

Thanks a lot @J4MMlE for the work. It looks super good already (for the record, I still haven't looked at the CMake configuration, I'll leave that to someone else).
The tests also look really really clean, I like that a lot.

I did have a minor concern regarding the numerical correctness, maybe you could check that out real quick? Either I am wrong or something is not quite working correctly.

Once my comments are addressed, feel free to remove the "Draft" status from this PR and request a review from Lukas.

@J4MMlE J4MMlE force-pushed the quaternion-rotation-merging branch from 042de31 to 290b626 Compare February 5, 2026 20:21
@J4MMlE J4MMlE marked this pull request as ready for review February 5, 2026 20:48
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 5, 2026

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • New Features
    • Added quaternion-based rotation gate merging optimization that combines consecutive rotation operations (RX, RY, RZ, U-gates) into simplified merged gates, reducing circuit complexity
    • Added --mlir-merge-rotation-gates CLI flag (disabled by default) to control optimization behavior

Walkthrough

Adds a quaternion-based MergeRotationGates MLIR transform, wires it into build and compiler pipeline behind a new mergeRotationGates config flag and CLI option, and adds unit tests plus CMake/test wiring for the new transforms.

Changes

Cohort / File(s) Summary
Config & CLI
mlir/include/mlir/Compiler/CompilerPipeline.h, mlir/tools/mqt-cc/mqt-cc.cpp
Adds bool mergeRotationGates = false to QuantumCompilerConfig, adds --merge-rotation-gates CLI flag and wires it into the compiler config.
Pass Declarations & TableGen
mlir/include/mlir/Dialect/QCO/Transforms/Passes.h, mlir/include/mlir/Dialect/QCO/Transforms/Passes.td, mlir/include/mlir/Dialect/QCO/CMakeLists.txt
Introduces TableGen pass declaration for MergeRotationGates, pass registration scaffolding, and includes Transforms in QCO include CMake.
Build Integration
mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt, mlir/include/mlir/Dialect/QCO/Transforms/CMakeLists.txt, mlir/lib/Dialect/QCO/CMakeLists.txt, mlir/lib/Compiler/CMakeLists.txt
Adds new MLIRQCOTransforms library and inc-gen target, exposes headers, enables exceptions, and links/transitively adds transforms to MQTCompilerPipeline.
Pass Implementation
mlir/lib/Dialect/QCO/Transforms/QuaternionMergeRotationGates.cpp
New greedy quaternion-based merge pass and pattern: quaternion type, axis detection, quaternion conversion, Hamilton product, U-gate reconstruction, pattern match&rewrite, and pass driver.
Compiler Pipeline
mlir/lib/Compiler/CompilerPipeline.cpp
Conditionally runs the MergeRotationGates pass in Stage 5 when config_.mergeRotationGates is true; includes pass header and gates PM invocation behind config check.
Unit Tests & Test CMake
mlir/unittests/CMakeLists.txt, mlir/unittests/Compiler/test_compiler_pipeline.cpp, mlir/unittests/Dialect/QCO/CMakeLists.txt, mlir/unittests/Dialect/QCO/Transforms/CMakeLists.txt, mlir/unittests/Dialect/QCO/Transforms/test_qco_quaternion_merge.cpp
Adds pipeline-level test toggling the flag and a comprehensive GoogleTest suite validating merge behavior, numerical correctness, greedy merging, edge cases; integrates tests into CMake.
Changelog
CHANGELOG.md
Adds PR reference #1407 and contributor attribution.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant CLI as mqt-cc (CLI)
  participant Pipeline as CompilerPipeline
  participant PM as PassManager
  participant QCO as MLIRQCOTransforms
  participant Module as mlir::ModuleOp

  User->>CLI: invoke with --merge-rotation-gates
  CLI->>Pipeline: set config.mergeRotationGates = true
  Pipeline->>PM: configure Stage 5 (conditional)
  PM->>QCO: run MergeRotationGates pass
  QCO->>Module: match & rewrite RX/RY/RZ/U -> merged U
  QCO-->>PM: pass finished
  PM-->>Pipeline: stage complete
  Pipeline-->>CLI: emit optimized IR
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • burgholzer

Poem

🐰 I hopped through qubits with angles bright,

Quaternions twined and fused the light,
Hamilton spun two gates to one,
I twitched my nose — the merge was done,
A joyful hop for quantum night!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add Rotation Gate Merging using Quaternions' directly and clearly summarizes the main feature introduced in this PR: quaternion-based rotation gate merging.
Description check ✅ Passed The PR description comprehensively covers motivation, technical approach, usage, and includes a completed checklist. All key aspects of the changeset are addressed with clear explanations of the quaternion-based fusion implementation.
Docstring Coverage ✅ Passed Docstring coverage is 83.58% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

No actionable comments were generated in the recent review. 🎉


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt`:
- Line 11: The global add_compile_options(-fexceptions) affects all targets;
instead remove that line and add a scoped compile option on the
MLIRQCOTransforms target by calling target_compile_options(MLIRQCOTransforms
PRIVATE -fexceptions) after the MLIRQCOTransforms target is created (e.g., after
the add_library/add_mlir_dialect or similar target definition) so only
MLIRQCOTransforms gets -fexceptions.
- Line 13: Remove the stray debug output in the CMakeLists by deleting or
guarding the message(STATUS "MLIR_DIALECT_LIBS contains: ${dialect_libs}") line;
either remove that message entirely from
mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt or wrap it behind a project-level
debug option (e.g., only call message when a DEBUG_CMAKE or similar variable is
enabled) so normal builds no longer emit the dialect_libs status line.

In `@mlir/lib/Dialect/QCO/Transforms/QuaternionMergeRotationGates.cpp`:
- Around line 363-370: Fix the comment typo on the line above the SelectOp
creations: change "weather" to "whether" in the sentence "// choose correct
alpha and gamma weather safe or not" so it reads "// choose correct alpha and
gamma whether safe or not" (locate the comment immediately preceding the
creation of alpha and gamma via rewriter.create<mlir::arith::SelectOp>).

In `@mlir/unittests/Dialect/QCO/Transforms/CMakeLists.txt`:
- Around line 11-20: Remove or replace the TODO in the target_link_libraries
block for mqt-core-mlir-dialect-qco-transforms-test: either delete the "# TODO
figure out correct dependencies" line or update it to a concrete status/note
(e.g., "Verified dependencies for CI" or list missing deps) so it no longer
suggests unfinished work; verify the linked targets (GTest::gtest_main,
MLIRQCOProgramBuilder, MLIRQCOTransforms, MLIRIR, MLIRPass, MLIRSupport,
LLVMSupport) are intentionally present before committing the change.

In `@mlir/unittests/Dialect/QCO/Transforms/test_qco_quaternion_merge.cpp`:
- Around line 543-577: Move the SCF dialect load into the test fixture SetUp:
add context.loadDialect<scf::SCFDialect>(); inside the
QCOQuaternionMergeTest::SetUp() implementation so all tests consistently load
SCFDialect, and remove the duplicate context.loadDialect<scf::SCFDialect>();
call from the multipleUseInIf test (which currently calls it inside that test
body).
- Around line 136-153: The buildRotations function reads gate.angles[0..2] for
UOp without checking length, risking OOB; update buildRotations to validate the
RotationGate angles vector before calling builder.u by asserting or returning an
error when gate.opName == UOp::getOperationName() and gate.angles.size() < 3 (or
providing safe defaults), e.g. add a size check for gate.angles >= 3 and emit a
clear assertion/log message that includes the offending gate so the caller can
diagnose; reference buildRotations, RotationGate, UOp, gate.angles, and
builder.u when locating the change.


get_property(dialect_libs GLOBAL PROPERTY MLIR_DIALECT_LIBS)
set(LIBRARIES ${dialect_libs} MQT::CoreIR)
add_compile_options(-fexceptions)
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider scoping the compile option to the target.

Using add_compile_options(-fexceptions) at directory scope affects all targets in this directory. Consider using target_compile_options instead to scope it to only MLIRQCOTransforms.

♻️ Suggested change
 get_property(dialect_libs GLOBAL PROPERTY MLIR_DIALECT_LIBS)
 set(LIBRARIES ${dialect_libs} MQT::CoreIR)
-add_compile_options(-fexceptions)

-message(STATUS "MLIR_DIALECT_LIBS contains: ${dialect_libs}")
-
 file(GLOB_RECURSE TRANSFORMS_SOURCES *.cpp)

 add_mlir_library(MLIRQCOTransforms ${TRANSFORMS_SOURCES} LINK_LIBS ${LIBRARIES} DEPENDS
                  MLIRQCOTransformsIncGen)
+
+target_compile_options(MLIRQCOTransforms PRIVATE -fexceptions)
🤖 Prompt for AI Agents
In `@mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt` at line 11, The global
add_compile_options(-fexceptions) affects all targets; instead remove that line
and add a scoped compile option on the MLIRQCOTransforms target by calling
target_compile_options(MLIRQCOTransforms PRIVATE -fexceptions) after the
MLIRQCOTransforms target is created (e.g., after the
add_library/add_mlir_dialect or similar target definition) so only
MLIRQCOTransforms gets -fexceptions.

@J4MMlE J4MMlE force-pushed the quaternion-rotation-merging branch from 290b626 to 6c5018e Compare February 6, 2026 12:34
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@mlir/lib/Dialect/QCO/Transforms/QuaternionMergeRotationGates.cpp`:
- Around line 136-144: Add a trailing llvm_unreachable() immediately after the
switch that handles RotationAxis to guard against future enum extensions and
silence non-exhaustive-switch warnings; locate the switch on axis (the one
returning quaternion literals for RotationAxis::X/Y/Z in
QuaternionMergeRotationGates.cpp) and append llvm_unreachable("Unhandled
RotationAxis") (and include the proper header <llvm/Support/ErrorHandling.h> if
not already included).
- Around line 309-316: The computed acos input bTemp3 (constructed from ww, zz,
bTemp1, bTemp2) can drift outside [-1,1]; clamp it to the valid domain before
calling mlir::math::AcosOp to avoid NaN. Replace the direct use of bTemp3 in the
AcosOp with a clamped value produced via mlir::arith::MinFOp and
mlir::arith::MaxFOp (or a combination) to bound it to [-1.0, 1.0] using the
existing constants (one, negOne or create negOne if needed); then pass that
clamped value to the creation of beta (rewriter.create<mlir::math::AcosOp>(loc,
clampedValue)). Ensure the new ops reference the same loc and use rewriter so
the IR stays consistent.

@J4MMlE J4MMlE force-pushed the quaternion-rotation-merging branch 2 times, most recently from 4cb71da to c875d00 Compare February 8, 2026 22:12
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@mlir/lib/Dialect/QCO/Transforms/QuaternionMergeRotationGates.cpp`:
- Around line 374-375: The alpha/gamma extraction in
QuaternionMergeRotationGates.cpp can produce angles outside [-PI, PI]; implement
normalization for the variables alpha and gamma (e.g., in the code path that
computes these values inside the QuaternionMergeRotationGates transform/pass) by
mapping them into the principal range [-M_PI, M_PI] using a modular reduction
(fmod/remainder) and ±2*PI adjustment so mathematically equivalent angles are
canonical; update any uses of alpha/gamma in emit/replace logic within the
QuaternionMergeRotationGates routine to use the normalized values.

In `@mlir/unittests/Dialect/QCO/Transforms/test_qco_quaternion_merge.cpp`:
- Around line 702-715: Update the test comment in TEST_F(QCOQuaternionMergeTest,
numericalRotationIdentity) to match the assertion in the test
(expectUGateParams(0, TAU, 0)) rather than claiming U(0, 0, 0) or U(PI, -PI, 0);
reference the actual expected output U(0, TAU, 0) (with TAU = 2π) in the comment
or alternatively change the assertion to expect U(0, 0, 0) if that is the
intended canonical form—locate the test by the name numericalRotationIdentity
and the call expectUGateParams(0, TAU, 0) to make the consistent update.

Comment on lines +374 to +375
// TODO: could add some normalization here for alpha and gamma otherwise
// they can be outside of [-PI, PI].
Copy link
Contributor

@coderabbitai coderabbitai bot Feb 8, 2026

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

TODO: angle normalization is deferred.

The alpha/gamma values can exceed [-π, π] after extraction. This could produce unexpected but mathematically equivalent gate parameters. Worth tracking as a follow-up if downstream consumers expect normalized angles.

Would you like me to open an issue to track the angle normalization TODO?

🤖 Prompt for AI Agents
In `@mlir/lib/Dialect/QCO/Transforms/QuaternionMergeRotationGates.cpp` around
lines 374 - 375, The alpha/gamma extraction in QuaternionMergeRotationGates.cpp
can produce angles outside [-PI, PI]; implement normalization for the variables
alpha and gamma (e.g., in the code path that computes these values inside the
QuaternionMergeRotationGates transform/pass) by mapping them into the principal
range [-M_PI, M_PI] using a modular reduction (fmod/remainder) and ±2*PI
adjustment so mathematically equivalent angles are canonical; update any uses of
alpha/gamma in emit/replace logic within the QuaternionMergeRotationGates
routine to use the normalized values.

Copy link
Member

Choose a reason for hiding this comment

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

Normalization seems reasonable here. I believe Qiskit has some information on the periodicity of the U operation in its code. If I remember correctly, it wasn't as simple as 2pi periodicity.

Copy link
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

🌐 Web query:

Qiskit U gate periodicity parameters theta phi lambda

💡 Result:

In Qiskit, U(θ, φ, λ) (aka the old U3) is defined by the matrix (up to a documented global-phase convention difference vs OpenQASM 2) (quantum.cloud.ibm.com):
[
U(θ,φ,λ)=
\begin{pmatrix}
\cos(\tfrac{θ}{2}) & -e^{iλ}\sin(\tfrac{θ}{2})\
e^{iφ}\sin(\tfrac{θ}{2}) & e^{i(φ+λ)}\cos(\tfrac{θ}{2})
\end{pmatrix}
]
and corresponds to a (R_z(φ)R_y(θ)R_z(λ)) Euler decomposition. (quantum.cloud.ibm.com)

Periodicity (exact matrix equality):

Periodicity up to global phase (physically equivalent operation):

  • (θ \sim θ + 2\pi) because (U(θ+2\pi,φ,λ) = -U(θ,φ,λ)=e^{i\pi}U(θ,φ,λ)) (just a global phase). (quantum.cloud.ibm.com)

Citations:


@burgholzer You're absolutely right! The U gate parameters have different periodicities: φ (phi) and λ (lambda) have 2π periodicity, while θ (theta) has 4π periodicity for exact matrix equality, though θ is 2π-periodic up to global phase.

In this implementation:

  • beta (the Y-rotation angle, corresponding to θ) is extracted via acos, which naturally returns values in [0, π], so it doesn't exceed the principal range
  • alpha and gamma (the Z-rotation angles, corresponding to φ and λ) have straightforward 2π periodicity and can indeed exceed [-π, π] after the atan2-based extraction

So the TODO comment is appropriately scoped—normalizing alpha and gamma to [-π, π] with modulo 2π is the right approach. The beta parameter is already well-behaved thanks to the acos domain.


🧠 Learnings used
Learnt from: DRovara
Repo: munich-quantum-toolkit/core PR: 1108
File: mlir/lib/Dialect/MQTOpt/Transforms/ReplaceBasisStateControlsWithIfPattern.cpp:171-180
Timestamp: 2025-10-09T13:13:51.224Z
Learning: In MQT Core MLIR, UnitaryInterface operations guarantee 1-1 correspondence between input and output qubits in the same order. When cloning or modifying unitary operations (e.g., removing controls), this correspondence is maintained by construction, so yielding getAllInQubits() in else-branches matches the result types from the operation's outputs.

Learnt from: burgholzer
Repo: munich-quantum-toolkit/core PR: 1330
File: mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp:126-139
Timestamp: 2026-02-09T15:19:47.622Z
Learning: In the QC dialect (mlir/lib/Dialect/QC/IR/Modifiers/InvOp.cpp), InvOp::verify intentionally restricts the body region to exactly two operations (one unitary + yield). Multi-op region inversion is by-design deferred to future work, potentially via a dedicated Unitary Operation for grouping gates.

Learnt from: li-mingbao
Repo: munich-quantum-toolkit/core PR: 1396
File: mlir/lib/Conversion/QCOToQC/QCOToQC.cpp:1045-1119
Timestamp: 2026-01-10T18:49:44.352Z
Learning: The QCOToQC conversion pass (mlir/lib/Conversion/QCOToQC/QCOToQC.cpp) does not need defensive mixed-type checks in its func conversion patterns (ConvertQCOFuncFuncOp, ConvertQCOFuncCallOp, ConvertQCOFuncReturnOp) because the conversion workflow always starts from QC to QCO, and the QCToQCO pass already enforces through its dynamic legality checks that func operations contain only qubit types (no mixed classical/quantum). This upstream guarantee justifies the all-qubit assumptions in QCOToQC patterns.

Learnt from: DRovara
Repo: munich-quantum-toolkit/core PR: 1108
File: mlir/test/Dialect/MQTOpt/Transforms/lift-measurements.mlir:269-288
Timestamp: 2025-10-09T13:20:11.483Z
Learning: In the MQT MLIR dialect, the `rz` gate should not be included in the `DIAGONAL_GATES` set for the `ReplaceBasisStateControlsWithIfPattern` because its operator matrix does not have the required shape | 1 0 | / | 0 x | for the targets-as-controls optimization. It is only included in `LiftMeasurementsAboveGatesPatterns` where the matrix structure requirement differs.

Learnt from: li-mingbao
Repo: munich-quantum-toolkit/core PR: 1396
File: mlir/lib/Conversion/QCToQCO/QCToQCO.cpp:1729-1763
Timestamp: 2026-01-10T16:28:41.975Z
Learning: In the QCToQCO conversion pass (mlir/lib/Conversion/QCToQCO/QCToQCO.cpp), the dynamic legality checks for func operations (func::CallOp, func::FuncOp, func::ReturnOp) and scf operations assume that operations use either all classical types or all qubit types, never mixed. Therefore, checking for the presence of qc::QubitType in operands or arguments is sufficient to determine if conversion is needed—there is no need to check both operands and results separately.
<!-- </add_learning]

Learnt from: denialhaag
Repo: munich-quantum-toolkit/core PR: 1264
File: mlir/lib/Dialect/Flux/IR/Operations/StandardGates/BarrierOp.cpp:45-54
Timestamp: 2025-12-09T00:55:11.926Z
Learning: In the Flux dialect (mlir/lib/Dialect/Flux), qubits follow single-user/linear-type semantics where each qubit value can only be consumed once, similar to MQTOpt. This invariant makes it safe to dereference getUsers().begin() in canonicalization patterns like MergeSubsequentBarrier in BarrierOp.cpp, as there will be at most one user per qubit output.

Learnt from: denialhaag
Repo: munich-quantum-toolkit/core PR: 1264
File: mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp:80-100
Timestamp: 2025-12-08T23:58:09.648Z
Learning: In the Quartz dialect (mlir/lib/Dialect/Quartz/IR/Modifiers/CtrlOp.cpp), quartz.ctrl uses reference semantics and does not return values, unlike flux.ctrl which uses value semantics and returns transformed qubits. When inlining a GPhaseOp in the CtrlInlineGPhase pattern, it's correct to create POp operations for positive controls and erase the CtrlOp without collecting or replacing result values.

Learnt from: li-mingbao
Repo: munich-quantum-toolkit/core PR: 1396
File: mlir/lib/Conversion/QCOToQC/QCOToQC.cpp:1070-1085
Timestamp: 2026-01-07T12:29:16.380Z
Learning: In the QCOToQC conversion pass (mlir/lib/Conversion/QCOToQC/QCOToQC.cpp), the ConvertQCOFuncFuncOp pattern assumes that when a func.func operation is matched for conversion, all of its arguments are qco.qubit types (never mixed qubit/classical). The pattern unconditionally converts all arguments to qc::QubitType based on this assumption.

Learnt from: denialhaag
Repo: munich-quantum-toolkit/core PR: 1264
File: mlir/include/mlir/Dialect/QCO/IR/QCOOps.td:259-259
Timestamp: 2025-12-17T17:44:31.349Z
Learning: In the QCO dialect (mlir/include/mlir/Dialect/QCO/IR/QCOOps.td), GPhaseOp intentionally uses `MemoryEffects<[MemWrite]>` instead of `Pure` to prevent the remove-dead-values pass from eliminating it. Since GPhaseOp is a zero-target operation with no result values, it would otherwise be removed by DCE, even though it has a meaningful effect on the global quantum state.

Learnt from: burgholzer
Repo: munich-quantum-toolkit/core PR: 1283
File: src/qir/runtime/QIR.cpp:196-201
Timestamp: 2025-11-01T15:57:31.153Z
Learning: In the QIR runtime (src/qir/runtime/QIR.cpp), the PRX gate (__quantum__qis__prx__body) is an alias for the R gate (Phased X-Rotation) and should call runtime.apply<qc::R>(theta, phi, qubit), not runtime.apply<qc::RX>() which is a single-parameter rotation gate.

Learnt from: taminob
Repo: munich-quantum-toolkit/core PR: 1206
File: mlir/include/mlir/Passes/Decomposition/GateSequence.h:58-68
Timestamp: 2026-02-06T22:14:18.231Z
Learning: In mlir/include/mlir/Passes/Decomposition/GateSequence.h, the complexity() method intentionally includes a conditional check for hasGlobalPhase() and adds helpers::getComplexity(qc::GPhase, 0) even though it currently returns 0. This is a forward-compatibility design pattern to allow future modification of the complexity calculation for global phase gates without requiring updates to the rest of the codebase.

Learnt from: li-mingbao
Repo: munich-quantum-toolkit/core PR: 1396
File: mlir/lib/Conversion/QCOToQC/QCOToQC.cpp:988-1024
Timestamp: 2026-01-10T16:07:55.896Z
Learning: In the QCOToQC conversion pass (mlir/lib/Conversion/QCOToQC/QCOToQC.cpp), the SCF operation conversion patterns (ConvertQCOScfYieldOp, ConvertQCOScfConditionOp, ConvertQCOScfIfOp, ConvertQCOScfWhileOp, ConvertQCOScfForOp) assume that all operands are qubit types (qco.qubit or qc.qubit), never mixed qubit/classical types. The conversion is scoped to handle all-qubit SCF operations only.

Learnt from: MatthiasReumann
Repo: munich-quantum-toolkit/core PR: 1301
File: mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/LayeredUnit.cpp:84-85
Timestamp: 2025-12-04T06:59:40.314Z
Learning: In the MQTOpt MLIR routing passes (NaiveRoutingPassSC, AStarRoutingPassSC), the input IR is guaranteed to contain only 1-qubit and 2-qubit gates. All 3+-qubit gates must be decomposed before routing; otherwise the input IR is invalid. This invariant allows skipTwoQubitBlock in LayeredUnit.cpp to safely assert wires.size() == 2.

@J4MMlE J4MMlE force-pushed the quaternion-rotation-merging branch 2 times, most recently from 8ff7ccc to f3cbe5b Compare February 8, 2026 22:18
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@mlir/lib/Dialect/QCO/Transforms/QuaternionMergeRotationGates.cpp`:
- Around line 419-434: matchAndRewrite currently inspects only the direct user
(userOP) which can be a YieldOp inside control regions (e.g. qco.ctrl/scf.if),
preventing merges of consecutive rotations; update the logic in matchAndRewrite
(and/or add a small helper like unwrapYieldUser) to recursively skip
mlir::YieldOp (and any trivial forwarding ops) when walking from op->getUsers()
to the real consuming operation so that areQuaternionMergeable sees the actual
successor UnitaryOpInterface instead of a YieldOp; adjust the place where userOP
is computed (and the dyn_cast to UnitartyOpInterface) to use this unwrapping
helper and keep fallbacks to mlir::failure() if no suitable consumer is found.

In `@mlir/unittests/Dialect/QCO/Transforms/test_qco_quaternion_merge.cpp`:
- Around line 138-155: The RX/RY/RZ cases in buildRotations use gate.angles[0]
without checking size; add assertions like in the U case to guard against OOB:
in function buildRotations, for the GateType::RX, GateType::RY and GateType::RZ
branches insert an assert(gate.angles.size() >= 1 && "RX/RY/RZ require 1 angle
parameter") before referencing gate.angles[0] so the code fails early with a
clear message.

Comment on lines +138 to +155
Value buildRotations(const std::vector<RotationGate>& rotations, Value& q) {
Value qubit = q;

for (const auto& gate : rotations) {
switch (gate.type) {
case GateType::RX:
qubit = builder.rx(gate.angles[0], qubit);
break;
case GateType::RY:
qubit = builder.ry(gate.angles[0], qubit);
break;
case GateType::RZ:
qubit = builder.rz(gate.angles[0], qubit);
break;
case GateType::U:
assert(gate.angles.size() >= 3 && "UOp requires 3 angle parameters");
qubit =
builder.u(gate.angles[0], gate.angles[1], gate.angles[2], qubit);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard RX/RY/RZ angle indexing to avoid accidental OOB.

gate.angles[0] is unguarded for single-parameter gates; adding asserts (like the U case) avoids hard-to-debug failures in future tests.

🛡️ Suggested guard
     switch (gate.type) {
     case GateType::RX:
+      assert(gate.angles.size() >= 1 && "RXOp requires 1 angle parameter");
       qubit = builder.rx(gate.angles[0], qubit);
       break;
     case GateType::RY:
+      assert(gate.angles.size() >= 1 && "RYOp requires 1 angle parameter");
       qubit = builder.ry(gate.angles[0], qubit);
       break;
     case GateType::RZ:
+      assert(gate.angles.size() >= 1 && "RZOp requires 1 angle parameter");
       qubit = builder.rz(gate.angles[0], qubit);
       break;
     case GateType::U:
       assert(gate.angles.size() >= 3 && "UOp requires 3 angle parameters");
       qubit =
           builder.u(gate.angles[0], gate.angles[1], gate.angles[2], qubit);
       break;
     }
🤖 Prompt for AI Agents
In `@mlir/unittests/Dialect/QCO/Transforms/test_qco_quaternion_merge.cpp` around
lines 138 - 155, The RX/RY/RZ cases in buildRotations use gate.angles[0] without
checking size; add assertions like in the U case to guard against OOB: in
function buildRotations, for the GateType::RX, GateType::RY and GateType::RZ
branches insert an assert(gate.angles.size() >= 1 && "RX/RY/RZ require 1 angle
parameter") before referencing gate.angles[0] so the code fails early with a
clear message.

@J4MMlE J4MMlE force-pushed the quaternion-rotation-merging branch 2 times, most recently from 439aeda to f6c28ba Compare February 8, 2026 22:53
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@mlir/include/mlir/Dialect/QCO/Transforms/Passes.td`:
- Around line 11-20: The MergeRotationGates pass declares dependentDialects but
omits the ArithDialect used by its implementation; update the pass definition
for MergeRotationGates to include the arith dialect in the dependentDialects
list (add "::mlir::arith::ArithDialect") so that arith::ConstantOp,
arith::DivFOp, arith::MulFOp, arith::SubFOp, arith::AddFOp, etc. are available
when the pass runs.

In `@mlir/lib/Dialect/QCO/Transforms/QuaternionMergeRotationGates.cpp`:
- Around line 419-446: The new merged-op must be created with the insertion
point set before the second gate to preserve SSA dominance: in matchAndRewrite,
create an mlir::OpBuilder::InsertionGuard guard(rewriter) and call
rewriter.setInsertionPointBefore(user.getOperation()) (or
rewriter.setInsertionPoint(user.getOperation())) immediately before invoking
createOpQuaternionMergedAngle(op, user, rewriter), then let the guard restore
the original insertion point; keep the subsequent rewriter.replaceOp(user,
newUser) and rewriter.eraseOp(op) unchanged.

In `@mlir/unittests/Dialect/QCO/Transforms/test_qco_quaternion_merge.cpp`:
- Around line 602-785: The tests reveal equivalent angles differing by multiples
of 2π (e.g. numericalRotationIdentity expects TAU vs 0); update
expectUGateParams to compare angles modulo 2*PI (TAU) instead of raw equality:
when retrieving theta/phi/lambda in expectUGateParams, normalize both expected
and actual angles into a canonical range (e.g. [0, 2*PI) or (-PI, PI]) or
perform equality by checking that their difference modulo TAU is within
tolerance; adjust comparisons used in expectUGateParams so tests like
numericalRotationIdentity and others accept equivalent-angle representations
without changing the test vectors.

Comment on lines +602 to +785
// ##################################################
// # Numerical Correctness
// ##################################################
// Expected values were computed using SymPy's quaternion algebra:
// https://docs.sympy.org/latest/modules/algebras.html#module-sympy.algebras.Quaternion

/**
* @brief Test: RX(1)->RY(1) should merge into
* U(1.27455578230629, -1.07542903757622, 0.495367289218673)
*/
TEST_F(QCOQuaternionMergeTest, numericalAccuracyRXRY) {
ASSERT_TRUE(
testGateMerge({{GateType::RX, {1.}}, {GateType::RY, {1.}}}).succeeded());
EXPECT_EQ(countOps<UOp>(), 1);
EXPECT_EQ(countOps<RXOp>(), 0);
EXPECT_EQ(countOps<RYOp>(), 0);

expectUGateParams(1.27455578230629, -1.07542903757622, 0.495367289218673);
}

/**
* @brief Test: RX(1)->RZ(1) should merge into
* U(1.00000000000000, -0.570796326794897, 1.57079632679490)
*/
TEST_F(QCOQuaternionMergeTest, numericalAccuracyRXRZ) {
ASSERT_TRUE(
testGateMerge({{GateType::RX, {1.}}, {GateType::RZ, {1.}}}).succeeded());
EXPECT_EQ(countOps<UOp>(), 1);
EXPECT_EQ(countOps<RXOp>(), 0);
EXPECT_EQ(countOps<RZOp>(), 0);

expectUGateParams(1.00000000000000, -0.570796326794897, 1.57079632679490);
}

/**
* @brief Test: RY(1)->RX(1) should merge into
* U(1.27455578230629, -0.495367289218673, 1.07542903757622)
*/
TEST_F(QCOQuaternionMergeTest, numericalAccuracyRYRX) {
ASSERT_TRUE(
testGateMerge({{GateType::RY, {1.}}, {GateType::RX, {1.}}}).succeeded());
EXPECT_EQ(countOps<UOp>(), 1);
EXPECT_EQ(countOps<RYOp>(), 0);
EXPECT_EQ(countOps<RXOp>(), 0);

expectUGateParams(1.27455578230629, -0.495367289218673, 1.07542903757622);
}

/**
* @brief Test: RY(1)->RZ(1) should merge into
* U(1.00000000000000, 1.00000000000000, 0.)
*/
TEST_F(QCOQuaternionMergeTest, numericalAccuracyRYRZ) {
ASSERT_TRUE(
testGateMerge({{GateType::RY, {1.}}, {GateType::RZ, {1.}}}).succeeded());
EXPECT_EQ(countOps<UOp>(), 1);
EXPECT_EQ(countOps<RYOp>(), 0);
EXPECT_EQ(countOps<RZOp>(), 0);

expectUGateParams(1.00000000000000, 1.00000000000000, 0.);
}

/**
* @brief Test: RZ(1)->RX(1) should merge into
* U(1.00000000000000, -1.57079632679490, 2.57079632679490)
*/
TEST_F(QCOQuaternionMergeTest, numericalAccuracyRZRX) {
ASSERT_TRUE(
testGateMerge({{GateType::RZ, {1.}}, {GateType::RX, {1.}}}).succeeded());
EXPECT_EQ(countOps<UOp>(), 1);
EXPECT_EQ(countOps<RZOp>(), 0);
EXPECT_EQ(countOps<RXOp>(), 0);

expectUGateParams(1.00000000000000, -1.57079632679490, 2.57079632679490);
}

/**
* @brief Test: RZ(1)->RY(1) should merge into
* U(1.00000000000000, 0, 1.00000000000000)
*/
TEST_F(QCOQuaternionMergeTest, numericalAccuracyRZRY) {
ASSERT_TRUE(
testGateMerge({{GateType::RZ, {1.}}, {GateType::RY, {1.}}}).succeeded());
EXPECT_EQ(countOps<UOp>(), 1);
EXPECT_EQ(countOps<RZOp>(), 0);
EXPECT_EQ(countOps<RYOp>(), 0);

expectUGateParams(1.00000000000000, 0., 1.00000000000000);
}

/**
* @brief Test: U(1,2,3)->U(4,5,6) should merge into
* U(2.03289042623884, 0.663830775701153, -5.43395386531173)
*/
TEST_F(QCOQuaternionMergeTest, numericalAccuracyUU) {
ASSERT_TRUE(
testGateMerge({{GateType::U, {1., 2., 3.}}, {GateType::U, {4., 5., 6.}}})
.succeeded());
EXPECT_EQ(countOps<UOp>(), 1);

expectUGateParams(2.03289042623884, 0.663830775701153, -5.43395386531173);
}

/**
* @brief Test: RZ(PI)->RY(PI)->RX(PI) should merge into
* U(0, 0, 0) or U(0, 2*PI, 0)
*/
TEST_F(QCOQuaternionMergeTest, numericalRotationIdentity) {
ASSERT_TRUE(
testGateMerge(
{{GateType::RZ, {PI}}, {GateType::RY, {PI}}, {GateType::RX, {PI}}})
.succeeded());
EXPECT_EQ(countOps<UOp>(), 1);
EXPECT_EQ(countOps<RYOp>(), 0);
EXPECT_EQ(countOps<RZOp>(), 0);

expectUGateParams(0, TAU, 0.);
}

/**
* @brief Test: RY(1)->RZ(1)->RY(-1)->RZ(-1) should merge into
* U(0, 0, 0)
*/
TEST_F(QCOQuaternionMergeTest, numericalRotationIdentity2) {
ASSERT_TRUE(testGateMerge({{GateType::RY, {1}},
{GateType::RZ, {1}},
{GateType::RZ, {-1}},
{GateType::RY, {-1}}})
.succeeded());
EXPECT_EQ(countOps<UOp>(), 1);
EXPECT_EQ(countOps<RYOp>(), 0);
EXPECT_EQ(countOps<RZOp>(), 0);

expectUGateParams(0., 0., 0.);
}

/**
* @brief Test: RX(0.001)->RY(0.001) should merge into
* U(0.00141421344452194, -0.785398413397490, 0.785397913397407)
*/
TEST_F(QCOQuaternionMergeTest, numericalSmallAngles) {
ASSERT_TRUE(testGateMerge({{GateType::RX, {0.001}}, {GateType::RY, {0.001}}})
.succeeded());
EXPECT_EQ(countOps<UOp>(), 1);
EXPECT_EQ(countOps<RXOp>(), 0);
EXPECT_EQ(countOps<RYOp>(), 0);

expectUGateParams(0.00141421344452194, -0.785398413397490, 0.785397913397407);
}

/**
* @brief Test: RX(PI)->RY(PI) should merge into
* U(0, -PI, 0.)
*/
TEST_F(QCOQuaternionMergeTest, numericalGimbalLock) {
ASSERT_TRUE(
testGateMerge({{GateType::RX, {PI}}, {GateType::RY, {PI}}}).succeeded());
EXPECT_EQ(countOps<UOp>(), 1);
EXPECT_EQ(countOps<RXOp>(), 0);
EXPECT_EQ(countOps<RYOp>(), 0);

expectUGateParams(0, -PI, 0.);
}

/**
* @brief Test: U(0, -2.0360075460227076, 0) -> U(0, 4.157656961105587, 0)
* should not produce NaN.
* These specific numbers would produce NaN if acos parameter would not be
* clamped to [-1, 1]
*/
TEST_F(QCOQuaternionMergeTest, numericalAcosClampingPreventsNaN) {
ASSERT_TRUE(testGateMerge({{GateType::U, {0, -2.0360075460227076, 0}},
{GateType::U, {0, 4.157656961105587, 0}}})
.succeeded());
EXPECT_EQ(countOps<UOp>(), 1);

auto params = getUGateParams();
ASSERT_TRUE(params.has_value());

auto [theta, phi, lambda] = *params;
EXPECT_FALSE(std::isnan(theta));
EXPECT_FALSE(std::isnan(phi));
EXPECT_FALSE(std::isnan(lambda));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Thorough numerical verification with edge case coverage.

The numerical correctness tests with SymPy-verified reference values are particularly valuable. The gimbal lock test (Line 756) and acos clamping test (Line 772) specifically target known numerical pitfalls in the quaternion-to-Euler conversion, which is excellent defensive testing.

One minor note: Line 131 has a TODO: maybe check angle equality modulo 2*PI. This could matter for tests like numericalRotationIdentity (Line 718) where the expected phi is TAU (2π) rather than 0 — both are physically equivalent. Consider whether this normalization should be added.

Would you like me to help implement the modulo-2π angle comparison in expectUGateParams, or open an issue to track it?

🤖 Prompt for AI Agents
In `@mlir/unittests/Dialect/QCO/Transforms/test_qco_quaternion_merge.cpp` around
lines 602 - 785, The tests reveal equivalent angles differing by multiples of 2π
(e.g. numericalRotationIdentity expects TAU vs 0); update expectUGateParams to
compare angles modulo 2*PI (TAU) instead of raw equality: when retrieving
theta/phi/lambda in expectUGateParams, normalize both expected and actual angles
into a canonical range (e.g. [0, 2*PI) or (-PI, PI]) or perform equality by
checking that their difference modulo TAU is within tolerance; adjust
comparisons used in expectUGateParams so tests like numericalRotationIdentity
and others accept equivalent-angle representations without changing the test
vectors.

@J4MMlE
Copy link
Author

J4MMlE commented Feb 9, 2026

Hey @DRovara & @burgholzer,

I think this is (finally) ready to be considered for merging. There are a few things that I would like to point out:

  1. Currently only RX, RY, RZ, and U gates are merged, but adding other gates should be trivial and could be addressed in a follow-up PR.
  2. @DRovara already pointed out (here) that controlled gates are not merged. This is also something which could be added in the future (I think there are some things to consider regarding global phase, right?).
  3. The resulting u-gate can contain parameters greater than 2PI. Maybe it might make sense to make the output bound to [0, 2PI] or [-PI, PI] (there is one test where the output is U(0, 2PI, 0) instead of U(0, 0, 0), which I think would be cleaner). Also, the tests could check for any result x mod 2PI.

Besides that, what I should put into the CHANGELOG.md - should I just add my name + #PR into Add initial infrastructure for new QC and QCO MLIR dialects?

Also, feel free to comment on the things that @coderabbitai pointed out, and which i have not addressed yet.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 9, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the CHANGELOG.md to see the current structure and help answer the changelog question
cat CHANGELOG.md | head -100

Length of output: 6935


🏁 Script executed:

#!/bin/bash
# Search for the specific entry mentioned by the user about QC and QCO MLIR dialects
rg -n "Add initial infrastructure for new QC and QCO MLIR dialects" CHANGELOG.md -A 5 -B 2

Length of output: 429


🏁 Script executed:

#!/bin/bash
# Check if there's already a section for version or unreleased changes
rg -n "Unreleased|## \[" CHANGELOG.md | head -20

Length of output: 379


@J4MMlE Great work on getting this ready! Here's my feedback on your points:

Implementation Points

1. Gate support (RX, RY, RZ, U only): This is a reasonable scope for the initial implementation. Adding support for other rotation gates (like phase gates, controlled rotations, etc.) in a follow-up PR makes perfect sense. The quaternion infrastructure you've built provides a solid foundation for extending to additional gate types.

2. Controlled gates: This is indeed a known limitation that @DRovara highlighted. The issue with qco.ctrl regions and yield operations is subtle but important. For now, documenting this limitation is fine, and a follow-up can address it—potentially with the UnitaryOpInterface helper method DRovara suggested. You're right that global phase considerations will need to be handled carefully for controlled gates.

3. Angle normalization: This is a good observation. For cleaner output and numerical stability, I'd recommend normalizing the U-gate parameters to [0, 2π) or [-π, π). The test case you mentioned (U(0, 2π, 0) vs U(0, 0, 0)) would indeed be cleaner as U(0, 0, 0). You could also make the test assertions check x mod 2π equivalence rather than exact equality, which would be more mathematically correct and robust to different normalization choices.

CHANGELOG Entry

Looking at the current CHANGELOG.md structure, you have two options:

Option 1 (Recommended): Add a new separate entry under the ### Added section, since this is a distinct feature:

- ✨ Add rotation gate merging using quaternion-based fusion for QCO dialect ([`#1407`]) ([**@J4MMlE**])

Option 2: Add your PR and name to the existing QCO infrastructure entry:

- ✨ Add initial infrastructure for new QC and QCO MLIR dialects ([`#1264`], [`#1402`], [`#1407`], [`#1428`], ...) ([**@burgholzer**], [**@denialhaag**], [**@taminob**], [**@DRovara**], [**@J4MMlE**], [**@li-mingbao**])

I'd recommend Option 1 because quaternion-based rotation merging is a substantial, user-visible optimization feature rather than just infrastructure work. It deserves its own line for visibility.

Outstanding Items

From my perspective, the implementation looks solid! The test coverage is comprehensive, the code structure is clean, and you've addressed the migration to the QCO dialect that @burgholzer requested. The items you've identified (gate support limitations, controlled gates, angle normalization) are all reasonable to defer or address in follow-ups rather than blocking this PR.

🐰✨


🧠 Learnings used
Learnt from: li-mingbao
Repo: munich-quantum-toolkit/core PR: 1396
File: mlir/lib/Conversion/QCOToQC/QCOToQC.cpp:1045-1119
Timestamp: 2026-01-10T18:49:44.352Z
Learning: The QCOToQC conversion pass (mlir/lib/Conversion/QCOToQC/QCOToQC.cpp) does not need defensive mixed-type checks in its func conversion patterns (ConvertQCOFuncFuncOp, ConvertQCOFuncCallOp, ConvertQCOFuncReturnOp) because the conversion workflow always starts from QC to QCO, and the QCToQCO pass already enforces through its dynamic legality checks that func operations contain only qubit types (no mixed classical/quantum). This upstream guarantee justifies the all-qubit assumptions in QCOToQC patterns.

Learnt from: burgholzer
Repo: munich-quantum-toolkit/core PR: 1283
File: src/qir/runtime/QIR.cpp:196-201
Timestamp: 2025-11-01T15:57:31.153Z
Learning: In the QIR runtime (src/qir/runtime/QIR.cpp), the PRX gate (__quantum__qis__prx__body) is an alias for the R gate (Phased X-Rotation) and should call runtime.apply<qc::R>(theta, phi, qubit), not runtime.apply<qc::RX>() which is a single-parameter rotation gate.

Learnt from: MatthiasReumann
Repo: munich-quantum-toolkit/core PR: 1301
File: mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/LayeredUnit.cpp:84-85
Timestamp: 2025-12-04T06:59:40.314Z
Learning: In the MQTOpt MLIR routing passes (NaiveRoutingPassSC, AStarRoutingPassSC), the input IR is guaranteed to contain only 1-qubit and 2-qubit gates. All 3+-qubit gates must be decomposed before routing; otherwise the input IR is invalid. This invariant allows skipTwoQubitBlock in LayeredUnit.cpp to safely assert wires.size() == 2.

@J4MMlE
Copy link
Author

J4MMlE commented Feb 9, 2026

@coderabbitai resume

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 9, 2026

✅ Actions performed

Reviews resumed.

@DRovara
Copy link
Collaborator

DRovara commented Feb 9, 2026

Thanks for the work, @J4MMlE!

  1. The resulting u-gate can contain parameters greater than 2PI. Maybe it might make sense to make the output bound to [0, 2PI] or [-PI, PI] (there is one test where the output is U(0, 2PI, 0) instead of U(0, 0, 0), which I think would be cleaner). Also, the tests could check for any result x mod 2PI.

I guess this would make sense as a canonicalisation for rotation gates. After all, there may be other passes that also lead to angles >= 2PI, so we could handle all of that in the canonicalisation.

Besides that, what I should put into the CHANGELOG.md - should I just add my name + #PR into Add initial infrastructure for new QC and QCO MLIR dialects?

Yeah, typically the name of the PR is descriptive enough, then just follow the same structure as other entires.

Also, feel free to comment on the things that @coderabbitai pointed out, and which i have not addressed yet.

Most of the comments that are left don't seem like too big of a deal.
That being said, this comment seems reasonably (didn't know we had an established pattern for this, but if we do we should definitely keep it up), and it also seems reasonable to add arith as a dependent dialect, I believe.

Other than that, I will yield for now and let @burgholzer have a look.

@DRovara DRovara requested a review from burgholzer February 9, 2026 07:59
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 9, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Fetch review comments on PR `#1407` to find the specific discussions mentioned
gh pr view 1407 --repo munich-quantum-toolkit/core --json reviews,comments,reviewThreads > pr_reviews.json

# Parse and display review threads to find the two specific discussions
cat pr_reviews.json | jq -r '.reviewThreads[] | "\n=== Thread ===\nPath: \(.path)\nLine: \(.line)\nComments: \(.comments | length)\n" + (.comments[] | "Author: \(.author.login)\nBody: \(.body)\n---")'

# Also try to get the specific comments directly using the GitHub API via gh
echo "=== Searching for specific discussions ==="
gh api repos/munich-quantum-toolkit/core/pulls/1407/comments | jq -r '.[] | select(.id == 2780012212 or .id == 2780012208) | "ID: \(.id)\nPath: \(.path)\nLine: \(.line)\nBody: \(.body)\n==="'

Length of output: 717


🏁 Script executed:

#!/bin/bash
# Fetch review comments using available fields
gh pr view 1407 --repo munich-quantum-toolkit/core --json reviews > pr_reviews.json

# Display reviews
cat pr_reviews.json | jq -r '.reviews[] | "=== Review by \(.author.login) ===\nState: \(.state)\nBody: \(.body)\n"'

# Get all review comments from the PR
gh api repos/munich-quantum-toolkit/core/pulls/1407/comments | jq -r '.[] | "=== Comment ID: \(.id) ===\nPath: \(.path)\nLine: \(.line // .original_line)\nAuthor: \(.user.login)\nBody: \(.body)\n"' | head -200

# Try to find comments with specific patterns about dependent dialects or established patterns
echo "=== Searching for arith dialect comment ==="
gh api repos/munich-quantum-toolkit/core/pulls/1407/comments | jq -r '.[] | select(.body | test("arith|dependent.*dialect"; "i")) | "ID: \(.id)\nPath: \(.path)\nBody: \(.body)\n==="'

echo "=== Searching for pattern comment ==="
gh api repos/munich-quantum-toolkit/core/pulls/1407/comments | jq -r '.[] | select(.body | test("pattern"; "i")) | "ID: \(.id)\nPath: \(.path)\nBody: \(.body)\n==="' | head -100

Length of output: 24599


@DRovara Thank you for the clear guidance! Your feedback is helpful for getting this PR across the finish line.

Angle normalization via canonicalization makes perfect sense – it's a more general solution that will benefit any pass that produces angles ≥ 2π, not just quaternion merging. That's the right architectural choice.

CHANGELOG structure – understood, will follow the existing pattern with a dedicated entry for this feature.

Regarding the two specific CodeRabbit comments:

  1. Established pattern: Based on the review thread, this appears to be about using TypeSwitch<> and isa<> for gate type checking rather than string comparisons – a cleaner MLIR idiom that @J4MMlE seems to have already explored in the thread.

  2. ArithDialect dependency: The pass creates various arith:: operations (ConstantOp, DivFOp, MulFOp, AddFOp, SubFOp, etc.) but the dependentDialects in Passes.td currently only lists MathDialect. Adding ArithDialect to that list ensures the dialect is loaded when the pass runs.

Both of these are straightforward fixes that align with MLIR best practices. Once @J4MMlE addresses these and @burgholzer gives the final review, this should be ready to merge! 🚀


🧠 Learnings used
Learnt from: li-mingbao
Repo: munich-quantum-toolkit/core PR: 1396
File: mlir/lib/Conversion/QCOToQC/QCOToQC.cpp:1045-1119
Timestamp: 2026-01-10T18:49:44.352Z
Learning: The QCOToQC conversion pass (mlir/lib/Conversion/QCOToQC/QCOToQC.cpp) does not need defensive mixed-type checks in its func conversion patterns (ConvertQCOFuncFuncOp, ConvertQCOFuncCallOp, ConvertQCOFuncReturnOp) because the conversion workflow always starts from QC to QCO, and the QCToQCO pass already enforces through its dynamic legality checks that func operations contain only qubit types (no mixed classical/quantum). This upstream guarantee justifies the all-qubit assumptions in QCOToQC patterns.

Learnt from: li-mingbao
Repo: munich-quantum-toolkit/core PR: 1396
File: mlir/lib/Conversion/QCToQCO/QCToQCO.cpp:1729-1763
Timestamp: 2026-01-10T16:28:41.975Z
Learning: In the QCToQCO conversion pass (mlir/lib/Conversion/QCToQCO/QCToQCO.cpp), the dynamic legality checks for func operations (func::CallOp, func::FuncOp, func::ReturnOp) and scf operations assume that operations use either all classical types or all qubit types, never mixed. Therefore, checking for the presence of qc::QubitType in operands or arguments is sufficient to determine if conversion is needed—there is no need to check both operands and results separately.
<!-- </add_learning]

Learnt from: MatthiasReumann
Repo: munich-quantum-toolkit/core PR: 1301
File: mlir/lib/Dialect/MQTOpt/Transforms/Transpilation/LayeredUnit.cpp:84-85
Timestamp: 2025-12-04T06:59:40.314Z
Learning: In the MQTOpt MLIR routing passes (NaiveRoutingPassSC, AStarRoutingPassSC), the input IR is guaranteed to contain only 1-qubit and 2-qubit gates. All 3+-qubit gates must be decomposed before routing; otherwise the input IR is invalid. This invariant allows skipTwoQubitBlock in LayeredUnit.cpp to safely assert wires.size() == 2.

Learnt from: burgholzer
Repo: munich-quantum-toolkit/core PR: 1283
File: src/qir/runtime/QIR.cpp:196-201
Timestamp: 2025-11-01T15:57:31.153Z
Learning: In the QIR runtime (src/qir/runtime/QIR.cpp), the PRX gate (__quantum__qis__prx__body) is an alias for the R gate (Phased X-Rotation) and should call runtime.apply<qc::R>(theta, phi, qubit), not runtime.apply<qc::RX>() which is a single-parameter rotation gate.

@burgholzer
Copy link
Member

Great to see this being ready for review 🎉

It's on my pile of PRs to go through. More likely than not, I am going to push some changes directly to the branch as I work through it. I'll try to get to the PR asap.

@J4MMlE J4MMlE force-pushed the quaternion-rotation-merging branch from f6c28ba to a80f315 Compare February 10, 2026 10:22
@J4MMlE J4MMlE force-pushed the quaternion-rotation-merging branch 2 times, most recently from bdba4ce to 2ac2923 Compare February 10, 2026 10:32
@J4MMlE J4MMlE force-pushed the quaternion-rotation-merging branch from 2ac2923 to aa8caf4 Compare February 10, 2026 10:50
Copy link
Member

@burgholzer burgholzer left a comment

Choose a reason for hiding this comment

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

Sorry for the delay in getting a review in for this PR.
Thanks for your work! I like how this is looking. Especially that it can also handle non-constant/runtime values.
While I haven't reviewed the tests in detail, I have accumulated quite a couple of comments/feedback on the actual pass logic.
While #1513 is not in, I'd want to focus this PR on the actual functional changes, before thinking about the tests too much.
Hope that makes sense.

bool printIRAfterAllStages = false;

/// Enable quaternion-based rotation gate merging
bool mergeRotationGates = false;
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
bool mergeRotationGates = false;
bool mergeSingleQubitRotationGates = false;

Just a suggestion to make this a little more precise. I am not sure that we will be handling two-qubit rotations anytime soon. This extends to the name of the pass itself and all occurrences throughout the PR.

"mlir::ModuleOp"> {
let summary = "Merge rotation gates using quaternion-based fusion";
let description = [{
Merges consecutive rotation gates of different types (rx, ry, rz, u)
Copy link
Member

Choose a reason for hiding this comment

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

What about U2Op (u2), ROp (r), and POp (p). These are also rotations and shouldn't be too hard to add so that the full spectrum of single-qubit rotations is covered, right?

Comment on lines 163 to 165
// Stage 5: Optimization passes
// TODO: Add optimization passes
addCleanupPasses(pm);
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
// Stage 5: Optimization passes
// TODO: Add optimization passes
addCleanupPasses(pm);
// Stage 5: Optimization passes

Now that we have an actual optimization pass, the cleanup passes are not needed here and we can actually run the pass if it is configured.

Comment on lines +168 to 173
if (config_.mergeRotationGates) {
pm.addPass(mlir::qco::createMergeRotationGates());
if (failed(pm.run(module))) {
return failure();
}
}
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
if (config_.mergeRotationGates) {
pm.addPass(mlir::qco::createMergeRotationGates());
if (failed(pm.run(module))) {
return failure();
}
}
if (config_.mergeRotationGates) {
pm.addPass(mlir::qco::createMergeRotationGates());
}
if (failed(pm.run(module))) {
return failure();
}

Running the pass manager for the optimization stage should not be conditional.
As we add compiler passes, this is the pass manager that will become increasingly complex supposedly.
If the current pass is not enabled, the pass manager should be empty and I would hope that the run method then simply returns immediately.

Comment on lines +9 to +10
get_property(dialect_libs GLOBAL PROPERTY MLIR_DIALECT_LIBS)
set(LIBRARIES ${dialect_libs} MQT::CoreIR)
Copy link
Member

Choose a reason for hiding this comment

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

I would prefer to more selectively link against the necessary MLIR libraries instead of this blanket link. This is also how we do it in the other CMake files in the new dialects.

Comment on lines +422 to +426
// QCO operations cannot contain control qubits, so no need to check for
// them
if (!op->hasOneUse()) {
return mlir::failure();
}
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't this be true by construction as we use linear types in QCO?
I believe this can be removed.

Copy link
Collaborator

Choose a reason for hiding this comment

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

We were recently discussing the implementation of a qco.if that keeps up linear types, so you would be right now.

Back when this PR was written, the plan was still to use scf.if, in which case there could have been two uses.

Copy link
Member

Choose a reason for hiding this comment

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

I think it is fair to assume now that we enforce linear types.

Comment on lines +428 to +429
const auto& users = op->getUsers();
auto* userOP = *users.begin();
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
const auto& users = op->getUsers();
auto* userOP = *users.begin();
auto* userOP = *op->getUsers().begin();

users is not needed elsewhere, might as well inline.

Comment on lines +438 to +446
rewriter.setInsertionPoint(user);
const UnitaryOpInterface newUser =
createOpQuaternionMergedAngle(op, user, rewriter);

// Replace user with newUser
rewriter.replaceOp(user, newUser);

// Erase op
rewriter.eraseOp(op);
Copy link
Member

Choose a reason for hiding this comment

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

I think this can be written a little bit more idiomatically. The rewriter has methods for directly replacing an operation with a new operation that could be used in createOpQuaternionMergedAngle.
I also think that, generally, op should be replaced here and userOp should be the one that is removed.
Finally, the setInsertionPoint should not be necessary then.

Comment on lines +479 to +482
// Apply patterns in an iterative and greedy manner.
if (mlir::failed(mlir::applyPatternsGreedily(op, std::move(patterns)))) {
signalPassFailure();
}
Copy link
Member

Choose a reason for hiding this comment

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

Would it make sense to collect runs of mergable gates as part of this pass or is it not to be expected that this yields much of an advantage?
e.g., imagine the code contains frequent sequences of rz-ry-rz gates that could be combined in one go.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c++ Anything related to C++ code feature New feature or request MLIR Anything related to MLIR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✨ MLIR - Support merging of more complex gates

3 participants