Skip to content
Merged
Show file tree
Hide file tree
Changes from 57 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
b7ac6b4
Transpiler: implement exact Pauli generators for LightCone in Rust
debasmita2102 Feb 12, 2026
75c3d1f
Lint: apply cargo fmt and fix clippy::map_clone in split_2q_unitaries.rs
debasmita2102 Feb 12, 2026
1ba26a7
Expose generator_observable to Python
debasmita2102 Feb 12, 2026
ab5ebad
Refactor generator logic to use qiskit_circuit::StandardGate
debasmita2102 Feb 16, 2026
b45d2ab
Update generator_observable with expansions for CCX/CCZ/CSwap
debasmita2102 Feb 19, 2026
307486f
Fix generator fallback for unsupported gates and update docs
debasmita2102 Feb 19, 2026
f36974a
Narrow PR scope: remove split_2q_unitaries and pyext python wrapper
debasmita2102 Mar 11, 2026
331a972
Add generator support for CS, CSdg, CSX, CCX, CCZ; add commutation tests
debasmita2102 Mar 11, 2026
c1c6474
style: run black on test_commutation_checker.py
debasmita2102 Mar 11, 2026
8b6ad0d
style: run rustfmt on standard_generators.rs
debasmita2102 Mar 11, 2026
d46faa9
lint: fix license headers and pylint warnings
debasmita2102 Mar 11, 2026
b379cab
style: remove decorative unicode lines and add operator equivalence t…
debasmita2102 Mar 11, 2026
8eca98f
fix: ensure strict Operator equivalence for all supported generators
debasmita2102 Mar 11, 2026
82d06c3
style: fix pylint reimport and missing variables in test_commutation_…
debasmita2102 Mar 11, 2026
ba51fa3
Add generic generator equivalence test, fix matrix conversion, and op…
debasmita2102 Mar 16, 2026
5a598f4
Format python tests
debasmita2102 Mar 16, 2026
479cb55
Update copyright year in standard_generators.rs
debasmita2102 Mar 16, 2026
6e49f7d
Clarify docstring for multi-qubit generators context
debasmita2102 Mar 16, 2026
41e6822
Fix XXPlusYY and XXMinusYY generator validation and test cases
debasmita2102 Mar 16, 2026
14d8260
Move test imports to top of file
debasmita2102 Mar 16, 2026
b42479c
Use ddt data parameterization for gate generator testing tests
debasmita2102 Mar 16, 2026
4bd721d
Refactor test_clifford_gates_have_generators to use ddt
debasmita2102 Mar 16, 2026
ea2f559
Add CPhase, CRX, CRY, CRZ to generator tests
debasmita2102 Mar 16, 2026
4f1b312
Update test_all_gates_operator_equivalence_ddt docstring to be more a…
debasmita2102 Mar 16, 2026
b783d6b
Move all inline imports to the top of the file
debasmita2102 Mar 16, 2026
9785156
Reorganize test data to group related gates logically
debasmita2102 Mar 16, 2026
2604588
Add tests for unsupported gates and parameters in generator logic
debasmita2102 Mar 16, 2026
800bbdf
Rename test helper to _commute to reflect that it uses either generat…
debasmita2102 Mar 16, 2026
246fa63
Clarify parameter handling in generator_observable docstring
debasmita2102 Mar 16, 2026
6ea2944
Comprehensive audit: Add Identity support and register all missing ga…
debasmita2102 Mar 18, 2026
7f37313
Fix: Correct gate generator expansions and resolve UnsortedIndices la…
debasmita2102 Mar 18, 2026
ee303f8
pose generator engine, restore overflow logic, expand tests, and cent…
debasmita2102 Mar 23, 2026
30903a9
Update crates/circuit/src/util.rs
debasmita2102 Mar 26, 2026
a221780
Update releasenotes/notes/pauli-evolution-commutation-8f7caab8.yaml
debasmita2102 Mar 26, 2026
56ddaf6
Update releasenotes/notes/pauli-evolution-commutation-8f7caab8.yaml
debasmita2102 Mar 26, 2026
bc07ec1
added missing gate, added test, copyright year, doc explaining
debasmita2102 Mar 26, 2026
b490441
restore negative constants to util.rs for generator expansions
debasmita2102 Mar 26, 2026
33effd8
fix lint errors in regression test and move imports to top
debasmita2102 Mar 26, 2026
c5e07f0
Final PR fixes: address remaining lint issues and update copyright he…
debasmita2102 Mar 27, 2026
b7329c7
Remove accidental temporary files from PR
debasmita2102 Mar 27, 2026
edc9c6b
Restore original copyright years for existing files (only PR-created …
debasmita2102 Mar 27, 2026
de6b82e
Permanently delete bicycle-architecture-compiler from branch
debasmita2102 Mar 27, 2026
6aededd
resolved merge conflicts
debasmita2102 Mar 31, 2026
514c166
removed redundant constants blocking branch conflict
debasmita2102 Mar 31, 2026
dfc6a45
fix lint and formatting issues
debasmita2102 Mar 31, 2026
37dd7fd
docstring edits
debasmita2102 Mar 31, 2026
267c106
docstring formatting
debasmita2102 Mar 31, 2026
fbd90d5
fixing formatting
debasmita2102 Apr 1, 2026
fbd68b4
revert docstring changes to fix lint failure
debasmita2102 Apr 1, 2026
34c2795
Reverted changes in max_entries_per_row
debasmita2102 Apr 1, 2026
70d1602
renaming tests
debasmita2102 Apr 1, 2026
16c88e6
readded the comments in cargo test
debasmita2102 Apr 1, 2026
f96a972
Cleanup and reword method
Cryoris Apr 1, 2026
9fb660d
Merge branch 'main' into lightcone-fix
Cryoris Apr 1, 2026
10c28cb
Reno and rm duplicated block
Cryoris Apr 1, 2026
3b70253
Sasha's review comments
Cryoris Apr 2, 2026
5cd398c
Remove some unused imports
Cryoris Apr 2, 2026
5e01660
Merge branch 'main' into lightcone-fix
Cryoris Apr 2, 2026
71cdd06
Move pickle import back to where it was
Cryoris Apr 2, 2026
b07f90d
Merge branch 'lightcone-fix' of github.com:debasmita2102/qiskit into …
Cryoris Apr 2, 2026
e5d15fd
Fix circuit crate dependency
Cryoris Apr 2, 2026
f78b306
Rm incorrect sentence on commutation
Cryoris Apr 2, 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
1 change: 1 addition & 0 deletions crates/pyext/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ fn _accelerate(m: &Bound<PyModule>) -> PyResult<()> {
add_submodule(m, ::qiskit_transpiler::passes::sabre::sabre, "sabre")?;
add_submodule(m, ::qiskit_accelerate::sampled_exp_val::sampled_exp_val, "sampled_exp_val")?;
add_submodule(m, ::qiskit_quantum_info::sparse_observable::sparse_observable, "sparse_observable")?;
add_submodule(m, ::qiskit_quantum_info::standard_generators::standard_generators, "standard_generators")?;
add_submodule(m, ::qiskit_quantum_info::sparse_pauli_op::sparse_pauli_op, "sparse_pauli_op")?;
add_submodule(m, ::qiskit_transpiler::passes::scheduling_mod, "scheduling")?;
add_submodule(m, ::qiskit_synthesis::matrix::sim::unitary_sim, "unitary_sim")?;
Expand Down
1 change: 1 addition & 0 deletions crates/quantum_info/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub mod clifford;
pub mod pauli_lindblad_map;
pub mod sparse_observable;
pub mod sparse_pauli_op;
pub mod standard_generators;
pub mod unitary_compose;
pub mod versor_u2;

Expand Down
343 changes: 343 additions & 0 deletions crates/quantum_info/src/standard_generators.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,343 @@
// This code is part of Qiskit.
//
// (C) Copyright IBM 2026
//
// This code is licensed under the Apache License, Version 2.0. You may
// obtain a copy of this license in the LICENSE.txt file in the root directory
// of this source tree or at https://www.apache.org/licenses/LICENSE-2.0.
//
// Any modifications or derivative works of this code must retain this
// copyright notice, and modified files need to carry a notice indicating
// that they have been altered from the originals.

// AI Attribution:
// This module was developed with assistance from GitHub Copilot integrated in VS Code.
// The underlying model : Claude Haiku 4.5.
// Portions of the Pauli generator mapping logic and SoA layout were generated
// and then manually verified for mathematical correctness against the commutation logic.

use crate::sparse_observable::SparseObservable;
use pyo3::prelude::*;
use pyo3::wrap_pyfunction;

use num_complex::Complex64;
use qiskit_circuit::operations::{Operation, Param, StandardGate};
use qiskit_util::complex::{C_ZERO, c64};
use std::f64::consts::{FRAC_PI_2, FRAC_PI_4, FRAC_PI_8, SQRT_2};

const C_FRAC_PI_2: Complex64 = c64(FRAC_PI_2, 0.0);
const C_FRAC_PI_4: Complex64 = c64(FRAC_PI_4, 0.0);
const C_FRAC_PI_8: Complex64 = c64(FRAC_PI_8, 0.0);
const C_FRAC_PI_2_SQRT_2: Complex64 = c64(FRAC_PI_2 / SQRT_2, 0.0);
const C_M_FRAC_PI_4: Complex64 = c64(-FRAC_PI_4, 0.0);
const C_M_FRAC_PI_8: Complex64 = c64(-FRAC_PI_8, 0.0);
const C_M_FRAC_PI_2_SQRT_2: Complex64 = c64(-FRAC_PI_2 / SQRT_2, 0.0);

// A constant cutoff below which we ignore the beta parameter of [StandardGate::XXPlusYY]
// and [StandardGate::XXMinusYY]
const BETA_TOLERANCE: f64 = 1e-10;

/// Return the exponent representation of a [StandardGate], if it is available.
///
/// We define the exponent $E$ of a gate $U$ as $U = \exp(-i E)$ up to a global phase.
/// For example, the [StandardGate::RX] has $E = \theta/2 X$. Since the return type is
/// [SparseObservable], which does not support parameterized coefficients, parameter values
/// of type [Param::ParameterExpression] default to 1.0.
///
/// # Arguments
///
/// * gate - The standard gate whose exponent we return.
/// * params - The gate's parameters. The length must equal the number of parameters the gate has.
///
/// # Returns
///
/// * Some(SparseObservable) - The exponent.
/// * None - If the exponent is not supported.
pub fn standard_gate_exponent(gate: StandardGate, params: &[Param]) -> Option<SparseObservable> {
let fixed_params = params
.iter()
.map(|p| match p {
Param::Float(f) => *f,
Param::ParameterExpression(_) => 1.0,
Param::Obj(_) => panic!("StandardGate does not have Param::Obj parameters"),
})
.collect::<Vec<f64>>();

let num_qubits = gate.num_qubits();

use crate::sparse_observable::BitTerm::*;

let (coeffs, bit_terms, indices, boundaries) = match gate {
StandardGate::GlobalPhase => (vec![c64(-fixed_params[0], 0.)], vec![], vec![], vec![0, 0]),
// H = exp(-i pi/sqrt(8) (X + Z))
StandardGate::H => (
vec![C_FRAC_PI_2_SQRT_2, C_FRAC_PI_2_SQRT_2],
vec![X, Z],
vec![0, 0],
vec![0, 1, 2],
),
// X = exp(-i pi/2 X), Y = exp(-i pi/2 Y), Z = exp(-i pi/2 Z)
StandardGate::X => (vec![C_FRAC_PI_2], vec![X], vec![0], vec![0, 1]),
StandardGate::Y => (vec![C_FRAC_PI_2], vec![Y], vec![0], vec![0, 1]),
StandardGate::Z => (vec![C_FRAC_PI_2], vec![Z], vec![0], vec![0, 1]),
// Identity: exp(-i 0) = I.
StandardGate::I => (vec![C_ZERO], vec![], vec![], vec![0, 0]),
// S = exp(-i pi/4 Z), Sdg = exp(-i (-pi/4) Z)
StandardGate::S => (vec![C_FRAC_PI_4], vec![Z], vec![0], vec![0, 1]),
StandardGate::Sdg => (vec![C_M_FRAC_PI_4], vec![Z], vec![0], vec![0, 1]),
// T = exp(-i pi/8 Z), Tdg = exp(-i (-pi/8) Z)
StandardGate::T => (vec![C_FRAC_PI_8], vec![Z], vec![0], vec![0, 1]),
StandardGate::Tdg => (vec![C_M_FRAC_PI_8], vec![Z], vec![0], vec![0, 1]),
// SX = exp(-i pi/4 X), SXdg = exp(-i (-pi/4) X)
StandardGate::SX => (vec![C_FRAC_PI_4], vec![X], vec![0], vec![0, 1]),
StandardGate::SXdg => (vec![C_M_FRAC_PI_4], vec![X], vec![0], vec![0, 1]),
// RX(t) = exp(-i t/2 X), RY, RZ=Phase, equivalently
StandardGate::RX => (
vec![c64(fixed_params[0] / 2.0, 0.0)],
vec![X],
vec![0],
vec![0, 1],
),
StandardGate::RY => (
vec![c64(fixed_params[0] / 2.0, 0.0)],
vec![Y],
vec![0],
vec![0, 1],
),
StandardGate::RZ | StandardGate::Phase | StandardGate::U1 => (
vec![c64(fixed_params[0] / 2.0, 0.0)],
vec![Z],
vec![0],
vec![0, 1],
),
// CX = exp(-i pi/4 (ZX - ZI - IX))
StandardGate::CX => (
vec![C_M_FRAC_PI_4, C_FRAC_PI_4, C_FRAC_PI_4],
vec![Z, X, Z, X],
vec![0, 1, 0, 1],
vec![0, 2, 3, 4],
),
// CY = exp(-i pi/4 (ZY - ZI - IY))
StandardGate::CY => (
vec![C_M_FRAC_PI_4, C_FRAC_PI_4, C_FRAC_PI_4],
vec![Z, Y, Z, Y],
vec![0, 1, 0, 1],
vec![0, 2, 3, 4],
),
// CZ = exp(-i pi/4 (ZZ - ZI - IZ))
StandardGate::CZ => (
vec![C_M_FRAC_PI_4, C_FRAC_PI_4, C_FRAC_PI_4],
vec![Z, Z, Z, Z],
vec![0, 1, 0, 1],
vec![0, 2, 3, 4],
),
// CS = exp(-i (-pi/8) (ZZ - ZI - IZ))
StandardGate::CS => (
vec![C_FRAC_PI_8, C_FRAC_PI_8, C_M_FRAC_PI_8],
vec![Z, Z, Z, Z],
vec![0, 1, 0, 1],
vec![0, 1, 2, 4],
),
// CSdg (inv sqrt(CZ)): factor +pi/8
StandardGate::CSdg => (
vec![C_M_FRAC_PI_8, C_M_FRAC_PI_8, C_FRAC_PI_8],
vec![Z, Z, Z, Z],
vec![0, 1, 0, 1],
vec![0, 1, 2, 4],
),
// CSX (sqrt(CX)/controlled-SX): factor -pi/8
StandardGate::CSX => (
vec![C_FRAC_PI_8, C_FRAC_PI_8, C_M_FRAC_PI_8],
vec![Z, X, Z, X],
vec![0, 1, 0, 1],
vec![0, 1, 2, 4],
),
// CRX(t) = exp(-i t/4 (-ZX + IX))
StandardGate::CRX => (
vec![
c64(-fixed_params[0] / 4.0, 0.0),
c64(fixed_params[0] / 4.0, 0.0),
],
vec![Z, X, X],
vec![0, 1, 1],
vec![0, 2, 3],
),
// CRY(t) = exp(-i t/4 (-ZY + IY))
StandardGate::CRY => (
vec![
c64(-fixed_params[0] / 4.0, 0.0),
c64(fixed_params[0] / 4.0, 0.0),
],
vec![Z, Y, Y],
vec![0, 1, 1],
vec![0, 2, 3],
),
// CRZ(t) = exp(-i t/4 (-ZZ + IZ))
StandardGate::CRZ => (
vec![
c64(-fixed_params[0] / 4.0, 0.0),
c64(fixed_params[0] / 4.0, 0.0),
],
vec![Z, Z, Z],
vec![0, 1, 1],
vec![0, 2, 3],
),
// CPhase(t) = exp(-i t/4 (-ZZ + ZI + IZ))
// (same as CRZ but shifts both Z0 and Z1, not just Z1)
StandardGate::CPhase => (
vec![
c64(-fixed_params[0] / 4.0, 0.0),
c64(fixed_params[0] / 4.0, 0.0),
c64(fixed_params[0] / 4.0, 0.0),
],
vec![Z, Z, Z, Z],
vec![0, 1, 0, 1],
vec![0, 2, 3, 4],
),
// Swap = exp(-i pi/4 (XX + YY + ZZ))
StandardGate::Swap => (
vec![C_FRAC_PI_4, C_FRAC_PI_4, C_FRAC_PI_4],
vec![X, X, Y, Y, Z, Z],
vec![0, 1, 0, 1, 0, 1],
vec![0, 2, 4, 6],
),
// ISwap = exp(-i (-pi/4) (XX + YY))
StandardGate::ISwap => (
vec![C_M_FRAC_PI_4, C_M_FRAC_PI_4],
vec![X, X, Y, Y],
vec![0, 1, 0, 1],
vec![0, 2, 4],
),
// RXX(t) = exp(-i t/2 XX)
StandardGate::RXX => (
vec![c64(fixed_params[0] / 2.0, 0.0)],
vec![X, X],
vec![0, 1],
vec![0, 2],
),
// RYY(t) = exp(-i t/2 YY)
StandardGate::RYY => (
vec![c64(fixed_params[0] / 2.0, 0.0)],
vec![Y, Y],
vec![0, 1],
vec![0, 2],
),
// RZZ(t) = exp(-i t/2 ZZ)
StandardGate::RZZ => (
vec![c64(fixed_params[0] / 2.0, 0.0)],
vec![Z, Z],
vec![0, 1],
vec![0, 2],
),
// RZX(t) = exp(-i t/2 ZX)
StandardGate::RZX => (
vec![c64(fixed_params[0] / 2.0, 0.0)],
vec![Z, X],
vec![0, 1],
vec![0, 2],
),
// XX+YY and XX-YY are just handled if the beta parameter is 0, in which case
// the generator is XX +- YY.
StandardGate::XXPlusYY | StandardGate::XXMinusYY => {
// This covers both parametric ``beta`` and if they are above tolerance
if fixed_params[1].abs() > BETA_TOLERANCE {
return None;
}

match gate {
StandardGate::XXPlusYY => (
vec![
c64(fixed_params[0] / 4.0, 0.0),
c64(fixed_params[0] / 4.0, 0.0),
],
vec![X, X, Y, Y],
vec![0, 1, 0, 1],
vec![0, 2, 4],
),
StandardGate::XXMinusYY => (
vec![
c64(fixed_params[0] / 4.0, 0.0),
c64(-fixed_params[0] / 4.0, 0.0),
],
vec![X, X, Y, Y],
vec![0, 1, 0, 1],
vec![0, 2, 4],
),
_ => unreachable!(),
}
}
// CCX = exp(-i pi/8 (ZZX - ZIX - IZX - ZZI + ZII + IZI + IIX))
StandardGate::CCX => (
vec![
C_FRAC_PI_8,
C_M_FRAC_PI_8,
C_M_FRAC_PI_8,
C_M_FRAC_PI_8,
C_FRAC_PI_8,
C_FRAC_PI_8,
C_FRAC_PI_8,
],
vec![Z, Z, X, Z, X, Z, X, Z, Z, Z, Z, X],
vec![0, 1, 2, 0, 2, 1, 2, 0, 1, 0, 1, 2],
vec![0, 3, 5, 7, 9, 10, 11, 12],
),
// Same as CCX but with Z on the target
StandardGate::CCZ => (
vec![
C_M_FRAC_PI_8,
C_FRAC_PI_8,
C_FRAC_PI_8,
C_FRAC_PI_8,
C_M_FRAC_PI_8,
C_M_FRAC_PI_8,
C_M_FRAC_PI_8,
],
vec![Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z, Z],
vec![0, 1, 2, 0, 2, 1, 2, 0, 1, 0, 1, 2],
vec![0, 3, 5, 7, 9, 10, 11, 12],
),
// CSwap = exp(-i pi/8 (ZII - ZXX - ZYY - ZZZ + IXX + IYY + IZZ))
StandardGate::CSwap => (
vec![
C_FRAC_PI_8,
C_M_FRAC_PI_8,
C_M_FRAC_PI_8,
C_M_FRAC_PI_8,
C_FRAC_PI_8,
C_FRAC_PI_8,
C_FRAC_PI_8,
],
vec![Z, Z, X, X, Z, Y, Y, Z, Z, Z, X, X, Y, Y, Z, Z],
vec![0, 0, 1, 2, 0, 1, 2, 0, 1, 2, 1, 2, 1, 2, 1, 2],
vec![0, 1, 4, 7, 10, 12, 14, 16],
),
// ECR = exp(-i pi/sqrt(8) (IX - XY))
StandardGate::ECR => (
vec![C_FRAC_PI_2_SQRT_2, C_M_FRAC_PI_2_SQRT_2],
vec![X, Y, X],
vec![0, 0, 1],
vec![0, 1, 3],
),
_ => return None,
};

// SAFETY: The internal data was constructed manually and is consistent.
Some(unsafe {
SparseObservable::new_unchecked(num_qubits, coeffs, bit_terms, indices, boundaries)
})
}

#[pyfunction(name = "_standard_gate_exponent")]
#[pyo3(signature = (gate, params=None))]
pub fn py_standard_gate_exponent(
gate: StandardGate,
params: Option<Vec<Param>>,
) -> Option<SparseObservable> {
let params = params.unwrap_or_default();
standard_gate_exponent(gate, &params)
}

pub fn standard_generators(m: &Bound<PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(py_standard_gate_exponent, m)?)?;
Ok(())
}
Loading
Loading