Skip to content

Conversation

@taminob
Copy link
Collaborator

@taminob taminob commented Sep 16, 2025

Description

TODO

Related to #1122

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.

@taminob taminob self-assigned this Sep 16, 2025
@taminob taminob force-pushed the taminob/mlir-two-qubit-gate-decomposition-pass branch from d7f49cc to 4bc6f15 Compare October 22, 2025 17:22
@taminob taminob force-pushed the taminob/mlir-two-qubit-gate-decomposition-pass branch from c4f6399 to ffb96f3 Compare November 18, 2025 13:36
@taminob taminob force-pushed the taminob/mlir-two-qubit-gate-decomposition-pass branch from 2e3a48f to f5ae2b0 Compare November 20, 2025 12:07
@taminob taminob force-pushed the taminob/mlir-two-qubit-gate-decomposition-pass branch 3 times, most recently from bbcec1e to 49d512f Compare November 21, 2025 16:15
@taminob
Copy link
Collaborator Author

taminob commented Nov 21, 2025

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 21, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 21, 2025

📝 Walkthrough

Walkthrough

A new gate decomposition pass is added to the MLIR MQTOpt dialect. The implementation includes pass declaration, Weyl-based two-qubit decomposition pattern logic, utility helpers for MLIR/quantum operations, CMake integration with Eigen dependency, and comprehensive MLIR tests.

Changes

Cohort / File(s) Change Summary
Build system & dependencies
CMakeLists.txt, mlir/lib/Dialect/MQTOpt/Transforms/CMakeLists.txt
Added FetchContent for remote Eigen dependency (tag 3.4.1) and linked Eigen3::Eigen library to MLIRMQTOptTransforms target.
Pass API declarations
mlir/include/mlir/Dialect/MQTOpt/Transforms/Passes.h, mlir/include/mlir/Dialect/MQTOpt/Transforms/Passes.td
Declared new populateGateDecompositionPatterns function and GateDecomposition pass definition with summary describing various gate decompositions.
Pass implementation
mlir/lib/Dialect/MQTOpt/Transforms/GateDecomposition.cpp
Implemented GateDecomposition class with runOnOperation override that initializes patterns, configures greedy rewrite strategy, and applies decompositions to operations.
Decomposition pattern & logic
mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp
Implemented GateDecompositionPattern with Weyl-based two-qubit decomposition framework including matrix operations, Euler basis support, series handling, and gate synthesis for UnitaryInterface operations.
Utility helpers
mlir/lib/Dialect/MQTOpt/Transforms/Helpers.h
Introduced helper utilities for MLIR value conversion, parameter extraction, qubit classification, and linear-algebra operations (Kronecker product, eigendecomposition, unitarity checks).
Test coverage
mlir/test/Dialect/MQTOpt/Transforms/gate-decomposition.mlir
Added comprehensive MLIR test module covering identity cancellation, CNOT reduction, decomposition patterns, and complex gate series with FileCheck assertions.

Sequence Diagram

sequenceDiagram
    participant Pass as GateDecomposition Pass
    participant Pattern as GateDecompositionPattern
    participant Decomposer as TwoQubitBasisDecomposer
    participant Rewriter as PatternRewriter
    
    Pass->>Pass: runOnOperation()
    Pass->>Pass: Create RewritePatternSet
    Pass->>Pattern: populateGateDecompositionPatterns()
    Pattern->>Pass: Register pattern
    
    Pass->>Pass: Configure GreedyRewriteConfig
    loop For each Operation (top-down)
        Pass->>Pattern: matchAndRewrite(UnitaryInterface op)
        alt Pattern Matches
            Pattern->>Decomposer: create() + twoQubitDecompose()
            Decomposer->>Decomposer: Weyl decomposition
            Decomposer->>Decomposer: Generate gate sequence
            Decomposer-->>Pattern: Return TwoQubitSeries
            Pattern->>Pattern: applySeries() - replace + validate
            Pattern->>Rewriter: Emit new operations
            Rewriter-->>Pass: Update IR
        else No Match
            Pattern-->>Pass: Skip operation
        end
    end
    
    alt Success
        Pass->>Pass: Pass succeeded
    else Failure
        Pass->>Pass: Signal pass failure
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas requiring extra attention:

  • mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp — High-density mathematical logic including Weyl decomposition, complex matrix operations (diagonalization, Kronecker products, canonical forms), and correctness of gate synthesis algorithms.
  • mlir/lib/Dialect/MQTOpt/Transforms/Helpers.h — Extensive template utilities and inline implementations (eigendecomposition, matrix conversions, type inference) that require careful validation of correctness and edge case handling.
  • Integration & consistency — Ensure proper data flow between GateDecomposition pass, GateDecompositionPattern, and Helpers utilities; verify that MLIR type handling and pattern application align.
  • mlir/test/Dialect/MQTOpt/Transforms/gate-decomposition.mlir — Verify test coverage is sufficient for diverse decomposition scenarios (identity cancellation, odd CNOT counts, blocked series, three+ basis gates) and that CHECK patterns are robust.

Suggested labels

feature, c++, MLIR

Suggested reviewers

  • burgholzer

Poem

🐰 A rabbit hops through quantum gates so fine,
Decomposing two-qubit unitary design,
With Weyl and Euler bases in array,
Patterns match and rewrite the day,
Eigen's matrices help gates align! 🎯✨

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The description is incomplete and mostly contains placeholder text ('TODO'), with only a related issue reference. Multiple required checklist items are unchecked and no actual description content is provided. Replace 'TODO' with a comprehensive description of the change, motivation, and dependencies. Complete the checklist items by addressing documentation, changelog, and style guidelines, or explicitly check/uncheck applicable items with justification.
Docstring Coverage ⚠️ Warning Docstring coverage is 37.88% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: implementing a two-qubit gate decomposition pass, which is the core feature of this pull request.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch taminob/mlir-two-qubit-gate-decomposition-pass

Tip

📝 Customizable high-level summaries are now available in beta!

You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b35d7be and 4ea6f02.

📒 Files selected for processing (8)
  • CMakeLists.txt (1 hunks)
  • mlir/include/mlir/Dialect/MQTOpt/Transforms/Passes.h (1 hunks)
  • mlir/include/mlir/Dialect/MQTOpt/Transforms/Passes.td (1 hunks)
  • mlir/lib/Dialect/MQTOpt/Transforms/CMakeLists.txt (1 hunks)
  • mlir/lib/Dialect/MQTOpt/Transforms/GateDecomposition.cpp (1 hunks)
  • mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp (1 hunks)
  • mlir/lib/Dialect/MQTOpt/Transforms/Helpers.h (1 hunks)
  • mlir/test/Dialect/MQTOpt/Transforms/gate-decomposition.mlir (1 hunks)
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
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: DRovara
Repo: munich-quantum-toolkit/core PR: 1108
File: mlir/lib/Dialect/MQTOpt/Transforms/DeadGateEliminationPattern.cpp:42-45
Timestamp: 2025-10-09T13:28:29.237Z
Learning: In the MQTOpt MLIR dialect, linear types enforce single-use semantics where each qubit value can only be consumed once, preventing duplicate deallocations and making RAUW operations safe when replacing output qubits with corresponding input qubits in transformation patterns.
📚 Learning: 2025-10-09T13:20:11.483Z
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.

Applied to files:

  • mlir/include/mlir/Dialect/MQTOpt/Transforms/Passes.td
  • mlir/test/Dialect/MQTOpt/Transforms/gate-decomposition.mlir
  • mlir/lib/Dialect/MQTOpt/Transforms/GateDecomposition.cpp
  • mlir/include/mlir/Dialect/MQTOpt/Transforms/Passes.h
  • mlir/lib/Dialect/MQTOpt/Transforms/Helpers.h
  • mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp
📚 Learning: 2025-10-09T13:28:29.237Z
Learnt from: DRovara
Repo: munich-quantum-toolkit/core PR: 1108
File: mlir/lib/Dialect/MQTOpt/Transforms/DeadGateEliminationPattern.cpp:42-45
Timestamp: 2025-10-09T13:28:29.237Z
Learning: In the MQTOpt MLIR dialect, linear types enforce single-use semantics where each qubit value can only be consumed once, preventing duplicate deallocations and making RAUW operations safe when replacing output qubits with corresponding input qubits in transformation patterns.

Applied to files:

  • mlir/test/Dialect/MQTOpt/Transforms/gate-decomposition.mlir
  • mlir/lib/Dialect/MQTOpt/Transforms/Helpers.h
📚 Learning: 2025-10-09T13:13:51.224Z
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.

Applied to files:

  • mlir/test/Dialect/MQTOpt/Transforms/gate-decomposition.mlir
  • mlir/lib/Dialect/MQTOpt/Transforms/Helpers.h
  • mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp
📚 Learning: 2025-11-03T23:09:26.881Z
Learnt from: burgholzer
Repo: munich-quantum-toolkit/core PR: 1287
File: test/qdmi/dd/CMakeLists.txt:9-21
Timestamp: 2025-11-03T23:09:26.881Z
Learning: The CMake functions `generate_device_defs_executable` and `generate_prefixed_qdmi_headers` used in QDMI device test CMakeLists.txt files are provided by the external QDMI library (fetched via FetchContent from https://github.com/Munich-Quantum-Software-Stack/qdmi.git), specifically in the cmake/PrefixHandling.cmake module of the QDMI repository.

Applied to files:

  • CMakeLists.txt
🪛 GitHub Check: 🇨‌ Lint / 🚨 Lint
mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp

[warning] 1689-1689: mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp:1689:30 [misc-include-cleaner]
no header providing "std::numeric_limits" is directly included


[warning] 929-929: mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp:929:14 [misc-include-cleaner]
no header providing "Eigen::Vector" is directly included

⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: 🐍 Test (macos-14) / 🐍 macos-14
  • GitHub Check: 🐍 Test (ubuntu-24.04-arm) / 🐍 ubuntu-24.04-arm
  • GitHub Check: 🐍 Test (ubuntu-24.04) / 🐍 ubuntu-24.04
  • GitHub Check: 🐍 Test (windows-2022) / 🐍 windows-2022
  • GitHub Check: 🐉 Test / 🍎 macos-15-intel with LLVM@21
🔇 Additional comments (3)
mlir/lib/Dialect/MQTOpt/Transforms/CMakeLists.txt (1)

10-10: Confirm that Eigen3::Eigen is the correct target from your Eigen FetchContent setup

Linking against Eigen3::Eigen here is fine as long as the FetchContent configuration really defines that target on all supported toolchains; otherwise this will fail to configure or link in some environments. Please double‑check the Eigen CMake export to ensure the target name and namespace match what you use here (or add an alias/guard if necessary).

mlir/include/mlir/Dialect/MQTOpt/Transforms/Passes.h (1)

38-38: New pattern population hook looks consistent with existing transforms

The populateGateDecompositionPatterns(RewritePatternSet&) declaration fits cleanly alongside the other populate* helpers and matches the usage in GateDecomposition.cpp. Just make sure the implementation in GateDecompositionPattern.cpp is actually compiled into MLIRMQTOptTransforms so the call site links correctly.

mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp (1)

460-540: Creation of GPhaseOp ignores potential control qubits.

When applying a series, the pattern adds a global phase gate via:

if (sequence.hasGlobalPhase()) {
  createOneParameterGate<GPhaseOp>(rewriter, location, sequence.globalPhase, {});
}

This always creates a GPhase with empty inQubits, even if the original series might have had controlled global phases. Given the dialect definition allows GPhaseOp to be controlled by qubits, this may drop control wiring when decomposing controlled global-phase structures. (mqt.readthedocs.io)

Please double‑check whether controlled GPhaseOps can appear in practice. If they can, you likely want to thread the relevant control qubits into the newly created GPhaseOp (and its types) instead of using an empty ValueRange.

⛔ Skipped due to learnings
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: 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: 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.

Comment on lines 348 to 328
bool appendTwoQubitGate(UnitaryInterface nextGate) {
auto opInQubits = nextGate.getAllInQubits();
auto&& firstOperand = opInQubits[0];
auto&& secondOperand = opInQubits[1];
auto* firstQubitIt = llvm::find(outQubits, firstOperand);
auto* secondQubitIt = llvm::find(outQubits, secondOperand);
if (firstQubitIt == outQubits.end() || secondQubitIt == outQubits.end()) {
// another qubit is involved, series is finished (except there only
// has been one qubit so far)
auto* it = llvm::find(outQubits, mlir::Value{});
if (it == outQubits.end()) {
return false;
}
// iterator in the operation input of "old" qubit that already has
// previous single-qubit gates in this series
auto it2 = llvm::find(opInQubits, firstQubitIt != outQubits.end()
? *firstQubitIt
: *secondQubitIt);
// new qubit ID based on position in outQubits
const QubitId newInQubitId = std::distance(outQubits.begin(), it);
// position in operation input; since there are only two qubits, it must
// be the "not old" one
const QubitId newOpInQubitId =
1 - std::distance(opInQubits.begin(), it2);

// update inQubit and update dangling iterator, then proceed as usual
inQubits[newInQubitId] = opInQubits[newOpInQubitId];
firstQubitIt = (firstQubitIt != outQubits.end()) ? firstQubitIt : it;
secondQubitIt = (secondQubitIt != outQubits.end()) ? secondQubitIt : it;

// before proceeding as usual, see if backtracking on the "new" qubit is
// possible to collect other single-qubit operations
backtrackSingleQubitSeries(newInQubitId);
}
const QubitId firstQubitId =
std::distance(outQubits.begin(), firstQubitIt);
const QubitId secondQubitId =
std::distance(outQubits.begin(), secondQubitIt);
*firstQubitIt = nextGate->getResult(0);
*secondQubitIt = nextGate->getResult(1);

gates.push_back(
{.op = nextGate, .qubitIds = {firstQubitId, secondQubitId}});
complexity += getComplexity(helpers::getQcType(nextGate), 2);
return true;
}
Copy link
Contributor

@coderabbitai coderabbitai bot Nov 21, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Iterator misuse: drop the * when capturing result of llvm::find.

In appendTwoQubitGate, llvm::find returns an iterator over outQubits (for std::array, this is already a pointer-like iterator). Declaring the variables with auto* makes them “pointer to iterator”, which breaks comparisons against outQubits.end() and use in std::distance, and should fail to compile.

Fix by storing the iterator directly:

-      auto* firstQubitIt = llvm::find(outQubits, firstOperand);
-      auto* secondQubitIt = llvm::find(outQubits, secondOperand);
+      auto firstQubitIt = llvm::find(outQubits, firstOperand);
+      auto secondQubitIt = llvm::find(outQubits, secondOperand);

The rest of the logic (firstQubitIt == outQubits.end(), std::distance(outQubits.begin(), firstQubitIt), etc.) then operates on iterators as intended.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
bool appendTwoQubitGate(UnitaryInterface nextGate) {
auto opInQubits = nextGate.getAllInQubits();
auto&& firstOperand = opInQubits[0];
auto&& secondOperand = opInQubits[1];
auto* firstQubitIt = llvm::find(outQubits, firstOperand);
auto* secondQubitIt = llvm::find(outQubits, secondOperand);
if (firstQubitIt == outQubits.end() || secondQubitIt == outQubits.end()) {
// another qubit is involved, series is finished (except there only
// has been one qubit so far)
auto* it = llvm::find(outQubits, mlir::Value{});
if (it == outQubits.end()) {
return false;
}
// iterator in the operation input of "old" qubit that already has
// previous single-qubit gates in this series
auto it2 = llvm::find(opInQubits, firstQubitIt != outQubits.end()
? *firstQubitIt
: *secondQubitIt);
// new qubit ID based on position in outQubits
const QubitId newInQubitId = std::distance(outQubits.begin(), it);
// position in operation input; since there are only two qubits, it must
// be the "not old" one
const QubitId newOpInQubitId =
1 - std::distance(opInQubits.begin(), it2);
// update inQubit and update dangling iterator, then proceed as usual
inQubits[newInQubitId] = opInQubits[newOpInQubitId];
firstQubitIt = (firstQubitIt != outQubits.end()) ? firstQubitIt : it;
secondQubitIt = (secondQubitIt != outQubits.end()) ? secondQubitIt : it;
// before proceeding as usual, see if backtracking on the "new" qubit is
// possible to collect other single-qubit operations
backtrackSingleQubitSeries(newInQubitId);
}
const QubitId firstQubitId =
std::distance(outQubits.begin(), firstQubitIt);
const QubitId secondQubitId =
std::distance(outQubits.begin(), secondQubitIt);
*firstQubitIt = nextGate->getResult(0);
*secondQubitIt = nextGate->getResult(1);
gates.push_back(
{.op = nextGate, .qubitIds = {firstQubitId, secondQubitId}});
complexity += getComplexity(helpers::getQcType(nextGate), 2);
return true;
}
bool appendTwoQubitGate(UnitaryInterface nextGate) {
auto opInQubits = nextGate.getAllInQubits();
auto&& firstOperand = opInQubits[0];
auto&& secondOperand = opInQubits[1];
auto firstQubitIt = llvm::find(outQubits, firstOperand);
auto secondQubitIt = llvm::find(outQubits, secondOperand);
if (firstQubitIt == outQubits.end() || secondQubitIt == outQubits.end()) {
// another qubit is involved, series is finished (except there only
// has been one qubit so far)
auto* it = llvm::find(outQubits, mlir::Value{});
if (it == outQubits.end()) {
return false;
}
// iterator in the operation input of "old" qubit that already has
// previous single-qubit gates in this series
auto it2 = llvm::find(opInQubits, firstQubitIt != outQubits.end()
? *firstQubitIt
: *secondQubitIt);
// new qubit ID based on position in outQubits
const QubitId newInQubitId = std::distance(outQubits.begin(), it);
// position in operation input; since there are only two qubits, it must
// be the "not old" one
const QubitId newOpInQubitId =
1 - std::distance(opInQubits.begin(), it2);
// update inQubit and update dangling iterator, then proceed as usual
inQubits[newInQubitId] = opInQubits[newOpInQubitId];
firstQubitIt = (firstQubitIt != outQubits.end()) ? firstQubitIt : it;
secondQubitIt = (secondQubitIt != outQubits.end()) ? secondQubitIt : it;
// before proceeding as usual, see if backtracking on the "new" qubit is
// possible to collect other single-qubit operations
backtrackSingleQubitSeries(newInQubitId);
}
const QubitId firstQubitId =
std::distance(outQubits.begin(), firstQubitIt);
const QubitId secondQubitId =
std::distance(outQubits.begin(), secondQubitIt);
*firstQubitIt = nextGate->getResult(0);
*secondQubitIt = nextGate->getResult(1);
gates.push_back(
{.op = nextGate, .qubitIds = {firstQubitId, secondQubitId}});
complexity += getComplexity(helpers::getQcType(nextGate), 2);
return true;
}
🤖 Prompt for AI Agents
In mlir/lib/Dialect/MQTOpt/Transforms/GateDecompositionPattern.cpp around lines
348-393, the iterators from llvm::find are stored as pointer-to-iterator (auto*)
which is incorrect; change firstQubitIt, secondQubitIt and the local it (and any
other variables receiving llvm::find results in this scope) to plain iterator
types (use auto without *) so comparisons to outQubits.end(), std::distance, and
dereferences operate on iterators as intended; ensure any subsequent assignments
that expect an iterator (e.g., firstQubitIt = it) still use matching iterator
types.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

While I agree with this issue (in case the container is exchanged), since std::array's iterator is defined as value_type*, dropping the pointer here will result in linter warnings.

Copy link
Contributor

Choose a reason for hiding this comment

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

@taminob, that's a fair point about the linter warnings. You have a few options to balance correctness with tooling ergonomics:

  1. Use explicit iterator type to satisfy both correctness and linters:

    typename std::array<mlir::Value, 2>::iterator firstQubitIt = llvm::find(outQubits, firstOperand);

    or more concisely:

    std::array<mlir::Value, 2>::iterator firstQubitIt = llvm::find(outQubits, firstOperand);
  2. Configure the linter to accept auto for pointer types when they're actually iterators (e.g., suppress the specific warning for this pattern).

  3. Keep auto* for now if the code compiles and works correctly in your environment, accepting the theoretical portability risk if std::array's iterator implementation changes.

Since the code is compiling and working, and you're aware of the trade-off, I'll leave the decision to you. The explicit iterator type would be the most robust solution if you want to avoid both the theoretical issue and the linter warnings.


🧠 Learnings used
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: 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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The first suggestion using decltype instead of the explicit type is good, but will trigger yet another linter warning (clang-tidy modernize-use-auto). @flowerthrower do you have an opinion on this topic?

Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@burgholzer I removed the * from the type now because the windows CI won't build otherwise, do you have a preferred way of resolving this issue? Putting NOLINT(readability-qualified-auto) everywhere would work, but is also messy - I don't see a good solution here.

Copy link
Member

Choose a reason for hiding this comment

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

Hm. I remember stumbling over this as well. I can't quite remover where, but it could have been as part of the dialect rewrite. I think that I added a NOLINT for that single occurrence.
Stupid thought, but have you tried auto&? do any of the systems complain about that?
if there are multiple instances of the rule violation in one place, you can always sandwich them between // NOLINTBEGIN(...) and // NOLINTEND(...) to hide the warning in a moderately clean way.

Comment on lines 280 to 317
module {
// CHECK-LABEL: func.func @testTwoBasisGateDecomposition
func.func @testTwoBasisGateDecomposition() {
// CHECK-DAG: %[[C0:.*]] = arith.constant -1.5707963267
// CHECK-DAG: %[[C1:.*]] = arith.constant -3.141592653
// CHECK-DAG: %[[C2:.*]] = arith.constant 1.2502369157
// CHECK-DAG: %[[C3:.*]] = arith.constant 1.8299117708
// CHECK-DAG: %[[C4:.*]] = arith.constant 2.8733113535
// CHECK-DAG: %[[C5:.*]] = arith.constant 3.0097427712
// CHECK-DAG: %[[C6:.*]] = arith.constant 0.2614370492
// CHECK-DAG: %[[C7:.*]] = arith.constant -0.131849882
// CHECK-DAG: %[[C8:.*]] = arith.constant 2.500000e+00
// CHECK-DAG: %[[C9:.*]] = arith.constant -1.570796326
// CHECK-DAG: %[[C10:.*]] = arith.constant 1.570796326
// CHECK-DAG: %[[C11:.*]] = arith.constant 0.785398163

// CHECK: %[[Q0_0:.*]] = mqtopt.allocQubit
// CHECK: %[[Q1_0:.*]] = mqtopt.allocQubit

// CHECK-NOT: mqtopt.gphase(%[[ANY:.*]])
// CHECK: %[[Q0_1:.*]] = mqtopt.ry(%[[C11]]) %[[Q0_0]]
// CHECK: %[[Q0_2:.*]] = mqtopt.rz(%[[C10]]) %[[Q0_1]]
// CHECK: %[[Q1_1:.*]] = mqtopt.rx(%[[C9]]) %[[Q1_0]]
// CHECK: %[[Q1_2:.*]] = mqtopt.ry(%[[C8]]) %[[Q1_1]]
// CHECK: %[[Q0_3:.*]], %[[Q1_3:.*]] = mqtopt.x() %[[Q0_2]] ctrl %[[Q1_2]]
// CHECK: %[[Q0_4:.*]] = mqtopt.rz(%[[C7]]) %[[Q0_3]]
// CHECK: %[[Q0_5:.*]] = mqtopt.ry(%[[C6]]) %[[Q0_4]]
// CHECK: %[[Q0_6:.*]] = mqtopt.rz(%[[C5]]) %[[Q0_5]]
// CHECK: %[[Q1_4:.*]] = mqtopt.rz(%[[C10]]) %[[Q1_3]]
// CHECK: %[[Q1_5:.*]] = mqtopt.ry(%[[C10]]) %[[Q1_4]]
// CHECK: %[[Q0_7:.*]], %[[Q1_6:.*]] = mqtopt.x() %[[Q0_6]] ctrl %[[Q1_5]]
// CHECK: %[[Q0_8:.*]] = mqtopt.rz(%[[C4]]) %[[Q0_7]]
// CHECK: %[[Q0_9:.*]] = mqtopt.ry(%[[C3]]) %[[Q0_8]]
// CHECK: %[[Q0_10:.*]] = mqtopt.rz(%[[C2]]) %[[Q0_9]]
// CHECK: %[[Q1_7:.*]] = mqtopt.rz(%[[C1]]) %[[Q1_6]]
// CHECK: %[[Q1_8:.*]] = mqtopt.rx(%[[C0]]) %[[Q1_7]]

// ensure no other operations are inserted
// CHECK-NOT: mqtopt.[[ANY:.*]](%[[ANY:.*]])

// CHECK: mqtopt.deallocQubit %[[Q0_10]]
// CHECK: mqtopt.deallocQubit %[[Q1_8]]

%cst0 = arith.constant 2.5 : f64
%cst1 = arith.constant 1.2 : f64
%cst2 = arith.constant 0.5 : f64

%q0_0 = mqtopt.allocQubit
%q1_0 = mqtopt.allocQubit

%q0_x, %q1_x = mqtopt.i() %q0_0 ctrl %q1_0: !mqtopt.Qubit ctrl !mqtopt.Qubit
%q0_1 = mqtopt.h() %q0_x: !mqtopt.Qubit
%q1_1, %q0_2 = mqtopt.x() %q1_x ctrl %q0_1: !mqtopt.Qubit ctrl !mqtopt.Qubit
%q0_3, %q1_2 = mqtopt.rzz(%cst0) %q0_2, %q1_1: !mqtopt.Qubit, !mqtopt.Qubit
%q1_3 = mqtopt.ry(%cst1) %q1_2: !mqtopt.Qubit
%q0_4 = mqtopt.rx(%cst1) %q0_3: !mqtopt.Qubit
%q0_5, %q1_4 = mqtopt.x() %q0_4 ctrl %q1_3: !mqtopt.Qubit ctrl !mqtopt.Qubit
%q0_6 = mqtopt.rz(%cst2) %q0_5: !mqtopt.Qubit
// make series longer to enforce decomposition
%q0_7, %q1_5 = mqtopt.i() %q0_6 ctrl %q1_4: !mqtopt.Qubit ctrl !mqtopt.Qubit
%q0_8, %q1_6 = mqtopt.i() %q0_7 ctrl %q1_5: !mqtopt.Qubit ctrl !mqtopt.Qubit

mqtopt.deallocQubit %q0_8
mqtopt.deallocQubit %q1_6

return
}
}
Copy link
Contributor

@coderabbitai coderabbitai bot Nov 21, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Be cautious with exact floating-point constants in decomposition checks

The complex two-/three-basis decomposition tests assert specific arith.constant f64 literals (e.g., 1.00543528660, 0.43624604946, etc.) for the synthesized single-qubit rotations. This gives very strong coverage of the numeric behavior, but it also makes the tests potentially brittle:

  • Small differences in libm implementations, compiler optimization levels, or Eigen’s internal algorithms could produce slightly different doubles for the same mathematical angles.
  • MLIR’s printing of f64 via APFloat is deterministic for a given bit pattern, but if the underlying computation changes even in the low bits, the decimal strings will change and FileCheck will fail.

If you expect to run this across multiple platforms/toolchains, consider relaxing the checks to focus on structure and gate types, for example:

  • Only checking that certain arith.constant ops exist (without pinning exact values), or
  • Encoding angles in a way that is less sensitive to small numerical variation (e.g., fixed symbolic multiples of π where applicable).

Right now, a small numeric change in the decomposition algorithm will require updating a lot of golden numbers in this file.

Also applies to: 352-432


🏁 Script executed:

#!/bin/bash
# First, let's examine the test file structure
fd gate-decomposition.mlir | head -5

Length of output: 59


🏁 Script executed:

#!/bin/bash
# Look at the full test file to understand context
fd gate-decomposition.mlir -x cat

Length of output: 21283


🏁 Script executed:

#!/bin/bash
# Search for the decomposition algorithm implementation
rg -i "decompos" --type=cpp --type=h -n -B2 -A5 | head -100

Length of output: 7525


🏁 Script executed:

#!/bin/bash
# Check if there are other tests with exact float constants in MLIR
rg -nP 'arith\.constant.*\d+\.\d{8,}' --type=mlir | wc -l

Length of output: 30


🌐 Web query:

MLIR FileCheck floating-point constant testing best practices

💡 Result:

Brief best-practices for testing floating-point constants in MLIR FileCheck tests

  1. Match the printer’s canonical form, not a hand-written decimal.

    • MLIR prints floats in a canonical (usually scientific) form in many places — prefer matching that representation (see MLIR testing examples). [1][2]
  2. Use regexes instead of literal numeric text.

    • Avoid brittle exact-text checks like 0.1 or 0.100000 — use FileCheck regex groups such as {{[-+]?[0-9]+(.[0-9]+)?([eE][+-]?[0-9]+)?}} to accept integer, decimal and scientific forms. [3][4]
  3. Capture and reuse constants with FileCheck variables when you care about identity.

    • If you need to assert the same constant value or SSA name appears again, capture it (e.g. %[[C:.*]] or [[VAL:{{[-+]?[0-9]+(.[0-9]+)?([eE][+-]?[0-9]+)?}}]]) and reuse the variable later. This tests sameness without repeating fragile literals. [3]
  4. Limit checks to what the test cares about (keep tests minimal).

    • Only assert the constant token/attribute you need (use CHECK / CHECK-SAME / CHECK-NEXT sparingly). Don’t check unrelated printer details. [1]
  5. When exact numeric formatting matters, document and assert the exact MLIR output.

    • If the pass is explicitly required to format a float a certain way (e.g., a literal pass that normalizes to 1.000000e-01), assert that exact form but keep such tests narrowly scoped. Use comments to explain why exactness is required. [2]
  6. For platforms/optimizations that may change rounding/printing, prefer tolerance in tests or move numeric correctness to execution-based tests.

    • If small canonicalization/rounding differences can occur, either (a) make the FileCheck pattern accept a range/format using regex, or (b) write a lit/driver test that runs and numerically validates the value at runtime instead of relying on text output.
  7. Useful FileCheck features to apply:

    • {{...}} regex blocks, [[VAR:...]] capture/substitute, CHECK-SAME / CHECK-DAG to control ordering, and --enable-var-scope or -D for external variables when needed. [3][4]

References

  • MLIR Testing Guide (FileCheck best practices). [1]
  • MLIR Quickstart/Examples showing float attribute formatting (e.g., 1.000000e-01). [2]
  • FileCheck documentation: regexes, variables and directives. [3][4]

Use FileCheck regexes and variables for floating-point constant matching instead of exact literals

The tests assert exact arith.constant f64 literals with 8–12 decimal places (e.g., 1.00543528660, 0.43624604946). This deviates from MLIR best practices and creates maintainability burden:

Per MLIR testing guidance, use regex patterns to match floating-point forms rather than exact decimal strings. For example, replace:

  • CHECK-DAG: %[[C0:.*]] = arith.constant 1.00543528660
    with a regex that accepts integer, decimal, or scientific notation, then capture and reuse the value:
  • CHECK-DAG: %[[C0:.*]] = arith.constant {{[-+]?[0-9]+(\.[0-9]+)?}}

Alternatively, capture constants and reuse them to test identity without repeating fragile literals. This ensures the test remains stable across compiler optimizations, platform differences, or changes to internal algorithms (e.g., Eigen), while still validating that the pass produces structurally correct decompositions.

Also implement testRepeatedDecomposition (lines 438–443)

The function contains only return despite its comment: "This test checks if the repeated application of the decomposition works by 'interrupting' the first series and then having a second one." Add the test logic described or remove the function if the scenario is not currently testable.

Applies to lines 280–347 (testTwoBasisGateDecomposition), 352–432 (testThreeBasisGateDecomposition), and other similar tests with exact constants.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

These tests need to be made platform-independent in general, because of floating-point precision issues the overall gate structure can change (different rotation gates with different values) - see the currently failing tests in e.g. the windows CI. @flowerthrower do you have an idea on how to test for equivalence of the overall pattern? My only idea would be to somehow integrate something that calculates the unitary matrix of an entire function and compare them in the end (but I'm unsure how to integrate that into the testing framework).

Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Collaborator

Choose a reason for hiding this comment

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

I would like to consult @burgholzer on this matter in a more general sense.
Given the fairly complex logic involved in the decomposition, I think it would be valuable to have a proper test of its execution. One more reason is that the decomposition does not involve MLIR at all.
In a standalone Google Test, standard gates with known unitary matrices could be decomposed independently of the MLIR infrastructure. The results could then be asserted against their expected parameters (e.g., K1, K2, ..., a, b, c, etc.), which can be inferred from Qiskit’s implementation or its tests. One could (and probably should) leverage the existing test infrastructure from the non-MLIR code for this. This may require slight adjustments to how the functions are exposed.
Conversely, the MLIR test could be relaxed somewhat—if it is known to be sensitive to floating-point errors—by using looser FileCheck patterns in the CHECK expressions.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The logic is already checked by the assert statements in the code itself - if the decomposition is wrong, the program should crash. I agree that an external test for the final result of the Weyl decomposition could be nice, but I don't see how we can check the internal steps (and if it is worth the effort in this case). Should I add a couple of matrice decompositions to mlir/unittests?

Copy link
Member

Choose a reason for hiding this comment

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

I have a couple of thoughts on this.
I agree with @flowerthrower that we can write tests for this part of the functionality that are independent from MLIR entirely. This might even justify moving the respective code outside of the MLIR subtree, but I probably wouldn't go as far.
Writing dedicated googletest-based unit tests seems to be a reasonable suggestion though that should allow us to write less brittle tests.
A great solution for equivalence checking would be our QCEC tool; but given how it builds directly on MQT Core itself, integrating it here (especially in C++) would be a mess.
Given how we are talking about 1- and 2- qubit gates only, it should be entirely feasible to write a very small script that multiplied the matrices and either does the equivalent of np.all_close() or computes tr(U^dagger V)/2^n if U and V are the two matrices to be compared and n is the number of qubits. Then, one would assert that the result is close enough to 1.

It would be good to come up with a couple of unit tests that assert the implementation produces sequences of gates we would expect and that this behaviour is maintained across updates.
I'll leave it at that for now. I'll probably have further comments when I actually look through the PR itself.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Given how we are talking about 1- and 2- qubit gates only, it should be entirely feasible to write a very small script that multiplied the matrices and either does the equivalent of np.all_close() or computes tr(U^dagger V)/2^n if U and V are the two matrices to be compared and n is the number of qubits.

Just to make sure I understand you correctly: you want a Python script using pymlir to go through the unprocessed test file and then the output of this pass and compare them? I think that would be overkill and lots of double effort because it would require re-implementing all of the matrix stuff of C++ in python and I don't really want to do that because the new interface will (hopefully) make all that redundant anyway. Instead, I think we should maybe update the ToQuantumComputing pattern to accept allocQubit and use that for easy equivalence checking - we'll need to re-write that pattern anyway soon with the updated unitary interface.

Overall, I agree that it would be valuable to verify that the steps "from MLIR to matrix" and "gate sequence to MLIR" are correct, but I'm not really sure if it is worth the effort at the moment if all the unitary matrix logic of the dialect rewrite is still pending.

Copy link
Member

Choose a reason for hiding this comment

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

No, nothing at all in Python (where did that come from? 🙃).

I did not mean script in the literal sense. I rather meant a function in C++ that does the respective check.
We have a very rough version of that in our C++ tests:

dd::MatrixDD e = dd::Package::makeIdent();
dd->incRef(e);
auto leftIt = qpe.begin();
auto rightIt = iqpe.begin();
while (leftIt != qpe.end() && rightIt != iqpe.end()) {
auto multLeft = dd->multiply(getDD(**leftIt, *dd), e);
auto multRight = dd->multiply(multLeft, getInverseDD(**rightIt, *dd));
dd->incRef(multRight);
dd->decRef(e);
e = multRight;
dd->garbageCollect();
++leftIt;
++rightIt;
}
while (leftIt != qpe.end()) {
auto multLeft = dd->multiply(getDD(**leftIt, *dd), e);
dd->incRef(multLeft);
dd->decRef(e);
e = multLeft;
dd->garbageCollect();
++leftIt;
}
while (rightIt != iqpe.end()) {
auto multRight = dd->multiply(e, getInverseDD(**rightIt, *dd));
dd->incRef(multRight);
dd->decRef(e);
e = multRight;
dd->garbageCollect();
++rightIt;
}
const auto finishedEC = std::chrono::steady_clock::now();
const auto preprocessing =
std::chrono::duration<double>(finishedTransformation - start).count();
const auto verification =
std::chrono::duration<double>(finishedEC - finishedTransformation)
.count();
std::stringstream ss{};
ss << "qpe_exact,transformation," << qpe.getNqubits() << "," << qpeNgates
<< ",2," << iqpeNgates << "," << preprocessing << "," << verification;
std::cout << ss.str() << "\n";
EXPECT_TRUE(e.isIdentity());

As Patrick already mentioned, this kind of functionality can be tested entirely decoupled from MLIR as it is purely linear algebra based.
So you could simply generate a new googletest target in the mlir/unittests directory and place the respective tests there.
This should also allow you to write way better tests as you could with the FileCheck-based tests.

Copy link
Collaborator Author

@taminob taminob Nov 26, 2025

Choose a reason for hiding this comment

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

No, nothing at all in Python (where did that come from? 🙃).

Oh sorry, I thought you meant a script in the literal sense because you also referred to numpy (although I now see you wrote "equivalent of np").

I'm already working on the unittests and re-structuring the code to extract the decompositions out of the pattern into header files for easier testing without including the source file. I thought you meant an additional test script to check the MLIR code, because otherwise something could go wrong in the translation from/to MLIR.

Comment on lines +438 to +373
module {
// CHECK-LABEL: func.func @testRepeatedDecomposition
func.func @testRepeatedDecomposition() {
return
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

testRepeatedDecomposition is currently a no-op and doesn’t test the described scenario

The comment says this test should check “repeated application of the decomposition” with an interrupted first series followed by a second one, but the function body is just:

func.func @testRepeatedDecomposition() {
  return
}

So this test currently only verifies that running the pass on an empty function is a no-op.

Either:

  • Flesh out the IR to match the described scenario (similar to the other tests), or
  • Update the comment to reflect that this is intentionally a trivial regression/compilation test.

As written, it’s misleading and provides no additional coverage.


I'll verify this review comment by examining the actual file and surrounding context.


---

🏁 Script executed:

```shell
#!/bin/bash
# Check the file and examine lines around 438-443
if [ -f "mlir/test/Dialect/MQTOpt/Transforms/gate-decomposition.mlir" ]; then
  echo "=== Lines 430-450 context ==="
  sed -n '430,450p' "mlir/test/Dialect/MQTOpt/Transforms/gate-decomposition.mlir"
  echo ""
  echo "=== Grep for testRepeatedDecomposition and surrounding context ==="
  grep -n -B5 -A10 "testRepeatedDecomposition" "mlir/test/Dialect/MQTOpt/Transforms/gate-decomposition.mlir"
else
  echo "File not found"
fi

Length of output: 1211


🏁 Script executed:

#!/bin/bash
# Look for other test functions to understand the pattern
grep -n "func.func @test" "mlir/test/Dialect/MQTOpt/Transforms/gate-decomposition.mlir" | head -20

Length of output: 1055


🏁 Script executed:

#!/bin/bash
# Check file line count and overall structure
wc -l "mlir/test/Dialect/MQTOpt/Transforms/gate-decomposition.mlir"

Length of output: 63


🏁 Script executed:

#!/bin/bash
# Search for any comments related to "repeated" or this specific test
grep -n -i "repeated\|decomposition" "mlir/test/Dialect/MQTOpt/Transforms/gate-decomposition.mlir" | tail -20

Length of output: 670


🏁 Script executed:

#!/bin/bash
# Get a broader view - examine the last 50 lines of the file
tail -50 "mlir/test/Dialect/MQTOpt/Transforms/gate-decomposition.mlir"

Length of output: 2160


🏁 Script executed:

#!/bin/bash
# Look at one of the complete test functions as a comparison
grep -n -B2 -A15 "func.func @test" "mlir/test/Dialect/MQTOpt/Transforms/gate-decomposition.mlir" | head -60

Length of output: 2052


Add test IR to testRepeatedDecomposition or clarify that it's intentionally empty

Line 436 describes this test as checking "repeated application of the decomposition" with an "interrupted first series and then having a second one," but the function body (lines 440-442) contains only return. This mismatch means the test provides no coverage for the stated scenario. Either populate the function with appropriate IR to match the comment's description (following the pattern of other tests in this file), or update the comment to clarify this is a trivial regression test.

🤖 Prompt for AI Agents
In mlir/test/Dialect/MQTOpt/Transforms/gate-decomposition.mlir around lines 438
to 443, the test function @testRepeatedDecomposition is empty but the test
description says it should exercise "repeated application of the decomposition"
with an interrupted first series and a second one; either populate the func.func
body with MLIR that creates the interrupted-first-series-then-second-series
decomposition scenario (follow the style and opcode patterns used by neighboring
tests in this file so the decomposition pass has gates to rewrite), or update
the surrounding comment/description to explicitly state this is intentionally an
empty/trivial regression test; ensure whichever you choose keeps the CHECK lines
consistent with the new content.

Copy link
Collaborator

@flowerthrower flowerthrower left a comment

Choose a reason for hiding this comment

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

Thank you @taminob for all your work. The math and logic appear to be correctly implemented. I loosely checked the expected K1, K2, ... decomposition values with a small debug script on my Mac and was able to reproduce (roughtly) the same values as the Qiskit decomposition.

The three remaining points are:

  1. Set up a proper test to automate exactly this verification process.
  2. Improve comments — just because the Rust implementation is poorly documented does not mean we should do the same.
    Especially since parts of your code are already nicely commented, it makes sense to extend this good style to the copied code.
  3. Revisit MLIR logic after dialect revamp

* gates.
* @param specialization Force the use this specialization.
*/
static TwoQubitWeylDecomposition create(const matrix4x4& unitaryMatrix,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please, please, please: comments to guide humans through whats going on here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sorry, I'll add some more - we really should be better than Qiskit here which was a pain to understand. Basically all the comments you see now were already added by me. :)

Did you perhaps understand the "re-order" step of the weyl coordinates? I already commented on what it does, but I have no idea why exactly this is necessary to be honest (although it also doesn't work without it).

Copy link
Collaborator

Choose a reason for hiding this comment

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

I also didn't fully understand that, but I guess it should be fine if it works.

Comment on lines 280 to 317
module {
// CHECK-LABEL: func.func @testTwoBasisGateDecomposition
func.func @testTwoBasisGateDecomposition() {
// CHECK-DAG: %[[C0:.*]] = arith.constant -1.5707963267
// CHECK-DAG: %[[C1:.*]] = arith.constant -3.141592653
// CHECK-DAG: %[[C2:.*]] = arith.constant 1.2502369157
// CHECK-DAG: %[[C3:.*]] = arith.constant 1.8299117708
// CHECK-DAG: %[[C4:.*]] = arith.constant 2.8733113535
// CHECK-DAG: %[[C5:.*]] = arith.constant 3.0097427712
// CHECK-DAG: %[[C6:.*]] = arith.constant 0.2614370492
// CHECK-DAG: %[[C7:.*]] = arith.constant -0.131849882
// CHECK-DAG: %[[C8:.*]] = arith.constant 2.500000e+00
// CHECK-DAG: %[[C9:.*]] = arith.constant -1.570796326
// CHECK-DAG: %[[C10:.*]] = arith.constant 1.570796326
// CHECK-DAG: %[[C11:.*]] = arith.constant 0.785398163

// CHECK: %[[Q0_0:.*]] = mqtopt.allocQubit
// CHECK: %[[Q1_0:.*]] = mqtopt.allocQubit

// CHECK-NOT: mqtopt.gphase(%[[ANY:.*]])
// CHECK: %[[Q0_1:.*]] = mqtopt.ry(%[[C11]]) %[[Q0_0]]
// CHECK: %[[Q0_2:.*]] = mqtopt.rz(%[[C10]]) %[[Q0_1]]
// CHECK: %[[Q1_1:.*]] = mqtopt.rx(%[[C9]]) %[[Q1_0]]
// CHECK: %[[Q1_2:.*]] = mqtopt.ry(%[[C8]]) %[[Q1_1]]
// CHECK: %[[Q0_3:.*]], %[[Q1_3:.*]] = mqtopt.x() %[[Q0_2]] ctrl %[[Q1_2]]
// CHECK: %[[Q0_4:.*]] = mqtopt.rz(%[[C7]]) %[[Q0_3]]
// CHECK: %[[Q0_5:.*]] = mqtopt.ry(%[[C6]]) %[[Q0_4]]
// CHECK: %[[Q0_6:.*]] = mqtopt.rz(%[[C5]]) %[[Q0_5]]
// CHECK: %[[Q1_4:.*]] = mqtopt.rz(%[[C10]]) %[[Q1_3]]
// CHECK: %[[Q1_5:.*]] = mqtopt.ry(%[[C10]]) %[[Q1_4]]
// CHECK: %[[Q0_7:.*]], %[[Q1_6:.*]] = mqtopt.x() %[[Q0_6]] ctrl %[[Q1_5]]
// CHECK: %[[Q0_8:.*]] = mqtopt.rz(%[[C4]]) %[[Q0_7]]
// CHECK: %[[Q0_9:.*]] = mqtopt.ry(%[[C3]]) %[[Q0_8]]
// CHECK: %[[Q0_10:.*]] = mqtopt.rz(%[[C2]]) %[[Q0_9]]
// CHECK: %[[Q1_7:.*]] = mqtopt.rz(%[[C1]]) %[[Q1_6]]
// CHECK: %[[Q1_8:.*]] = mqtopt.rx(%[[C0]]) %[[Q1_7]]

// ensure no other operations are inserted
// CHECK-NOT: mqtopt.[[ANY:.*]](%[[ANY:.*]])

// CHECK: mqtopt.deallocQubit %[[Q0_10]]
// CHECK: mqtopt.deallocQubit %[[Q1_8]]

%cst0 = arith.constant 2.5 : f64
%cst1 = arith.constant 1.2 : f64
%cst2 = arith.constant 0.5 : f64

%q0_0 = mqtopt.allocQubit
%q1_0 = mqtopt.allocQubit

%q0_x, %q1_x = mqtopt.i() %q0_0 ctrl %q1_0: !mqtopt.Qubit ctrl !mqtopt.Qubit
%q0_1 = mqtopt.h() %q0_x: !mqtopt.Qubit
%q1_1, %q0_2 = mqtopt.x() %q1_x ctrl %q0_1: !mqtopt.Qubit ctrl !mqtopt.Qubit
%q0_3, %q1_2 = mqtopt.rzz(%cst0) %q0_2, %q1_1: !mqtopt.Qubit, !mqtopt.Qubit
%q1_3 = mqtopt.ry(%cst1) %q1_2: !mqtopt.Qubit
%q0_4 = mqtopt.rx(%cst1) %q0_3: !mqtopt.Qubit
%q0_5, %q1_4 = mqtopt.x() %q0_4 ctrl %q1_3: !mqtopt.Qubit ctrl !mqtopt.Qubit
%q0_6 = mqtopt.rz(%cst2) %q0_5: !mqtopt.Qubit
// make series longer to enforce decomposition
%q0_7, %q1_5 = mqtopt.i() %q0_6 ctrl %q1_4: !mqtopt.Qubit ctrl !mqtopt.Qubit
%q0_8, %q1_6 = mqtopt.i() %q0_7 ctrl %q1_5: !mqtopt.Qubit ctrl !mqtopt.Qubit

mqtopt.deallocQubit %q0_8
mqtopt.deallocQubit %q1_6

return
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

I would like to consult @burgholzer on this matter in a more general sense.
Given the fairly complex logic involved in the decomposition, I think it would be valuable to have a proper test of its execution. One more reason is that the decomposition does not involve MLIR at all.
In a standalone Google Test, standard gates with known unitary matrices could be decomposed independently of the MLIR infrastructure. The results could then be asserted against their expected parameters (e.g., K1, K2, ..., a, b, c, etc.), which can be inferred from Qiskit’s implementation or its tests. One could (and probably should) leverage the existing test infrastructure from the non-MLIR code for this. This may require slight adjustments to how the functions are exposed.
Conversely, the MLIR test could be relaxed somewhat—if it is known to be sensitive to floating-point errors—by using looser FileCheck patterns in the CHECK expressions.

@taminob taminob force-pushed the taminob/mlir-two-qubit-gate-decomposition-pass branch 2 times, most recently from 7fc67db to 18cf6b0 Compare November 26, 2025 20:27
@taminob
Copy link
Collaborator Author

taminob commented Nov 27, 2025

I reworked the tests now (added unittests and made the MLIR tests more robust/less precise).

A couple of questions:

  • Is this approach to testing good now or do you think the MLIR tests are too imprecise?
  • Should I adjust this PR to also include the single-qubit decomposition in the pattern or should we do this in a follow-up PR? The full code already exists and it would be the most performant way to add the single-qubit decomposition in the same pass because the qubit series is collected anyway (although the pattern is quite big aleady).
  • The Windows ARM build has an internal alignment issue in Eigen (https://libeigen.gitlab.io/eigen/docs-nightly/group__TopicUnalignedArrayAssert.html; the link in the build log is broken). Will you resolve this issue already in the dialect rewrite that'll introduce Eigen?
  • I currently use two unsupported (so experimental) headers of Eigen. Is this fine or do we want to implement that ourselves? It's for kroneckerProduct() and Eigen::Matrix::exp()

@burgholzer
Copy link
Member

I reworked the tests now (added unittests and made the MLIR tests more robust/less precise).

A couple of questions:

  • Is this approach to testing good now or do you think the MLIR tests are too imprecise?

I just skimmed through both the lit tests and the unit tests and I think the overall direction is fine. I am pretty sure that I will have some more detailed comments when I check the PR in more detail.

  • Should I adjust this PR to also include the single-qubit decomposition in the pattern or should we do this in a follow-up PR? The full code already exists and it would be the most performant way to add the single-qubit decomposition in the same pass because the qubit series is collected anyway (although the pattern is quite big aleady).

Hm. multiple thoughts on this.
I foresee two different use cases for the synthesis routines here:

  • collection and resynthesis of arbitrary runs of two-qubit blocks with no real constraints on the gate set (maybe with a parameter that let's one choose the entangling gate; like CX or CZ or RZZ or ...)
  • synthesis of two-qubit operations (or blocks thereof) to a dedicated/native gateset of a divide (where only those operations are permitted in the decomposition).

These two might actually be more or less the same, the first might just be the second with some reasonable and relaxed defaults on the native gate-set.
I think it could make sense to include the single-qubit synthesis here as well because, as you said, the series are collected anyway, so one might as well apply this to those sequences in the same go.
The only concern that I would probably have is performance. I would assume that single-qubit synthesis is much more efficient than the two-qubit one. If all one does is two-qubit synthesis, then this might become costly at some point.

I am not quite sure that I follow this. That link says:

There are 4 known causes for this issue. If you can target [c++17] only with a recent compiler (e.g., GCC>=7, clang>=5, MSVC>=19.12), then you're lucky: enabling c++17 should be enough (if not, please report to us). Otherwise, please read on to understand those issues and learn how to fix them.

We are using more modern compilers than this (by far). The current CI runs report

-- The C compiler identification is MSVC 19.44.35217.0
-- The CXX compiler identification is MSVC 19.44.35217.0

This seems to indicate to me that you might need to dig a little deeper there.
I am not yet 100% sure if the first version of the dialect rewrite will immediately include the Eigen dependency. At the moment, the following signature is used in the unitary interface

InterfaceMethod<
            "Attempts to extract the static unitary matrix. "
            "Returns std::nullopt if the operation is symbolic or dynamic.",
            "DenseElementsAttr", "tryGetStaticMatrix", (ins)
        >,

An example of how that Attr is constructed would be

inline DenseElementsAttr getMatrixX(MLIRContext* ctx) {
  const auto& complexType = ComplexType::get(Float64Type::get(ctx));
  const auto& type = RankedTensorType::get({2, 2}, complexType);
  const auto& matrix = {0.0 + 0i, 1.0 + 0i,  // row 0
                        1.0 + 0i, 0.0 + 0i}; // row 1
  return DenseElementsAttr::get(type, matrix);
}

This might not be final yet; mostly because I still want to explore some options that might allow us to make the gates/gate matrices singletons and avoid repeated constructions of matrices for gates where this is definitely not necessary.

  • I currently use two unsupported (so experimental) headers of Eigen. Is this fine or do we want to implement that ourselves? It's for kroneckerProduct() and Eigen::Matrix::exp()

I am not too familiar with the details of Eigen, but just from the name alone, I have an aversion to using such headers. Is there any kind of intuition for how well these are tested/work/can be used?
Kronecker Product shouldn't be particularly hard to implement, I suppose.
The Matrix exponential certainly is a little more complicated.
This is definitely not a decision, but just a little bit of caution.

@taminob
Copy link
Collaborator Author

taminob commented Nov 28, 2025

I am not too familiar with the details of Eigen, but just from the name alone, I have an aversion to using such headers. Is there any kind of intuition for how well these are tested/work/can be used?
Kronecker Product shouldn't be particularly hard to implement, I suppose.
The Matrix exponential certainly is a little more complicated.
This is definitely not a decision, but just a little bit of caution.

The documentation of unsupported states:

This is the API documentation for Eigen's unsupported modules.

These modules are contributions from various users. They are provided "as is", without any support.

I think it might be mainly about the stability of the interfaces - so it could happen that different Eigen versions will break API/ABI. However, since they are user-provided, I guess it also heavily depends on the actual function to be used (see also this stackoverflow question.
Looking at the history of MatrixExponential.h, it has been a while since the last actual change, so I don't think that it is likely to have any functionality issues. Same for the KroneckerProduct.

I am not quite sure that I follow this. That link says:

There are 4 known causes for this issue. If you can target [c++17] only with a recent compiler (e.g., GCC>=7, clang>=5, MSVC>=19.12), then you're lucky: enabling c++17 should be enough (if not, please report to us). Otherwise, please read on to understand those issues and learn how to fix them.

We are using more modern compilers than this (by far). The current CI runs report

I'll look into it, although it's a bit hard on a different architecture. Could well be that it is caused by differences in alignment on ARM.

@taminob taminob force-pushed the taminob/mlir-two-qubit-gate-decomposition-pass branch 4 times, most recently from a9d1fc9 to 7efae47 Compare November 28, 2025 12:52
@taminob
Copy link
Collaborator Author

taminob commented Nov 28, 2025

I'll look into it, although it's a bit hard on a different architecture. Could well be that it is caused by differences in alignment on ARM.

Unfortunately, I couldn't really find the source of the error - also because I didn't manage to get a stacktrace in the CI and so can't really pinpoint the issue. I tried using C++23 stacktrace which did compile, but I didn't manage to override Eigen's eigen_assert at the place this is triggered. I didn't manage to reproduce this locally.

I think the cause might be one of https://libeigen.gitlab.io/eigen/docs-nightly/group__TopicPassingByValue.html or https://libeigen.gitlab.io/eigen/docs-nightly/group__TopicStlContainers.html (which should be fixed since C++11, but maybe LLVM+MSVC behaves buggy here - who knows) which are quite serious and need to be considered when using Eigen (at least I wasn't aware of it).

I'd suggest to simply set EIGEN_MAX_STATIC_ALIGN_BYTES to zero for ARM+Windows as advised in the documentation. The drawback is potentially non-optimal vectorization. I did this by defining EIGEN_DONT_ALIGN_STATICALLY.

@taminob taminob force-pushed the taminob/mlir-two-qubit-gate-decomposition-pass branch from aee1e26 to 888deb2 Compare November 30, 2025 10:56
@taminob taminob force-pushed the taminob/mlir-two-qubit-gate-decomposition-pass branch from 888deb2 to 7a80d30 Compare December 1, 2025 09:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants