Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5d2d7d0
Coercing integer types to floats when appending a PPR to a circuit
alexanderivrii Mar 15, 2026
dff5668
Extending RemoveIdentityEquiv and CommutativeOptimization to handle PPRs
alexanderivrii Mar 15, 2026
49960e1
reno
alexanderivrii Mar 15, 2026
0dde194
Improve commutation checking of PPRs and PPMs
alexanderivrii Mar 15, 2026
4cd1ff4
reno
alexanderivrii Mar 16, 2026
789e1c6
Merge branch 'main' into opt_ppr_ppm_in_cc
alexanderivrii Mar 31, 2026
de13108
canonicalizing PPM instruction as well
alexanderivrii Mar 31, 2026
087362f
faster method based on comparing two sorted vectors
alexanderivrii Mar 31, 2026
08f9fa5
Revert "faster method based on comparing two sorted vectors"
alexanderivrii Apr 1, 2026
1beefa4
moving the efficient check to commutative optimization
alexanderivrii Apr 1, 2026
e45c48a
merge with main
alexanderivrii Apr 3, 2026
6cbce16
More tests for commutations of pauli-based gates
alexanderivrii Apr 3, 2026
b654130
Adding CommutativeOptimization tests
alexanderivrii Apr 3, 2026
d889df0
enforcing PPM & PPR types for sorted checking
alexanderivrii Apr 4, 2026
06cbb55
Replacing vec by hash_map
alexanderivrii Apr 4, 2026
f4a9df6
sorting arguments before pauli-based check
alexanderivrii Apr 4, 2026
b13a58d
Addressing review comments by Janani
alexanderivrii Apr 4, 2026
76a8c74
removing gates equivalent to identity
alexanderivrii Apr 9, 2026
55e93d1
fix to actually simplify the circuit + tests
alexanderivrii Apr 9, 2026
c90835c
merge with main + a few fixes
alexanderivrii Apr 16, 2026
b3e6456
improve commutation checker
alexanderivrii Apr 16, 2026
a685517
updating commutative optimization for PPMs
alexanderivrii Apr 16, 2026
9f7a432
merge with main
alexanderivrii Apr 17, 2026
fb03d53
code cleanup
alexanderivrii Apr 17, 2026
c2c3ca5
typos and comments, following review
alexanderivrii Apr 17, 2026
a669772
combining tests
alexanderivrii Apr 17, 2026
a67a060
Addressing review comments
alexanderivrii Apr 23, 2026
d13a51a
ore review comments
alexanderivrii Apr 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions crates/transpiler/src/commutation_checker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ fn try_extract_op_from_ppr(
Some(out.compose_map(&local, |i| qubits[i as usize].0))
}

Comment thread
alexanderivrii marked this conversation as resolved.
fn try_pauli_generator(
fn try_sparse_observable_generator(
operation: &OperationRef,
qubits: &[Qubit],
num_qubits: u32,
Expand All @@ -248,6 +248,18 @@ fn try_pauli_generator(
}
}

/// Checks if the generator can be represented as a single Pauli term.
/// If so, returns the generator in the (Z, X) form.
pub fn try_pauli_generator<'a>(
operation: &'a OperationRef,
) -> Option<(&'a Vec<bool>, &'a Vec<bool>)> {
Comment thread
jan-an marked this conversation as resolved.
match operation {
OperationRef::PauliProductRotation(ppr) => Some((&ppr.z, &ppr.x)),
OperationRef::PauliProductMeasurement(ppm) => Some((&ppm.z, &ppm.x)),
_ => None,
}
}

fn get_bits_from_py<T>(
py_bits1: &Bound<'_, PyTuple>,
py_bits2: &Bound<'_, PyTuple>,
Expand Down Expand Up @@ -521,10 +533,27 @@ impl CommutationChecker {
_ => (),
};

if let (Some((z1, x1)), Some((z2, x2))) =
(try_pauli_generator(op1), try_pauli_generator(op2))
{
let max_q1 = qargs1.iter().map(|q| q.index()).max().unwrap_or(0);
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.

Why is the fallback value for unwrap_or 0?

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.

I have replaced this code with one using hashmap, so this is no longer relevant. However, the motivation for the previous code was that if qargs1 is empty, then max() returns None, and we can let the maximum qubit used to be 0.

let mut in_q1 = vec![usize::MAX; max_q1 + 1];
for (i, &q) in qargs1.iter().enumerate() {
in_q1[q.index()] = i;
}
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.

I think we discussed this offline but I don't remember the answer: would it be faster to sort both qargs (log timing) and then iterate, rather than finding the max (linear)?

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.

I tried this now and it became 30% slower, see #15815 for how the experiments were run.

Copy link
Copy Markdown
Member Author

@alexanderivrii alexanderivrii Apr 4, 2026

Choose a reason for hiding this comment

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

I agree that for robustness it's best to avoid linear scaling with the total number of qubits in the circuit, so I changed the implementation to use a HashMap instead of a vector. For large Pauli strings over all of circuit qubits (as in the HamLib experiment mentioned in the summary) this makes the implementation about 5% slower, however for short Pauli strings with large qubit indices this improves the implementation. I also moved the optional reversing of the operations to be done earlier, so that we are filling the hashmap with the smaller number of qubits. See 06cbb55 and f4a9df6.

Comment thread
Cryoris marked this conversation as resolved.
let mut parity = false;
for (j, &q) in qargs2.iter().enumerate() {
if let Some(&i) = in_q1.get(q.index()).filter(|&&i| i != usize::MAX) {
parity ^= (x1[i] && z2[j]) ^ (z1[i] && x2[j]);
}
}
return Ok(!parity);
}

// Handle commutations in between Pauli-based gates, like PauliGate or PauliEvolutionGate
let size = qargs1.iter().chain(qargs2.iter()).max().unwrap().0 + 1;
if let Some(obs1) = try_pauli_generator(op1, qargs1, size) {
if let Some(obs2) = try_pauli_generator(op2, qargs2, size) {
if let Some(obs1) = try_sparse_observable_generator(op1, qargs1, size) {
if let Some(obs2) = try_sparse_observable_generator(op2, qargs2, size) {
return Ok(obs1.commutes(&obs2, tol));
}
}
Expand Down
62 changes: 59 additions & 3 deletions crates/transpiler/src/passes/commutative_optimization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ use pyo3::{Bound, PyResult, pyfunction, wrap_pyfunction};
use qiskit_circuit::instruction::Parameters;
use smallvec::smallvec;

use crate::commutation_checker::{CommutationChecker, try_matrix_with_definition};
use crate::commutation_checker::{
CommutationChecker, try_matrix_with_definition, try_pauli_generator,
};
use crate::passes::remove_identity_equiv::{average_gate_fidelity_below_tol, is_identity_equiv};
use qiskit_circuit::circuit_instruction::OperationFromPython;
use qiskit_circuit::dag_circuit::DAGCircuit;
use qiskit_circuit::operations::{
Operation, OperationRef, Param, PauliBased, PauliProductRotation, StandardGate, multiply_param,
radd_param,
Operation, OperationRef, Param, PauliBased, PauliProductMeasurement, PauliProductRotation,
StandardGate, multiply_param, radd_param,
};
use qiskit_circuit::{BlocksMode, Clbit, NoBlocks, Qubit, imports};

Expand Down Expand Up @@ -229,6 +231,39 @@ fn canonicalize(
return Some((canonical_instruction, Param::Float(0.)));
}

if let OperationRef::PauliProductMeasurement(ppm) = inst.op.view() {
Comment thread
alexanderivrii marked this conversation as resolved.
let qargs = dag.get_qargs(inst.qubits);
let mut paired = qargs
.iter()
.zip(ppm.z.iter())
.zip(ppm.x.iter())
.map(|((q, z), x)| (q, z, x))
.collect::<Vec<_>>();
paired.sort_by_key(|(q, _, _)| **q);
let (sorted_qargs, sorted_z, sorted_x) =
paired
.into_iter()
.multiunzip::<(Vec<Qubit>, Vec<bool>, Vec<bool>)>();
let sorted_ppm = PauliProductMeasurement {
z: sorted_z,
x: sorted_x,
neg: ppm.neg,
};

let sorted_qubits = dag.add_qargs(&sorted_qargs);

let canonical_instruction = PackedInstruction {
op: PauliBased::PauliProductMeasurement(sorted_ppm).into(),
qubits: sorted_qubits,
clbits: inst.clbits,
params: inst.params.clone(),
label: None,
#[cfg(feature = "cache_pygates")]
py_op: std::sync::OnceLock::new(),
};
return Some((canonical_instruction, Param::Float(0.)));
}

None
}

Expand Down Expand Up @@ -256,6 +291,27 @@ fn commute(
let op1 = inst1.op.view();
let op2 = inst2.op.view();

if let (Some((z1, x1)), Some((z2, x2))) = (try_pauli_generator(&op1), try_pauli_generator(&op2))
{
let mut parity = false;

// The qubits in PPRs and PPMs are known to be sorted by qubit index.
let (n1, n2) = (qargs1.len(), qargs2.len());
let (mut i1, mut i2) = (0, 0);
while i1 < n1 && i2 < n2 {
match qargs1[i1].cmp(&qargs2[i2]) {
std::cmp::Ordering::Less => i1 += 1,
std::cmp::Ordering::Greater => i2 += 1,
std::cmp::Ordering::Equal => {
parity ^= (x1[i1] && z2[i2]) ^ (z1[i1] && x2[i2]);
i1 += 1;
i2 += 1;
}
}
}
return Ok(!parity);
}

Ok(commutation_checker.commute(
&op1,
inst1.params.as_deref(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
features_transpiler:
- Improved the performance of commutation checking for :class:`.PauliProductRotationGate` and
:class:`.PauliProductMeasurement`.
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.

Would we like to mention here very briefly as to what has improved to improve the performance of the commutation checker?

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.

Thanks for the great feedback, Janani! I have addressed and your other review comments in b13a58d.