Skip to content

Refactor/abstract qubit sparse pauli-type interfaces#14901

Open
DanPuzzuoli wants to merge 33 commits into
Qiskit:mainfrom
DanPuzzuoli:qubit-sparse-pauli-trait
Open

Refactor/abstract qubit sparse pauli-type interfaces#14901
DanPuzzuoli wants to merge 33 commits into
Qiskit:mainfrom
DanPuzzuoli:qubit-sparse-pauli-trait

Conversation

@DanPuzzuoli

Copy link
Copy Markdown
Contributor

Summary

This PR follows up #14759 (which adds phased versions of QubitSparsePauli and QubitSparsePauliList), and anticipates adding even more similar classes (QubitSparsePauliSet and PhasedQubitSparsePauliSet). These classes all share overlapping behaviour, and also have high maintenance cost due to needing to define both rust and python interfaces. As such it is worth "getting it right" by taking some time to define their interfaces more carefully and to refactor their shared behaviour into a single location/implementation where possible.

The PR currently contains an initial pass for the "collection-type" objects: currently the list classesQubitSparsePauliList and PhasedQubitSparsePauliList, the unimplemented "set" objects, and also PauliLindbladMap. I view these as classes that store a collection of qubit-sparse Paulis along with contextual data (e.g. phase, or rates, probabilities, etc...). I think one of the things that's unclear to me is how much the "contextual data" will impact the ability to centralize functionality (i.e. cases of very similar behaviour but where it doesn't make sense to use the same signature), but in any case I think it's clearly useful to do this to the degree that it is possible.

To do this we need to define both rust and python interfaces. As suggested by @jakelishman , it seems the technical path forward is:

  1. Use traits to define rust interfaces and default implementations.
  2. Use macro rules to define the python interfaces, as traits unfortunately don't interact well with pyo3.

Details and comments

Rust interface

For the rust interface, I've currently defined a QubitSparsePauliListLike trait in `qubit_sparse_pauli.rs", which starts with the interface definitions (code from @jakelishman ):

pub trait QubitSparsePauliListLike {
    fn pauli_list(&self) -> &QubitSparsePauliList;
    fn pauli_list_mut(&mut self) -> &mut QubitSparsePauliList;

For QubitSparsePauliList these return self, but other classes will need to return the internally stored QubitSparsePauliList (or simply generate it if it is no longer part of the interal data structure).

From there, "property" methods are defined in terms of these:

/// Get the indices of each [Pauli].
    #[inline]
    fn indices(&self) -> &[u32] {
        &self.pauli_list().indices()
    }

    /// Get the boundaries of each term.
    #[inline]
    fn boundaries(&self) -> &[usize] {
        &self.pauli_list().boundaries()
    }

    /// Get the [Pauli]s in the list.
    #[inline]
    fn paulis(&self) -> &[Pauli] {
        &self.pauli_list().paulis()
    }

Lastly, some methods that are primarily based around modifying the qubit-sparse pauli structure:

    /// Drop every Pauli on the given `indices`, effectively replacing them with an identity.
    fn drop_paulis(&self, indices: HashSet<u32>) -> Result<Self, CoherenceError> where Self: Sized;

    /// Apply a transpiler layout.
    fn apply_layout(&self, layout: Option<&[u32]>, num_qubits: u32) -> Result<Self, CoherenceError> where Self: Sized;

    /// Drop qubits corresponding to the given `indices`.
    fn drop_qubits(&self, indices: HashSet<u32>) -> Result<Self, CoherenceError> where Self: Sized;
}

Both QubitSparsePauliList and PhasedQubitSparsePauli list have implementations of this trait. Note that the implementations of the last three methods would be extremely similar for all of the considered classes: calling the same method on the underlying QubitSparsePauliList and putting the result, along with copies of the context-dependant data in each class implementing the trait, into a new instance of said class. If there is a way to write a default implementation of such functions that could copy over all other data, irrespective of what it is, that'd be very nice.

Python interface

This is quite minimal but I've currently defined:

/// macro for generating pymethods for QubitSparsePauliListLike python interfaces
macro_rules! impl_py_qspl_methods {
    ($ty:ty) => {
        #[pymethods]
        impl $ty {
            #[getter]
            #[inline]
            pub fn num_qubits(&self) -> PyResult<u32> {
                let inner = self.inner.read().map_err(|_| InnerReadError)?;
                Ok(inner.num_qubits())
            }

            /// The number of elements in the list.
            #[getter]
            #[inline]
            pub fn num_terms(&self) -> PyResult<usize> {
                let inner = self.inner.read().map_err(|_| InnerReadError)?;
                Ok(inner.num_terms())
            }
        }
    }
}

in the src/pauli_lindblad_map/mod.rs file. I've defined there because the rules for where macros live are more constrained; this was the first thing I tried that compiled but presumably we'll want to find a better place for it. I haven't included it yet but I'll add apply_layout, drop_paulis/keep_paulis, and drop_qubits/keep_qubits (I haven't looked super closely at what else).

The python version of the classes again have a lot of similarly defined methods, but with slightly different signatures. E.g. many of the constructors are similar in spirit, and iteration/indexing over the constituent units is similar. I figure these things may be handle-able somehow with macros but it'll require a better understanding of rust typing than I have.

Other classes

The other classes to consider are QubitSparsePauli, PhasedQubitSparsePauli, and GeneratorTerm (the "single-element" version of a PauliLindbladMap). These classes are much simpler so I think the maintenance cost is less of a concern, but it'd be nice to do a similar trait/macro setup for them.

@DanPuzzuoli DanPuzzuoli requested a review from a team as a code owner August 13, 2025 22:24
@qiskit-bot

Copy link
Copy Markdown
Collaborator

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

  • @Qiskit/terra-core

@coveralls

Copy link
Copy Markdown

Pull Request Test Coverage Report for Build 16950745364

Warning: This coverage report may be inaccurate.

This pull request's base commit is no longer the HEAD commit of its target branch. This means it includes changes from outside the original pull request, including, potentially, unrelated coverage changes.

Details

  • 855 of 915 (93.44%) changed or added relevant lines in 4 files are covered.
  • 14 unchanged lines in 5 files lost coverage.
  • Overall coverage increased (+0.04%) to 88.284%

Changes Missing Coverage Covered Lines Changed/Added Lines %
crates/quantum_info/src/pauli_lindblad_map/pauli_lindblad_map_class.rs 23 26 88.46%
crates/quantum_info/src/pauli_lindblad_map/qubit_sparse_pauli.rs 138 144 95.83%
crates/quantum_info/src/pauli_lindblad_map/phased_qubit_sparse_pauli.rs 684 735 93.06%
Files with Coverage Reduction New Missed Lines %
crates/circuit/src/parameter/symbol_expr.rs 1 74.58%
crates/quantum_info/src/pauli_lindblad_map/pauli_lindblad_map_class.rs 1 96.75%
crates/quantum_info/src/pauli_lindblad_map/qubit_sparse_pauli.rs 1 89.94%
crates/qasm2/src/lex.rs 5 92.01%
crates/qasm2/src/parse.rs 6 97.56%
Totals Coverage Status
Change from base Build 16944327987: 0.04%
Covered Lines: 87915
Relevant Lines: 99582

💛 - Coveralls

@ShellyGarion ShellyGarion added mod: primitives Related to the Primitives module mod: quantum info Related to the Quantum Info module (States & Operators) labels Aug 14, 2025
@jakelishman jakelishman added this to the 2.3.0 milestone Oct 8, 2025
@github-project-automation github-project-automation Bot moved this to Ready in Qiskit 2.3 Oct 8, 2025
@jakelishman jakelishman self-assigned this Oct 9, 2025
order.sort_unstable_by_key(|a| indices[*a]);
let paulis = order.iter().map(|i| paulis[*i]).collect();
let mut sorted_indices = Vec::<u32>::with_capacity(order.len());
for i in order {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thank you for your contribution! This PR is a great refactoring for pauli-related structures, bringing them under one umbrella macro for the python interface. Your code LGTM. But I have one minor refactoring suggestion for enhanced code clarity and minimization.

This sorting logic here may be written as:

let mut order: Vec<_> = indices.iter().copied().enumerate().collect();
order.sort_unstable_by_key(|(_, index)| *index);

let (sorted_indices, sorted_paulis): (Vec<u32>, Vec<Pauli>) = order
    .into_iter()
    .map(|(i, index)| (index, paulis[i]))
    .unzip();

and perhaps after the index sorting a .window(2) based check can be done to validate an inconsistency in the ascending order, as:

if order.windows(2).any(|i| i[0].1 >= i[1].1) {
    return Err(CoherenceError::UnsortedIndices.into());
}

@Cryoris Cryoris modified the milestones: 2.3.0, 2.4.0 Dec 12, 2025
@Cryoris Cryoris removed this from Qiskit 2.3 Dec 12, 2025
@jakelishman jakelishman modified the milestones: 2.4.0, 2.5.0 Mar 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

mod: primitives Related to the Primitives module mod: quantum info Related to the Quantum Info module (States & Operators)

Projects

Status: Ready

Development

Successfully merging this pull request may close these issues.

7 participants