✨ Add Rotation Gate Merging using Quaternions#1407
✨ Add Rotation Gate Merging using Quaternions#1407J4MMlE wants to merge 6 commits intomunich-quantum-toolkit:mainfrom
Conversation
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
|
Hey @J4MMlE 👋🏻 How much of an ask would it be to directly base this pass on the QCO dialect and its infrastructure? |
106575c to
7528b05
Compare
There was a problem hiding this comment.
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!
mlir/lib/Dialect/QCO/Transforms/QuaternionMergeRotationGates.cpp
Outdated
Show resolved
Hide resolved
mlir/lib/Dialect/QCO/Transforms/QuaternionMergeRotationGates.cpp
Outdated
Show resolved
Hide resolved
mlir/lib/Dialect/QCO/Transforms/QuaternionMergeRotationGates.cpp
Outdated
Show resolved
Hide resolved
|
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. Imagine the following pseudo-code: Here, the first 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 My personal gut feeling is that it would be a nice helper method to implement for the |
aaa7096 to
94ea576
Compare
f0989ad to
045857a
Compare
DRovara
left a comment
There was a problem hiding this comment.
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.
mlir/lib/Dialect/QCO/Transforms/QuaternionMergeRotationGates.cpp
Outdated
Show resolved
Hide resolved
mlir/unittests/Dialect/QCO/Transforms/test_qco_quaternion_merge.cpp
Outdated
Show resolved
Hide resolved
042de31 to
290b626
Compare
📝 WalkthroughSummary by CodeRabbitRelease Notes
WalkthroughAdds a quaternion-based MergeRotationGates MLIR transform, wires it into build and compiler pipeline behind a new Changes
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
🧹 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.
290b626 to
6c5018e
Compare
There was a problem hiding this comment.
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.
mlir/lib/Dialect/QCO/Transforms/QuaternionMergeRotationGates.cpp
Outdated
Show resolved
Hide resolved
4cb71da to
c875d00
Compare
There was a problem hiding this comment.
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.
| // TODO: could add some normalization here for alpha and gamma otherwise | ||
| // they can be outside of [-PI, PI]. |
There was a problem hiding this comment.
🧹 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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
🧩 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):
- (φ \sim φ + 2\pi k) (period (2\pi)) because only (e^{iφ}) appears. (quantum.cloud.ibm.com)
- (λ \sim λ + 2\pi k) (period (2\pi)) because only (e^{iλ}) appears. (quantum.cloud.ibm.com)
- (θ) is (4\pi)-periodic as a matrix because (\cos(θ/2),\sin(θ/2)) are (4\pi)-periodic. (quantum.cloud.ibm.com)
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:
- 1: https://quantum.cloud.ibm.com/docs/api/qiskit/qiskit.circuit.library.U3Gate?utm_source=openai
- 2: https://quantum.cloud.ibm.com/docs/api/qiskit/qiskit.circuit.library.U3Gate?utm_source=openai
- 3: https://quantum.cloud.ibm.com/docs/api/qiskit/qiskit.circuit.library.U3Gate?utm_source=openai
- 4: https://quantum.cloud.ibm.com/docs/api/qiskit/qiskit.circuit.library.U3Gate?utm_source=openai
- 5: https://quantum.cloud.ibm.com/docs/api/qiskit/qiskit.circuit.library.U3Gate?utm_source=openai
- 6: https://quantum.cloud.ibm.com/docs/api/qiskit/qiskit.circuit.library.U3Gate?utm_source=openai
@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.
8ff7ccc to
f3cbe5b
Compare
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
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.
439aeda to
f6c28ba
Compare
There was a problem hiding this comment.
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.
| // ################################################## | ||
| // # 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)); | ||
| } |
There was a problem hiding this comment.
🧹 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.
|
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:
Besides that, what I should put into the CHANGELOG.md - should I just add my name + #PR into Also, feel free to comment on the things that @coderabbitai pointed out, and which i have not addressed yet. |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 6935 🏁 Script executed: Length of output: 429 🏁 Script executed: Length of output: 379
Implementation Points1. 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 3. Angle normalization: This is a good observation. For cleaner output and numerical stability, I'd recommend normalizing the U-gate parameters to CHANGELOG EntryLooking at the current CHANGELOG.md structure, you have two options: Option 1 (Recommended): Add a new separate entry under the - ✨ 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 ItemsFrom 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 🐰✨ 🧠 Learnings used |
|
@coderabbitai resume |
✅ Actions performedReviews resumed. |
|
Thanks for the work, @J4MMlE!
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.
Yeah, typically the name of the PR is descriptive enough, then just follow the same structure as other entires.
Most of the comments that are left don't seem like too big of a deal. Other than that, I will yield for now and let @burgholzer have a look. |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 717 🏁 Script executed: Length of output: 24599
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:
Both of these are straightforward fixes that align with MLIR best practices. Once 🧠 Learnings used |
|
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. |
f6c28ba to
a80f315
Compare
bdba4ce to
2ac2923
Compare
2ac2923 to
aa8caf4
Compare
burgholzer
left a comment
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
| 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) |
There was a problem hiding this comment.
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?
| // Stage 5: Optimization passes | ||
| // TODO: Add optimization passes | ||
| addCleanupPasses(pm); |
There was a problem hiding this comment.
| // 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.
| if (config_.mergeRotationGates) { | ||
| pm.addPass(mlir::qco::createMergeRotationGates()); | ||
| if (failed(pm.run(module))) { | ||
| return failure(); | ||
| } | ||
| } |
There was a problem hiding this comment.
| 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.
| get_property(dialect_libs GLOBAL PROPERTY MLIR_DIALECT_LIBS) | ||
| set(LIBRARIES ${dialect_libs} MQT::CoreIR) |
There was a problem hiding this comment.
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.
| // QCO operations cannot contain control qubits, so no need to check for | ||
| // them | ||
| if (!op->hasOneUse()) { | ||
| return mlir::failure(); | ||
| } |
There was a problem hiding this comment.
Shouldn't this be true by construction as we use linear types in QCO?
I believe this can be removed.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I think it is fair to assume now that we enforce linear types.
| const auto& users = op->getUsers(); | ||
| auto* userOP = *users.begin(); |
There was a problem hiding this comment.
| const auto& users = op->getUsers(); | |
| auto* userOP = *users.begin(); | |
| auto* userOP = *op->getUsers().begin(); |
users is not needed elsewhere, might as well inline.
| rewriter.setInsertionPoint(user); | ||
| const UnitaryOpInterface newUser = | ||
| createOpQuaternionMergedAngle(op, user, rewriter); | ||
|
|
||
| // Replace user with newUser | ||
| rewriter.replaceOp(user, newUser); | ||
|
|
||
| // Erase op | ||
| rewriter.eraseOp(op); |
There was a problem hiding this comment.
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.
| // Apply patterns in an iterative and greedy manner. | ||
| if (mlir::failed(mlir::applyPatternsGreedily(op, std::move(patterns)))) { | ||
| signalPassFailure(); | ||
| } |
There was a problem hiding this comment.
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.
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 + rxorry + 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:
ugate. (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:
Checklist:
- [ ] I have added migration instructions to the upgrade guide (if needed).