Skip to content

✨ Compose multi-gate unitary matrices in CtrlOp and InvOp bodies#1809

Merged
simon1hofmann merged 15 commits into
mainfrom
fix/ctrl-inv-body-matrix
Jun 29, 2026
Merged

✨ Compose multi-gate unitary matrices in CtrlOp and InvOp bodies#1809
simon1hofmann merged 15 commits into
mainfrom
fix/ctrl-inv-body-matrix

Conversation

@simon1hofmann

@simon1hofmann simon1hofmann commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Description

  • Add shared composeSingleQubitBodyMatrix in QCOUtils to compose compile-time single-qubit unitaries inside modifier bodies (in program order).
  • Extend CtrlOp::getUnitaryMatrix() to use composition when the body is not a single unitary gate (e.g. ctrl { x; h }).
  • Refactor InvOp::getUnitaryMatrix() to reuse the same helper and take the adjoint, removing duplicated logic.
  • Add matrix tests for controlled modifier bodies, including inv inside ctrl.

Problem

CtrlOp::getUnitaryMatrix() only handled bodies with exactly one unitary op via getSoleBodyUnitary. Multi-gate bodies like ctrl { x; h } returned nullopt.

Solution

Extract composeSingleQubitBodyMatrix(Block&) into mlir/lib/Dialect/QCO/IR/QCOUtils.cpp and use it from both modifiers:

  • CtrlOp: sole body unitary → existing fast path; otherwise (1 target) compose body gates and embed as controlled block.
  • InvOp: sole body unitary → adjoint of target matrix; otherwise compose body and return fromAdjoint(composed).

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.

If PR contains AI-assisted content:

  • I have disclosed the use of AI tools in the PR description as per our AI Usage Guidelines.
  • AI-assisted commits include an Assisted-by: [Model Name] via [Tool Name] footer.
  • I confirm that I have personally reviewed and understood all AI-generated content, and accept full responsibility for it.

@simon1hofmann simon1hofmann self-assigned this Jun 24, 2026
@simon1hofmann simon1hofmann added enhancement Improvement of existing feature c++ Anything related to C++ code MLIR Anything related to MLIR labels Jun 24, 2026
@simon1hofmann simon1hofmann added this to the MLIR Support milestone Jun 24, 2026
@codecov

codecov Bot commented Jun 24, 2026

Copy link
Copy Markdown

Codecov Report

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

Files with missing lines Patch % Lines
mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp 88.2% 2 Missing ⚠️
mlir/lib/Dialect/QCO/IR/QCOUtils.cpp 93.9% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@simon1hofmann

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

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

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

A shared utility composes single-qubit body matrices and is wired into CtrlOp and InvOp. New controlled-program helpers and unit tests validate the updated unitary extraction paths. The changelog is also updated with the PR reference.

Changes

Shared body-matrix composition utility and modifier integration

Layer / File(s) Summary
QCOUtils public API and build wiring
mlir/include/mlir/Dialect/QCO/QCOUtils.h, mlir/lib/Dialect/QCO/IR/CMakeLists.txt, mlir/lib/Dialect/QCO/IR/QCOUtils.cpp
Declares composeSingleQubitBodyMatrix(Block&), registers QCOUtils.cpp, and implements block scanning that composes unitary matrices, applies global phase, ignores barriers, and returns std::nullopt on unsupported bodies.
CtrlOp and InvOp integration
mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp, mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp
CtrlOp::getUnitaryMatrix now composes a target matrix for single-target bodies when no sole unitary is available, and InvOp::getUnitaryMatrix now uses the shared composer before taking the adjoint.
New test programs and matrix unit tests
mlir/unittests/programs/qco_programs.h, mlir/unittests/programs/qco_programs.cpp, mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp
Adds controlledXH and controlledInverseHT helpers and tests the resulting 4x4 matrices for singleControlledH, controlledXH, and controlledInverseHT.
CHANGELOG
CHANGELOG.md
Adds [#1809] to the unreleased entry and its link definition.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Possibly related PRs

Suggested labels

feature

Suggested reviewers

  • burgholzer

Poem

🐇 A matrix hop, a phase in sight,
Two-qubit bodies now compose just right.
Ctrl and Inv join hands in code,
And tests now guard the quantum road.
Hop-hop hooray, the utils sing!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately describes the main change: composing multi-gate unitary matrices in CtrlOp and InvOp bodies.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The description covers the required summary, problem, solution, and checklist sections and is largely complete, with only optional fields omitted.

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

✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix/ctrl-inv-body-matrix

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp (1)

416-418: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Don't short-circuit single-target phase-only bodies to identity.

The getNumBodyUnitaries() == 0 fast path skips composeSingleQubitBodyMatrix, so inv bodies with one target and only GPhaseOp/barriers still return identity instead of the adjoint phase matrix. Narrow this early return to truly empty or barrier-only identities, and let one-target phase-only bodies reach the shared composer.

Also applies to: 432-435

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp` around lines 416 - 418, The
early return in InvOp’s body-matrix construction is too broad: it treats any
zero-unitary body as identity and skips composeSingleQubitBodyMatrix, which
breaks single-target bodies that contain only GPhaseOp and barriers. Narrow the
fast path in InvOp so it only returns identity for truly empty or barrier-only
cases, and let one-target phase-only bodies flow through the shared
composer/adjoint logic; apply the same fix wherever this zero-unitary shortcut
appears.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp`:
- Around line 101-112: The QCO IR matrix test dereferences the result of
CtrlOp::getUnitaryMatrix() without first checking it for null, so a failed
lookup would crash instead of producing a clean test failure. In
test_qco_ir_matrix.cpp, update the test around ctrlOp and matrix to assert the
returned matrix object before calling matrix->isApprox(expected). Keep the
existing matrix comparison, but ensure the assertion on matrix happens first so
the test fails safely.

---

Outside diff comments:
In `@mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp`:
- Around line 416-418: The early return in InvOp’s body-matrix construction is
too broad: it treats any zero-unitary body as identity and skips
composeSingleQubitBodyMatrix, which breaks single-target bodies that contain
only GPhaseOp and barriers. Narrow the fast path in InvOp so it only returns
identity for truly empty or barrier-only cases, and let one-target phase-only
bodies flow through the shared composer/adjoint logic; apply the same fix
wherever this zero-unitary shortcut appears.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1c3aa8c8-b77e-4405-acbf-e6a462d5d44c

📥 Commits

Reviewing files that changed from the base of the PR and between 4065349 and 2973264.

📒 Files selected for processing (9)
  • CHANGELOG.md
  • mlir/include/mlir/Dialect/QCO/QCOUtils.h
  • mlir/lib/Dialect/QCO/IR/CMakeLists.txt
  • mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Dialect/QCO/IR/Modifiers/InvOp.cpp
  • mlir/lib/Dialect/QCO/IR/QCOUtils.cpp
  • mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp
  • mlir/unittests/programs/qco_programs.cpp
  • mlir/unittests/programs/qco_programs.h

Comment thread mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp
@simon1hofmann

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

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.

@denialhaag

Copy link
Copy Markdown
Member

We can probably mark this as closing #1760, but we should create a new issue for PowOp if we do not already implement the matrices in #1603.

@denialhaag

Copy link
Copy Markdown
Member

We can probably mark this as closing #1760, but we should create a new issue for PowOp if we do not already implement the matrices in #1603.

Ahh, I just realized that this PR is only targeting single-target modifiers. I think this is a perfectly mergeable improvement on the status quo, but it might be better to adapt the wording in #1760 rather than marking this PR as closing it. At some point, we probably want to compose the unitary matrices of arbitrary modifier operations, which will add some more complexity. Sorry for the confusion!

@mergify mergify Bot added the conflict label Jun 24, 2026

@burgholzer burgholzer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This generally LGTM with some small exceptions that are probably worth revisiting.

Comment thread mlir/include/mlir/Dialect/QCO/QCOUtils.h Outdated
Comment thread mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp Outdated
Comment thread mlir/lib/Dialect/QCO/IR/QCOUtils.cpp
Comment thread mlir/lib/Dialect/QCO/IR/QCOUtils.cpp Outdated
Comment thread mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp Outdated
Comment thread mlir/unittests/programs/qco_programs.h Outdated
simon1hofmann and others added 3 commits June 25, 2026 10:04
Co-authored-by: Lukas Burgholzer <burgholzer@me.com>
Signed-off-by: simon1hofmann <119581649+simon1hofmann@users.noreply.github.com>
Signed-off-by: simon1hofmann <119581649+simon1hofmann@users.noreply.github.com>
@mergify mergify Bot removed the conflict label Jun 25, 2026
… single-qubit operations and update tests for new functionality.
@mergify mergify Bot added the conflict label Jun 25, 2026
simon1hofmann and others added 2 commits June 25, 2026 11:06
Signed-off-by: simon1hofmann <119581649+simon1hofmann@users.noreply.github.com>
@mergify mergify Bot removed the conflict label Jun 25, 2026

@burgholzer burgholzer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One more round of changes until we can get this in 😊

Comment thread mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp Outdated
Comment thread mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp Outdated
Comment thread mlir/lib/Dialect/QCO/IR/QCOUtils.cpp Outdated
Comment thread mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp Outdated
simon1hofmann and others added 3 commits June 25, 2026 12:43
Co-authored-by: Lukas Burgholzer <burgholzer@me.com>
Signed-off-by: simon1hofmann <119581649+simon1hofmann@users.noreply.github.com>
@simon1hofmann simon1hofmann requested a review from burgholzer June 25, 2026 11:22

@burgholzer burgholzer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hm. The code here generally looks good to me now; except for the endianness thing you brought up.

I think we should strive to use a consistent endianness representation throughout the MQT. Currently, the interpretation between the MQT Core IR/the DD Package and the MLIR code base seem to differ. This sounds like it could lead to problems down the way.

In "old" MQT Core, the higher valued qubit index is always the more significant entry. DDs are always constructed with highest index on top. Thus, CX(1, 0) (1 as control, 0 as target) has the textbook CNOT matrix $diag(I, X)$, while CX(0, 1) has the "less usual matrix representation.
This is also compatible with Qiskit: https://quantum.cloud.ibm.com/docs/en/api/qiskit/qiskit.circuit.library.CXGate#cxgate

Any thoughts on how to best handle this?

@simon1hofmann

simon1hofmann commented Jun 29, 2026

Copy link
Copy Markdown
Contributor Author

Hm. The code here generally looks good to me now; except for the endianness thing you brought up.

I think we should strive to use a consistent endianness representation throughout the MQT. Currently, the interpretation between the MQT Core IR/the DD Package and the MLIR code base seem to differ. This sounds like it could lead to problems down the way.

In "old" MQT Core, the higher valued qubit index is always the more significant entry. DDs are always constructed with highest index on top. Thus, CX(1, 0) (1 as control, 0 as target) has the textbook CNOT matrix d i a g ( I , X ) , while CX(0, 1) has the "less usual matrix representation. This is also compatible with Qiskit: https://quantum.cloud.ibm.com/docs/en/api/qiskit/qiskit.circuit.library.CXGate#cxgate

Any thoughts on how to best handle this?

So there was basically an inconsistency before that already, as this was the test for CNOT:

TEST_F(QCOMatrixTest, CXOpMatrix) {
  auto moduleOp = QCOProgramBuilder::build(context.get(), singleControlledX);
  ASSERT_TRUE(moduleOp);

  // Get the operation from the module
  auto funcOp = *moduleOp->getBody()->getOps<func::FuncOp>().begin();
  auto ctrlOp = *funcOp.getBody().getOps<CtrlOp>().begin();
  auto matrix = ctrlOp.getUnitaryMatrix();

  // Get the definition of the matrix from the DD library
  const auto cx = qc::StandardOperation(1, 0, qc::OpType::X);
  const auto dd = std::make_unique<dd::Package>(2);
  const auto cxDD = dd::getDD(cx, *dd);
  const auto definition = cxDD.getMatrix(2);

So what we would have to change:

  • for e.g. CX(0, 1), instead of embedding X in the bottom right corner, we would have to map control/target operands to the right indices to get the same matrix representation as in Qiskit.
  • for asymmetric gates (e.g. DCX) we have to reorder the definition of its unitary matrix.

The code won't be as nice anymore, as it's not as straightforward as creating an identity matrix and setting the bottom right corner by the body matrix anymore.

Should we do that as part of this PR?
As far as I can tell, no parts in the code base yet rely on the concrete ordering besides Matrix.cpp and the corresponding tests. Two qubit synthesis will be affected, which should be easy to fix.

@burgholzer

Copy link
Copy Markdown
Member

Hm. The code here generally looks good to me now; except for the endianness thing you brought up.
I think we should strive to use a consistent endianness representation throughout the MQT. Currently, the interpretation between the MQT Core IR/the DD Package and the MLIR code base seem to differ. This sounds like it could lead to problems down the way.
In "old" MQT Core, the higher valued qubit index is always the more significant entry. DDs are always constructed with highest index on top. Thus, CX(1, 0) (1 as control, 0 as target) has the textbook CNOT matrix d i a g ( I , X ) , while CX(0, 1) has the "less usual matrix representation. This is also compatible with Qiskit: https://quantum.cloud.ibm.com/docs/en/api/qiskit/qiskit.circuit.library.CXGate#cxgate
Any thoughts on how to best handle this?

So there was basically an inconsistency before that already, as this was the test for CNOT:

TEST_F(QCOMatrixTest, CXOpMatrix) {
  auto moduleOp = QCOProgramBuilder::build(context.get(), singleControlledX);
  ASSERT_TRUE(moduleOp);

  // Get the operation from the module
  auto funcOp = *moduleOp->getBody()->getOps<func::FuncOp>().begin();
  auto ctrlOp = *funcOp.getBody().getOps<CtrlOp>().begin();
  auto matrix = ctrlOp.getUnitaryMatrix();

  // Get the definition of the matrix from the DD library
  const auto cx = qc::StandardOperation(1, 0, qc::OpType::X);
  const auto dd = std::make_unique<dd::Package>(2);
  const auto cxDD = dd::getDD(cx, *dd);
  const auto definition = cxDD.getMatrix(2);

So what we would have to change:

  • for e.g. CX(0, 1), instead of embedding X in the bottom right corner, we would have to map control/target operands to the right indices to get the same matrix representation as in Qiskit.
  • for asymmetric gates (e.g. DCX) we have to reorder the definition of its unitary matrix.

Should we do that as part of this PR? As far as I can tell, no parts in the code base yet rely on the concrete ordering besides Matrix.cpp and the corresponding tests. Two qubit synthesis will be affected, which should be easy to fix.

Hm. The thing is that the two-target gates should be fine already. We run unit tests that compare the generated matrix with the DD Package one. And I believe these already use the same ordering of the qubits. Worth checking though.

I would guess that the only "mistake" here is in the control modifier. But then again, I am not even sure if this is an error in the matrix computation itself. The current code simply puts the matrix in the bottom right corner of the larger identity matrix. This is correct in one case (cx(1, 0)) but wrong in the other (cx(0, 1)). If you now flip the logic for the matrix generation, then it will just flip which case works correctly and which one does not.

I think the problem is coming from the fact that the current implementation does not dictate any kind of endianness, but rather works as "control is higher, target is lower"-convention.
I suppose there is no way in actually changing that except if we would impose some kind of qubit order as an input to the operation. That does not seem to make too much sense to me though.

Okay.. lots of loud thinking. Overall this has led me to believe that there is nothing actually wrong here. As long as the unitary synthesis rewires the matrices accordingly so that they are assembled and combined "correctly" with the assumption on the qubit ordering from above, we should be good.

Does my rambling make any sense? Might also be worth to fact check with an LLM. @coderabbitai can you do that?

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate relevant files =="
fd 'CtrlOp.cpp|Matrix.cpp|test_qco_ir_matrix.cpp|QCOUtils.cpp|qco_programs.cpp' mlir

echo
echo "== Outline: CtrlOp.cpp =="
ast-grep outline mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp --view expanded || true

echo
echo "== CtrlOp.cpp =="
sed -n '1,220p' mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp

echo
echo "== Outline: Matrix.cpp candidates =="
fd 'Matrix.cpp' mlir --exec ast-grep outline {} --view expanded || true

echo
echo "== Matrix.cpp content (first 260 lines) =="
fd 'Matrix.cpp' mlir --exec sed -n '1,260p' {}

echo
echo "== test_qco_ir_matrix.cpp relevant excerpts =="
sed -n '1,260p' mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp

echo
echo "== qco_programs.cpp relevant excerpts =="
sed -n '1,220p' mlir/unittests/programs/qco_programs.cpp

echo
echo "== Search for DD-based matrix comparisons in tests =="
rg -n -C3 'getDD|StandardOperation|matrix4FromDefinition|DCX|CXOpMatrix|CtrlOp' mlir/unittests

Length of output: 103485


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== CtrlOp.cpp lower half =="
sed -n '220,420p' mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp

echo
echo "== Matrix.h relevant declarations =="
sed -n '1,260p' mlir/include/mlir/Dialect/QCO/Utils/Matrix.h

echo
echo "== Search for embed APIs/usages =="
rg -n -C3 'embedInNqubit|kron|copyBottomRightCorner|getUnitaryMatrix\(' mlir/include/mlir/Dialect/QCO/Utils/Matrix.h mlir/lib/Dialect/QCO/Utils/Matrix.cpp mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp

Length of output: 21671


@burgholzer Yes — your rambling mostly makes sense, with one important correction:

the codebase does already encode an endianness convention.
It’s not truly “no endianness, just control-higher/target-lower.”

What I found:

  • In mlir/lib/Dialect/QCO/Utils/Matrix.cpp, Line 194 explicitly says qubit 0 is the MSB of the basis-state index.
  • In mlir/include/mlir/Dialect/QCO/Utils/Matrix.h, Line 242 and Line 417 document the same MSB-first convention for embedding and Kronecker products.
  • The fixed two-qubit gate matrices are tested directly against DD definitions in mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp, e.g. DCXOpMatrix at Line 328, so your hunch that the built-in two-target gates are probably already fine looks well supported.

The suspicious part is indeed CtrlOp::getUnitaryMatrix():

  • In mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp, Line 304 onward, it constructs the controlled matrix as
    I_(2^controls) ⊗ U
    by creating an identity and putting the target block into the bottom-right corner.
  • That is only correct when the chosen wire ordering means “all controls occupy the more significant bits and all targets the less significant bits.”

So the fact-check is:

  1. You are right that the likely issue is localized to CtrlOp::getUnitaryMatrix(), not the general two-qubit gate definitions.
  2. But the reason is not that the system has no endianness.
    The matrix layer already has one: MSB-first / qubit 0 is most significant.
  3. The real mismatch is that CtrlOp::getUnitaryMatrix() does not map the actual control/target operand positions onto that convention. It just assumes the controlled block belongs in the bottom-right corner.

So I’d phrase it like this:

  • If CtrlOp::getUnitaryMatrix() is meant to return the matrix in the same global convention as the rest of Matrix.cpp and DD, then it is incomplete for operand orders that do not match “controls first, targets last.”
  • If downstream synthesis later rewires/permutates things before composing bigger matrices, then execution semantics may still be fine — but the standalone meaning of getUnitaryMatrix() is then narrower/less explicit than the rest of the API suggests.

My practical recommendation for this PR:

  • Do not expand scope unless you want to tackle qubit-permutation semantics properly.
  • This PR’s current goal (“compose multi-gate single-target bodies”) looks mergeable.
  • But I would not treat it as resolving the broader endianness / arbitrary controlled-matrix ordering problem from #1760.
  • A good follow-up would be:
    • add tests for both orderings of controlled single-target ops, e.g. the equivalents of CX(1, 0) and CX(0, 1);
    • decide whether CtrlOp::getUnitaryMatrix() should encode operand-position-aware permutation into the global MSB-first convention.

Short version: your conclusion is close, but I would tighten it to “the bug is probably in CtrlOp::getUnitaryMatrix()’s embedding logic, not in the overall matrix convention.”

(•ᴗ•)🐇

@simon1hofmann

simon1hofmann commented Jun 29, 2026

Copy link
Copy Markdown
Contributor Author

Hm. The code here generally looks good to me now; except for the endianness thing you brought up.
I think we should strive to use a consistent endianness representation throughout the MQT. Currently, the interpretation between the MQT Core IR/the DD Package and the MLIR code base seem to differ. This sounds like it could lead to problems down the way.
In "old" MQT Core, the higher valued qubit index is always the more significant entry. DDs are always constructed with highest index on top. Thus, CX(1, 0) (1 as control, 0 as target) has the textbook CNOT matrix d i a g ( I , X ) , while CX(0, 1) has the "less usual matrix representation. This is also compatible with Qiskit: https://quantum.cloud.ibm.com/docs/en/api/qiskit/qiskit.circuit.library.CXGate#cxgate
Any thoughts on how to best handle this?

So there was basically an inconsistency before that already, as this was the test for CNOT:

TEST_F(QCOMatrixTest, CXOpMatrix) {
  auto moduleOp = QCOProgramBuilder::build(context.get(), singleControlledX);
  ASSERT_TRUE(moduleOp);

  // Get the operation from the module
  auto funcOp = *moduleOp->getBody()->getOps<func::FuncOp>().begin();
  auto ctrlOp = *funcOp.getBody().getOps<CtrlOp>().begin();
  auto matrix = ctrlOp.getUnitaryMatrix();

  // Get the definition of the matrix from the DD library
  const auto cx = qc::StandardOperation(1, 0, qc::OpType::X);
  const auto dd = std::make_unique<dd::Package>(2);
  const auto cxDD = dd::getDD(cx, *dd);
  const auto definition = cxDD.getMatrix(2);

So what we would have to change:

  • for e.g. CX(0, 1), instead of embedding X in the bottom right corner, we would have to map control/target operands to the right indices to get the same matrix representation as in Qiskit.
  • for asymmetric gates (e.g. DCX) we have to reorder the definition of its unitary matrix.

Should we do that as part of this PR? As far as I can tell, no parts in the code base yet rely on the concrete ordering besides Matrix.cpp and the corresponding tests. Two qubit synthesis will be affected, which should be easy to fix.

Hm. The thing is that the two-target gates should be fine already. We run unit tests that compare the generated matrix with the DD Package one. And I believe these already use the same ordering of the qubits. Worth checking though.

I would guess that the only "mistake" here is in the control modifier. But then again, I am not even sure if this is an error in the matrix computation itself. The current code simply puts the matrix in the bottom right corner of the larger identity matrix. This is correct in one case (cx(1, 0)) but wrong in the other (cx(0, 1)). If you now flip the logic for the matrix generation, then it will just flip which case works correctly and which one does not.

I think the problem is coming from the fact that the current implementation does not dictate any kind of endianness, but rather works as "control is higher, target is lower"-convention. I suppose there is no way in actually changing that except if we would impose some kind of qubit order as an input to the operation. That does not seem to make too much sense to me though.

Okay.. lots of loud thinking. Overall this has led me to believe that there is nothing actually wrong here. As long as the unitary synthesis rewires the matrices accordingly so that they are assembled and combined "correctly" with the assumption on the qubit ordering from above, we should be good.

Does my rambling make any sense? Might also be worth to fact check with an LLM. @coderabbitai can you do that?

CtrlOp::getUnitaryMatrix() always embeds in the bottom right corner, I compared the unitary matrix of b.cx(q[0], q[1]) and b.cx(q[1], q[0]), which are exactly the same.

Should we merge this PR and then fix CtrlOp::getUnitaryMatrix() in a follow-up?

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Tip

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

@simon1hofmann Yes — based on that check, I’d treat this as mergeable with a scoped follow-up.

Your experiment is a strong signal that mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp is currently ignoring operand order in CtrlOp::getUnitaryMatrix(): if b.cx(q[0], q[1]) and b.cx(q[1], q[0]) produce the same matrix, then the standalone matrix API is not faithfully encoding control/target placement.

So my recommendation would be:

  • Merge PR #1809 for what it actually adds: composition of multi-gate single-qubit bodies in modifiers.
  • Do not present it as resolving the broader controlled-matrix ordering / endianness problem from #1760.
  • Track CtrlOp::getUnitaryMatrix() as a follow-up.

A good follow-up issue would cover:

  • making CtrlOp::getUnitaryMatrix() operand-position-aware instead of always embedding in the bottom-right block;
  • verifying behavior for both CX(0, 1) and CX(1, 0);
  • adding at least one asymmetric regression test, e.g. DCX, so swapped operands cannot accidentally pass.

So: yes, merge now, but only if you also keep the scope explicit and file the follow-up.

If you want, I can open that GitHub issue for you with the relevant context and acceptance criteria.

(•ᵕᴗᵕ•)つ🐇


✏️ Learnings added
Learnt from: simon1hofmann
URL: https://github.com/munich-quantum-toolkit/core/pull/1809

Timestamp: 2026-06-29T15:07:06.575Z
Learning: In `mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp`, `CtrlOp::getUnitaryMatrix()` currently embeds the body matrix into the bottom-right corner in a way that makes operand-swapped controlled-X constructions produce the same unitary matrix; `b.cx(q[0], q[1])` and `b.cx(q[1], q[0])` were observed to yield identical matrices in PR `#1809` discussion.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Tip

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

@simon1hofmann Yes — based on that check, I would treat this as a pre-existing correctness gap in CtrlOp::getUnitaryMatrix(), not a reason to block this PR.

My recommendation:

  • merge this PR
  • do not present it as closing the broader controlled-matrix / endianness problem in #1760
  • open a follow-up issue specifically for CtrlOp::getUnitaryMatrix()

Reasoning:

  • this PR’s scoped improvement is still valid: composing multi-gate single-target modifier bodies
  • the “always embed in the bottom-right corner” behavior is a broader semantic issue that already existed
  • your experiment showing b.cx(q[0], q[1]) and b.cx(q[1], q[0]) yield the same matrix is strong evidence that CtrlOp::getUnitaryMatrix() is currently ignoring operand order

So I’d frame it as:

  • this PR improves supported body composition
  • follow-up: make mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp generate matrices consistent with the project’s chosen qubit-order convention, and add regression tests for both control/target orderings

Concrete follow-up issue scope:

  • fix CtrlOp::getUnitaryMatrix() to account for actual control/target positions
  • add tests distinguishing CX(0, 1) and CX(1, 0)
  • verify consistency against DD-backed expectations
  • clarify whether #1760 should remain open for the more general modifier-matrix story

If you want, I can open that GitHub issue for you now.
(ᵔᴥᵔ)


✏️ Learnings added
Learnt from: simon1hofmann
URL: https://github.com/munich-quantum-toolkit/core/pull/1809

Timestamp: 2026-06-29T15:07:25.431Z
Learning: In the MQT Core MLIR QCO dialect, `mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp` currently computes the same unitary matrix for `b.cx(q[0], q[1])` and `b.cx(q[1], q[0])` because `CtrlOp::getUnitaryMatrix()` embeds the controlled body matrix into the bottom-right corner without accounting for operand order. This is a pre-existing issue broader than PR `#1809`'s body-composition change.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@burgholzer burgholzer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Following the suggestion by the rabbit, let's merge this for now, and resolve the underlying issue in a follow up.

I think it might also be worth checking what Qiskit does here. As far as I can tell, they implement CX as a singleton gate with a fixed matrix. As such, I am not sure that they have the concept of "give me the matrix for this particar instatiation of an operation applied to these qubits". So it has to do this rewiring somewhere as well.
But let's maybe take this discussion to a follow-up issue or pull request.
It does not affect the functionality of this PR.

@simon1hofmann

Copy link
Copy Markdown
Contributor Author

Following the suggestion by the rabbit, let's merge this for now, and resolve the underlying issue in a follow up.

I think it might also be worth checking what Qiskit does here. As far as I can tell, they implement CX as a singleton gate with a fixed matrix. As such, I am not sure that they have the concept of "give me the matrix for this particar instatiation of an operation applied to these qubits". So it has to do this rewiring somewhere as well. But let's maybe take this discussion to a follow-up issue or pull request. It does not affect the functionality of this PR.

Sounds good, created a follow-up issue: #1821

@simon1hofmann simon1hofmann merged commit e587a12 into main Jun 29, 2026
33 checks passed
@simon1hofmann simon1hofmann deleted the fix/ctrl-inv-body-matrix branch June 29, 2026 15:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c++ Anything related to C++ code enhancement Improvement of existing feature MLIR Anything related to MLIR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants