Skip to content

✨ Add Rotation Gate Merging using Quaternions#1407

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

✨ Add Rotation Gate Merging using Quaternions#1407
J4MMlE wants to merge 31 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 QCO 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, r, p, u2, 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-single-qubit-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

✅ All modified and coverable lines are covered by tests.

📢 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 a new optimization pass that merges consecutive single-qubit rotation gates using quaternion-based mathematics, helping reduce circuit depth. Enable with the new --mlir-merge-single-qubit-rotation-gates compiler flag.
  • Tests

    • Added comprehensive test suite for the rotation gate merging optimization.

Walkthrough

Adds a quaternion-based MergeRotationGates MLIR pass, a QuantumCompilerConfig flag and CLI option to enable it, conditionally integrates the pass into the compiler pipeline, updates CMake linkages and test targets, and adds unit tests and a changelog entry.

Changes

Cohort / File(s) Summary
Config & Pass Declarations
mlir/include/mlir/Compiler/CompilerPipeline.h, mlir/include/mlir/Dialect/QCO/Transforms/Passes.td, mlir/include/mlir/Dialect/QCO/Transforms/Passes.h
Adds bool mergeSingleQubitRotationGates = false to QuantumCompilerConfig; introduces MergeRotationGates TableGen pass; removes a small comment block in Passes.h.
Optimization Implementation
mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp
New greedy rewrite pass merging consecutive rotation-like gates into one UOp via quaternion conversion, Hamilton product accumulation, quaternion→ZYZ extraction with gimbal-lock handling, SSA rewiring, and pass failure signaling on pattern application failure.
Pipeline Integration & CLI
mlir/lib/Compiler/CompilerPipeline.cpp, mlir/tools/mqt-cc/mqt-cc.cpp
Includes QCO pass header; Stage 5 conditionally adds createMergeRotationGates() when config_.mergeSingleQubitRotationGates is true; adds CLI flag --mlir-merge-single-qubit-rotation-gates wired into the compiler config.
Build System (Compiler & QCO)
mlir/lib/Compiler/CMakeLists.txt, mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt
Adds MLIRQCOTransforms to MQTCompilerPipeline public link libs; replaces ${dialect_libs} with explicit MLIRArithDialect and MLIRMathDialect in QCO transforms private links.
Unit Tests & Test CMake
mlir/unittests/Compiler/test_compiler_pipeline.cpp, mlir/unittests/Dialect/QCO/Transforms/Optimizations/CMakeLists.txt, mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_quaternion_merge.cpp
Extends test harness to pass the merge flag; adds new test executable and extensive tests validating structural and numerical correctness of quaternion merging across many scenarios, including NaN/gimbal-lock checks.
Test Integration & Minor CMake Edits
mlir/unittests/Dialect/QCO/Transforms/CMakeLists.txt, mlir/unittests/Dialect/QCO/Transforms/Mapping/CMakeLists.txt
Adds add_subdirectory(Optimizations) to include new tests; reorders some PRIVATE link libraries in an existing test CMake file.
Changelog
CHANGELOG.md
Adds Unreleased entry and PR/contributor reference for the new pass (PR #1407).

Sequence Diagram(s)

sequenceDiagram
    rect rgba(200,200,255,0.5)
    participant CLI as CLI (mqt-cc)
    end
    rect rgba(200,255,200,0.5)
    participant Pipeline as CompilerPipeline
    participant PM as PassManager
    participant QCO as MLIRQCOTransforms
    end

    CLI->>Pipeline: set QuantumCompilerConfig.mergeSingleQubitRotationGates
    Pipeline->>PM: build Stage 5 passes
    alt mergeSingleQubitRotationGates == true
        Pipeline->>PM: add MergeRotationGates pass
    end
    PM->>QCO: execute MergeRotationGates (pattern rewrites)
    QCO-->>PM: success / failure
    PM-->>Pipeline: continue remaining stages
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • burgholzer

Poem

🐰 I hop through quaternions, light and fleet,
I twirl two rotations till they meet.
Hamilton hums, angles find their way,
One neat U replaces the fray.
Hooray — small circuits, happy feet.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding quaternion-based rotation gate merging. It is concise, specific, and directly reflects the primary objective of the PR.
Description check ✅ Passed The PR description is comprehensive, covering motivation, implementation details, usage, and checklist items. All major required sections are present and appropriately filled.
Docstring Coverage ✅ Passed Docstring coverage is 87.59% 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 unit tests (beta)
  • Create PR with unit tests

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.

@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.

@J4MMlE J4MMlE force-pushed the quaternion-rotation-merging branch 2 times, most recently from 8ff7ccc to f3cbe5b Compare February 8, 2026 22:18
@J4MMlE J4MMlE force-pushed the quaternion-rotation-merging branch from b73951e to 51fbbfa Compare March 21, 2026 23:15
@J4MMlE
Copy link
Author

J4MMlE commented Mar 21, 2026

@burgholzer its ready for review again!

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: 5

♻️ Duplicate comments (1)
mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp (1)

591-605: ⚠️ Potential issue | 🟠 Major

Reintroduce an explicit insertion-point guard before building the merged chain.

All arith::/math:: ops for the merged quaternion are created at the current rewrite insertion point, which is still the chain head here. If a later gate angle is produced by an arith op between chain elements, the new ops will be inserted before that definition and violate SSA dominance. Please move the insertion point to chain.back().getOperation() (or just before the first post-chain consumer) while materializing constants, qAccum, and newOp.

🐛 Proposed fix
     auto chain = collectChain(op);
     if (chain.size() < 2) {
       return failure();
     }
 
+    OpBuilder::InsertionGuard guard(rewriter);
+    rewriter.setInsertionPoint(chain.back().getOperation());
+
     auto loc = op->getLoc();
     auto constants = createConstants(loc, rewriter);
 
     // Initialize quaternion accumulator from the first operation
     auto qAccum = quaternionFromRotation(chain.front(), constants, rewriter);

Run the following script to confirm the missing insertion-point adjustment and the lack of a regression test with late-defined scalar angles:

#!/bin/bash
set -euo pipefail

echo "=== matchAndRewrite body ==="
fd QuaternionMergeRotationGates.cpp --exec sed -n '580,610p' {}

echo
echo "=== quaternion-merge tests using arithmetic between mergeable gates ==="
fd test_qco_quaternion_merge.cpp --exec rg -n 'arith\.(addf|subf|mulf|divf)' {}

Expected result: matchAndRewrite shows no setInsertionPoint* call before building constants/qAccum/newOp, and the test search returns no coverage for a late-defined angle value.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp`
around lines 591 - 605, The merged-quaternion ops are currently created at the
rewrite's existing insertion point (head of the chain), risking SSA dominance if
a later gate angle is produced by an arith op; before calling createConstants,
quaternionFromRotation (for the first op), the loop that folds with
hamiltonProduct, and before uOpFromQuaternion, set the rewriter insertion point
to chain.back().getOperation() (or the operation immediately before the first
post-chain consumer) so all arith::/math:: ops for the merged quaternion are
emitted after the chain elements; locate these calls (createConstants,
quaternionFromRotation, hamiltonProduct, uOpFromQuaternion) and wrap them with a
temporary insertion-point guard (setInsertionPoint / setInsertionPointAfter or
equivalent) and restore the original insertion point after newOp is created.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp`:
- Around line 555-568: Remove the stale pairwise helper by deleting the
createOpQuaternionMergedAngle function definition (and any forward declaration)
and any references to it so there is only the chain-based merge path
(matchAndRewrite) left; search for the symbol createOpQuaternionMergedAngle and
remove its declaration/definition from QuaternionMergeRotationGates.cpp and any
header, ensure no callers remain, remove any now-unused includes or using
directives, and run the build/static analysis to confirm the unused-function
warning is resolved.
- Around line 367-376: collectChain currently dereferences
current->getUsers().begin() before verifying there is a user, which causes UB
for a dead tail; update collectChain to first check that current->getUsers() is
non-empty (and ideally that it has exactly one user if that's required) before
dereferencing, then fetch the user, test
areQuaternionMergeable(*current.getOperation(), *userOp) and only then push/cast
and advance current; modify the loop around current, userOp and the
areQuaternionMergeable call so dereferencing never happens when getUsers() is
empty.

In `@mlir/tools/mqt-cc/mqt-cc.cpp`:
- Around line 74-77: The cl::opt option mergeSingleQubitRotationGates is
declared const but must follow the file convention of mutable command-line
options; remove the const and declare it as static cl::opt<bool>
mergeSingleQubitRotationGates(...) so ParseCommandLineOptions can update it and
the declaration matches other options in this file.

In `@mlir/unittests/Compiler/test_compiler_pipeline.cpp`:
- Around line 195-199: The test comment is inaccurate: it claims the test
compares outputs “with and without the pass enabled” but the code actually
compares afterQCOCanon and afterOptimization from a single run; update the
comment to accurately describe the current comparison logic (that the test
compares the optimization output stages after running the pass, specifically
comparing the afterQCOCanon and afterOptimization results) and remove or reword
the “with and without the pass enabled” phrasing so it matches the behavior
around afterQCOCanon and afterOptimization in test_compiler_pipeline.cpp.

In
`@mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_quaternion_merge.cpp`:
- Around line 503-517: The test currently uses two RX gates (builder.rx) which
don't prove cross-qubit safety because same-axis RX+RX are intentionally not
merged; change the second gate to a different mergeable type (e.g.,
builder.ry(1.0, qubit2) or a UOp) so the pass must respect qubit identity, then
update the assertions: keep invoking runMergePass(module.get()) and assert that
both separate rotations remain (e.g., EXPECT_EQ(countOps<RXOp>(), 1) and
EXPECT_EQ(countOps<RYOp>(), 1)) and/or assert that no UOp was produced
(EXPECT_EQ(countOps<UOp>(), 0)); target symbols:
QCOQuaternionMergeTest::dontMergeGatesFromDifferentQubits, builder.rx,
builder.ry (or UOp), runMergePass, countOps<RXOp>(), countOps<RYOp>(),
countOps<UOp>().

---

Duplicate comments:
In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp`:
- Around line 591-605: The merged-quaternion ops are currently created at the
rewrite's existing insertion point (head of the chain), risking SSA dominance if
a later gate angle is produced by an arith op; before calling createConstants,
quaternionFromRotation (for the first op), the loop that folds with
hamiltonProduct, and before uOpFromQuaternion, set the rewriter insertion point
to chain.back().getOperation() (or the operation immediately before the first
post-chain consumer) so all arith::/math:: ops for the merged quaternion are
emitted after the chain elements; locate these calls (createConstants,
quaternionFromRotation, hamiltonProduct, uOpFromQuaternion) and wrap them with a
temporary insertion-point guard (setInsertionPoint / setInsertionPointAfter or
equivalent) and restore the original insertion point after newOp is created.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ac9eb884-6cfb-41bb-bec4-2be8b1a0698c

📥 Commits

Reviewing files that changed from the base of the PR and between 1a22b2b and b73951e.

📒 Files selected for processing (11)
  • CHANGELOG.md
  • mlir/include/mlir/Compiler/CompilerPipeline.h
  • mlir/include/mlir/Dialect/QCO/Transforms/Passes.td
  • mlir/lib/Compiler/CMakeLists.txt
  • mlir/lib/Compiler/CompilerPipeline.cpp
  • mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt
  • mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp
  • mlir/tools/mqt-cc/mqt-cc.cpp
  • mlir/unittests/Compiler/test_compiler_pipeline.cpp
  • mlir/unittests/Dialect/QCO/Transforms/Mapping/CMakeLists.txt
  • mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_quaternion_merge.cpp

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: 1

♻️ Duplicate comments (1)
mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp (1)

555-568: 🧹 Nitpick | 🔵 Trivial

Remove the stale pairwise merge helper.

createOpQuaternionMergedAngle is no longer referenced after the chain-based rewrite. Keeping a second merge path here makes future fixes easy to miss, and Cppcheck is already flagging it as unused.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp`
around lines 555 - 568, Remove the now-unused pairwise merge helper by deleting
the createOpQuaternionMergedAngle function definition (and any corresponding
forward declaration) and any code that references it; locate the function
signature using the symbols UnitaryOpInterface and PatternRewriter and the body
that calls quaternionFromRotation, hamiltonProduct, and uOpFromQuaternion, then
remove that entire helper to avoid a duplicate merge path and silence the
Cppcheck unused warning.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp`:
- Around line 597-604: qAccum may drift from unit length after repeated
hamiltonProduct calls (from quaternionFromRotation and hamiltonProduct) and
uOpFromQuaternion assumes a unit quaternion; add a normalize step before calling
uOpFromQuaternion: implement a helper normalizeQuaternion(Quaternion, Location,
const Constants&, PatternRewriter&) that computes sqrt(w*w+x*x+y*y+z*z), divides
each component by the norm (using math::SqrtOp and arithmetic ops), and replace
the direct use of qAccum with the normalized quaternion when invoking
uOpFromQuaternion.

---

Duplicate comments:
In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp`:
- Around line 555-568: Remove the now-unused pairwise merge helper by deleting
the createOpQuaternionMergedAngle function definition (and any corresponding
forward declaration) and any code that references it; locate the function
signature using the symbols UnitaryOpInterface and PatternRewriter and the body
that calls quaternionFromRotation, hamiltonProduct, and uOpFromQuaternion, then
remove that entire helper to avoid a duplicate merge path and silence the
Cppcheck unused warning.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ce48f6a1-6e52-4dad-a4aa-afd4a413b5f0

📥 Commits

Reviewing files that changed from the base of the PR and between b73951e and 51fbbfa.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • mlir/include/mlir/Dialect/QCO/Transforms/Passes.td
  • mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp
  • mlir/tools/mqt-cc/mqt-cc.cpp
  • mlir/unittests/Compiler/test_compiler_pipeline.cpp
  • mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_quaternion_merge.cpp

@J4MMlE J4MMlE force-pushed the quaternion-rotation-merging branch from 51fbbfa to b2a7d4b Compare March 21, 2026 23:58
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.

♻️ Duplicate comments (4)
mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_quaternion_merge.cpp (1)

132-141: ⚠️ Potential issue | 🟡 Minor

Compare U angles modulo periodicity to prevent false-negative tests.

expectUGateParams currently does direct numeric comparison, but these parameters are periodic; the identity test even documents two equivalent forms (phi = 0 or ) while asserting only one. This can fail on equivalent outputs.

♻️ Proposed fix
+  static double circularDiff(double a, double b, double period) {
+    const double diff = std::fmod(std::abs(a - b), period);
+    return std::min(diff, period - diff);
+  }
+
   void expectUGateParams(double expectedTheta, double expectedPhi,
                          double expectedLambda, double tolerance = 1e-8) {
     auto params = getUGateParams();
     ASSERT_TRUE(params.has_value());
 
     auto [theta, phi, lambda] = *params;
-    EXPECT_NEAR(theta, expectedTheta, tolerance);
-    EXPECT_NEAR(phi, expectedPhi, tolerance);
-    EXPECT_NEAR(lambda, expectedLambda, tolerance);
+    EXPECT_NEAR(circularDiff(theta, expectedTheta, 4.0 * PI), 0.0, tolerance);
+    EXPECT_NEAR(circularDiff(phi, expectedPhi, TAU), 0.0, tolerance);
+    EXPECT_NEAR(circularDiff(lambda, expectedLambda, TAU), 0.0, tolerance);
   }

Based on learnings: In MQT Core’s OpenQASM 2 U definition, theta is 4π periodic while phi and lambda are 2π periodic.

Also applies to: 741-754

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_quaternion_merge.cpp`
around lines 132 - 141, The test's expectUGateParams does direct numeric
comparisons causing false negatives for equivalent U gates; update
expectUGateParams (and the other similar assertions around the second block
referencing U parameters) to compare angles modulo periodicity: normalize theta
differences modulo 4π and phi/lambda modulo 2π (e.g., compute minimal
wrap-around difference between expected and actual angle and then use
EXPECT_NEAR on that minimal difference with the given tolerance); locate
getUGateParams usage and replace the three EXPECT_NEAR checks with
wrapped-comparison logic so equivalent angles like 0 vs 2π (or theta differing
by 4π) no longer fail.
mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp (3)

601-608: ⚠️ Potential issue | 🟠 Major

Renormalize qAccum before converting back to Euler angles.

uOpFromQuaternion assumes a unit quaternion, but this path now chains several Hamilton products without the old quaternion→Euler→quaternion reset. Floating-point drift in qAccum will skew beta, and near the gimbal-lock thresholds it can also flip the safe branch. Normalize once before Line 608.

➕ Suggested fix
   for (auto chainOp : llvm::drop_begin(chain)) {
     auto qi = quaternionFromRotation(chainOp, constants, rewriter);
     qAccum = hamiltonProduct(qi, qAccum, loc, rewriter);
   }
+  qAccum = normalizeQuaternion(qAccum, loc, constants, rewriter);

   // Convert merged quaternion back to UOp
   auto newOp = uOpFromQuaternion(qAccum, op, constants, rewriter);
static Quaternion normalizeQuaternion(Quaternion q, Location loc,
                                      const Constants& constants,
                                      PatternRewriter& rewriter) {
  auto ww = arith::MulFOp::create(rewriter, loc, q.w, q.w);
  auto xx = arith::MulFOp::create(rewriter, loc, q.x, q.x);
  auto yy = arith::MulFOp::create(rewriter, loc, q.y, q.y);
  auto zz = arith::MulFOp::create(rewriter, loc, q.z, q.z);

  auto sum1 = arith::AddFOp::create(rewriter, loc, ww, xx);
  auto sum2 = arith::AddFOp::create(rewriter, loc, yy, zz);
  auto normSq = arith::AddFOp::create(rewriter, loc, sum1, sum2);
  auto norm = math::SqrtOp::create(rewriter, loc, normSq);
  auto invNorm = arith::DivFOp::create(rewriter, loc, constants.one, norm);

  return {
      .w = arith::MulFOp::create(rewriter, loc, q.w, invNorm),
      .x = arith::MulFOp::create(rewriter, loc, q.x, invNorm),
      .y = arith::MulFOp::create(rewriter, loc, q.y, invNorm),
      .z = arith::MulFOp::create(rewriter, loc, q.z, invNorm),
  };
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp`
around lines 601 - 608, Normalize qAccum before converting back to Euler angles:
insert a call to a new helper (e.g., normalizeQuaternion) that takes the
Quaternion qAccum, Location loc, Constants& constants, and PatternRewriter&
rewriter, computes its magnitude via w^2+x^2+y^2+z^2, sqrt, and multiplies each
component by 1/norm to produce a unit quaternion, then pass the normalized
result into uOpFromQuaternion instead of raw qAccum; implement
normalizeQuaternion returning a Quaternion and call it just before the
uOpFromQuaternion(qAccum, op, constants, rewriter) invocation.

595-608: ⚠️ Potential issue | 🔴 Critical

Build the merged quaternion where every later parameter dominates.

Line 603 can consume angle SSA values from gates that appear after chain.front(), but the current insertion point is still at the chain head. That emits arith/math ops using non-dominating operands as soon as a later rotation angle is computed between the head and tail, which makes the rewritten IR invalid. Move the whole merge computation under an insertion guard to a point before chain.back().

🛠️ Suggested fix
   auto loc = op->getLoc();
-  auto constants = createConstants(loc, rewriter);
+  OpBuilder::InsertionGuard guard(rewriter);
+  rewriter.setInsertionPoint(chain.back().getOperation());
+  auto constants = createConstants(loc, rewriter);

   // Initialize quaternion accumulator from the first operation
   auto qAccum = quaternionFromRotation(chain.front(), constants, rewriter);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp`
around lines 595 - 608, The merge loop computes and emits arithmetic using SSA
values from later gates while the rewriter insertion point is still at the chain
head; move the entire quaternion-merge computation (creation of constants,
quaternionFromRotation calls, hamiltonProduct folding, and uOpFromQuaternion)
under an insertion-point guard so all new ops are inserted at a location that
dominates all consumed angle SSA values — specifically set the rewriter
insertion point to just before chain.back() (or use a
ScopedInsertionPoint/rewriter.setInsertionPoint before chain.back()) before
calling createConstants, quaternionFromRotation, hamiltonProduct, and
uOpFromQuaternion to ensure later parameters dominate.

547-572: 🧹 Nitpick | 🔵 Trivial

Remove the stale pairwise merge helper.

createOpQuaternionMergedAngle no longer has callers after the chain-based rewrite landed, so it is now dead code and a second merge path that can drift from matchAndRewrite. Please delete it instead of maintaining both implementations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp`
around lines 547 - 572, Delete the now-dead pairwise merge helper function
createOpQuaternionMergedAngle and any forward declarations or declarations
referring to it so only the chain-based merge remains; remove the entire static
function body (and any comments immediately attached) from
QuaternionMergeRotationGates.cpp, search for and eliminate any remaining
references to createOpQuaternionMergedAngle (including tests or headers) so
compilation still succeeds and matchAndRewrite remains the single merge
implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp`:
- Around line 601-608: Normalize qAccum before converting back to Euler angles:
insert a call to a new helper (e.g., normalizeQuaternion) that takes the
Quaternion qAccum, Location loc, Constants& constants, and PatternRewriter&
rewriter, computes its magnitude via w^2+x^2+y^2+z^2, sqrt, and multiplies each
component by 1/norm to produce a unit quaternion, then pass the normalized
result into uOpFromQuaternion instead of raw qAccum; implement
normalizeQuaternion returning a Quaternion and call it just before the
uOpFromQuaternion(qAccum, op, constants, rewriter) invocation.
- Around line 595-608: The merge loop computes and emits arithmetic using SSA
values from later gates while the rewriter insertion point is still at the chain
head; move the entire quaternion-merge computation (creation of constants,
quaternionFromRotation calls, hamiltonProduct folding, and uOpFromQuaternion)
under an insertion-point guard so all new ops are inserted at a location that
dominates all consumed angle SSA values — specifically set the rewriter
insertion point to just before chain.back() (or use a
ScopedInsertionPoint/rewriter.setInsertionPoint before chain.back()) before
calling createConstants, quaternionFromRotation, hamiltonProduct, and
uOpFromQuaternion to ensure later parameters dominate.
- Around line 547-572: Delete the now-dead pairwise merge helper function
createOpQuaternionMergedAngle and any forward declarations or declarations
referring to it so only the chain-based merge remains; remove the entire static
function body (and any comments immediately attached) from
QuaternionMergeRotationGates.cpp, search for and eliminate any remaining
references to createOpQuaternionMergedAngle (including tests or headers) so
compilation still succeeds and matchAndRewrite remains the single merge
implementation.

In
`@mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_quaternion_merge.cpp`:
- Around line 132-141: The test's expectUGateParams does direct numeric
comparisons causing false negatives for equivalent U gates; update
expectUGateParams (and the other similar assertions around the second block
referencing U parameters) to compare angles modulo periodicity: normalize theta
differences modulo 4π and phi/lambda modulo 2π (e.g., compute minimal
wrap-around difference between expected and actual angle and then use
EXPECT_NEAR on that minimal difference with the given tolerance); locate
getUGateParams usage and replace the three EXPECT_NEAR checks with
wrapped-comparison logic so equivalent angles like 0 vs 2π (or theta differing
by 4π) no longer fail.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f5ced74e-9e8e-46f7-bb09-8a1038b2d2fe

📥 Commits

Reviewing files that changed from the base of the PR and between 51fbbfa and b2a7d4b.

📒 Files selected for processing (4)
  • mlir/include/mlir/Dialect/QCO/Transforms/Passes.td
  • mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp
  • mlir/unittests/Compiler/test_compiler_pipeline.cpp
  • mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_quaternion_merge.cpp

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

♻️ Duplicate comments (1)
mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp (1)

574-581: 🧹 Nitpick | 🔵 Trivial

Consider renormalizing the accumulated quaternion before Euler extraction.

After chaining multiple Hamilton products (lines 575-578), floating-point drift can move |qAccum| slightly off 1.0. While the acos input clamping at lines 481-484 partially mitigates this for beta, explicit renormalization would provide additional robustness for longer chains:

// After the Hamilton product loop:
// qAccum = normalizeQuaternion(qAccum, loc, constants, rewriter);

This is a minor robustness enhancement rather than a correctness bug—for typical chain lengths (2-5 gates), the accumulated error is negligible.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp`
around lines 574 - 581, After accumulating quaternions with
quaternionFromRotation and hamiltonProduct into qAccum, normalize qAccum before
converting back to a UOp to avoid FP drift: insert a call to a quaternion
normalization helper (e.g., normalizeQuaternion that computes the quaternion
norm and divides components using constants/rewriter and loc) immediately after
the Hamilton product loop and before uOpFromQuaternion(qAccum, op, constants,
rewriter); if no normalizeQuaternion exists, add one that uses the existing
constants/rewriter pattern to compute 1.0 / sqrt(sumsq) and multiply qAccum
components by that scalar.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp`:
- Around line 374-380: The while loop body with the single-statement sequence
that gets the user and checks mergeability (using current, userOp,
areQuaternionMergeable, chain and UnitaryOpInterface) lacks braces; add braces
around the entire body of the while loop so the block that computes userOp,
tests areQuaternionMergeable(*current.getOperation(), *userOp), pushes to chain,
and updates current is enclosed in { ... } for consistent style and to avoid
future accidental errors.
- Around line 608-627: Move the MergeRotationGates pass struct into the existing
anonymous namespace to give it internal linkage: relocate the definition of
struct MergeRotationGates (which inherits
impl::MergeRotationGatesBase<MergeRotationGates>) inside the file's anonymous
namespace and keep its override of runOnOperation unchanged; ensure
populateMergeRotationGatesPatterns remains accessible to the pass (either by
leaving that function in the anonymous namespace or converting it to a method)
so the call inside runOnOperation continues to compile.
- Around line 15-25: The build may rely on a transitive include for
llvm::drop_begin used in QuaternionMergeRotationGates.cpp; add an explicit
include of the header that defines it by importing <llvm/ADT/STLExtras.h> at the
top of the file so references to llvm::drop_begin(chain) are resolved directly
(look for usages of llvm::drop_begin and the file
QuaternionMergeRotationGates.cpp to place the new include alongside the other
llvm includes).

---

Duplicate comments:
In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp`:
- Around line 574-581: After accumulating quaternions with
quaternionFromRotation and hamiltonProduct into qAccum, normalize qAccum before
converting back to a UOp to avoid FP drift: insert a call to a quaternion
normalization helper (e.g., normalizeQuaternion that computes the quaternion
norm and divides components using constants/rewriter and loc) immediately after
the Hamilton product loop and before uOpFromQuaternion(qAccum, op, constants,
rewriter); if no normalizeQuaternion exists, add one that uses the existing
constants/rewriter pattern to compute 1.0 / sqrt(sumsq) and multiply qAccum
components by that scalar.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 73653c15-2a0d-4626-9811-68c8d547c584

📥 Commits

Reviewing files that changed from the base of the PR and between b2a7d4b and f889a11.

📒 Files selected for processing (1)
  • mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp

@burgholzer
Copy link
Member

@burgholzer its ready for review again!

Could you please first address the linter warnings as well as the open CodeRabbit comments?

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.

♻️ Duplicate comments (2)
mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp (2)

571-580: ⚠️ Potential issue | 🟠 Major

Renormalize qAccum before converting it back to Euler angles.

uOpFromQuaternion assumes a unit quaternion, but Lines 574-577 now fold an arbitrary-length Hamilton-product chain without reprojecting onto the unit sphere. That drift skews beta and can flip the gimbal-lock branch near 0/PI. A small normalizeQuaternion(...) helper that divides by sqrt(w*w + x*x + y*y + z*z) is enough here.

♻️ Proposed fix
     for (auto chainOp : llvm::drop_begin(chain)) {
       auto qi = quaternionFromRotation(chainOp, constants, rewriter);
       qAccum = hamiltonProduct(qi, qAccum, loc, rewriter);
     }
+    qAccum = normalizeQuaternion(qAccum, loc, constants, rewriter);
 
     // Convert merged quaternion back to UOp
     auto newOp = uOpFromQuaternion(qAccum, op, constants, rewriter);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp`
around lines 571 - 580, After folding the chain into qAccum via
quaternionFromRotation and hamiltonProduct, renormalize qAccum before converting
back to Euler with uOpFromQuaternion: add or call a normalizeQuaternion(qAccum)
helper that divides each component by sqrt(w*w + x*x + y*y + z*z) (and
defensively handle zero/nan norm), then pass the normalized quaternion to
uOpFromQuaternion to avoid drift and gimbal-lock flips.

567-580: ⚠️ Potential issue | 🔴 Critical

Move the rewrite insertion point to the chain tail before building the quaternion IR.

Line 568 still creates the helper arith/math ops at the pattern’s current insertion point. If a later gate in the chain gets its angle from an SSA value defined between the head and tail, those new ops will reference that value before it dominates, and the rewrite emits invalid IR. Use an InsertionGuard and set the insertion point to chain.back().getOperation() before createConstants, quaternionFromRotation, and uOpFromQuaternion, then add a regression where the second angle is produced by an arith.addf between two mergeable gates.

♻️ Proposed fix
   auto loc = op->getLoc();
+  OpBuilder::InsertionGuard guard(rewriter);
+  rewriter.setInsertionPoint(chain.back().getOperation());
   auto constants = createConstants(loc, rewriter);
#!/bin/bash
echo "=== matchAndRewrite insertion point ==="
sed -n '556,586p' mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp
echo
echo "=== current test coverage for SSA-computed angle values ==="
rg -n "arith\\.(addf|subf|mulf|divf)|builder\\.(rx|ry|rz|p|r|u2|u)\\(" mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_quaternion_merge.cpp -C2
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp`
around lines 567 - 580, The rewrite currently creates helper arith/math ops at
the pattern's original insertion point, which can break SSA dominance; wrap the
rewrite with an mlir::OpBuilder::InsertionGuard and call
rewriter.setInsertionPointAfter(chain.back().getOperation()) (or
setInsertionPoint(chain.back().getOperation())) before calling createConstants,
quaternionFromRotation, and uOpFromQuaternion so all helper ops are emitted at
the chain tail; update the code locations around
qAccum/quaternionFromRotation/hamiltonProduct/uOpFromQuaternion to use the
guarded inserter and add a unit test where the second gate's angle is computed
via an arith.addf between two mergeable gates to verify correct dominance.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp`:
- Around line 571-580: After folding the chain into qAccum via
quaternionFromRotation and hamiltonProduct, renormalize qAccum before converting
back to Euler with uOpFromQuaternion: add or call a normalizeQuaternion(qAccum)
helper that divides each component by sqrt(w*w + x*x + y*y + z*z) (and
defensively handle zero/nan norm), then pass the normalized quaternion to
uOpFromQuaternion to avoid drift and gimbal-lock flips.
- Around line 567-580: The rewrite currently creates helper arith/math ops at
the pattern's original insertion point, which can break SSA dominance; wrap the
rewrite with an mlir::OpBuilder::InsertionGuard and call
rewriter.setInsertionPointAfter(chain.back().getOperation()) (or
setInsertionPoint(chain.back().getOperation())) before calling createConstants,
quaternionFromRotation, and uOpFromQuaternion so all helper ops are emitted at
the chain tail; update the code locations around
qAccum/quaternionFromRotation/hamiltonProduct/uOpFromQuaternion to use the
guarded inserter and add a unit test where the second gate's angle is computed
via an arith.addf between two mergeable gates to verify correct dominance.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0bacb27c-224d-4f9d-9129-a39f77fba6e8

📥 Commits

Reviewing files that changed from the base of the PR and between f889a11 and 114392b.

📒 Files selected for processing (2)
  • mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp
  • mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_quaternion_merge.cpp

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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp`:
- Around line 205-225: Update the docblocks for quaternionFromZYZ and the
UOp/ZYZ helper functions to explicitly state that the quaternion computed
represents the ZYZ decomposition only up to a global phase relative to the
OpenQASM2 U(theta,phi,lambda) matrix; i.e., call out that the quaternionization
is exact modulo a global phase and therefore not phase-sensitive, and add the
same caveat to the other helper docblocks mentioned (the other ZYZ/UOp helpers).
Ensure the wording names the functions (quaternionFromZYZ and the UOp/ZYZ
helpers) so future users know these helpers are not suitable for phase-sensitive
contexts.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e6034b88-8205-45e5-8b89-2146cc3d279d

📥 Commits

Reviewing files that changed from the base of the PR and between 114392b and 5fb5edc.

📒 Files selected for processing (1)
  • mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp

@J4MMlE J4MMlE force-pushed the quaternion-rotation-merging branch from 5fb5edc to e606175 Compare March 22, 2026 15:17
};

/**
* @brief Checks if an operation is a mergeable rotation gate.
Copy link
Author

Choose a reason for hiding this comment

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

This function already is pretty self-explanatory so I don't know if such a long docstring is justified. Might delete...


protected:
void runOnOperation() override {
// Get the current operation being operated on.
Copy link
Author

Choose a reason for hiding this comment

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

Should default MLIR boilerplate like this have comments? Delete?

@J4MMlE J4MMlE force-pushed the quaternion-rotation-merging branch from e606175 to 212605a Compare March 22, 2026 21:54
@J4MMlE
Copy link
Author

J4MMlE commented Mar 23, 2026

@coderabbitai resume

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 23, 2026

✅ Actions performed

Reviews resumed.

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.

♻️ Duplicate comments (3)
mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp (2)

193-201: ⚠️ Potential issue | 🟡 Minor

Restore the trailing llvm_unreachable() in createAxisQuaternion.

The switch is exhaustive for today's enum, but this function still has a no-return path. That can reintroduce -Wreturn-type noise on some compilers and becomes UB if RotationAxis ever grows.

♻️ Suggested fix
     case RotationAxis::Z:
       return {.w = cos, .x = constants.zero, .y = constants.zero, .z = sin};
     }
+    llvm_unreachable("Unhandled RotationAxis");
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp`
around lines 193 - 201, The switch in createAxisQuaternion returns for all
current RotationAxis values but leaves a theoretical no-return path; restore a
final llvm_unreachable() (or equivalent assert) after the switch in
createAxisQuaternion to silence -Wreturn-type and prevent UB if RotationAxis is
extended in the future, ensuring the function always has a terminal unreachable
statement for the default/unhandled case.

85-94: 🧹 Nitpick | 🔵 Trivial

Use MLIR RTTI instead of getName() for the mergeability check.

This predicate is defining chain boundaries, so coupling it to textual op names makes it more brittle than the isa/TypeSwitch style you already use everywhere else in this file.

mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_quaternion_merge.cpp (1)

125-137: ⚠️ Potential issue | 🟡 Minor

Compare U parameters modulo their natural periods in the helper.

expectUGateParams currently treats equivalent outputs like 0 vs or π vs as different, so a mathematically correct rewrite can fail these tests after harmless canonicalization changes.

♻️ Suggested fix
+  static double periodicDiff(double actual, double expected, double period) {
+    return std::abs(std::remainder(actual - expected, period));
+  }
+
   void expectUGateParams(double expectedTheta, double expectedPhi,
                          double expectedLambda, double tolerance = 1e-8) {
     auto params = getUGateParams();
     ASSERT_TRUE(params.has_value());
 
     auto [theta, phi, lambda] = *params;
-    EXPECT_NEAR(theta, expectedTheta, tolerance);
-    EXPECT_NEAR(phi, expectedPhi, tolerance);
-    EXPECT_NEAR(lambda, expectedLambda, tolerance);
+    EXPECT_NEAR(periodicDiff(theta, expectedTheta, 4.0 * PI), 0.0, tolerance);
+    EXPECT_NEAR(periodicDiff(phi, expectedPhi, 2.0 * PI), 0.0, tolerance);
+    EXPECT_NEAR(periodicDiff(lambda, expectedLambda, 2.0 * PI), 0.0,
+                tolerance);
   }

Based on learnings, in MQT Core the U gate uses the OpenQASM 2 definition, so θ is 4π-periodic while φ and λ are 2π-periodic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_quaternion_merge.cpp`
around lines 125 - 137, The helper expectUGateParams should compare angles
modulo their natural periods instead of raw values: retrieve params via
getUGateParams(), unwrap the optional as done now, then normalize theta to the
canonical range modulo 4π and normalize phi and lambda modulo 2π (e.g., map to
[-period/2, period/2] or [0,period) consistently) before calling EXPECT_NEAR;
update expectUGateParams to perform these modular reductions so equivalent
angles like 0 vs 2π or π vs -π are treated as equal.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp`:
- Around line 193-201: The switch in createAxisQuaternion returns for all
current RotationAxis values but leaves a theoretical no-return path; restore a
final llvm_unreachable() (or equivalent assert) after the switch in
createAxisQuaternion to silence -Wreturn-type and prevent UB if RotationAxis is
extended in the future, ensuring the function always has a terminal unreachable
statement for the default/unhandled case.

In
`@mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_quaternion_merge.cpp`:
- Around line 125-137: The helper expectUGateParams should compare angles modulo
their natural periods instead of raw values: retrieve params via
getUGateParams(), unwrap the optional as done now, then normalize theta to the
canonical range modulo 4π and normalize phi and lambda modulo 2π (e.g., map to
[-period/2, period/2] or [0,period) consistently) before calling EXPECT_NEAR;
update expectUGateParams to perform these modular reductions so equivalent
angles like 0 vs 2π or π vs -π are treated as equal.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 03f31963-5f48-44c3-9b5d-066d5d834544

📥 Commits

Reviewing files that changed from the base of the PR and between 114392b and 212605a.

📒 Files selected for processing (2)
  • mlir/lib/Dialect/QCO/Transforms/Optimizations/QuaternionMergeRotationGates.cpp
  • mlir/unittests/Dialect/QCO/Transforms/Optimizations/test_qco_quaternion_merge.cpp

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

4 participants