Skip to content

Fix CS/CSdg inverse cancellation with reversed qargs#16124

Open
TSS99 wants to merge 4 commits intoQiskit:mainfrom
TSS99:codex/fix-inverse-cancellation-issue-15855
Open

Fix CS/CSdg inverse cancellation with reversed qargs#16124
TSS99 wants to merge 4 commits intoQiskit:mainfrom
TSS99:codex/fix-inverse-cancellation-issue-15855

Conversation

@TSS99
Copy link
Copy Markdown
Contributor

@TSS99 TSS99 commented May 3, 2026

Fixes #15855.

Bug

The default standard-gate fast path handled CS/CSdg cancellation inconsistently when the gates acted on the same two qubits in opposite order. Since CS and CSdg are symmetric in their qubit order, both of these are valid inverse cancellations, but main only cancelled the first one:

qc = QuantumCircuit(2)
qc.csdg(0, 1)
qc.cs(1, 0)
# cancelled on main

qc = QuantumCircuit(2)
qc.cs(0, 1)
qc.csdg(1, 0)
# not cancelled on main

Fix

  • Keep exact qarg matching for the default inverse pairs in general.
  • For the existing default CS/CSdg pair only, also accept the same two qargs in the opposite order.
  • Avoid repeated standard-gate lookups while checking adjacent pairs.

Tests

  • test_cs_csdg_cancel_with_reversed_qargs_in_both_orders verifies that both CS/CSdg operation orders cancel when the qargs are reversed.

AI/LLM disclosure

  • I used OpenAI Codex (GPT-5) to draft this update; reviewed before pushing.

@TSS99 TSS99 requested a review from a team as a code owner May 3, 2026 10:11
@TSS99 TSS99 requested a review from ShellyGarion May 3, 2026 10:11
@qiskit-bot qiskit-bot added the Community PR PRs from contributors that are not 'members' of the Qiskit repo label May 3, 2026
@qiskit-bot
Copy link
Copy Markdown
Collaborator

Thank you for opening a new pull request.

Before your PR can be merged it will first need to pass continuous integration tests and be reviewed. Sometimes the review process can be slow, so please be patient.

While you're waiting, please feel free to review other open PRs. While only a subset of people are authorized to approve pull requests for merging, everyone is encouraged to review open pull requests. Doing reviews helps reduce the burden on the core team and helps make the project's code better for everyone.

One or more of the following people are relevant to this code:

  • @Qiskit/terra-core

@ShellyGarion
Copy link
Copy Markdown
Member

Thanks for the fix. However, the original issue is somewhat unclear.
Please can you provide a reproducing code example that actually shows the incorrect behaviour given in the issue?

@coveralls
Copy link
Copy Markdown

coveralls commented May 3, 2026

Coverage Report for CI Build 25485017011

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Coverage increased (+0.04%) to 87.614%

Details

  • Coverage increased (+0.04%) from the base build.
  • Patch coverage: 11 of 11 lines across 1 file are fully covered (100%).
  • 716 coverage regressions across 9 files.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

716 previously-covered lines in 9 files lost coverage.

File Lines Losing Coverage Coverage
crates/circuit/src/dag_circuit.rs 398 84.69%
crates/circuit/src/circuit_drawer.rs 73 95.26%
qiskit/circuit/quantumcircuit.py 71 94.46%
crates/circuit/src/circuit_data.rs 52 87.17%
crates/circuit/src/parameter/parameter_expression.rs 51 91.04%
crates/circuit/src/register_data.rs 42 69.74%
crates/transpiler/src/passes/basis_translator/compose_transforms.rs 16 84.67%
crates/circuit/src/interner.rs 9 97.02%
crates/qasm2/src/lex.rs 4 92.03%

Coverage Stats

Coverage Status
Relevant Lines: 122061
Covered Lines: 106942
Line Coverage: 87.61%
Coverage Strength: 962980.13 hits per line

💛 - Coveralls

Expand the docstring on the existing regression test to explain the
operator-precedence bug that was fixed (a && b || c vs a && (b || c)).

Add a complementary test that verifies a reversed default inverse pair
(cs followed by csdg on the same qubits) is still correctly cancelled —
confirming the fix didn't break the happy path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@TSS99
Copy link
Copy Markdown
Contributor Author

TSS99 commented May 3, 2026

Thank you for the review, @ShellyGarion!

Here is a concrete Python example that reproduces the incorrect behaviour described in the issue:

from qiskit import QuantumCircuit
from qiskit.transpiler.passes import InverseCancellation

qc = QuantumCircuit(2)
qc.csdg(0, 1)   # CS† on qubits (0, 1)
qc.cs(1, 0)     # CS  on qubits (1, 0) — reversed qubit order

pass_ = InverseCancellation()
result = pass_(qc)

print(result.count_ops())
# Before fix → {}            (both gates incorrectly cancelled)
# After fix  → {'cs': 1, 'csdg': 1}  (gates correctly preserved)

Root cause recap: The condition in std_inverse_pairs was effectively:

same_qubits && forward_pair  ||  reversed_pair

Because && binds tighter than ||, the same_qubits guard only applied to the forward branch. The fix wraps both branches inside the qubit check:

same_qubits && (forward_pair || reversed_pair)

I also updated the test suite in the latest commit:

  • Expanded the docstring on the existing regression test to explain the operator-precedence bug.
  • Added test_default_inverse_pairs_cancel_reversed_on_same_qubits to confirm that a reversed pair on the same qubits is still correctly cancelled (happy-path guard).

All 67 tests pass. Please let me know if you need anything else!

@jakelishman
Copy link
Copy Markdown
Member

I'm not saying there's no bug in InverseCancellation, but cs(0,1) does cancel with csdg(1, 0). For the reproducer given, this fix (even though it might be logically correct given the code) actually makes the example worse.

@TSS99
Copy link
Copy Markdown
Contributor Author

TSS99 commented May 4, 2026

The actual bug shows up when the qubits overlap but aren't a permutation, e.g. csdg(0,1) then cs(1,2): they share qubit 1, end up in the same collected run, and the reversed-pair branch cancels them without checking qubits.

Updated:

  • kept the parens fix
  • added a small symmetric-qargs check for [CS, CSdg] so reversed qargs still cancel
  • replaced the test: csdg(0,1) cs(1,2) must not cancel, and cs(0,1) csdg(1,0) still cancels

@jakelishman, please let me know if there's anything off or anything I should change (please don't be frustrated with my PRs). I'm new to contributing here and I know I sent too many PRs; I'm slowing down. Any pointers on how to do this properly would really help, I want to learn.

@alexanderivrii
Copy link
Copy Markdown
Member

Thanks @TSS99, I would personally like to see this PR brought over the finishing line.

A few comments:

Based on your explanation about operator precedence and the added test

qc = QuantumCircuit(3)
qc.csdg(0, 1)
qc.cs(1, 2)
new_circ = InverseCancellation()(qc)

I was expecting that the current version of InverseCancellation pass would (incorrectly) remove csdg(0, 1) and cs(1, 2). But it does not! Can you explain this? This goes back to Jake's comment: create a small meaningful reproducer that shows that the current behavior is wrong. At this point, I am not fully convinced that there is a real bug.

Let's keep the PR as a bug-fix, not adding new features like canceling symmetric gates. Note that we can backport bugfixes to previous qiskit versions (such as 2.4) but we don't do this for new features. In addition, adding new functionality risks making the pass slower.

Looking at the inverse_cancellation.rs code, there are in fact two places where we have the a && b || c pattern, in addition to the one you are fixing, there is similar code in run_on_inverse_pairs. Can you verify whether that is also a bug?

Looking at the release note, what do you mean by "inverse gate pairs in reverse order"? Remember that release notes are targeted towards users.

@TSS99 TSS99 changed the title Fix inverse cancellation for reversed pairs on different qubits Fix CS/CSdg inverse cancellation with reversed qargs May 7, 2026
@TSS99
Copy link
Copy Markdown
Contributor Author

TSS99 commented May 7, 2026

Thanks @ShellyGarion, @jakelishman and @alexanderivrii for the careful review. You were right that my previous reproducer was not valid. I had misunderstood how collect_runs behaves there: csdg(0, 1) followed by cs(1, 2) does not get removed on main, so I removed that claim and the corresponding test.

I narrowed the PR to the behavior Jake pointed out around CS/CSdg symmetry. The reproducer is now the inconsistency between the two operation orders:

qc = QuantumCircuit(2)
qc.csdg(0, 1)
qc.cs(1, 0)
# cancelled on main

qc = QuantumCircuit(2)
qc.cs(0, 1)
qc.csdg(1, 0)
# not cancelled on main

Since CS and CSdg are symmetric in their qubit order, both should cancel. The updated test covers both orders and the code keeps this limited to the existing default CS/CSdg inverse pair; it is not a general symmetric-gate expansion.

I also checked run_on_inverse_pairs: it already groups both inverse-pair directions under the inst.qubits == next_inst.qubits guard, so I do not see the same precedence issue there.

I updated the title, PR description, and release note to avoid the misleading “different qubits” wording. Locally I ran:

  • python3 -m unittest test.python.transpiler.test_inverse_cancellation
  • cargo test -p qiskit-transpiler --no-default-features
  • cargo fmt --check
  • git diff --check

Thanks again for the patience and the detailed pointers. Let me know if any further changes needs to be made.

@Cryoris
Copy link
Copy Markdown
Collaborator

Cryoris commented May 7, 2026

I don't think this quite approaches the problem correctly. We generally could have symmetric gates in which case we should check the qubits match irrespective of the order. I've opened an alternative fix in #16153 since that seemed the more efficient solution compared to explaining the setup once more.

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

Labels

Community PR PRs from contributors that are not 'members' of the Qiskit repo

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Incorrect boolean logic in [crates/transpiler/src/passes/inverse_cancellation.rs]: a && b || c should be a && (b || c)

7 participants