diff --git a/crates/accelerate/src/sparse_observable.rs b/crates/accelerate/src/sparse_observable.rs index 4d0faf4c2195..3e477a5a6436 100644 --- a/crates/accelerate/src/sparse_observable.rs +++ b/crates/accelerate/src/sparse_observable.rs @@ -2396,24 +2396,50 @@ impl PySparseObservable { Ok(inner.into()) } - #[pyo3(signature = ())] - fn to_sparse_list(&self, py: Python) -> PyResult> { + /// Express the observable in terms of a sparse list format. + /// + /// This is the counter-operation of :meth:`.SparseObservable.from_sparse_list`. + /// + /// Args: + /// only_pauli: If ``True``, express the observable only in terms of non-identity Paulis, + /// :math:`X`, :math:`Y`, and :math:`Z`. Beware that this will use at least :math:`2^n` + /// terms if there are :math:`n` single-qubit projectors present, which can lead + /// to an exponentially expensive representation. Defaults to ``False``. + /// + /// Examples: + /// + /// >>> obs = SparseObservable.from_list([("IIXIZ", 2j), ("IIZIX", 2j)]) + /// >>> reconstructed = SparseObservable.from_sparse_list(obs.to_sparse_list(), obs.num_qubits) + /// >>> assert obs == reconstructed + #[pyo3(signature = (only_paulis=false))] + fn to_sparse_list(&self, py: Python, only_paulis: bool) -> PyResult> { let inner = self.inner.read().map_err(|_| InnerReadError)?; - let sparse_list = inner - .to_paulis() - .map(move |(bits, indices, coeff)| { - 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)?; + // 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]) - }) - .collect::>>()?; + PyTuple::new(py, vec![py_string.as_any(), py_indices.as_any(), &py_coeff]) + }; + + // to map onto a Pauli list, we first have to expand all projectors, otherwise + // we can just directly iterate over the view + let sparse_list = match only_paulis { + false => inner + .iter() + .map(|view| to_py_tuple(view.bit_terms, view.indices, view.coeff)) + .collect::>>()?, + true => inner + .to_paulis() + .map(move |(bits, indices, coeff)| to_py_tuple(&bits, &indices, coeff)) + .collect::>>()?, + }; let out = PyList::new(py, sparse_list)?; Ok(out.unbind()) diff --git a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py index 5d7da4f3bcbf..fca511ea5c6d 100644 --- a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py +++ b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py @@ -967,7 +967,7 @@ def from_sparse_observable( category=RuntimeWarning, ) - as_sparse_list = obs.to_sparse_list() + as_sparse_list = obs.to_sparse_list(only_paulis=True) return SparsePauliOp.from_sparse_list(as_sparse_list, obs.num_qubits) def to_list(self, array: bool = False): diff --git a/test/python/quantum_info/test_sparse_observable.py b/test/python/quantum_info/test_sparse_observable.py index 2af1927ce3fd..ee3225afa707 100644 --- a/test/python/quantum_info/test_sparse_observable.py +++ b/test/python/quantum_info/test_sparse_observable.py @@ -2028,8 +2028,22 @@ def test_to_sparse_list(self): self.assertEqual("ZYX", sparse_list[0][0]) self.assertListEqual([0, 1, 2], sparse_list[0][1]) - obs = SparseObservable("lrI0") + obs = SparseObservable.from_list([("lrI0", 0.5), ("YYIZ", -1j)]) sparse_list = obs.to_sparse_list() + with self.subTest(msg="multiple"): + self.assertEqual(2, len(sparse_list)) + + self.assertEqual("0rl", sparse_list[0][0]) + self.assertEqual([0, 2, 3], sparse_list[0][1]) + self.assertAlmostEqual(0.5, sparse_list[0][2]) + + self.assertEqual("ZYY", sparse_list[1][0]) + self.assertEqual([0, 2, 3], sparse_list[1][1]) + self.assertAlmostEqual(-1j, sparse_list[1][2]) + + def test_to_sparse_pauli_list(self): + obs = SparseObservable("lrI0") + sparse_list = obs.to_sparse_list(only_paulis=True) as_spo = SparsePauliOp.from_sparse_list(sparse_list, 4) expect = SparsePauliOp.from_sparse_list( @@ -2045,6 +2059,20 @@ def test_to_sparse_list(self): ], 4, ) + self.assertEqual(Operator(expect), Operator(as_spo)) + + def test_sparse_list_roundtrip(self): + """Test dumping into a sparse list and constructing from one.""" + obs = SparseObservable.from_list( + [ + ("IIXIZ", 2j), + ("IIZIX", 2j), + ("++III", -1.5), + ("--III", -1.5), + ("IrIlI", 0.5), + ("IIrIl", 0.5), + ] + ) - with self.subTest(msg="lrI0"): - self.assertEqual(Operator(expect), Operator(as_spo)) + reconstructed = SparseObservable.from_sparse_list(obs.to_sparse_list(), obs.num_qubits) + self.assertEqual(obs, reconstructed)