Skip to content
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

Add SparseObservable.compose #13766

Merged
merged 7 commits into from
Mar 3, 2025

Conversation

jakelishman
Copy link
Member

@jakelishman jakelishman commented Jan 30, 2025

Summary

This adds matrix multiplication between SparseObservables (or one SparseObservable and anything that can be coerced to an observable), using the same signature as similar methods in quantum_info.

The current implementation attempts to be relatively memory efficient, though there are specialisations that may be implemented if we wanted to be faster:

  • if all the contained terms are Paulis, so each pairwise multiplication produces either zero or one alphabet letters, we currently have a redundant copy-in and copy-out of the Cartesian-product generator that effectively doubles the overhead. We could check whether each term contains only Paulis once, and then avoid that overhead in those cases.

  • alternatively, the Cartesian-product iterator could be taught to use the buffers of the output SparseObservable directly as its output space, rather than requiring copy-out of its buffers, which would remove the overhead from all calls, not just the single-Pauli ones, but requires a bit more bookkeeping, since the references would keep need to be updated.

  • the case of qargs being not None and front=True currently involves an extra copy step to simplify the logic. If this is an important case, we could add a right-matmul specialisation of compose_map.

Details and comments

This commit currently does not contain tests, because we currently don't have suitable methods to test the validity of the output without coupling to the particular choice of factorisation of the matrix multiplication. We need either a way to convert to a Pauli-only representation (with the exponential explosion that entails) or to a matrix; either of these can be uniquely canonicalised.

WIP until it has tests.

Close #13390

@jakelishman jakelishman added Changelog: New Feature Include in the "Added" section of the changelog mod: quantum info Related to the Quantum Info module (States & Operators) labels Jan 30, 2025
@jakelishman jakelishman added this to the 2.0.0 milestone Jan 30, 2025
@coveralls
Copy link

coveralls commented Jan 30, 2025

Pull Request Test Coverage Report for Build 13631003893

Details

  • 285 of 446 (63.9%) changed or added relevant lines in 2 files are covered.
  • 23 unchanged lines in 2 files lost coverage.
  • Overall coverage decreased (-0.1%) to 87.088%

Changes Missing Coverage Covered Lines Changed/Added Lines %
crates/accelerate/src/sparse_observable/mod.rs 281 304 92.43%
crates/accelerate/src/sparse_observable/lookup.rs 4 142 2.82%
Files with Coverage Reduction New Missed Lines %
crates/qasm2/src/lex.rs 5 91.73%
crates/qasm2/src/parse.rs 18 96.68%
Totals Coverage Status
Change from base Build 13629615284: -0.1%
Covered Lines: 75826
Relevant Lines: 87068

💛 - Coveralls

This adds matrix multiplication between `SparseObservable`s (or one
`SparseObservable` and anything that can be coerced to an observable),
using the same signature as similar methods in `quantum_info`.

The current implementation attempts to be relatively memory efficient,
though there are specialisations that may be implemented if we wanted to
be faster:

- if all the contained terms are Paulis, so each pairwise multiplication
  produces either zero or one alphabet letters, we currently have a
  redundant copy-in and copy-out of the Cartesian-product generator that
  effectively doubles the overhead.  We could check whether each term
  contains only Paulis once, and then avoid that overhead in those
  cases.

- alternatively, the Cartesian-product iterator could be taught to use
  the buffers of the output `SparseObservable` directly as its output
  space, rather than requiring copy-out of its buffers, which would
  remove the overhead from all calls, not just the single-Pauli ones,
  but requires a bit more bookkeeping, since the references would keep
  need to be updated.

- the case of `qargs` being not `None` and `front=True` currently
  involves an extra copy step to simplify the logic.  If this is an
  important case, we could add a right-matmul specialisation of
  `compose_map`.

This commit currently does not contain tests, because we currently don't
have suitable methods to test the validity of the output without
coupling to the particular choice of factorisation of the matrix
multiplication.  We need either a way to convert to a Pauli-only
representation (with the exponential explosion that entails) or to a
matrix; either of these can be uniquely canonicalised.
Before Rust 1.83, `const` functions can't borrow `static`s, even
immutably and without interior mutability.o

There's no need for `matmul` to be `const`; it's always inlined anyway,
and `matmul_generate` can be used in `const` contexts.
@jakelishman jakelishman force-pushed the sparse-observable/compose branch from 4dc04bf to 449ced2 Compare February 14, 2025 23:26
@jakelishman
Copy link
Member Author

jakelishman commented Feb 14, 2025

Ok, I've written all the tests as being against the SparsePauliOp equivalent and subsequent canonicalisation, so we don't have to commit to specific factorisations.

@jakelishman jakelishman marked this pull request as ready for review February 14, 2025 23:27
@jakelishman jakelishman requested a review from a team as a code owner February 14, 2025 23:27
@qiskit-bot
Copy link
Collaborator

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

  • @Qiskit/terra-core

Copy link
Contributor

@Cryoris Cryoris left a comment

Choose a reason for hiding this comment

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

This looks very nice, and given that it also worked in the Jordan-Wigner demo and the tests seems to cover a lot of edge cases I'm very happy with this PR. I just have a few question below, otherwise we can merge 🙂

/// If `self` and `other` have different numbers of qubits.
pub fn compose(&self, other: &SparseObservable) -> SparseObservable {
if other.num_qubits != self.num_qubits {
panic!(
Copy link
Contributor

Choose a reason for hiding this comment

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

Theres the ArithmeticError::MismatchedQubits which we could return here

Copy link
Member Author

Choose a reason for hiding this comment

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

I panicked in the Rust-space methods by analogy to how the other mathematical functions work, and how ndarray works, etc - it's a Rust programmer logic error to pass bad inputs to compose, and it's a simple invariant to uphold.

If you want, we can have try_compose and try_compose_map for Result variants (there's a couple more errors we'd have to add), and compose, compose_map for panicking ones.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, yeah, that also makes sense. This comment mainly comes from the fact that we raise errors in the Python function, but we can add try_ variants also later 👍🏻

Copy link
Contributor

Choose a reason for hiding this comment

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

Is there some norm/guidelines in Rust as to what goes into mod.rs and what goes in separate files? To me its a bit confusing having a lot of logic in mod.rs as I'd expect this to live in some sparse_observable.rs file or something -- but if this is common practice I'm also fine

/// alphabet.
#[pyfunction]
#[pyo3(name = "label")]
fn bit_term_label(py: Python, slf: BitTerm) -> &Bound<PyString> {
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the difference of this to BitTerm.py_label?

Copy link
Member Author

Choose a reason for hiding this comment

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

it's in the comment 1 line before this diff haha:

This doesn't use py_label so we can use intern!.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ahh, this reads as "we would like to use intern! hence we don't use py_label"

Copy link
Contributor

@Cryoris Cryoris left a comment

Choose a reason for hiding this comment

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

LGTM, thank you!

@Cryoris Cryoris enabled auto-merge March 3, 2025 14:59
@Cryoris Cryoris added this pull request to the merge queue Mar 3, 2025
Merged via the queue into Qiskit:main with commit 4bef428 Mar 3, 2025
20 checks passed
@jakelishman jakelishman deleted the sparse-observable/compose branch March 3, 2025 16:58
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: quantum info Related to the Quantum Info module (States & Operators)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implement SparseObservable.compose
4 participants