-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Support SparseObservable
to SparsePauliOp
conversions
#13758
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
Conversation
and SparseObservable.to_sparse_list, which can also be used for the PauliEvolutionGate compat
One or more of the following people are relevant to this code:
|
Pull Request Test Coverage Report for Build 13157527333Warning: 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
💛 - Coveralls |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for doing this - one main question about the exposed interfaces, and a couple of minor bits on testing.
test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py
Outdated
Show resolved
Hide resolved
test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py
Outdated
Show resolved
Hide resolved
- to_paulis -> SparseObservable - test more edge cases - don't guarantee any ordering
…erra into observable-interconnect
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for doing this - it'll be a big help for tests, and it's good to add the conversion, even if it's expensive in some cases.
We need to add the new methods into sensible places into the class-level documentation - they're not linked at the moment.
pub fn to_paulis(&self) -> Result<Self, CoherenceError> { | ||
let mut paulis: Vec<BitTerm> = Vec::new(); // maybe get capacity here | ||
let mut indices: Vec<u32> = Vec::new(); | ||
let mut coeffs: Vec<Complex64> = Vec::new(); | ||
let mut boundaries: Vec<usize> = vec![0]; | ||
|
||
for view in self.iter() { | ||
let num_projectors = view | ||
.bit_terms | ||
.iter() | ||
.filter(|&bit| bit.is_projector()) | ||
.count(); | ||
let div = 2_f64.powi(num_projectors as i32); | ||
|
||
let combinations = view | ||
.bit_terms | ||
.iter() | ||
.map(bit_term_as_pauli) | ||
.multi_cartesian_product(); | ||
|
||
for combination in combinations { | ||
let mut positive = true; | ||
|
||
for (index, (sign, bit)) in combination.iter().enumerate() { | ||
positive &= sign; | ||
if let Some(bit) = bit { | ||
paulis.push(*bit); | ||
indices.push(view.indices[index]); | ||
} | ||
} | ||
boundaries.push(paulis.len()); | ||
|
||
let coeff = if positive { view.coeff } else { -view.coeff }; | ||
coeffs.push(coeff / div) | ||
} | ||
} | ||
|
||
SparseObservable::new(self.num_qubits, coeffs, paulis, indices, boundaries) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function should never return a CoherenceError
- the input SparseObservable
is guaranteed valid, and all valid SparseObservable
s have a valid representation in the pure-Pauli-operator basis. We can construct the output SparseObservable
directly using the record-struct constructor (SparseObservable {num_qubits: self.num_qubits, ..}
) or using SparseObservable::new_unchecked
with a mild preference on my side for the record-struct constructor.
fn bit_term_as_pauli(bit: &BitTerm) -> Vec<(bool, Option<BitTerm>)> { | ||
match bit { | ||
BitTerm::X => vec![(true, Some(BitTerm::X))], | ||
BitTerm::Y => vec![(true, Some(BitTerm::Y))], | ||
BitTerm::Z => vec![(true, Some(BitTerm::Z))], | ||
BitTerm::Plus => vec![(true, None), (true, Some(BitTerm::X))], | ||
BitTerm::Minus => vec![(true, None), (false, Some(BitTerm::X))], | ||
BitTerm::Left => vec![(true, None), (true, Some(BitTerm::Y))], | ||
BitTerm::Right => vec![(true, None), (false, Some(BitTerm::Y))], | ||
BitTerm::Zero => vec![(true, None), (true, Some(BitTerm::Z))], | ||
BitTerm::One => vec![(true, None), (false, Some(BitTerm::Z))], | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather unimportant, but these could all return &'static [(bool, Option<BitTerm>)]
, rather than eagerly allocating a Vec
.
/// | ||
/// This can be seen as counter-operation of :meth:`.SparseObservable.from_sparse_list`, however | ||
/// the order of terms is not guaranteed to be the same at after a roundtrip to a sparse | ||
/// list and back. Use :meth:`SparseObservable.simplify` to obtain a canonical representation. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this note about SparseObservable.simplify
actually belongs in the to_paulis
method above - the recipe for a completely canonical representation of a SparseObservable
is obs.to_paulis().simplify()
, nothing to do with to_sparse_list
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, I'll add it to the as_paulis
method. For to_sparse_list
I also thought we didn't want to guarantee an order, so I left the comment that doing the roundtrip SparseObservable -> sparse list -> SparseObservable
might not maintain the order of terms.
// turn a 3-tuple of (bit terms, indices, coeff) into a Python tuple | ||
let to_py_tuple = |bits: &[BitTerm], indices: &[u32], coeff: Complex64| { | ||
let mut pauli_string = String::new(); | ||
for bit in bits { | ||
pauli_string.push_str(bit.py_label()); | ||
} | ||
let py_string = PyString::new(py, &pauli_string).unbind(); | ||
let py_indices = PyList::new(py, indices)?.unbind(); | ||
let py_coeff = coeff.into_py_any(py)?; | ||
|
||
PyTuple::new(py, vec![py_string.as_any(), py_indices.as_any(), &py_coeff]) | ||
}; | ||
|
||
let sparse_list = inner | ||
.iter() | ||
.map(|view| to_py_tuple(view.bit_terms, view.indices, view.coeff)) | ||
.collect::<PyResult<Vec<_>>>()?; | ||
|
||
let out = PyList::new(py, sparse_list)?; | ||
Ok(out.unbind()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two points here, both pretty minor:
- why deconstruct the
SparseView
beforeto_py_tuple
and not just accept that in the closure? - it'd probably be a bit more efficient to construct a
let out = PyList::empty(py)?;
and dofor term in inner.iter(): out.append(to_py_tuple(term)?)?;
than to collect into aVec
and then convert that into a list
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- that's legacy from when
to_paulis
returned the bits/indices/coeffs separately - good idea, I was looking for a way to avoid this!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in 984a75c
@staticmethod | ||
def from_sparse_observable(obs: SparseObservable) -> SparsePauliOp: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you want to add a variant to the __init__
method that handles this case too? quantum_info
types (for better or worse) check everything under the sun in their default constructors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we said we don't want to add this because it would give a very easy path to a potentially very expensive representation -- so it's after a "you-need-an-explicit-constructor"-wall 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok sure, that's valid too. I forgot we'd said that haha
let mut pauli_string = String::new(); | ||
for bit in bits { | ||
pauli_string.push_str(bit.py_label()); | ||
} | ||
let py_string = PyString::new(py, &pauli_string).unbind(); | ||
let py_indices = PyList::new(py, indices)?.unbind(); | ||
let py_coeff = coeff.into_py_any(py)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think about reversing the bit terms and the indices on output, so the Pauli string comes out in "reading" order?
Minor, but the necessary capacity of pauli_string
is known immediately; all the py_label
return values are (necessarily) a single ASCII letter, so pauli_string
needs a capacity of bits.len()
, and we could use String::with_capacity
on it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good, that'd be a bit more intuitive 🙂 Also that'll verify that the tests are indeed order-independent 😉
Good call on the with_capacity
👍🏻
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in 984a75c
/// >>> obs = SparseObservable("+") | ||
/// >>> obs.to_paulis() | ||
/// <SparseObservable with 2 terms on 1 qubit: (0.5+0j)() + (0.5+0j)(X_0)> | ||
fn to_paulis(&self) -> PyResult<Self> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very minor: the name to_paulis
in the context of Qiskit makes me think it might come out related to the Pauli
class. Perhaps as_paulis
makes it clearer from reading that it remains a SparseObservable
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in 984a75c
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for this. I tweaked the docs slightly to add a couple more examples, and modify the cross-links a little bit, but other than that it looks good to go to me.
Summary
Add
SparseObservable.to_sparse_list
to generate a sparse Pauli list representation, which is used to implementSparsePauliOp.from_sparse_observable
. This method will also allow usingSparseObservable
s in aPauliEvolutionGate
(follow up PR).Details and comments
In as many places as I could, I added a warning that this conversion will blow up if one attempts to convert a large number of projectors to a Pauli representation.