Skip to content

Fixed commutation checking between two Pauli product measurements#16023

Merged
Cryoris merged 5 commits intoQiskit:mainfrom
alexanderivrii:fix-ppm-ppm-commutation
Apr 15, 2026
Merged

Fixed commutation checking between two Pauli product measurements#16023
Cryoris merged 5 commits intoQiskit:mainfrom
alexanderivrii:fix-ppm-ppm-commutation

Conversation

@alexanderivrii
Copy link
Copy Markdown
Member

@alexanderivrii alexanderivrii commented Apr 14, 2026

Summary

We have recently realized that checking commutativity between two PauliProductMeasurement instructions (introduced in #15359) is incorrect in the case that both measurements measure to the same classical bit. In this case, the later measurement overwrites the result of the earlier measurement, and thus the order of these instructions in the quantum circuit matters. This PR applies a simple fix of saying that two Pauli product measurement instruction do not commute unless they are completely identical (including pauli, qubits and clbits).

(Updated) This also handles "canonicalization": PPM(XZ, qubits=[0,1], clbits=[0]) is the same as PPM(ZX, qubits=[1,0], clbits=[0]), and so they are now also computed to commute.

Note: the current strategy of constructing sparse observable is in the spirit of the existing code, and I am trying to do minimal changes in order to backport a variant of this to 2.4. I have a followup PR #15815 that greatly increases the efficiency of commuting PPRs/PPMs among themselves (including canonicalization, but temporarily having the same problems as being fixed here).

This also updates commutativity checking tests to pass clbits when checking commutativity between two PPMs.

One other question: do we want to backport the fix to 2.4? If so, we would need to adjust this PR to the commutation checker version before #15488.

AI/LLM disclosure

  • I didn't use LLM tooling, or only used it privately.
  • I used the following tool to help write this PR description:
  • I used the following tool to generate or modify code:

@alexanderivrii alexanderivrii requested a review from Cryoris April 14, 2026 13:54
@alexanderivrii alexanderivrii requested a review from a team as a code owner April 14, 2026 13:54
@alexanderivrii alexanderivrii added the Changelog: Fixed Add a "Fixed" entry in the GitHub Release changelog. label Apr 14, 2026
@qiskit-bot
Copy link
Copy Markdown
Collaborator

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

  • @Qiskit/terra-core

Copy link
Copy Markdown
Member

@jakelishman jakelishman left a comment

Choose a reason for hiding this comment

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

It's a known bug with a simple solution, so we probably should backport a fix. If the commutation checker has changed substantially since 2.4 froze, though, then you might need to manually write a different and minimal patch to stable/2.4 instead of trying to auto-backport this one.

Comment on lines +512 to +513
let pauli1 = try_pauli_generator(op1, qargs1, size).expect("extracting sparse observable generator from pauli product measurement in infallible");
let pauli2 = try_pauli_generator(op2, qargs2, size).expect("extracting sparse observable generator from pauli product measurement in infallible");
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.

It's not necessarily for this PR, but these expects are an indication that the API is wrong here. There should probably be a function

observable_generator_from_ppm(ppm: &PauliProductMeasurement, qubits: &[Qubit], num_qubits: u32) -> SparseObservable {}

that you can call here. (and try_pauli_generator would use that, etc)

It's generally not a great idea to go from a specific type (&PauliProductMeasurement here) in your pattern guard back up to a more generic type (OperationRef) - it often leads to this kind of code that has to make assumptions about validity, when the language gives us tools to have the compiler check it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, I was thinking about this, especially after reviewing your PRs :smiling. I have implemented this suggestion in 6323efa, mostly because I also needed to return the phase of the pauli in addition to the sparse observable.

Comment on lines +506 to +508
// If both PPMs write to the same classical bit, it's generally incorrect to interchange them.
// So we return true only if both PPMs are identical.
return Ok(qargs1 == qargs2 && ppm1 == ppm2);
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.

From your PR comment, you were worried about the case (ZX, [0, 1]) and (XZ, [1, 0]). The simplest algorithm would be to "canonicalise" both PPMs into sorted order in terms of the qargs and then do an elementwise comparison. That's (naively, at least) $\mathcal O(n \lg n)$ for $n$ qubits.

I wouldn't say you have to do that in this PR, though.

Fwiw, if the two PPMs are identical, then any optimisation that applies to one necessarily applies to the other too, so on the face of it, it doesn't really matter whether you report them as commuting or not, but it would matter if you're then intending to group things into mutually commuting terms; if you report them as falsely non-commuting then you might fail to commute another rotation through both measures and to cancel with something on the other side.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Great point on collecting mutually commuting sets of gates - I have not thought of that.

I have fixed the canonicalization problem in 6323efa in the spirit of the existing code: constructing SparseObservable generators and checking whether they are equal. In the process I realized that try_pauli_generator ignores the phase of a pauli product measurement instruction, which is probably fine in general, but not in this particular case -- as this would lead to incorrectly commuting PPM("XYZ") and PPM("-XYZ").

Note that constructing a SparseObservable instead of just retrieving the Pauli is an overkill, and I have a follow-up PR #15815 that significantly improves upon commuting PPRs/PPMs among themselves (including canonicalization, but temporary having the same problems that are being fixed here).

Comment on lines +509 to +513
} else {
// PPMs write to different classical bits, and they commute if and only if their pauli generators do.
let size = qargs1.iter().chain(qargs2.iter()).max().unwrap().0 + 1;
let pauli1 = try_pauli_generator(op1, qargs1, size).expect("extracting sparse observable generator from pauli product measurement in infallible");
let pauli2 = try_pauli_generator(op2, qargs2, size).expect("extracting sparse observable generator from pauli product measurement in infallible");
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 else block basically just decays to the code that's right below it anyway, especially with the current use of the generic try_pauli_generator, and you don't actually need to extract the ppm objects to check the cargs (bugs in the test suite notwithstanding), so you could potentially replace this whole if let with

if num_cargs1 > 0 && num_cargs2 > 0 && cargs1 == cargs2 {
    return Ok(op1 == op2);
}

and avoid all the rest of the extra code?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good point, I have removed the else branch in 6323efa.

@alexanderivrii alexanderivrii added the stable backport potential Make Mergify open a backport PR to the most recent stable branch on merge. label Apr 15, 2026
@alexanderivrii alexanderivrii added this to the 2.4.0 milestone Apr 15, 2026
@alexanderivrii
Copy link
Copy Markdown
Member Author

I think that we should be able to backport this to stable/2.4 in the current form.

Comment thread crates/transpiler/src/commutation_checker.rs
"XXIY",
[0, 1, 3, 2],
True,
), # same Paulis and qubits up to reordering
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit: This formatting makes it quite hard to read the cases -- I would guess it's the comment that triggers black to choose this formatting. Does it look nicer if you do

# same Paulis and qubits up to reordering
("XXIY", ...),

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

changed in 8ef7b5b - indeed black has reformatted the nice comments I had locally before

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

should be finally ok with 027aeb4

Comment thread test/python/circuit/test_commutation_checker.py
@alexanderivrii alexanderivrii requested a review from Cryoris April 15, 2026 08:19
@coveralls
Copy link
Copy Markdown

Coverage Report for CI Build 24443416127

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.008%) to 87.489%

Details

  • Coverage increased (+0.008%) from the base build.
  • Patch coverage: 19 of 19 lines across 1 file are fully covered (100%).
  • 41 coverage regressions across 5 files.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

41 previously-covered lines in 5 files lost coverage.

File Lines Losing Coverage Coverage
qiskit/circuit/quantumcircuit.py 33 94.46%
crates/qasm2/src/lex.rs 4 92.29%
crates/circuit/src/lib.rs 2 96.15%
crates/circuit/src/parameter/parameter_expression.rs 1 90.53%
crates/circuit/src/parameter/symbol_expr.rs 1 73.88%

Coverage Stats

Coverage Status
Relevant Lines: 119519
Covered Lines: 104566
Line Coverage: 87.49%
Coverage Strength: 979971.27 hits per line

💛 - Coveralls

@Cryoris Cryoris enabled auto-merge April 15, 2026 09:53
@Cryoris Cryoris added this pull request to the merge queue Apr 15, 2026
Merged via the queue into Qiskit:main with commit 89ff976 Apr 15, 2026
26 checks passed
@github-project-automation github-project-automation Bot moved this from Ready to Done in Qiskit 2.4 Apr 15, 2026
mergify Bot pushed a commit that referenced this pull request Apr 15, 2026
…6023)

* Fix commutation checking between two PPMs

* fix for the case qubits are different

* additional fixes and improvements following the review

* improve test formatting

* formatting (again)

(cherry picked from commit 89ff976)

# Conflicts:
#	crates/transpiler/src/commutation_checker.rs
#	test/python/circuit/test_commutation_checker.py
github-merge-queue Bot pushed a commit that referenced this pull request Apr 15, 2026
…ckport #16023) (#16040)

* Fixed commutation checking between two Pauli product measurements (#16023)

* Fix commutation checking between two PPMs

* fix for the case qubits are different

* additional fixes and improvements following the review

* improve test formatting

* formatting (again)

(cherry picked from commit 89ff976)

# Conflicts:
#	crates/transpiler/src/commutation_checker.rs
#	test/python/circuit/test_commutation_checker.py

* fix conflicts

* remove comment

---------

Co-authored-by: Alexander Ivrii <alexi@il.ibm.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Changelog: Fixed Add a "Fixed" entry in the GitHub Release changelog. stable backport potential Make Mergify open a backport PR to the most recent stable branch on merge.

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

5 participants