Skip to content

Add QubitSparsePauli and QubitSparsePauliList classes to quantum_info #14283

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 58 commits into from
May 30, 2025

Conversation

DanPuzzuoli
Copy link
Contributor

@DanPuzzuoli DanPuzzuoli commented May 2, 2025

Summary

Edit: This is the first PR in a series of three introducing QubitSparsePauli, QubitSparsePauliList, and PauliLindbladMap. This PR is primarily about introducing the data structure for QubitSparsePauli and QubitSparsePauliList.

This PR adds QubitSparsePauli and QubitSparsePauliList representing, respectively, a Pauli operator and a list of Pauli operators in qubit-sparse format. These classes are essentially copies of SparseTerm and SparseObservable, with the changes:

  • The alphabet is restricted to the symbols "IXYZ"
  • They do not have coefficients (they do not represent sums or terms in sums, just qubit-sparse Paulis and lists of qubit-sparse Paulis)

The choice to develop these classes was made during the first attempt at implementing PauliLindbladMap; see the closed and incomplete PR #14260 . PauliLindbladMap required storing a qubit-sparse list of paulis, but we realized that we want the user to be able to construct qubit-sparse Paulis and qubit-sparse Pauli lists directly to be used as method arguments (e.g. a pauli_fidelity method) or to be able to return a qubit-sparsely specified list of Paulis (e.g. a sample method which could return a list of qubit-sparse Paulis when drawing multiple samples). As such it made sense to make QubitSparsePauli and QubitSparsePauliList as standalone objects. I've put these in a pauli_lindblad_map folder and rust module in anticipation of also adding a PauliLindbladMap class there.

In a subsequent PR, I will remake PauliLindbladMap, and it will make use of QubitSparsePauliList to internally store the list of operators.

Details and comments

As mentioned these classes are nearly direct copies of SparseTerm and SparseObservable, but with a lot of minor docs changes, changes to names of methods (e.g. SparseObservable.zero was renamed to QubitSparsePauliList.empty), and in some cases removal of functionality that doesn't make sense for a "list" (e.g. SparseObservable.identity was deleted). Some more detailed (but non-exhaustive) notes:

  • SparseObservable renamed QubitSparsePauliList
    • Removed “identity”, zero → empty
  • SparseTerm renamed QubitSparsePauli
    • Modified constructors to be similar in format to QubitSparsePauliList. I.e. previously PyQubitSparsePauli.py_new was like PyQubitSparsePauliList.from_raw_parts. I’ve added from_pauli, from_sparse_label, from_label, changed py_new → from_raw_parts, and made the default constructor delegate to the others (similar to PyQubitSparsePauliList)
    • Note that this class lacks some capabilities of QubitSparsePauliList. E.g. the attributes are not writable, and they return raw arrays (as opposed to views)
  • Removed all non-Pauli characters
    • removed functions like “pauli_bases” that only make sense in the context with non-Pauli characters

At the moment I couldn't get the docs to build locally due to some issues with doxygen.

@DanPuzzuoli DanPuzzuoli requested a review from SamFerracin May 2, 2025 18:44
@DanPuzzuoli DanPuzzuoli requested a review from a team as a code owner May 2, 2025 18:44
@qiskit-bot
Copy link
Collaborator

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

  • @Qiskit/terra-core

@DanPuzzuoli DanPuzzuoli force-pushed the qubit-sparse-pauli branch from 9eb7d07 to b90b0cf Compare May 2, 2025 18:47
@coveralls
Copy link

coveralls commented May 2, 2025

Pull Request Test Coverage Report for Build 15333944839

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

  • 748 of 856 (87.38%) changed or added relevant lines in 10 files are covered.
  • 495 unchanged lines in 9 files lost coverage.
  • Overall coverage increased (+0.004%) to 87.843%

Changes Missing Coverage Covered Lines Changed/Added Lines %
crates/quantum_info/src/pauli_lindblad_map/qubit_sparse_pauli.rs 731 839 87.13%
Files with Coverage Reduction New Missed Lines %
crates/qasm2/src/expr.rs 1 94.23%
crates/transpiler/src/passes/unitary_synthesis.rs 1 94.57%
crates/qasm2/src/lex.rs 3 92.23%
crates/qasm2/src/parse.rs 6 97.15%
qiskit/transpiler/instruction_durations.py 8 85.59%
qiskit/visualization/circuit/latex.py 52 77.45%
crates/cext/src/circuit.rs 56 84.18%
qiskit/visualization/dag_visualization.py 84 10.69%
qiskit/visualization/circuit/matplotlib.py 284 49.02%
Totals Coverage Status
Change from base Build 15312889782: 0.004%
Covered Lines: 80398
Relevant Lines: 91525

💛 - Coveralls

with self.assertRaisesRegex(ValueError, "explicitly given 'num_qubits'"):
QubitSparsePauli(data, num_qubits=data.num_qubits + 1)

with_phase = Pauli("-jIYYXY")
Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, you went for ignoring the phases then. Let's make sure that everyone sees this before we merge, and speak now or forever hold your peace

Copy link
Member

Choose a reason for hiding this comment

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

This feels fine to me if explicitly documented, which it is.

)

def test_from_raw_parts(self):
# Happiest path: exactly typed inputs.
Copy link
Contributor

Choose a reason for hiding this comment

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

What do you mean here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was copied over from the SparseObservable tests. This test case tests various input types that are flagged with comments, and I think this comment is just flagging that the first test block is for when the inputs are supplied in the ideal type.

pub mod qubit_sparse_pauli;

pub fn pauli_lindblad_map(m: &Bound<PyModule>) -> PyResult<()> {
m.add_class::<PyQubitSparsePauliList>()?;
Copy link
Contributor

Choose a reason for hiding this comment

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

Check if qiskit tends to use alphabetical order. If so, switch these two

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In general I believe it does, so I'll switch them.

Copy link
Member

Choose a reason for hiding this comment

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

The order you add stuff here doesn't really matter - if we're usually alphabetical, it's probably just individual people all separately choosing that for aesthetic reasons.

/// `TypeError`-related ones are statically forbidden (within Rust) by the language, and conversion
/// failures on entry to Rust from Python space will automatically raise `TypeError`.
#[derive(Error, Debug)]
pub enum CoherenceError {
Copy link
Contributor

Choose a reason for hiding this comment

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

I assume you use them all?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think the only one not used in this PR is the first one, but it will be used by PauliLindbladMap in the subsequent PR.

/// ================== =========== =============================================================
/// Attribute Length Description
/// ================== =========== =============================================================
/// :attr:`bit_terms` :math:`s` Each of the non-identity single-qubit Pauli operators. These
Copy link
Contributor

Choose a reason for hiding this comment

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

I would prefer something less computer-sciency and more quantum, like "pauli_terms." And if so, I would rename also the rust class. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I would vote to keep it as is for interface consistency with SparseObservable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Update: The C tests are currently failing due to a naming collision between the BitTerm here and the BitTerm in sparse_observable/mod.rs. Even if we can fix this technically and keep the name, this adds a point in favour of changing the name: the naming collision could be confusing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just renamed BitTerm to Pauli based on suggestion by @jakelishman

@DanPuzzuoli DanPuzzuoli requested a review from jakelishman May 2, 2025 20:53
@DanPuzzuoli DanPuzzuoli force-pushed the qubit-sparse-pauli branch from e6ec9d2 to 07d896d Compare May 6, 2025 17:53
@ShellyGarion ShellyGarion added mod: quantum info Related to the Quantum Info module (States & Operators) mod: primitives Related to the Primitives module labels May 7, 2025
Copy link
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.

Overall, this looks like it'd be generally fine to me. I don't love the amount of code duplication from SparseObservable, but I also don't have any reasonable alternative.

I appreciate the effort you went to to strip the concept of "arithmetic" from the object, since it's list-like and not sum-like, both in any remnant method names and the documentation.

One question about the structure: since you're making a "collection" not a "sum": do you think that a set-like interface (with automatic de-duplication) would be most appropriate for you, or should duplicates definitely be allowed? SparseObservable uses the list-like form because it was optimising to allow direct list-like access into the fields even from Python space, in part to allow fast arithmetic operations. I suspect that that might be less of a concern for you, and you'll care more about union/intersection operations?

As a follow-on from that: do you need to expose the backing data structures (boundaries and the flat lists indices and paulis) to Python space? Just asking because if there's no need and you can provide higher-level interfaces only, I'd keep the internals private - that a) simplifies the code a whole bunch, and b) gives us way more flexibility in the future.

I haven't reviewed #14328 in any real detail, but some considerations from having scanned it:

  • I see that you make all the internal fields of QubitSparsePauliList pub. I'd be really cautious about that, because it allows users of the struct to rely on the internals, and to make arbitrary changes to them, which can destroy data coherence really easily. I suspect that many of the uses of that can be wrapped up into safe interfaces (like iterating over terms, etc), and hopefully all the remaining cases can just have extra Rust-space methods as well. If that's not possible, it might indicate an API-boundary problem, so it's worth consideration now.
  • Given how PauliLindbladMap associates a rate with a QubitSparsePauliList, that does feel like a set-like backing might be appropriate?

The Sphinx docs errors look like they were mostly caused by the naming clash with qiskit.quantum_info.Pauli. The solution will be to replace anywhere (other than this file) in Qiskit that writes :class:`.Pauli` with :class:`~.quantum_info.Pauli` which should do it. It's slightly annoying busy-work, but it's something we have to do with other classes already.

@DanPuzzuoli
Copy link
Contributor Author

DanPuzzuoli commented May 14, 2025

One question about the structure: since you're making a "collection" not a "sum": do you think that a set-like interface (with automatic de-duplication) would be most appropriate for you, or should duplicates definitely be allowed? SparseObservable uses the list-like form because it was optimising to allow direct list-like access into the fields even from Python space, in part to allow fast arithmetic operations. I suspect that that might be less of a concern for you, and you'll care more about union/intersection operations?

So we need QubitSparsePauliList to:

  • Maintain ordering in relation to rates for PauliLindbladMap. My understanding is that any set construction wouldn't come with a guaranteed ordering? (See the comment on the rate <-> Pauli association below.)
  • It's also going to be used within the return type of PauliLindbladMap.sample, which will return n samples from the pseudo-probability representation of the Pauli Lindblad map (each "sample" is of type (bool, QubitSparsePauli)). In this case duplication is possible, and again here the operators all need to be associated with another value (a boolean) so it seems like some maintenance of ordering is necessary.

As a follow-on from that: do you need to expose the backing data structures (boundaries and the flat lists indices and paulis) to Python space? Just asking because if there's no need and you can provide higher-level interfaces only, I'd keep the internals private - that a) simplifies the code a whole bunch, and b) gives us way more flexibility in the future.

I think probably yes... otherwise I'm not sure how a user would even know what data the object contains? I guess they could iterate through it and look at the underlying QubitSparsePaulis? Tbh it isn't immediately clear if this is necessary, but it seems odd to not be able to look inside.

I haven't reviewed #14328 in any real detail, but some considerations from having scanned it:

* I see that you make all the internal fields of `QubitSparsePauliList` `pub`.  I'd be _really_ cautious about that, because it allows users of the struct to rely on the internals, and to make arbitrary changes to them, which can destroy data coherence really easily.  I suspect that many of the uses of that can be wrapped up into safe interfaces (like iterating over terms, etc), and hopefully all the remaining cases can just have extra Rust-space methods as well.  If that's not possible, it might indicate an API-boundary problem, so it's worth consideration now.

I can think about this when progressing to that PR - I'll need to remove the pub and see what breaks. Tbh I think I just made these pub to access them easily (as a rust newb) so it probably won't be hard to remove.

* Given how `PauliLindbladMap` associates a rate with a `QubitSparsePauliList`, that does feel like a set-like backing might be appropriate?

A rate is not associated with a QubitSparsePauliList - rates is a list of the same length as QubitSparsePauliList, and rates[i] is associated with qubit_sparse_pauli_list[i] (the ordering matters here). There may be some confusion from my original proposal, which did have a set associated with each rate. We ended up deciding to drop this. In a subsequent PR I will add simplify() (like for SparseObservable) for collecting terms with the same operator.

The Sphinx docs errors look like they were mostly caused by the naming clash with qiskit.quantum_info.Pauli. The solution will be to replace anywhere (other than this file) in Qiskit that writes :class:`.Pauli` with :class:`~.quantum_info.Pauli` which should do it. It's slightly annoying busy-work, but it's something we have to do with other classes already.

Okay I'll fix this, thanks.

@DanPuzzuoli DanPuzzuoli requested a review from a team as a code owner May 14, 2025 21:56
@DanPuzzuoli
Copy link
Contributor Author

In the latest round of commits I've removed the paulis, indices, and boundaries attributes of QubitSparsePauliList, along with the QubitSparsePauliList.from_raw_parts constructor (which no longer makes sense as the user won't know what the "raw parts" are), as well as the Pauli attribute from QubitSparsePauliList. To coincide with this, I've changed the class documentation to no longer describe the internal array storage methods. As it is no longer needed, I've removed all of the ArrayView machinery.

I've tried the following:

The Sphinx docs errors look like they were mostly caused by the naming clash with qiskit.quantum_info.Pauli. The solution will be to replace anywhere (other than this file) in Qiskit that writes :class:`.Pauli` with :class:`~.quantum_info.Pauli` which should do it. It's slightly annoying busy-work, but it's something we have to do with other classes already.

but am still locally getting some docs build errors that are unclear, e.g.:

/Users/danielpuzzuoli/Documents/software_projects/qiskit/.tox/docs/lib/python3.13/site-packages/qiskit/result/sampled_expval.py:
docstring of qiskit.result.sampled_expval.sampled_expectation_value:1: 
WARNING: more than one target found for cross-reference 'Pauli': qiskit.quantum_info.Pauli,
 qiskit.quantum_info.operators.symplectic.pauli.Pauli, 
qiskit.quantum_info.QubitSparsePauli.Pauli

Interestingly this file does no documentation referencing to quantum_info.Pauli, though it does include Pauli in a type hint.

@DanPuzzuoli DanPuzzuoli force-pushed the qubit-sparse-pauli branch from 5d1302c to 1c9585e Compare May 15, 2025 00:29
The principle conflict is caused by quantum-info-related modules moving
into a separate `qiskit_quantum_info` crate, which this merge resolution
does.
Copy link
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.

This was a slightly surreal experience, since so much of this (especially the test suite!) was originally stuff I wrote haha.

I had a couple of minor questions, but nothing major about the interfaces from this point. I fixed the merge conflicts (#14342 moved where the file should be - note it's in crates/quantum_info now), and a minor documentation hold-over from SparseObservable that I spotted.

Happy to merge based on answers to the last couple of questions.

Comment on lines 290 to 299
@ddt.data(
# This is every combination of (0, 1, many) for (terms, qubits, non-identites per term).
QubitSparsePauli.from_label("IIXIZI"),
QubitSparsePauli.from_label("X"),
)
def test_repr(self, data):
# The purpose of this is just to test that the `repr` doesn't crash, rather than asserting
# that it has any particular form.
self.assertIsInstance(repr(data), str)
self.assertIn("QubitSparsePauli", repr(data))
Copy link
Member

Choose a reason for hiding this comment

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

I don't think this comment is accurate (it's missing the empty Pauli), but also both "terms" and "non-identities per term" would only apply to QubitSparsePauliList, not QubitSparsePauli.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nice catch - I dropped the comment, and added an additional test case

QubitSparsePauli.from_label("III")

Comment on lines 831 to 840
@ddt.data(
# This is every combination of (0, 1, many) for (terms, qubits, non-identites per term).
QubitSparsePauliList.empty(0),
QubitSparsePauliList.empty(1),
QubitSparsePauliList.empty(10),
QubitSparsePauliList.from_label("IIXIZI"),
QubitSparsePauliList.from_label("X"),
QubitSparsePauliList.from_list(["YIXZII"]),
QubitSparsePauliList.from_list(["YIXZII", "ZZYYXX"]),
)
Copy link
Member

Choose a reason for hiding this comment

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

Not sure the comment is accurate here either, but then again, I wouldn't 100% swear I was correct in my original either.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Likewise dropped the comment here, and added the test case:

QubitSparsePauliList.from_list(["IIIIII", "ZZYYXX"])

(adding a case with an identity operator).

@jakelishman jakelishman force-pushed the qubit-sparse-pauli branch from cd2e806 to ae371d7 Compare May 29, 2025 17:09
@DanPuzzuoli
Copy link
Contributor Author

Thanks for the thorough review Jake! I've dropped the methods you mentioned, removed the comments, and rerun the formatting.

Copy link
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.

Ace, thanks Dan - this looks like a solid base to me.

We need to remember to include a release note before the actual release, but we can do that in the last PR of the series. (edit: now I see you've got the base of one in the later PRs - nice.)

@jakelishman jakelishman added this pull request to the merge queue May 30, 2025
@jakelishman jakelishman added the Changelog: New Feature Include in the "Added" section of the changelog label May 30, 2025
Merged via the queue into Qiskit:main with commit 09e0de0 May 30, 2025
27 checks passed
rahaman-quantum pushed a commit to rahaman-quantum/qiskit that referenced this pull request Jun 20, 2025
…Qiskit#14283)

* attempting to add initial structure

* adding module creation function

* partial progress with copying

* partial progress 2

* partial progress 3

* copying over

* successful compilation and import

* minor changes

* renaming SparseTerm to QubitSparsePauli

* starting to transfer tests

* moving more tests

* updating repr of QubitSparsePauliList and adding tests

* testing from_pauli

* more test progress

* moved relevant QubitSparsePauliList tests

* adding from_label and from_pauli constructors to QubitSparsePauli

* adding tests from from_pauli and from_label

* added from_sparse_label method

* fixing QubitSparsePauli constructors

* working through tests for QubitSparsePuli

* saving

* done adding tests (I think)

* formatting and linting

* QubitSparsePauli main doc

* done initial pass on QubitSparsePauliList docs

* initial pass on docs for QubitSparsePauli

* last edits

* Update crates/accelerate/src/pauli_lindblad_map/qubit_sparse_pauli.rs

Co-authored-by: Samuele Ferracin <sam.ferracin@ibm.com>

* Update crates/accelerate/src/pauli_lindblad_map/qubit_sparse_pauli.rs

Co-authored-by: Samuele Ferracin <sam.ferracin@ibm.com>

* changing the order of the class additions in mod.rs

* attempting to fix table

* fixing last warning in docs build

* renaming BitTerm -> Pauli

* fixing formatting

* fixing table indentation

* fixing error message

* making error checking test more specific

* cleaning up some docs

* fixing test

* changing all docs references to standard quantum_info to :class:

* updating all pauli references

* removing from_raw_parts and raw array data properties from QubitSparsePauliList

* deleting documentation for QubitSparsePauliList that details the internal array storage

* removing the Pauli attribute from QubitSparsePauliList

* fixing tests

* re-adding Pauli alphabet to QubitSparsePauli

* formatting

* minor docs change

* removing IterMut

* removing unnecessary methods return mutable references

* fixing method name

* linting

* Correct extraneous documentation

* removing Pauli.py_name and changing all calls to it to py_label

* dropping other unused methods

* fixing test comments/cases

* formatting

---------

Co-authored-by: Samuele Ferracin <sam.ferracin@ibm.com>
Co-authored-by: Jake Lishman <jake.lishman@ibm.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Changelog: New Feature Include in the "Added" section of the changelog mod: primitives Related to the Primitives module mod: quantum info Related to the Quantum Info module (States & Operators)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants